diff --git a/.faq/FAQ.md b/.faq/FAQ.md index 6adf000a70..0dd6a22d3b 100644 --- a/.faq/FAQ.md +++ b/.faq/FAQ.md @@ -1,12 +1,17 @@ +--- +hide: + - navigation +--- + # Frequently Asked Questions -{%- for question in questions %} -- [{{ question.title }}](#{{ question.slug }}) -{%- endfor %} +Welcome to the Textual FAQ. +Here we try and answer any question that comes up frequently. +If you can't find what you are looking for here, see our other [help](./help.md) channels. {%- for question in questions %} @@ -15,8 +20,8 @@ {{ question.body }} -{%- endfor %} +--- -
+{%- endfor %} Generated by [FAQtory](https://github.com/willmcgugan/faqtory) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 3693892fc0..e056fa1142 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] defaults: run: shell: bash @@ -33,6 +33,7 @@ jobs: with: python-version: ${{ matrix.python-version }} architecture: x64 + allow-prereleases: true - name: Load cached venv id: cached-poetry-dependencies uses: actions/cache@v3 @@ -40,7 +41,7 @@ jobs: path: .venv key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} - name: Install dependencies - run: poetry install + run: poetry install --extras syntax if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' # - name: Typecheck with mypy # run: | @@ -54,4 +55,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: snapshot-report-textual - path: tests/snapshot_tests/output/snapshot_report.html + path: snapshot_report.html diff --git a/CHANGELOG.md b/CHANGELOG.md index b0875aa41f..aaed90401d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,189 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). + ## Unreleased +### Fixed + +- Fixed issue with `LRUCache.discard` https://github.com/Textualize/textual/issues/3537 +- Fixed `DataTable` not scrolling to rows that were just added https://github.com/Textualize/textual/pull/3552 +- Fixed cache bug with `DataTable.update_cell` https://github.com/Textualize/textual/pull/3551 + +### Added + +- Added `key` argument to the `DataTable.sort()` method, allowing the table to be sorted using a custom function (or other callable) https://github.com/Textualize/textual/issues/2261 + +### Changed + +- Buttons will now display multiple lines, and have auto height https://github.com/Textualize/textual/pull/3539 + + +## [0.40.0] - 2023-10-11 + +- Added `loading` reactive property to widgets https://github.com/Textualize/textual/pull/3509 + +## [0.39.0] - 2023-10-10 + +### Fixed + +- `Pilot.click`/`Pilot.hover` can't use `Screen` as a selector https://github.com/Textualize/textual/issues/3395 +- App exception when a `Tree` is initialized/mounted with `disabled=True` https://github.com/Textualize/textual/issues/3407 +- Fixed `print` locations not being correctly reported in `textual console` https://github.com/Textualize/textual/issues/3237 +- Fix location of IME and emoji popups https://github.com/Textualize/textual/pull/3408 +- Fixed application freeze when pasting an emoji into an application on Windows https://github.com/Textualize/textual/issues/3178 +- Fixed duplicate option ID handling in the `OptionList` https://github.com/Textualize/textual/issues/3455 +- Fix crash when removing and updating DataTable cell at same time https://github.com/Textualize/textual/pull/3487 +- Fixed fractional styles to allow integer values https://github.com/Textualize/textual/issues/3414 +- Stop eating stdout/stderr in headless mode - print works again in tests https://github.com/Textualize/textual/pull/3486 + +### Added + +- `OutOfBounds` exception to be raised by `Pilot` https://github.com/Textualize/textual/pull/3360 +- `TextArea.cursor_screen_offset` property for getting the screen-relative position of the cursor https://github.com/Textualize/textual/pull/3408 +- `Input.cursor_screen_offset` property for getting the screen-relative position of the cursor https://github.com/Textualize/textual/pull/3408 +- Reactive `cell_padding` (and respective parameter) to define horizontal cell padding in data table columns https://github.com/Textualize/textual/issues/3435 +- Added `Input.clear` method https://github.com/Textualize/textual/pull/3430 +- Added `TextArea.SelectionChanged` and `TextArea.Changed` messages https://github.com/Textualize/textual/pull/3442 +- Added `wait_for_dismiss` parameter to `App.push_screen` https://github.com/Textualize/textual/pull/3477 +- Allow scrollbar-size to be set to 0 to achieve scrollable containers with no visible scrollbars https://github.com/Textualize/textual/pull/3488 + +### Changed + +- Breaking change: tree-sitter and tree-sitter-languages dependencies moved to `syntax` extra https://github.com/Textualize/textual/pull/3398 +- `Pilot.click`/`Pilot.hover` now raises `OutOfBounds` when clicking outside visible screen https://github.com/Textualize/textual/pull/3360 +- `Pilot.click`/`Pilot.hover` now return a Boolean indicating whether the click/hover landed on the widget that matches the selector https://github.com/Textualize/textual/pull/3360 +- Added a delay to when the `No Matches` message appears in the command palette, thus removing a flicker https://github.com/Textualize/textual/pull/3399 + +## [0.38.1] - 2023-09-21 + +### Fixed + +- Hotfix - added missing highlight files in build distribution https://github.com/Textualize/textual/pull/3370 + +## [0.38.0] - 2023-09-21 + +### Added + +- Added a TextArea https://github.com/Textualize/textual/pull/2931 +- Added :dark and :light pseudo classes + +### Fixed + +- Fixed `DataTable` not updating component styles on hot-reloading https://github.com/Textualize/textual/issues/3312 + +### Changed + +- Breaking change: CSS in DEFAULT_CSS is now automatically scoped to the widget (set SCOPED_CSS=False) to disable + +## [0.37.1] - 2023-09-16 + +### Fixed + +- Fixed the command palette crashing with a `TimeoutError` in any Python before 3.11 https://github.com/Textualize/textual/issues/3320 +- Fixed `Input` event leakage from `CommandPalette` to `App`. + +### Changed + +- Breaking change: Changed `Markdown.goto_anchor` to return a boolean (if the anchor was found) instead of `None` https://github.com/Textualize/textual/pull/3334 + +## [0.37.0] - 2023-09-15 + +### Added + +- Added the command palette https://github.com/Textualize/textual/pull/3058 +- `Input` is now validated when focus moves out of it https://github.com/Textualize/textual/pull/3193 +- Attribute `Input.validate_on` (and `__init__` parameter of the same name) to customise when validation occurs https://github.com/Textualize/textual/pull/3193 +- Screen-specific (sub-)title attributes https://github.com/Textualize/textual/pull/3199: + - `Screen.TITLE` + - `Screen.SUB_TITLE` + - `Screen.title` + - `Screen.sub_title` +- Properties `Header.screen_title` and `Header.screen_sub_title` https://github.com/Textualize/textual/pull/3199 +- Added `DirectoryTree.DirectorySelected` message https://github.com/Textualize/textual/issues/3200 +- Added `widgets.Collapsible` contributed by Sunyoung Yoo https://github.com/Textualize/textual/pull/2989 + +### Fixed + +- Fixed a crash when removing an option from an `OptionList` while the mouse is hovering over the last option https://github.com/Textualize/textual/issues/3270 +- Fixed a crash in `MarkdownViewer` when clicking on a link that contains an anchor https://github.com/Textualize/textual/issues/3094 +- Fixed wrong message pump in pop_screen https://github.com/Textualize/textual/pull/3315 + +### Changed + +- Widget.notify and App.notify are now thread-safe https://github.com/Textualize/textual/pull/3275 +- Breaking change: Widget.notify and App.notify now return None https://github.com/Textualize/textual/pull/3275 +- App.unnotify is now private (renamed to App._unnotify) https://github.com/Textualize/textual/pull/3275 +- `Markdown.load` will now attempt to scroll to a related heading if an anchor is provided https://github.com/Textualize/textual/pull/3244 +- `ProgressBar` explicitly supports being set back to its indeterminate state https://github.com/Textualize/textual/pull/3286 + +## [0.36.0] - 2023-09-05 + +### Added + +- TCSS styles `layer` and `layers` can be strings https://github.com/Textualize/textual/pull/3169 +- `App.return_code` for the app return code https://github.com/Textualize/textual/pull/3202 +- Added `animate` switch to `Tree.scroll_to_line` and `Tree.scroll_to_node` https://github.com/Textualize/textual/pull/3210 +- Added `Rule` widget https://github.com/Textualize/textual/pull/3209 +- Added App.current_mode to get the current mode https://github.com/Textualize/textual/pull/3233 + +### Changed + +- Reactive callbacks are now scheduled on the message pump of the reactable that is watching instead of the owner of reactive attribute https://github.com/Textualize/textual/pull/3065 +- Callbacks scheduled with `call_next` will now have the same prevented messages as when the callback was scheduled https://github.com/Textualize/textual/pull/3065 +- Added `cursor_type` to the `DataTable` constructor. +- Fixed `push_screen` not updating Screen.CSS styles https://github.com/Textualize/textual/issues/3217 +- `DataTable.add_row` accepts `height=None` to automatically compute optimal height for a row https://github.com/Textualize/textual/pull/3213 + +### Fixed + +- Fixed flicker when calling pop_screen multiple times https://github.com/Textualize/textual/issues/3126 +- Fixed setting styles.layout not updating https://github.com/Textualize/textual/issues/3047 +- Fixed flicker when scrolling tree up or down a line https://github.com/Textualize/textual/issues/3206 + +## [0.35.1] + +### Fixed + +- Fixed flash of 80x24 interface in textual-web + +## [0.35.0] + +### Added + +- Ability to enable/disable tabs via the reactive `disabled` in tab panes https://github.com/Textualize/textual/pull/3152 +- Textual-web driver support for Windows + +### Fixed + +- Could not hide/show/disable/enable tabs in nested `TabbedContent` https://github.com/Textualize/textual/pull/3150 + +## [0.34.0] - 2023-08-22 + +### Added + +- Methods `TabbedContent.disable_tab` and `TabbedContent.enable_tab` https://github.com/Textualize/textual/pull/3112 +- Methods `Tabs.disable` and `Tabs.enable` https://github.com/Textualize/textual/pull/3112 +- Messages `Tab.Disabled`, `Tab.Enabled`, `Tabs.TabDisabled` and `Tabs.Enabled` https://github.com/Textualize/textual/pull/3112 +- Methods `TabbedContent.hide_tab` and `TabbedContent.show_tab` https://github.com/Textualize/textual/pull/3112 +- Methods `Tabs.hide` and `Tabs.show` https://github.com/Textualize/textual/pull/3112 +- Messages `Tabs.TabHidden` and `Tabs.TabShown` https://github.com/Textualize/textual/pull/3112 +- Added `ListView.extend` method to append multiple items https://github.com/Textualize/textual/pull/3012 + +### Changed + +- grid-columns and grid-rows now accept an `auto` token to detect the optimal size https://github.com/Textualize/textual/pull/3107 +- LoadingIndicator now has a minimum height of 1 line. + +### Fixed + +- Fixed auto height container with default grid-rows https://github.com/Textualize/textual/issues/1597 +- Fixed `page_up` and `page_down` bug in `DataTable` when `show_header = False` https://github.com/Textualize/textual/pull/3093 +- Fixed issue with visible children inside invisible container when moving focus https://github.com/Textualize/textual/issues/3053 + +## [0.33.0] - 2023-08-15 + + ### Fixed - Fixed unintuitive sizing behaviour of TabbedContent https://github.com/Textualize/textual/issues/2411 @@ -14,11 +195,26 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed background refresh https://github.com/Textualize/textual/issues/3055 - Fixed `SelectionList.clear_options` https://github.com/Textualize/textual/pull/3075 - `MouseMove` events bubble up from widgets. `App` and `Screen` receive `MouseMove` events even if there's no Widget under the cursor. https://github.com/Textualize/textual/issues/2905 +- Fixed click on double-width char https://github.com/Textualize/textual/issues/2968 + +### Changed + +- Breaking change: `DOMNode.visible` now takes into account full DOM to report whether a node is visible or not. + +### Removed + +- Property `Widget.focusable_children` https://github.com/Textualize/textual/pull/3070 ### Added + - Added an interface for replacing prompt of an individual option in an `OptionList` https://github.com/Textualize/textual/issues/2603 - Added `DirectoryTree.reload_node` method https://github.com/Textualize/textual/issues/2757 -- Added `key` argument to the `DataTable.sort()` method, allowing the table to be sorted using a custom function (or other callable) https://github.com/Textualize/textual/issues/2261 +- Added widgets.Digit https://github.com/Textualize/textual/pull/3073 +- Added `BORDER_TITLE` and `BORDER_SUBTITLE` classvars to Widget https://github.com/Textualize/textual/pull/3097 + +### Changed + +- DescendantBlur and DescendantFocus can now be used with @on decorator ## [0.32.0] - 2023-08-03 @@ -1167,6 +1363,18 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040 - New handler system for messages that doesn't require inheritance - Improved traceback handling +[0.40.0]: https://github.com/Textualize/textual/compare/v0.39.0...v0.40.0 +[0.39.0]: https://github.com/Textualize/textual/compare/v0.38.1...v0.39.0 +[0.38.1]: https://github.com/Textualize/textual/compare/v0.38.0...v0.38.1 +[0.38.0]: https://github.com/Textualize/textual/compare/v0.37.1...v0.38.0 +[0.37.1]: https://github.com/Textualize/textual/compare/v0.37.0...v0.37.1 +[0.37.0]: https://github.com/Textualize/textual/compare/v0.36.0...v0.37.0 +[0.36.0]: https://github.com/Textualize/textual/compare/v0.35.1...v0.36.0 +[0.35.1]: https://github.com/Textualize/textual/compare/v0.35.0...v0.35.1 +[0.35.0]: https://github.com/Textualize/textual/compare/v0.34.0...v0.35.0 +[0.34.0]: https://github.com/Textualize/textual/compare/v0.33.0...v0.34.0 +[0.33.0]: https://github.com/Textualize/textual/compare/v0.32.0...v0.33.0 +[0.32.0]: https://github.com/Textualize/textual/compare/v0.31.0...v0.32.0 [0.31.0]: https://github.com/Textualize/textual/compare/v0.30.0...v0.31.0 [0.30.0]: https://github.com/Textualize/textual/compare/v0.29.0...v0.30.0 [0.29.0]: https://github.com/Textualize/textual/compare/v0.28.1...v0.29.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..b7d3488111 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,103 @@ +# Contributing to Textual + +First of all, thanks for taking the time to contribute to Textual! + +## How can I contribute? + +You can contribute to Textual in many ways: + + 1. [Report a bug](https://github.com/textualize/textual/issues/new?title=%5BBUG%5D%20short%20bug%20description&template=bug_report.md) + 2. Add a new feature + 3. Fix a bug + 4. Improve the documentation + + +## Setup + +To make a code or documentation contribution you will need to set up Textual locally. +You can follow these steps: + + 1. Make sure you have Poetry installed ([see instructions here](https://python-poetry.org)) + 2. Clone the Textual repository + 3. Run `poetry shell` to create a virtual environment for the dependencies + 4. Run `poetry install` to install all dependencies + 5. Make sure the latest version of Textual was installed by running the command `textual --version` + 6. Install the pre-commit hooks with the command `pre-commit install` + +## Demo + +Once you have Textual installed, run the Textual demo to get an impression of what Textual can do and to double check that everything was installed correctly: + +```bash +python -m textual +``` + +## Guidelines + +- Read any issue instructions carefully. Feel free to ask for clarification if any details are missing. + +- Add docstrings to all of your code (functions, methods, classes, ...). The codebase should have enough examples for you to copy from. + +- Write tests for your code. + - If you are fixing a bug, make sure to add regression tests that link to the original issue. + - If you are implementing a visual element, make sure to add _snapshot tests_. [See below](#snapshot-testing) for more details. + +## Before opening a PR + +Before you open your PR, please go through this checklist and make sure you've checked all the items that apply: + + - [ ] Update the `CHANGELOG.md` + - [ ] Format your code with black (`make format`) + - [ ] All your code has docstrings in the style of the rest of the codebase + - [ ] Your code passes all tests (`make test`) + +([Read this](#makefile-commands) if the command `make` doesn't work for you.) + +## Updating and building the documentation + +If you change the documentation, you will want to build the documentation to make sure everything looks like it should. +The command `make docs-serve-offline` should start a server that will let you preview the documentation locally and that should reload whenever you save changes to the documentation or the code files. + +([Read this](#makefile-commands) if the command `make` doesn't work for you.) + +## After opening a PR + +When you open a PR, your code will be reviewed by one of the Textual maintainers. +In that review process, + +- We will take a look at all of the changes you are making +- We might ask for clarifications (why did you do X or Y?) +- We might ask for more tests/more documentation +- We might ask for some code changes + +The sole purpose of those interactions is to make sure that, in the long run, everyone has the best experience possible with Textual and with the feature you are implementing/fixing. + +Don't be discouraged if a reviewer asks for code changes. +If you go through our history of pull requests, you will see that every single one of the maintainers has had to make changes following a review. + +## Snapshot testing + +Snapshot tests ensure that visual things (like widgets) look like they are supposed to. +PR [#1969](https://github.com/Textualize/textual/pull/1969) is a good example of what adding snapshot tests looks like: it amounts to a change in the file `tests/snapshot_tests/test_snapshots.py` that should run an app that you write and compare it against a historic snapshot of what that app should look like. + +When you create a new snapshot test, run it with `pytest -vv tests/snapshot_tests/test_snapshots.py`. +Because you just created this snapshot test, there is no history to compare against and the test will fail. +After running the snapshot tests, you should see a link that opens an interface in your browser. +This interface should show all failing snapshot tests and a side-by-side diff between what the app looked like when the test ran versus the historic snapshot. + +Make sure your snapshot app looks like it is supposed to and that you didn't break any other snapshot tests. +If everything looks fine, you can run `make test-snapshot-update` to update the snapshot history with your new snapshot. +This will write to the file `tests/snapshot_tests/__snapshots__/test_snapshots.ambr`, which you should NOT modify by hand. + +([Read this](#makefile-commands) if the command `make` doesn't work for you.) + +## Join the community + +Seems a little overwhelming? +Join our community on [Discord](https://discord.gg/uNRPEGCV) to get help! + +## Makefile commands + +Textual has a `Makefile` file that contains the most common commands used when developing Textual. +([Read about Make and makefiles on Wikipedia.](https://en.wikipedia.org/wiki/Make_(software))) +If you don't have Make and you're on Windows, you may want to [install Make](https://stackoverflow.com/q/32127524/2828287). diff --git a/Makefile b/Makefile index a663100bbc..9fe2b53427 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,10 @@ unit-test: test-snapshot-update: $(run) pytest --cov-report term-missing --cov=textual tests/ -vv --snapshot-update +.PHONY: coverage +coverage: + $(run) coverage html + .PHONY: typecheck typecheck: $(run) mypy src/textual @@ -28,6 +32,10 @@ format-check: clean-screenshot-cache: rm -rf .screenshot_cache +.PHONY: faq +faq: + $(run) faqtory build + .PHONY: docs-offline-nav docs-offline-nav: echo "INHERIT: mkdocs-offline.yml" > mkdocs-nav-offline.yml @@ -67,10 +75,6 @@ docs-deploy: clean-screenshot-cache docs-online-nav $(run) mkdocs gh-deploy --config-file mkdocs-nav-online.yml rm -f mkdocs-nav-online.yml -.PHONY: faq -faq: - $(run) faqtory build - .PHONY: build build: docs-build-offline poetry build @@ -89,3 +93,7 @@ update: .PHONY: install-pre-commit install-pre-commit: $(run) pre-commit install + +.PHONY: demo +demo: + $(run) python -m textual diff --git a/FAQ.md b/docs/FAQ.md similarity index 90% rename from FAQ.md rename to docs/FAQ.md index 9279823c13..4b280e9dda 100644 --- a/FAQ.md +++ b/docs/FAQ.md @@ -1,19 +1,17 @@ +--- +hide: + - navigation +--- + # Frequently Asked Questions -- [Does Textual support images?](#does-textual-support-images) -- [How can I fix ImportError cannot import name ComposeResult from textual.app ?](#how-can-i-fix-importerror-cannot-import-name-composeresult-from-textualapp-) -- [How can I select and copy text in a Textual app?](#how-can-i-select-and-copy-text-in-a-textual-app) -- [How can I set a translucent app background?](#how-can-i-set-a-translucent-app-background) -- [How do I center a widget in a screen?](#how-do-i-center-a-widget-in-a-screen) -- [How do I fix WorkerDeclarationError?](#how-do-i-fix-workerdeclarationerror) -- [How do I pass arguments to an app?](#how-do-i-pass-arguments-to-an-app) -- [No widget called TextLog](#no-widget-called-textlog) -- [Why do some key combinations never make it to my app?](#why-do-some-key-combinations-never-make-it-to-my-app) -- [Why doesn't Textual look good on macOS?](#why-doesn't-textual-look-good-on-macos) -- [Why doesn't Textual support ANSI themes?](#why-doesn't-textual-support-ansi-themes) -- [Why doesn't the `DataTable` scroll programmatically?](#why-doesn't-the-`datatable`-scroll-programmatically) + + +Welcome to the Textual FAQ. +Here we try and answer any question that comes up frequently. +If you can't find what you are looking for here, see our other [help](./help.md) channels. ## Does Textual support images? @@ -22,6 +20,8 @@ Textual doesn't have built-in support for images yet, but it is on the [Roadmap] See also the [rich-pixels](https://github.com/darrenburns/rich-pixels) project for a Rich renderable for images that works with Textual. +--- + ## How can I fix ImportError cannot import name ComposeResult from textual.app ? @@ -33,6 +33,8 @@ The following should do it: pip install textual-dev -U ``` +--- + ## How can I select and copy text in a Textual app? @@ -46,6 +48,8 @@ may expect from the command line. The exact modifier key depends on the terminal Refer to the documentation for your terminal emulator, if it is not listed above. +--- + ## How can I set a translucent app background? @@ -56,9 +60,16 @@ Textual uses 16.7 million colors where available which enables consistent colors For more information on ANSI colors in Textual, see [Why no Ansi Themes?](#why-doesnt-textual-support-ansi-themes). +--- + ## How do I center a widget in a screen? +!!! tip + + See [*How To Center Things*](https://textual.textualize.io/how-to/center-things/) in the + Textual documentation for a more comprensive answer to this question. + To center a widget within a container use [`align`](https://textual.textualize.io/styles/align/). But remember that `align` works on the *children* of a container, it isn't something you use @@ -146,6 +157,8 @@ if __name__ == "__main__": ButtonApp().run() ``` +--- + ## How do I fix WorkerDeclarationError? @@ -169,6 +182,8 @@ async def run_in_background(): This change was made because it was too easy to accidentally create a threaded worker, which may produce unexpected results. +--- + ## How do I pass arguments to an app? @@ -203,6 +218,8 @@ Greetings(to_greet="davep").run() Greetings("Well hello", "there").run() ``` +--- + ## No widget called TextLog @@ -216,6 +233,8 @@ Here's how you should import RichLog: from textual.widgets import RichLog ``` +--- + ## Why do some key combinations never make it to my app? @@ -246,6 +265,8 @@ If you need to test what [key combinations](https://textual.textualize.io/guide/input/#keyboard-input) work in different environments you can try them out with `textual keys`. +--- + ## Why doesn't Textual look good on macOS? @@ -282,6 +303,8 @@ We recommend any of the following terminals: Screenshot 2023-06-19 at 11 00 25 +--- + ## Why doesn't Textual support ANSI themes? @@ -296,6 +319,8 @@ Textual has a design system which guarantees apps will be readable on all platfo There is currently a light and dark version of the design system, but more are planned. It will also be possible for users to customize the source colors on a per-app or per-system basis. This means that in the future you will be able to modify the core colors to blend in with your chosen terminal theme. +--- + ## Why doesn't the `DataTable` scroll programmatically? @@ -305,6 +330,6 @@ If you would like the table itself to scroll, set the height to something other **NOTE:** As of Textual v0.31.0 the `max-height` of a `DataTable` is set to `100%`, this will mean that the above is no longer the default experience. -
+--- Generated by [FAQtory](https://github.com/willmcgugan/faqtory) diff --git a/docs/api/app.md b/docs/api/app.md index 3a797ce06f..3a83ec5f02 100644 --- a/docs/api/app.md +++ b/docs/api/app.md @@ -1 +1,5 @@ ::: textual.app + options: + filters: + - "!^_" + - "^__init__$" diff --git a/docs/api/command.md b/docs/api/command.md new file mode 100644 index 0000000000..865a605910 --- /dev/null +++ b/docs/api/command.md @@ -0,0 +1 @@ +::: textual.command diff --git a/docs/api/fuzzy_matcher.md b/docs/api/fuzzy_matcher.md new file mode 100644 index 0000000000..0269ad2db0 --- /dev/null +++ b/docs/api/fuzzy_matcher.md @@ -0,0 +1 @@ +::: textual.fuzzy diff --git a/docs/api/logger.md b/docs/api/logger.md index bd76afceca..096ca3011c 100644 --- a/docs/api/logger.md +++ b/docs/api/logger.md @@ -1 +1,5 @@ +# Logger + +A [logger class](/guide/devtools/#logging-handler) that logs to the Textual [console](/guide/devtools#console). + ::: textual.Logger diff --git a/docs/api/system_commands_source.md b/docs/api/system_commands_source.md new file mode 100644 index 0000000000..4778761810 --- /dev/null +++ b/docs/api/system_commands_source.md @@ -0,0 +1 @@ +::: textual._system_commands diff --git a/docs/api/widget.md b/docs/api/widget.md index 3888a30d8f..072e9625e0 100644 --- a/docs/api/widget.md +++ b/docs/api/widget.md @@ -1 +1,5 @@ ::: textual.widget + options: + filters: + - "!^_" + - "^__init__$" diff --git a/docs/blog/.authors.yml b/docs/blog/.authors.yml index 5ed343e2f9..6a4ff04bbd 100644 --- a/docs/blog/.authors.yml +++ b/docs/blog/.authors.yml @@ -1,16 +1,17 @@ -willmcgugan: - name: Will McGugan - description: CEO / code-monkey - avatar: https://github.com/willmcgugan.png -darrenburns: - name: Darren Burns - description: Code-monkey - avatar: https://github.com/darrenburns.png -davep: - name: Dave Pearson - description: Code-monkey - avatar: https://github.com/davep.png -rodrigo: - name: Rodrigo Girão Serrão - description: Code-monkey - avatar: https://github.com/rodrigogiraoserrao.png +authors: + willmcgugan: + name: Will McGugan + description: CEO / code-monkey + avatar: https://github.com/willmcgugan.png + darrenburns: + name: Darren Burns + description: Code-monkey + avatar: https://github.com/darrenburns.png + davep: + name: Dave Pearson + description: Code-monkey + avatar: https://github.com/davep.png + rodrigo: + name: Rodrigo Girão Serrão + description: Code-monkey + avatar: https://github.com/rodrigogiraoserrao.png diff --git a/docs/blog/images/text-area-learnings/cursor_position_updating_via_api.png b/docs/blog/images/text-area-learnings/cursor_position_updating_via_api.png new file mode 100644 index 0000000000..c10f78dc84 Binary files /dev/null and b/docs/blog/images/text-area-learnings/cursor_position_updating_via_api.png differ diff --git a/docs/blog/images/text-area-learnings/maintain_offset.gif b/docs/blog/images/text-area-learnings/maintain_offset.gif new file mode 100644 index 0000000000..d39bca5e0d Binary files /dev/null and b/docs/blog/images/text-area-learnings/maintain_offset.gif differ diff --git a/docs/blog/images/text-area-learnings/text-area-api-insert.gif b/docs/blog/images/text-area-learnings/text-area-api-insert.gif new file mode 100644 index 0000000000..529eb01e3d Binary files /dev/null and b/docs/blog/images/text-area-learnings/text-area-api-insert.gif differ diff --git a/docs/blog/images/text-area-learnings/text-area-pyinstrument.png b/docs/blog/images/text-area-learnings/text-area-pyinstrument.png new file mode 100644 index 0000000000..2a8cc3609c Binary files /dev/null and b/docs/blog/images/text-area-learnings/text-area-pyinstrument.png differ diff --git a/docs/blog/images/text-area-learnings/text-area-syntax-error.gif b/docs/blog/images/text-area-learnings/text-area-syntax-error.gif new file mode 100644 index 0000000000..0a74cb649e Binary files /dev/null and b/docs/blog/images/text-area-learnings/text-area-syntax-error.gif differ diff --git a/docs/blog/images/text-area-learnings/text-area-theme-cycle.gif b/docs/blog/images/text-area-learnings/text-area-theme-cycle.gif new file mode 100644 index 0000000000..c73e9dd9eb Binary files /dev/null and b/docs/blog/images/text-area-learnings/text-area-theme-cycle.gif differ diff --git a/docs/blog/images/text-area-learnings/text-area-welcome.gif b/docs/blog/images/text-area-learnings/text-area-welcome.gif new file mode 100644 index 0000000000..baaf821edc Binary files /dev/null and b/docs/blog/images/text-area-learnings/text-area-welcome.gif differ diff --git a/docs/blog/images/textual-plotext/demo1.png b/docs/blog/images/textual-plotext/demo1.png new file mode 100644 index 0000000000..359ace1e92 Binary files /dev/null and b/docs/blog/images/textual-plotext/demo1.png differ diff --git a/docs/blog/images/textual-plotext/demo2.png b/docs/blog/images/textual-plotext/demo2.png new file mode 100644 index 0000000000..50d47c090a Binary files /dev/null and b/docs/blog/images/textual-plotext/demo2.png differ diff --git a/docs/blog/images/textual-plotext/demo3.png b/docs/blog/images/textual-plotext/demo3.png new file mode 100644 index 0000000000..25866ded17 Binary files /dev/null and b/docs/blog/images/textual-plotext/demo3.png differ diff --git a/docs/blog/images/textual-plotext/demo4.png b/docs/blog/images/textual-plotext/demo4.png new file mode 100644 index 0000000000..84050cd849 Binary files /dev/null and b/docs/blog/images/textual-plotext/demo4.png differ diff --git a/docs/blog/images/textual-plotext/scatter.png b/docs/blog/images/textual-plotext/scatter.png new file mode 100644 index 0000000000..fdfba71393 Binary files /dev/null and b/docs/blog/images/textual-plotext/scatter.png differ diff --git a/docs/blog/images/textual-plotext/weather.png b/docs/blog/images/textual-plotext/weather.png new file mode 100644 index 0000000000..9a7063acc4 Binary files /dev/null and b/docs/blog/images/textual-plotext/weather.png differ diff --git a/docs/blog/posts/release0-38-0.md b/docs/blog/posts/release0-38-0.md new file mode 100644 index 0000000000..f08756b13e --- /dev/null +++ b/docs/blog/posts/release0-38-0.md @@ -0,0 +1,107 @@ +--- +draft: false +date: 2023-09-21 +categories: + - Release +title: "Textual 0.38.0 adds a syntax aware TextArea" +authors: + - willmcgugan +--- + +# Textual 0.38.0 adds a syntax aware TextArea + +This is the second big feature release this month after last week's [command palette](./release0.37.0.md). + + + +The [TextArea](../../widgets/text_area.md) has finally landed. +I know a lot of folk have been waiting for this one. +Textual's TextArea is a fully-featured widget for editing code, with syntax highlighting and line numbers. +It is highly configurable, and looks great. + +Darren Burns (the author of this widget) has penned a terrific write-up on the TextArea. +See [Things I learned while building Textual's TextArea](./text-area-learnings.md) for some of the challenges he faced. + + +## Scoped CSS + +Another notable feature added in 0.38.0 is *scoped* CSS. +A common gotcha in building Textual widgets is that you could write CSS that impacted styles outside of that widget. + +Consider the following widget: + +```python +class MyWidget(Widget): + DEFAULT_CSS = """ + MyWidget { + height: auto; + border: magenta; + } + Label { + border: solid green; + } + """ + + def compose(self) -> ComposeResult: + yield Label("foo") + yield Label("bar") +``` + +The author has intended to style the labels in that widget by adding a green border. +This does work for the widget in question, but (prior to 0.38.0) the `Label` rule would style *all* Labels (including any outside of the widget) — which was probably not intended. + +With version 0.38.0, the CSS is scoped so that only the widget's labels will be styled. +This is almost always what you want, which is why it is enabled by default. +If you do want to style something outside of the widget you can set `SCOPED_CSS=False` (as a classvar). + + +## Light and Dark pseudo selectors + +We've also made a slight quality of life improvement to the CSS, by adding `:light` and `:dark` pseudo selectors. +This allows you to change styles depending on whether you have dark mode enabled or not. + +This was possible before, just a little verbose. +Here's how you would do it in 0.37.0: + +```css +App.-dark-mode MyWidget Label { + ... +} +``` + +In 0.38.0 it's a little more concise and readable: + +```css +MyWidget:dark Label { + ... +} +``` + +## Testing guide + +Not strictly part of the release, but we've added a [guide on testing](/guide/testing) Textual apps. + +As you may know, we are on a mission to make TUIs a serious proposition for critical apps, which makes testing essential. +We've extracted and documented our internal testing tools, including our snapshot tests pytest plugin [pytest-textual-snapshot](https://pypi.org/project/pytest-textual-snapshot/). + +This gives devs powerful tools to ensure the quality of their apps. +Let us know your thoughts on that! + +## Release notes + +See the [release](https://github.com/Textualize/textual/releases/tag/v0.38.0) page for the full details on this release. + + +## What's next? + +There's lots of features planned over the next few months. +One feature I am particularly excited by is a widget to generate plots by wrapping the awesome [Plotext](https://pypi.org/project/plotext/) library. +Check out some early work on this feature: + +
+ +
+ +## Join us + +Join our [Discord server](https://discord.gg/Enf6Z3qhVr) if you want to discuss Textual with the Textualize devs, or the community. diff --git a/docs/blog/posts/release0.37.0.md b/docs/blog/posts/release0.37.0.md new file mode 100644 index 0000000000..fd6a55c16c --- /dev/null +++ b/docs/blog/posts/release0.37.0.md @@ -0,0 +1,85 @@ +--- +draft: false +date: 2023-09-15 +categories: + - Release +title: "Textual 0.37.0 adds a command palette" +authors: + - willmcgugan +--- + + +# Textual 0.37.0 adds a command palette + +Textual version 0.37.0 has landed! +The highlight of this release is the new command palette. + + + +A command palette gives users quick access to features in your app. +If you hit ctrl+backslash in a Textual app, it will bring up the command palette where you can start typing commands. +The commands are matched with a *fuzzy* search, so you only need to type two or three characters to get to any command. + +Here's a video of it in action: + +
+ +
+ +Adding your own commands to the command palette is a piece of cake. +Here's the (command) Provider class used in the example above: + +```python +class ColorCommands(Provider): + """A command provider to select colors.""" + + async def search(self, query: str) -> Hits: + """Called for each key.""" + matcher = self.matcher(query) + for color in COLOR_NAME_TO_RGB.keys(): + score = matcher.match(color) + if score > 0: + yield Hit( + score, + matcher.highlight(color), + partial(self.app.post_message, SwitchColor(color)), + ) +``` + +And here is how you add a provider to your app: + +```python +class ColorApp(App): + """Experiment with the command palette.""" + + COMMANDS = App.COMMANDS | {ColorCommands} +``` + +We're excited about this feature because it is a step towards bringing a common user interface to Textual apps. + +!!! quote + + It's a Textual app. I know this. + + — You, maybe. + +The goal is to be able to build apps that may look quite different, but take no time to learn, because once you learn how to use one Textual app, you can use them all. + +See the Guide for details on how to work with the [command palette](../../guide/command_palette.md). + +## What else? + +Also in 0.37.0 we have a new [Collapsible](/widget_gallery/#collapsible) widget, which is a great way of adding content while avoiding a cluttered screen. + +And of course, bug fixes and other updates. See the [release](https://github.com/Textualize/textual/releases/tag/v0.37.0) page for the full details. + +## What's next? + +Coming very soon, is a new TextEditor widget. +This is a super powerful widget to enter arbitrary text, with beautiful syntax highlighting for a number of languages. +We're expecting that to land next week. +Watch this space, or join the [Discord server](https://discord.gg/Enf6Z3qhVr) if you want to be the first to try it out. + +## Join us + +Join our [Discord server](https://discord.gg/Enf6Z3qhVr) if you want to discuss Textual with the Textualize devs, or the community. diff --git a/docs/blog/posts/text-area-learnings.md b/docs/blog/posts/text-area-learnings.md new file mode 100644 index 0000000000..552ee7997e --- /dev/null +++ b/docs/blog/posts/text-area-learnings.md @@ -0,0 +1,211 @@ +--- +draft: false +date: 2023-09-18 +categories: + - DevLog +authors: + - darrenburns +title: "Things I learned while building Textual's TextArea" +--- + +# Things I learned building a text editor for the terminal + +`TextArea` is the latest widget to be added to Textual's [growing collection](https://textual.textualize.io/widget_gallery/). +It provides a multi-line space to edit text, and features optional syntax highlighting for a selection of languages. + +![text-area-welcome.gif](../images/text-area-learnings/text-area-welcome.gif) + +Adding a `TextArea` to your Textual app is as simple as adding this to your `compose` method: + +```python +yield TextArea() +``` + +Enabling syntax highlighting for a language is as simple as: + +```python +yield TextArea(language="python") +``` + +Working on the `TextArea` widget for Textual taught me a lot about Python and my general +approach to software engineering. It gave me an appreciation for the subtle functionality behind +the editors we use on a daily basis — features we may not even notice, despite +some engineer spending hours perfecting it to provide a small boost to our development experience. + +This post is a tour of some of these learnings. + + + +## Vertical cursor movement is more than just `cursor_row++` + +When you move the cursor vertically, you can't simply keep the same column index and clamp it within the line. +Editors should maintain the visual column offset where possible, +meaning they must account for double-width emoji (sigh 😔) and East-Asian characters. + +![maintain_offset.gif](../images/text-area-learnings/maintain_offset.gif){ loading=lazy } + +Notice that although the cursor is on column 11 while on line 1, it lands on column 6 when it +arrives at line 3. +This is because the 6th character of line 3 _visually_ aligns with the 11th character of line 1. + + +## Edits from other sources may move my cursor + +There are two ways to interact with the `TextArea`: + +1. You can type into it. +2. You can make API calls to edit the content in it. + +In the example below, `Hello, world!\n` is repeatedly inserted at the start of the document via the +API. +Notice that this updates the location of my cursor, ensuring that I don't lose my place. + +![text-area-api-insert.gif](../images/text-area-learnings/text-area-api-insert.gif){ loading=lazy } + +This subtle feature should aid those implementing collaborative and multi-cursor editing. + +This turned out to be one of the more complex features of the whole project, and went through several iterations before I was happy with the result. + +Thankfully it resulted in some wonderful Tetris-esque whiteboards along the way! + +
+ ![cursor_position_updating_via_api.png](../images/text-area-learnings/cursor_position_updating_via_api.png){ loading=lazy } +
A TetrisArea white-boarding session.
+
+ +Sometimes stepping away from the screen and scribbling on a whiteboard with your colleagues (thanks [Dave](https://fosstodon.org/@davep)!) is what's needed to finally crack a tough problem. + +Many thanks to [David Brochart](https://mastodon.top/@davidbrochart) for sending me down this rabbit hole! + +## Spending a few minutes running a profiler can be really beneficial + +While building the `TextArea` widget I avoided heavy optimisation work that may have affected +readability or maintainability. + +However, I did run a profiler in an attempt to detect flawed assumptions or mistakes which were +affecting the performance of my code. + +I spent around 30 minutes profiling `TextArea` +using [pyinstrument](https://pyinstrument.readthedocs.io/en/latest/home.html), and the result was a +**~97%** reduction in the time taken to handle a key press. +What an amazing return on investment for such a minimal time commitment! + + +
+ ![text-area-pyinstrument.png](../images/text-area-learnings/text-area-pyinstrument.png){ loading=lazy } +
"pyinstrument -r html" produces this beautiful output.
+
+ +pyinstrument unveiled two issues that were massively impacting performance. + +### 1. Reparsing highlighting queries on each key press + +I was constructing a tree-sitter `Query` object on each key press, incorrectly assuming it was a +low-overhead call. +This query was completely static, so I moved it into the constructor ensuring the object was created +only once. +This reduced key processing time by around 94% - a substantial and very much noticeable improvement. + +This seems obvious in hindsight, but the code in question was written earlier in the project and had +been relegated in my mind to "code that works correctly and will receive less attention from here on +out". +pyinstrument quickly brought this code back to my attention and highlighted it as a glaring +performance bug. + +### 2. NamedTuples are slower than I expected + +In Python, `NamedTuple`s are slow to create relative to `tuple`s, and this cost was adding up inside +an extremely hot loop which was instantiating a large number of them. +pyinstrument revealed that a large portion of the time during syntax highlighting was spent inside `NamedTuple.__new__`. + +Here's a quick benchmark which constructs 10,000 `NamedTuple`s: + +```toml +❯ hyperfine -w 2 'python sandbox/darren/make_namedtuples.py' +Benchmark 1: python sandbox/darren/make_namedtuples.py + Time (mean ± σ): 15.9 ms ± 0.5 ms [User: 12.8 ms, System: 2.5 ms] + Range (min … max): 15.2 ms … 18.4 ms 165 runs +``` + +Here's the same benchmark using `tuple` instead: + +```toml +❯ hyperfine -w 2 'python sandbox/darren/make_tuples.py' +Benchmark 1: python sandbox/darren/make_tuples.py + Time (mean ± σ): 9.3 ms ± 0.5 ms [User: 6.8 ms, System: 2.0 ms] + Range (min … max): 8.7 ms … 12.3 ms 256 runs +``` + +Switching to `tuple` resulted in another noticeable increase in responsiveness. +Key-press handling time dropped by almost 50%! +Unfortunately, this change _does_ impact readability. +However, the scope in which these tuples were used was very small, and so I felt it was a worthy trade-off. + + +## Syntax highlighting is very different from what I expected + +In order to support syntax highlighting, we make use of +the [tree-sitter](https://tree-sitter.github.io/tree-sitter/) library, which maintains a syntax tree +representing the structure of our document. + +To perform highlighting, we follow these steps: + +1. The user edits the document. +2. We inform tree-sitter of the location of this edit. +3. tree-sitter intelligently parses only the subset of the document impacted by the change, updating the tree. +4. We run a query against the tree to retrieve ranges of text we wish to highlight. +5. These ranges are mapped to styles (defined by the chosen "theme"). +6. These styles to the appropriate text ranges when rendering the widget. + +
+ ![text-area-theme-cycle.gif](../images/text-area-learnings/text-area-theme-cycle.gif){ loading=lazy } +
Cycling through a few of the builtin themes.
+
+ +Another benefit that I didn't consider before working on this project is that tree-sitter +parsers can also be used to highlight syntax errors in a document. +This can be useful in some situations - for example, highlighting mismatched HTML closing tags: + +
+ ![text-area-syntax-error.gif](../images/text-area-learnings/text-area-syntax-error.gif){ loading=lazy } +
Highlighting mismatched closing HTML tags in red.
+
+ +Before building this widget, I was oblivious as to how we might approach syntax highlighting. +Without tree-sitter's incremental parsing approach, I'm not sure reasonable performance would have +been feasible. + +## Edits are replacements + +All single-cursor edits can be distilled into a single behaviour: `replace_range`. +This replaces a range of characters with some text. +We can use this one method to easily implement deletion, insertion, and replacement of text. + +- Inserting text is replacing a zero-width range with the text to insert. +- Pressing backspace (delete left) is just replacing the character behind the cursor with an empty + string. +- Selecting text and pressing delete is just replacing the selected text with an empty string. +- Selecting text and pasting is replacing the selected text with some other text. + +This greatly simplified my initial approach, which involved unique implementations for inserting and +deleting. + + +## The line between "text area" and "VSCode in the terminal" + +A project like this has no clear finish line. +There are always new features, optimisations, and refactors waiting to be made. + +So where do we draw the line? + +We want to provide a widget which can act as both a basic multiline text area that +anyone can drop into their app, yet powerful and extensible enough to act as the foundation +for a Textual-powered text editor. + +Yet, the more features we add, the more opinionated the widget becomes, and the less that users +will feel like they can build it into their _own_ thing. +Finding the sweet spot between feature-rich and flexible is no easy task. + +I don't think the answer is clear, and I don't believe it's possible to please everyone. + +Regardless, I'm happy with where we've landed, and I'm really excited to see what people build using `TextArea` in the future! diff --git a/docs/blog/posts/textual-plotext.md b/docs/blog/posts/textual-plotext.md new file mode 100644 index 0000000000..491e0d7621 --- /dev/null +++ b/docs/blog/posts/textual-plotext.md @@ -0,0 +1,118 @@ +--- +draft: false +date: 2023-10-04 +categories: + - DevLog +title: "Announcing textual-plotext" +authors: + - davep +--- + +# Announcing textual-plotext + +It's no surprise that a common question on the [Textual Discord +server](https://discord.gg/Enf6Z3qhVr) is how to go about producing plots in +the terminal. A popular solution that has been suggested is +[Plotext](https://github.com/piccolomo/plotext). While Plotext doesn't +directly support Textual, it is [easy to use with +Rich](https://github.com/piccolomo/plotext/blob/master/readme/environments.md#rich) +and, because of this, we wanted to make it just as easy to use in your +Textual applications. + + + +With this in mind we've created +[`textual-plotext`](https://github.com/Textualize/textual-plotext): a library +that provides a widget for using Plotext plots in your app. In doing this +we've tried our best to make it as similar as possible to using Plotext in a +conventional Python script. + +Take this code from the [Plotext README](https://github.com/piccolomo/plotext#readme): + +```python +import plotext as plt +y = plt.sin() # sinusoidal test signal +plt.scatter(y) +plt.title("Scatter Plot") # to apply a title +plt.show() # to finally plot +``` + +The Textual equivalent of this (including everything needed to make this a +fully-working Textual application) is: + +```python +from textual.app import App, ComposeResult + +from textual_plotext import PlotextPlot + +class ScatterApp(App[None]): + + def compose(self) -> ComposeResult: + yield PlotextPlot() + + def on_mount(self) -> None: + plt = self.query_one(PlotextPlot).plt + y = plt.sin() # sinusoidal test signal + plt.scatter(y) + plt.title("Scatter Plot") # to apply a title + +if __name__ == "__main__": + ScatterApp().run() +``` + +When run the result will look like this: + +![Scatter plot in a Textual application](/blog/images/textual-plotext/scatter.png) + +Aside from a couple of the more far-out plot types[^1] you should find that +everything you can do with Plotext in a conventional script can also be done +in a Textual application. + +Here's a small selection of screenshots from a demo built into the library, +each of the plots taken from the Plotext README: + +![Sample from the library demo application](/blog/images/textual-plotext/demo1.png) + +![Sample from the library demo application](/blog/images/textual-plotext/demo2.png) + +![Sample from the library demo application](/blog/images/textual-plotext/demo3.png) + +![Sample from the library demo application](/blog/images/textual-plotext/demo4.png) + +A key design goal of this widget is that you can develop your plots so that +the resulting code looks very similar to that in the Plotext documentation. +The core difference is that, where you'd normally import the `plotext` +module `as plt` and then call functions via `plt`, you instead use the `plt` +property made available by the widget. + +You don't even need to call the `build` or `show` functions as +`textual-plotext` takes care of this for you. You can see this in action in +the scatter code shown earlier. + +Of course, moving any existing plotting code into your Textual app means you +will need to think about how you get the data and when and where you build +your plot. This might be where the [Textual worker +API](https://textual.textualize.io/guide/workers/) becomes useful. + +We've included a longer-form example application that shows off the glorious +Scottish weather we enjoy here at Textual Towers, with [an application that +uses workers to pull down weather data from a year ago and plot +it](https://github.com/Textualize/textual-plotext/blob/main/examples/textual_towers_weather.py). + +![The Textual Towers weather history app](/blog/images/textual-plotext/weather.png) + +If you are an existing Plotext user who wants to turn your plots into full +terminal applications, we think this will be very familiar and accessible. +If you're a Textual user who wants to add plots to your application, we +think Plotext is a great library for this. + +If you have any questions about this, or anything else to do with Textual, +feel free to come and join us on our [Discord +server](https://discord.gg/Enf6Z3qhVr) or in our [GitHub +discussions](https://github.com/Textualize/textual/discussions). + +[^1]: Right now there's no [animated + gif](https://github.com/piccolomo/plotext/blob/master/readme/image.md#gif-plot) + or + [video](https://github.com/piccolomo/plotext/blob/master/readme/video.md) + support. diff --git a/docs/blog/posts/textual-web.md b/docs/blog/posts/textual-web.md new file mode 100644 index 0000000000..e819bb3309 --- /dev/null +++ b/docs/blog/posts/textual-web.md @@ -0,0 +1,45 @@ +--- +draft: false +date: 2023-09-06 +categories: + - News +title: "What is Textual Web?" +authors: + - willmcgugan +--- + +# What is Textual Web? + +If you know us, you will know that we are the team behind [Rich](https://github.com/Textualize/rich) and [Textual](https://github.com/Textualize/textual) — two popular Python libraries that work magic in the terminal. + +!!! note + + Not to mention [Rich-CLI](https://github.com/Textualize/rich-cli), [Trogon](https://github.com/Textualize/trogon), and [Frogmouth](https://github.com/Textualize/frogmouth) + +Today we are adding one project more to that lineup: [textual-web](https://github.com/Textualize/textual-web). + + + + +Textual Web takes a Textual-powered TUI and turns it in to a web application. +Here's a video of that in action: + +
+ +
+ +With the `textual-web` command you can publish any Textual app on the web, making it available to anyone you send the URL to. +This works without creating a socket server on your machine, so you won't have to configure firewalls and ports to share your applications. + +We're excited about the possibilities here. +Textual web apps are fast to spin up and tear down, and they can run just about anywhere that has an outgoing internet connection. +They can be built by a single developer without any experience with a traditional web stack. +All you need is proficiency in Python and a little time to read our [lovely docs](https://textual.textualize.io/). + +Future releases will expose more of the Web platform APIs to Textual apps, such as notifications and file system access. +We plan to do this in a way that allows the same (Python) code to drive those features. +For instance, a Textual app might save a file to disk in a terminal, but offer to download it in the browser. + +Also in the pipeline is [PWA](https://en.wikipedia.org/wiki/Progressive_web_app) support, so you can build terminal apps, web apps, and desktop apps with a single codebase. + +Textual Web is currently in a public beta. Join our [Discord server](https://discord.gg/Enf6Z3qhVr) if you would like to help us test, or if you have any questions. diff --git a/docs/css_types/text_align.md b/docs/css_types/text_align.md index 95119e676e..0234b952aa 100644 --- a/docs/css_types/text_align.md +++ b/docs/css_types/text_align.md @@ -29,7 +29,7 @@ A [``](./text_align.md) can be any of the following values: ```sass Label { - rule: justify; + text-align: justify; } ``` diff --git a/docs/custom_theme/main.html b/docs/custom_theme/main.html index fbbfd659ab..b87addcdd5 100644 --- a/docs/custom_theme/main.html +++ b/docs/custom_theme/main.html @@ -30,4 +30,23 @@ + + {% endblock %} diff --git a/docs/events/blur.md b/docs/events/blur.md index 067e7bde9d..df317c5f45 100644 --- a/docs/events/blur.md +++ b/docs/events/blur.md @@ -12,3 +12,9 @@ _No other attributes_ ## Code ::: textual.events.Blur + +## See also + +- [DescendantBlur](descendant_blur.md) +- [DescendantFocus](descendant_focus.md) +- [Focus](focus.md) diff --git a/docs/events/descendant_blur.md b/docs/events/descendant_blur.md index bfe0799f68..c2f447b1f4 100644 --- a/docs/events/descendant_blur.md +++ b/docs/events/descendant_blur.md @@ -12,3 +12,9 @@ _No other attributes_ ## Code ::: textual.events.DescendantBlur + +## See also + +- [Blur](blur.md) +- [DescendantFocus](descendant_focus.md) +- [Focus](focus.md) diff --git a/docs/events/descendant_focus.md b/docs/events/descendant_focus.md index 9090cd65d4..9eb3821805 100644 --- a/docs/events/descendant_focus.md +++ b/docs/events/descendant_focus.md @@ -12,3 +12,9 @@ _No other attributes_ ## Code ::: textual.events.DescendantFocus + +## See also + +- [Blur](blur.md) +- [DescendantBlur](descendant_blur.md) +- [Focus](focus.md) diff --git a/docs/events/focus.md b/docs/events/focus.md index 54f4b2a486..e2c710f115 100644 --- a/docs/events/focus.md +++ b/docs/events/focus.md @@ -12,3 +12,9 @@ _No other attributes_ ## Code ::: textual.events.Focus + +## See also + +- [Blur](blur.md) +- [DescendantBlur](descendant_blur.md) +- [DescendantFocus](descendant_focus.md) diff --git a/docs/examples/app/question02.py b/docs/examples/app/question02.py index 65eb79f655..9548a47ec9 100644 --- a/docs/examples/app/question02.py +++ b/docs/examples/app/question02.py @@ -1,9 +1,9 @@ from textual.app import App, ComposeResult -from textual.widgets import Label, Button +from textual.widgets import Button, Label class QuestionApp(App[str]): - CSS_PATH = "question02.css" + CSS_PATH = "question02.tcss" def compose(self) -> ComposeResult: yield Label("Do you love Textual?", id="question") diff --git a/docs/examples/app/question02.css b/docs/examples/app/question02.tcss similarity index 82% rename from docs/examples/app/question02.css rename to docs/examples/app/question02.tcss index 1f1a3b84bd..165591f395 100644 --- a/docs/examples/app/question02.css +++ b/docs/examples/app/question02.tcss @@ -1,8 +1,8 @@ Screen { layout: grid; grid-size: 2; - grid-gutter: 2; - padding: 2; + grid-gutter: 2; + padding: 2; } #question { width: 100%; @@ -10,7 +10,7 @@ Screen { column-span: 2; content-align: center bottom; text-style: bold; -} +} Button { width: 100%; diff --git a/docs/examples/app/question_title01.py b/docs/examples/app/question_title01.py index 55dc43599a..fd52aa3be2 100644 --- a/docs/examples/app/question_title01.py +++ b/docs/examples/app/question_title01.py @@ -3,7 +3,7 @@ class MyApp(App[str]): - CSS_PATH = "question02.css" + CSS_PATH = "question02.tcss" TITLE = "A Question App" SUB_TITLE = "The most important question" diff --git a/docs/examples/app/question_title02.py b/docs/examples/app/question_title02.py index c279d7e200..50f4673c38 100644 --- a/docs/examples/app/question_title02.py +++ b/docs/examples/app/question_title02.py @@ -4,7 +4,7 @@ class MyApp(App[str]): - CSS_PATH = "question02.css" + CSS_PATH = "question02.tcss" TITLE = "A Question App" SUB_TITLE = "The most important question" diff --git a/docs/examples/events/dictionary.py b/docs/examples/events/dictionary.py index 51421ad392..cb987875fc 100644 --- a/docs/examples/events/dictionary.py +++ b/docs/examples/events/dictionary.py @@ -15,7 +15,7 @@ class DictionaryApp(App): """Searches a dictionary API as-you-type.""" - CSS_PATH = "dictionary.css" + CSS_PATH = "dictionary.tcss" def compose(self) -> ComposeResult: yield Input(placeholder="Search for a word") diff --git a/docs/examples/events/dictionary.css b/docs/examples/events/dictionary.tcss similarity index 65% rename from docs/examples/events/dictionary.css rename to docs/examples/events/dictionary.tcss index 9b5e489adb..1413dd9884 100644 --- a/docs/examples/events/dictionary.css +++ b/docs/examples/events/dictionary.tcss @@ -3,21 +3,21 @@ Screen { } Input { - dock: top; + dock: top; width: 100%; height: 1; padding: 0 1; - margin: 1 1 0 1; + margin: 1 1 0 1; } #results { - width: auto; + width: auto; min-height: 100%; } #results-container { background: $background 50%; - overflow: auto; - margin: 1 2; + overflow: auto; + margin: 1 2; height: 100%; } diff --git a/docs/examples/events/on_decorator.css b/docs/examples/events/on_decorator.tcss similarity index 100% rename from docs/examples/events/on_decorator.css rename to docs/examples/events/on_decorator.tcss diff --git a/docs/examples/events/on_decorator01.py b/docs/examples/events/on_decorator01.py index 7b9c0276e2..ac8e2ccd28 100644 --- a/docs/examples/events/on_decorator01.py +++ b/docs/examples/events/on_decorator01.py @@ -4,7 +4,7 @@ class OnDecoratorApp(App): - CSS_PATH = "on_decorator.css" + CSS_PATH = "on_decorator.tcss" def compose(self) -> ComposeResult: """Three buttons.""" diff --git a/docs/examples/events/on_decorator02.py b/docs/examples/events/on_decorator02.py index 87546841ac..1481040952 100644 --- a/docs/examples/events/on_decorator02.py +++ b/docs/examples/events/on_decorator02.py @@ -4,7 +4,7 @@ class OnDecoratorApp(App): - CSS_PATH = "on_decorator.css" + CSS_PATH = "on_decorator.tcss" def compose(self) -> ComposeResult: """Three buttons.""" diff --git a/docs/examples/guide/actions/actions05.py b/docs/examples/guide/actions/actions05.py index 05a7d64066..341dc72153 100644 --- a/docs/examples/guide/actions/actions05.py +++ b/docs/examples/guide/actions/actions05.py @@ -15,7 +15,7 @@ def action_set_background(self, color: str) -> None: class ActionsApp(App): - CSS_PATH = "actions05.css" + CSS_PATH = "actions05.tcss" BINDINGS = [ ("r", "set_background('red')", "Red"), ("g", "set_background('green')", "Green"), diff --git a/docs/examples/guide/actions/actions05.css b/docs/examples/guide/actions/actions05.tcss similarity index 100% rename from docs/examples/guide/actions/actions05.css rename to docs/examples/guide/actions/actions05.tcss diff --git a/docs/examples/guide/command_palette/command01.py b/docs/examples/guide/command_palette/command01.py new file mode 100644 index 0000000000..b4026f3ec6 --- /dev/null +++ b/docs/examples/guide/command_palette/command01.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from functools import partial +from pathlib import Path + +from rich.syntax import Syntax + +from textual.app import App, ComposeResult +from textual.command import Hit, Hits, Provider +from textual.containers import VerticalScroll +from textual.widgets import Static + + +class PythonFileCommands(Provider): + """A command provider to open a Python file in the current working directory.""" + + def read_files(self) -> list[Path]: + """Get a list of Python files in the current working directory.""" + return list(Path("./").glob("*.py")) + + async def startup(self) -> None: # (1)! + """Called once when the command palette is opened, prior to searching.""" + worker = self.app.run_worker(self.read_files, thread=True) + self.python_paths = await worker.wait() + + async def search(self, query: str) -> Hits: # (2)! + """Search for Python files.""" + matcher = self.matcher(query) # (3)! + + app = self.app + assert isinstance(app, ViewerApp) + + for path in self.python_paths: + command = f"open {str(path)}" + score = matcher.match(command) # (4)! + if score > 0: + yield Hit( + score, + matcher.highlight(command), # (5)! + partial(app.open_file, path), + help="Open this file in the viewer", + ) + + +class ViewerApp(App): + """Demonstrate a command source.""" + + COMMANDS = App.COMMANDS | {PythonFileCommands} # (6)! + + def compose(self) -> ComposeResult: + with VerticalScroll(): + yield Static(id="code", expand=True) + + def open_file(self, path: Path) -> None: + """Open and display a file with syntax highlighting.""" + syntax = Syntax.from_path( + str(path), + line_numbers=True, + word_wrap=False, + indent_guides=True, + theme="github-dark", + ) + self.query_one("#code", Static).update(syntax) + + +if __name__ == "__main__": + app = ViewerApp() + app.run() diff --git a/docs/examples/guide/dom4.py b/docs/examples/guide/dom4.py index 3191138d4c..3fc6e19ad7 100644 --- a/docs/examples/guide/dom4.py +++ b/docs/examples/guide/dom4.py @@ -1,12 +1,12 @@ from textual.app import App, ComposeResult from textual.containers import Container, Horizontal -from textual.widgets import Header, Footer, Static, Button +from textual.widgets import Button, Footer, Header, Static QUESTION = "Do you want to learn about Textual CSS?" class ExampleApp(App): - CSS_PATH = "dom4.css" + CSS_PATH = "dom4.tcss" def compose(self) -> ComposeResult: yield Header() diff --git a/docs/examples/guide/dom4.css b/docs/examples/guide/dom4.tcss similarity index 99% rename from docs/examples/guide/dom4.css rename to docs/examples/guide/dom4.tcss index 8ac843abbb..1dd6f3a6ca 100644 --- a/docs/examples/guide/dom4.css +++ b/docs/examples/guide/dom4.tcss @@ -27,5 +27,3 @@ Button { height: auto; dock: bottom; } - - diff --git a/docs/examples/guide/input/binding01.py b/docs/examples/guide/input/binding01.py index 0716fbe103..661a6efce2 100644 --- a/docs/examples/guide/input/binding01.py +++ b/docs/examples/guide/input/binding01.py @@ -8,7 +8,7 @@ class Bar(Static): class BindingApp(App): - CSS_PATH = "binding01.css" + CSS_PATH = "binding01.tcss" BINDINGS = [ ("r", "add_bar('red')", "Add Red"), ("g", "add_bar('green')", "Add Green"), diff --git a/docs/examples/guide/input/binding01.css b/docs/examples/guide/input/binding01.tcss similarity index 83% rename from docs/examples/guide/input/binding01.css rename to docs/examples/guide/input/binding01.tcss index 9c8b6390fd..d76d1e4598 100644 --- a/docs/examples/guide/input/binding01.css +++ b/docs/examples/guide/input/binding01.tcss @@ -1,5 +1,5 @@ Bar { - height: 5; + height: 5; content-align: center middle; text-style: bold; margin: 1 2; diff --git a/docs/examples/guide/input/key03.py b/docs/examples/guide/input/key03.py index c524b658cc..02e692ff44 100644 --- a/docs/examples/guide/input/key03.py +++ b/docs/examples/guide/input/key03.py @@ -11,7 +11,7 @@ def on_key(self, event: events.Key) -> None: class InputApp(App): """App to display key events.""" - CSS_PATH = "key03.css" + CSS_PATH = "key03.tcss" def compose(self) -> ComposeResult: yield KeyLogger() diff --git a/docs/examples/guide/input/key03.css b/docs/examples/guide/input/key03.tcss similarity index 93% rename from docs/examples/guide/input/key03.css rename to docs/examples/guide/input/key03.tcss index 601612492e..c59129fb76 100644 --- a/docs/examples/guide/input/key03.css +++ b/docs/examples/guide/input/key03.tcss @@ -4,7 +4,7 @@ Screen { grid-columns: 1fr; } -KeyLogger { +KeyLogger { border: blank; } diff --git a/docs/examples/guide/input/mouse01.py b/docs/examples/guide/input/mouse01.py index 6860f9ecf1..88ce4a94f9 100644 --- a/docs/examples/guide/input/mouse01.py +++ b/docs/examples/guide/input/mouse01.py @@ -18,7 +18,7 @@ class Ball(Static): class MouseApp(App): - CSS_PATH = "mouse01.css" + CSS_PATH = "mouse01.tcss" def compose(self) -> ComposeResult: yield RichLog() diff --git a/docs/examples/guide/input/mouse01.css b/docs/examples/guide/input/mouse01.tcss similarity index 100% rename from docs/examples/guide/input/mouse01.css rename to docs/examples/guide/input/mouse01.tcss diff --git a/docs/examples/guide/layout/combining_layouts.py b/docs/examples/guide/layout/combining_layouts.py index e608152cd9..791a5ebc6e 100644 --- a/docs/examples/guide/layout/combining_layouts.py +++ b/docs/examples/guide/layout/combining_layouts.py @@ -4,7 +4,7 @@ class CombiningLayoutsExample(App): - CSS_PATH = "combining_layouts.css" + CSS_PATH = "combining_layouts.tcss" def compose(self) -> ComposeResult: yield Header() diff --git a/docs/examples/guide/layout/combining_layouts.css b/docs/examples/guide/layout/combining_layouts.tcss similarity index 100% rename from docs/examples/guide/layout/combining_layouts.css rename to docs/examples/guide/layout/combining_layouts.tcss diff --git a/docs/examples/guide/layout/dock_layout1_sidebar.py b/docs/examples/guide/layout/dock_layout1_sidebar.py index 81eb948056..fb3023dedc 100644 --- a/docs/examples/guide/layout/dock_layout1_sidebar.py +++ b/docs/examples/guide/layout/dock_layout1_sidebar.py @@ -10,7 +10,7 @@ class DockLayoutExample(App): - CSS_PATH = "dock_layout1_sidebar.css" + CSS_PATH = "dock_layout1_sidebar.tcss" def compose(self) -> ComposeResult: yield Static("Sidebar", id="sidebar") diff --git a/docs/examples/guide/layout/dock_layout1_sidebar.css b/docs/examples/guide/layout/dock_layout1_sidebar.tcss similarity index 100% rename from docs/examples/guide/layout/dock_layout1_sidebar.css rename to docs/examples/guide/layout/dock_layout1_sidebar.tcss diff --git a/docs/examples/guide/layout/dock_layout2_sidebar.py b/docs/examples/guide/layout/dock_layout2_sidebar.py index 0da8f78c39..32699f1e07 100644 --- a/docs/examples/guide/layout/dock_layout2_sidebar.py +++ b/docs/examples/guide/layout/dock_layout2_sidebar.py @@ -10,7 +10,7 @@ class DockLayoutExample(App): - CSS_PATH = "dock_layout2_sidebar.css" + CSS_PATH = "dock_layout2_sidebar.tcss" def compose(self) -> ComposeResult: yield Static("Sidebar2", id="another-sidebar") diff --git a/docs/examples/guide/layout/dock_layout2_sidebar.css b/docs/examples/guide/layout/dock_layout2_sidebar.tcss similarity index 100% rename from docs/examples/guide/layout/dock_layout2_sidebar.css rename to docs/examples/guide/layout/dock_layout2_sidebar.tcss diff --git a/docs/examples/guide/layout/dock_layout3_sidebar_header.py b/docs/examples/guide/layout/dock_layout3_sidebar_header.py index 076e57c2af..0250f24751 100644 --- a/docs/examples/guide/layout/dock_layout3_sidebar_header.py +++ b/docs/examples/guide/layout/dock_layout3_sidebar_header.py @@ -10,7 +10,7 @@ class DockLayoutExample(App): - CSS_PATH = "dock_layout3_sidebar_header.css" + CSS_PATH = "dock_layout3_sidebar_header.tcss" def compose(self) -> ComposeResult: yield Header(id="header") diff --git a/docs/examples/guide/layout/dock_layout3_sidebar_header.css b/docs/examples/guide/layout/dock_layout3_sidebar_header.tcss similarity index 100% rename from docs/examples/guide/layout/dock_layout3_sidebar_header.css rename to docs/examples/guide/layout/dock_layout3_sidebar_header.tcss diff --git a/docs/examples/guide/layout/grid_layout1.py b/docs/examples/guide/layout/grid_layout1.py index 943f18cb78..645f0d8956 100644 --- a/docs/examples/guide/layout/grid_layout1.py +++ b/docs/examples/guide/layout/grid_layout1.py @@ -3,7 +3,7 @@ class GridLayoutExample(App): - CSS_PATH = "grid_layout1.css" + CSS_PATH = "grid_layout1.tcss" def compose(self) -> ComposeResult: yield Static("One", classes="box") diff --git a/docs/examples/guide/layout/grid_layout1.css b/docs/examples/guide/layout/grid_layout1.tcss similarity index 100% rename from docs/examples/guide/layout/grid_layout1.css rename to docs/examples/guide/layout/grid_layout1.tcss diff --git a/docs/examples/guide/layout/grid_layout2.py b/docs/examples/guide/layout/grid_layout2.py index 407c081e11..7ad91c840e 100644 --- a/docs/examples/guide/layout/grid_layout2.py +++ b/docs/examples/guide/layout/grid_layout2.py @@ -3,7 +3,7 @@ class GridLayoutExample(App): - CSS_PATH = "grid_layout2.css" + CSS_PATH = "grid_layout2.tcss" def compose(self) -> ComposeResult: yield Static("One", classes="box") diff --git a/docs/examples/guide/layout/grid_layout2.css b/docs/examples/guide/layout/grid_layout2.tcss similarity index 100% rename from docs/examples/guide/layout/grid_layout2.css rename to docs/examples/guide/layout/grid_layout2.tcss diff --git a/docs/examples/guide/layout/grid_layout3_row_col_adjust.py b/docs/examples/guide/layout/grid_layout3_row_col_adjust.py index c75a58da1e..e667b82ae3 100644 --- a/docs/examples/guide/layout/grid_layout3_row_col_adjust.py +++ b/docs/examples/guide/layout/grid_layout3_row_col_adjust.py @@ -3,7 +3,7 @@ class GridLayoutExample(App): - CSS_PATH = "grid_layout3_row_col_adjust.css" + CSS_PATH = "grid_layout3_row_col_adjust.tcss" def compose(self) -> ComposeResult: yield Static("One", classes="box") diff --git a/docs/examples/guide/layout/grid_layout3_row_col_adjust.css b/docs/examples/guide/layout/grid_layout3_row_col_adjust.tcss similarity index 100% rename from docs/examples/guide/layout/grid_layout3_row_col_adjust.css rename to docs/examples/guide/layout/grid_layout3_row_col_adjust.tcss diff --git a/docs/examples/guide/layout/grid_layout4_row_col_adjust.py b/docs/examples/guide/layout/grid_layout4_row_col_adjust.py index f11a2d5b0d..08bee49f2b 100644 --- a/docs/examples/guide/layout/grid_layout4_row_col_adjust.py +++ b/docs/examples/guide/layout/grid_layout4_row_col_adjust.py @@ -3,7 +3,7 @@ class GridLayoutExample(App): - CSS_PATH = "grid_layout4_row_col_adjust.css" + CSS_PATH = "grid_layout4_row_col_adjust.tcss" def compose(self) -> ComposeResult: yield Static("One", classes="box") diff --git a/docs/examples/guide/layout/grid_layout4_row_col_adjust.css b/docs/examples/guide/layout/grid_layout4_row_col_adjust.tcss similarity index 100% rename from docs/examples/guide/layout/grid_layout4_row_col_adjust.css rename to docs/examples/guide/layout/grid_layout4_row_col_adjust.tcss diff --git a/docs/examples/guide/layout/grid_layout5_col_span.py b/docs/examples/guide/layout/grid_layout5_col_span.py index d7fe1cb83a..deeec06931 100644 --- a/docs/examples/guide/layout/grid_layout5_col_span.py +++ b/docs/examples/guide/layout/grid_layout5_col_span.py @@ -3,7 +3,7 @@ class GridLayoutExample(App): - CSS_PATH = "grid_layout5_col_span.css" + CSS_PATH = "grid_layout5_col_span.tcss" def compose(self) -> ComposeResult: yield Static("One", classes="box") diff --git a/docs/examples/guide/layout/grid_layout5_col_span.css b/docs/examples/guide/layout/grid_layout5_col_span.tcss similarity index 100% rename from docs/examples/guide/layout/grid_layout5_col_span.css rename to docs/examples/guide/layout/grid_layout5_col_span.tcss diff --git a/docs/examples/guide/layout/grid_layout6_row_span.py b/docs/examples/guide/layout/grid_layout6_row_span.py index 54630b081b..ff056ddc34 100644 --- a/docs/examples/guide/layout/grid_layout6_row_span.py +++ b/docs/examples/guide/layout/grid_layout6_row_span.py @@ -3,7 +3,7 @@ class GridLayoutExample(App): - CSS_PATH = "grid_layout6_row_span.css" + CSS_PATH = "grid_layout6_row_span.tcss" def compose(self) -> ComposeResult: yield Static("One", classes="box") diff --git a/docs/examples/guide/layout/grid_layout6_row_span.css b/docs/examples/guide/layout/grid_layout6_row_span.tcss similarity index 100% rename from docs/examples/guide/layout/grid_layout6_row_span.css rename to docs/examples/guide/layout/grid_layout6_row_span.tcss diff --git a/docs/examples/guide/layout/grid_layout7_gutter.py b/docs/examples/guide/layout/grid_layout7_gutter.py index db916858c0..bc8eabdb44 100644 --- a/docs/examples/guide/layout/grid_layout7_gutter.py +++ b/docs/examples/guide/layout/grid_layout7_gutter.py @@ -3,7 +3,7 @@ class GridLayoutExample(App): - CSS_PATH = "grid_layout7_gutter.css" + CSS_PATH = "grid_layout7_gutter.tcss" def compose(self) -> ComposeResult: yield Static("One", classes="box") diff --git a/docs/examples/guide/layout/grid_layout7_gutter.css b/docs/examples/guide/layout/grid_layout7_gutter.tcss similarity index 100% rename from docs/examples/guide/layout/grid_layout7_gutter.css rename to docs/examples/guide/layout/grid_layout7_gutter.tcss diff --git a/docs/examples/guide/layout/grid_layout_auto.py b/docs/examples/guide/layout/grid_layout_auto.py new file mode 100644 index 0000000000..1ed98e6f85 --- /dev/null +++ b/docs/examples/guide/layout/grid_layout_auto.py @@ -0,0 +1,19 @@ +from textual.app import App, ComposeResult +from textual.widgets import Static + + +class GridLayoutExample(App): + CSS_PATH = "grid_layout_auto.tcss" + + def compose(self) -> ComposeResult: + yield Static("First column", classes="box") + yield Static("Two", classes="box") + yield Static("Three", classes="box") + yield Static("Four", classes="box") + yield Static("Five", classes="box") + yield Static("Six", classes="box") + + +if __name__ == "__main__": + app = GridLayoutExample() + app.run() diff --git a/docs/examples/guide/layout/grid_layout_auto.tcss b/docs/examples/guide/layout/grid_layout_auto.tcss new file mode 100644 index 0000000000..e95839d04f --- /dev/null +++ b/docs/examples/guide/layout/grid_layout_auto.tcss @@ -0,0 +1,11 @@ +Screen { + layout: grid; + grid-size: 3; + grid-columns: auto 1fr 1fr; + grid-rows: 25% 75%; +} + +.box { + height: 100%; + border: solid green; +} diff --git a/docs/examples/guide/layout/horizontal_layout.py b/docs/examples/guide/layout/horizontal_layout.py index 40997293f4..eccd11396b 100644 --- a/docs/examples/guide/layout/horizontal_layout.py +++ b/docs/examples/guide/layout/horizontal_layout.py @@ -3,7 +3,7 @@ class HorizontalLayoutExample(App): - CSS_PATH = "horizontal_layout.css" + CSS_PATH = "horizontal_layout.tcss" def compose(self) -> ComposeResult: yield Static("One", classes="box") diff --git a/docs/examples/guide/layout/horizontal_layout.css b/docs/examples/guide/layout/horizontal_layout.tcss similarity index 100% rename from docs/examples/guide/layout/horizontal_layout.css rename to docs/examples/guide/layout/horizontal_layout.tcss diff --git a/docs/examples/guide/layout/horizontal_layout_overflow.py b/docs/examples/guide/layout/horizontal_layout_overflow.py index b5be0e96df..d960fdbba3 100644 --- a/docs/examples/guide/layout/horizontal_layout_overflow.py +++ b/docs/examples/guide/layout/horizontal_layout_overflow.py @@ -3,7 +3,7 @@ class HorizontalLayoutExample(App): - CSS_PATH = "horizontal_layout_overflow.css" + CSS_PATH = "horizontal_layout_overflow.tcss" def compose(self) -> ComposeResult: yield Static("One", classes="box") diff --git a/docs/examples/guide/layout/horizontal_layout_overflow.css b/docs/examples/guide/layout/horizontal_layout_overflow.tcss similarity index 100% rename from docs/examples/guide/layout/horizontal_layout_overflow.css rename to docs/examples/guide/layout/horizontal_layout_overflow.tcss diff --git a/docs/examples/guide/layout/layers.py b/docs/examples/guide/layout/layers.py index 06afbd29aa..e7dfae2af4 100644 --- a/docs/examples/guide/layout/layers.py +++ b/docs/examples/guide/layout/layers.py @@ -3,7 +3,7 @@ class LayersExample(App): - CSS_PATH = "layers.css" + CSS_PATH = "layers.tcss" def compose(self) -> ComposeResult: yield Static("box1 (layer = above)", id="box1") diff --git a/docs/examples/guide/layout/layers.css b/docs/examples/guide/layout/layers.tcss similarity index 100% rename from docs/examples/guide/layout/layers.css rename to docs/examples/guide/layout/layers.tcss diff --git a/docs/examples/guide/layout/utility_containers.py b/docs/examples/guide/layout/utility_containers.py index eadf58b4c6..5eb0b05901 100644 --- a/docs/examples/guide/layout/utility_containers.py +++ b/docs/examples/guide/layout/utility_containers.py @@ -4,7 +4,7 @@ class UtilityContainersExample(App): - CSS_PATH = "utility_containers.css" + CSS_PATH = "utility_containers.tcss" def compose(self) -> ComposeResult: yield Horizontal( diff --git a/docs/examples/guide/layout/utility_containers.css b/docs/examples/guide/layout/utility_containers.tcss similarity index 100% rename from docs/examples/guide/layout/utility_containers.css rename to docs/examples/guide/layout/utility_containers.tcss diff --git a/docs/examples/guide/layout/utility_containers_using_with.py b/docs/examples/guide/layout/utility_containers_using_with.py index d09a3481ec..e72ec74f81 100644 --- a/docs/examples/guide/layout/utility_containers_using_with.py +++ b/docs/examples/guide/layout/utility_containers_using_with.py @@ -4,7 +4,7 @@ class UtilityContainersExample(App): - CSS_PATH = "utility_containers.css" + CSS_PATH = "utility_containers.tcss" def compose(self) -> ComposeResult: with Horizontal(): diff --git a/docs/examples/guide/layout/vertical_layout.py b/docs/examples/guide/layout/vertical_layout.py index 233407ac32..19fca3a284 100644 --- a/docs/examples/guide/layout/vertical_layout.py +++ b/docs/examples/guide/layout/vertical_layout.py @@ -3,7 +3,7 @@ class VerticalLayoutExample(App): - CSS_PATH = "vertical_layout.css" + CSS_PATH = "vertical_layout.tcss" def compose(self) -> ComposeResult: yield Static("One", classes="box") diff --git a/docs/examples/guide/layout/vertical_layout.css b/docs/examples/guide/layout/vertical_layout.tcss similarity index 100% rename from docs/examples/guide/layout/vertical_layout.css rename to docs/examples/guide/layout/vertical_layout.tcss diff --git a/docs/examples/guide/layout/vertical_layout_scrolled.py b/docs/examples/guide/layout/vertical_layout_scrolled.py index 984040ef7f..b3f42b6072 100644 --- a/docs/examples/guide/layout/vertical_layout_scrolled.py +++ b/docs/examples/guide/layout/vertical_layout_scrolled.py @@ -3,7 +3,7 @@ class VerticalLayoutScrolledExample(App): - CSS_PATH = "vertical_layout_scrolled.css" + CSS_PATH = "vertical_layout_scrolled.tcss" def compose(self) -> ComposeResult: yield Static("One", classes="box") diff --git a/docs/examples/guide/layout/vertical_layout_scrolled.css b/docs/examples/guide/layout/vertical_layout_scrolled.tcss similarity index 100% rename from docs/examples/guide/layout/vertical_layout_scrolled.css rename to docs/examples/guide/layout/vertical_layout_scrolled.tcss diff --git a/docs/examples/guide/reactivity/computed01.py b/docs/examples/guide/reactivity/computed01.py index dcef731ff4..072d12c312 100644 --- a/docs/examples/guide/reactivity/computed01.py +++ b/docs/examples/guide/reactivity/computed01.py @@ -6,7 +6,7 @@ class ComputedApp(App): - CSS_PATH = "computed01.css" + CSS_PATH = "computed01.tcss" red = reactive(0) green = reactive(0) diff --git a/docs/examples/guide/reactivity/computed01.css b/docs/examples/guide/reactivity/computed01.tcss similarity index 100% rename from docs/examples/guide/reactivity/computed01.css rename to docs/examples/guide/reactivity/computed01.tcss diff --git a/docs/examples/guide/reactivity/refresh01.py b/docs/examples/guide/reactivity/refresh01.py index d01e1031c6..8f9aceed5c 100644 --- a/docs/examples/guide/reactivity/refresh01.py +++ b/docs/examples/guide/reactivity/refresh01.py @@ -14,7 +14,7 @@ def render(self) -> str: class WatchApp(App): - CSS_PATH = "refresh01.css" + CSS_PATH = "refresh01.tcss" def compose(self) -> ComposeResult: yield Input(placeholder="Enter your name") diff --git a/docs/examples/guide/reactivity/refresh01.css b/docs/examples/guide/reactivity/refresh01.tcss similarity index 100% rename from docs/examples/guide/reactivity/refresh01.css rename to docs/examples/guide/reactivity/refresh01.tcss diff --git a/docs/examples/guide/reactivity/refresh02.py b/docs/examples/guide/reactivity/refresh02.py index 28da2549c1..24752096bd 100644 --- a/docs/examples/guide/reactivity/refresh02.py +++ b/docs/examples/guide/reactivity/refresh02.py @@ -14,7 +14,7 @@ def render(self) -> str: class WatchApp(App): - CSS_PATH = "refresh02.css" + CSS_PATH = "refresh02.tcss" def compose(self) -> ComposeResult: yield Input(placeholder="Enter your name") diff --git a/docs/examples/guide/reactivity/refresh02.css b/docs/examples/guide/reactivity/refresh02.tcss similarity index 100% rename from docs/examples/guide/reactivity/refresh02.css rename to docs/examples/guide/reactivity/refresh02.tcss diff --git a/docs/examples/guide/reactivity/validate01.py b/docs/examples/guide/reactivity/validate01.py index d424a5274e..65d8113c07 100644 --- a/docs/examples/guide/reactivity/validate01.py +++ b/docs/examples/guide/reactivity/validate01.py @@ -5,7 +5,7 @@ class ValidateApp(App): - CSS_PATH = "validate01.css" + CSS_PATH = "validate01.tcss" count = reactive(0) diff --git a/docs/examples/guide/reactivity/validate01.css b/docs/examples/guide/reactivity/validate01.tcss similarity index 100% rename from docs/examples/guide/reactivity/validate01.css rename to docs/examples/guide/reactivity/validate01.tcss diff --git a/docs/examples/guide/reactivity/watch01.py b/docs/examples/guide/reactivity/watch01.py index 5d2cacffd4..7c3160cc28 100644 --- a/docs/examples/guide/reactivity/watch01.py +++ b/docs/examples/guide/reactivity/watch01.py @@ -6,7 +6,7 @@ class WatchApp(App): - CSS_PATH = "watch01.css" + CSS_PATH = "watch01.tcss" color = reactive(Color.parse("transparent")) # (1)! diff --git a/docs/examples/guide/reactivity/watch01.css b/docs/examples/guide/reactivity/watch01.tcss similarity index 94% rename from docs/examples/guide/reactivity/watch01.css rename to docs/examples/guide/reactivity/watch01.tcss index 1b1fce667d..1159431a2f 100644 --- a/docs/examples/guide/reactivity/watch01.css +++ b/docs/examples/guide/reactivity/watch01.tcss @@ -15,7 +15,7 @@ Input { border: wide $secondary; } -#new { +#new { height: 100%; border: wide $secondary; } diff --git a/docs/examples/guide/screens/modal01.py b/docs/examples/guide/screens/modal01.py index 8d740f9f26..2966e25737 100644 --- a/docs/examples/guide/screens/modal01.py +++ b/docs/examples/guide/screens/modal01.py @@ -33,7 +33,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: class ModalApp(App): """An app with a modal dialog.""" - CSS_PATH = "modal01.css" + CSS_PATH = "modal01.tcss" BINDINGS = [("q", "request_quit", "Quit")] def compose(self) -> ComposeResult: diff --git a/docs/examples/guide/screens/modal01.css b/docs/examples/guide/screens/modal01.tcss similarity index 100% rename from docs/examples/guide/screens/modal01.css rename to docs/examples/guide/screens/modal01.tcss diff --git a/docs/examples/guide/screens/modal02.py b/docs/examples/guide/screens/modal02.py index bfadd06db6..2d3210c670 100644 --- a/docs/examples/guide/screens/modal02.py +++ b/docs/examples/guide/screens/modal02.py @@ -33,7 +33,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: class ModalApp(App): """An app with a modal dialog.""" - CSS_PATH = "modal01.css" + CSS_PATH = "modal01.tcss" BINDINGS = [("q", "request_quit", "Quit")] def compose(self) -> ComposeResult: diff --git a/docs/examples/guide/screens/modal03.py b/docs/examples/guide/screens/modal03.py index e19fc527bf..410722255c 100644 --- a/docs/examples/guide/screens/modal03.py +++ b/docs/examples/guide/screens/modal03.py @@ -33,7 +33,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None: class ModalApp(App): """An app with a modal dialog.""" - CSS_PATH = "modal01.css" + CSS_PATH = "modal01.tcss" BINDINGS = [("q", "request_quit", "Quit")] def compose(self) -> ComposeResult: diff --git a/docs/examples/guide/screens/modes01.py b/docs/examples/guide/screens/modes01.py new file mode 100644 index 0000000000..c56741dddd --- /dev/null +++ b/docs/examples/guide/screens/modes01.py @@ -0,0 +1,42 @@ +from textual.app import App, ComposeResult +from textual.screen import Screen +from textual.widgets import Footer, Placeholder + + +class DashboardScreen(Screen): + def compose(self) -> ComposeResult: + yield Placeholder("Dashboard Screen") + yield Footer() + + +class SettingsScreen(Screen): + def compose(self) -> ComposeResult: + yield Placeholder("Settings Screen") + yield Footer() + + +class HelpScreen(Screen): + def compose(self) -> ComposeResult: + yield Placeholder("Help Screen") + yield Footer() + + +class ModesApp(App): + BINDINGS = [ + ("d", "switch_mode('dashboard')", "Dashboard"), # (1)! + ("s", "switch_mode('settings')", "Settings"), + ("h", "switch_mode('help')", "Help"), + ] + MODES = { + "dashboard": DashboardScreen, # (2)! + "settings": SettingsScreen, + "help": HelpScreen, + } + + def on_mount(self) -> None: + self.switch_mode("dashboard") # (3)! + + +if __name__ == "__main__": + app = ModesApp() + app.run() diff --git a/docs/examples/guide/screens/screen01.py b/docs/examples/guide/screens/screen01.py index 7b83cedee9..568c98d97a 100644 --- a/docs/examples/guide/screens/screen01.py +++ b/docs/examples/guide/screens/screen01.py @@ -2,7 +2,6 @@ from textual.screen import Screen from textual.widgets import Static - ERROR_TEXT = """ An error has occurred. To continue: @@ -25,7 +24,7 @@ def compose(self) -> ComposeResult: class BSODApp(App): - CSS_PATH = "screen01.css" + CSS_PATH = "screen01.tcss" SCREENS = {"bsod": BSOD()} BINDINGS = [("b", "push_screen('bsod')", "BSOD")] diff --git a/docs/examples/guide/screens/screen02.css b/docs/examples/guide/screens/screen01.tcss similarity index 92% rename from docs/examples/guide/screens/screen02.css rename to docs/examples/guide/screens/screen01.tcss index 0ee028ebe7..7d0ee443a9 100644 --- a/docs/examples/guide/screens/screen02.css +++ b/docs/examples/guide/screens/screen01.tcss @@ -5,7 +5,7 @@ BSOD { } BSOD>Static { - width: 70; + width: 70; } #title { diff --git a/docs/examples/guide/screens/screen02.py b/docs/examples/guide/screens/screen02.py index f422a410e5..b15e84d62a 100644 --- a/docs/examples/guide/screens/screen02.py +++ b/docs/examples/guide/screens/screen02.py @@ -2,7 +2,6 @@ from textual.screen import Screen from textual.widgets import Static - ERROR_TEXT = """ An error has occurred. To continue: @@ -25,7 +24,7 @@ def compose(self) -> ComposeResult: class BSODApp(App): - CSS_PATH = "screen02.css" + CSS_PATH = "screen02.tcss" BINDINGS = [("b", "push_screen('bsod')", "BSOD")] def on_mount(self) -> None: diff --git a/docs/examples/guide/screens/screen01.css b/docs/examples/guide/screens/screen02.tcss similarity index 92% rename from docs/examples/guide/screens/screen01.css rename to docs/examples/guide/screens/screen02.tcss index 0ee028ebe7..7d0ee443a9 100644 --- a/docs/examples/guide/screens/screen01.css +++ b/docs/examples/guide/screens/screen02.tcss @@ -5,7 +5,7 @@ BSOD { } BSOD>Static { - width: 70; + width: 70; } #title { diff --git a/docs/examples/guide/testing/rgb.py b/docs/examples/guide/testing/rgb.py new file mode 100644 index 0000000000..d8b49cd1c3 --- /dev/null +++ b/docs/examples/guide/testing/rgb.py @@ -0,0 +1,42 @@ +from textual import on +from textual.app import App, ComposeResult +from textual.containers import Horizontal +from textual.widgets import Button, Footer + + +class RGBApp(App): + CSS = """ + Screen { + align: center middle; + } + Horizontal { + width: auto; + height: auto; + } + """ + + BINDINGS = [ + ("r", "switch_color('red')", "Go Red"), + ("g", "switch_color('green')", "Go Green"), + ("b", "switch_color('blue')", "Go Blue"), + ] + + def compose(self) -> ComposeResult: + with Horizontal(): + yield Button("Red", id="red") + yield Button("Green", id="green") + yield Button("Blue", id="blue") + yield Footer() + + @on(Button.Pressed) + def pressed_button(self, event: Button.Pressed) -> None: + assert event.button.id is not None + self.action_switch_color(event.button.id) + + def action_switch_color(self, color: str) -> None: + self.screen.styles.background = color + + +if __name__ == "__main__": + app = RGBApp() + app.run() diff --git a/docs/examples/guide/testing/test_rgb.py b/docs/examples/guide/testing/test_rgb.py new file mode 100644 index 0000000000..030f62b505 --- /dev/null +++ b/docs/examples/guide/testing/test_rgb.py @@ -0,0 +1,42 @@ +from rgb import RGBApp + +from textual.color import Color + + +async def test_keys(): # (1)! + """Test pressing keys has the desired result.""" + app = RGBApp() + async with app.run_test() as pilot: # (2)! + # Test pressing the R key + await pilot.press("r") # (3)! + assert app.screen.styles.background == Color.parse("red") # (4)! + + # Test pressing the G key + await pilot.press("g") + assert app.screen.styles.background == Color.parse("green") + + # Test pressing the B key + await pilot.press("b") + assert app.screen.styles.background == Color.parse("blue") + + # Test pressing the X key + await pilot.press("x") + # No binding (so no change to the color) + assert app.screen.styles.background == Color.parse("blue") + + +async def test_buttons(): + """Test pressing keys has the desired result.""" + app = RGBApp() + async with app.run_test() as pilot: + # Test clicking the "red" button + await pilot.click("#red") # (5)! + assert app.screen.styles.background == Color.parse("red") + + # Test clicking the "green" button + await pilot.click("#green") + assert app.screen.styles.background == Color.parse("green") + + # Test clicking the "blue" button + await pilot.click("#blue") + assert app.screen.styles.background == Color.parse("blue") diff --git a/docs/examples/guide/widgets/fizzbuzz01.py b/docs/examples/guide/widgets/fizzbuzz01.py index 129abdd074..48471ef502 100644 --- a/docs/examples/guide/widgets/fizzbuzz01.py +++ b/docs/examples/guide/widgets/fizzbuzz01.py @@ -19,7 +19,7 @@ def on_mount(self) -> None: class FizzBuzzApp(App): - CSS_PATH = "fizzbuzz01.css" + CSS_PATH = "fizzbuzz01.tcss" def compose(self) -> ComposeResult: yield FizzBuzz() diff --git a/docs/examples/guide/widgets/fizzbuzz02.css b/docs/examples/guide/widgets/fizzbuzz01.tcss similarity index 83% rename from docs/examples/guide/widgets/fizzbuzz02.css rename to docs/examples/guide/widgets/fizzbuzz01.tcss index a8fe581c1b..1854e861a1 100644 --- a/docs/examples/guide/widgets/fizzbuzz02.css +++ b/docs/examples/guide/widgets/fizzbuzz01.tcss @@ -6,5 +6,5 @@ FizzBuzz { width: auto; height: auto; background: $primary; - color: $text; + color: $text; } diff --git a/docs/examples/guide/widgets/fizzbuzz02.py b/docs/examples/guide/widgets/fizzbuzz02.py index 58618aba5c..f9459237f4 100644 --- a/docs/examples/guide/widgets/fizzbuzz02.py +++ b/docs/examples/guide/widgets/fizzbuzz02.py @@ -24,7 +24,7 @@ def get_content_width(self, container: Size, viewport: Size) -> int: class FizzBuzzApp(App): - CSS_PATH = "fizzbuzz02.css" + CSS_PATH = "fizzbuzz02.tcss" def compose(self) -> ComposeResult: yield FizzBuzz() diff --git a/docs/examples/guide/widgets/fizzbuzz01.css b/docs/examples/guide/widgets/fizzbuzz02.tcss similarity index 77% rename from docs/examples/guide/widgets/fizzbuzz01.css rename to docs/examples/guide/widgets/fizzbuzz02.tcss index ed041d2dc0..1854e861a1 100644 --- a/docs/examples/guide/widgets/fizzbuzz01.css +++ b/docs/examples/guide/widgets/fizzbuzz02.tcss @@ -5,6 +5,6 @@ Screen { FizzBuzz { width: auto; height: auto; - background: $primary; + background: $primary; color: $text; } diff --git a/docs/examples/guide/widgets/hello01.css b/docs/examples/guide/widgets/hello01.tcss similarity index 100% rename from docs/examples/guide/widgets/hello01.css rename to docs/examples/guide/widgets/hello01.tcss diff --git a/docs/examples/guide/widgets/hello02.py b/docs/examples/guide/widgets/hello02.py index ffab9fd1b3..87a782f1f2 100644 --- a/docs/examples/guide/widgets/hello02.py +++ b/docs/examples/guide/widgets/hello02.py @@ -10,7 +10,7 @@ def render(self) -> RenderResult: class CustomApp(App): - CSS_PATH = "hello02.css" + CSS_PATH = "hello02.tcss" def compose(self) -> ComposeResult: yield Hello() diff --git a/docs/examples/guide/widgets/hello02.css b/docs/examples/guide/widgets/hello02.tcss similarity index 100% rename from docs/examples/guide/widgets/hello02.css rename to docs/examples/guide/widgets/hello02.tcss diff --git a/docs/examples/guide/widgets/hello03.py b/docs/examples/guide/widgets/hello03.py index 62708a3e2e..e98148f49d 100644 --- a/docs/examples/guide/widgets/hello03.py +++ b/docs/examples/guide/widgets/hello03.py @@ -3,7 +3,6 @@ from textual.app import App, ComposeResult from textual.widgets import Static - hellos = cycle( [ "Hola", @@ -37,7 +36,7 @@ def next_word(self) -> None: class CustomApp(App): - CSS_PATH = "hello03.css" + CSS_PATH = "hello03.tcss" def compose(self) -> ComposeResult: yield Hello() diff --git a/docs/examples/guide/widgets/hello03.css b/docs/examples/guide/widgets/hello03.tcss similarity index 100% rename from docs/examples/guide/widgets/hello03.css rename to docs/examples/guide/widgets/hello03.tcss diff --git a/docs/examples/guide/widgets/hello04.py b/docs/examples/guide/widgets/hello04.py index 40e3fc4360..92dbef3fdf 100644 --- a/docs/examples/guide/widgets/hello04.py +++ b/docs/examples/guide/widgets/hello04.py @@ -3,7 +3,6 @@ from textual.app import App, ComposeResult from textual.widgets import Static - hellos = cycle( [ "Hola", @@ -48,7 +47,7 @@ def next_word(self) -> None: class CustomApp(App): - CSS_PATH = "hello04.css" + CSS_PATH = "hello04.tcss" def compose(self) -> ComposeResult: yield Hello() diff --git a/docs/examples/guide/widgets/hello04.css b/docs/examples/guide/widgets/hello04.tcss similarity index 100% rename from docs/examples/guide/widgets/hello04.css rename to docs/examples/guide/widgets/hello04.tcss diff --git a/docs/examples/guide/widgets/hello05.py b/docs/examples/guide/widgets/hello05.py index 1430138b86..fbb7acb9c7 100644 --- a/docs/examples/guide/widgets/hello05.py +++ b/docs/examples/guide/widgets/hello05.py @@ -3,7 +3,6 @@ from textual.app import App, ComposeResult from textual.widgets import Static - hellos = cycle( [ "Hola", @@ -34,7 +33,7 @@ def action_next_word(self) -> None: class CustomApp(App): - CSS_PATH = "hello05.css" + CSS_PATH = "hello05.tcss" def compose(self) -> ComposeResult: yield Hello() diff --git a/docs/examples/guide/widgets/hello05.css b/docs/examples/guide/widgets/hello05.tcss similarity index 100% rename from docs/examples/guide/widgets/hello05.css rename to docs/examples/guide/widgets/hello05.tcss diff --git a/docs/examples/guide/widgets/hello06.py b/docs/examples/guide/widgets/hello06.py new file mode 100644 index 0000000000..4ce1a9abfa --- /dev/null +++ b/docs/examples/guide/widgets/hello06.py @@ -0,0 +1,47 @@ +from itertools import cycle + +from textual.app import App, ComposeResult +from textual.widgets import Static + +hellos = cycle( + [ + "Hola", + "Bonjour", + "Guten tag", + "Salve", + "Nǐn hǎo", + "Olá", + "Asalaam alaikum", + "Konnichiwa", + "Anyoung haseyo", + "Zdravstvuyte", + "Hello", + ] +) + + +class Hello(Static): + """Display a greeting.""" + + BORDER_TITLE = "Hello Widget" # (1)! + + def on_mount(self) -> None: + self.action_next_word() + self.border_subtitle = "Click for next hello" # (2)! + + def action_next_word(self) -> None: + """Get a new hello and update the content area.""" + hello = next(hellos) + self.update(f"[@click='next_word']{hello}[/], [b]World[/b]!") + + +class CustomApp(App): + CSS_PATH = "hello05.tcss" + + def compose(self) -> ComposeResult: + yield Hello() + + +if __name__ == "__main__": + app = CustomApp() + app.run() diff --git a/docs/examples/guide/widgets/hello06.tcss b/docs/examples/guide/widgets/hello06.tcss new file mode 100644 index 0000000000..1e46fd4155 --- /dev/null +++ b/docs/examples/guide/widgets/hello06.tcss @@ -0,0 +1,12 @@ +Screen { + align: center middle; +} + +Hello { + width: 40; + height: 9; + padding: 1 2; + background: $panel; + border: $secondary tall; + content-align: center middle; +} diff --git a/docs/examples/guide/widgets/loading01.py b/docs/examples/guide/widgets/loading01.py new file mode 100644 index 0000000000..3e25899cbe --- /dev/null +++ b/docs/examples/guide/widgets/loading01.py @@ -0,0 +1,54 @@ +from asyncio import sleep +from random import randint + +from textual import work +from textual.app import App, ComposeResult +from textual.widgets import DataTable + +ROWS = [ + ("lane", "swimmer", "country", "time"), + (4, "Joseph Schooling", "Singapore", 50.39), + (2, "Michael Phelps", "United States", 51.14), + (5, "Chad le Clos", "South Africa", 51.14), + (6, "László Cseh", "Hungary", 51.14), + (3, "Li Zhuhao", "China", 51.26), + (8, "Mehdy Metella", "France", 51.58), + (7, "Tom Shields", "United States", 51.73), + (1, "Aleksandr Sadovnikov", "Russia", 51.84), + (10, "Darren Burns", "Scotland", 51.84), +] + + +class DataApp(App): + CSS = """ + Screen { + layout: grid; + grid-size: 2; + } + DataTable { + height: 1fr; + } + """ + + def compose(self) -> ComposeResult: + yield DataTable() + yield DataTable() + yield DataTable() + yield DataTable() + + def on_mount(self) -> None: + for data_table in self.query(DataTable): + data_table.loading = True # (1)! + self.load_data(data_table) + + @work + async def load_data(self, data_table: DataTable) -> None: + await sleep(randint(2, 10)) # (2)! + data_table.add_columns(*ROWS[0]) + data_table.add_rows(ROWS[1:]) + data_table.loading = False # (3)! + + +if __name__ == "__main__": + app = DataApp() + app.run() diff --git a/docs/examples/guide/workers/weather.css b/docs/examples/guide/workers/weather.tcss similarity index 100% rename from docs/examples/guide/workers/weather.css rename to docs/examples/guide/workers/weather.tcss diff --git a/docs/examples/guide/workers/weather01.py b/docs/examples/guide/workers/weather01.py index f8f9338d06..4e1009ffdd 100644 --- a/docs/examples/guide/workers/weather01.py +++ b/docs/examples/guide/workers/weather01.py @@ -9,7 +9,7 @@ class WeatherApp(App): """App to display the current weather.""" - CSS_PATH = "weather.css" + CSS_PATH = "weather.tcss" def compose(self) -> ComposeResult: yield Input(placeholder="Enter a City") diff --git a/docs/examples/guide/workers/weather02.py b/docs/examples/guide/workers/weather02.py index 25b2b24049..db20fa30cf 100644 --- a/docs/examples/guide/workers/weather02.py +++ b/docs/examples/guide/workers/weather02.py @@ -9,7 +9,7 @@ class WeatherApp(App): """App to display the current weather.""" - CSS_PATH = "weather.css" + CSS_PATH = "weather.tcss" def compose(self) -> ComposeResult: yield Input(placeholder="Enter a City") diff --git a/docs/examples/guide/workers/weather03.py b/docs/examples/guide/workers/weather03.py index 6fc10082f5..d268449e5f 100644 --- a/docs/examples/guide/workers/weather03.py +++ b/docs/examples/guide/workers/weather03.py @@ -10,7 +10,7 @@ class WeatherApp(App): """App to display the current weather.""" - CSS_PATH = "weather.css" + CSS_PATH = "weather.tcss" def compose(self) -> ComposeResult: yield Input(placeholder="Enter a City") diff --git a/docs/examples/guide/workers/weather04.py b/docs/examples/guide/workers/weather04.py index 13820927ab..ca7dfeee23 100644 --- a/docs/examples/guide/workers/weather04.py +++ b/docs/examples/guide/workers/weather04.py @@ -11,7 +11,7 @@ class WeatherApp(App): """App to display the current weather.""" - CSS_PATH = "weather.css" + CSS_PATH = "weather.tcss" def compose(self) -> ComposeResult: yield Input(placeholder="Enter a City") diff --git a/docs/examples/guide/workers/weather05.py b/docs/examples/guide/workers/weather05.py index ea8871d7c1..c1da80cb4d 100644 --- a/docs/examples/guide/workers/weather05.py +++ b/docs/examples/guide/workers/weather05.py @@ -12,7 +12,7 @@ class WeatherApp(App): """App to display the current weather.""" - CSS_PATH = "weather.css" + CSS_PATH = "weather.tcss" def compose(self) -> ComposeResult: yield Input(placeholder="Enter a City") diff --git a/docs/examples/how-to/layout.py b/docs/examples/how-to/layout.py index 430670673c..2ede1ea409 100644 --- a/docs/examples/how-to/layout.py +++ b/docs/examples/how-to/layout.py @@ -58,7 +58,7 @@ def compose(self) -> ComposeResult: class LayoutApp(App): - CSS_PATH = "layout.css" + CSS_PATH = "layout.tcss" def on_ready(self) -> None: self.push_screen(TweetScreen()) diff --git a/docs/examples/styles/align.py b/docs/examples/styles/align.py index 89f293aae5..a19a803f64 100644 --- a/docs/examples/styles/align.py +++ b/docs/examples/styles/align.py @@ -8,4 +8,4 @@ def compose(self): yield Label("Take note, browsers.", classes="box") -app = AlignApp(css_path="align.css") +app = AlignApp(css_path="align.tcss") diff --git a/docs/examples/styles/align.css b/docs/examples/styles/align.tcss similarity index 100% rename from docs/examples/styles/align.css rename to docs/examples/styles/align.tcss diff --git a/docs/examples/styles/align_all.py b/docs/examples/styles/align_all.py index 1ff8d6040c..2d409414f1 100644 --- a/docs/examples/styles/align_all.py +++ b/docs/examples/styles/align_all.py @@ -6,7 +6,7 @@ class AlignAllApp(App): """App that illustrates all alignments.""" - CSS_PATH = "align_all.css" + CSS_PATH = "align_all.tcss" def compose(self) -> ComposeResult: yield Container(Label("left top"), id="left-top") diff --git a/docs/examples/styles/align_all.css b/docs/examples/styles/align_all.tcss similarity index 100% rename from docs/examples/styles/align_all.css rename to docs/examples/styles/align_all.tcss diff --git a/docs/examples/styles/background.py b/docs/examples/styles/background.py index 6d8669baa4..5c5db8bc76 100644 --- a/docs/examples/styles/background.py +++ b/docs/examples/styles/background.py @@ -9,4 +9,4 @@ def compose(self): yield Label("Widget 3", id="static3") -app = BackgroundApp(css_path="background.css") +app = BackgroundApp(css_path="background.tcss") diff --git a/docs/examples/styles/background.css b/docs/examples/styles/background.tcss similarity index 100% rename from docs/examples/styles/background.css rename to docs/examples/styles/background.tcss diff --git a/docs/examples/styles/background_transparency.py b/docs/examples/styles/background_transparency.py index 942bb51b8a..abcdc30375 100644 --- a/docs/examples/styles/background_transparency.py +++ b/docs/examples/styles/background_transparency.py @@ -4,6 +4,7 @@ class BackgroundTransparencyApp(App): """Simple app to exemplify different transparency settings.""" + def compose(self) -> ComposeResult: yield Static("10%", id="t10") yield Static("20%", id="t20") @@ -17,4 +18,4 @@ def compose(self) -> ComposeResult: yield Static("100%", id="t100") -app = BackgroundTransparencyApp(css_path="background_transparency.css") +app = BackgroundTransparencyApp(css_path="background_transparency.tcss") diff --git a/docs/examples/styles/background_transparency.css b/docs/examples/styles/background_transparency.tcss similarity index 100% rename from docs/examples/styles/background_transparency.css rename to docs/examples/styles/background_transparency.tcss diff --git a/docs/examples/styles/border.py b/docs/examples/styles/border.py index d426e85b0d..31d244f2c1 100644 --- a/docs/examples/styles/border.py +++ b/docs/examples/styles/border.py @@ -9,4 +9,4 @@ def compose(self): yield Label("My border is tall blue", id="label3") -app = BorderApp(css_path="border.css") +app = BorderApp(css_path="border.tcss") diff --git a/docs/examples/styles/border.css b/docs/examples/styles/border.tcss similarity index 100% rename from docs/examples/styles/border.css rename to docs/examples/styles/border.tcss diff --git a/docs/examples/styles/border_all.py b/docs/examples/styles/border_all.py index c5dbdac9ac..2fab42f352 100644 --- a/docs/examples/styles/border_all.py +++ b/docs/examples/styles/border_all.py @@ -24,4 +24,4 @@ def compose(self): ) -app = AllBordersApp(css_path="border_all.css") +app = AllBordersApp(css_path="border_all.tcss") diff --git a/docs/examples/styles/border_all.css b/docs/examples/styles/border_all.tcss similarity index 100% rename from docs/examples/styles/border_all.css rename to docs/examples/styles/border_all.tcss diff --git a/docs/examples/styles/border_sub_title_align_all.py b/docs/examples/styles/border_sub_title_align_all.py index f3c6a1cc97..1ec8340433 100644 --- a/docs/examples/styles/border_sub_title_align_all.py +++ b/docs/examples/styles/border_sub_title_align_all.py @@ -68,7 +68,7 @@ def compose(self): ) -app = BorderSubTitleAlignAll(css_path="border_sub_title_align_all.css") +app = BorderSubTitleAlignAll(css_path="border_sub_title_align_all.tcss") if __name__ == "__main__": app.run() diff --git a/docs/examples/styles/border_sub_title_align_all.css b/docs/examples/styles/border_sub_title_align_all.tcss similarity index 100% rename from docs/examples/styles/border_sub_title_align_all.css rename to docs/examples/styles/border_sub_title_align_all.tcss diff --git a/docs/examples/styles/border_subtitle_align.py b/docs/examples/styles/border_subtitle_align.py index 9c48a78aca..4c858b3df1 100644 --- a/docs/examples/styles/border_subtitle_align.py +++ b/docs/examples/styles/border_subtitle_align.py @@ -17,4 +17,4 @@ def compose(self): yield lbl -app = BorderSubtitleAlignApp(css_path="border_subtitle_align.css") +app = BorderSubtitleAlignApp(css_path="border_subtitle_align.tcss") diff --git a/docs/examples/styles/border_subtitle_align.css b/docs/examples/styles/border_subtitle_align.tcss similarity index 100% rename from docs/examples/styles/border_subtitle_align.css rename to docs/examples/styles/border_subtitle_align.tcss diff --git a/docs/examples/styles/border_title_align.py b/docs/examples/styles/border_title_align.py index 674a65ec33..ba790104f8 100644 --- a/docs/examples/styles/border_title_align.py +++ b/docs/examples/styles/border_title_align.py @@ -17,4 +17,4 @@ def compose(self): yield lbl -app = BorderTitleAlignApp(css_path="border_title_align.css") +app = BorderTitleAlignApp(css_path="border_title_align.tcss") diff --git a/docs/examples/styles/border_title_align.css b/docs/examples/styles/border_title_align.tcss similarity index 100% rename from docs/examples/styles/border_title_align.css rename to docs/examples/styles/border_title_align.tcss diff --git a/docs/examples/styles/border_title_colors.py b/docs/examples/styles/border_title_colors.py index 1af74ccc6d..5e8cca3fd4 100644 --- a/docs/examples/styles/border_title_colors.py +++ b/docs/examples/styles/border_title_colors.py @@ -3,7 +3,7 @@ class BorderTitleApp(App): - CSS_PATH = "border_title_colors.css" + CSS_PATH = "border_title_colors.tcss" def compose(self) -> ComposeResult: yield Label("Hello, World!") diff --git a/docs/examples/styles/border_title_colors.css b/docs/examples/styles/border_title_colors.tcss similarity index 100% rename from docs/examples/styles/border_title_colors.css rename to docs/examples/styles/border_title_colors.tcss diff --git a/docs/examples/styles/box_sizing.py b/docs/examples/styles/box_sizing.py index 32fc56c6be..9bd4511891 100644 --- a/docs/examples/styles/box_sizing.py +++ b/docs/examples/styles/box_sizing.py @@ -8,4 +8,4 @@ def compose(self): yield Static("I'm using content-box!", id="static2") -app = BoxSizingApp(css_path="box_sizing.css") +app = BoxSizingApp(css_path="box_sizing.tcss") diff --git a/docs/examples/styles/box_sizing.css b/docs/examples/styles/box_sizing.tcss similarity index 100% rename from docs/examples/styles/box_sizing.css rename to docs/examples/styles/box_sizing.tcss diff --git a/docs/examples/styles/color.py b/docs/examples/styles/color.py index 0f10ea39ed..bef97429f8 100644 --- a/docs/examples/styles/color.py +++ b/docs/examples/styles/color.py @@ -9,4 +9,4 @@ def compose(self): yield Label("I'm hsl(240, 100%, 50%)!", id="label3") -app = ColorApp(css_path="color.css") +app = ColorApp(css_path="color.tcss") diff --git a/docs/examples/styles/color.css b/docs/examples/styles/color.tcss similarity index 100% rename from docs/examples/styles/color.css rename to docs/examples/styles/color.tcss diff --git a/docs/examples/styles/color_auto.py b/docs/examples/styles/color_auto.py index 5202415c52..4bb18f6e49 100644 --- a/docs/examples/styles/color_auto.py +++ b/docs/examples/styles/color_auto.py @@ -11,4 +11,4 @@ def compose(self): yield Label("The quick brown fox jumps over the lazy dog!", id="lbl5") -app = ColorApp(css_path="color_auto.css") +app = ColorApp(css_path="color_auto.tcss") diff --git a/docs/examples/styles/color_auto.css b/docs/examples/styles/color_auto.tcss similarity index 100% rename from docs/examples/styles/color_auto.css rename to docs/examples/styles/color_auto.tcss diff --git a/docs/examples/styles/column_span.py b/docs/examples/styles/column_span.py index 272819621a..6d9b582ba5 100644 --- a/docs/examples/styles/column_span.py +++ b/docs/examples/styles/column_span.py @@ -16,4 +16,4 @@ def compose(self): ) -app = MyApp(css_path="column_span.css") +app = MyApp(css_path="column_span.tcss") diff --git a/docs/examples/styles/column_span.css b/docs/examples/styles/column_span.tcss similarity index 100% rename from docs/examples/styles/column_span.css rename to docs/examples/styles/column_span.tcss diff --git a/docs/examples/styles/content_align.py b/docs/examples/styles/content_align.py index 25bb3b29c5..71348d3032 100644 --- a/docs/examples/styles/content_align.py +++ b/docs/examples/styles/content_align.py @@ -9,4 +9,4 @@ def compose(self): yield Label("...Horizontally [i]and[/] vertically!", id="box3") -app = ContentAlignApp(css_path="content_align.css") +app = ContentAlignApp(css_path="content_align.tcss") diff --git a/docs/examples/styles/content_align.css b/docs/examples/styles/content_align.tcss similarity index 100% rename from docs/examples/styles/content_align.css rename to docs/examples/styles/content_align.tcss diff --git a/docs/examples/styles/content_align_all.py b/docs/examples/styles/content_align_all.py index 0460116672..5ba2bce7d6 100644 --- a/docs/examples/styles/content_align_all.py +++ b/docs/examples/styles/content_align_all.py @@ -15,4 +15,4 @@ def compose(self): yield Label("right bottom", id="right-bottom") -app = AllContentAlignApp(css_path="content_align_all.css") +app = AllContentAlignApp(css_path="content_align_all.tcss") diff --git a/docs/examples/styles/content_align_all.css b/docs/examples/styles/content_align_all.tcss similarity index 100% rename from docs/examples/styles/content_align_all.css rename to docs/examples/styles/content_align_all.tcss diff --git a/docs/examples/styles/display.py b/docs/examples/styles/display.py index 1e68c6e33d..4da6aa2cae 100644 --- a/docs/examples/styles/display.py +++ b/docs/examples/styles/display.py @@ -9,4 +9,4 @@ def compose(self): yield Static("Widget 3") -app = DisplayApp(css_path="display.css") +app = DisplayApp(css_path="display.tcss") diff --git a/docs/examples/styles/display.css b/docs/examples/styles/display.tcss similarity index 100% rename from docs/examples/styles/display.css rename to docs/examples/styles/display.tcss diff --git a/docs/examples/styles/dock_all.py b/docs/examples/styles/dock_all.py index 30907f98a8..f1b024f239 100644 --- a/docs/examples/styles/dock_all.py +++ b/docs/examples/styles/dock_all.py @@ -14,4 +14,4 @@ def compose(self): ) -app = DockAllApp(css_path="dock_all.css") +app = DockAllApp(css_path="dock_all.tcss") diff --git a/docs/examples/styles/dock_all.css b/docs/examples/styles/dock_all.tcss similarity index 100% rename from docs/examples/styles/dock_all.css rename to docs/examples/styles/dock_all.tcss diff --git a/docs/examples/styles/grid.py b/docs/examples/styles/grid.py index 1901d18628..0c43607c09 100644 --- a/docs/examples/styles/grid.py +++ b/docs/examples/styles/grid.py @@ -13,4 +13,4 @@ def compose(self): yield Static("Grid cell 7", id="static7") -app = GridApp(css_path="grid.css") +app = GridApp(css_path="grid.tcss") diff --git a/docs/examples/styles/grid.css b/docs/examples/styles/grid.tcss similarity index 100% rename from docs/examples/styles/grid.css rename to docs/examples/styles/grid.tcss diff --git a/docs/examples/styles/grid_columns.py b/docs/examples/styles/grid_columns.py index 05c772d561..6abbbc5a4d 100644 --- a/docs/examples/styles/grid_columns.py +++ b/docs/examples/styles/grid_columns.py @@ -19,4 +19,4 @@ def compose(self): ) -app = MyApp(css_path="grid_columns.css") +app = MyApp(css_path="grid_columns.tcss") diff --git a/docs/examples/styles/grid_columns.css b/docs/examples/styles/grid_columns.tcss similarity index 100% rename from docs/examples/styles/grid_columns.css rename to docs/examples/styles/grid_columns.tcss diff --git a/docs/examples/styles/grid_gutter.py b/docs/examples/styles/grid_gutter.py index 363d4a37f7..211b0e8c09 100644 --- a/docs/examples/styles/grid_gutter.py +++ b/docs/examples/styles/grid_gutter.py @@ -17,4 +17,4 @@ def compose(self): ) -app = MyApp(css_path="grid_gutter.css") +app = MyApp(css_path="grid_gutter.tcss") diff --git a/docs/examples/styles/grid_gutter.css b/docs/examples/styles/grid_gutter.tcss similarity index 100% rename from docs/examples/styles/grid_gutter.css rename to docs/examples/styles/grid_gutter.tcss diff --git a/docs/examples/styles/grid_rows.py b/docs/examples/styles/grid_rows.py index ed06f7b6dd..508c0143a4 100644 --- a/docs/examples/styles/grid_rows.py +++ b/docs/examples/styles/grid_rows.py @@ -19,4 +19,4 @@ def compose(self): ) -app = MyApp(css_path="grid_rows.css") +app = MyApp(css_path="grid_rows.tcss") diff --git a/docs/examples/styles/grid_rows.css b/docs/examples/styles/grid_rows.tcss similarity index 100% rename from docs/examples/styles/grid_rows.css rename to docs/examples/styles/grid_rows.tcss diff --git a/docs/examples/styles/grid_size_both.py b/docs/examples/styles/grid_size_both.py index 6383cc760f..0e60188191 100644 --- a/docs/examples/styles/grid_size_both.py +++ b/docs/examples/styles/grid_size_both.py @@ -14,4 +14,4 @@ def compose(self): ) -app = MyApp(css_path="grid_size_both.css") +app = MyApp(css_path="grid_size_both.tcss") diff --git a/docs/examples/styles/grid_size_both.css b/docs/examples/styles/grid_size_both.tcss similarity index 100% rename from docs/examples/styles/grid_size_both.css rename to docs/examples/styles/grid_size_both.tcss diff --git a/docs/examples/styles/grid_size_columns.py b/docs/examples/styles/grid_size_columns.py index 06be941503..c6d3392d5b 100644 --- a/docs/examples/styles/grid_size_columns.py +++ b/docs/examples/styles/grid_size_columns.py @@ -14,4 +14,4 @@ def compose(self): ) -app = MyApp(css_path="grid_size_columns.css") +app = MyApp(css_path="grid_size_columns.tcss") diff --git a/docs/examples/styles/grid_size_columns.css b/docs/examples/styles/grid_size_columns.tcss similarity index 100% rename from docs/examples/styles/grid_size_columns.css rename to docs/examples/styles/grid_size_columns.tcss diff --git a/docs/examples/styles/height.py b/docs/examples/styles/height.py index 00e3963c40..7eba3bbe10 100644 --- a/docs/examples/styles/height.py +++ b/docs/examples/styles/height.py @@ -7,4 +7,4 @@ def compose(self): yield Widget() -app = HeightApp(css_path="height.css") +app = HeightApp(css_path="height.tcss") diff --git a/docs/examples/styles/height.css b/docs/examples/styles/height.tcss similarity index 100% rename from docs/examples/styles/height.css rename to docs/examples/styles/height.tcss diff --git a/docs/examples/styles/height_comparison.py b/docs/examples/styles/height_comparison.py index 41d8f0a766..5fc72b237f 100644 --- a/docs/examples/styles/height_comparison.py +++ b/docs/examples/styles/height_comparison.py @@ -25,4 +25,4 @@ def compose(self): yield Ruler() -app = HeightComparisonApp(css_path="height_comparison.css") +app = HeightComparisonApp(css_path="height_comparison.tcss") diff --git a/docs/examples/styles/height_comparison.css b/docs/examples/styles/height_comparison.tcss similarity index 100% rename from docs/examples/styles/height_comparison.css rename to docs/examples/styles/height_comparison.tcss diff --git a/docs/examples/styles/layout.py b/docs/examples/styles/layout.py index bc87a4bb00..07be94c630 100644 --- a/docs/examples/styles/layout.py +++ b/docs/examples/styles/layout.py @@ -19,4 +19,4 @@ def compose(self): ) -app = LayoutApp(css_path="layout.css") +app = LayoutApp(css_path="layout.tcss") diff --git a/docs/examples/styles/layout.css b/docs/examples/styles/layout.tcss similarity index 100% rename from docs/examples/styles/layout.css rename to docs/examples/styles/layout.tcss diff --git a/docs/examples/styles/link_background.py b/docs/examples/styles/link_background.py index dc21f32984..6cc0161ef5 100644 --- a/docs/examples/styles/link_background.py +++ b/docs/examples/styles/link_background.py @@ -22,4 +22,4 @@ def compose(self): ) -app = LinkBackgroundApp(css_path="link_background.css") +app = LinkBackgroundApp(css_path="link_background.tcss") diff --git a/docs/examples/styles/link_background.css b/docs/examples/styles/link_background.tcss similarity index 100% rename from docs/examples/styles/link_background.css rename to docs/examples/styles/link_background.tcss diff --git a/docs/examples/styles/link_color.py b/docs/examples/styles/link_color.py index 85cd36c18f..bd093093b1 100644 --- a/docs/examples/styles/link_color.py +++ b/docs/examples/styles/link_color.py @@ -22,4 +22,4 @@ def compose(self): ) -app = LinkColorApp(css_path="link_color.css") +app = LinkColorApp(css_path="link_color.tcss") diff --git a/docs/examples/styles/link_color.css b/docs/examples/styles/link_color.tcss similarity index 100% rename from docs/examples/styles/link_color.css rename to docs/examples/styles/link_color.tcss diff --git a/docs/examples/styles/link_hover_background.py b/docs/examples/styles/link_hover_background.py index f1112b3558..d7d4d4928b 100644 --- a/docs/examples/styles/link_hover_background.py +++ b/docs/examples/styles/link_hover_background.py @@ -22,4 +22,4 @@ def compose(self): ) -app = LinkHoverBackgroundApp(css_path="link_hover_background.css") +app = LinkHoverBackgroundApp(css_path="link_hover_background.tcss") diff --git a/docs/examples/styles/link_hover_background.css b/docs/examples/styles/link_hover_background.tcss similarity index 100% rename from docs/examples/styles/link_hover_background.css rename to docs/examples/styles/link_hover_background.tcss diff --git a/docs/examples/styles/link_hover_color.py b/docs/examples/styles/link_hover_color.py index 56093563de..67b3acd21e 100644 --- a/docs/examples/styles/link_hover_color.py +++ b/docs/examples/styles/link_hover_color.py @@ -22,4 +22,4 @@ def compose(self): ) -app = LinkHoverColorApp(css_path="link_hover_color.css") +app = LinkHoverColorApp(css_path="link_hover_color.tcss") diff --git a/docs/examples/styles/link_hover_color.css b/docs/examples/styles/link_hover_color.tcss similarity index 100% rename from docs/examples/styles/link_hover_color.css rename to docs/examples/styles/link_hover_color.tcss diff --git a/docs/examples/styles/link_hover_style.py b/docs/examples/styles/link_hover_style.py index 6a6ef3dbac..6ffe727d37 100644 --- a/docs/examples/styles/link_hover_style.py +++ b/docs/examples/styles/link_hover_style.py @@ -22,4 +22,4 @@ def compose(self): ) -app = LinkHoverStyleApp(css_path="link_hover_style.css") +app = LinkHoverStyleApp(css_path="link_hover_style.tcss") diff --git a/docs/examples/styles/link_hover_style.css b/docs/examples/styles/link_hover_style.tcss similarity index 100% rename from docs/examples/styles/link_hover_style.css rename to docs/examples/styles/link_hover_style.tcss diff --git a/docs/examples/styles/link_style.py b/docs/examples/styles/link_style.py index 5ed9f12a9c..bab0d7eb8c 100644 --- a/docs/examples/styles/link_style.py +++ b/docs/examples/styles/link_style.py @@ -22,4 +22,4 @@ def compose(self): ) -app = LinkStyleApp(css_path="link_style.css") +app = LinkStyleApp(css_path="link_style.tcss") diff --git a/docs/examples/styles/link_style.css b/docs/examples/styles/link_style.tcss similarity index 100% rename from docs/examples/styles/link_style.css rename to docs/examples/styles/link_style.tcss diff --git a/docs/examples/styles/links.py b/docs/examples/styles/links.py index ddd4729748..93e9eead39 100644 --- a/docs/examples/styles/links.py +++ b/docs/examples/styles/links.py @@ -12,4 +12,4 @@ def compose(self) -> ComposeResult: yield Static(TEXT, id="custom") -app = LinksApp(css_path="links.css") +app = LinksApp(css_path="links.tcss") diff --git a/docs/examples/styles/links.css b/docs/examples/styles/links.tcss similarity index 100% rename from docs/examples/styles/links.css rename to docs/examples/styles/links.tcss diff --git a/docs/examples/styles/margin.py b/docs/examples/styles/margin.py index 7551d58656..03cd13d21d 100644 --- a/docs/examples/styles/margin.py +++ b/docs/examples/styles/margin.py @@ -15,4 +15,4 @@ def compose(self): yield Label(TEXT) -app = MarginApp(css_path="margin.css") +app = MarginApp(css_path="margin.tcss") diff --git a/docs/examples/styles/margin.css b/docs/examples/styles/margin.tcss similarity index 100% rename from docs/examples/styles/margin.css rename to docs/examples/styles/margin.tcss diff --git a/docs/examples/styles/margin_all.py b/docs/examples/styles/margin_all.py index b88705f263..11d6ae3fad 100644 --- a/docs/examples/styles/margin_all.py +++ b/docs/examples/styles/margin_all.py @@ -17,4 +17,4 @@ def compose(self): ) -app = MarginAllApp(css_path="margin_all.css") +app = MarginAllApp(css_path="margin_all.tcss") diff --git a/docs/examples/styles/margin_all.css b/docs/examples/styles/margin_all.tcss similarity index 100% rename from docs/examples/styles/margin_all.css rename to docs/examples/styles/margin_all.tcss diff --git a/docs/examples/styles/max_height.py b/docs/examples/styles/max_height.py index 7f3a00fdae..b0b0bce391 100644 --- a/docs/examples/styles/max_height.py +++ b/docs/examples/styles/max_height.py @@ -13,4 +13,4 @@ def compose(self): ) -app = MaxHeightApp(css_path="max_height.css") +app = MaxHeightApp(css_path="max_height.tcss") diff --git a/docs/examples/styles/max_height.css b/docs/examples/styles/max_height.tcss similarity index 100% rename from docs/examples/styles/max_height.css rename to docs/examples/styles/max_height.tcss diff --git a/docs/examples/styles/max_width.py b/docs/examples/styles/max_width.py index 7ea482dc71..c944ff795b 100644 --- a/docs/examples/styles/max_width.py +++ b/docs/examples/styles/max_width.py @@ -13,4 +13,4 @@ def compose(self): ) -app = MaxWidthApp(css_path="max_width.css") +app = MaxWidthApp(css_path="max_width.tcss") diff --git a/docs/examples/styles/max_width.css b/docs/examples/styles/max_width.tcss similarity index 100% rename from docs/examples/styles/max_width.css rename to docs/examples/styles/max_width.tcss diff --git a/docs/examples/styles/min_height.py b/docs/examples/styles/min_height.py index b6e02dedaf..6df7b24522 100644 --- a/docs/examples/styles/min_height.py +++ b/docs/examples/styles/min_height.py @@ -13,4 +13,4 @@ def compose(self): ) -app = MinHeightApp(css_path="min_height.css") +app = MinHeightApp(css_path="min_height.tcss") diff --git a/docs/examples/styles/min_height.css b/docs/examples/styles/min_height.tcss similarity index 100% rename from docs/examples/styles/min_height.css rename to docs/examples/styles/min_height.tcss diff --git a/docs/examples/styles/min_width.py b/docs/examples/styles/min_width.py index b008812660..197dbe40e0 100644 --- a/docs/examples/styles/min_width.py +++ b/docs/examples/styles/min_width.py @@ -13,4 +13,4 @@ def compose(self): ) -app = MinWidthApp(css_path="min_width.css") +app = MinWidthApp(css_path="min_width.tcss") diff --git a/docs/examples/styles/min_width.css b/docs/examples/styles/min_width.tcss similarity index 100% rename from docs/examples/styles/min_width.css rename to docs/examples/styles/min_width.tcss diff --git a/docs/examples/styles/offset.py b/docs/examples/styles/offset.py index 01ceccc421..5593f9e9af 100644 --- a/docs/examples/styles/offset.py +++ b/docs/examples/styles/offset.py @@ -9,4 +9,4 @@ def compose(self): yield Label("Chani (offset 0 -3)", classes="chani") -app = OffsetApp(css_path="offset.css") +app = OffsetApp(css_path="offset.tcss") diff --git a/docs/examples/styles/offset.css b/docs/examples/styles/offset.tcss similarity index 100% rename from docs/examples/styles/offset.css rename to docs/examples/styles/offset.tcss diff --git a/docs/examples/styles/opacity.py b/docs/examples/styles/opacity.py index 5a079fb642..e3cdd1db7c 100644 --- a/docs/examples/styles/opacity.py +++ b/docs/examples/styles/opacity.py @@ -11,4 +11,4 @@ def compose(self): yield Label("opacity: 100%", id="full-opacity") -app = OpacityApp(css_path="opacity.css") +app = OpacityApp(css_path="opacity.tcss") diff --git a/docs/examples/styles/opacity.css b/docs/examples/styles/opacity.tcss similarity index 100% rename from docs/examples/styles/opacity.css rename to docs/examples/styles/opacity.tcss diff --git a/docs/examples/styles/outline.py b/docs/examples/styles/outline.py index 5f82c85dc0..b2e679a0b6 100644 --- a/docs/examples/styles/outline.py +++ b/docs/examples/styles/outline.py @@ -1,7 +1,6 @@ from textual.app import App from textual.widgets import Label - TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. @@ -16,4 +15,4 @@ def compose(self): yield Label(TEXT) -app = OutlineApp(css_path="outline.css") +app = OutlineApp(css_path="outline.tcss") diff --git a/docs/examples/styles/outline.css b/docs/examples/styles/outline.tcss similarity index 100% rename from docs/examples/styles/outline.css rename to docs/examples/styles/outline.tcss diff --git a/docs/examples/styles/outline_all.py b/docs/examples/styles/outline_all.py index 5c7a5f445a..c64645e98f 100644 --- a/docs/examples/styles/outline_all.py +++ b/docs/examples/styles/outline_all.py @@ -23,4 +23,5 @@ def compose(self): Label("wide", id="wide"), ) -app = AllOutlinesApp(css_path="outline_all.css") + +app = AllOutlinesApp(css_path="outline_all.tcss") diff --git a/docs/examples/styles/outline_all.css b/docs/examples/styles/outline_all.tcss similarity index 100% rename from docs/examples/styles/outline_all.css rename to docs/examples/styles/outline_all.tcss diff --git a/docs/examples/styles/outline_vs_border.py b/docs/examples/styles/outline_vs_border.py index 62b072ebd0..80e656bcf5 100644 --- a/docs/examples/styles/outline_vs_border.py +++ b/docs/examples/styles/outline_vs_border.py @@ -1,7 +1,6 @@ from textual.app import App from textual.widgets import Label - TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. @@ -18,4 +17,4 @@ def compose(self): yield Label(TEXT, classes="outline border") -app = OutlineBorderApp(css_path="outline_vs_border.css") +app = OutlineBorderApp(css_path="outline_vs_border.tcss") diff --git a/docs/examples/styles/outline_vs_border.css b/docs/examples/styles/outline_vs_border.tcss similarity index 100% rename from docs/examples/styles/outline_vs_border.css rename to docs/examples/styles/outline_vs_border.tcss diff --git a/docs/examples/styles/overflow.py b/docs/examples/styles/overflow.py index debe0252d3..9fe7cf9253 100644 --- a/docs/examples/styles/overflow.py +++ b/docs/examples/styles/overflow.py @@ -19,4 +19,4 @@ def compose(self): ) -app = OverflowApp(css_path="overflow.css") +app = OverflowApp(css_path="overflow.tcss") diff --git a/docs/examples/styles/overflow.css b/docs/examples/styles/overflow.tcss similarity index 100% rename from docs/examples/styles/overflow.css rename to docs/examples/styles/overflow.tcss diff --git a/docs/examples/styles/padding.py b/docs/examples/styles/padding.py index 13c43381ab..e6ed1a9f6d 100644 --- a/docs/examples/styles/padding.py +++ b/docs/examples/styles/padding.py @@ -15,4 +15,4 @@ def compose(self): yield Label(TEXT) -app = PaddingApp(css_path="padding.css") +app = PaddingApp(css_path="padding.tcss") diff --git a/docs/examples/styles/padding.css b/docs/examples/styles/padding.tcss similarity index 100% rename from docs/examples/styles/padding.css rename to docs/examples/styles/padding.tcss diff --git a/docs/examples/styles/padding_all.py b/docs/examples/styles/padding_all.py index f9387ed55b..c857c26c1a 100644 --- a/docs/examples/styles/padding_all.py +++ b/docs/examples/styles/padding_all.py @@ -17,4 +17,4 @@ def compose(self): ) -app = PaddingAllApp(css_path="padding_all.css") +app = PaddingAllApp(css_path="padding_all.tcss") diff --git a/docs/examples/styles/padding_all.css b/docs/examples/styles/padding_all.tcss similarity index 100% rename from docs/examples/styles/padding_all.css rename to docs/examples/styles/padding_all.tcss diff --git a/docs/examples/styles/row_span.py b/docs/examples/styles/row_span.py index 826dc13ebe..adfca09099 100644 --- a/docs/examples/styles/row_span.py +++ b/docs/examples/styles/row_span.py @@ -16,4 +16,4 @@ def compose(self): ) -app = MyApp(css_path="row_span.css") +app = MyApp(css_path="row_span.tcss") diff --git a/docs/examples/styles/row_span.css b/docs/examples/styles/row_span.tcss similarity index 100% rename from docs/examples/styles/row_span.css rename to docs/examples/styles/row_span.tcss diff --git a/docs/examples/styles/scrollbar_corner_color.py b/docs/examples/styles/scrollbar_corner_color.py index 9e20fedbb8..4247099adb 100644 --- a/docs/examples/styles/scrollbar_corner_color.py +++ b/docs/examples/styles/scrollbar_corner_color.py @@ -16,4 +16,4 @@ def compose(self): yield Label(TEXT.replace("\n", " ") + "\n" + TEXT * 10) -app = ScrollbarCornerColorApp(css_path="scrollbar_corner_color.css") +app = ScrollbarCornerColorApp(css_path="scrollbar_corner_color.tcss") diff --git a/docs/examples/styles/scrollbar_corner_color.css b/docs/examples/styles/scrollbar_corner_color.tcss similarity index 100% rename from docs/examples/styles/scrollbar_corner_color.css rename to docs/examples/styles/scrollbar_corner_color.tcss diff --git a/docs/examples/styles/scrollbar_gutter.py b/docs/examples/styles/scrollbar_gutter.py index b847b3434b..42bc81d495 100644 --- a/docs/examples/styles/scrollbar_gutter.py +++ b/docs/examples/styles/scrollbar_gutter.py @@ -15,4 +15,4 @@ def compose(self): yield Static(TEXT, id="text-box") -app = ScrollbarGutterApp(css_path="scrollbar_gutter.css") +app = ScrollbarGutterApp(css_path="scrollbar_gutter.tcss") diff --git a/docs/examples/styles/scrollbar_gutter.css b/docs/examples/styles/scrollbar_gutter.tcss similarity index 100% rename from docs/examples/styles/scrollbar_gutter.css rename to docs/examples/styles/scrollbar_gutter.tcss diff --git a/docs/examples/styles/scrollbar_size.py b/docs/examples/styles/scrollbar_size.py index 971a65a2ac..0191a1a111 100644 --- a/docs/examples/styles/scrollbar_size.py +++ b/docs/examples/styles/scrollbar_size.py @@ -17,4 +17,4 @@ def compose(self): yield ScrollableContainer(Label(TEXT * 5), classes="panel") -app = ScrollbarApp(css_path="scrollbar_size.css") +app = ScrollbarApp(css_path="scrollbar_size.tcss") diff --git a/docs/examples/styles/scrollbar_size.css b/docs/examples/styles/scrollbar_size.tcss similarity index 84% rename from docs/examples/styles/scrollbar_size.css rename to docs/examples/styles/scrollbar_size.tcss index 4165f287ca..95ac3171f7 100644 --- a/docs/examples/styles/scrollbar_size.css +++ b/docs/examples/styles/scrollbar_size.tcss @@ -11,5 +11,5 @@ Label { .panel { scrollbar-size: 10 4; - padding: 1 2; -} + padding: 1 2; +} diff --git a/docs/examples/styles/scrollbar_size2.py b/docs/examples/styles/scrollbar_size2.py index 88dbba2e43..d7c9c55e98 100644 --- a/docs/examples/styles/scrollbar_size2.py +++ b/docs/examples/styles/scrollbar_size2.py @@ -21,6 +21,6 @@ def compose(self): ) -app = ScrollbarApp(css_path="scrollbar_size2.css") +app = ScrollbarApp(css_path="scrollbar_size2.tcss") if __name__ == "__main__": app.run() diff --git a/docs/examples/styles/scrollbar_size2.css b/docs/examples/styles/scrollbar_size2.tcss similarity index 100% rename from docs/examples/styles/scrollbar_size2.css rename to docs/examples/styles/scrollbar_size2.tcss diff --git a/docs/examples/styles/scrollbars.py b/docs/examples/styles/scrollbars.py index 3a6a45570b..2762313b5e 100644 --- a/docs/examples/styles/scrollbars.py +++ b/docs/examples/styles/scrollbars.py @@ -20,6 +20,6 @@ def compose(self): ) -app = ScrollbarApp(css_path="scrollbars.css") +app = ScrollbarApp(css_path="scrollbars.tcss") if __name__ == "__main__": app.run() diff --git a/docs/examples/styles/scrollbars.css b/docs/examples/styles/scrollbars.tcss similarity index 100% rename from docs/examples/styles/scrollbars.css rename to docs/examples/styles/scrollbars.tcss diff --git a/docs/examples/styles/scrollbars2.py b/docs/examples/styles/scrollbars2.py index 988b871300..be26ca4c00 100644 --- a/docs/examples/styles/scrollbars2.py +++ b/docs/examples/styles/scrollbars2.py @@ -16,4 +16,4 @@ def compose(self): yield Label(TEXT * 10) -app = Scrollbar2App(css_path="scrollbars2.css") +app = Scrollbar2App(css_path="scrollbars2.tcss") diff --git a/docs/examples/styles/scrollbars2.css b/docs/examples/styles/scrollbars2.tcss similarity index 100% rename from docs/examples/styles/scrollbars2.css rename to docs/examples/styles/scrollbars2.tcss diff --git a/docs/examples/styles/text_align.py b/docs/examples/styles/text_align.py index 0c72a17f36..3608f2cfe6 100644 --- a/docs/examples/styles/text_align.py +++ b/docs/examples/styles/text_align.py @@ -19,4 +19,4 @@ def compose(self): ) -app = TextAlign(css_path="text_align.css") +app = TextAlign(css_path="text_align.tcss") diff --git a/docs/examples/styles/text_align.css b/docs/examples/styles/text_align.tcss similarity index 100% rename from docs/examples/styles/text_align.css rename to docs/examples/styles/text_align.tcss diff --git a/docs/examples/styles/text_opacity.py b/docs/examples/styles/text_opacity.py index 351093e670..f34340c2dd 100644 --- a/docs/examples/styles/text_opacity.py +++ b/docs/examples/styles/text_opacity.py @@ -11,4 +11,4 @@ def compose(self): yield Label("text-opacity: 100%", id="full-opacity") -app = TextOpacityApp(css_path="text_opacity.css") +app = TextOpacityApp(css_path="text_opacity.tcss") diff --git a/docs/examples/styles/text_opacity.css b/docs/examples/styles/text_opacity.tcss similarity index 100% rename from docs/examples/styles/text_opacity.css rename to docs/examples/styles/text_opacity.tcss diff --git a/docs/examples/styles/text_style.py b/docs/examples/styles/text_style.py index 6dd1476ebe..01f7610d2f 100644 --- a/docs/examples/styles/text_style.py +++ b/docs/examples/styles/text_style.py @@ -17,4 +17,4 @@ def compose(self): yield Label(TEXT, id="lbl3") -app = TextStyleApp(css_path="text_style.css") +app = TextStyleApp(css_path="text_style.tcss") diff --git a/docs/examples/styles/text_style.css b/docs/examples/styles/text_style.tcss similarity index 86% rename from docs/examples/styles/text_style.css rename to docs/examples/styles/text_style.tcss index b0a4041ba0..93ecbd2525 100644 --- a/docs/examples/styles/text_style.css +++ b/docs/examples/styles/text_style.tcss @@ -1,9 +1,9 @@ Screen { - layout: horizontal; + layout: horizontal; } Label { width: 1fr; -} +} #lbl1 { background: red 30%; text-style: bold; diff --git a/docs/examples/styles/text_style_all.py b/docs/examples/styles/text_style_all.py index 9bb21b0625..c4533a7f6e 100644 --- a/docs/examples/styles/text_style_all.py +++ b/docs/examples/styles/text_style_all.py @@ -25,4 +25,4 @@ def compose(self): ) -app = AllTextStyleApp(css_path="text_style_all.css") +app = AllTextStyleApp(css_path="text_style_all.tcss") diff --git a/docs/examples/styles/text_style_all.css b/docs/examples/styles/text_style_all.tcss similarity index 100% rename from docs/examples/styles/text_style_all.css rename to docs/examples/styles/text_style_all.tcss diff --git a/docs/examples/styles/tint.py b/docs/examples/styles/tint.py index a77bc4a5e1..ea512b9226 100644 --- a/docs/examples/styles/tint.py +++ b/docs/examples/styles/tint.py @@ -12,4 +12,4 @@ def compose(self): yield widget -app = TintApp(css_path="tint.css") +app = TintApp(css_path="tint.tcss") diff --git a/docs/examples/styles/tint.css b/docs/examples/styles/tint.tcss similarity index 100% rename from docs/examples/styles/tint.css rename to docs/examples/styles/tint.tcss diff --git a/docs/examples/styles/visibility.py b/docs/examples/styles/visibility.py index b9f7902435..fe67aa31c8 100644 --- a/docs/examples/styles/visibility.py +++ b/docs/examples/styles/visibility.py @@ -9,4 +9,4 @@ def compose(self): yield Label("Widget 3") -app = VisibilityApp(css_path="visibility.css") +app = VisibilityApp(css_path="visibility.tcss") diff --git a/docs/examples/styles/visibility.css b/docs/examples/styles/visibility.tcss similarity index 100% rename from docs/examples/styles/visibility.css rename to docs/examples/styles/visibility.tcss diff --git a/docs/examples/styles/visibility_containers.py b/docs/examples/styles/visibility_containers.py index a94de145d9..8be5633867 100644 --- a/docs/examples/styles/visibility_containers.py +++ b/docs/examples/styles/visibility_containers.py @@ -27,4 +27,4 @@ def compose(self): ) -app = VisibilityContainersApp(css_path="visibility_containers.css") +app = VisibilityContainersApp(css_path="visibility_containers.tcss") diff --git a/docs/examples/styles/visibility_containers.css b/docs/examples/styles/visibility_containers.tcss similarity index 100% rename from docs/examples/styles/visibility_containers.css rename to docs/examples/styles/visibility_containers.tcss diff --git a/docs/examples/styles/width.py b/docs/examples/styles/width.py index d70868231a..736f527495 100644 --- a/docs/examples/styles/width.py +++ b/docs/examples/styles/width.py @@ -7,4 +7,4 @@ def compose(self): yield Widget() -app = WidthApp(css_path="width.css") +app = WidthApp(css_path="width.tcss") diff --git a/docs/examples/styles/width.css b/docs/examples/styles/width.tcss similarity index 71% rename from docs/examples/styles/width.css rename to docs/examples/styles/width.tcss index 0f067e2363..1fca93ce30 100644 --- a/docs/examples/styles/width.css +++ b/docs/examples/styles/width.tcss @@ -1,4 +1,4 @@ -Screen > Widget { +Screen > Widget { background: green; width: 50%; color: white; diff --git a/docs/examples/styles/width_comparison.py b/docs/examples/styles/width_comparison.py index f801bde4af..509479b155 100644 --- a/docs/examples/styles/width_comparison.py +++ b/docs/examples/styles/width_comparison.py @@ -25,6 +25,6 @@ def compose(self): yield Ruler() -app = WidthComparisonApp(css_path="width_comparison.css") +app = WidthComparisonApp(css_path="width_comparison.tcss") if __name__ == "__main__": app.run() diff --git a/docs/examples/styles/width_comparison.css b/docs/examples/styles/width_comparison.tcss similarity index 100% rename from docs/examples/styles/width_comparison.css rename to docs/examples/styles/width_comparison.tcss diff --git a/docs/examples/tutorial/stopwatch.py b/docs/examples/tutorial/stopwatch.py index af3c3503cb..e1497a67be 100644 --- a/docs/examples/tutorial/stopwatch.py +++ b/docs/examples/tutorial/stopwatch.py @@ -71,7 +71,7 @@ def compose(self) -> ComposeResult: class StopwatchApp(App): """A Textual app to manage stopwatches.""" - CSS_PATH = "stopwatch.css" + CSS_PATH = "stopwatch.tcss" BINDINGS = [ ("d", "toggle_dark", "Toggle dark mode"), diff --git a/docs/examples/tutorial/stopwatch.css b/docs/examples/tutorial/stopwatch.tcss similarity index 100% rename from docs/examples/tutorial/stopwatch.css rename to docs/examples/tutorial/stopwatch.tcss diff --git a/docs/examples/tutorial/stopwatch02.css b/docs/examples/tutorial/stopwatch02.tcss similarity index 100% rename from docs/examples/tutorial/stopwatch02.css rename to docs/examples/tutorial/stopwatch02.tcss diff --git a/docs/examples/tutorial/stopwatch03.py b/docs/examples/tutorial/stopwatch03.py index 8e1fcfb140..7ade4dd59d 100644 --- a/docs/examples/tutorial/stopwatch03.py +++ b/docs/examples/tutorial/stopwatch03.py @@ -21,7 +21,7 @@ def compose(self) -> ComposeResult: class StopwatchApp(App): """A Textual app to manage stopwatches.""" - CSS_PATH = "stopwatch03.css" + CSS_PATH = "stopwatch03.tcss" BINDINGS = [("d", "toggle_dark", "Toggle dark mode")] def compose(self) -> ComposeResult: diff --git a/docs/examples/tutorial/stopwatch03.css b/docs/examples/tutorial/stopwatch03.tcss similarity index 100% rename from docs/examples/tutorial/stopwatch03.css rename to docs/examples/tutorial/stopwatch03.tcss diff --git a/docs/examples/tutorial/stopwatch04.py b/docs/examples/tutorial/stopwatch04.py index 36acc023c9..65f75ea68c 100644 --- a/docs/examples/tutorial/stopwatch04.py +++ b/docs/examples/tutorial/stopwatch04.py @@ -28,7 +28,7 @@ def compose(self) -> ComposeResult: class StopwatchApp(App): """A Textual app to manage stopwatches.""" - CSS_PATH = "stopwatch04.css" + CSS_PATH = "stopwatch04.tcss" BINDINGS = [("d", "toggle_dark", "Toggle dark mode")] def compose(self) -> ComposeResult: diff --git a/docs/examples/tutorial/stopwatch04.css b/docs/examples/tutorial/stopwatch04.tcss similarity index 100% rename from docs/examples/tutorial/stopwatch04.css rename to docs/examples/tutorial/stopwatch04.tcss diff --git a/docs/examples/tutorial/stopwatch05.py b/docs/examples/tutorial/stopwatch05.py index fee7691d4e..19f6366f77 100644 --- a/docs/examples/tutorial/stopwatch05.py +++ b/docs/examples/tutorial/stopwatch05.py @@ -48,7 +48,7 @@ def compose(self) -> ComposeResult: class StopwatchApp(App): """A Textual app to manage stopwatches.""" - CSS_PATH = "stopwatch04.css" + CSS_PATH = "stopwatch04.tcss" BINDINGS = [("d", "toggle_dark", "Toggle dark mode")] def compose(self) -> ComposeResult: diff --git a/docs/examples/tutorial/stopwatch06.py b/docs/examples/tutorial/stopwatch06.py index b78864c874..ee5db13267 100644 --- a/docs/examples/tutorial/stopwatch06.py +++ b/docs/examples/tutorial/stopwatch06.py @@ -71,7 +71,7 @@ def compose(self) -> ComposeResult: class StopwatchApp(App): """A Textual app to manage stopwatches.""" - CSS_PATH = "stopwatch04.css" + CSS_PATH = "stopwatch04.tcss" BINDINGS = [("d", "toggle_dark", "Toggle dark mode")] def compose(self) -> ComposeResult: diff --git a/docs/examples/widgets/button.py b/docs/examples/widgets/button.py index 09339ccb0b..afed67ac9c 100644 --- a/docs/examples/widgets/button.py +++ b/docs/examples/widgets/button.py @@ -4,7 +4,7 @@ class ButtonsApp(App[str]): - CSS_PATH = "button.css" + CSS_PATH = "button.tcss" def compose(self) -> ComposeResult: yield Horizontal( diff --git a/docs/examples/widgets/button.css b/docs/examples/widgets/button.tcss similarity index 100% rename from docs/examples/widgets/button.css rename to docs/examples/widgets/button.tcss diff --git a/docs/examples/widgets/checkbox.py b/docs/examples/widgets/checkbox.py index 75eadda0cf..b31d1afb04 100644 --- a/docs/examples/widgets/checkbox.py +++ b/docs/examples/widgets/checkbox.py @@ -4,7 +4,7 @@ class CheckboxApp(App[None]): - CSS_PATH = "checkbox.css" + CSS_PATH = "checkbox.tcss" def compose(self) -> ComposeResult: with VerticalScroll(): diff --git a/docs/examples/widgets/checkbox.css b/docs/examples/widgets/checkbox.tcss similarity index 100% rename from docs/examples/widgets/checkbox.css rename to docs/examples/widgets/checkbox.tcss diff --git a/docs/examples/widgets/collapsible.py b/docs/examples/widgets/collapsible.py new file mode 100644 index 0000000000..d34ebca403 --- /dev/null +++ b/docs/examples/widgets/collapsible.py @@ -0,0 +1,46 @@ +from textual.app import App, ComposeResult +from textual.widgets import Collapsible, Footer, Label, Markdown + +LETO = """\ +# Duke Leto I Atreides + +Head of House Atreides.""" + +JESSICA = """ +# Lady Jessica + +Bene Gesserit and concubine of Leto, and mother of Paul and Alia. +""" + +PAUL = """ +# Paul Atreides + +Son of Leto and Jessica. +""" + + +class CollapsibleApp(App[None]): + """An example of collapsible container.""" + + BINDINGS = [ + ("c", "collapse_or_expand(True)", "Collapse All"), + ("e", "collapse_or_expand(False)", "Expand All"), + ] + + def compose(self) -> ComposeResult: + """Compose app with collapsible containers.""" + yield Footer() + with Collapsible(collapsed=False, title="Leto"): + yield Label(LETO) + yield Collapsible(Markdown(JESSICA), collapsed=False, title="Jessica") + with Collapsible(collapsed=True, title="Paul"): + yield Markdown(PAUL) + + def action_collapse_or_expand(self, collapse: bool) -> None: + for child in self.walk_children(Collapsible): + child.collapsed = collapse + + +if __name__ == "__main__": + app = CollapsibleApp() + app.run() diff --git a/docs/examples/widgets/collapsible_custom_symbol.py b/docs/examples/widgets/collapsible_custom_symbol.py new file mode 100644 index 0000000000..d2fa266aa6 --- /dev/null +++ b/docs/examples/widgets/collapsible_custom_symbol.py @@ -0,0 +1,25 @@ +from textual.app import App, ComposeResult +from textual.containers import Horizontal +from textual.widgets import Collapsible, Label + + +class CollapsibleApp(App[None]): + def compose(self) -> ComposeResult: + with Horizontal(): + with Collapsible( + collapsed_symbol=">>>", + expanded_symbol="v", + ): + yield Label("Hello, world.") + + with Collapsible( + collapsed_symbol=">>>", + expanded_symbol="v", + collapsed=False, + ): + yield Label("Hello, world.") + + +if __name__ == "__main__": + app = CollapsibleApp() + app.run() diff --git a/docs/examples/widgets/collapsible_nested.py b/docs/examples/widgets/collapsible_nested.py new file mode 100644 index 0000000000..d4b65835f7 --- /dev/null +++ b/docs/examples/widgets/collapsible_nested.py @@ -0,0 +1,14 @@ +from textual.app import App, ComposeResult +from textual.widgets import Collapsible, Label + + +class CollapsibleApp(App[None]): + def compose(self) -> ComposeResult: + with Collapsible(collapsed=False): + with Collapsible(): + yield Label("Hello, world.") + + +if __name__ == "__main__": + app = CollapsibleApp() + app.run() diff --git a/docs/examples/widgets/content_switcher.py b/docs/examples/widgets/content_switcher.py index f9197a2996..82cb43aace 100644 --- a/docs/examples/widgets/content_switcher.py +++ b/docs/examples/widgets/content_switcher.py @@ -30,7 +30,7 @@ class ContentSwitcherApp(App[None]): - CSS_PATH = "content_switcher.css" + CSS_PATH = "content_switcher.tcss" def compose(self) -> ComposeResult: with Horizontal(id="buttons"): # (1)! diff --git a/docs/examples/widgets/content_switcher.css b/docs/examples/widgets/content_switcher.tcss similarity index 100% rename from docs/examples/widgets/content_switcher.css rename to docs/examples/widgets/content_switcher.tcss diff --git a/docs/examples/widgets/header_app_title.py b/docs/examples/widgets/header_app_title.py new file mode 100644 index 0000000000..1b433148e2 --- /dev/null +++ b/docs/examples/widgets/header_app_title.py @@ -0,0 +1,16 @@ +from textual.app import App, ComposeResult +from textual.widgets import Header + + +class HeaderApp(App): + def compose(self) -> ComposeResult: + yield Header() + + def on_mount(self) -> None: + self.title = "Header Application" + self.sub_title = "With title and sub-title" + + +if __name__ == "__main__": + app = HeaderApp() + app.run() diff --git a/docs/examples/widgets/horizontal_rules.py b/docs/examples/widgets/horizontal_rules.py new file mode 100644 index 0000000000..643f129bbe --- /dev/null +++ b/docs/examples/widgets/horizontal_rules.py @@ -0,0 +1,27 @@ +from textual.app import App, ComposeResult +from textual.containers import Vertical +from textual.widgets import Label, Rule + + +class HorizontalRulesApp(App): + CSS_PATH = "horizontal_rules.tcss" + + def compose(self) -> ComposeResult: + with Vertical(): + yield Label("solid (default)") + yield Rule() + yield Label("heavy") + yield Rule(line_style="heavy") + yield Label("thick") + yield Rule(line_style="thick") + yield Label("dashed") + yield Rule(line_style="dashed") + yield Label("double") + yield Rule(line_style="double") + yield Label("ascii") + yield Rule(line_style="ascii") + + +if __name__ == "__main__": + app = HorizontalRulesApp() + app.run() diff --git a/docs/examples/widgets/horizontal_rules.tcss b/docs/examples/widgets/horizontal_rules.tcss new file mode 100644 index 0000000000..fad6140e1f --- /dev/null +++ b/docs/examples/widgets/horizontal_rules.tcss @@ -0,0 +1,13 @@ +Screen { + align: center middle; +} + +Vertical { + height: auto; + width: 80%; +} + +Label { + width: 100%; + text-align: center; +} diff --git a/docs/examples/widgets/java_highlights.scm b/docs/examples/widgets/java_highlights.scm new file mode 100644 index 0000000000..b6259be125 --- /dev/null +++ b/docs/examples/widgets/java_highlights.scm @@ -0,0 +1,140 @@ +; 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 + +[ + (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" + "return" + "sealed" + "static" + "strictfp" + "switch" + "synchronized" + "throw" + "throws" + "to" + "transient" + "transitive" + "try" + "uses" + "volatile" + "while" + "with" +] @keyword diff --git a/docs/examples/widgets/list_view.py b/docs/examples/widgets/list_view.py index a1880dc50f..cfd5ad60f2 100644 --- a/docs/examples/widgets/list_view.py +++ b/docs/examples/widgets/list_view.py @@ -1,10 +1,9 @@ from textual.app import App, ComposeResult -from textual.widgets import ListView, ListItem, Label, Footer +from textual.widgets import Footer, Label, ListItem, ListView class ListViewExample(App): - - CSS_PATH = "list_view.css" + CSS_PATH = "list_view.tcss" def compose(self) -> ComposeResult: yield ListView( diff --git a/docs/examples/widgets/list_view.css b/docs/examples/widgets/list_view.tcss similarity index 100% rename from docs/examples/widgets/list_view.css rename to docs/examples/widgets/list_view.tcss diff --git a/docs/examples/widgets/option_list.css b/docs/examples/widgets/option_list.tcss similarity index 100% rename from docs/examples/widgets/option_list.css rename to docs/examples/widgets/option_list.tcss diff --git a/docs/examples/widgets/option_list_options.py b/docs/examples/widgets/option_list_options.py index de9157c1cf..611a7ef088 100644 --- a/docs/examples/widgets/option_list_options.py +++ b/docs/examples/widgets/option_list_options.py @@ -4,7 +4,7 @@ class OptionListApp(App[None]): - CSS_PATH = "option_list.css" + CSS_PATH = "option_list.tcss" def compose(self) -> ComposeResult: yield Header() diff --git a/docs/examples/widgets/option_list_strings.py b/docs/examples/widgets/option_list_strings.py index d170efa571..a475a4eabe 100644 --- a/docs/examples/widgets/option_list_strings.py +++ b/docs/examples/widgets/option_list_strings.py @@ -3,7 +3,7 @@ class OptionListApp(App[None]): - CSS_PATH = "option_list.css" + CSS_PATH = "option_list.tcss" def compose(self) -> ComposeResult: yield Header() diff --git a/docs/examples/widgets/option_list_tables.py b/docs/examples/widgets/option_list_tables.py index ff4f2d0541..fec121b648 100644 --- a/docs/examples/widgets/option_list_tables.py +++ b/docs/examples/widgets/option_list_tables.py @@ -22,7 +22,7 @@ class OptionListApp(App[None]): - CSS_PATH = "option_list.css" + CSS_PATH = "option_list.tcss" @staticmethod def colony(name: str, god: str, population: str, capital: str) -> Table: diff --git a/docs/examples/widgets/placeholder.py b/docs/examples/widgets/placeholder.py index 9c7d6eb0e1..89089b6253 100644 --- a/docs/examples/widgets/placeholder.py +++ b/docs/examples/widgets/placeholder.py @@ -4,7 +4,7 @@ class PlaceholderApp(App): - CSS_PATH = "placeholder.css" + CSS_PATH = "placeholder.tcss" def compose(self) -> ComposeResult: yield VerticalScroll( diff --git a/docs/examples/widgets/placeholder.css b/docs/examples/widgets/placeholder.tcss similarity index 100% rename from docs/examples/widgets/placeholder.css rename to docs/examples/widgets/placeholder.tcss diff --git a/docs/examples/widgets/progress_bar.py b/docs/examples/widgets/progress_bar.py index e75df7ca3a..cb4510eb52 100644 --- a/docs/examples/widgets/progress_bar.py +++ b/docs/examples/widgets/progress_bar.py @@ -4,7 +4,7 @@ class FundingProgressApp(App[None]): - CSS_PATH = "progress_bar.css" + CSS_PATH = "progress_bar.tcss" TITLE = "Funding tracking" diff --git a/docs/examples/widgets/progress_bar.css b/docs/examples/widgets/progress_bar.tcss similarity index 100% rename from docs/examples/widgets/progress_bar.css rename to docs/examples/widgets/progress_bar.tcss diff --git a/docs/examples/widgets/progress_bar_styled.py b/docs/examples/widgets/progress_bar_styled.py index d09e8e8d02..96c5005bab 100644 --- a/docs/examples/widgets/progress_bar_styled.py +++ b/docs/examples/widgets/progress_bar_styled.py @@ -6,7 +6,7 @@ class StyledProgressBar(App[None]): BINDINGS = [("s", "start", "Start")] - CSS_PATH = "progress_bar_styled.css" + CSS_PATH = "progress_bar_styled.tcss" progress_timer: Timer """Timer to simulate progress happening.""" diff --git a/docs/examples/widgets/progress_bar_styled.css b/docs/examples/widgets/progress_bar_styled.tcss similarity index 100% rename from docs/examples/widgets/progress_bar_styled.css rename to docs/examples/widgets/progress_bar_styled.tcss diff --git a/docs/examples/widgets/progress_bar_styled_.py b/docs/examples/widgets/progress_bar_styled_.py index 5a73cc2f46..8428f359a1 100644 --- a/docs/examples/widgets/progress_bar_styled_.py +++ b/docs/examples/widgets/progress_bar_styled_.py @@ -6,7 +6,7 @@ class StyledProgressBar(App[None]): BINDINGS = [("s", "start", "Start")] - CSS_PATH = "progress_bar_styled.css" + CSS_PATH = "progress_bar_styled.tcss" progress_timer: Timer """Timer to simulate progress happening.""" diff --git a/docs/examples/widgets/radio_button.py b/docs/examples/widgets/radio_button.py index 316d89100d..b9383c7099 100644 --- a/docs/examples/widgets/radio_button.py +++ b/docs/examples/widgets/radio_button.py @@ -3,7 +3,7 @@ class RadioChoicesApp(App[None]): - CSS_PATH = "radio_button.css" + CSS_PATH = "radio_button.tcss" def compose(self) -> ComposeResult: with RadioSet(): diff --git a/docs/examples/widgets/radio_button.css b/docs/examples/widgets/radio_button.tcss similarity index 100% rename from docs/examples/widgets/radio_button.css rename to docs/examples/widgets/radio_button.tcss diff --git a/docs/examples/widgets/radio_set.py b/docs/examples/widgets/radio_set.py index c09c9d6be1..e41b94fcac 100644 --- a/docs/examples/widgets/radio_set.py +++ b/docs/examples/widgets/radio_set.py @@ -4,7 +4,7 @@ class RadioChoicesApp(App[None]): - CSS_PATH = "radio_set.css" + CSS_PATH = "radio_set.tcss" def compose(self) -> ComposeResult: with Horizontal(): diff --git a/docs/examples/widgets/radio_set.css b/docs/examples/widgets/radio_set.tcss similarity index 100% rename from docs/examples/widgets/radio_set.css rename to docs/examples/widgets/radio_set.tcss diff --git a/docs/examples/widgets/radio_set_changed.py b/docs/examples/widgets/radio_set_changed.py index 4af563c391..8c890bb6c6 100644 --- a/docs/examples/widgets/radio_set_changed.py +++ b/docs/examples/widgets/radio_set_changed.py @@ -4,7 +4,7 @@ class RadioSetChangedApp(App[None]): - CSS_PATH = "radio_set_changed.css" + CSS_PATH = "radio_set_changed.tcss" def compose(self) -> ComposeResult: with VerticalScroll(): diff --git a/docs/examples/widgets/radio_set_changed.css b/docs/examples/widgets/radio_set_changed.tcss similarity index 100% rename from docs/examples/widgets/radio_set_changed.css rename to docs/examples/widgets/radio_set_changed.tcss diff --git a/docs/examples/widgets/select.css b/docs/examples/widgets/select.tcss similarity index 100% rename from docs/examples/widgets/select.css rename to docs/examples/widgets/select.tcss diff --git a/docs/examples/widgets/select_widget.py b/docs/examples/widgets/select_widget.py index 73b02c25b9..6bf62215b9 100644 --- a/docs/examples/widgets/select_widget.py +++ b/docs/examples/widgets/select_widget.py @@ -10,7 +10,7 @@ class SelectApp(App): - CSS_PATH = "select.css" + CSS_PATH = "select.tcss" def compose(self) -> ComposeResult: yield Header() diff --git a/docs/examples/widgets/selection_list.css b/docs/examples/widgets/selection_list.tcss similarity index 100% rename from docs/examples/widgets/selection_list.css rename to docs/examples/widgets/selection_list.tcss diff --git a/docs/examples/widgets/selection_list_selected.py b/docs/examples/widgets/selection_list_selected.py index 954fb36b11..cd8dbc6a8a 100644 --- a/docs/examples/widgets/selection_list_selected.py +++ b/docs/examples/widgets/selection_list_selected.py @@ -7,7 +7,7 @@ class SelectionListApp(App[None]): - CSS_PATH = "selection_list_selected.css" + CSS_PATH = "selection_list_selected.tcss" def compose(self) -> ComposeResult: yield Header() diff --git a/docs/examples/widgets/selection_list_selected.css b/docs/examples/widgets/selection_list_selected.tcss similarity index 100% rename from docs/examples/widgets/selection_list_selected.css rename to docs/examples/widgets/selection_list_selected.tcss diff --git a/docs/examples/widgets/selection_list_selections.py b/docs/examples/widgets/selection_list_selections.py index 4a5e582a07..68390f0959 100644 --- a/docs/examples/widgets/selection_list_selections.py +++ b/docs/examples/widgets/selection_list_selections.py @@ -4,7 +4,7 @@ class SelectionListApp(App[None]): - CSS_PATH = "selection_list.css" + CSS_PATH = "selection_list.tcss" def compose(self) -> ComposeResult: yield Header() diff --git a/docs/examples/widgets/selection_list_tuples.py b/docs/examples/widgets/selection_list_tuples.py index bff54e69cc..29e1c91d7e 100644 --- a/docs/examples/widgets/selection_list_tuples.py +++ b/docs/examples/widgets/selection_list_tuples.py @@ -3,7 +3,7 @@ class SelectionListApp(App[None]): - CSS_PATH = "selection_list.css" + CSS_PATH = "selection_list.tcss" def compose(self) -> ComposeResult: yield Header() diff --git a/docs/examples/widgets/sparkline.py b/docs/examples/widgets/sparkline.py index 766e3426d5..662bf66641 100644 --- a/docs/examples/widgets/sparkline.py +++ b/docs/examples/widgets/sparkline.py @@ -9,7 +9,7 @@ class SparklineSummaryFunctionApp(App[None]): - CSS_PATH = "sparkline.css" + CSS_PATH = "sparkline.tcss" def compose(self) -> ComposeResult: yield Sparkline(data, summary_function=max) # (1)! diff --git a/docs/examples/widgets/sparkline.css b/docs/examples/widgets/sparkline.tcss similarity index 100% rename from docs/examples/widgets/sparkline.css rename to docs/examples/widgets/sparkline.tcss diff --git a/docs/examples/widgets/sparkline_basic.py b/docs/examples/widgets/sparkline_basic.py index 3f1f0a804d..eb4099a192 100644 --- a/docs/examples/widgets/sparkline_basic.py +++ b/docs/examples/widgets/sparkline_basic.py @@ -5,7 +5,7 @@ class SparklineBasicApp(App[None]): - CSS_PATH = "sparkline_basic.css" + CSS_PATH = "sparkline_basic.tcss" def compose(self) -> ComposeResult: yield Sparkline( # (2)! diff --git a/docs/examples/widgets/sparkline_basic.css b/docs/examples/widgets/sparkline_basic.tcss similarity index 100% rename from docs/examples/widgets/sparkline_basic.css rename to docs/examples/widgets/sparkline_basic.tcss diff --git a/docs/examples/widgets/sparkline_colors.py b/docs/examples/widgets/sparkline_colors.py index bf33317230..d6a4549a6e 100644 --- a/docs/examples/widgets/sparkline_colors.py +++ b/docs/examples/widgets/sparkline_colors.py @@ -5,7 +5,7 @@ class SparklineColorsApp(App[None]): - CSS_PATH = "sparkline_colors.css" + CSS_PATH = "sparkline_colors.tcss" def compose(self) -> ComposeResult: nums = [abs(sin(x / 3.14)) for x in range(0, 360 * 6, 20)] diff --git a/docs/examples/widgets/sparkline_colors.css b/docs/examples/widgets/sparkline_colors.tcss similarity index 100% rename from docs/examples/widgets/sparkline_colors.css rename to docs/examples/widgets/sparkline_colors.tcss diff --git a/docs/examples/widgets/switch.py b/docs/examples/widgets/switch.py index 54a59ad63d..69e79be4e7 100644 --- a/docs/examples/widgets/switch.py +++ b/docs/examples/widgets/switch.py @@ -1,6 +1,6 @@ from textual.app import App, ComposeResult from textual.containers import Horizontal -from textual.widgets import Switch, Static +from textual.widgets import Static, Switch class SwitchApp(App): @@ -30,6 +30,6 @@ def compose(self) -> ComposeResult: ) -app = SwitchApp(css_path="switch.css") +app = SwitchApp(css_path="switch.tcss") if __name__ == "__main__": app.run() diff --git a/docs/examples/widgets/switch.css b/docs/examples/widgets/switch.tcss similarity index 100% rename from docs/examples/widgets/switch.css rename to docs/examples/widgets/switch.tcss diff --git a/docs/examples/widgets/text_area_custom_language.py b/docs/examples/widgets/text_area_custom_language.py new file mode 100644 index 0000000000..70ee7e16b9 --- /dev/null +++ b/docs/examples/widgets/text_area_custom_language.py @@ -0,0 +1,34 @@ +from pathlib import Path + +from tree_sitter_languages import get_language + +from textual.app import App, ComposeResult +from textual.widgets import TextArea + +java_language = get_language("java") +java_highlight_query = (Path(__file__).parent / "java_highlights.scm").read_text() +java_code = """\ +class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello, World!"); + } +} +""" + + +class TextAreaCustomLanguage(App): + def compose(self) -> ComposeResult: + text_area = TextArea(text=java_code) + text_area.cursor_blink = False + + # Register the Java language and highlight query + text_area.register_language(java_language, java_highlight_query) + + # Switch to Java + text_area.language = "java" + yield text_area + + +app = TextAreaCustomLanguage() +if __name__ == "__main__": + app.run() diff --git a/docs/examples/widgets/text_area_custom_theme.py b/docs/examples/widgets/text_area_custom_theme.py new file mode 100644 index 0000000000..c2c81a115f --- /dev/null +++ b/docs/examples/widgets/text_area_custom_theme.py @@ -0,0 +1,42 @@ +from rich.style import Style + +from textual._text_area_theme import TextAreaTheme +from textual.app import App, ComposeResult +from textual.widgets import TextArea + +TEXT = """\ +# says hello +def hello(name): + print("hello" + name) + +# says goodbye +def goodbye(name): + print("goodbye" + name) +""" + +MY_THEME = TextAreaTheme( + # This name will be used to refer to the theme... + name="my_cool_theme", + # Basic styles such as background, cursor, selection, gutter, etc... + cursor_style=Style(color="white", bgcolor="blue"), + cursor_line_style=Style(bgcolor="yellow"), + # `syntax_styles` maps tokens parsed from the document to Rich styles. + syntax_styles={ + "string": Style(color="red"), + "comment": Style(color="magenta"), + }, +) + + +class TextAreaCustomThemes(App): + def compose(self) -> ComposeResult: + text_area = TextArea(TEXT, language="python") + text_area.cursor_blink = False + text_area.register_theme(MY_THEME) + text_area.theme = "my_cool_theme" + yield text_area + + +app = TextAreaCustomThemes() +if __name__ == "__main__": + app.run() diff --git a/docs/examples/widgets/text_area_example.py b/docs/examples/widgets/text_area_example.py new file mode 100644 index 0000000000..2e0e31c060 --- /dev/null +++ b/docs/examples/widgets/text_area_example.py @@ -0,0 +1,20 @@ +from textual.app import App, ComposeResult +from textual.widgets import TextArea + +TEXT = """\ +def hello(name): + print("hello" + name) + +def goodbye(name): + print("goodbye" + name) +""" + + +class TextAreaExample(App): + def compose(self) -> ComposeResult: + yield TextArea(TEXT, language="python") + + +app = TextAreaExample() +if __name__ == "__main__": + app.run() diff --git a/docs/examples/widgets/text_area_extended.py b/docs/examples/widgets/text_area_extended.py new file mode 100644 index 0000000000..8ac237db88 --- /dev/null +++ b/docs/examples/widgets/text_area_extended.py @@ -0,0 +1,23 @@ +from textual import events +from textual.app import App, ComposeResult +from textual.widgets import TextArea + + +class ExtendedTextArea(TextArea): + """A subclass of TextArea with parenthesis-closing functionality.""" + + def _on_key(self, event: events.Key) -> None: + if event.character == "(": + self.insert("()") + self.move_cursor_relative(columns=-1) + event.prevent_default() + + +class TextAreaKeyPressHook(App): + def compose(self) -> ComposeResult: + yield ExtendedTextArea(language="python") + + +app = TextAreaKeyPressHook() +if __name__ == "__main__": + app.run() diff --git a/docs/examples/widgets/text_area_selection.py b/docs/examples/widgets/text_area_selection.py new file mode 100644 index 0000000000..4165eb2d2d --- /dev/null +++ b/docs/examples/widgets/text_area_selection.py @@ -0,0 +1,23 @@ +from textual.app import App, ComposeResult +from textual.widgets import TextArea +from textual.widgets.text_area import Selection + +TEXT = """\ +def hello(name): + print("hello" + name) + +def goodbye(name): + print("goodbye" + name) +""" + + +class TextAreaSelection(App): + def compose(self) -> ComposeResult: + text_area = TextArea(TEXT, language="python") + text_area.selection = Selection(start=(0, 0), end=(2, 0)) # (1)! + yield text_area + + +app = TextAreaSelection() +if __name__ == "__main__": + app.run() diff --git a/docs/examples/widgets/vertical_rules.py b/docs/examples/widgets/vertical_rules.py new file mode 100644 index 0000000000..5001045305 --- /dev/null +++ b/docs/examples/widgets/vertical_rules.py @@ -0,0 +1,27 @@ +from textual.app import App, ComposeResult +from textual.containers import Horizontal +from textual.widgets import Label, Rule + + +class VerticalRulesApp(App): + CSS_PATH = "vertical_rules.tcss" + + def compose(self) -> ComposeResult: + with Horizontal(): + yield Label("solid") + yield Rule(orientation="vertical") + yield Label("heavy") + yield Rule(orientation="vertical", line_style="heavy") + yield Label("thick") + yield Rule(orientation="vertical", line_style="thick") + yield Label("dashed") + yield Rule(orientation="vertical", line_style="dashed") + yield Label("double") + yield Rule(orientation="vertical", line_style="double") + yield Label("ascii") + yield Rule(orientation="vertical", line_style="ascii") + + +if __name__ == "__main__": + app = VerticalRulesApp() + app.run() diff --git a/docs/examples/widgets/vertical_rules.tcss b/docs/examples/widgets/vertical_rules.tcss new file mode 100644 index 0000000000..f2148af1c0 --- /dev/null +++ b/docs/examples/widgets/vertical_rules.tcss @@ -0,0 +1,14 @@ +Screen { + align: center middle; +} + +Horizontal { + width: auto; + height: 80%; +} + +Label { + width: 6; + height: 100%; + text-align: center; +} diff --git a/docs/guide/CSS.md b/docs/guide/CSS.md index 49d297b8fd..aad037cba6 100644 --- a/docs/guide/CSS.md +++ b/docs/guide/CSS.md @@ -2,13 +2,13 @@ Textual uses CSS to apply style to widgets. If you have any exposure to web development you will have encountered CSS, but don't worry if you haven't: this chapter will get you up to speed. -## Stylesheets +!!! tip "VSCode User?" -CSS stands for _Cascading Stylesheets_. A stylesheet is a list of styles and rules about how those styles should be applied to a web page. In the case of Textual, the stylesheet applies [styles](./styles.md) to widgets, but otherwise it is the same idea. + The official [Textual CSS](https://marketplace.visualstudio.com/items?itemName=Textualize.textual-syntax-highlighter) extension adds syntax highlighting for both external files and inline CSS. -When Textual loads CSS it sets attributes on your widgets' `style` object. The effect is the same as if you had set attributes in Python. +## Stylesheets -CSS is typically stored in an external file with the extension `.css` alongside your Python code. +CSS stands for _Cascading Stylesheet_. A stylesheet is a list of styles and rules about how those styles should be applied to a web page. In the case of Textual, the stylesheet applies [styles](./styles.md) to widgets, but otherwise it is the same idea. Let's look at some Textual CSS. @@ -52,6 +52,7 @@ The lines inside the curly braces contains CSS _rules_, which consist of a rule The first rule in the above example reads `"dock: top;"`. The rule name is `dock` which tells Textual to place the widget on an edge of the screen. The text after the colon is `top` which tells Textual to dock to the _top_ of the screen. Other valid values for `dock` are "right", "bottom", or "left"; but "top" is most appropriate for a header. + ## The DOM The DOM, or _Document Object Model_, is a term borrowed from the web world. Textual doesn't use documents but the term has stuck. In Textual CSS, the DOM is an arrangement of widgets you can visualize as a tree-like structure. @@ -112,11 +113,10 @@ To further explore the DOM, we're going to build a simple dialog with a question - `textual.widgets.Static` For simple content. - `textual.widgets.Button` For a clickable button. -=== "dom3.py" - ```python hl_lines="12 13 14 15 16 17 18 19 20" - --8<-- "docs/examples/guide/dom3.py" - ``` +```python hl_lines="12 13 14 15 16 17 18 19 20" title="dom3.py" +--8<-- "docs/examples/guide/dom3.py" +``` We've added a Container to our DOM which (as the name suggests) is a container for other widgets. The container has a number of other widgets passed as positional arguments which will be added as the children of the container. Not all widgets accept child widgets in this way. A Button widget doesn't require any children, for example. @@ -138,7 +138,13 @@ You may recognize some elements in the above screenshot, but it doesn't quite lo To add a stylesheet set the `CSS_PATH` classvar to a relative path: -```python hl_lines="9" + +!!! note + + Textual CSS files are typically given the extension `.tcss` to differentiate them from browser CSS (`.css`). + + +```python hl_lines="9" title="dom4.py" --8<-- "docs/examples/guide/dom4.py" ``` @@ -147,8 +153,8 @@ These are used by the CSS to identify parts of the DOM. We will cover these in t Here's the CSS file we are applying: -```sass ---8<-- "docs/examples/guide/dom4.css" +```sass title="dom4.tcss" +--8<-- "docs/examples/guide/dom4.tcss" ``` The CSS contains a number of rule sets with a selector and a list of rules. You can also add comments with text between `/*` and `*/` which will be ignored by Textual. Add comments to leave yourself reminders or to temporarily disable selectors. @@ -318,7 +324,9 @@ Here are some other pseudo classes: - `: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. -- `:focus-within` Matches widgets with a focused a child widget. +- `:focus-within` Matches widgets with a focused child widget. +- `:dark` Matches widgets in dark mode (where `App.dark == True`). +- `:light` Matches widgets in dark mode (where `App.dark == False`). ## Combinators diff --git a/docs/guide/actions.md b/docs/guide/actions.md index 6b193df1db..a59440a359 100644 --- a/docs/guide/actions.md +++ b/docs/guide/actions.md @@ -104,10 +104,10 @@ The following example defines a custom widget with its own `set_background` acti --8<-- "docs/examples/guide/actions/actions05.py" ``` -=== "actions05.css" +=== "actions05.tcss" - ```sass title="actions05.css" - --8<-- "docs/examples/guide/actions/actions05.css" + ```sass title="actions05.tcss" + --8<-- "docs/examples/guide/actions/actions05.tcss" ``` There are two instances of the custom widget mounted. If you click the links in either of them it will changed the background for that widget only. The ++r++, ++g++, and ++b++ key bindings are set on the App so will set the background for the screen. diff --git a/docs/guide/app.md b/docs/guide/app.md index 5a5a48265f..648847ec28 100644 --- a/docs/guide/app.md +++ b/docs/guide/app.md @@ -204,11 +204,50 @@ The addition of `[str]` tells mypy that `run()` is expected to return a string. Type annotations are entirely optional (but recommended) with Textual. +### Return code + +When you exit a Textual app with [`App.exit()`][textual.app.App.exit], you can optionally specify a *return code* with the `return_code` parameter. + + +!!! info "What are return codes?" + + Returns codes are a standard feature provided by your operating system. + When any application exits it can return an integer to indicate if it was successful or not. + A return code of `0` indicates success, any other value indicates that an error occurred. + The exact meaning of a non-zero return code is application-dependant. + +When a Textual app exits normally, the return code will be `0`. If there is an unhandled exception, Textual will set a return code of `1`. +You may want to set a different value for the return code if there is error condition that you want to differentiate from an unhandled exception. + +Here's an example of setting a return code for an error condition: + +```python +if critical_error: + self.exit(return_code=4, message="Critical error occurred") +``` + +The app's return code can be queried with `app.return_code`, which will be `None` if it hasn't been set, or an integer. + +Textual won't explicitly exit the process. +To exit the app with a return code, you should call `sys.exit`. +Here's how you might do that: + +```python +if __name__ == "__main__" + app = MyApp() + app.run() + import sys + sys.exit(app.return_code or 0) +``` ## CSS Textual apps can reference [CSS](CSS.md) files which define how your app and widgets will look, while keeping your Python code free of display related code (which tends to be messy). +!!! info + + Textual apps typically use the extension `.tcss` for external CSS files to differentiate them from browser (`.css`) files. + The chapter on [Textual CSS](CSS.md) describes how to use CSS in detail. For now let's look at how your app references external CSS files. The following example enables loading of CSS by adding a `CSS_PATH` class variable: @@ -221,13 +260,13 @@ The following example enables loading of CSS by adding a `CSS_PATH` class variab We also added an `id` to the `Label`, because we want to style it in the CSS. -If the path is relative (as it is above) then it is taken as relative to where the app is defined. Hence this example references `"question01.css"` in the same directory as the Python code. Here is that CSS file: +If the path is relative (as it is above) then it is taken as relative to where the app is defined. Hence this example references `"question01.tcss"` in the same directory as the Python code. Here is that CSS file: -```sass title="question02.css" ---8<-- "docs/examples/app/question02.css" +```sass title="question02.tcss" +--8<-- "docs/examples/app/question02.tcss" ``` -When `"question02.py"` runs it will load `"question02.css"` and update the app and widgets accordingly. Even though the code is almost identical to the previous sample, the app now looks quite different: +When `"question02.py"` runs it will load `"question02.tcss"` and update the app and widgets accordingly. Even though the code is almost identical to the previous sample, the app now looks quite different: ```{.textual path="docs/examples/app/question02.py"} ``` diff --git a/docs/guide/command_palette.md b/docs/guide/command_palette.md new file mode 100644 index 0000000000..0dd15af0f1 --- /dev/null +++ b/docs/guide/command_palette.md @@ -0,0 +1,124 @@ +# Command Palette + +Textual apps have a built-in *command palette*, which gives users a quick way to access certain functionality within your app. + +In this chapter we will explain what a command palette is, how to use it, and how you can add your own commands. + +## Launching the command palette + +Press ++ctrl++ + `\` (ctrl and backslash) to invoke the command palette screen, which contains of a single input widget. +Textual will suggest commands as you type in that input. +Press ++up++ or ++down++ to select a command from the list, and ++enter++ to invoke it. + +Commands are looked up via a *fuzzy* search, which means Textual will show commands that match the keys you type in the same order, but not necessarily at the start of the command. +For instance the "Toggle light/dark mode" command will be shown if you type "to" (for **to**ggle), but you could also type "dm" (to match **d**ark **m**ode). +This scheme allows the user to quickly get to a particular command with a minimum of key-presses. + + +=== "Command Palette" + + ```{.textual path="docs/examples/guide/command_palette/command01.py" press="ctrl+backslash"} + ``` + +=== "Command Palette after 't'" + + ```{.textual path="docs/examples/guide/command_palette/command01.py" press="ctrl+backslash,t"} + ``` + +=== "Command Palette after 'td'" + + ```{.textual path="docs/examples/guide/command_palette/command01.py" press="ctrl+backslash,t,d"} + ``` + + + +## Default commands + +Textual apps have the following commands enabled by default: + +- `"Toggle light/dark mode"` + This will toggle between light and dark mode, by setting `App.dark` to either `True` or `False`. +- `"Quit the application"` + Quits the application. The equivalent of pressing ++ctrl+C++. +- `"Play the bell"` + Plays the terminal bell, by calling [`App.bell`][textual.app.App.bell]. + + +## Command providers + +To add your own command(s) to the command palette, define a [`command.Provider`][textual.command.Provider] class then add it to the [`COMMANDS`][textual.app.App.COMMANDS] class var on your `App` class. + +Let's look at a simple example which adds the ability to open Python files via the command palette. + +The following example will display a blank screen initially, but if you bring up the command palette and start typing the name of a Python file, it will show the command to open it. + +!!! tip + + If you are running that example from the repository, you may want to add some additional Python files to see how the examples works with multiple files. + + + ```python title="command01.py" hl_lines="14-42 45" + --8<-- "docs/examples/guide/command_palette/command01.py" + ``` + + 1. This method is called when the command palette is first opened. + 2. Called on each key-press. + 3. Get a [Matcher][textual.fuzzy.Matcher] instance to compare against hits. + 4. Use the matcher to get a score. + 5. Highlights matching letters in the search. + 6. Adds our custom command provider and the default command provider. + +There are three methods you can override in a command provider: [`startup`][textual.command.Provider.startup], [`search`][textual.command.Provider.search], and [`shutdown`][textual.command.Provider.shutdown]. +All of these methods should be coroutines (`async def`). Only `search` is required, the other methods are optional. +Let's explore those methods in detail. + +### startup method + +The [`startup`][textual.command.Provider.startup] method is called when the command palette is opened. +You can use this method as way of performing work that needs to be done prior to searching. +In the example, we use this method to get the Python (.py) files in the current working directory. + +### search method + +The [`search`][textual.command.Provider.search] method is responsible for finding results (or *hits*) that match the user's input. +This method should *yield* [`Hit`][textual.command.Hit] objects for any command that matches the `query` argument. + +Exactly how the matching is implemented is up to the author of the command provider, but we recommend using the builtin fuzzy matcher object, which you can get by calling [`matcher`][textual.command.Provider.matcher]. +This object has a [`match()`][textual.fuzzy.Matcher.match] method which compares the user's search term against the potential command and returns a *score*. +A score of zero means *no hit*, and you can discard the potential command. +A score of above zero indicates the confidence in the result, where 1 is an exact match, and anything lower indicates a less confident match. + +The [`Hit`][textual.command.Hit] contains information about the score (used in ordering) and how the hit should be displayed, and an optional help string. +It also contains a callback, which will be run if the user selects that command. + +In the example above, the callback is a lambda which calls the `open_file` method in the example app. + +!!! note + + Unlike most other places in Textual, errors in command provider will not *exit* the app. + This is a deliberate design decision taken to prevent a single broken `Provider` class from making the command palette unusable. + Errors in command providers will be logged to the [console](./devtools.md). + +### Shutdown method + +The [`shutdown`][textual.command.Provider.shutdown] method is called when the command palette is closed. +You can use this as a hook to gracefully close any objects you created in [`startup`][textual.command.Provider.startup]. + +## Screen commands + +You can also associate commands with a screen by adding a `COMMANDS` class var to your Screen class. + +Commands defined on a screen are only considered when that screen is active. +You can use this to implement commands that are specific to a particular screen, that wouldn't be applicable everywhere in the app. + +## Disabling the command palette + +The command palette is enabled by default. +If you would prefer not to have the command palette, you can set `ENABLE_COMMAND_PALETTE = False` on your app class. + +Here's an app class with no command palette: + +```python +class NoPaletteApp(App): + ENABLE_COMMAND_PALETTE = False +``` diff --git a/docs/guide/events.md b/docs/guide/events.md index 19ac92e829..bd89515f37 100644 --- a/docs/guide/events.md +++ b/docs/guide/events.md @@ -311,10 +311,10 @@ Let's look at an example which looks up word definitions from an [api](https://d ```python title="dictionary.py" hl_lines="28" --8<-- "docs/examples/events/dictionary.py" ``` -=== "dictionary.css" +=== "dictionary.tcss" - ```python title="dictionary.css" - --8<-- "docs/examples/events/dictionary.css" + ```python title="dictionary.tcss" + --8<-- "docs/examples/events/dictionary.tcss" ``` === "Output" diff --git a/docs/guide/input.md b/docs/guide/input.md index a7e288e1b0..644ca162ad 100644 --- a/docs/guide/input.md +++ b/docs/guide/input.md @@ -94,10 +94,10 @@ The following example shows how focus works in practice. --8<-- "docs/examples/guide/input/key03.py" ``` -=== "key03.css" +=== "key03.tcss" - ```python title="key03.css" hl_lines="15-17" - --8<-- "docs/examples/guide/input/key03.css" + ```python title="key03.tcss" hl_lines="15-17" + --8<-- "docs/examples/guide/input/key03.tcss" ``` === "Output" @@ -136,10 +136,10 @@ The following example binds the keys ++r++, ++g++, and ++b++ to an action which --8<-- "docs/examples/guide/input/binding01.py" ``` -=== "binding01.css" +=== "binding01.tcss" - ```python title="binding01.css" - --8<-- "docs/examples/guide/input/binding01.css" + ```python title="binding01.tcss" + --8<-- "docs/examples/guide/input/binding01.tcss" ``` === "Output" @@ -206,10 +206,10 @@ The following example shows mouse movements being used to _attach_ a widget to t --8<-- "docs/examples/guide/input/mouse01.py" ``` -=== "mouse01.css" +=== "mouse01.tcss" - ```python title="mouse01.css" - --8<-- "docs/examples/guide/input/mouse01.css" + ```python title="mouse01.tcss" + --8<-- "docs/examples/guide/input/mouse01.tcss" ``` If you run `mouse01.py` you should find that it logs the mouse move event, and keeps a widget pinned directly under the cursor. diff --git a/docs/guide/layout.md b/docs/guide/layout.md index 3564208a9a..3ef77ced7e 100644 --- a/docs/guide/layout.md +++ b/docs/guide/layout.md @@ -25,15 +25,15 @@ The example below demonstrates how children are arranged inside a container with --8<-- "docs/examples/guide/layout/vertical_layout.py" ``` -=== "vertical_layout.css" +=== "vertical_layout.tcss" ```sass hl_lines="2" - --8<-- "docs/examples/guide/layout/vertical_layout.css" + --8<-- "docs/examples/guide/layout/vertical_layout.tcss" ``` Notice that the first widget yielded from the `compose` method appears at the top of the display, the second widget appears below it, and so on. -Inside `vertical_layout.css`, we've assigned `layout: vertical` to `Screen`. +Inside `vertical_layout.tcss`, we've assigned `layout: vertical` to `Screen`. `Screen` is the parent container of the widgets yielded from the `App.compose` method, and can be thought of as the terminal window itself. !!! note @@ -90,10 +90,10 @@ The example below shows how we can arrange widgets horizontally, with minimal ch --8<-- "docs/examples/guide/layout/horizontal_layout.py" ``` -=== "horizontal_layout.css" +=== "horizontal_layout.tcss" ```sass hl_lines="2" - --8<-- "docs/examples/guide/layout/horizontal_layout.css" + --8<-- "docs/examples/guide/layout/horizontal_layout.tcss" ``` @@ -123,10 +123,10 @@ To enable horizontal scrolling, we can use the `overflow-x: auto;` declaration: --8<-- "docs/examples/guide/layout/horizontal_layout_overflow.py" ``` -=== "horizontal_layout_overflow.css" +=== "horizontal_layout_overflow.tcss" ```sass hl_lines="3" - --8<-- "docs/examples/guide/layout/horizontal_layout_overflow.css" + --8<-- "docs/examples/guide/layout/horizontal_layout_overflow.tcss" ``` With `overflow-x: auto;`, Textual automatically adds a horizontal scrollbar since the width of the children @@ -152,10 +152,10 @@ In other words, we have a single row containing two columns. --8<-- "docs/examples/guide/layout/utility_containers.py" ``` -=== "utility_containers.css" +=== "utility_containers.tcss" ```sass hl_lines="2" - --8<-- "docs/examples/guide/layout/utility_containers.css" + --8<-- "docs/examples/guide/layout/utility_containers.tcss" ``` You may be tempted to use many levels of nested utility containers in order to build advanced, grid-like layouts. @@ -191,10 +191,10 @@ Let's update the [utility containers](#utility-containers) example to use the co --8<-- "docs/examples/guide/layout/utility_containers.py" ``` -=== "utility_containers.css" +=== "utility_containers.tcss" ```sass - --8<-- "docs/examples/guide/layout/utility_containers.css" + --8<-- "docs/examples/guide/layout/utility_containers.tcss" ``` === "Output" @@ -233,10 +233,10 @@ The following example creates a 3 x 2 grid and adds six widgets to it --8<-- "docs/examples/guide/layout/grid_layout1.py" ``` -=== "grid_layout1.css" +=== "grid_layout1.tcss" ```sass hl_lines="2 3" - --8<-- "docs/examples/guide/layout/grid_layout1.css" + --8<-- "docs/examples/guide/layout/grid_layout1.tcss" ``` @@ -254,10 +254,10 @@ If we were to yield a seventh widget from our `compose` method, it would not be --8<-- "docs/examples/guide/layout/grid_layout2.py" ``` -=== "grid_layout2.css" +=== "grid_layout2.tcss" ```sass hl_lines="3" - --8<-- "docs/examples/guide/layout/grid_layout2.css" + --8<-- "docs/examples/guide/layout/grid_layout2.tcss" ``` Since we specified that our grid has three columns (`grid-size: 3`), and we've yielded seven widgets in total, @@ -286,10 +286,10 @@ We'll make the first column take up half of the screen width, with the other two --8<-- "docs/examples/guide/layout/grid_layout3_row_col_adjust.py" ``` -=== "grid_layout3_row_col_adjust.css" +=== "grid_layout3_row_col_adjust.tcss" ```sass hl_lines="4" - --8<-- "docs/examples/guide/layout/grid_layout3_row_col_adjust.css" + --8<-- "docs/examples/guide/layout/grid_layout3_row_col_adjust.tcss" ``` @@ -315,10 +315,10 @@ and the second row to `75%` height (while retaining the `grid-columns` change fr --8<-- "docs/examples/guide/layout/grid_layout4_row_col_adjust.py" ``` -=== "grid_layout4_row_col_adjust.css" +=== "grid_layout4_row_col_adjust.tcss" ```sass hl_lines="5" - --8<-- "docs/examples/guide/layout/grid_layout4_row_col_adjust.css" + --8<-- "docs/examples/guide/layout/grid_layout4_row_col_adjust.tcss" ``` @@ -326,6 +326,33 @@ If you don't specify enough values in a `grid-columns` or `grid-rows` declaratio For example, if your grid has four columns (i.e. `grid-size: 4;`), then `grid-columns: 2 4;` is equivalent to `grid-columns: 2 4 2 4;`. If it instead had three columns, then `grid-columns: 2 4;` would be equivalent to `grid-columns: 2 4 2;`. +#### Auto rows / columns + +The `grid-columns` and `grid-rows` rules can both accept a value of "auto" in place of any of the dimensions, which tells Textual to calculate an optimal size based on the content. + +Let's modify the previous example to make the first column an `auto` column. + +=== "Output" + + ```{.textual path="docs/examples/guide/layout/grid_layout_auto.py"} + ``` + +=== "grid_layout_auto.py" + + ```python hl_lines="6 9" + --8<-- "docs/examples/guide/layout/grid_layout_auto.py" + ``` + +=== "grid_layout_auto.tcss" + + ```sass hl_lines="4" + --8<-- "docs/examples/guide/layout/grid_layout_auto.tcss" + ``` + +Notice how the first column is just wide enough to fit the content of each cell. +The layout will adjust accordingly if you update the content for any widget in that column. + + ### Cell spans Cells may _span_ multiple rows or columns, to create more interesting grid arrangements. @@ -348,10 +375,10 @@ We'll also add a slight tint using `tint: magenta 40%;` to draw attention to it. --8<-- "docs/examples/guide/layout/grid_layout5_col_span.py" ``` -=== "grid_layout5_col_span.css" +=== "grid_layout5_col_span.tcss" ```sass hl_lines="6-9" - --8<-- "docs/examples/guide/layout/grid_layout5_col_span.css" + --8<-- "docs/examples/guide/layout/grid_layout5_col_span.tcss" ``` @@ -381,10 +408,10 @@ We again target widget `#two` in our CSS, and add a `row-span: 2;` declaration t --8<-- "docs/examples/guide/layout/grid_layout6_row_span.py" ``` -=== "grid_layout6_row_span.css" +=== "grid_layout6_row_span.tcss" ```sass hl_lines="8" - --8<-- "docs/examples/guide/layout/grid_layout6_row_span.css" + --8<-- "docs/examples/guide/layout/grid_layout6_row_span.tcss" ``` @@ -413,10 +440,10 @@ Now if we add `grid-gutter: 1;` to our grid, one cell of spacing appears between --8<-- "docs/examples/guide/layout/grid_layout7_gutter.py" ``` -=== "grid_layout7_gutter.css" +=== "grid_layout7_gutter.tcss" ```sass hl_lines="4" - --8<-- "docs/examples/guide/layout/grid_layout7_gutter.css" + --8<-- "docs/examples/guide/layout/grid_layout7_gutter.tcss" ``` Notice that gutter only applies _between_ the cells in a grid, pushing them away from each other. @@ -453,10 +480,10 @@ The code below shows a simple sidebar implementation. --8<-- "docs/examples/guide/layout/dock_layout1_sidebar.py" ``` -=== "dock_layout1_sidebar.css" +=== "dock_layout1_sidebar.tcss" ```sass hl_lines="2" - --8<-- "docs/examples/guide/layout/dock_layout1_sidebar.css" + --8<-- "docs/examples/guide/layout/dock_layout1_sidebar.tcss" ``` If we run the app above and scroll down, the body text will scroll but the sidebar does not (note the position of the scrollbar in the output shown above). @@ -477,10 +504,10 @@ This new sidebar is double the width of the one previous one, and has a `deeppin --8<-- "docs/examples/guide/layout/dock_layout2_sidebar.py" ``` -=== "dock_layout2_sidebar.css" +=== "dock_layout2_sidebar.tcss" ```sass hl_lines="1-6" - --8<-- "docs/examples/guide/layout/dock_layout2_sidebar.css" + --8<-- "docs/examples/guide/layout/dock_layout2_sidebar.tcss" ``` Notice that the original sidebar (`#sidebar`) appears on top of the newly docked widget. @@ -501,10 +528,10 @@ We can yield it inside `compose`, and without any additional CSS, we get a heade --8<-- "docs/examples/guide/layout/dock_layout3_sidebar_header.py" ``` -=== "dock_layout3_sidebar_header.css" +=== "dock_layout3_sidebar_header.tcss" ```sass - --8<-- "docs/examples/guide/layout/dock_layout3_sidebar_header.css" + --8<-- "docs/examples/guide/layout/dock_layout3_sidebar_header.tcss" ``` If we wished for the sidebar to appear below the header, it'd simply be a case of yielding the sidebar before we yield the header. @@ -544,10 +571,10 @@ However, in this case, both `#box1` and `#box2` are assigned to layers which def --8<-- "docs/examples/guide/layout/layers.py" ``` -=== "layers.css" +=== "layers.tcss" ```sass hl_lines="3 14 19" - --8<-- "docs/examples/guide/layout/layers.css" + --8<-- "docs/examples/guide/layout/layers.tcss" ``` ## Offsets @@ -585,10 +612,10 @@ The example below shows how an advanced layout can be built by combining the var --8<-- "docs/examples/guide/layout/combining_layouts.py" ``` -=== "combining_layouts.css" +=== "combining_layouts.tcss" ```sass - --8<-- "docs/examples/guide/layout/combining_layouts.css" + --8<-- "docs/examples/guide/layout/combining_layouts.tcss" ``` Textual layouts make it easy to design and build real-life applications with relatively little code. diff --git a/docs/guide/reactivity.md b/docs/guide/reactivity.md index 438fe6984c..4a7c2cd41b 100644 --- a/docs/guide/reactivity.md +++ b/docs/guide/reactivity.md @@ -79,10 +79,10 @@ Let's look at an example which illustrates this. In the following app, the value --8<-- "docs/examples/guide/reactivity/refresh01.py" ``` -=== "refresh01.css" +=== "refresh01.tcss" ```sass - --8<-- "docs/examples/guide/reactivity/refresh01.css" + --8<-- "docs/examples/guide/reactivity/refresh01.tcss" ``` === "Output" @@ -123,10 +123,10 @@ The following example modifies "refresh01.py" so that the greeting has an automa 1. This attribute will update the layout when changed. -=== "refresh02.css" +=== "refresh02.tcss" ```sass hl_lines="7-9" - --8<-- "docs/examples/guide/reactivity/refresh02.css" + --8<-- "docs/examples/guide/reactivity/refresh02.tcss" ``` === "Output" @@ -150,10 +150,10 @@ A common use for this is to restrict numbers to a given range. The following exa --8<-- "docs/examples/guide/reactivity/validate01.py" ``` -=== "validate01.css" +=== "validate01.tcss" ```sass - --8<-- "docs/examples/guide/reactivity/validate01.css" + --8<-- "docs/examples/guide/reactivity/validate01.tcss" ``` === "Output" @@ -183,10 +183,10 @@ The following app will display any color you type in to the input. Try it with a 2. Called when `self.color` is changed. 3. New color is assigned here. -=== "watch01.css" +=== "watch01.tcss" ```sass - --8<-- "docs/examples/guide/reactivity/watch01.css" + --8<-- "docs/examples/guide/reactivity/watch01.tcss" ``` === "Output" @@ -219,10 +219,10 @@ The following example uses a computed attribute. It displays three inputs for ea 1. Combines color components in to a Color object. 2. The watch method is called when the _result_ of `compute_color` changes. -=== "computed01.css" +=== "computed01.tcss" ```sass - --8<-- "docs/examples/guide/reactivity/computed01.css" + --8<-- "docs/examples/guide/reactivity/computed01.tcss" ``` === "Output" diff --git a/docs/guide/screens.md b/docs/guide/screens.md index 25915513c2..b9aefdc175 100644 --- a/docs/guide/screens.md +++ b/docs/guide/screens.md @@ -24,10 +24,10 @@ Let's look at a simple example of writing a screen class to simulate Window's [b --8<-- "docs/examples/guide/screens/screen01.py" ``` -=== "screen01.css" +=== "screen01.tcss" - ```sass title="screen01.css" - --8<-- "docs/examples/guide/screens/screen01.css" + ```sass title="screen01.tcss" + --8<-- "docs/examples/guide/screens/screen01.tcss" ``` === "Output" @@ -53,10 +53,10 @@ You can also _install_ new named screens dynamically with the [install_screen][t --8<-- "docs/examples/guide/screens/screen02.py" ``` -=== "screen02.css" +=== "screen02.tcss" - ```sass title="screen02.css" - --8<-- "docs/examples/guide/screens/screen02.css" + ```sass title="screen02.tcss" + --8<-- "docs/examples/guide/screens/screen02.tcss" ``` === "Output" @@ -169,10 +169,10 @@ From the quit screen you can click either Quit to exit the app immediately, or C --8<-- "docs/examples/guide/screens/modal01.py" ``` -=== "modal01.css" +=== "modal01.tcss" - ```sass title="modal01.css" - --8<-- "docs/examples/guide/screens/modal01.css" + ```sass title="modal01.tcss" + --8<-- "docs/examples/guide/screens/modal01.tcss" ``` @@ -211,10 +211,10 @@ Let's see what happens when we use `ModalScreen`. --8<-- "docs/examples/guide/screens/modal02.py" ``` -=== "modal01.css" +=== "modal01.tcss" - ```sass title="modal01.css" - --8<-- "docs/examples/guide/screens/modal01.css" + ```sass title="modal01.tcss" + --8<-- "docs/examples/guide/screens/modal01.tcss" ``` Now when we press ++q++, the dialog is displayed over the main screen. @@ -238,10 +238,10 @@ Let's modify the previous example to use `dismiss` rather than an explicit `pop_ 1. See below for an explanation of the `[bool]` -=== "modal01.css" +=== "modal01.tcss" - ```sass title="modal01.css" - --8<-- "docs/examples/guide/screens/modal01.css" + ```sass title="modal01.tcss" + --8<-- "docs/examples/guide/screens/modal01.tcss" ``` In the `on_button_pressed` message handler we call `dismiss` with a boolean that indicates if the user has chosen to quit the app. @@ -256,3 +256,63 @@ Returning data in this way can help keep your code manageable by making it easy You may have noticed in the previous example that we changed the base class to `ModalScreen[bool]`. The addition of `[bool]` adds typing information that tells the type checker to expect a boolean in the call to `dismiss`, and that any callback set in `push_screen` should also expect the same type. As always, typing is optional in Textual, but this may help you catch bugs. + + +## Modes + +Some apps may benefit from having multiple screen stacks, rather than just one. +Consider an app with a dashboard screen, a settings screen, and a help screen. +These are independent in the sense that we don't want to prevent the user from switching between them, even if there are one or more modal screens on the screen stack. +But we may still want each individual screen to have a navigation stack where we can push and pop screens. + +In Textual we can manage this with *modes*. +A mode is simply a named screen stack, which we can switch between as required. +When we switch modes, the topmost screen in the new mode becomes the active visible screen. + +The following diagram illustrates such an app with modes. +On startup the app switches to the "dashboard" mode which makes the top of the stack visible. + +
+--8<-- "docs/images/screens/modes1.excalidraw.svg" +
+ +If we later change the mode to "settings", the top of that mode's screen stack becomes visible. + +
+--8<-- "docs/images/screens/modes2.excalidraw.svg" +
+ +To add modes to your app, define a [`MODES`][textual.app.App.MODES] class variable in your App class which should be a `dict` that maps the name of the mode on to either a screen object, a callable that returns a screen, or the name of an installed screen. +However you specify it, the values in `MODES` set the base screen for each mode's screen stack. + +You can switch between these screens at any time by calling [`App.switch_mode`][textual.app.App.switch_mode]. +When you switch to a new mode, the topmost screen in the new stack becomes visible. +Any calls to [`App.push_screen`][textual.app.App.push_screen] or [`App.pop_screen`][textual.app.App.pop_screen] will affect only the active mode. + +Let's look at an example with modes: + +=== "modes01.py" + + ```python hl_lines="25-29 30-34 37" + --8<-- "docs/examples/guide/screens/modes01.py" + ``` + + 1. `switch_mode` is a builtin action to switch modes. + 2. Associates `DashboardScreen` with the name "dashboard". + 3. Switches to the dashboard mode. + +=== "Output" + + ```{.textual path="docs/examples/guide/screens/modes01.py"} + ``` + +=== "Output (after pressing S)" + + ```{.textual path="docs/examples/guide/screens/modes01.py", press="s"} + ``` + +Here we have defined three screens. +One for a dashboard, one for settings, and one for help. +We've bound keys to each of these screens, so the user can switch between the screens. + +Pressing ++d++, ++s++, or ++h++ switches between these modes. diff --git a/docs/guide/testing.md b/docs/guide/testing.md new file mode 100644 index 0000000000..25baba6c40 --- /dev/null +++ b/docs/guide/testing.md @@ -0,0 +1,305 @@ +# Testing + +Code testing is an important part of software development. +This chapter will cover how to write tests for your Textual apps. + +## What is testing? + +It is common to write tests alongside your app. +A *test* is simply a function that confirms your app is working correctly. + +!!! tip "Learn more about testing" + + We recommend [Python Testing with pytest](https://pythontest.com/pytest-book/) for a comprehensive guide to writing tests. + +## Do you need to write tests? + +The short answer is "no", you don't *need* to write tests. + +In practice however, it is almost always a good idea to write tests. +Writing code that is completely bug free is virtually impossible, even for experienced developers. +If you want to have confidence that your application will run as you intended it to, then you should write tests. +Your test code will help you find bugs early, and alert you if you accidentally break something in the future. + +## Testing frameworks for Textual + +Textual doesn't require any particular test framework. +You can use any test framework you are familiar with, but we will be using [pytest](https://docs.pytest.org/) in this chapter. + + +## Testing apps + +You can often test Textual code in the same way as any other app, and use similar techniques. +But when testing user interface interactions, you may need to use Textual's dedicated test features. + +Let's write a simple Textual app so we can demonstrate how to test it. +The following app shows three buttons labelled "red", "green", and "blue". +Clicking one of those buttons or pressing a corresponding ++r++, ++g++, and ++b++ key will change the background color. + +=== "rgb.py" + + ```python + --8<-- "docs/examples/guide/testing/rgb.py" + ``` + +=== "Output" + + ```{.textual path="docs/examples/guide/testing/rgb.py"} + ``` + +Although it is straightforward to test an app like this manually, it is not practical to click every button and hit every key in your app after changing a single line of code. +Tests allow us to automate such testing so we can quickly simulate user interactions and check the result. + +To test our simple app we will use the [`run_test()`][textual.app.App.run_test] method on the `App` class. +This replaces the usual call to [`run()`][textual.app.App.run] and will run the app in *headless* mode, which prevents Textual from updating the terminal but otherwise behaves as normal. + +The `run_test()` method is an *async context manager* which returns a [`Pilot`][textual.pilot.Pilot] object. +You can use this object to interact with the app as if you were operating it with a keyboard and mouse. + +Let's look at the tests for the example above: + +```python title="test_rgb.py" +--8<-- "docs/examples/guide/testing/test_rgb.py" +``` + +1. The `run_test()` method requires that it run in a coroutine, so tests must use the `async` keyword. +2. This runs the app and returns a Pilot instance we can use to interact with it. +3. Simulates pressing the ++r++ key. +4. This checks that pressing the ++r++ key has resulted in the background color changing. +5. Simulates clicking on the widget with an `id` of `red` (the button labelled "Red"). + +There are two tests defined in `test_rgb.py`. +The first to test keys and the second to test button clicks. +Both tests first construct an instance of the app and then call `run_test()` to get a Pilot object. +The `test_keys` function simulates key presses with [`Pilot.press`][textual.pilot.Pilot.press], and `test_buttons` simulates button clicks with [`Pilot.click`][textual.pilot.Pilot.click]. + +After simulating a user interaction, Textual tests will typically check the state has been updated with an `assert` statement. +The `pytest` module will record any failures of these assert statements as a test fail. + +If you run the tests with `pytest test_rgb.py` you should get 2 passes, which will confirm that the user will be able to click buttons or press the keys to change the background color. + +If you later update this app, and accidentally break this functionality, one or more of your tests will fail. +Knowing which test has failed will help you quickly track down where your code was broken. + +## Simulating key presses + +We've seen how the [`press`][textual.pilot.Pilot] method simulates keys. +You can also supply multiple keys to simulate the user typing in to the app. +Here's an example of simulating the user typing the word "hello". + +```python +await pilot.press("h", "e", "l", "l", "o") +``` + +Each string creates a single keypress. +You can also use the name for non-printable keys (such as "enter") and the "ctrl+" modifier. +These are the same identifiers as used for key events, which you can experiment with by running `textual keys`. + +## Simulating clicks + +You can simulate mouse clicks in a similar way with [`Pilot.click`][textual.pilot.Pilot.click]. +If you supply a CSS selector Textual will simulate clicking on the matching widget. + +!!! note + + If there is another widget in front of the widget you want to click, you may end up clicking the topmost widget rather than the widget indicated in the selector. + This is generally what you want, because a real user would experience the same thing. + +### Clicking the screen + +If you don't supply a CSS selector, then the click will be relative to the screen. +For example, the following simulates a click at (0, 0): + +```python +await pilot.click() +``` + +### Click offsets + +If you supply an `offset` value, it will be added to the coordinates of the simulated click. +For example the following line would simulate a click at the coordinates (10, 5). + + +```python +await pilot.click(offset=(10, 5)) +``` + +If you combine this with a selector, then the offset will be relative to the widget. +Here's how you would click the line *above* a button. + +```python +await pilot.click(Button, offset(0, -1)) +``` + +### Modifier keys + +You can simulate clicks in combination with modifier keys, by setting the `shift`, `meta`, or `control` parameters. +Here's how you could simulate ctrl-clicking a widget with an ID of "slider": + +```python +await pilot.click("#slider", control=True) +``` + +## Changing the screen size + +The default size of a simulated app is (80, 24). +You may want to test what happens when the app has a different size. +To do this, set the `size` parameter of [`run_test`][textual.app.App.run_test] to a different size. +For example, here is how you would simulate a terminal resized to 100 columns and 50 lines: + +```python +async with app.run_test(size=(100, 50)) as pilot: + ... +``` + +## Pausing the pilot + +Some actions in a Textual app won't change the state immediately. +For instance, messages may take a moment to bubble from the widget that sent them. +If you were to post a message and immediately `assert` you may find that it fails because the message hasn't yet been processed. + +You can generally solve this by calling [`pause()`][textual.pilot.Pilot.pause] which will wait for all pending messages to be processed. +You can also supply a `delay` parameter, which will insert a delay prior to waiting for pending messages. + + +## Textual's tests + +Textual itself has a large battery of tests. +If you are interested in how we write tests, see the [tests/](https://github.com/Textualize/textual/tree/main/tests) directory in the Textual repository. + +## Snapshot testing + +Snapshot testing is the process of recording the output of a test, and comparing it against the output from previous runs. + +Textual uses snapshot testing internally to ensure that the builtin widgets look and function correctly in every release. +We've made the pytest plugin we built available for public use. + +The [official Textual pytest plugin](https://github.com/Textualize/pytest-textual-snapshot) can help you catch otherwise difficult to detect visual changes in your app. + +It works by generating an SVG _screenshot_ (such as the images in these docs) from your app. +If the screenshot changes in any test run, you will have the opportunity to visually compare the new output against previous runs. + + +### Installing the plugin + +You can install `pytest-textual-snapshot` using your favorite package manager (`pip`, `poetry`, etc.). + +``` +pip install pytest-textual-snapshot +``` + +### Creating a snapshot test + +With the package installed, you now have access to the `snap_compare` pytest fixture. + +Let's look at an example of how we'd create a snapshot test for the [calculator app](https://github.com/Textualize/textual/blob/main/examples/calculator.py) below. + +```{.textual path="examples/calculator.py" columns=100 lines=41 press="3,.,1,4,5,9,2,wait:400"} +``` + +First, we need to create a new test and specify the path to the Python file containing the app. +This path should be relative to the location of the test. + +```python +def test_calculator(snap_compare): + assert snap_compare("path/to/calculator.py") +``` + +Let's run the test as normal using `pytest`. + +``` +pytest +``` + +When this test runs for the first time, an SVG screenshot of the calculator app is generated, and the test will fail. +Snapshot tests always fail on the first run, since there's no previous version to compare the snapshot to. + +![snapshot_report_console_output.png](../images/testing/snapshot_report_console_output.png) + +If you open the snapshot report in your browser, you'll see something like this: + +![snapshot_report_example.png](../images/testing/snapshot_report_example.png) + +!!! tip + + You can usually open the link directly from the terminal, but some terminal emulators may + require you to hold ++ctrl++ or ++command++ while clicking for links to work. + +The report explains that there's "No history for this test". +It's our job to validate that the initial snapshot looks correct before proceeding. +Our calculator is rendering as we expect, so we'll save this snapshot: + +``` +pytest --snapshot-update +``` + +!!! warning + + Only ever run pytest with `--snapshot-update` if you're happy with how the output looks + on the left hand side of the snapshot report. When using `--snapshot-update`, you're saying "I'm happy with all of the + screenshots in the snapshot test report, and they will now represent the ground truth which all future runs will be compared + against". As such, you should only run `pytest --snapshot-update` _after_ running `pytest` and confirming the output looks good. + +Now that our snapshot is saved, if we run `pytest` (with no arguments) again, the test will pass. +This is because the screenshot taken during this test run matches the one we saved earlier. + +### Catching a bug + +The real power of snapshot testing comes from its ability to catch visual regressions which could otherwise easily be missed. + +Imagine a new developer joins your team, and tries to make a few changes to the calculator. +While making this change they accidentally break some styling which removes the orange coloring from the buttons on the right of the app. +When they run `pytest`, they're presented with a report which reveals the damage: + +![snapshot_report_diff_before.png](../images/testing/snapshot_report_diff_before.png) + +On the right, we can see our "historical" snapshot - this is the one we saved earlier. +On the left is how our app is currently rendering - clearly not how we intended! + +We can click the "Show difference" toggle at the top right of the diff to overlay the two versions: + +![snapshot_report_diff_after.png](../images/testing/snapshot_report_diff_after.png) + +This reveals another problem, which could easily be missed in a quick visual inspection - +our new developer has also deleted the number 4! + +!!! tip + + Snapshot tests work well in CI on all supported operating systems, and the snapshot + report is just an HTML file which can be exported as a build artifact. + + +### Pressing keys + +You can simulate pressing keys before the snapshot is captured using the `press` parameter. + +```python +def test_calculator_pressing_numbers(snap_compare): + assert snap_compare("path/to/calculator.py", press=["1", "2", "3"]) +``` + +### Changing the terminal size + +To capture the snapshot with a different terminal size, pass a tuple `(width, height)` as the `terminal_size` parameter. + +```python +def test_calculator(snap_compare): + assert snap_compare("path/to/calculator.py", terminal_size=(50, 100)) +``` + +### Running setup code + +You can also run arbitrary code before the snapshot is captured using the `run_before` parameter. + +In this example, we use `run_before` to hover the mouse cursor over the widget with ID `number-5` +before taking the snapshot. + +```python +def test_calculator_hover_number(snap_compare): + async def run_before(pilot) -> None: + await pilot.hover("#number-5") + + assert snap_compare("path/to/calculator.py", run_before=run_before) +``` + +For more information, visit the [`pytest-textual-snapshot` repo on GitHub](https://github.com/Textualize/pytest-textual-snapshot). diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index f205a93196..b39fa1999f 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -40,10 +40,10 @@ This (very simple) custom widget may be [styled](./styles.md) in the same way as --8<-- "docs/examples/guide/widgets/hello02.py" ``` -=== "hello02.css" +=== "hello02.tcss" - ```sass title="hello02.css" - --8<-- "docs/examples/guide/widgets/hello02.css" + ```sass title="hello02.tcss" + --8<-- "docs/examples/guide/widgets/hello02.tcss" ``` The addition of the CSS has completely transformed our custom widget. @@ -63,10 +63,10 @@ Let's use Static to create a widget which cycles through "hello" in various lang --8<-- "docs/examples/guide/widgets/hello03.py" ``` -=== "hello03.css" +=== "hello03.tcss" - ```sass title="hello03.css" - --8<-- "docs/examples/guide/widgets/hello03.css" + ```sass title="hello03.tcss" + --8<-- "docs/examples/guide/widgets/hello03.tcss" ``` === "Output" @@ -92,10 +92,10 @@ Here's the Hello example again, this time the widget has embedded default CSS: --8<-- "docs/examples/guide/widgets/hello04.py" ``` -=== "hello04.css" +=== "hello04.tcss" - ```sass title="hello04.css" - --8<-- "docs/examples/guide/widgets/hello04.css" + ```sass title="hello04.tcss" + --8<-- "docs/examples/guide/widgets/hello04.tcss" ``` === "Output" @@ -103,6 +103,13 @@ Here's the Hello example again, this time the widget has embedded default CSS: ```{.textual path="docs/examples/guide/widgets/hello04.py"} ``` +#### Scoped CSS + +Default CSS is *scoped* by default. +All this means is that CSS defined in `DEFAULT_CSS` will affect the widget and potentially its children only. +This is to prevent you from inadvertently breaking an unrelated widget. + +You can disabled scoped CSS by setting the class var `SCOPED_CSS` to `False`. #### Default specificity @@ -128,10 +135,10 @@ Let's use markup links in the hello example so that the greeting becomes a link --8<-- "docs/examples/guide/widgets/hello05.py" ``` -=== "hello05.css" +=== "hello05.tcss" - ```sass title="hello05.css" - --8<-- "docs/examples/guide/widgets/hello05.css" + ```sass title="hello05.tcss" + --8<-- "docs/examples/guide/widgets/hello05.tcss" ``` === "Output" @@ -142,6 +149,47 @@ Let's use markup links in the hello example so that the greeting becomes a link If you run this example you will see that the greeting has been underlined, which indicates it is clickable. If you click on the greeting it will run the `next_word` action which updates the next word. +## Border titles + +Every widget has a [`border_title`][textual.widgets.Widget.border_title] and [`border_subtitle`][textual.widgets.Widget.border_subtitle] attribute. +Setting `border_title` will display text within the top border, and setting `border_subtitle` will display text within the bottom border. + +!!! note + + Border titles will only display if the widget has a [border](../styles/border.md) enabled. + +The default value for these attributes is empty string, which disables the title. +You can change the default value for the title attributes with the [`BORDER_TITLE`][textual.widget.Widget.BORDER_TITLE] and [`BORDER_SUBTITLE`][textual.widget.Widget.BORDER_SUBTITLE] class variables. + +Let's demonstrate setting a title, both as a class variable and a instance variable: + + +=== "hello06.py" + + ```python title="hello06.py" hl_lines="26 30" + --8<-- "docs/examples/guide/widgets/hello06.py" + ``` + + 1. Setting the default for the `title` attribute via class variable. + 2. Setting `subtitle` via an instance attribute. + +=== "hello06.tcss" + + ```sass title="hello06.tcss" + --8<-- "docs/examples/guide/widgets/hello06.tcss" + ``` + +=== "Output" + + ```{.textual path="docs/examples/guide/widgets/hello06.py"} + ``` + +Note that titles are limited to a single line of text. +If the supplied text is too long to fit within the widget, it will be cropped (and an ellipsis added). + +There are a number of styles that influence how titles are displayed (color and alignment). +See the [style reference](../styles/index.md) for details. + ## Rich renderables In previous examples we've set strings as content for Widgets. You can also use special objects called [renderables](https://rich.readthedocs.io/en/latest/protocol.html) for advanced visuals. You can use any renderable defined in [Rich](https://github.com/Textualize/rich) or third party libraries. @@ -156,10 +204,10 @@ This app will "play" fizz buzz by displaying a table of the first 15 numbers and --8<-- "docs/examples/guide/widgets/fizzbuzz01.py" ``` -=== "fizzbuzz01.css" +=== "fizzbuzz01.tcss" - ```sass title="fizzbuzz01.css" hl_lines="32-35" - --8<-- "docs/examples/guide/widgets/fizzbuzz01.css" + ```sass title="fizzbuzz01.tcss" hl_lines="32-35" + --8<-- "docs/examples/guide/widgets/fizzbuzz01.tcss" ``` === "Output" @@ -180,10 +228,10 @@ Let's modify the default width for the fizzbuzz example. By default, the table w --8<-- "docs/examples/guide/widgets/fizzbuzz02.py" ``` -=== "fizzbuzz02.css" +=== "fizzbuzz02.tcss" - ```sass title="fizzbuzz02.css" - --8<-- "docs/examples/guide/widgets/fizzbuzz02.css" + ```sass title="fizzbuzz02.tcss" + --8<-- "docs/examples/guide/widgets/fizzbuzz02.tcss" ``` === "Output" @@ -246,6 +294,37 @@ Add a rule to your CSS that targets `Tooltip`. Here's an example: ```{.textual path="docs/examples/guide/widgets/tooltip02.py" hover="Button"} ``` +## Loading indicator + +Widgets have a [`loading`][textual.widget.Widget.loading] reactive which when set to `True` will temporarily replace your widget with a [`LoadingIndicator`](../widgets/loading_indicator.md). + +You can use this to indicate to the user that the app is currently working on getting data, and there will be content when that data is available. +Let's look at an example of this. + +=== "loading01.py" + + ```python title="loading01.py" + --8<-- "docs/examples/guide/widgets/loading01.py" + ``` + + 1. Shows the loading indicator in place of the data table. + 2. Insert a random sleep to simulate a network request. + 3. Show the new data. + +=== "Output" + + ```{.textual path="docs/examples/guide/widgets/loading01.py"} + ``` + + +In this example we have four [DataTable](../widgets/data_table.md) widgets, which we put into a loading state by setting the widget's `loading` property to `True`. +This will temporarily replace the widget with a loading indicator animation. +When the (simulated) data has been retrieved, we reset the `loading` property to show the new data. + +!!! tip + + See the guide on [Workers](./workers.md) if you want to know more about the `@work` decorator. + ## Line API A downside of widgets that return Rich renderables is that Textual will redraw the entire widget when its state is updated or it changes size. @@ -472,7 +551,7 @@ In this section we will show how to design and build a fully-working app, while ### Designing the app -We are going to build a *byte editor* which allows you to enter a number in both decimal and binary. You could use this a teaching aid for binary numbers. +We are going to build a *byte editor* which allows you to enter a number in both decimal and binary. You could use this as a teaching aid for binary numbers. Here's a sketch of what the app should ultimately look like: @@ -485,7 +564,11 @@ Here's a sketch of what the app should ultimately look like: --8<-- "docs/images/byte01.excalidraw.svg" -There are three types of built-in widget in the sketch, namely ([Input](../widgets/input.md), [Label](../widgets/label.md), and [Switch](../widgets/switch.md)). Rather than manage these as a single collection of widgets, we can arrange them in to logical groups with compound widgets. This will make our app easier to work with. +There are three types of built-in widget in the sketch, namely ([Input](../widgets/input.md), [Label](../widgets/label.md), and [Switch](../widgets/switch.md)). Rather than manage these as a single collection of widgets, we can arrange them into logical groups with compound widgets. This will make our app easier to work with. + +??? textualize "Try in Textual-web" + +
### Identifying components @@ -522,7 +605,7 @@ Note the `compose()` methods of each of the widgets. - The `ByteInput` yields 8 `BitSwitch` widgets and arranges them horizontally. It also adds a `focus-within` style in its CSS to draw an accent border when any of the switches are focused. -- The `ByteEditor` yields a `ByteInput` and an `Input` control. The default CSS stacks the two controls on top of each other to divide the screen in to two parts. +- The `ByteEditor` yields a `ByteInput` and an `Input` control. The default CSS stacks the two controls on top of each other to divide the screen into two parts. With these three widgets, the [DOM](CSS.md#the-dom) for our app will look like this: diff --git a/docs/guide/workers.md b/docs/guide/workers.md index bcd3e087ba..a8eff8432d 100644 --- a/docs/guide/workers.md +++ b/docs/guide/workers.md @@ -26,10 +26,10 @@ The following app uses [httpx](https://www.python-httpx.org/) to get the current --8<-- "docs/examples/guide/workers/weather01.py" ``` -=== "weather.css" +=== "weather.tcss" - ```sass title="weather.css" - --8<-- "docs/examples/guide/workers/weather.css" + ```sass title="weather.tcss" + --8<-- "docs/examples/guide/workers/weather.tcss" ``` === "Output" diff --git a/docs/how-to/design-a-layout.md b/docs/how-to/design-a-layout.md index b9f17c51c7..80c82c9d7e 100644 --- a/docs/how-to/design-a-layout.md +++ b/docs/how-to/design-a-layout.md @@ -27,6 +27,11 @@ Here's our sketch: It's rough, but it's all we need. +??? textualize "Try in Textual-web" + +
+ + ## Tip 2. Work outside in Like a sculpture with a block of marble, it is best to work from the outside towards the center. diff --git a/docs/images/icons/logo light transparent.svg b/docs/images/icons/logo light transparent.svg new file mode 100644 index 0000000000..411625e4a7 --- /dev/null +++ b/docs/images/icons/logo light transparent.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/docs/images/screens/modes1.excalidraw.svg b/docs/images/screens/modes1.excalidraw.svg new file mode 100644 index 0000000000..d57f8c6b2f --- /dev/null +++ b/docs/images/screens/modes1.excalidraw.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO2aW0/bSFx1MDAxNMff+ylQ9mVXKu7cL5VWK6ClXHUwMDA1UmhJuZRtVTn2JPHGsY3tJEDFd99jw8ZcdTAwMTdcYiSkXHUwMDA0Km1cdTAwMWWCPWfsOTPz+885M+HHi5WVRnpcdTAwMWWZxuuVhjlzbN9zY3vceJmVj0yceGFcdTAwMDAmkt8n4TB28pq9NI2S169eXHLsuG/SyLdcdTAwMWRjjbxkaPtJOnS90HLCwSsvNYPkr+x711x1MDAxZZg/o3DgprFVNLJqXFwvXHLjq7aMb1x1MDAwNiZIXHUwMDEzePvfcL+y8iP/LnnnevYgXGbcvHpuKLmnab10N1xmclcx5YpcdTAwMTOJNZrU8JI30FpqXFwwd8BjU1iyosaRjYZ7zeh78+JTW9uO2t48+/ChaLbj+X4rPfdzp5JcdTAwMTD6UtiSNFx1MDAwZfvmyHPTXtZ2rXzaU3E47PZcdTAwMDKTJJVnwsh2vPRcdTAwMWPKeOG7XHUwMDFkdPNXXHUwMDE0JWdwtyotjVx00pxcdTAwMTFOlGKMTMxXz1OLXHUwMDEwwlx1MDAwNYyT0lxuXHUwMDA2pObYRujDPIBjv6H8U7jWtp1+XHUwMDE3/Fx1MDAwYtyiXHUwMDBl5rbd7lx1MDAxNHXG190lQlpMM1x1MDAwMe1ThbVWk1x1MDAxYT3jdXtp1jvOLCWxkFxiXHUwMDBiTqUo/DD5dGioXHUwMDAwb2BsYshcdTAwMWGPttyci2/lXHUwMDExXHUwMDBi3OtcdTAwMTFcdTAwMGKGvl/4m1x1MDAxOd6WWCqeXHUwMDE5Rq59NelYwDhQxbGkolx1MDAxOCnfXHUwMDBi+vXX+aHTLzjJSy9fzo0n43wqnoogISijbGY8t9+hQ9U8+dTc2Wz7zY3Drffn0f5T4onRvXxyXHUwMDBi5lRrwpiQmFAlK3zChFtcdTAwMDIhSiSlSmjJXHUwMDE2wrNjt1x1MDAxMeKPgyehjGOt0Fx1MDAxMvBkQiMk+Vx1MDAxMvBUpaGo4amFXCLgzVx1MDAxY3SK3ogo01x1MDAxMu6J8665ftzdx1x1MDAwM3fzmdOJLYyBTC44V1xmRlx1MDAxZtEqnlhCXHUwMDA1ilx1MDAxOPArXHUwMDA1XHUwMDExdLHlU1x1MDAxMUdj8yh8YkJcdTAwMTiGJYUsXHUwMDAxUIZcdTAwMTFF0NxcdTAwMTJcdTAwMDClkkxcdTAwMDM0XHUwMDBiaowjiWdcdTAwMDd0dEC3u1EoRu/eXHUwMDFlOKnauaCO/5SA0vv4lDhcdTAwMDOUQ1dcdTAwMDVDWlx1MDAxMF6hkyNkMWCTaSGZYrTu1nxwtlxya7vtXz22g4gxXHUwMDEzVC6DzVIwuFx1MDAxMduZZJrCnM1cZufnL/2NcNPZ+4L39b67ZtZcdTAwMDej95+fNZxUUEtcbqmk1Fx1MDAwMsM3rcGpLSWIJCBhzLhaiM2OK1xyZv+zOTObnN3BJkJKc0g8Z2bT/b56vLcrm+GgXHUwMDFk+73xXHUwMDA2e+tvnjw3Ni3IXCJplkUykucuvFx1MDAwNiu3lIZcdTAwMDSTMYlcdTAwMTFcdTAwMTdcdTAwMTVWmURgXHUwMDE08Fx1MDAxNIxccqRcdTAwMDJ0IViZI0znl89CfzasqTlLbyNcdTAwMTWXwlaNVMVhaYG1Q85cZqp/SFtvdk9OTtaJcS76slx1MDAxZnL1aVxuqD3b6VxyY/P0SShcdTAwMTOWXHUwMDEwkFxcMlx1MDAwNmkoIZKzXG6cXHUwMDEySVx1MDAwYlZRyHUgxSOYL5aCTovymCuLaq6xlkAmYvImnOXk91xuR4Ipw1x1MDAxY5V20o+II+TmisyTc1x1MDAxNtNcdTAwMWVcdTAwMDZpy7vIk0ZVKd20XHUwMDA3nn9embmcU1x1MDAxOKmvXHLXTnrt0I7dr41Gxbzme90gx810qkynnmP7XHUwMDEzc1x1MDAxYUaF1YHmbC8w8ZZbdzuMva5cdTAwMTfY/ue7m4ZcdTAwMWWb95OVwiolg207MZk1z4pcdTAwMWakQqBrarzAXGa2o4ry2eOFQVx0+uLt75zzXvfirFx1MDAxZq7udFx1MDAwZbaeVobsPlx1MDAxNSpMsr0ggTVcdTAwMDfyadiD10SoLYIo5lx1MDAxYWFcYjNqsXOKaVwiXHUwMDE0ylx1MDAxMopqXGJRXGJyJlXKSZ6NXGKh92iek4lFRdgzfrR8/dVbfVTpTVx1MDAwZoDZgVx1MDAxOFwipdbuXHUwMDEz3sH2If9cdTAwMThcdTAwMWSGp7J1vrHzXHUwMDBmYmunb5vPXFx4jHKLYlx1MDAwNblcdTAwMWJHnGOlqvuIXFx5XHUwMDA0Q1qnRCa/R1xuf0RZsIvWRCDCqS6r6dlIXHUwMDBmKTlXOrao9Fx1MDAxMpOmXtBNli+/21p+TFx0lnO0mzt5yLnpPLsl0Vx1MDAxMuHx+MBcdTAwMWZ8XGLGXFydXlx1MDAxY1x1MDAwZtvjvYeJkNTKXHUwMDFmL1x0JcRCXHUwMDAy4p5cdTAwMDagQVx1MDAwNJRU41x1MDAxZtHEXHUwMDAyocLOKtNcdTAwMDfEpukqVLLD21x1MDAwZlShVlkrXHUwMDEwZJDimpaOwO9cdTAwMTChUOAz4z9vS3RtKPgpze1ol1x1MDAxY21/POqf4vFWa20zSnbtuNjqVWCz4zhcdTAwMWM3JpbL66s7XHUwMDE0rjQofJ5fpVx1MDAxNlP4mpN6I7Py+8hLvLZv/liuyqe3/jOUfjX4t0mdsHrpROqwXHUwMDA3k0rL2bebd9PwJEqX91x0PYt0QnPKXHUwMDE51Vx1MDAxNFx1MDAxM1ngklx1MDAxZqxAMOZIXHSuXGLnTEl2x1HIXHUwMDAyQkdcdTAwMTZWXHUwMDFjgStMZb9OXHUwMDEzSW45XGZhzJKIXHUwMDExgYXQWGB9U/lCQFx1MDAwZlx1MDAxOCqWqvulX2j6P1bIdcnlg6LywzWbpHacrnuBXHUwMDBika7q2PU/RGzNXHUwMDEwTXKVO8PMy1VkIcmFJJpjXGJZTFx1MDAxNucm2cjYUbbJgSqUXG5CKMKSiJtdN4FbuFTthZ2kXHUwMDFi4WDgpdD/j6FcdTAwMTek9Vx1MDAxYXmH1jLh9Yx9Q//w5rKtrtAoe2N1+S2uVlxuhvObyfW3l7fWXr2Dr+xzg6zihS/Kf7M1O2+iYUdRK4WZn0xcdTAwMTSg5rnXS27Rz8bIM+P121x1MDAwZbDzT1x1MDAxNlxy8rHOllx1MDAwNpPjePni8l9cdTAwMDSHyVx1MDAxMCJ9 + + + + "dashboard""help""settings"Active (visible) diff --git a/docs/images/screens/modes2.excalidraw.svg b/docs/images/screens/modes2.excalidraw.svg new file mode 100644 index 0000000000..97e38ad8ff --- /dev/null +++ b/docs/images/screens/modes2.excalidraw.svg @@ -0,0 +1,16 @@ + + + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nN2a2VLbSFx1MDAxNIbv81x1MDAxNJTnNii9L6mammJfMmxcdTAwMDFcdTAwMDJkkkrJUttWLEtCkrFJinef01xuYy1gNrNlfGHsPrL6tPr7z1wi8fPN3FxcKz9PTOv9XFzLjD03XGb81Fx1MDAxZLXe2vEzk2ZBXHUwMDFjgYlcdTAwMTTfs3iYesWRvTxPsvfv3lxy3LRv8iR0PeOcXHUwMDA12dBccrN86Fx1MDAwN7HjxYN3QW5cdTAwMDbZX/Z921x1MDAxZJg/k3jg56lTTjJv/CCP019zmdBcZkyUZ3D2f+D73NzP4r3inVx1MDAxZriDOPKLw1x1MDAwYkPpXHUwMDFlJbw5ulx1MDAxZEeFq5xcdIlcdTAwMTlcIlx1MDAxM3uQLcNcXLnxwdhcdTAwMDF/TWmxQ62j3Z01s7iXu9m8TpLTlW/JsEPLSTtBXHUwMDE47ufnYeFSXHUwMDE2w0pKW5ancd9cdTAwMWNcdTAwMDV+3lx1MDAwMytujE/7VVx1MDAxYVx1MDAwZru9yGRZ7Tdx4npBfm5cdTAwMTeHJoNu1C1OUY6M7Y80dbDWmGtcIpggQtOJ2f6eKe4wXCK1XHUwMDA2XHUwMDEzZVxcNdxaikPYXHUwMDAzcOtcdTAwMGZUvErH2q7X74J3kV9cdTAwMWWDueu2O+Uxo8vFXHUwMDEyIVx1MDAxZKaZUIxRXHUwMDA1zpSz9EzQ7eXWTc5cdTAwMWMlsZBcYlx1MDAwYk6lKP0wxWbAXHUwMDAy7Fx1MDAxOVx1MDAxOJtcdTAwMTjs5MmGXzDxtXq9XCL/8npFwzAs/bWGlSZHVZYq27y/sX7a3/Hzle1w72Rk0sWTxW97k3XVwHPTNFx1MDAxZbUmlovLT6VHw8R3f1x1MDAwMYXh4iuGXHUwMDA1oVKWSIZB1G86XHUwMDFixl6/ZLBcdTAwMTi9eHtv8JlcdTAwMTLTwFx1MDAwNyooJVx1MDAwMle24jb0+3LpQ1x1MDAxYeJvx6j9PTzoxt3R9+HWK0dcdTAwMWbYXHUwMDE2XHUwMDAwNiGMXHUwMDEy1Fx1MDAwMJ9cdEchLlx1MDAwNVx1MDAxMoLCzrCZyO+4bYT405BPQJewUejRyH9ccmxqoqexSTVBmHBy96jMR8nR1tjfpOM12U+Xu0ujT/HglaOpXHUwMDFkiLlSaURcdTAwMTmSVNbYpGClWDFFuCRIK8ZnglNcdTAwMTFPY/MkcGKQXHUwMDE2xoqQ/1x1MDAxN520kiWbJYOWmFx1MDAxM87Znek88Vx1MDAwZdaOt7zOp7PNY7W1tDFYWF7cfkk62W10akxcdTAwMWNCXHUwMDA0x5hSpKSqwVx0VDpcdTAwMWHUXHTVhKRcdTAwMTji60xstlxya/vt36RkuFx1MDAxMU0hlHpcdTAwMTY01TQ0MVKwYCwqaf82NlPlkf7ybrhcdTAwMWLujE829lx1MDAwZvn6SftF61mMboOTXHUwMDBiu+1cdTAwMWFTXHUwMDBiqNSkXHUwMDFlOpkmXHUwMDBlXHUwMDEyXG5ziVx1MDAxONdcdTAwMTQ1/bpnWvelwez3p1x1MDAxM1xuXHUwMDFjhckz0Mn59F6LXG6GIKKgO8OZeWp1NzvrbVxmP6ydjr6vXHUwMDA0KplfeXVwOohcdTAwMTIoppVgRFx1MDAwYlx1MDAwNVA2aJVcdTAwMGXCSENcdTAwMDcmXHUwMDEwpFx1MDAwZVanlUNzhlxixlx1MDAwMlxuXHUwMDFlTORsRSjzhOn8XHUwMDBmitDHpTU34/w6VGGSqYGUI4FcdTAwMTnn8u7dUZet7+xcdTAwMWRcdTAwMWStRHtYXHUwMDFk8uHpeEzaXHUwMDBiU1jtuV5vmJpcdTAwMTdP84QpXHUwMDA3NpxcdMKVRErX2Vx1MDAxNLZcYoXuiCPIKZg+UZ7HXFw5VHONtVx1MDAwNDBcdTAwMTGTV9mkvEkjwZTZPVwiz0GjJlCG03vQWG56XHUwMDFj5fvBXHUwMDBme+GJqo2uuoMgPK/tW4EpXFypLy3fzXrt2E39L61WzbxcdTAwMTBcdTAwMDZdS24rNJ060nngueHEnMdJafVgOjeITLrhN92O06BcdTAwMWJEbnhw89SwYrM+XHRcdTAwMTRO5W5a282MtdpcdTAwMDXyXHUwMDA3ibBcdTAwMWHymlwiXHUwMDE0UHpcIoird+9cdTAwMDPnPZxcdTAwMWZcdTAwMWZ8Xo/ib5tcdTAwMWZHW6uHy8qLXrlcYjFEXFyHKi6FgFpcdTAwMDZcdTAwMTFdr2ckkkWG4IgwTrSa7Vx1MDAwNt00XHUwMDE1XG7lXGJFNWPgXHUwMDAwZqpSlLxcdTAwMWFcdTAwMTVCI4Lv0/rNqsKeXHST51x1MDAxN2Bz1ifVnsDN0Yn2JKOSQ69992Ltx+r4cIVcdTAwMDdq6cTdPmDR2s5cdTAwMGZ/8PFltXd7LyEocygniDJbc6Ayxf2SXHUwMDFlNLpSI6lcdTAwMTDngqjZWompXHSQKEdcdTAwMGLoY1x1MDAwNII0o6tyejXaI5yx+9Rjs2ovM3lcdTAwMWVE3ez59XfdzE+pQYxlc7TMf1RqXHUwMDAxjcHd85//cXfps4/mN8+S4OPSh8V2XHUwMDEwz88/TIOkMf6EXHUwMDFhZNpB0MkjibWwbVJNhERxh2PMidJQqaLqfbcrKlSyw9tcdTAwMGZToYI4QDn0b1xiQ9yrdlx1MDAxZjeIUEDJXGZtwuP1RJeGa1x1MDAxZknhwd/n/vKmiFx1MDAwZldcdTAwMGU6cZxiubpQ3pmowXb/R1Ka2j/PpvBcdTAwMDUvXHUwMDBmzszzars552Oo+teFvk7WeuqTN4K0JETf48HbzTv/XCKqlreK2lxuXG60LyhcdTAwMTSuiuD6LWRcbjlPSlvTaY5cdTAwMTWYb3i+MYOoqVx1MDAwMz08gTDKtOQgVVROM1G11I5iXHUwMDFjXHUwMDBlXHUwMDAxzdubhldFblx1MDAxZlx1MDAwZlx1MDAxMlapXHJuV3kp3/9QIZcjXHUwMDE3XHUwMDBmLH5cdTAwMWYqzyx303wxiHxIanXHLv+lYuNcdTAwMGWJo1x1MDAxMLQ3tF5cIlx1MDAwNyvrk6BcblOoQVx1MDAxMKGVo7puYmOpXHUwMDAztVxmWFx1MDAxMId2XHUwMDFlXHUwMDEyXHUwMDE4u7J2XHUwMDEz+aVP9WW4Wb5cdTAwMTRcdTAwMGZcdTAwMDZBXHUwMDBlXHUwMDE3YDdcdTAwMGWivHlEsaJcdTAwMDUrvJ5xr6hcdTAwMWXOXFy1NVx1MDAxNZrYM9ZDbflprmS4+DL5/PXttUdPx8u+roBVnu5N9a+NzsVcdTAwMDQtN0n2c9j4yT5cdTAwMDFpgX9cdTAwMTlcXMtVts5cdTAwMDIzWrzuZnXxsnG/uNI2MJiCxos3XHUwMDE3/1x1MDAwMiHc1HkifQ== + + + + "dashboard""help""settings"Active diff --git a/docs/images/testing/snapshot_report_console_output.png b/docs/images/testing/snapshot_report_console_output.png new file mode 100644 index 0000000000..50389b4102 Binary files /dev/null and b/docs/images/testing/snapshot_report_console_output.png differ diff --git a/docs/images/testing/snapshot_report_diff_after.png b/docs/images/testing/snapshot_report_diff_after.png new file mode 100644 index 0000000000..99334082dd Binary files /dev/null and b/docs/images/testing/snapshot_report_diff_after.png differ diff --git a/docs/images/testing/snapshot_report_diff_before.png b/docs/images/testing/snapshot_report_diff_before.png new file mode 100644 index 0000000000..575cafd44b Binary files /dev/null and b/docs/images/testing/snapshot_report_diff_before.png differ diff --git a/docs/images/testing/snapshot_report_example.png b/docs/images/testing/snapshot_report_example.png new file mode 100644 index 0000000000..5cb49828f2 Binary files /dev/null and b/docs/images/testing/snapshot_report_example.png differ diff --git a/docs/index.md b/docs/index.md index c8e48c5748..1c06781407 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,11 +1,19 @@ -# Introduction +--- +hide: + - toc + - navigation +--- -Welcome to the [Textual](https://github.com/Textualize/textual) framework documentation. +!!! tip inline end + + See the navigation links in the header or side-bar. -!!! tip + Click :octicons-three-bars-16: (top left) on mobile. - See the navigation links in the header or side-bars. Click the :octicons-three-bars-16: button (top left) on mobile. +# Welcome + +Welcome to the [Textual](https://github.com/Textualize/textual) framework documentation. [Get started](./getting_started.md){ .md-button .md-button--primary } or go straight to the [Tutorial](./tutorial.md) @@ -16,7 +24,7 @@ Welcome to the [Textual](https://github.com/Textualize/textual) framework docume Textual is a *Rapid Application Development* framework for Python, built by [Textualize.io](https://www.textualize.io). -Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and (*coming soon*) a web browser. +Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal *or* a [web browser](https://github.com/Textualize/textual-web)! diff --git a/docs/roadmap.md b/docs/roadmap.md index 90e05d1e1d..d5a9f1a3b6 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -19,9 +19,8 @@ High-level features we plan on implementing. * [x] Monochrome mode * [ ] High contrast theme * [ ] Color-blind themes -- [ ] Command interface - * [ ] Command menu - * [ ] Fuzzy search +- [X] Command palette + * [X] Fuzzy search - [ ] Configuration (.toml based extensible configuration format) - [x] Console - [ ] Devtools @@ -75,8 +74,8 @@ Widgets are key to making user-friendly interfaces. The builtin widgets should c - [X] Spark-lines - [X] Switch - [X] Tabs -- [ ] TextArea (multi-line input) - * [ ] Basic controls +- [X] TextArea (multi-line input) + * [X] Basic controls * [ ] Indentation guides * [ ] Smart features for various languages - * [ ] Syntax highlighting + * [X] Syntax highlighting diff --git a/docs/snippets/border_sub_title_align_all_example.md b/docs/snippets/border_sub_title_align_all_example.md index 22049c434e..6550a92ca0 100644 --- a/docs/snippets/border_sub_title_align_all_example.md +++ b/docs/snippets/border_sub_title_align_all_example.md @@ -24,10 +24,10 @@ Open the code tabs to see the details of the code examples. 10. The title and subtitle are aligned on the right and very long, so they get truncated and we can still see the leftmost character of the border edge. 11. An auxiliary function to create labels with border title and subtitle. -=== "border_sub_title_align_all.css" +=== "border_sub_title_align_all.tcss" ```sass hl_lines="12 16 30 34 41 46" - --8<-- "docs/examples/styles/border_sub_title_align_all.css" + --8<-- "docs/examples/styles/border_sub_title_align_all.tcss" ``` 1. The default alignment for the title is `left` and the default alignment for the subtitle is `right`. diff --git a/docs/snippets/border_title_color.md b/docs/snippets/border_title_color.md index c2a69052c2..36b473c24a 100644 --- a/docs/snippets/border_title_color.md +++ b/docs/snippets/border_title_color.md @@ -11,8 +11,8 @@ The following examples demonstrates customization of the border color and text s --8<-- "docs/examples/styles/border_title_colors.py" ``` -=== "border_title_colors.css" +=== "border_title_colors.tcss" ```sass - --8<-- "docs/examples/styles/border_title_colors.css" + --8<-- "docs/examples/styles/border_title_colors.tcss" ``` diff --git a/docs/snippets/border_vs_outline_example.md b/docs/snippets/border_vs_outline_example.md index 7b177b7ded..55d035f47d 100644 --- a/docs/snippets/border_vs_outline_example.md +++ b/docs/snippets/border_vs_outline_example.md @@ -14,8 +14,8 @@ This example also shows that a widget cannot contain both a `border` and an `out --8<-- "docs/examples/styles/outline_vs_border.py" ``` -=== "outline_vs_border.css" +=== "outline_vs_border.tcss" ```sass hl_lines="5-7 9-11" - --8<-- "docs/examples/styles/outline_vs_border.css" + --8<-- "docs/examples/styles/outline_vs_border.tcss" ``` diff --git a/docs/styles/_template.md b/docs/styles/_template.md index b01be7e8f7..5cddb8919c 100644 --- a/docs/styles/_template.md +++ b/docs/styles/_template.md @@ -44,10 +44,10 @@ Short description of the first example. --8<-- "docs/examples/styles/style.py" ``` -=== "style.css" +=== "style.tcss" ```sass - --8<-- "docs/examples/styles/style.css" + --8<-- "docs/examples/styles/style.tcss" ``` --> @@ -66,10 +66,10 @@ Short description of the second example. --8<-- "docs/examples/styles/style.py" ``` -=== "style.css" +=== "style.tcss" ```sass - --8<-- "docs/examples/styles/style.css" + --8<-- "docs/examples/styles/style.tcss" ``` --> diff --git a/docs/styles/align.md b/docs/styles/align.md index bdcca7cd6f..810e26303a 100644 --- a/docs/styles/align.md +++ b/docs/styles/align.md @@ -32,10 +32,10 @@ This example contains a simple app with two labels centered on the screen with ` --8<-- "docs/examples/styles/align.py" ``` -=== "align.css" +=== "align.tcss" ```sass hl_lines="2" - --8<-- "docs/examples/styles/align.css" + --8<-- "docs/examples/styles/align.tcss" ``` ### All alignments @@ -54,10 +54,10 @@ Each label has been aligned differently inside its container, and its text shows --8<-- "docs/examples/styles/align_all.py" ``` -=== "align_all.css" +=== "align_all.tcss" ```sass hl_lines="2 6 10 14 18 22 26 30 34" - --8<-- "docs/examples/styles/align_all.css" + --8<-- "docs/examples/styles/align_all.tcss" ``` ## CSS diff --git a/docs/styles/background.md b/docs/styles/background.md index b89113502e..9a1c4f04f2 100644 --- a/docs/styles/background.md +++ b/docs/styles/background.md @@ -27,10 +27,10 @@ This example creates three widgets and applies a different background to each. --8<-- "docs/examples/styles/background.py" ``` -=== "background.css" +=== "background.tcss" ```sass hl_lines="9 13 17" - --8<-- "docs/examples/styles/background.css" + --8<-- "docs/examples/styles/background.tcss" ``` ### Different opacity settings @@ -48,10 +48,10 @@ The next example creates ten widgets laid out side by side to show the effect of --8<-- "docs/examples/styles/background_transparency.py" ``` -=== "background_transparency.css" +=== "background_transparency.tcss" ```sass hl_lines="2 6 10 14 18 22 26 30 34 38" - --8<-- "docs/examples/styles/background_transparency.css" + --8<-- "docs/examples/styles/background_transparency.tcss" ``` ## CSS diff --git a/docs/styles/border.md b/docs/styles/border.md index f6be998f9c..71643fd4fe 100644 --- a/docs/styles/border.md +++ b/docs/styles/border.md @@ -51,10 +51,10 @@ This examples shows three widgets with different border styles. --8<-- "docs/examples/styles/border.py" ``` -=== "border.css" +=== "border.tcss" ```sass hl_lines="4 10 16" - --8<-- "docs/examples/styles/border.css" + --8<-- "docs/examples/styles/border.tcss" ``` ### All border types @@ -72,10 +72,10 @@ The next example shows a grid with all the available border types. --8<-- "docs/examples/styles/border_all.py" ``` -=== "border_all.css" +=== "border_all.tcss" ```sass - --8<-- "docs/examples/styles/border_all.css" + --8<-- "docs/examples/styles/border_all.tcss" ``` ### Borders and outlines diff --git a/docs/styles/border_subtitle_align.md b/docs/styles/border_subtitle_align.md index a6c0533b79..7723062cdb 100644 --- a/docs/styles/border_subtitle_align.md +++ b/docs/styles/border_subtitle_align.md @@ -33,10 +33,10 @@ This example shows three labels, each with a different border subtitle alignment --8<-- "docs/examples/styles/border_subtitle_align.py" ``` -=== "border_subtitle_align.css" +=== "border_subtitle_align.tcss" ```sass - --8<-- "docs/examples/styles/border_subtitle_align.css" + --8<-- "docs/examples/styles/border_subtitle_align.tcss" ``` diff --git a/docs/styles/border_title_align.md b/docs/styles/border_title_align.md index 7b059dfb71..f2d9a61f9f 100644 --- a/docs/styles/border_title_align.md +++ b/docs/styles/border_title_align.md @@ -33,10 +33,10 @@ This example shows three labels, each with a different border title alignment: --8<-- "docs/examples/styles/border_title_align.py" ``` -=== "border_title_align.css" +=== "border_title_align.tcss" ```sass - --8<-- "docs/examples/styles/border_title_align.css" + --8<-- "docs/examples/styles/border_title_align.tcss" ``` diff --git a/docs/styles/box_sizing.md b/docs/styles/box_sizing.md index c6f08cdd2a..147929f8ba 100644 --- a/docs/styles/box_sizing.md +++ b/docs/styles/box_sizing.md @@ -32,10 +32,10 @@ The bottom widget has `box-sizing: content-box` which increases the size of the --8<-- "docs/examples/styles/box_sizing.py" ``` -=== "box_sizing.css" +=== "box_sizing.tcss" ```sass hl_lines="2 6" - --8<-- "docs/examples/styles/box_sizing.css" + --8<-- "docs/examples/styles/box_sizing.tcss" ``` ## CSS diff --git a/docs/styles/color.md b/docs/styles/color.md index 9b45f4b83c..49b55dbb00 100644 --- a/docs/styles/color.md +++ b/docs/styles/color.md @@ -29,10 +29,10 @@ This example sets a different text color for each of three different widgets. --8<-- "docs/examples/styles/color.py" ``` -=== "color.css" +=== "color.tcss" ```sass hl_lines="8 12 16" - --8<-- "docs/examples/styles/color.css" + --8<-- "docs/examples/styles/color.tcss" ``` ### Auto @@ -50,10 +50,10 @@ The next example shows how `auto` chooses between a lighter or a darker text col --8<-- "docs/examples/styles/color_auto.py" ``` -=== "color_auto.css" +=== "color_auto.tcss" ```sass hl_lines="2" - --8<-- "docs/examples/styles/color_auto.css" + --8<-- "docs/examples/styles/color_auto.tcss" ``` ## CSS diff --git a/docs/styles/content_align.md b/docs/styles/content_align.md index a35abc3e55..63d0ba298f 100644 --- a/docs/styles/content_align.md +++ b/docs/styles/content_align.md @@ -37,10 +37,10 @@ This first example shows three labels stacked vertically, each with different co --8<-- "docs/examples/styles/content_align.py" ``` -=== "content_align.css" +=== "content_align.tcss" ```sass hl_lines="2 7-8 13" - --8<-- "docs/examples/styles/content_align.css" + --8<-- "docs/examples/styles/content_align.tcss" ``` ### All content alignments @@ -59,10 +59,10 @@ Each label has its text aligned differently. --8<-- "docs/examples/styles/content_align_all.py" ``` -=== "content_align_all.css" +=== "content_align_all.tcss" ```sass hl_lines="2 5 8 11 14 17 20 23 26" - --8<-- "docs/examples/styles/content_align_all.css" + --8<-- "docs/examples/styles/content_align_all.tcss" ``` ## CSS diff --git a/docs/styles/display.md b/docs/styles/display.md index 34fbb3165a..6a40dfcb54 100644 --- a/docs/styles/display.md +++ b/docs/styles/display.md @@ -30,10 +30,10 @@ Note that the second widget is hidden by adding the `"remove"` class which sets --8<-- "docs/examples/styles/display.py" ``` -=== "display.css" +=== "display.tcss" ```sass hl_lines="13" - --8<-- "docs/examples/styles/display.css" + --8<-- "docs/examples/styles/display.tcss" ``` ## CSS diff --git a/docs/styles/dock.md b/docs/styles/dock.md index 09135d3000..25464c5f7b 100644 --- a/docs/styles/dock.md +++ b/docs/styles/dock.md @@ -28,10 +28,10 @@ Notice that even though the content is scrolled, the sidebar remains fixed. --8<-- "docs/examples/guide/layout/dock_layout1_sidebar.py" ``` -=== "dock_layout1_sidebar.css" +=== "dock_layout1_sidebar.tcss" ```sass hl_lines="2" - --8<-- "docs/examples/guide/layout/dock_layout1_sidebar.css" + --8<-- "docs/examples/guide/layout/dock_layout1_sidebar.tcss" ``` ### Advanced usage @@ -50,10 +50,10 @@ The labels will remain in that position (docked) even if the container they are --8<-- "docs/examples/styles/dock_all.py" ``` -=== "dock_all.css" +=== "dock_all.tcss" ```sass hl_lines="2-5 8-11 14-17 20-23" - --8<-- "docs/examples/styles/dock_all.css" + --8<-- "docs/examples/styles/dock_all.tcss" ``` ## CSS diff --git a/docs/styles/grid/column_span.md b/docs/styles/grid/column_span.md index d15c1a4748..d712edb835 100644 --- a/docs/styles/grid/column_span.md +++ b/docs/styles/grid/column_span.md @@ -29,10 +29,10 @@ The example below shows a 4 by 4 grid where many placeholders span over several --8<-- "docs/examples/styles/column_span.py" ``` -=== "column_span.css" +=== "column_span.tcss" ```sass hl_lines="2 5 8 11 14 20" - --8<-- "docs/examples/styles/column_span.css" + --8<-- "docs/examples/styles/column_span.tcss" ``` ## CSS diff --git a/docs/styles/grid/grid_columns.md b/docs/styles/grid/grid_columns.md index 25435326b8..89b589c6d6 100644 --- a/docs/styles/grid/grid_columns.md +++ b/docs/styles/grid/grid_columns.md @@ -40,10 +40,10 @@ Because there are more rows than scalars in the style definition, the scalars wi --8<-- "docs/examples/styles/grid_columns.py" ``` -=== "grid_columns.css" +=== "grid_columns.tcss" ```sass hl_lines="3" - --8<-- "docs/examples/styles/grid_columns.css" + --8<-- "docs/examples/styles/grid_columns.tcss" ``` ## CSS diff --git a/docs/styles/grid/grid_gutter.md b/docs/styles/grid/grid_gutter.md index 7abb7c7f1d..39e8981c55 100644 --- a/docs/styles/grid/grid_gutter.md +++ b/docs/styles/grid/grid_gutter.md @@ -13,11 +13,11 @@ No spacing is added between the edges of the cells and the edges of the containe ## Syntax --8<-- "docs/snippets/syntax_block_start.md" -grid-gutter: <scalar> [<scalar>]; +grid-gutter: <integer> [<integer>]; --8<-- "docs/snippets/syntax_block_end.md" -The `grid-gutter` style takes one or two [``](../../css_types/scalar.md) that set the length of the gutter along the vertical and horizontal axes. -If only one [``](../../css_types/scalar.md) is supplied, it sets the vertical and horizontal gutters. +The `grid-gutter` style takes one or two [``](../../css_types/integer.md) that set the length of the gutter along the vertical and horizontal axes. +If only one [``](../../css_types/integer.md) is supplied, it sets the vertical and horizontal gutters. If two are supplied, they set the vertical and horizontal gutters, respectively. ## Example @@ -35,10 +35,10 @@ The example below employs a common trick to apply visually consistent spacing ar --8<-- "docs/examples/styles/grid_gutter.py" ``` -=== "grid_gutter.css" +=== "grid_gutter.tcss" ```sass hl_lines="3" - --8<-- "docs/examples/styles/grid_gutter.css" + --8<-- "docs/examples/styles/grid_gutter.tcss" ``` 1. We set the horizontal gutter to be double the vertical gutter because terminal cells are typically two times taller than they are wide. Thus, the result shows visually consistent spacing around grid cells. @@ -47,7 +47,7 @@ The example below employs a common trick to apply visually consistent spacing ar ```sass /* Set vertical and horizontal gutters to be the same */ -grid-gutter: 5%; +grid-gutter: 5; /* Set vertical and horizontal gutters separately */ grid-gutter: 1 2; diff --git a/docs/styles/grid/grid_rows.md b/docs/styles/grid/grid_rows.md index 29f8939421..816ce708ef 100644 --- a/docs/styles/grid/grid_rows.md +++ b/docs/styles/grid/grid_rows.md @@ -40,10 +40,10 @@ Because there are more rows than scalars in the style definition, the scalars wi --8<-- "docs/examples/styles/grid_rows.py" ``` -=== "grid_rows.css" +=== "grid_rows.tcss" ```sass hl_lines="3" - --8<-- "docs/examples/styles/grid_rows.css" + --8<-- "docs/examples/styles/grid_rows.tcss" ``` ## CSS diff --git a/docs/styles/grid/grid_size.md b/docs/styles/grid/grid_size.md index b1cdcc1bb8..b225b858fc 100644 --- a/docs/styles/grid/grid_size.md +++ b/docs/styles/grid/grid_size.md @@ -35,10 +35,10 @@ In the first example, we create a grid with 2 columns and 5 rows, although we do --8<-- "docs/examples/styles/grid_size_both.py" ``` -=== "grid_size_both.css" +=== "grid_size_both.tcss" ```sass hl_lines="2" - --8<-- "docs/examples/styles/grid_size_both.css" + --8<-- "docs/examples/styles/grid_size_both.tcss" ``` 1. Create a grid with 2 columns and 4 rows. @@ -58,10 +58,10 @@ In the second example, we create a grid with 2 columns and however many rows are --8<-- "docs/examples/styles/grid_size_columns.py" ``` -=== "grid_size_columns.css" +=== "grid_size_columns.tcss" ```sass hl_lines="2" - --8<-- "docs/examples/styles/grid_size_columns.css" + --8<-- "docs/examples/styles/grid_size_columns.tcss" ``` 1. Create a grid with 2 columns and however many rows. diff --git a/docs/styles/grid/index.md b/docs/styles/grid/index.md index 6263c88cc5..012365e00e 100644 --- a/docs/styles/grid/index.md +++ b/docs/styles/grid/index.md @@ -49,10 +49,10 @@ The spacing between grid cells is defined by the `grid-gutter` style. --8<-- "docs/examples/styles/grid.py" ``` -=== "grid.css" +=== "grid.tcss" ```sass - --8<-- "docs/examples/styles/grid.css" + --8<-- "docs/examples/styles/grid.tcss" ``` !!! warning diff --git a/docs/styles/grid/row_span.md b/docs/styles/grid/row_span.md index e8fcbcb087..145015e434 100644 --- a/docs/styles/grid/row_span.md +++ b/docs/styles/grid/row_span.md @@ -32,10 +32,10 @@ After placing the placeholders `#p1`, `#p2`, `#p3`, and `#p4`, the next availabl --8<-- "docs/examples/styles/row_span.py" ``` -=== "row_span.css" +=== "row_span.tcss" ```sass hl_lines="2 5 8 11 14 17 20" - --8<-- "docs/examples/styles/row_span.css" + --8<-- "docs/examples/styles/row_span.tcss" ``` ## CSS diff --git a/docs/styles/height.md b/docs/styles/height.md index 329a8bf09b..e3f8b980c0 100644 --- a/docs/styles/height.md +++ b/docs/styles/height.md @@ -28,10 +28,10 @@ This examples creates a widget with a height of 50% of the screen. --8<-- "docs/examples/styles/height.py" ``` -=== "height.css" +=== "height.tcss" ```sass hl_lines="3" - --8<-- "docs/examples/styles/height.css" + --8<-- "docs/examples/styles/height.tcss" ``` ### All height formats @@ -53,10 +53,10 @@ Open the CSS file tab to see the comments that explain how each height is comput 1. The id of the placeholder identifies which unit will be used to set the height of the widget. -=== "height_comparison.css" +=== "height_comparison.tcss" ```sass hl_lines="2 5 8 11 14 17 20 23 26" - --8<-- "docs/examples/styles/height_comparison.css" + --8<-- "docs/examples/styles/height_comparison.tcss" ``` 1. This sets the height to 2 lines. diff --git a/docs/styles/layer.md b/docs/styles/layer.md index 43dabed456..d1504dd592 100644 --- a/docs/styles/layer.md +++ b/docs/styles/layer.md @@ -35,10 +35,10 @@ However, since `#box1` is on the higher layer, it is drawn on top of `#box2`. --8<-- "docs/examples/guide/layout/layers.py" ``` -=== "layers.css" +=== "layers.tcss" ```sass hl_lines="3 14 19" - --8<-- "docs/examples/guide/layout/layers.css" + --8<-- "docs/examples/guide/layout/layers.tcss" ``` ## CSS diff --git a/docs/styles/layers.md b/docs/styles/layers.md index 3259ec4ea5..685b5659cf 100644 --- a/docs/styles/layers.md +++ b/docs/styles/layers.md @@ -33,10 +33,10 @@ However, since `#box1` is on the higher layer, it is drawn on top of `#box2`. --8<-- "docs/examples/guide/layout/layers.py" ``` -=== "layers.css" +=== "layers.tcss" ```sass hl_lines="3 14 19" - --8<-- "docs/examples/guide/layout/layers.css" + --8<-- "docs/examples/guide/layout/layers.tcss" ``` ## CSS diff --git a/docs/styles/layout.md b/docs/styles/layout.md index ab62f02c23..deda25d0cf 100644 --- a/docs/styles/layout.md +++ b/docs/styles/layout.md @@ -36,10 +36,10 @@ To learn more about the grid layout, you can see the [layout guide](../guide/lay --8<-- "docs/examples/styles/layout.py" ``` -=== "layout.css" +=== "layout.tcss" ```sass hl_lines="2 8" - --8<-- "docs/examples/styles/layout.css" + --8<-- "docs/examples/styles/layout.tcss" ``` ## CSS diff --git a/docs/styles/links/index.md b/docs/styles/links/index.md index 6ae29ee042..f2984ba046 100644 --- a/docs/styles/links/index.md +++ b/docs/styles/links/index.md @@ -50,10 +50,10 @@ The second label uses CSS to customize the link color, background, and style. --8<-- "docs/examples/styles/links.py" ``` -=== "links.css" +=== "links.tcss" ```sass - --8<-- "docs/examples/styles/links.css" + --8<-- "docs/examples/styles/links.tcss" ``` ## Additional Notes diff --git a/docs/styles/links/link_background.md b/docs/styles/links/link_background.md index 1040dfd7b4..a9ccc96cbb 100644 --- a/docs/styles/links/link_background.md +++ b/docs/styles/links/link_background.md @@ -35,10 +35,10 @@ It also shows that `link-background` does not affect hyperlinks. 3. This label has an "action link" that can be styled with `link-background`. 4. This label has an "action link" that can be styled with `link-background`. -=== "link_background.css" +=== "link_background.tcss" ```sass hl_lines="2 6 10" - --8<-- "docs/examples/styles/link_background.css" + --8<-- "docs/examples/styles/link_background.tcss" ``` 1. This will only affect one of the labels because action links are the only links that this rule affects. diff --git a/docs/styles/links/link_color.md b/docs/styles/links/link_color.md index 4a49fc95b0..44e0cd72ac 100644 --- a/docs/styles/links/link_color.md +++ b/docs/styles/links/link_color.md @@ -35,10 +35,10 @@ It also shows that `link-color` does not affect hyperlinks. 3. This label has an "action link" that can be styled with `link-color`. 4. This label has an "action link" that can be styled with `link-color`. -=== "link_color.css" +=== "link_color.tcss" ```sass hl_lines="2 6 10" - --8<-- "docs/examples/styles/link_color.css" + --8<-- "docs/examples/styles/link_color.tcss" ``` 1. This will only affect one of the labels because action links are the only links that this rule affects. diff --git a/docs/styles/links/link_hover_background.md b/docs/styles/links/link_hover_background.md index 732054755e..e396e0c615 100644 --- a/docs/styles/links/link_hover_background.md +++ b/docs/styles/links/link_hover_background.md @@ -44,10 +44,10 @@ It also shows that `link-hover-background` does not affect hyperlinks. 3. This label has an "action link" that can be styled with `link-hover-background`. 4. This label has an "action link" that can be styled with `link-hover-background`. -=== "link_hover_background.css" +=== "link_hover_background.tcss" ```sass hl_lines="2 6 10" - --8<-- "docs/examples/styles/link_hover_background.css" + --8<-- "docs/examples/styles/link_hover_background.tcss" ``` 1. This will only affect one of the labels because action links are the only links that this rule affects. diff --git a/docs/styles/links/link_hover_color.md b/docs/styles/links/link_hover_color.md index 2b9a97f356..b525647314 100644 --- a/docs/styles/links/link_hover_color.md +++ b/docs/styles/links/link_hover_color.md @@ -48,10 +48,10 @@ It also shows that `link-hover-color` does not affect hyperlinks. 3. This label has an "action link" that can be styled with `link-hover-color`. 4. This label has an "action link" that can be styled with `link-hover-color`. -=== "link_hover_color.css" +=== "link_hover_color.tcss" ```sass hl_lines="2 6 10" - --8<-- "docs/examples/styles/link_hover_color.css" + --8<-- "docs/examples/styles/link_hover_color.tcss" ``` 1. This will only affect one of the labels because action links are the only links that this rule affects. diff --git a/docs/styles/links/link_hover_style.md b/docs/styles/links/link_hover_style.md index ec3ec3f22a..53cee01ec4 100644 --- a/docs/styles/links/link_hover_style.md +++ b/docs/styles/links/link_hover_style.md @@ -48,10 +48,10 @@ It also shows that `link-hover-style` does not affect hyperlinks. 3. This label has an "action link" that can be styled with `link-hover-style`. 4. This label has an "action link" that can be styled with `link-hover-style`. -=== "link_hover_style.css" +=== "link_hover_style.tcss" ```sass hl_lines="2 6 10" - --8<-- "docs/examples/styles/link_hover_style.css" + --8<-- "docs/examples/styles/link_hover_style.tcss" ``` 1. This will only affect one of the labels because action links are the only links that this rule affects. diff --git a/docs/styles/links/link_style.md b/docs/styles/links/link_style.md index 529ebbdbf2..b5d100b8c5 100644 --- a/docs/styles/links/link_style.md +++ b/docs/styles/links/link_style.md @@ -39,10 +39,10 @@ It also shows that `link-style` does not affect hyperlinks. 3. This label has an "action link" that can be styled with `link-style`. 4. This label has an "action link" that can be styled with `link-style`. -=== "link_style.css" +=== "link_style.tcss" ```sass hl_lines="2 6 10" - --8<-- "docs/examples/styles/link_style.css" + --8<-- "docs/examples/styles/link_style.tcss" ``` 1. This will only affect one of the labels because action links are the only links that this rule affects. diff --git a/docs/styles/margin.md b/docs/styles/margin.md index 6fcdc8c8e0..a8f47832ea 100644 --- a/docs/styles/margin.md +++ b/docs/styles/margin.md @@ -49,10 +49,10 @@ In the example below we add a large margin to a label, which makes it move away --8<-- "docs/examples/styles/margin.py" ``` -=== "margin.css" +=== "margin.tcss" ```sass hl_lines="7" - --8<-- "docs/examples/styles/margin.css" + --8<-- "docs/examples/styles/margin.tcss" ``` ### All margin settings @@ -71,10 +71,10 @@ In each cell, we have a placeholder that has its margins set in different ways. --8<-- "docs/examples/styles/margin_all.py" ``` -=== "margin_all.css" +=== "margin_all.tcss" ```sass hl_lines="25 29 33 37 41 45 49 53" - --8<-- "docs/examples/styles/margin_all.css" + --8<-- "docs/examples/styles/margin_all.tcss" ``` ## CSS diff --git a/docs/styles/max_height.md b/docs/styles/max_height.md index 70671c4c8d..d23faa9bab 100644 --- a/docs/styles/max_height.md +++ b/docs/styles/max_height.md @@ -27,10 +27,10 @@ Then, we set `max-height` individually on each placeholder. --8<-- "docs/examples/styles/max_height.py" ``` -=== "max_height.css" +=== "max_height.tcss" ```sass hl_lines="12 16 20 24" - --8<-- "docs/examples/styles/max_height.css" + --8<-- "docs/examples/styles/max_height.tcss" ``` 1. This won't affect the placeholder because its height is less than the maximum height. diff --git a/docs/styles/max_width.md b/docs/styles/max_width.md index 5556647397..5d4596ad05 100644 --- a/docs/styles/max_width.md +++ b/docs/styles/max_width.md @@ -27,10 +27,10 @@ Then, we set `max-width` individually on each placeholder. --8<-- "docs/examples/styles/max_width.py" ``` -=== "max_width.css" +=== "max_width.tcss" ```sass hl_lines="12 16 20 24" - --8<-- "docs/examples/styles/max_width.css" + --8<-- "docs/examples/styles/max_width.tcss" ``` 1. This won't affect the placeholder because its width is less than the maximum width. diff --git a/docs/styles/min_height.md b/docs/styles/min_height.md index 4118fd3906..6c23958cc1 100644 --- a/docs/styles/min_height.md +++ b/docs/styles/min_height.md @@ -27,10 +27,10 @@ Then, we set `min-height` individually on each placeholder. --8<-- "docs/examples/styles/min_height.py" ``` -=== "min_height.css" +=== "min_height.tcss" ```sass hl_lines="13 17 21 25" - --8<-- "docs/examples/styles/min_height.css" + --8<-- "docs/examples/styles/min_height.tcss" ``` 1. This won't affect the placeholder because its height is larger than the minimum height. diff --git a/docs/styles/min_width.md b/docs/styles/min_width.md index 8c3e14a460..a8771fc0b3 100644 --- a/docs/styles/min_width.md +++ b/docs/styles/min_width.md @@ -27,10 +27,10 @@ Then, we set `min-width` individually on each placeholder. --8<-- "docs/examples/styles/min_width.py" ``` -=== "min_width.css" +=== "min_width.tcss" ```sass hl_lines="13 17 21 25" - --8<-- "docs/examples/styles/min_width.css" + --8<-- "docs/examples/styles/min_width.tcss" ``` 1. This won't affect the placeholder because its width is larger than the minimum width. diff --git a/docs/styles/offset.md b/docs/styles/offset.md index a9ee2b6f44..47c836166b 100644 --- a/docs/styles/offset.md +++ b/docs/styles/offset.md @@ -30,10 +30,10 @@ In this example, we have 3 widgets with differing offsets. --8<-- "docs/examples/styles/offset.py" ``` -=== "offset.css" +=== "offset.tcss" ```sass hl_lines="13 20 27" - --8<-- "docs/examples/styles/offset.css" + --8<-- "docs/examples/styles/offset.tcss" ``` ## CSS diff --git a/docs/styles/opacity.md b/docs/styles/opacity.md index 9b6e4303fd..69b657401e 100644 --- a/docs/styles/opacity.md +++ b/docs/styles/opacity.md @@ -34,10 +34,10 @@ When the opacity is zero, all we see is the (black) background. --8<-- "docs/examples/styles/opacity.py" ``` -=== "opacity.css" +=== "opacity.tcss" ```sass hl_lines="2 6 10 14 18" - --8<-- "docs/examples/styles/opacity.css" + --8<-- "docs/examples/styles/opacity.tcss" ``` ## CSS diff --git a/docs/styles/outline.md b/docs/styles/outline.md index 0de97a3ac4..b3fb75ff2a 100644 --- a/docs/styles/outline.md +++ b/docs/styles/outline.md @@ -48,10 +48,10 @@ Note how the outline occludes the text area. --8<-- "docs/examples/styles/outline.py" ``` -=== "outline.css" +=== "outline.tcss" ```sass hl_lines="8" - --8<-- "docs/examples/styles/outline.css" + --8<-- "docs/examples/styles/outline.tcss" ``` ### All outline types @@ -69,10 +69,10 @@ The next example shows a grid with all the available outline types. --8<-- "docs/examples/styles/outline_all.py" ``` -=== "outline_all.css" +=== "outline_all.tcss" ```sass hl_lines="2 6 10 14 18 22 26 30 34 38 42 46 50 54 58" - --8<-- "docs/examples/styles/outline_all.css" + --8<-- "docs/examples/styles/outline_all.tcss" ``` ### Borders and outlines diff --git a/docs/styles/overflow.md b/docs/styles/overflow.md index 110e3dc580..d4807ae4dd 100644 --- a/docs/styles/overflow.md +++ b/docs/styles/overflow.md @@ -45,10 +45,10 @@ The right side has `overflow-y: hidden` which will prevent a scrollbar from bein --8<-- "docs/examples/styles/overflow.py" ``` -=== "overflow.css" +=== "overflow.tcss" ```sass hl_lines="19" - --8<-- "docs/examples/styles/overflow.css" + --8<-- "docs/examples/styles/overflow.tcss" ``` ## CSS diff --git a/docs/styles/padding.md b/docs/styles/padding.md index 0a9c88f265..a26d767b9a 100644 --- a/docs/styles/padding.md +++ b/docs/styles/padding.md @@ -48,10 +48,10 @@ This example adds padding around some text. --8<-- "docs/examples/styles/padding.py" ``` -=== "padding.css" +=== "padding.tcss" ```sass hl_lines="7" - --8<-- "docs/examples/styles/padding.css" + --8<-- "docs/examples/styles/padding.tcss" ``` ### All padding settings @@ -71,10 +71,10 @@ The effect of each padding setting is noticeable in the colored background aroun --8<-- "docs/examples/styles/padding_all.py" ``` -=== "padding_all.css" +=== "padding_all.tcss" ```sass hl_lines="16 20 24 28 32 36 40 44" - --8<-- "docs/examples/styles/padding_all.css" + --8<-- "docs/examples/styles/padding_all.tcss" ``` ## CSS diff --git a/docs/styles/scrollbar_colors/index.md b/docs/styles/scrollbar_colors/index.md index fce1aeb9ad..c0ef25a37e 100644 --- a/docs/styles/scrollbar_colors/index.md +++ b/docs/styles/scrollbar_colors/index.md @@ -49,8 +49,8 @@ The right panel sets `scrollbar-background`, `scrollbar-color`, and `scrollbar-c --8<-- "docs/examples/styles/scrollbars.py" ``` -=== "scrollbars.css" +=== "scrollbars.tcss" ```sass - --8<-- "docs/examples/styles/scrollbars.css" + --8<-- "docs/examples/styles/scrollbars.tcss" ``` diff --git a/docs/styles/scrollbar_colors/scrollbar_background.md b/docs/styles/scrollbar_colors/scrollbar_background.md index 7b901a11e1..5dff38c3e1 100644 --- a/docs/styles/scrollbar_colors/scrollbar_background.md +++ b/docs/styles/scrollbar_colors/scrollbar_background.md @@ -26,10 +26,10 @@ The `scrollbar-background` style sets the background color of the scrollbar. --8<-- "docs/examples/styles/scrollbars2.py" ``` -=== "scrollbars2.css" +=== "scrollbars2.tcss" ```sass hl_lines="2" - --8<-- "docs/examples/styles/scrollbars2.css" + --8<-- "docs/examples/styles/scrollbars2.tcss" ``` ## CSS diff --git a/docs/styles/scrollbar_colors/scrollbar_background_active.md b/docs/styles/scrollbar_colors/scrollbar_background_active.md index 54e68d640d..41e687f582 100644 --- a/docs/styles/scrollbar_colors/scrollbar_background_active.md +++ b/docs/styles/scrollbar_colors/scrollbar_background_active.md @@ -27,10 +27,10 @@ The `scrollbar-background-active` style sets the background color of the scrollb --8<-- "docs/examples/styles/scrollbars2.py" ``` -=== "scrollbars2.css" +=== "scrollbars2.tcss" ```sass hl_lines="3" - --8<-- "docs/examples/styles/scrollbars2.css" + --8<-- "docs/examples/styles/scrollbars2.tcss" ``` ## CSS diff --git a/docs/styles/scrollbar_colors/scrollbar_background_hover.md b/docs/styles/scrollbar_colors/scrollbar_background_hover.md index 8ae8f7aed0..caaa552a10 100644 --- a/docs/styles/scrollbar_colors/scrollbar_background_hover.md +++ b/docs/styles/scrollbar_colors/scrollbar_background_hover.md @@ -27,10 +27,10 @@ The `scrollbar-background-hover` style sets the background color of the scrollba --8<-- "docs/examples/styles/scrollbars2.py" ``` -=== "scrollbars2.css" +=== "scrollbars2.tcss" ```sass hl_lines="4" - --8<-- "docs/examples/styles/scrollbars2.css" + --8<-- "docs/examples/styles/scrollbars2.tcss" ``` ## CSS diff --git a/docs/styles/scrollbar_colors/scrollbar_color.md b/docs/styles/scrollbar_colors/scrollbar_color.md index b8ea43fe3b..dac2d0daa7 100644 --- a/docs/styles/scrollbar_colors/scrollbar_color.md +++ b/docs/styles/scrollbar_colors/scrollbar_color.md @@ -27,10 +27,10 @@ The `scrollbar-color` style sets the color of the scrollbar. --8<-- "docs/examples/styles/scrollbars2.py" ``` -=== "scrollbars2.css" +=== "scrollbars2.tcss" ```sass hl_lines="5" - --8<-- "docs/examples/styles/scrollbars2.css" + --8<-- "docs/examples/styles/scrollbars2.tcss" ``` ## CSS diff --git a/docs/styles/scrollbar_colors/scrollbar_color_active.md b/docs/styles/scrollbar_colors/scrollbar_color_active.md index 473c82309e..34ffeff813 100644 --- a/docs/styles/scrollbar_colors/scrollbar_color_active.md +++ b/docs/styles/scrollbar_colors/scrollbar_color_active.md @@ -27,10 +27,10 @@ The `scrollbar-color-active` style sets the color of the scrollbar when the thum --8<-- "docs/examples/styles/scrollbars2.py" ``` -=== "scrollbars2.css" +=== "scrollbars2.tcss" ```sass hl_lines="6" - --8<-- "docs/examples/styles/scrollbars2.css" + --8<-- "docs/examples/styles/scrollbars2.tcss" ``` ## CSS diff --git a/docs/styles/scrollbar_colors/scrollbar_color_hover.md b/docs/styles/scrollbar_colors/scrollbar_color_hover.md index c08703cce5..25e06b436e 100644 --- a/docs/styles/scrollbar_colors/scrollbar_color_hover.md +++ b/docs/styles/scrollbar_colors/scrollbar_color_hover.md @@ -27,10 +27,10 @@ The `scrollbar-color-hover` style sets the color of the scrollbar when the curso --8<-- "docs/examples/styles/scrollbars2.py" ``` -=== "scrollbars2.css" +=== "scrollbars2.tcss" ```sass hl_lines="7" - --8<-- "docs/examples/styles/scrollbars2.css" + --8<-- "docs/examples/styles/scrollbars2.tcss" ``` ## CSS diff --git a/docs/styles/scrollbar_colors/scrollbar_corner_color.md b/docs/styles/scrollbar_colors/scrollbar_corner_color.md index 11584ebb5c..7482cd62a1 100644 --- a/docs/styles/scrollbar_colors/scrollbar_corner_color.md +++ b/docs/styles/scrollbar_colors/scrollbar_corner_color.md @@ -25,10 +25,10 @@ The example below sets the scrollbar corner (bottom-right corner of the screen) --8<-- "docs/examples/styles/scrollbar_corner_color.py" ``` -=== "scrollbar_corner_color.css" +=== "scrollbar_corner_color.tcss" ```sass hl_lines="3" - --8<-- "docs/examples/styles/scrollbar_corner_color.css" + --8<-- "docs/examples/styles/scrollbar_corner_color.tcss" ``` ## CSS diff --git a/docs/styles/scrollbar_gutter.md b/docs/styles/scrollbar_gutter.md index a20db9fda7..1666f8a03a 100644 --- a/docs/styles/scrollbar_gutter.md +++ b/docs/styles/scrollbar_gutter.md @@ -33,10 +33,10 @@ terminal window. --8<-- "docs/examples/styles/scrollbar_gutter.py" ``` -=== "scrollbar_gutter.css" +=== "scrollbar_gutter.tcss" ```sass hl_lines="2" - --8<-- "docs/examples/styles/scrollbar_gutter.css" + --8<-- "docs/examples/styles/scrollbar_gutter.tcss" ``` ## CSS diff --git a/docs/styles/scrollbar_size.md b/docs/styles/scrollbar_size.md index a6bea39276..6ff8384c80 100644 --- a/docs/styles/scrollbar_size.md +++ b/docs/styles/scrollbar_size.md @@ -34,16 +34,21 @@ In this example we modify the size of the widget's scrollbar to be _much_ larger --8<-- "docs/examples/styles/scrollbar_size.py" ``` -=== "scrollbar_size.css" +=== "scrollbar_size.tcss" ```sass hl_lines="13" - --8<-- "docs/examples/styles/scrollbar_size.css" + --8<-- "docs/examples/styles/scrollbar_size.tcss" ``` ### Scrollbar sizes comparison In the next example we show three containers with differently sized scrollbars. +!!! tip + + If you want to hide the scrollbar but still allow the container to scroll + using the mousewheel or keyboard, you can set the scrollbar size to `0`. + === "Output" ```{.textual path="docs/examples/styles/scrollbar_size2.py"} @@ -55,10 +60,10 @@ In the next example we show three containers with differently sized scrollbars. --8<-- "docs/examples/styles/scrollbar_size2.py" ``` -=== "scrollbar_size2.css" +=== "scrollbar_size2.tcss" ```sass hl_lines="6 11 16" - --8<-- "docs/examples/styles/scrollbar_size2.css" + --8<-- "docs/examples/styles/scrollbar_size2.tcss" ``` ## CSS diff --git a/docs/styles/text_align.md b/docs/styles/text_align.md index aee6950914..d503f6de2a 100644 --- a/docs/styles/text_align.md +++ b/docs/styles/text_align.md @@ -29,10 +29,10 @@ This example shows, from top to bottom: `left`, `center`, `right`, and `justify` --8<-- "docs/examples/styles/text_align.py" ``` -=== "text_align.css" +=== "text_align.tcss" ```sass hl_lines="2 7 12 17" - --8<-- "docs/examples/styles/text_align.css" + --8<-- "docs/examples/styles/text_align.tcss" ``` [//]: # (TODO: Add an example that shows how `start` and `end` change when RTL support is added.) diff --git a/docs/styles/text_opacity.md b/docs/styles/text_opacity.md index b680a765ff..d178800c32 100644 --- a/docs/styles/text_opacity.md +++ b/docs/styles/text_opacity.md @@ -36,10 +36,10 @@ This example shows, from top to bottom, increasing `text-opacity` values. --8<-- "docs/examples/styles/text_opacity.py" ``` -=== "text_opacity.css" +=== "text_opacity.tcss" ```sass hl_lines="2 6 10 14 18" - --8<-- "docs/examples/styles/text_opacity.css" + --8<-- "docs/examples/styles/text_opacity.tcss" ``` ## CSS diff --git a/docs/styles/text_style.md b/docs/styles/text_style.md index 8e9cb4775f..e684b440e1 100644 --- a/docs/styles/text_style.md +++ b/docs/styles/text_style.md @@ -27,10 +27,10 @@ Each of the three text panels has a different text style, respectively `bold`, ` --8<-- "docs/examples/styles/text_style.py" ``` -=== "text_style.css" +=== "text_style.tcss" ```sass hl_lines="9 13 17" - --8<-- "docs/examples/styles/text_style.css" + --8<-- "docs/examples/styles/text_style.tcss" ``` ### All text styles @@ -48,10 +48,10 @@ The next example shows all different text styles on their own, as well as some c --8<-- "docs/examples/styles/text_style_all.py" ``` -=== "text_style_all.css" +=== "text_style_all.tcss" ```sass hl_lines="2 6 10 14 18 22 26 30" - --8<-- "docs/examples/styles/text_style_all.css" + --8<-- "docs/examples/styles/text_style_all.tcss" ``` ## CSS diff --git a/docs/styles/tint.md b/docs/styles/tint.md index 4a4dfc15b0..cc2b29f46b 100644 --- a/docs/styles/tint.md +++ b/docs/styles/tint.md @@ -27,10 +27,10 @@ This examples shows a green tint with gradually increasing alpha. 1. We set the tint to a `Color` instance with varying levels of opacity, set through the method [with_alpha][textual.color.Color.with_alpha]. -=== "tint.css" +=== "tint.tcss" ```sass - --8<-- "docs/examples/styles/tint.css" + --8<-- "docs/examples/styles/tint.tcss" ``` ## CSS diff --git a/docs/styles/visibility.md b/docs/styles/visibility.md index 38d958c925..b80105a48c 100644 --- a/docs/styles/visibility.md +++ b/docs/styles/visibility.md @@ -45,10 +45,10 @@ Note that the second widget is hidden while leaving a space where it would have --8<-- "docs/examples/styles/visibility.py" ``` -=== "visibility.css" +=== "visibility.tcss" ```sass hl_lines="14" - --8<-- "docs/examples/styles/visibility.css" + --8<-- "docs/examples/styles/visibility.tcss" ``` ### Overriding container visibility @@ -72,10 +72,10 @@ The containers all have a white background, and then: --8<-- "docs/examples/styles/visibility_containers.py" ``` -=== "visibility_containers.css" +=== "visibility_containers.tcss" ```sass hl_lines="2-3 6 8-10 12-14 16-18" - --8<-- "docs/examples/styles/visibility_containers.css" + --8<-- "docs/examples/styles/visibility_containers.tcss" ``` 1. The padding and the white background let us know when the `Horizontal` is visible. diff --git a/docs/styles/width.md b/docs/styles/width.md index e7196b5d7b..a0f7553bac 100644 --- a/docs/styles/width.md +++ b/docs/styles/width.md @@ -28,10 +28,10 @@ This example adds a widget with 50% width of the screen. --8<-- "docs/examples/styles/width.py" ``` -=== "width.css" +=== "width.tcss" ```sass hl_lines="3" - --8<-- "docs/examples/styles/width.css" + --8<-- "docs/examples/styles/width.tcss" ``` ### All width formats @@ -49,10 +49,10 @@ This example adds a widget with 50% width of the screen. 1. The id of the placeholder identifies which unit will be used to set the width of the widget. -=== "width_comparison.css" +=== "width_comparison.tcss" ```sass hl_lines="2 5 8 11 14 17 20 23 26" - --8<-- "docs/examples/styles/width_comparison.css" + --8<-- "docs/examples/styles/width_comparison.tcss" ``` 1. This sets the width to 9 columns. diff --git a/docs/stylesheets/custom.css b/docs/stylesheets/custom.css index ee10d06544..6d54ce45b4 100644 --- a/docs/stylesheets/custom.css +++ b/docs/stylesheets/custom.css @@ -14,6 +14,7 @@ h3 .doc-heading code { } body[data-md-color-primary="black"] .excalidraw svg { + will-change: filter; filter: invert(100%) hue-rotate(180deg); } @@ -72,3 +73,45 @@ td code { opacity: 0.85; } + +.textual-web-demo iframe { + border: none; + width: 100%; + aspect-ratio: 16 / 9; + padding: 0; + margin: 0; +} + + +.textual-web-demo { + display: flex; + width: 100%; + aspect-ratio: 16 / 9; + padding: 0; + margin: 0; + opacity: 0; + transition: 0.3s opacity; +} + +.textual-web-demo.-loaded { + opacity: 1.0; + transition: 0.3s opacity; +} + +:root { + --md-admonition-icon--textualize: url('/images/icons/logo light transparent.svg') +} +.md-typeset .admonition.textualize, +.md-typeset details.textualize { + border-color: rgb(43, 155, 70); +} +.md-typeset .textualize > .admonition-title, +.md-typeset .textualize > summary { + background-color: rgba(43, 155, 70, 0.1); +} +.md-typeset .textualize > .admonition-title::before, +.md-typeset .textualize > summary::before { + background-color: rgb(43, 155, 70); + -webkit-mask-image: var(--md-admonition-icon--textualize); + mask-image: var(--md-admonition-icon--textualize); +} diff --git a/docs/tutorial.md b/docs/tutorial.md index df39fef8f4..c457a25b55 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -31,6 +31,21 @@ Here's what the finished app will look like: ```{.textual path="docs/examples/tutorial/stopwatch.py" title="stopwatch.py" press="tab,enter,tab,enter,tab,enter,tab,enter"} ``` +### Try it out! + +The following is *not* a screenshot, but a fully interactive Textual app running in your browser. + + +!!! textualize "Try in Textual-web" + +
+ + +!!! tip + + See [textual-web](https://github.com/Textualize/textual-web) if you are interested in publishing your Textual apps on the web. + + ### Get the code If you want to try the finished Stopwatch app and follow along with the code, first make sure you have [Textual installed](getting_started.md) then check out the [Textual](https://github.com/Textualize/textual) repository: @@ -216,8 +231,8 @@ Let's add a CSS file to our application. Adding the `CSS_PATH` class variable tells Textual to load the following file when the app starts: -```sass title="stopwatch03.css" ---8<-- "docs/examples/tutorial/stopwatch03.css" +```sass title="stopwatch03.tcss" +--8<-- "docs/examples/tutorial/stopwatch03.tcss" ``` If we run the app now, it will look *very* different. @@ -225,11 +240,11 @@ If we run the app now, it will look *very* different. ```{.textual path="docs/examples/tutorial/stopwatch03.py" title="stopwatch03.py"} ``` -This app looks much more like our sketch. Let's look at how Textual uses `stopwatch03.css` to apply styles. +This app looks much more like our sketch. Let's look at how Textual uses `stopwatch03.tcss` to apply styles. ### CSS basics -CSS files contain a number of _declaration blocks_. Here's the first such block from `stopwatch03.css` again: +CSS files contain a number of _declaration blocks_. Here's the first such block from `stopwatch03.tcss` again: ```sass Stopwatch { @@ -258,7 +273,7 @@ Here's how this CSS code changes how the `Stopwatch` widget is displayed. - `padding: 1` sets a padding of 1 cell around the child widgets. -Here's the rest of `stopwatch03.css` which contains further declaration blocks: +Here's the rest of `stopwatch03.tcss` which contains further declaration blocks: ```sass TimeDisplay { @@ -308,8 +323,8 @@ We can accomplish this with a CSS _class_. Not to be confused with a Python clas Here's the new CSS: -```sass title="stopwatch04.css" hl_lines="33-53" ---8<-- "docs/examples/tutorial/stopwatch04.css" +```sass title="stopwatch04.tcss" hl_lines="33-53" +--8<-- "docs/examples/tutorial/stopwatch04.tcss" ``` These new rules are prefixed with `.started`. The `.` indicates that `.started` refers to a CSS class called "started". The new styles will be applied only to widgets that have this CSS class. diff --git a/docs/widget_gallery.md b/docs/widget_gallery.md index da06823945..ca82b5d4e3 100644 --- a/docs/widget_gallery.md +++ b/docs/widget_gallery.md @@ -34,6 +34,16 @@ A classic checkbox control. ```{.textual path="docs/examples/widgets/checkbox.py"} ``` +## Collapsible + +Content that may be toggled on and off by clicking a title. + +[Collapsible reference](./widgets/collapsible.md){ .md-button .md-button--primary } + + +```{.textual path="docs/examples/widgets/collapsible.py"} +``` + ## ContentSwitcher @@ -225,6 +235,16 @@ Display and update text in a scrolling panel. ```{.textual path="docs/examples/widgets/rich_log.py" press="H,i"} ``` +## Rule + +A rule widget to separate content, similar to a `
` HTML tag. + +[Rule reference](./widgets/rule.md){ .md-button .md-button--primary } + + +```{.textual path="docs/examples/widgets/horizontal_rules.py"} +``` + ## Select Select from a number of possible options. @@ -261,7 +281,7 @@ Displays simple static content. Typically used as a base class. ## Switch -A on / off control, inspired by toggle buttons. +An on / off control, inspired by toggle buttons. [Switch reference](./widgets/switch.md){ .md-button .md-button--primary } @@ -287,6 +307,14 @@ A Combination of Tabs and ContentSwitcher to navigate static content. ```{.textual path="docs/examples/widgets/tabbed_content.py" press="j"} ``` +## TextArea + +A multi-line text area which supports syntax highlighting various languages. + +[TextArea reference](./widgets/text_area.md){ .md-button .md-button--primary } + +```{.textual path="docs/examples/widgets/text_area_example.py" columns="42" lines="8"} +``` ## Tree diff --git a/docs/widgets/_template.md b/docs/widgets/_template.md index 519173aa26..70c6a43385 100644 --- a/docs/widgets/_template.md +++ b/docs/widgets/_template.md @@ -23,15 +23,16 @@ Example app showing the widget: --8<-- "docs/examples/widgets/checkbox.py" ``` -=== "checkbox.css" +=== "checkbox.tcss" ```sass - --8<-- "docs/examples/widgets/checkbox.css" + --8<-- "docs/examples/widgets/checkbox.tcss" ``` -## Reactive attributes +## Reactive Attributes +## Messages ## Bindings diff --git a/docs/widgets/button.md b/docs/widgets/button.md index 5ce84c8f99..55a4120f9d 100644 --- a/docs/widgets/button.md +++ b/docs/widgets/button.md @@ -23,10 +23,10 @@ Clicking any of the non-disabled buttons in the example app below will result in --8<-- "docs/examples/widgets/button.py" ``` -=== "button.css" +=== "button.tcss" ```sass - --8<-- "docs/examples/widgets/button.css" + --8<-- "docs/examples/widgets/button.tcss" ``` ## Reactive Attributes @@ -41,6 +41,14 @@ Clicking any of the non-disabled buttons in the example app below will result in - [Button.Pressed][textual.widgets.Button.Pressed] +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. + ## Additional Notes - The spacing between the text and the edges of a button are _not_ due to padding. The default styling for a `Button` has the `height` set to 3 lines and a `min-width` of 16 columns. To create a button with zero visible padding, you will need to change these values and also remove the border with `border: none;`. diff --git a/docs/widgets/checkbox.md b/docs/widgets/checkbox.md index 57e0c2d216..0b227ba311 100644 --- a/docs/widgets/checkbox.md +++ b/docs/widgets/checkbox.md @@ -22,10 +22,10 @@ The example below shows check boxes in various states. --8<-- "docs/examples/widgets/checkbox.py" ``` -=== "checkbox.css" +=== "checkbox.tcss" ```sass - --8<-- "docs/examples/widgets/checkbox.css" + --8<-- "docs/examples/widgets/checkbox.tcss" ``` ## Reactive Attributes @@ -34,6 +34,10 @@ The example below shows check boxes in various states. | ------- | ------ | ------- | -------------------------- | | `value` | `bool` | `False` | The value of the checkbox. | +## Messages + +- [Checkbox.Changed][textual.widgets.Checkbox.Changed] + ## Bindings The checkbox widget defines the following bindings: @@ -45,17 +49,13 @@ The checkbox widget defines the following bindings: ## Component Classes -The checkbox widget provides the following component classes: +The checkbox widget inherits the following component classes: ::: textual.widgets._toggle_button.ToggleButton.COMPONENT_CLASSES options: show_root_heading: false show_root_toc_entry: false -## Messages - -- [Checkbox.Changed][textual.widgets.Checkbox.Changed] - --- diff --git a/docs/widgets/collapsible.md b/docs/widgets/collapsible.md new file mode 100644 index 0000000000..009f7f760b --- /dev/null +++ b/docs/widgets/collapsible.md @@ -0,0 +1,149 @@ +# Collapsible + +!!! tip "Added in version 0.37" + +A container with a title that can be used to show (expand) or hide (collapse) content, either by clicking or focusing and pressing ++enter++. + +- [x] Focusable +- [x] Container + + +## Composing + +You can add content to a Collapsible widget either by passing in children to the constructor, or with a context manager (`with` statement). + +Here is an example of using the constructor to add content: + +```python +def compose(self) -> ComposeResult: + yield Collapsible(Label("Hello, world.")) +``` + +Here's how the to use it with the context manager: + +```python +def compose(self) -> ComposeResult: + with Collapsible(): + yield Label("Hello, world.") +``` + +The second form is generally preferred, but the end result is the same. + +## Title + +The default title "Toggle" can be customized by setting the `title` parameter of the constructor: + +```python +def compose(self) -> ComposeResult: + with Collapsible(title="An interesting story."): + yield Label("Interesting but verbose story.") +``` + +## Initial State + +The initial state of the `Collapsible` widget can be customized via the `collapsed` parameter of the constructor: + +```python +def compose(self) -> ComposeResult: + with Collapsible(title="Contents 1", collapsed=False): + yield Label("Hello, world.") + + with Collapsible(title="Contents 2", collapsed=True): # Default. + yield Label("Hello, world.") +``` + +## Collapse/Expand Symbols + +The symbols used to show the collapsed / expanded state can be customized by setting the parameters `collapsed_symbol` and `expanded_symbol`: + +```python +def compose(self) -> ComposeResult: + with Collapsible(collapsed_symbol=">>>", expanded_symbol="v"): + yield Label("Hello, world.") +``` + +## Examples + + +The following example contains three `Collapsible`s in different states. + +=== "All expanded" + + ```{.textual path="docs/examples/widgets/collapsible.py" press="e"} + ``` + +=== "All collapsed" + + ```{.textual path="docs/examples/widgets/collapsible.py" press="c"} + ``` + +=== "Mixed" + + ```{.textual path="docs/examples/widgets/collapsible.py"} + ``` + +=== "collapsible.py" + + ```python + --8<-- "docs/examples/widgets/collapsible.py" + ``` + +### Setting Initial State + +The example below shows nested `Collapsible` widgets and how to set their initial state. + + +=== "Output" + + ```{.textual path="docs/examples/widgets/collapsible_nested.py"} + ``` + +=== "collapsible_nested.py" + + ```python hl_lines="7" + --8<-- "docs/examples/widgets/collapsible_nested.py" + ``` + +### Custom Symbols + +The following example shows `Collapsible` widgets with custom expand/collapse symbols. + + +=== "Output" + + ```{.textual path="docs/examples/widgets/collapsible_custom_symbol.py"} + ``` + +=== "collapsible_custom_symbol.py" + + ```python + --8<-- "docs/examples/widgets/collapsible_custom_symbol.py" + ``` + +## Reactive Attributes + +| Name | Type | Default | Description | +| ----------- | ------ | ------- | ---------------------------------------------------- | +| `collapsed` | `bool` | `True` | Controls the collapsed/expanded state of the widget. | + +## Messages + +This widget posts no messages. + +## Bindings + +The collapsible widget defines the following binding on its title: + +::: textual.widgets._collapsible.CollapsibleTitle.BINDINGS + options: + show_root_heading: false + show_root_toc_entry: false + +## Component Classes + +This widget has no component classes. + + +::: textual.widgets.Collapsible + options: + heading_level: 2 diff --git a/docs/widgets/content_switcher.md b/docs/widgets/content_switcher.md index 24ecf4afee..126213c94b 100644 --- a/docs/widgets/content_switcher.md +++ b/docs/widgets/content_switcher.md @@ -33,10 +33,10 @@ between the different views. 4. Note that the initial visible content is set by its ID, see below. 5. When a button is pressed, its ID is used to switch to a different widget in the `ContentSwitcher`. Remember that IDs are unique within parent, so the buttons and the widgets in the `ContentSwitcher` can share IDs. -=== "content_switcher.css" +=== "content_switcher.tcss" ~~~sass - --8<-- "docs/examples/widgets/content_switcher.css" + --8<-- "docs/examples/widgets/content_switcher.tcss" ~~~ When the user presses the "Markdown" button the view is switched: @@ -50,6 +50,18 @@ When the user presses the "Markdown" button the view is switched: | --------- | --------------- | ------- | ----------------------------------------------------------------------- | | `current` | `str` \| `None` | `None` | The ID of the currently-visible child. `None` means nothing is visible. | +## Messages + +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. + --- diff --git a/docs/widgets/data_table.md b/docs/widgets/data_table.md index bd4919c733..ab1981c0f1 100644 --- a/docs/widgets/data_table.md +++ b/docs/widgets/data_table.md @@ -10,7 +10,7 @@ A table widget optimized for displaying a lot of data. ### Adding data The following example shows how to fill a table with data. -First, we use [add_columns][textual.widgets.DataTable.add_rows] to include the `lane`, `swimmer`, `country`, and `time` columns in the table. +First, we use [add_columns][textual.widgets.DataTable.add_columns] to include the `lane`, `swimmer`, `country`, and `time` columns in the table. After that, we use the [add_rows][textual.widgets.DataTable.add_rows] method to insert the rows into the table. === "Output" @@ -228,3 +228,8 @@ The data table widget provides the following component classes: ::: textual.widgets.DataTable options: heading_level: 2 + +::: textual.widgets.data_table + options: + show_root_heading: true + show_root_toc_entry: true diff --git a/docs/widgets/digits.md b/docs/widgets/digits.md index 6dd33044ce..4fb919f762 100644 --- a/docs/widgets/digits.md +++ b/docs/widgets/digits.md @@ -44,15 +44,19 @@ Here's another example which uses `Digits` to display the current time: --8<-- "docs/examples/widgets/clock.py" ``` -## Reactive attributes +## Reactive Attributes This widget has no reactive attributes. +## Messages + +This widget posts no messages. + ## Bindings This widget has no bindings. -## Component classes +## Component Classes This widget has no component classes. diff --git a/docs/widgets/directory_tree.md b/docs/widgets/directory_tree.md index 56f1a00375..992a9fc127 100644 --- a/docs/widgets/directory_tree.md +++ b/docs/widgets/directory_tree.md @@ -34,10 +34,6 @@ and directories: --8<-- "docs/examples/widgets/directory_tree_filtered.py" ~~~ -## Messages - -- [DirectoryTree.FileSelected][textual.widgets.DirectoryTree.FileSelected] - ## Reactive Attributes | Name | Type | Default | Description | @@ -46,6 +42,14 @@ and directories: | `show_guides` | `bool` | `True` | Show guide lines between levels. | | `guide_depth` | `int` | `4` | Amount of indentation between parent and child. | +## Messages + +- [DirectoryTree.FileSelected][textual.widgets.DirectoryTree.FileSelected] + +## Bindings + +The directory tree widget inherits [the bindings from the tree widget][textual.widgets.Tree.BINDINGS]. + ## Component Classes The directory tree widget provides the following component classes: diff --git a/docs/widgets/footer.md b/docs/widgets/footer.md index 4affbe2191..fcb25cf836 100644 --- a/docs/widgets/footer.md +++ b/docs/widgets/footer.md @@ -30,7 +30,11 @@ widget. Notice how the `Footer` automatically displays the keybinding. ## Messages -This widget sends no messages. +This widget posts no messages. + +## Bindings + +This widget has no bindings. ## Component Classes diff --git a/docs/widgets/header.md b/docs/widgets/header.md index 3286b3e8c1..1ffdf70dd1 100644 --- a/docs/widgets/header.md +++ b/docs/widgets/header.md @@ -2,6 +2,10 @@ A simple header widget which docks itself to the top of the parent container. +!!! note + + The application title which is shown in the header is taken from the [`title`][textual.app.App.title] and [`sub_title`][textual.app.App.sub_title] of the application. + - [ ] Focusable - [ ] Container @@ -20,6 +24,19 @@ The example below shows an app with a `Header`. --8<-- "docs/examples/widgets/header.py" ``` +This example shows how to set the text in the `Header` using `App.title` and `App.sub_title`: + +=== "Output" + + ```{.textual path="docs/examples/widgets/header_app_title.py"} + ``` + +=== "header_app_title.py" + + ```python + --8<-- "docs/examples/widgets/header_app_title.py" + ``` + ## Reactive Attributes | Name | Type | Default | Description | @@ -28,7 +45,15 @@ The example below shows an app with a `Header`. ## Messages -This widget sends no messages. +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. --- diff --git a/docs/widgets/input.md b/docs/widgets/input.md index e1f6191e57..cd861a79b5 100644 --- a/docs/widgets/input.md +++ b/docs/widgets/input.md @@ -26,7 +26,14 @@ The example below shows how you might create a simple form using two `Input` wid You can supply one or more *[validators][textual.validation.Validator]* to the `Input` widget to validate the value. -When the value changes or the `Input` is submitted, all the supplied validators will run. +All the supplied validators will run when the value changes, the `Input` is submitted, or focus moves _out_ of the `Input`. +The values `"changed"`, `"submitted"`, and `"blur"`, can be passed as an iterable to the `Input` parameter `validate_on` to request that validation occur only on the respective mesages. +(See [`InputValidationOn`][textual.widgets._input.InputValidationOn] and [`Input.validate_on`][textual.widgets.Input.validate_on].) +For example, the code below creates an `Input` widget that only gets validated when the value is submitted explicitly: + +```python +input = Input(validate_on=["submitted"]) +``` Validation is considered to have failed if *any* of the validators fail. @@ -81,7 +88,7 @@ as seen for `Palindrome` in the example above. ## Bindings -The Input widget defines the following bindings: +The input widget defines the following bindings: ::: textual.widgets.Input.BINDINGS options: diff --git a/docs/widgets/label.md b/docs/widgets/label.md index ae1216d0a2..2a0c1819a7 100644 --- a/docs/widgets/label.md +++ b/docs/widgets/label.md @@ -28,7 +28,15 @@ This widget has no reactive attributes. ## Messages -This widget sends no messages. +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. --- diff --git a/docs/widgets/list_item.md b/docs/widgets/list_item.md index 309079ea87..c4d306cb78 100644 --- a/docs/widgets/list_item.md +++ b/docs/widgets/list_item.md @@ -29,12 +29,17 @@ of multiple `ListItem`s. The arrow keys can be used to navigate the list. | ------------- | ------ | ------- | ------------------------------------ | | `highlighted` | `bool` | `False` | True if this ListItem is highlighted | +## Messages -#### Attributes +This widget posts no messages. -| attribute | type | purpose | -| --------- | ---------- | --------------------------- | -| `item` | `ListItem` | The item that was selected. | +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. --- diff --git a/docs/widgets/list_view.md b/docs/widgets/list_view.md index 06ef905852..d5c85cdbc6 100644 --- a/docs/widgets/list_view.md +++ b/docs/widgets/list_view.md @@ -23,17 +23,17 @@ The example below shows an app with a simple `ListView`. --8<-- "docs/examples/widgets/list_view.py" ``` -=== "list_view.css" +=== "list_view.tcss" ```sass - --8<-- "docs/examples/widgets/list_view.css" + --8<-- "docs/examples/widgets/list_view.tcss" ``` ## Reactive Attributes -| Name | Type | Default | Description | -| ------- | ----- | ------- | ------------------------------- | -| `index` | `int` | `0` | The currently highlighted index | +| Name | Type | Default | Description | +| ------- | ----- | ------- | -------------------------------- | +| `index` | `int` | `0` | The currently highlighted index. | ## Messages @@ -49,6 +49,10 @@ The list view widget defines the following bindings: show_root_heading: false show_root_toc_entry: false +## Component Classes + +This widget has no component classes. + --- diff --git a/docs/widgets/loading_indicator.md b/docs/widgets/loading_indicator.md index 1936115522..2a5235d12e 100644 --- a/docs/widgets/loading_indicator.md +++ b/docs/widgets/loading_indicator.md @@ -7,6 +7,23 @@ Displays pulsating dots to indicate when data is being loaded. - [ ] Focusable - [ ] Container +## Example + +Simple usage example: + +=== "Output" + + ```{.textual path="docs/examples/widgets/loading_indicator.py"} + ``` + +=== "loading_indicator.py" + + ```python + --8<-- "docs/examples/widgets/loading_indicator.py" + ``` + +## Changing Indicator Color + You can set the color of the loading indicator by setting its `color` style. Here's how you would do that with CSS: @@ -17,17 +34,22 @@ LoadingIndicator { } ``` +## Reactive Attributes -=== "Output" +This widget has no reactive attributes. - ```{.textual path="docs/examples/widgets/loading_indicator.py"} - ``` +## Messages -=== "loading_indicator.py" +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. - ```python - --8<-- "docs/examples/widgets/loading_indicator.py" - ``` --- diff --git a/docs/widgets/log.md b/docs/widgets/log.md index 04e54f0f00..72509313a6 100644 --- a/docs/widgets/log.md +++ b/docs/widgets/log.md @@ -37,10 +37,17 @@ The example below shows how to write text to a `Log` widget: | `max_lines` | `int` | `None` | Maximum number of lines in the log or `None` for no maximum. | | `auto_scroll` | `bool` | `False` | Scroll to end of log when new lines are added. | - ## Messages -This widget sends no messages. +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. --- diff --git a/docs/widgets/markdown.md b/docs/widgets/markdown.md index 6897c4c713..1382d1a5aa 100644 --- a/docs/widgets/markdown.md +++ b/docs/widgets/markdown.md @@ -27,12 +27,29 @@ The following example displays Markdown from a string. --8<-- "docs/examples/widgets/markdown.py" ~~~ +## Reactive Attributes + +This widget has no reactive attributes. + ## Messages - [Markdown.TableOfContentsUpdated][textual.widgets.Markdown.TableOfContentsUpdated] - [Markdown.TableOfContentsSelected][textual.widgets.Markdown.TableOfContentsSelected] - [Markdown.LinkClicked][textual.widgets.Markdown.LinkClicked] +## Bindings + +This widget has no bindings. + +## Component Classes + +The markdown widget provides the following component classes: + +::: textual.widgets.Markdown.COMPONENT_CLASSES + options: + show_root_heading: false + show_root_toc_entry: false + ## See Also diff --git a/docs/widgets/markdown_viewer.md b/docs/widgets/markdown_viewer.md index 6a4e3f47df..d830281fd4 100644 --- a/docs/widgets/markdown_viewer.md +++ b/docs/widgets/markdown_viewer.md @@ -33,6 +33,18 @@ The following example displays Markdown from a string and a Table of Contents. | ------------------------ | ---- | ------- | ----------------------------------------------------------------- | | `show_table_of_contents` | bool | True | Wether a Table of Contents should be displayed with the Markdown. | +## Messages + +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. + ## See Also * [Markdown][textual.widgets.Markdown] code reference diff --git a/docs/widgets/option_list.md b/docs/widgets/option_list.md index a7094ba64a..b0a3170857 100644 --- a/docs/widgets/option_list.md +++ b/docs/widgets/option_list.md @@ -25,10 +25,10 @@ options: --8<-- "docs/examples/widgets/option_list_strings.py" ~~~ -=== "option_list.css" +=== "option_list.tcss" ~~~python - --8<-- "docs/examples/widgets/option_list.css" + --8<-- "docs/examples/widgets/option_list.tcss" ~~~ ### Options as `Option` instances @@ -48,10 +48,10 @@ class can be used to add separator lines between options. --8<-- "docs/examples/widgets/option_list_options.py" ~~~ -=== "option_list.css" +=== "option_list.tcss" ~~~python - --8<-- "docs/examples/widgets/option_list.css" + --8<-- "docs/examples/widgets/option_list.tcss" ~~~ ### Options as Rich renderables @@ -73,10 +73,10 @@ tables](https://rich.readthedocs.io/en/latest/tables.html): --8<-- "docs/examples/widgets/option_list_tables.py" ~~~ -=== "option_list.css" +=== "option_list.tcss" ~~~python - --8<-- "docs/examples/widgets/option_list.css" + --8<-- "docs/examples/widgets/option_list.tcss" ~~~ ## Reactive Attributes diff --git a/docs/widgets/placeholder.md b/docs/widgets/placeholder.md index 2dbdbd6583..c8006d780a 100644 --- a/docs/widgets/placeholder.md +++ b/docs/widgets/placeholder.md @@ -26,10 +26,10 @@ The example below shows each placeholder variant. --8<-- "docs/examples/widgets/placeholder.py" ``` -=== "placeholder.css" +=== "placeholder.tcss" ```sass - --8<-- "docs/examples/widgets/placeholder.css" + --8<-- "docs/examples/widgets/placeholder.tcss" ``` ## Reactive Attributes @@ -41,7 +41,15 @@ The example below shows each placeholder variant. ## Messages -This widget sends no messages. +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. --- diff --git a/docs/widgets/progress_bar.md b/docs/widgets/progress_bar.md index 1ef573b3f7..ab927aa763 100644 --- a/docs/widgets/progress_bar.md +++ b/docs/widgets/progress_bar.md @@ -65,10 +65,10 @@ The example below shows a simple app with a progress bar that is keeping track o 1. We create a progress bar with a total of `100` steps and we hide the ETA countdown because we are not keeping track of a continuous, uninterrupted task. -=== "progress_bar.css" +=== "progress_bar.tcss" ```sass - --8<-- "docs/examples/widgets/progress_bar.css" + --8<-- "docs/examples/widgets/progress_bar.tcss" ``` @@ -98,21 +98,12 @@ Refer to the [section below](#styling-the-progress-bar) for more information. --8<-- "docs/examples/widgets/progress_bar_styled.py" ``` -=== "progress_bar_styled.css" +=== "progress_bar_styled.tcss" ```sass - --8<-- "docs/examples/widgets/progress_bar_styled.css" + --8<-- "docs/examples/widgets/progress_bar_styled.tcss" ``` -## Reactive Attributes - -| Name | Type | Default | Description | -| ------------ | ------- | ------- | ------------------------------------------------------------------------------------------------------- | -| `percentage` | `float | None` | The read-only percentage of progress that has been made. This is `None` if the `total` hasn't been set. | -| `progress` | `float` | `0` | The number of steps of progress already made. | -| `total` | `float | None` | The total number of steps that we are keeping track of. | - - ## Styling the Progress Bar The progress bar is composed of three sub-widgets that can be styled independently: @@ -130,8 +121,27 @@ The progress bar is composed of three sub-widgets that can be styled independent show_root_heading: false show_root_toc_entry: false ---- +## Reactive Attributes + +| Name | Type | Default | Description | +| ------------ | ------- | ------- | ------------------------------------------------------------------------------------------------------- | +| `percentage` | `float | None` | The read-only percentage of progress that has been made. This is `None` if the `total` hasn't been set. | +| `progress` | `float` | `0` | The number of steps of progress already made. | +| `total` | `float | None` | The total number of steps that we are keeping track of. | + +## Messages +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. + +--- ::: textual.widgets.ProgressBar options: diff --git a/docs/widgets/radiobutton.md b/docs/widgets/radiobutton.md index c23fa44b78..8161ceaf17 100644 --- a/docs/widgets/radiobutton.md +++ b/docs/widgets/radiobutton.md @@ -24,10 +24,10 @@ The example below shows radio buttons, used within a [`RadioSet`](./radioset.md) --8<-- "docs/examples/widgets/radio_button.py" ``` -=== "radio_button.css" +=== "radio_button.tcss" ```sass - --8<-- "docs/examples/widgets/radio_button.css" + --8<-- "docs/examples/widgets/radio_button.tcss" ``` ## Reactive Attributes @@ -36,6 +36,10 @@ The example below shows radio buttons, used within a [`RadioSet`](./radioset.md) | ------- | ------ | ------- | ------------------------------ | | `value` | `bool` | `False` | The value of the radio button. | +## Messages + +- [RadioButton.Changed][textual.widgets.RadioButton.Changed] + ## Bindings The radio button widget defines the following bindings: @@ -47,17 +51,13 @@ The radio button widget defines the following bindings: ## Component Classes -The radio button widget provides the following component classes: +The checkbox widget inherits the following component classes: ::: textual.widgets._toggle_button.ToggleButton.COMPONENT_CLASSES options: show_root_heading: false show_root_toc_entry: false -## Messages - -- [RadioButton.Changed][textual.widgets.RadioButton.Changed] - ## See Also - [RadioSet](./radioset.md) diff --git a/docs/widgets/radioset.md b/docs/widgets/radioset.md index 1aa632d9b8..78a6e28d9a 100644 --- a/docs/widgets/radioset.md +++ b/docs/widgets/radioset.md @@ -9,6 +9,8 @@ A container widget that groups [`RadioButton`](./radiobutton.md)s together. ## Example +### Simple example + The example below shows two radio sets, one built using a collection of [radio buttons](./radiobutton.md), the other a collection of simple strings. @@ -23,17 +25,13 @@ The example below shows two radio sets, one built using a collection of --8<-- "docs/examples/widgets/radio_set.py" ``` -=== "radio_set.css" +=== "radio_set.tcss" ```sass - --8<-- "docs/examples/widgets/radio_set.css" + --8<-- "docs/examples/widgets/radio_set.tcss" ``` -## Messages - -- [RadioSet.Changed][textual.widgets.RadioSet.Changed] - -#### Example +### Reacting to Changes in a Radio Set Here is an example of using the message to react to changes in a `RadioSet`: @@ -48,12 +46,29 @@ Here is an example of using the message to react to changes in a `RadioSet`: --8<-- "docs/examples/widgets/radio_set_changed.py" ``` -=== "radio_set_changed.css" +=== "radio_set_changed.tcss" ```sass - --8<-- "docs/examples/widgets/radio_set_changed.css" + --8<-- "docs/examples/widgets/radio_set_changed.tcss" ``` +## Messages + +- [RadioSet.Changed][textual.widgets.RadioSet.Changed] + +## Bindings + +The `RadioSet` widget defines the following bindings: + +::: textual.widgets.RadioSet.BINDINGS + options: + show_root_heading: false + show_root_toc_entry: false + +## Component Classes + +This widget has no component classes. + ## See Also diff --git a/docs/widgets/rich_log.md b/docs/widgets/rich_log.md index 2778db7ea3..5f373218fd 100644 --- a/docs/widgets/rich_log.md +++ b/docs/widgets/rich_log.md @@ -42,6 +42,14 @@ The example below shows an application showing a `RichLog` with different kinds This widget sends no messages. +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. + --- diff --git a/docs/widgets/rule.md b/docs/widgets/rule.md new file mode 100644 index 0000000000..5740b42376 --- /dev/null +++ b/docs/widgets/rule.md @@ -0,0 +1,83 @@ +# Rule + +A rule widget to separate content, similar to a `
` HTML tag. + +- [ ] Focusable +- [ ] Container + +## Examples + +### Horizontal Rule + +The default orientation of a rule is horizontal. + +The example below shows horizontal rules with all the available line styles. + +=== "Output" + + ```{.textual path="docs/examples/widgets/horizontal_rules.py"} + ``` + +=== "horizontal_rules.py" + + ```python + --8<-- "docs/examples/widgets/horizontal_rules.py" + ``` + +=== "horizontal_rules.tcss" + + ```sass + --8<-- "docs/examples/widgets/horizontal_rules.tcss" + ``` + +### Vertical Rule + +The example below shows vertical rules with all the available line styles. + +=== "Output" + + ```{.textual path="docs/examples/widgets/vertical_rules.py"} + ``` + +=== "vertical_rules.py" + + ```python + --8<-- "docs/examples/widgets/vertical_rules.py" + ``` + +=== "vertical_rules.tcss" + + ```sass + --8<-- "docs/examples/widgets/vertical_rules.tcss" + ``` + +## Reactive Attributes + +| Name | Type | Default | Description | +| ------------- | ----------------- | -------------- | ---------------------------- | +| `orientation` | `RuleOrientation` | `"horizontal"` | The orientation of the rule. | +| `line_style` | `LineStyle` | `"solid"` | The line style of the rule. | + +## Messages + +This widget sends no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. + +--- + + +::: textual.widgets.Rule + options: + heading_level: 2 + +::: textual.widgets.rule + options: + show_root_heading: true + show_root_toc_entry: true diff --git a/docs/widgets/select.md b/docs/widgets/select.md index 2ef0cf8f60..6f9690cb24 100644 --- a/docs/widgets/select.md +++ b/docs/widgets/select.md @@ -52,18 +52,14 @@ The following example presents a `Select` with a number of options. --8<-- "docs/examples/widgets/select_widget.py" ``` -=== "select.css" +=== "select.tcss" ```sass - --8<-- "docs/examples/widgets/select.css" + --8<-- "docs/examples/widgets/select.tcss" ``` -## Messages - -- [Select.Changed][textual.widgets.Select.Changed] - -## Reactive attributes +## Reactive Attributes | Name | Type | Default | Description | @@ -71,6 +67,9 @@ The following example presents a `Select` with a number of options. | `expanded` | `bool` | `False` | True to expand the options overlay. | | `value` | `SelectType` \| `None` | `None` | Current value of the Select. | +## Messages + +- [Select.Changed][textual.widgets.Select.Changed] ## Bindings @@ -81,6 +80,9 @@ The Select widget defines the following bindings: show_root_heading: false show_root_toc_entry: false +## Component Classes + +This widget has no component classes. --- diff --git a/docs/widgets/selection_list.md b/docs/widgets/selection_list.md index 620dff8730..f1c63b8d49 100644 --- a/docs/widgets/selection_list.md +++ b/docs/widgets/selection_list.md @@ -52,10 +52,10 @@ optionally contain a flag for the initial selected state of the option. 1. Note that the `SelectionList` is typed as `int`, for the type of the values. -=== "selection_list.css" +=== "selection_list.tcss" ~~~python - --8<-- "docs/examples/widgets/selection_list.css" + --8<-- "docs/examples/widgets/selection_list.tcss" ~~~ ### Selections as Selection objects @@ -76,10 +76,10 @@ Alternatively, selections can be passed in as 1. Note that the `SelectionList` is typed as `int`, for the type of the values. -=== "selection_list.css" +=== "selection_list.tcss" ~~~python - --8<-- "docs/examples/widgets/selection_list.css" + --8<-- "docs/examples/widgets/selection_list.tcss" ~~~ ### Handling changes to the selections @@ -103,10 +103,10 @@ collection of selected values: 1. Note that the `SelectionList` is typed as `str`, for the type of the values. -=== "selection_list.css" +=== "selection_list.tcss" ~~~python - --8<-- "docs/examples/widgets/selection_list_selected.css" + --8<-- "docs/examples/widgets/selection_list_selected.tcss" ~~~ ## Reactive Attributes diff --git a/docs/widgets/sparkline.md b/docs/widgets/sparkline.md index 7670f3c924..61f13da75d 100644 --- a/docs/widgets/sparkline.md +++ b/docs/widgets/sparkline.md @@ -36,10 +36,10 @@ The example below illustrates the relationship between the data, its length, the The largest value of each chunk is 2, 4, and 8, respectively. That explains why the first bar is half the height of the second and the second bar is half the height of the third. -=== "sparkline_basic.css" +=== "sparkline_basic.tcss" ```sass - --8<-- "docs/examples/widgets/sparkline_basic.css" + --8<-- "docs/examples/widgets/sparkline_basic.tcss" ``` 1. By setting the width to 3 we get three buckets. @@ -64,10 +64,10 @@ The summary function is what determines the height of each bar. 2. Each bar will show the mean value of that bucket. 3. Each bar will show the smaller value of that bucket. -=== "sparkline.css" +=== "sparkline.tcss" ```sass - --8<-- "docs/examples/widgets/sparkline.css" + --8<-- "docs/examples/widgets/sparkline.tcss" ``` ### Changing the colors @@ -85,10 +85,10 @@ The example below shows how to use component classes to change the colors of the --8<-- "docs/examples/widgets/sparkline_colors.py" ``` -=== "sparkline_colors.css" +=== "sparkline_colors.tcss" ```sass - --8<-- "docs/examples/widgets/sparkline_colors.css" + --8<-- "docs/examples/widgets/sparkline_colors.tcss" ``` @@ -102,7 +102,20 @@ The example below shows how to use component classes to change the colors of the ## Messages -This widget sends no messages. +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +The sparkline widget provides the following component classes: + +::: textual.widgets.Sparkline.COMPONENT_CLASSES + options: + show_root_heading: false + show_root_toc_entry: false --- diff --git a/docs/widgets/static.md b/docs/widgets/static.md index 561f053431..9df032994b 100644 --- a/docs/widgets/static.md +++ b/docs/widgets/static.md @@ -27,7 +27,15 @@ This widget has no reactive attributes. ## Messages -This widget sends no messages. +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. ## See Also diff --git a/docs/widgets/switch.md b/docs/widgets/switch.md index e228d8f902..1482c08a8d 100644 --- a/docs/widgets/switch.md +++ b/docs/widgets/switch.md @@ -20,10 +20,10 @@ The example below shows switches in various states. --8<-- "docs/examples/widgets/switch.py" ``` -=== "switch.css" +=== "switch.tcss" ```sass - --8<-- "docs/examples/widgets/switch.css" + --8<-- "docs/examples/widgets/switch.tcss" ``` ## Reactive Attributes @@ -32,6 +32,10 @@ The example below shows switches in various states. | ------- | ------ | ------- | ------------------------ | | `value` | `bool` | `False` | The value of the switch. | +## Messages + +- [Switch.Changed][textual.widgets.Switch.Changed] + ## Bindings The switch widget defines the following bindings: @@ -50,10 +54,6 @@ The switch widget provides the following component classes: show_root_heading: false show_root_toc_entry: false -## Messages - -- [Switch.Changed][textual.widgets.Switch.Changed] - ## Additional Notes - To remove the spacing around a `Switch`, set `border: none;` and `padding: 0;`. diff --git a/docs/widgets/tabbed_content.md b/docs/widgets/tabbed_content.md index 7a61318dfc..f121e314e8 100644 --- a/docs/widgets/tabbed_content.md +++ b/docs/widgets/tabbed_content.md @@ -94,7 +94,7 @@ The following example contains a `TabbedContent` with three tabs. --8<-- "docs/examples/widgets/tabbed_content.py" ``` -## Reactive attributes +## Reactive Attributes | Name | Type | Default | Description | | -------- | ----- | ------- | -------------------------------------------------------------- | @@ -105,6 +105,14 @@ The following example contains a `TabbedContent` with three tabs. - [TabbedContent.TabActivated][textual.widgets.TabbedContent.TabActivated] +## Bindings + +This widget has no bindings. + +## Component Classes + +This widget has no component classes. + ## See also diff --git a/docs/widgets/tabs.md b/docs/widgets/tabs.md index b7d7130d74..a076fb715b 100644 --- a/docs/widgets/tabs.md +++ b/docs/widgets/tabs.md @@ -73,6 +73,9 @@ The Tabs widget defines the following bindings: show_root_heading: false show_root_toc_entry: false +## Component Classes + +This widget has no component classes. --- diff --git a/docs/widgets/text_area.md b/docs/widgets/text_area.md new file mode 100644 index 0000000000..a8c648d649 --- /dev/null +++ b/docs/widgets/text_area.md @@ -0,0 +1,491 @@ + +# TextArea + +!!! tip "Added in version 0.38.0" + +A widget for editing text which may span multiple lines. +Supports syntax highlighting for a selection of languages. + +- [x] Focusable +- [ ] Container + + +## Guide + +### Syntax highlighting dependencies + +To enable syntax highlighting, you'll need to install the `syntax` extra dependencies: + +=== "pip" + + ``` + pip install "textual[syntax]" + ``` + +=== "poetry" + + ``` + poetry add "textual[syntax]" + ``` + +This will install `tree-sitter` and `tree-sitter-languages`. +These packages are distributed as binary wheels, so it may limit your applications ability to run in environments where these wheels are not supported. + +### Loading text + +In this example we load some initial text into the `TextArea`, and set the language to `"python"` to enable syntax highlighting. + +=== "Output" + + ```{.textual path="docs/examples/widgets/text_area_example.py" columns="42" lines="8"} + ``` + +=== "text_area_example.py" + + ```python + --8<-- "docs/examples/widgets/text_area_example.py" + ``` + +To load content into the `TextArea` after it has already been created, +use the [`load_text`][textual.widgets._text_area.TextArea.load_text] method. + +To update the parser used for syntax highlighting, set the [`language`][textual.widgets._text_area.TextArea.language] reactive attribute: + +```python +# Set the language to Markdown +text_area.language = "markdown" +``` + +!!! note + Syntax highlighting is unavailable on Python 3.7. + +!!! note + More built-in languages will be added in the future. For now, you can [add your own](#adding-support-for-custom-languages). + + +### Reading content from `TextArea` + +There are a number of ways to retrieve content from the `TextArea`: + +- The [`TextArea.text`][textual.widgets._text_area.TextArea.text] property returns all content in the text area as a string. +- The [`TextArea.selected_text`][textual.widgets._text_area.TextArea.selected_text] property returns the text corresponding to the current selection. +- The [`TextArea.get_text_range`][textual.widgets._text_area.TextArea.get_text_range] method returns the text between two locations. + +In all cases, when multiple lines of text are retrieved, the [document line separator](#line-separators) will be used. + +### Editing content inside `TextArea` + +The content of the `TextArea` can be updated using the [`replace`][textual.widgets._text_area.TextArea.replace] method. +This method is the programmatic equivalent of selecting some text and then pasting. + +Some other convenient methods are available, such as [`insert`][textual.widgets._text_area.TextArea.insert], [`delete`][textual.widgets._text_area.TextArea.delete], and [`clear`][textual.widgets._text_area.TextArea.clear]. + +### Working with the cursor + +#### Moving the cursor + +The cursor location is available via the [`cursor_location`][textual.widgets._text_area.TextArea.cursor_location] property, which represents +the location of the cursor as a tuple `(row_index, column_index)`. These indices are zero-based. +Writing a new value to `cursor_location` will immediately update the location of the cursor. + +```python +>>> text_area = TextArea() +>>> text_area.cursor_location +(0, 0) +>>> text_area.cursor_location = (0, 4) +>>> text_area.cursor_location +(0, 4) +``` + +`cursor_location` is a simple way to move the cursor programmatically, but it doesn't let us select text. + +#### Selecting text + +To select text, we can use the `selection` reactive attribute. +Let's select the first two lines of text in a document by adding `text_area.selection = Selection(start=(0, 0), end=(2, 0))` to our code: + +=== "Output" + + ```{.textual path="docs/examples/widgets/text_area_selection.py" columns="42" lines="8"} + ``` + +=== "text_area_selection.py" + + ```python hl_lines="17" + --8<-- "docs/examples/widgets/text_area_selection.py" + ``` + + 1. Selects the first two lines of text. + +Note that selections can happen in both directions, so `Selection((2, 0), (0, 0))` is also valid. + +!!! tip + + The `end` attribute of the `selection` is always equal to `TextArea.cursor_location`. In other words, + the `cursor_location` attribute is simply a convenience for accessing `text_area.selection.end`. + +#### More cursor utilities + +There are a number of additional utility methods available for interacting with the cursor. + +##### Location information + +A number of properties exist on `TextArea` which give information about the current cursor location. +These properties begin with `cursor_at_`, and return booleans. +For example, [`cursor_at_start_of_line`][textual.widgets._text_area.TextArea.cursor_at_start_of_line] tells us if the cursor is at a start of line. + +We can also check the location the cursor _would_ arrive at if we were to move it. +For example, [`get_cursor_right_location`][textual.widgets._text_area.TextArea.get_cursor_right_location] returns the location +the cursor would move to if it were to move right. +A number of similar methods exist, with names like `get_cursor_*_location`. + +##### Cursor movement methods + +The [`move_cursor`][textual.widgets._text_area.TextArea.move_cursor] method allows you to move the cursor to a new location while selecting +text, or move the cursor and scroll to keep it centered. + +```python +# Move the cursor from its current location to row index 4, +# column index 8, while selecting all the text between. +text_area.move_cursor((4, 8), select=True) +``` + +The [`move_cursor_relative`][textual.widgets._text_area.TextArea.move_cursor_relative] method offers a very similar interface, but moves the cursor relative +to its current location. + +##### Common selections + +There are some methods available which make common selections easier: + +- [`select_line`][textual.widgets._text_area.TextArea.select_line] selects a line by index. Bound to ++f6++ by default. +- [`select_all`][textual.widgets._text_area.TextArea.select_all] selects all text. Bound to ++f7++ by default. + +### Themes + +`TextArea` ships with some builtin themes, and you can easily add your own. + +Themes give you control over the look and feel, including syntax highlighting, +the cursor, selection, gutter, and more. + +#### Using builtin themes + +The initial theme of the `TextArea` is determined by the `theme` parameter. + +```python +# Create a TextArea with the 'dracula' theme. +yield TextArea("print(123)", language="python", theme="dracula") +``` + +You can check which themes are available using the [`available_themes`][textual.widgets._text_area.TextArea.available_themes] property. + +```python +>>> text_area = TextArea() +>>> print(text_area.available_themes) +{'dracula', 'github_light', 'monokai', 'vscode_dark'} +``` + +After creating a `TextArea`, you can change the theme by setting the [`theme`][textual.widgets._text_area.TextArea.theme] +attribute to one of the available themes. + +```python +text_area.theme = "vscode_dark" +``` + +On setting this attribute the `TextArea` will immediately refresh to display the updated theme. + +#### Custom themes + +Using custom (non-builtin) themes is two-step process: + +1. Create an instance of [`TextAreaTheme`][textual.widgets.text_area.TextAreaTheme]. +2. Register it using [`TextArea.register_theme`][textual.widgets._text_area.TextArea.register_theme]. + +##### 1. Creating a theme + +Let's create a simple theme, `"my_cool_theme"`, which colors the cursor blue, and the cursor line yellow. +Our theme will also syntax highlight strings as red, and comments as magenta. + +```python +from rich.style import Style +from textual.widgets.text_area import TextAreaTheme +# ... +my_theme = TextAreaTheme( + # This name will be used to refer to the theme... + name="my_cool_theme", + # Basic styles such as background, cursor, selection, gutter, etc... + cursor_style=Style(color="white", bgcolor="blue"), + cursor_line_style=Style(bgcolor="yellow"), + # `syntax_styles` is for syntax highlighting. + # It maps tokens parsed from the document to Rich styles. + syntax_styles={ + "string": Style(color="red"), + "comment": Style(color="magenta"), + } +) +``` + +Attributes like `cursor_style` and `cursor_line_style` apply general language-agnostic +styling to the widget. + +The `syntax_styles` attribute of `TextAreaTheme` is used for syntax highlighting and +depends on the `language` currently in use. +For more details, see [syntax highlighting](#syntax-highlighting). + +If you wish to build on an existing theme, you can obtain a reference to it using the [`TextAreaTheme.get_builtin_theme`][textual.widgets.text_area.TextAreaTheme.get_builtin_theme] classmethod: + +```python +from textual.widgets.text_area import TextAreaTheme + +monokai = TextAreaTheme.get_builtin_theme("monokai") +``` + +##### 2. Registering a theme + +Our theme can now be registered with the `TextArea` instance. + +```python +text_area.register_theme(my_theme) +``` + +After registering a theme, it'll appear in the `available_themes`: + +```python +>>> print(text_area.available_themes) +{'dracula', 'github_light', 'monokai', 'vscode_dark', 'my_cool_theme'} +``` + +We can now switch to it: + +```python +text_area.theme = "my_cool_theme" +``` + +This immediately updates the appearance of the `TextArea`: + +```{.textual path="docs/examples/widgets/text_area_custom_theme.py" columns="42" lines="8"} +``` + +### Indentation + +The character(s) inserted when you press tab is controlled by setting the `indent_type` attribute to either `tabs` or `spaces`. + +If `indent_type == "spaces"`, pressing ++tab++ will insert up to `indent_width` spaces in order to align with the next tab stop. + +### Line separators + +When content is loaded into `TextArea`, the content is scanned from beginning to end +and the first occurrence of a line separator is recorded. + +This separator will then be used when content is later read from the `TextArea` via +the `text` property. The `TextArea` widget does not support exporting text which +contains mixed line endings. + +Similarly, newline characters pasted into the `TextArea` will be converted. + +You can check the line separator of the current document by inspecting `TextArea.document.newline`: + +```python +>>> text_area = TextArea() +>>> text_area.document.newline +'\n' +``` + +### Line numbers + +The gutter (column on the left containing line numbers) can be toggled by setting +the `show_line_numbers` attribute to `True` or `False`. + +Setting this attribute will immediately repaint the `TextArea` to reflect the new value. + +### Extending `TextArea` + +Sometimes, you may wish to subclass `TextArea` to add some extra functionality. +In this section, we'll briefly explore how we can extend the widget to achieve common goals. + +#### Hooking into key presses + +You may wish to hook into certain key presses to inject some functionality. +This can be done by over-riding `_on_key` and adding the required functionality. + +##### Example - closing parentheses automatically + +Let's extend `TextArea` to add a feature which automatically closes parentheses and moves the cursor to a sensible location. + +```python +--8<-- "docs/examples/widgets/text_area_extended.py" +``` + +This intercepts the key handler when `"("` is pressed, and inserts `"()"` instead. +It then moves the cursor so that it lands between the open and closing parentheses. + +Typing `def hello(` into the `TextArea` results in the bracket automatically being closed: + +```{.textual path="docs/examples/widgets/text_area_extended.py" columns="36" lines="4" press="d,e,f,space,h,e,l,l,o,left_parenthesis"} +``` + +### Advanced concepts + +#### Syntax highlighting + +Syntax highlighting inside the `TextArea` is powered by a library called [`tree-sitter`](https://tree-sitter.github.io/tree-sitter/). + +Each time you update the document in a `TextArea`, an internal syntax tree is updated. +This tree is frequently _queried_ to find location ranges relevant to syntax highlighting. +We give these ranges _names_, and ultimately map them to Rich styles inside `TextAreaTheme.syntax_styles`. + +To illustrate how this works, lets look at how the "Monokai" `TextAreaTheme` highlights Markdown files. + +When the `language` attribute is set to `"markdown"`, a highlight query similar to the one below is used (trimmed for brevity). + +```scheme +(heading_content) @heading +(link) @link +``` + +This highlight query maps `heading_content` nodes returned by the Markdown parser to the name `@heading`, +and `link` nodes to the name `@link`. + +Inside our `TextAreaTheme.syntax_styles` dict, we can map the name `@heading` to a Rich style. +Here's a snippet from the "Monokai" theme which does just that: + +```python +TextAreaTheme( + name="monokai", + base_style=Style(color="#f8f8f2", bgcolor="#272822"), + gutter_style=Style(color="#90908a", bgcolor="#272822"), + # ... + syntax_styles={ + # Colorise @heading and make them bold + "heading": Style(color="#F92672", bold=True), + # Colorise and underline @link + "link": Style(color="#66D9EF", underline=True), + # ... + }, +) +``` + +To understand which names can be mapped inside `syntax_styles`, we recommend looking at the existing +themes and highlighting queries (`.scm` files) in the Textual repository. + +!!! tip + + You may also wish to take a look at the contents of `TextArea._highlights` on an + active `TextArea` instance to see which highlights have been generated for the + open document. + +#### Adding support for custom languages + +To add support for a language to a `TextArea`, use the [`register_language`][textual.widgets._text_area.TextArea.register_language] method. + +To register a language, we require two things: + +1. A tree-sitter `Language` object which contains the grammar for the language. +2. A highlight query which is used for [syntax highlighting](#syntax-highlighting). + +##### Example - adding Java support + +The easiest way to obtain a `Language` object is using the [`py-tree-sitter-languages`](https://github.com/grantjenks/py-tree-sitter-languages) package. Here's how we can use this package to obtain a reference to a `Language` object representing Java: + +```python +from tree_sitter_languages import get_language +java_language = get_language("java") +``` + +!!! note + + `py-tree-sitter-languages` may not be available on some architectures (e.g. Macbooks with Apple Silicon running Python 3.7). + +The exact version of the parser used when you call `get_language` can be checked via +the [`repos.txt` file](https://github.com/grantjenks/py-tree-sitter-languages/blob/a6d4f7c903bf647be1bdcfa504df967d13e40427/repos.txt) in +the version of `py-tree-sitter-languages` you're using. This file contains links to the GitHub +repos and commit hashes of the tree-sitter parsers. In these repos you can often find pre-made highlight queries at `queries/highlights.scm`, +and a file showing all the available node types which can be used in highlight queries at `src/node-types.json`. + +Since we're adding support for Java, lets grab the Java highlight query from the repo by following these steps: + +1. Open [`repos.txt` file](https://github.com/grantjenks/py-tree-sitter-languages/blob/a6d4f7c903bf647be1bdcfa504df967d13e40427/repos.txt) from the `py-tree-sitter-languages` repo. +2. Find the link corresponding to `tree-sitter-java` and go to the repo on GitHub (you may also need to go to the specific commit referenced in `repos.txt`). +3. Go to [`queries/highlights.scm`](https://github.com/tree-sitter/tree-sitter-java/blob/ac14b4b1884102839455d32543ab6d53ae089ab7/queries/highlights.scm) to see the example highlight query for Java. + +Be sure to check the license in the repo to ensure it can be freely copied. + +!!! warning + + It's important to use a highlight query which is compatible with the parser in use, so + pay attention to the commit hash when visiting the repo via `repos.txt`. + +We now have our `Language` and our highlight query, so we can register Java as a language. + +```python +--8<-- "docs/examples/widgets/text_area_custom_language.py" +``` + +Running our app, we can see that the Java code is highlighted. +We can freely edit the text, and the syntax highlighting will update immediately. + +```{.textual path="docs/examples/widgets/text_area_custom_language.py" columns="52" lines="8"} +``` + +Recall that we map names (like `@heading`) from the tree-sitter highlight query to Rich style objects inside the `TextAreaTheme.syntax_styles` dictionary. +If you notice some highlights are missing after registering a language, the issue may be: + +1. The current `TextAreaTheme` doesn't contain a mapping for the name in the highlight query. Adding a new to `syntax_styles` should resolve the issue. +2. The highlight query doesn't assign a name to the pattern you expect to be highlighted. In this case you'll need to update the highlight query to assign to the name. + +!!! tip + + The names assigned in tree-sitter highlight queries are often reused across multiple languages. + For example, `@string` is used in many languages to highlight strings. + +## Reactive attributes + +| Name | Type | Default | Description | +|------------------------|--------------------------|--------------------|--------------------------------------------------| +| `language` | `str | None` | `None` | The language to use for syntax highlighting. | +| `theme` | `str | None` | `TextAreaTheme.default()` | The theme to use for syntax highlighting. | +| `selection` | `Selection` | `Selection()` | The current selection. | +| `show_line_numbers` | `bool` | `True` | Show or hide line numbers. | +| `indent_width` | `int` | `4` | The number of spaces to indent and width of tabs. | +| `match_cursor_bracket` | `bool` | `True` | Enable/disable highlighting matching brackets under cursor. | +| `cursor_blink` | `bool` | `True` | Enable/disable blinking of the cursor when the widget has focus. | + +## Messages + +- [TextArea.Changed][textual.widgets._text_area.TextArea.Changed] +- [TextArea.SelectionChanged][textual.widgets._text_area.TextArea.SelectionChanged] + +## Bindings + +The `TextArea` widget defines the following bindings: + +::: textual.widgets._text_area.TextArea.BINDINGS + options: + show_root_heading: false + show_root_toc_entry: false + + +## Component classes + +The `TextArea` widget defines no component classes. + +Styling should be done exclusively via [`TextAreaTheme`][textual.widgets.text_area.TextAreaTheme]. + +## See also + +- [`Input`][textual.widgets.Input] - for single-line text input. +- [`TextAreaTheme`][textual.widgets.text_area.TextAreaTheme] - for theming the `TextArea`. +- The tree-sitter documentation [website](https://tree-sitter.github.io/tree-sitter/). +- The tree-sitter Python bindings [repository](https://github.com/tree-sitter/py-tree-sitter). +- `py-tree-sitter-languages` [repository](https://github.com/grantjenks/py-tree-sitter-languages) (provides binary wheels for a large variety of tree-sitter languages). + +--- + +::: textual.widgets._text_area.TextArea + options: + heading_level: 2 + +--- + +::: textual.widgets.text_area + options: + heading_level: 2 diff --git a/docs/widgets/toast.md b/docs/widgets/toast.md index 647f730369..9f54c0f47b 100644 --- a/docs/widgets/toast.md +++ b/docs/widgets/toast.md @@ -71,6 +71,27 @@ Toast.-information .toast--title { --8<-- "docs/examples/widgets/toast.py" ``` +## Reactive Attributes + +This widget has no reactive attributes. + +## Messages + +This widget posts no messages. + +## Bindings + +This widget has no bindings. + +## Component Classes + +The toast widget provides the following component classes: + +::: textual.widgets._toast.Toast.COMPONENT_CLASSES + options: + show_root_heading: false + show_root_toc_entry: false + --- ::: textual.widgets._toast diff --git a/examples/calculator.py b/examples/calculator.py index 2720e84d4c..90e566c694 100644 --- a/examples/calculator.py +++ b/examples/calculator.py @@ -13,13 +13,13 @@ from textual.containers import Container from textual.css.query import NoMatches from textual.reactive import var -from textual.widgets import Button, Static +from textual.widgets import Button, Digits class CalculatorApp(App): """A working 'desktop' calculator.""" - CSS_PATH = "calculator.css" + CSS_PATH = "calculator.tcss" numbers = var("0") show_ac = var(True) @@ -42,7 +42,7 @@ class CalculatorApp(App): def watch_numbers(self, value: str) -> None: """Called when numbers is updated.""" - self.query_one("#numbers", Static).update(value) + self.query_one("#numbers", Digits).update(value) def compute_show_ac(self) -> bool: """Compute switch to show AC or C button""" @@ -56,7 +56,7 @@ def watch_show_ac(self, show_ac: bool) -> None: def compose(self) -> ComposeResult: """Add our buttons.""" with Container(id="calculator"): - yield Static(id="numbers") + yield Digits(id="numbers") yield Button("AC", id="ac", variant="primary") yield Button("C", id="c", variant="primary") yield Button("+/-", id="plus-minus", variant="primary") @@ -83,7 +83,6 @@ def on_key(self, event: events.Key) -> None: def press(button_id: str) -> None: """Press a button, should it exist.""" - try: self.query_one(f"#{button_id}", Button).press() except NoMatches: diff --git a/examples/calculator.css b/examples/calculator.tcss similarity index 88% rename from examples/calculator.css rename to examples/calculator.tcss index 7e292dd2cd..f25b387fcd 100644 --- a/examples/calculator.css +++ b/examples/calculator.tcss @@ -21,11 +21,12 @@ Button { #numbers { column-span: 4; - content-align: right middle; padding: 0 1; height: 100%; background: $primary-lighten-2; color: $text; + content-align: center middle; + text-align: right; } #number-0 { diff --git a/examples/code_browser.css b/examples/code_browser.css deleted file mode 100644 index 9a2c295c95..0000000000 --- a/examples/code_browser.css +++ /dev/null @@ -1,26 +0,0 @@ -Screen { - background: $surface-darken-1; -} - -#tree-view { - display: none; - scrollbar-gutter: stable; - overflow: auto; - width: auto; - height: 100%; - dock: left; -} - -CodeBrowser.-show-tree #tree-view { - display: block; - max-width: 50%; -} - - -#code-view { - overflow: auto scroll; - min-width: 100%; -} -#code { - width: auto; -} diff --git a/examples/code_browser.py b/examples/code_browser.py index 025f99f653..f3d482d5e2 100644 --- a/examples/code_browser.py +++ b/examples/code_browser.py @@ -11,7 +11,6 @@ from rich.syntax import Syntax from rich.traceback import Traceback -from textual import events from textual.app import App, ComposeResult from textual.containers import Container, VerticalScroll from textual.reactive import var @@ -21,7 +20,7 @@ class CodeBrowser(App): """Textual code browser app.""" - CSS_PATH = "code_browser.css" + CSS_PATH = "code_browser.tcss" BINDINGS = [ ("f", "toggle_files", "Toggle Files"), ("q", "quit", "Quit"), @@ -43,7 +42,7 @@ def compose(self) -> ComposeResult: yield Static(id="code", expand=True) yield Footer() - def on_mount(self, event: events.Mount) -> None: + def on_mount(self) -> None: self.query_one(DirectoryTree).focus() def on_directory_tree_file_selected( diff --git a/examples/code_browser.tcss b/examples/code_browser.tcss new file mode 100644 index 0000000000..05928614b3 --- /dev/null +++ b/examples/code_browser.tcss @@ -0,0 +1,26 @@ +Screen { + background: $surface-darken-1; +} + +#tree-view { + display: none; + scrollbar-gutter: stable; + overflow: auto; + width: auto; + height: 100%; + dock: left; +} + +CodeBrowser.-show-tree #tree-view { + display: block; + max-width: 50%; +} + + +#code-view { + overflow: auto scroll; + min-width: 100%; +} +#code { + width: auto; +} diff --git a/examples/color_command.py b/examples/color_command.py new file mode 100644 index 0000000000..bd4148657b --- /dev/null +++ b/examples/color_command.py @@ -0,0 +1,67 @@ +from dataclasses import dataclass +from functools import partial + +from textual import on +from textual._color_constants import COLOR_NAME_TO_RGB +from textual.app import App, ComposeResult +from textual.command import Hit, Hits, Provider +from textual.message import Message +from textual.widgets import Header, Static + + +@dataclass +class SwitchColor(Message, bubble=False): + """Message to tell the app to switch color.""" + + color: str + + +class ColorCommands(Provider): + """A command provider to select colors.""" + + async def search(self, query: str) -> Hits: + """Called for each key.""" + matcher = self.matcher(query) + for color in COLOR_NAME_TO_RGB.keys(): + score = matcher.match(color) + if score > 0: + yield Hit( + score, + matcher.highlight(color), + partial(self.app.post_message, SwitchColor(color)), + ) + + +class ColorBlock(Static): + """Simple block of color.""" + + DEFAULT_CSS = """ + ColorBlock{ + padding: 3 6; + margin: 1 2; + color: auto; + } + """ + + +class ColorApp(App): + """Experiment with the command palette.""" + + COMMANDS = App.COMMANDS | {ColorCommands} + TITLE = "Press ctrl + \\ and type a color" + + def compose(self) -> ComposeResult: + yield Header() + + @on(SwitchColor) + def switch_color(self, event: SwitchColor) -> None: + """Adds a color block on demand.""" + color_block = ColorBlock(event.color) + color_block.styles.background = event.color + self.mount(color_block) + self.screen.scroll_end() + + +if __name__ == "__main__": + app = ColorApp() + app.run() diff --git a/examples/dictionary.py b/examples/dictionary.py index a693451021..b8b31096dd 100644 --- a/examples/dictionary.py +++ b/examples/dictionary.py @@ -15,7 +15,7 @@ class DictionaryApp(App): """Searches ab dictionary API as-you-type.""" - CSS_PATH = "dictionary.css" + CSS_PATH = "dictionary.tcss" def compose(self) -> ComposeResult: yield Input(placeholder="Search for a word") diff --git a/examples/dictionary.css b/examples/dictionary.tcss similarity index 100% rename from examples/dictionary.css rename to examples/dictionary.tcss diff --git a/examples/five_by_five.py b/examples/five_by_five.py index 11e3f1e1f3..6859cf86c6 100644 --- a/examples/five_by_five.py +++ b/examples/five_by_five.py @@ -302,7 +302,7 @@ def on_mount(self) -> None: class FiveByFive(App[None]): """Main 5x5 application class.""" - CSS_PATH = "five_by_five.css" + CSS_PATH = "five_by_five.tcss" """The name of the stylesheet for the app.""" SCREENS = {"help": Help} diff --git a/examples/five_by_five.css b/examples/five_by_five.tcss similarity index 97% rename from examples/five_by_five.css rename to examples/five_by_five.tcss index 329f230f28..5f435ecdd1 100644 --- a/examples/five_by_five.css +++ b/examples/five_by_five.tcss @@ -82,4 +82,4 @@ Help { border: round $primary-lighten-3; } -/* five_by_five.css ends here */ +/* five_by_five.tcss ends here */ diff --git a/examples/markdown.py b/examples/markdown.py index 0ade6718a7..a6d26cb190 100644 --- a/examples/markdown.py +++ b/examples/markdown.py @@ -1,4 +1,5 @@ from pathlib import Path +from sys import argv from textual.app import App, ComposeResult from textual.reactive import var @@ -44,4 +45,6 @@ async def action_forward(self) -> None: if __name__ == "__main__": app = MarkdownApp() + if len(argv) > 1 and Path(argv[1]).exists(): + app.path = Path(argv[1]) app.run() diff --git a/faq.yml b/faq.yml index 1172358872..5e9112ad5b 100644 --- a/faq.yml +++ b/faq.yml @@ -1,7 +1,7 @@ # FAQtory settings -faq_url: "https://github.com/textualize/textual/blob/main/FAQ.md" # Replace this with the URL to your FAQ.md! +faq_url: "https://textual.textualize.io/FAQ/" # Replace this with the URL to your FAQ.md! questions_path: "./questions" # Where questions should be stored -output_path: "./FAQ.md" # Where FAQ.md should be generated +output_path: "./docs/FAQ.md" # Where FAQ.md should be generated templates_path: ".faq" # Path to templates diff --git a/mkdocs-common.yml b/mkdocs-common.yml index 0d59822f6a..23000851dd 100644 --- a/mkdocs-common.yml +++ b/mkdocs-common.yml @@ -9,6 +9,7 @@ markdown_extensions: - admonition - def_list - meta + - footnotes - toc: permalink: true @@ -34,10 +35,12 @@ markdown_extensions: alternate_style: true - pymdownx.snippets - markdown.extensions.attr_list + - pymdownx.details theme: name: material custom_dir: docs/custom_theme + logo: images/icons/logo light transparent.svg features: - navigation.tabs - navigation.indexes @@ -46,18 +49,18 @@ theme: - content.code.annotate - content.code.copy palette: - - media: "(prefers-color-scheme: light)" - scheme: default - accent: purple - toggle: - icon: material/weather-sunny - name: Switch to dark mode - - media: "(prefers-color-scheme: dark)" - scheme: slate - primary: black - toggle: - icon: material/weather-night - name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: default + accent: purple + toggle: + icon: material/weather-sunny + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + toggle: + icon: material/weather-night + name: Switch to light mode plugins: search: @@ -78,6 +81,17 @@ plugins: - "!^_" - "^__init__$" - "!^can_replace$" + # Hide some methods that Widget subclasses implement but that we don't want + # to be shown in the docs. + # This is then overridden in widget.md and app.md so that it shows in the + # base class. + - "!^compose$" + - "!^render$" + - "!^render_line$" + - "!^render_lines$" + - "!^get_content_width$" + - "!^get_content_height$" + - "!^compose_add_child$" watch: - mkdocs-common.yml - mkdocs-nav.yml @@ -89,11 +103,9 @@ plugins: - "**/_template.md" - "snippets/*" - extra_css: - stylesheets/custom.css - extra: social: - icon: fontawesome/brands/twitter diff --git a/mkdocs-nav.yml b/mkdocs-nav.yml index adacbb6723..2d61063111 100644 --- a/mkdocs-nav.yml +++ b/mkdocs-nav.yml @@ -1,205 +1,214 @@ nav: - - Introduction: - - "index.md" - - "getting_started.md" - - "help.md" - - "tutorial.md" - - Guide: - - "guide/index.md" - - "guide/devtools.md" - - "guide/app.md" - - "guide/styles.md" - - "guide/CSS.md" - - "guide/design.md" - - "guide/queries.md" - - "guide/layout.md" - - "guide/events.md" - - "guide/input.md" - - "guide/actions.md" - - "guide/reactivity.md" - - "guide/widgets.md" - - "guide/animation.md" - - "guide/screens.md" - - "guide/workers.md" - - "widget_gallery.md" - - Reference: - - "reference/index.md" - - CSS Types: - - "css_types/index.md" - - "css_types/border.md" - - "css_types/color.md" - - "css_types/horizontal.md" - - "css_types/integer.md" - - "css_types/name.md" - - "css_types/number.md" - - "css_types/overflow.md" - - "css_types/percentage.md" - - "css_types/scalar.md" - - "css_types/text_align.md" - - "css_types/text_style.md" - - "css_types/vertical.md" - - Events: - - "events/index.md" - - "events/blur.md" - - "events/descendant_blur.md" - - "events/descendant_focus.md" - - "events/enter.md" - - "events/focus.md" - - "events/hide.md" - - "events/key.md" - - "events/leave.md" - - "events/load.md" - - "events/mount.md" - - "events/mouse_capture.md" - - "events/click.md" - - "events/mouse_down.md" - - "events/mouse_move.md" - - "events/mouse_release.md" - - "events/mouse_scroll_down.md" - - "events/mouse_scroll_up.md" - - "events/mouse_up.md" - - "events/paste.md" - - "events/resize.md" - - "events/screen_resume.md" - - "events/screen_suspend.md" - - "events/show.md" - - Styles: - - "styles/align.md" - - "styles/background.md" - - "styles/border.md" - - "styles/border_subtitle_align.md" - - "styles/border_subtitle_background.md" - - "styles/border_subtitle_color.md" - - "styles/border_subtitle_style.md" - - "styles/border_title_align.md" - - "styles/border_title_background.md" - - "styles/border_title_color.md" - - "styles/border_title_style.md" - - "styles/box_sizing.md" - - "styles/color.md" - - "styles/content_align.md" - - "styles/display.md" - - "styles/dock.md" - - "styles/index.md" - - Grid: - - "styles/grid/index.md" - - "styles/grid/column_span.md" - - "styles/grid/grid_columns.md" - - "styles/grid/grid_gutter.md" - - "styles/grid/grid_rows.md" - - "styles/grid/grid_size.md" - - "styles/grid/row_span.md" - - "styles/height.md" - - "styles/layer.md" - - "styles/layers.md" - - "styles/layout.md" - - Links: - - "styles/links/index.md" - - "styles/links/link_background.md" - - "styles/links/link_color.md" - - "styles/links/link_hover_background.md" - - "styles/links/link_hover_color.md" - - "styles/links/link_hover_style.md" - - "styles/links/link_style.md" - - "styles/margin.md" - - "styles/max_height.md" - - "styles/max_width.md" - - "styles/min_height.md" - - "styles/min_width.md" - - "styles/offset.md" - - "styles/opacity.md" - - "styles/outline.md" - - "styles/overflow.md" - - "styles/padding.md" - - Scrollbar colors: - - "styles/scrollbar_colors/index.md" - - "styles/scrollbar_colors/scrollbar_background.md" - - "styles/scrollbar_colors/scrollbar_background_active.md" - - "styles/scrollbar_colors/scrollbar_background_hover.md" - - "styles/scrollbar_colors/scrollbar_color.md" - - "styles/scrollbar_colors/scrollbar_color_active.md" - - "styles/scrollbar_colors/scrollbar_color_hover.md" - - "styles/scrollbar_colors/scrollbar_corner_color.md" - - "styles/scrollbar_gutter.md" - - "styles/scrollbar_size.md" - - "styles/text_align.md" - - "styles/text_opacity.md" - - "styles/text_style.md" - - "styles/tint.md" - - "styles/visibility.md" - - "styles/width.md" - - Widgets: - - "widgets/button.md" - - "widgets/checkbox.md" - - "widgets/content_switcher.md" - - "widgets/data_table.md" - - "widgets/digits.md" - - "widgets/directory_tree.md" - - "widgets/footer.md" - - "widgets/header.md" - - "widgets/index.md" - - "widgets/input.md" - - "widgets/label.md" - - "widgets/list_item.md" - - "widgets/list_view.md" - - "widgets/loading_indicator.md" - - "widgets/log.md" - - "widgets/markdown_viewer.md" - - "widgets/markdown.md" - - "widgets/option_list.md" - - "widgets/placeholder.md" - - "widgets/pretty.md" - - "widgets/progress_bar.md" - - "widgets/radiobutton.md" - - "widgets/radioset.md" - - "widgets/rich_log.md" - - "widgets/select.md" - - "widgets/selection_list.md" - - "widgets/sparkline.md" - - "widgets/static.md" - - "widgets/switch.md" - - "widgets/tabbed_content.md" - - "widgets/tabs.md" - - "widgets/tree.md" - - API: - - "api/index.md" - - "api/app.md" - - "api/await_remove.md" - - "api/binding.md" - - "api/color.md" - - "api/containers.md" - - "api/coordinate.md" - - "api/dom_node.md" - - "api/events.md" - - "api/errors.md" - - "api/filter.md" - - "api/geometry.md" - - "api/logger.md" - - "api/logging.md" - - "api/map_geometry.md" - - "api/message_pump.md" - - "api/message.md" - - "api/on.md" - - "api/pilot.md" - - "api/query.md" - - "api/reactive.md" - - "api/screen.md" - - "api/scrollbar.md" - - "api/scroll_view.md" - - "api/strip.md" - - "api/suggester.md" - - "api/timer.md" - - "api/types.md" - - "api/validation.md" - - "api/walk.md" - - "api/widget.md" - - "api/work.md" - - "api/worker.md" - - "api/worker_manager.md" - - "How To": - - "how-to/index.md" - - "how-to/center-things.md" - - "how-to/design-a-layout.md" - - "roadmap.md" - - "Blog": - - blog/index.md + - "index.md" + - Introduction: + - "getting_started.md" + - "help.md" + - "tutorial.md" + - Guide: + - "guide/index.md" + - "guide/devtools.md" + - "guide/app.md" + - "guide/styles.md" + - "guide/CSS.md" + - "guide/design.md" + - "guide/queries.md" + - "guide/layout.md" + - "guide/events.md" + - "guide/input.md" + - "guide/actions.md" + - "guide/reactivity.md" + - "guide/widgets.md" + - "guide/animation.md" + - "guide/screens.md" + - "guide/workers.md" + - "guide/command_palette.md" + - "guide/testing.md" + - "widget_gallery.md" + - Reference: + - "reference/index.md" + - CSS Types: + - "css_types/index.md" + - "css_types/border.md" + - "css_types/color.md" + - "css_types/horizontal.md" + - "css_types/integer.md" + - "css_types/name.md" + - "css_types/number.md" + - "css_types/overflow.md" + - "css_types/percentage.md" + - "css_types/scalar.md" + - "css_types/text_align.md" + - "css_types/text_style.md" + - "css_types/vertical.md" + - Events: + - "events/index.md" + - "events/blur.md" + - "events/descendant_blur.md" + - "events/descendant_focus.md" + - "events/enter.md" + - "events/focus.md" + - "events/hide.md" + - "events/key.md" + - "events/leave.md" + - "events/load.md" + - "events/mount.md" + - "events/mouse_capture.md" + - "events/click.md" + - "events/mouse_down.md" + - "events/mouse_move.md" + - "events/mouse_release.md" + - "events/mouse_scroll_down.md" + - "events/mouse_scroll_up.md" + - "events/mouse_up.md" + - "events/paste.md" + - "events/resize.md" + - "events/screen_resume.md" + - "events/screen_suspend.md" + - "events/show.md" + - Styles: + - "styles/align.md" + - "styles/background.md" + - "styles/border.md" + - "styles/border_subtitle_align.md" + - "styles/border_subtitle_background.md" + - "styles/border_subtitle_color.md" + - "styles/border_subtitle_style.md" + - "styles/border_title_align.md" + - "styles/border_title_background.md" + - "styles/border_title_color.md" + - "styles/border_title_style.md" + - "styles/box_sizing.md" + - "styles/color.md" + - "styles/content_align.md" + - "styles/display.md" + - "styles/dock.md" + - "styles/index.md" + - Grid: + - "styles/grid/index.md" + - "styles/grid/column_span.md" + - "styles/grid/grid_columns.md" + - "styles/grid/grid_gutter.md" + - "styles/grid/grid_rows.md" + - "styles/grid/grid_size.md" + - "styles/grid/row_span.md" + - "styles/height.md" + - "styles/layer.md" + - "styles/layers.md" + - "styles/layout.md" + - Links: + - "styles/links/index.md" + - "styles/links/link_background.md" + - "styles/links/link_color.md" + - "styles/links/link_hover_background.md" + - "styles/links/link_hover_color.md" + - "styles/links/link_hover_style.md" + - "styles/links/link_style.md" + - "styles/margin.md" + - "styles/max_height.md" + - "styles/max_width.md" + - "styles/min_height.md" + - "styles/min_width.md" + - "styles/offset.md" + - "styles/opacity.md" + - "styles/outline.md" + - "styles/overflow.md" + - "styles/padding.md" + - Scrollbar colors: + - "styles/scrollbar_colors/index.md" + - "styles/scrollbar_colors/scrollbar_background.md" + - "styles/scrollbar_colors/scrollbar_background_active.md" + - "styles/scrollbar_colors/scrollbar_background_hover.md" + - "styles/scrollbar_colors/scrollbar_color.md" + - "styles/scrollbar_colors/scrollbar_color_active.md" + - "styles/scrollbar_colors/scrollbar_color_hover.md" + - "styles/scrollbar_colors/scrollbar_corner_color.md" + - "styles/scrollbar_gutter.md" + - "styles/scrollbar_size.md" + - "styles/text_align.md" + - "styles/text_opacity.md" + - "styles/text_style.md" + - "styles/tint.md" + - "styles/visibility.md" + - "styles/width.md" + - Widgets: + - "widgets/button.md" + - "widgets/checkbox.md" + - "widgets/collapsible.md" + - "widgets/content_switcher.md" + - "widgets/data_table.md" + - "widgets/digits.md" + - "widgets/directory_tree.md" + - "widgets/footer.md" + - "widgets/header.md" + - "widgets/index.md" + - "widgets/input.md" + - "widgets/label.md" + - "widgets/list_item.md" + - "widgets/list_view.md" + - "widgets/loading_indicator.md" + - "widgets/log.md" + - "widgets/markdown_viewer.md" + - "widgets/markdown.md" + - "widgets/option_list.md" + - "widgets/placeholder.md" + - "widgets/pretty.md" + - "widgets/progress_bar.md" + - "widgets/radiobutton.md" + - "widgets/radioset.md" + - "widgets/rich_log.md" + - "widgets/rule.md" + - "widgets/select.md" + - "widgets/selection_list.md" + - "widgets/sparkline.md" + - "widgets/static.md" + - "widgets/switch.md" + - "widgets/tabbed_content.md" + - "widgets/tabs.md" + - "widgets/text_area.md" + - "widgets/tree.md" + - API: + - "api/index.md" + - "api/app.md" + - "api/await_remove.md" + - "api/binding.md" + - "api/color.md" + - "api/command.md" + - "api/containers.md" + - "api/coordinate.md" + - "api/dom_node.md" + - "api/events.md" + - "api/errors.md" + - "api/filter.md" + - "api/fuzzy_matcher.md" + - "api/geometry.md" + - "api/logger.md" + - "api/logging.md" + - "api/map_geometry.md" + - "api/message_pump.md" + - "api/message.md" + - "api/on.md" + - "api/pilot.md" + - "api/query.md" + - "api/reactive.md" + - "api/screen.md" + - "api/scrollbar.md" + - "api/scroll_view.md" + - "api/strip.md" + - "api/suggester.md" + - "api/system_commands_source.md" + - "api/timer.md" + - "api/types.md" + - "api/validation.md" + - "api/walk.md" + - "api/widget.md" + - "api/work.md" + - "api/worker.md" + - "api/worker_manager.md" + - "How To": + - "how-to/index.md" + - "how-to/center-things.md" + - "how-to/design-a-layout.md" + - "FAQ.md" + - "roadmap.md" + - "Blog": + - blog/index.md diff --git a/poetry.lock b/poetry.lock index 9835091abd..65114c3f63 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,101 +1,10 @@ -# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. - [[package]] name = "aiohttp" -version = "3.8.5" +version = "3.8.6" description = "Async http client/server framework (asyncio)" category = "dev" optional = false python-versions = ">=3.6" -files = [ - {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a94159871304770da4dd371f4291b20cac04e8c94f11bdea1c3478e557fbe0d8"}, - {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:13bf85afc99ce6f9ee3567b04501f18f9f8dbbb2ea11ed1a2e079670403a7c84"}, - {file = "aiohttp-3.8.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ce2ac5708501afc4847221a521f7e4b245abf5178cf5ddae9d5b3856ddb2f3a"}, - {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96943e5dcc37a6529d18766597c491798b7eb7a61d48878611298afc1fca946c"}, - {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ad5c3c4590bb3cc28b4382f031f3783f25ec223557124c68754a2231d989e2b"}, - {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c413c633d0512df4dc7fd2373ec06cc6a815b7b6d6c2f208ada7e9e93a5061d"}, - {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df72ac063b97837a80d80dec8d54c241af059cc9bb42c4de68bd5b61ceb37caa"}, - {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c48c5c0271149cfe467c0ff8eb941279fd6e3f65c9a388c984e0e6cf57538e14"}, - {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:368a42363c4d70ab52c2c6420a57f190ed3dfaca6a1b19afda8165ee16416a82"}, - {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7607ec3ce4993464368505888af5beb446845a014bc676d349efec0e05085905"}, - {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0d21c684808288a98914e5aaf2a7c6a3179d4df11d249799c32d1808e79503b5"}, - {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:312fcfbacc7880a8da0ae8b6abc6cc7d752e9caa0051a53d217a650b25e9a691"}, - {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ad093e823df03bb3fd37e7dec9d4670c34f9e24aeace76808fc20a507cace825"}, - {file = "aiohttp-3.8.5-cp310-cp310-win32.whl", hash = "sha256:33279701c04351a2914e1100b62b2a7fdb9a25995c4a104259f9a5ead7ed4802"}, - {file = "aiohttp-3.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:6e4a280e4b975a2e7745573e3fc9c9ba0d1194a3738ce1cbaa80626cc9b4f4df"}, - {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae871a964e1987a943d83d6709d20ec6103ca1eaf52f7e0d36ee1b5bebb8b9b9"}, - {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:461908b2578955045efde733719d62f2b649c404189a09a632d245b445c9c975"}, - {file = "aiohttp-3.8.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72a860c215e26192379f57cae5ab12b168b75db8271f111019509a1196dfc780"}, - {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc14be025665dba6202b6a71cfcdb53210cc498e50068bc088076624471f8bb9"}, - {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8af740fc2711ad85f1a5c034a435782fbd5b5f8314c9a3ef071424a8158d7f6b"}, - {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:841cd8233cbd2111a0ef0a522ce016357c5e3aff8a8ce92bcfa14cef890d698f"}, - {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed1c46fb119f1b59304b5ec89f834f07124cd23ae5b74288e364477641060ff"}, - {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84f8ae3e09a34f35c18fa57f015cc394bd1389bce02503fb30c394d04ee6b938"}, - {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62360cb771707cb70a6fd114b9871d20d7dd2163a0feafe43fd115cfe4fe845e"}, - {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:23fb25a9f0a1ca1f24c0a371523546366bb642397c94ab45ad3aedf2941cec6a"}, - {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0ba0d15164eae3d878260d4c4df859bbdc6466e9e6689c344a13334f988bb53"}, - {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5d20003b635fc6ae3f96d7260281dfaf1894fc3aa24d1888a9b2628e97c241e5"}, - {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0175d745d9e85c40dcc51c8f88c74bfbaef9e7afeeeb9d03c37977270303064c"}, - {file = "aiohttp-3.8.5-cp311-cp311-win32.whl", hash = "sha256:2e1b1e51b0774408f091d268648e3d57f7260c1682e7d3a63cb00d22d71bb945"}, - {file = "aiohttp-3.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:043d2299f6dfdc92f0ac5e995dfc56668e1587cea7f9aa9d8a78a1b6554e5755"}, - {file = "aiohttp-3.8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cae533195e8122584ec87531d6df000ad07737eaa3c81209e85c928854d2195c"}, - {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f21e83f355643c345177a5d1d8079f9f28b5133bcd154193b799d380331d5d3"}, - {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a75ef35f2df54ad55dbf4b73fe1da96f370e51b10c91f08b19603c64004acc"}, - {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e2e9839e14dd5308ee773c97115f1e0a1cb1d75cbeeee9f33824fa5144c7634"}, - {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44e65da1de4403d0576473e2344828ef9c4c6244d65cf4b75549bb46d40b8dd"}, - {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d847e4cde6ecc19125ccbc9bfac4a7ab37c234dd88fbb3c5c524e8e14da543"}, - {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:c7a815258e5895d8900aec4454f38dca9aed71085f227537208057853f9d13f2"}, - {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:8b929b9bd7cd7c3939f8bcfffa92fae7480bd1aa425279d51a89327d600c704d"}, - {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:5db3a5b833764280ed7618393832e0853e40f3d3e9aa128ac0ba0f8278d08649"}, - {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:a0215ce6041d501f3155dc219712bc41252d0ab76474615b9700d63d4d9292af"}, - {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:fd1ed388ea7fbed22c4968dd64bab0198de60750a25fe8c0c9d4bef5abe13824"}, - {file = "aiohttp-3.8.5-cp36-cp36m-win32.whl", hash = "sha256:6e6783bcc45f397fdebc118d772103d751b54cddf5b60fbcc958382d7dd64f3e"}, - {file = "aiohttp-3.8.5-cp36-cp36m-win_amd64.whl", hash = "sha256:b5411d82cddd212644cf9360879eb5080f0d5f7d809d03262c50dad02f01421a"}, - {file = "aiohttp-3.8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:01d4c0c874aa4ddfb8098e85d10b5e875a70adc63db91f1ae65a4b04d3344cda"}, - {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5980a746d547a6ba173fd5ee85ce9077e72d118758db05d229044b469d9029a"}, - {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a482e6da906d5e6e653be079b29bc173a48e381600161c9932d89dfae5942ef"}, - {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80bd372b8d0715c66c974cf57fe363621a02f359f1ec81cba97366948c7fc873"}, - {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1161b345c0a444ebcf46bf0a740ba5dcf50612fd3d0528883fdc0eff578006a"}, - {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd56db019015b6acfaaf92e1ac40eb8434847d9bf88b4be4efe5bfd260aee692"}, - {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:153c2549f6c004d2754cc60603d4668899c9895b8a89397444a9c4efa282aaf4"}, - {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4a01951fabc4ce26ab791da5f3f24dca6d9a6f24121746eb19756416ff2d881b"}, - {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bfb9162dcf01f615462b995a516ba03e769de0789de1cadc0f916265c257e5d8"}, - {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7dde0009408969a43b04c16cbbe252c4f5ef4574ac226bc8815cd7342d2028b6"}, - {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4149d34c32f9638f38f544b3977a4c24052042affa895352d3636fa8bffd030a"}, - {file = "aiohttp-3.8.5-cp37-cp37m-win32.whl", hash = "sha256:68c5a82c8779bdfc6367c967a4a1b2aa52cd3595388bf5961a62158ee8a59e22"}, - {file = "aiohttp-3.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2cf57fb50be5f52bda004b8893e63b48530ed9f0d6c96c84620dc92fe3cd9b9d"}, - {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:eca4bf3734c541dc4f374ad6010a68ff6c6748f00451707f39857f429ca36ced"}, - {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1274477e4c71ce8cfe6c1ec2f806d57c015ebf84d83373676036e256bc55d690"}, - {file = "aiohttp-3.8.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:28c543e54710d6158fc6f439296c7865b29e0b616629767e685a7185fab4a6b9"}, - {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:910bec0c49637d213f5d9877105d26e0c4a4de2f8b1b29405ff37e9fc0ad52b8"}, - {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5443910d662db951b2e58eb70b0fbe6b6e2ae613477129a5805d0b66c54b6cb7"}, - {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e460be6978fc24e3df83193dc0cc4de46c9909ed92dd47d349a452ef49325b7"}, - {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb1558def481d84f03b45888473fc5a1f35747b5f334ef4e7a571bc0dfcb11f8"}, - {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34dd0c107799dcbbf7d48b53be761a013c0adf5571bf50c4ecad5643fe9cfcd0"}, - {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aa1990247f02a54185dc0dff92a6904521172a22664c863a03ff64c42f9b5410"}, - {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0e584a10f204a617d71d359fe383406305a4b595b333721fa50b867b4a0a1548"}, - {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a3cf433f127efa43fee6b90ea4c6edf6c4a17109d1d037d1a52abec84d8f2e42"}, - {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c11f5b099adafb18e65c2c997d57108b5bbeaa9eeee64a84302c0978b1ec948b"}, - {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:84de26ddf621d7ac4c975dbea4c945860e08cccde492269db4e1538a6a6f3c35"}, - {file = "aiohttp-3.8.5-cp38-cp38-win32.whl", hash = "sha256:ab88bafedc57dd0aab55fa728ea10c1911f7e4d8b43e1d838a1739f33712921c"}, - {file = "aiohttp-3.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:5798a9aad1879f626589f3df0f8b79b3608a92e9beab10e5fda02c8a2c60db2e"}, - {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a6ce61195c6a19c785df04e71a4537e29eaa2c50fe745b732aa937c0c77169f3"}, - {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:773dd01706d4db536335fcfae6ea2440a70ceb03dd3e7378f3e815b03c97ab51"}, - {file = "aiohttp-3.8.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f83a552443a526ea38d064588613aca983d0ee0038801bc93c0c916428310c28"}, - {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f7372f7341fcc16f57b2caded43e81ddd18df53320b6f9f042acad41f8e049a"}, - {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea353162f249c8097ea63c2169dd1aa55de1e8fecbe63412a9bc50816e87b761"}, - {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d47ae48db0b2dcf70bc8a3bc72b3de86e2a590fc299fdbbb15af320d2659de"}, - {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d827176898a2b0b09694fbd1088c7a31836d1a505c243811c87ae53a3f6273c1"}, - {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3562b06567c06439d8b447037bb655ef69786c590b1de86c7ab81efe1c9c15d8"}, - {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4e874cbf8caf8959d2adf572a78bba17cb0e9d7e51bb83d86a3697b686a0ab4d"}, - {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6809a00deaf3810e38c628e9a33271892f815b853605a936e2e9e5129762356c"}, - {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:33776e945d89b29251b33a7e7d006ce86447b2cfd66db5e5ded4e5cd0340585c"}, - {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eaeed7abfb5d64c539e2db173f63631455f1196c37d9d8d873fc316470dfbacd"}, - {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e91d635961bec2d8f19dfeb41a539eb94bd073f075ca6dae6c8dc0ee89ad6f91"}, - {file = "aiohttp-3.8.5-cp39-cp39-win32.whl", hash = "sha256:00ad4b6f185ec67f3e6562e8a1d2b69660be43070bd0ef6fcec5211154c7df67"}, - {file = "aiohttp-3.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:c0a9034379a37ae42dea7ac1e048352d96286626251862e448933c0f59cbd79c"}, - {file = "aiohttp-3.8.5.tar.gz", hash = "sha256:b9552ec52cc147dbf1944ac7ac98af7602e51ea2dcd076ed194ca3c0d1c7d0bc"}, -] [package.dependencies] aiosignal = ">=1.1.2" @@ -118,10 +27,6 @@ description = "aiosignal: a list of registered asynchronous callbacks" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, -] [package.dependencies] frozenlist = ">=1.1.0" @@ -133,10 +38,6 @@ description = "High level compatibility layer for multiple asynchronous event lo category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, - {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, -] [package.dependencies] exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} @@ -151,15 +52,11 @@ trio = ["trio (<0.22)"] [[package]] name = "async-timeout" -version = "4.0.2" +version = "4.0.3" description = "Timeout context manager for asyncio programs" category = "dev" optional = false -python-versions = ">=3.6" -files = [ - {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, - {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, -] +python-versions = ">=3.7" [package.dependencies] typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""} @@ -171,10 +68,6 @@ description = "Enhance the standard unittest package with features for testing a category = "dev" optional = false python-versions = ">=3.5" -files = [ - {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, - {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, -] [[package]] name = "attrs" @@ -183,10 +76,6 @@ description = "Classes Without Boilerplate" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, -] [package.dependencies] importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} @@ -198,6 +87,20 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib- tests = ["attrs[tests-no-zope]", "zope-interface"] tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +[[package]] +name = "babel" +version = "2.13.0" +description = "Internationalization utilities" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + [[package]] name = "black" version = "23.3.0" @@ -205,33 +108,6 @@ description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, - {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, - {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, - {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, - {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, - {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, - {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, - {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, - {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, - {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, - {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, - {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, - {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, - {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, -] [package.dependencies] click = ">=8.0.0" @@ -256,10 +132,6 @@ description = "A decorator for caching properties in classes." category = "dev" optional = false python-versions = "*" -files = [ - {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, - {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, -] [[package]] name = "certifi" @@ -268,10 +140,6 @@ description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false python-versions = ">=3.6" -files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, -] [[package]] name = "cfgv" @@ -280,107 +148,22 @@ description = "Validate configuration and produce human readable error messages. category = "dev" optional = false python-versions = ">=3.6.1" -files = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, -] [[package]] name = "charset-normalizer" -version = "3.2.0" +version = "3.3.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, - {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, -] [[package]] name = "click" -version = "8.1.6" +version = "8.1.7" description = "Composable command line interface toolkit" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, - {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, -] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -393,10 +176,6 @@ description = "Cross-platform colored terminal text." category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] [[package]] name = "colored" @@ -405,9 +184,6 @@ description = "Simple library for color and formatting to terminal" category = "dev" optional = false python-versions = "*" -files = [ - {file = "colored-1.4.4.tar.gz", hash = "sha256:04ff4d4dd514274fe3b99a21bb52fb96f2688c01e93fba7bef37221e7cb56ce0"}, -] [[package]] name = "coverage" @@ -416,68 +192,6 @@ description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, - {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, - {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, - {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, - {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, - {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, - {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, - {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, - {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, - {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, - {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, - {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, - {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, - {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, - {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, - {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, - {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, - {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, -] [package.extras] toml = ["tomli"] @@ -489,22 +203,14 @@ description = "Distribution utilities" category = "dev" optional = false python-versions = "*" -files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, -] [[package]] name = "exceptiongroup" -version = "1.1.2" +version = "1.1.3" description = "Backport of PEP 654 (exception groups)" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, - {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, -] [package.extras] test = ["pytest (>=6)"] @@ -516,10 +222,6 @@ description = "A platform independent file lock." category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, - {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, -] [package.extras] docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] @@ -532,82 +234,6 @@ description = "A list-like structure which implements collections.abc.MutableSeq category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"}, - {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"}, - {file = "frozenlist-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd"}, - {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f"}, - {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420"}, - {file = "frozenlist-1.3.3-cp310-cp310-win32.whl", hash = "sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642"}, - {file = "frozenlist-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1"}, - {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7"}, - {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678"}, - {file = "frozenlist-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b"}, - {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4"}, - {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81"}, - {file = "frozenlist-1.3.3-cp311-cp311-win32.whl", hash = "sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8"}, - {file = "frozenlist-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32"}, - {file = "frozenlist-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d"}, - {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb"}, - {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401"}, - {file = "frozenlist-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a"}, - {file = "frozenlist-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411"}, - {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a"}, - {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5"}, - {file = "frozenlist-1.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2"}, - {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13"}, - {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3"}, - {file = "frozenlist-1.3.3-cp38-cp38-win32.whl", hash = "sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b"}, - {file = "frozenlist-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef"}, - {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf"}, - {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1"}, - {file = "frozenlist-1.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b"}, - {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab"}, - {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1"}, - {file = "frozenlist-1.3.3-cp39-cp39-win32.whl", hash = "sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38"}, - {file = "frozenlist-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9"}, - {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"}, -] [[package]] name = "ghp-import" @@ -616,10 +242,6 @@ description = "Copy your docs directly to the gh-pages branch." category = "dev" optional = false python-versions = "*" -files = [ - {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, - {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, -] [package.dependencies] python-dateutil = ">=2.8.1" @@ -634,30 +256,25 @@ description = "Git Object Database" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, - {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"}, -] [package.dependencies] smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.32" +version = "3.1.37" description = "GitPython is a Python library used to interact with Git repositories" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "GitPython-3.1.32-py3-none-any.whl", hash = "sha256:e3d59b1c2c6ebb9dfa7a184daf3b6dd4914237e7488a1730a6d8f6f5d0b4187f"}, - {file = "GitPython-3.1.32.tar.gz", hash = "sha256:8d9b8cb1e80b9735e8717c9362079d3ce4c6e5ddeebedd0361b228c3a67a62f6"}, -] [package.dependencies] gitdb = ">=4.0.1,<5" typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} +[package.extras] +test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-sugar"] + [[package]] name = "griffe" version = "0.30.1" @@ -665,10 +282,6 @@ description = "Signatures for entire Python programs. Extract the structure, the category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "griffe-0.30.1-py3-none-any.whl", hash = "sha256:b2f3df6952995a6bebe19f797189d67aba7c860755d3d21cc80f64d076d0154c"}, - {file = "griffe-0.30.1.tar.gz", hash = "sha256:007cc11acd20becf1bb8f826419a52b9d403bbad9d8c8535699f5440ddc0a109"}, -] [package.dependencies] cached-property = {version = "*", markers = "python_version < \"3.8\""} @@ -681,10 +294,6 @@ description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] [package.dependencies] typing-extensions = {version = "*", markers = "python_version < \"3.8\""} @@ -696,10 +305,6 @@ description = "A minimal low-level HTTP client." category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, - {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, -] [package.dependencies] anyio = ">=3.0,<5.0" @@ -718,10 +323,6 @@ description = "The next generation HTTP client." category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, - {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, -] [package.dependencies] certifi = "*" @@ -742,10 +343,6 @@ description = "File identification library for Python" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, - {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, -] [package.extras] license = ["ukkonen"] @@ -757,10 +354,6 @@ description = "Internationalized Domain Names in Applications (IDNA)" category = "dev" optional = false python-versions = ">=3.5" -files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] [[package]] name = "importlib-metadata" @@ -769,10 +362,6 @@ description = "Read metadata from Python packages" category = "main" optional = false python-versions = ">=3.7" -files = [ - {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, - {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, -] [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} @@ -790,10 +379,6 @@ description = "brain-dead simple config-ini parsing" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] [[package]] name = "jinja2" @@ -802,10 +387,6 @@ description = "A very fast and expressive template engine." category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] [package.dependencies] MarkupSafe = ">=2.0" @@ -820,10 +401,6 @@ description = "Links recognition library with FULL unicode support." category = "main" optional = false python-versions = ">=3.7" -files = [ - {file = "linkify-it-py-2.0.2.tar.gz", hash = "sha256:19f3060727842c254c808e99d465c80c49d2c7306788140987a1a7a29b0d6ad2"}, - {file = "linkify_it_py-2.0.2-py3-none-any.whl", hash = "sha256:a3a24428f6c96f27370d7fe61d2ac0be09017be5190d68d8658233171f1b6541"}, -] [package.dependencies] uc-micro-py = "*" @@ -841,10 +418,6 @@ description = "Python implementation of John Gruber's Markdown." category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"}, - {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"}, -] [package.dependencies] importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} @@ -860,10 +433,6 @@ description = "Python port of markdown-it. Markdown parsing, done right!" category = "main" optional = false python-versions = ">=3.7" -files = [ - {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, - {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, -] [package.dependencies] linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""} @@ -888,58 +457,6 @@ description = "Safely add untrusted strings to HTML/XML markup." category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, -] [[package]] name = "mdit-py-plugins" @@ -948,10 +465,6 @@ description = "Collection of plugins for markdown-it-py" category = "main" optional = false python-versions = ">=3.7" -files = [ - {file = "mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a"}, - {file = "mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e"}, -] [package.dependencies] markdown-it-py = ">=1.0.0,<3.0.0" @@ -968,10 +481,6 @@ description = "Markdown URL utilities" category = "main" optional = false python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] [[package]] name = "mergedeep" @@ -980,22 +489,14 @@ description = "A deep merge function for 🐍." category = "dev" optional = false python-versions = ">=3.6" -files = [ - {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, - {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, -] [[package]] name = "mkdocs" -version = "1.5.2" +version = "1.5.3" description = "Project documentation with Markdown." category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "mkdocs-1.5.2-py3-none-any.whl", hash = "sha256:60a62538519c2e96fe8426654a67ee177350451616118a41596ae7c876bb7eac"}, - {file = "mkdocs-1.5.2.tar.gz", hash = "sha256:70d0da09c26cff288852471be03c23f0f521fc15cf16ac89c7a3bfb9ae8d24f9"}, -] [package.dependencies] click = ">=7.0" @@ -1025,10 +526,6 @@ description = "Automatically link across pages in MkDocs." category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"}, - {file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"}, -] [package.dependencies] Markdown = ">=3.3" @@ -1041,47 +538,38 @@ description = "A mkdocs plugin that lets you exclude files or trees." category = "dev" optional = false python-versions = "*" -files = [ - {file = "mkdocs-exclude-1.0.2.tar.gz", hash = "sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51"}, -] [package.dependencies] mkdocs = "*" [[package]] name = "mkdocs-material" -version = "9.1.21" +version = "9.2.7" description = "Documentation that simply works" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "mkdocs_material-9.1.21-py3-none-any.whl", hash = "sha256:58bb2f11ef240632e176d6f0f7d1cff06be1d11c696a5a1b553b808b4280ed47"}, - {file = "mkdocs_material-9.1.21.tar.gz", hash = "sha256:71940cdfca84ab296b6362889c25395b1621273fb16c93deda257adb7ff44ec8"}, -] [package.dependencies] -colorama = ">=0.4" -jinja2 = ">=3.0" -markdown = ">=3.2" -mkdocs = ">=1.5.0" -mkdocs-material-extensions = ">=1.1" -pygments = ">=2.14" -pymdown-extensions = ">=9.9.1" -regex = ">=2022.4.24" -requests = ">=2.26" +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.5,<2.0" +mkdocs-material-extensions = ">=1.1,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4,<2023.0" +requests = ">=2.26,<3.0" [[package]] name = "mkdocs-material-extensions" -version = "1.1.1" +version = "1.2" description = "Extension pack for Python Markdown and MkDocs Material." category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"}, - {file = "mkdocs_material_extensions-1.1.1.tar.gz", hash = "sha256:9c003da71e2cc2493d910237448c672e00cefc800d3d6ae93d2fc69979e3bd93"}, -] [[package]] name = "mkdocs-rss-plugin" @@ -1090,10 +578,6 @@ description = "MkDocs plugin which generates a static RSS feed using git log and category = "dev" optional = false python-versions = ">=3.7, <4" -files = [ - {file = "mkdocs-rss-plugin-1.5.0.tar.gz", hash = "sha256:4178b3830dcbad9b53b12459e315b1aad6b37d1e7e5c56c686866a10f99878a4"}, - {file = "mkdocs_rss_plugin-1.5.0-py2.py3-none-any.whl", hash = "sha256:2ab14c20bf6b7983acbe50181e7e4a0778731d9c2d5c38107ca7047a7abd2165"}, -] [package.dependencies] GitPython = ">=3.1,<3.2" @@ -1112,10 +596,6 @@ description = "Automatic documentation from sources, for MkDocs." category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "mkdocstrings-0.20.0-py3-none-any.whl", hash = "sha256:f17fc2c4f760ec302b069075ef9e31045aa6372ca91d2f35ded3adba8e25a472"}, - {file = "mkdocstrings-0.20.0.tar.gz", hash = "sha256:c757f4f646d4f939491d6bc9256bfe33e36c5f8026392f49eaa351d241c838e5"}, -] [package.dependencies] Jinja2 = ">=2.11.1" @@ -1138,10 +618,6 @@ description = "A Python handler for mkdocstrings." category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "mkdocstrings_python-0.10.1-py3-none-any.whl", hash = "sha256:ef239cee2c688e2b949a0a47e42a141d744dd12b7007311b3309dc70e3bafc5c"}, - {file = "mkdocstrings_python-0.10.1.tar.gz", hash = "sha256:b72301fff739070ec517b5b36bf2f7c49d1360a275896a64efb97fc17d3f3968"}, -] [package.dependencies] griffe = ">=0.24" @@ -1154,71 +630,6 @@ description = "MessagePack serializer" category = "dev" optional = false python-versions = "*" -files = [ - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a"}, - {file = "msgpack-1.0.5-cp310-cp310-win32.whl", hash = "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea"}, - {file = "msgpack-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f5ae84c5c8a857ec44dc180a8b0cc08238e021f57abdf51a8182e915e6299f0"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e6ca5d5699bcd89ae605c150aee83b5321f2115695e741b99618f4856c50898"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5494ea30d517a3576749cad32fa27f7585c65f5f38309c88c6d137877fa28a5a"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab2f3331cb1b54165976a9d976cb251a83183631c88076613c6c780f0d6e45a"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28592e20bbb1620848256ebc105fc420436af59515793ed27d5c77a217477705"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe5c63197c55bce6385d9aee16c4d0641684628f63ace85f73571e65ad1c1e8d"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed40e926fa2f297e8a653c954b732f125ef97bdd4c889f243182299de27e2aa9"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b2de4c1c0538dcb7010902a2b97f4e00fc4ddf2c8cda9749af0e594d3b7fa3d7"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf22a83f973b50f9d38e55c6aade04c41ddda19b00c4ebc558930d78eecc64ed"}, - {file = "msgpack-1.0.5-cp311-cp311-win32.whl", hash = "sha256:c396e2cc213d12ce017b686e0f53497f94f8ba2b24799c25d913d46c08ec422c"}, - {file = "msgpack-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c4c68d87497f66f96d50142a2b73b97972130d93677ce930718f68828b382e2"}, - {file = "msgpack-1.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a2b031c2e9b9af485d5e3c4520f4220d74f4d222a5b8dc8c1a3ab9448ca79c57"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f837b93669ce4336e24d08286c38761132bc7ab29782727f8557e1eb21b2080"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1d46dfe3832660f53b13b925d4e0fa1432b00f5f7210eb3ad3bb9a13c6204a6"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:366c9a7b9057e1547f4ad51d8facad8b406bab69c7d72c0eb6f529cf76d4b85f"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4c075728a1095efd0634a7dccb06204919a2f67d1893b6aa8e00497258bf926c"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:f933bbda5a3ee63b8834179096923b094b76f0c7a73c1cfe8f07ad608c58844b"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:36961b0568c36027c76e2ae3ca1132e35123dcec0706c4b7992683cc26c1320c"}, - {file = "msgpack-1.0.5-cp36-cp36m-win32.whl", hash = "sha256:b5ef2f015b95f912c2fcab19c36814963b5463f1fb9049846994b007962743e9"}, - {file = "msgpack-1.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:288e32b47e67f7b171f86b030e527e302c91bd3f40fd9033483f2cacc37f327a"}, - {file = "msgpack-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:137850656634abddfb88236008339fdaba3178f4751b28f270d2ebe77a563b6c"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c05a4a96585525916b109bb85f8cb6511db1c6f5b9d9cbcbc940dc6b4be944b"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a62ec00b636583e5cb6ad313bbed36bb7ead5fa3a3e38938503142c72cba4f"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef8108f8dedf204bb7b42994abf93882da1159728a2d4c5e82012edd92c9da9f"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1835c84d65f46900920b3708f5ba829fb19b1096c1800ad60bae8418652a951d"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e57916ef1bd0fee4f21c4600e9d1da352d8816b52a599c46460e93a6e9f17086"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17358523b85973e5f242ad74aa4712b7ee560715562554aa2134d96e7aa4cbbf"}, - {file = "msgpack-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:cb5aaa8c17760909ec6cb15e744c3ebc2ca8918e727216e79607b7bbce9c8f77"}, - {file = "msgpack-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:ab31e908d8424d55601ad7075e471b7d0140d4d3dd3272daf39c5c19d936bd82"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0"}, - {file = "msgpack-1.0.5-cp38-cp38-win32.whl", hash = "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e"}, - {file = "msgpack-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11"}, - {file = "msgpack-1.0.5-cp39-cp39-win32.whl", hash = "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc"}, - {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"}, - {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"}, -] [[package]] name = "multidict" @@ -1227,82 +638,6 @@ description = "multidict implementation" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, - {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, - {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, - {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, - {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, - {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, - {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, - {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, - {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, - {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, - {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, - {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, - {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, -] [[package]] name = "mypy" @@ -1311,34 +646,6 @@ description = "Optional static typing for Python" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, - {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, - {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, - {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, - {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, - {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, - {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, - {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, - {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, - {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, - {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, - {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, - {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, - {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, - {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, - {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, - {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, - {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, - {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, - {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, - {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, - {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, - {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, - {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, - {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, - {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, -] [package.dependencies] mypy-extensions = ">=1.0.0" @@ -1359,10 +666,6 @@ description = "Type system extensions for programs checked with the mypy type ch category = "dev" optional = false python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] [[package]] name = "nodeenv" @@ -1371,49 +674,41 @@ description = "Node.js virtual environment builder" category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" -files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, -] [package.dependencies] setuptools = "*" [[package]] name = "packaging" -version = "23.1" +version = "23.2" description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, -] [[package]] -name = "pathspec" -version = "0.11.2" -description = "Utility library for gitignore style pattern matching of file paths." +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, -] [[package]] name = "platformdirs" -version = "3.10.0" +version = "3.11.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, - {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, -] [package.dependencies] typing-extensions = {version = ">=4.7.1", markers = "python_version < \"3.8\""} @@ -1429,10 +724,6 @@ description = "plugin and hook calling mechanisms for python" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, - {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, -] [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} @@ -1448,10 +739,6 @@ description = "A framework for managing and maintaining multi-language pre-commi category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, - {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, -] [package.dependencies] cfgv = ">=2.0.0" @@ -1463,46 +750,37 @@ virtualenv = ">=20.10.0" [[package]] name = "pygments" -version = "2.15.1" +version = "2.16.1" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false python-versions = ">=3.7" -files = [ - {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, - {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, -] [package.extras] plugins = ["importlib-metadata"] [[package]] name = "pymdown-extensions" -version = "10.1" +version = "10.2.1" description = "Extension pack for Python Markdown." category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "pymdown_extensions-10.1-py3-none-any.whl", hash = "sha256:ef25dbbae530e8f67575d222b75ff0649b1e841e22c2ae9a20bad9472c2207dc"}, - {file = "pymdown_extensions-10.1.tar.gz", hash = "sha256:508009b211373058debb8247e168de4cbcb91b1bff7b5e961b2c3e864e00b195"}, -] [package.dependencies] markdown = ">=3.2" pyyaml = "*" +[package.extras] +extra = ["pygments (>=2.12)"] + [[package]] name = "pytest" -version = "7.4.0" +version = "7.4.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, - {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, -] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} @@ -1518,15 +796,11 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "pytest-aiohttp" -version = "1.0.4" +version = "1.0.5" description = "Pytest plugin for aiohttp support" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "pytest-aiohttp-1.0.4.tar.gz", hash = "sha256:39ff3a0d15484c01d1436cbedad575c6eafbf0f57cdf76fb94994c97b5b8c5a4"}, - {file = "pytest_aiohttp-1.0.4-py3-none-any.whl", hash = "sha256:1d2dc3a304c2be1fd496c0c2fb6b31ab60cd9fc33984f761f951f8ea1eb4ca95"}, -] [package.dependencies] aiohttp = ">=3.8.1" @@ -1543,10 +817,6 @@ description = "Pytest support for asyncio" category = "dev" optional = false python-versions = ">=3.7" -files = [ - {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, - {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, -] [package.dependencies] pytest = ">=7.0.0" @@ -1563,10 +833,6 @@ description = "Pytest plugin for measuring coverage." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, - {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, -] [package.dependencies] coverage = ">=5.2.1" @@ -1578,15 +844,11 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-textual-snapshot" -version = "0.2.0" +version = "0.4.0" description = "Snapshot testing for Textual apps" category = "dev" optional = false python-versions = ">=3.6,<4.0" -files = [ - {file = "pytest_textual_snapshot-0.2.0-py3-none-any.whl", hash = "sha256:663fe07bf62181ec0c63139daaeaf50eb8088164037eb30d721f028adc9edc8c"}, - {file = "pytest_textual_snapshot-0.2.0.tar.gz", hash = "sha256:5e9f8c4b1b011bdae67d4f1129530afd6611f3f8bcf03cf06699402179bc12cf"}, -] [package.dependencies] jinja2 = ">=3.0.0" @@ -1602,10 +864,6 @@ description = "Extensions to the standard Python datetime module" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] [package.dependencies] six = ">=1.5" @@ -1617,10 +875,6 @@ description = "World timezone definitions, modern and historical" category = "dev" optional = false python-versions = "*" -files = [ - {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, - {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, -] [[package]] name = "pyyaml" @@ -1629,48 +883,6 @@ description = "YAML parser and emitter for Python" category = "dev" optional = false python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {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-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"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {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-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {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"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {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-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"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {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-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"}, -] [[package]] name = "pyyaml-env-tag" @@ -1679,268 +891,1284 @@ description = "A custom YAML tag for referencing environment variables in YAML f category = "dev" optional = false python-versions = ">=3.6" -files = [ - {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, - {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, -] [package.dependencies] pyyaml = "*" [[package]] name = "regex" -version = "2023.6.3" +version = "2022.10.31" description = "Alternative regular expression module, to replace re." category = "dev" optional = false python-versions = ">=3.6" -files = [ - {file = "regex-2023.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:824bf3ac11001849aec3fa1d69abcb67aac3e150a933963fb12bda5151fe1bfd"}, - {file = "regex-2023.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05ed27acdf4465c95826962528f9e8d41dbf9b1aa8531a387dee6ed215a3e9ef"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b49c764f88a79160fa64f9a7b425620e87c9f46095ef9c9920542ab2495c8bc"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8e3f1316c2293e5469f8f09dc2d76efb6c3982d3da91ba95061a7e69489a14ef"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43e1dd9d12df9004246bacb79a0e5886b3b6071b32e41f83b0acbf293f820ee8"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4959e8bcbfda5146477d21c3a8ad81b185cd252f3d0d6e4724a5ef11c012fb06"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af4dd387354dc83a3bff67127a124c21116feb0d2ef536805c454721c5d7993d"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2239d95d8e243658b8dbb36b12bd10c33ad6e6933a54d36ff053713f129aa536"}, - {file = "regex-2023.6.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:890e5a11c97cf0d0c550eb661b937a1e45431ffa79803b942a057c4fb12a2da2"}, - {file = "regex-2023.6.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a8105e9af3b029f243ab11ad47c19b566482c150c754e4c717900a798806b222"}, - {file = "regex-2023.6.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:25be746a8ec7bc7b082783216de8e9473803706723b3f6bef34b3d0ed03d57e2"}, - {file = "regex-2023.6.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3676f1dd082be28b1266c93f618ee07741b704ab7b68501a173ce7d8d0d0ca18"}, - {file = "regex-2023.6.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:10cb847aeb1728412c666ab2e2000ba6f174f25b2bdc7292e7dd71b16db07568"}, - {file = "regex-2023.6.3-cp310-cp310-win32.whl", hash = "sha256:dbbbfce33cd98f97f6bffb17801b0576e653f4fdb1d399b2ea89638bc8d08ae1"}, - {file = "regex-2023.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:c5f8037000eb21e4823aa485149f2299eb589f8d1fe4b448036d230c3f4e68e0"}, - {file = "regex-2023.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c123f662be8ec5ab4ea72ea300359023a5d1df095b7ead76fedcd8babbedf969"}, - {file = "regex-2023.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9edcbad1f8a407e450fbac88d89e04e0b99a08473f666a3f3de0fd292badb6aa"}, - {file = "regex-2023.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcba6dae7de533c876255317c11f3abe4907ba7d9aa15d13e3d9710d4315ec0e"}, - {file = "regex-2023.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29cdd471ebf9e0f2fb3cac165efedc3c58db841d83a518b082077e612d3ee5df"}, - {file = "regex-2023.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12b74fbbf6cbbf9dbce20eb9b5879469e97aeeaa874145517563cca4029db65c"}, - {file = "regex-2023.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c29ca1bd61b16b67be247be87390ef1d1ef702800f91fbd1991f5c4421ebae8"}, - {file = "regex-2023.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77f09bc4b55d4bf7cc5eba785d87001d6757b7c9eec237fe2af57aba1a071d9"}, - {file = "regex-2023.6.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ea353ecb6ab5f7e7d2f4372b1e779796ebd7b37352d290096978fea83c4dba0c"}, - {file = "regex-2023.6.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:10590510780b7541969287512d1b43f19f965c2ece6c9b1c00fc367b29d8dce7"}, - {file = "regex-2023.6.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e2fbd6236aae3b7f9d514312cdb58e6494ee1c76a9948adde6eba33eb1c4264f"}, - {file = "regex-2023.6.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:6b2675068c8b56f6bfd5a2bda55b8accbb96c02fd563704732fd1c95e2083461"}, - {file = "regex-2023.6.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74419d2b50ecb98360cfaa2974da8689cb3b45b9deff0dcf489c0d333bcc1477"}, - {file = "regex-2023.6.3-cp311-cp311-win32.whl", hash = "sha256:fb5ec16523dc573a4b277663a2b5a364e2099902d3944c9419a40ebd56a118f9"}, - {file = "regex-2023.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:09e4a1a6acc39294a36b7338819b10baceb227f7f7dbbea0506d419b5a1dd8af"}, - {file = "regex-2023.6.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0654bca0cdf28a5956c83839162692725159f4cda8d63e0911a2c0dc76166525"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:463b6a3ceb5ca952e66550a4532cef94c9a0c80dc156c4cc343041951aec1697"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87b2a5bb5e78ee0ad1de71c664d6eb536dc3947a46a69182a90f4410f5e3f7dd"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6343c6928282c1f6a9db41f5fd551662310e8774c0e5ebccb767002fcf663ca9"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6192d5af2ccd2a38877bfef086d35e6659566a335b1492786ff254c168b1693"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74390d18c75054947e4194019077e243c06fbb62e541d8817a0fa822ea310c14"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:742e19a90d9bb2f4a6cf2862b8b06dea5e09b96c9f2df1779e53432d7275331f"}, - {file = "regex-2023.6.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8abbc5d54ea0ee80e37fef009e3cec5dafd722ed3c829126253d3e22f3846f1e"}, - {file = "regex-2023.6.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c2b867c17a7a7ae44c43ebbeb1b5ff406b3e8d5b3e14662683e5e66e6cc868d3"}, - {file = "regex-2023.6.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d831c2f8ff278179705ca59f7e8524069c1a989e716a1874d6d1aab6119d91d1"}, - {file = "regex-2023.6.3-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ee2d1a9a253b1729bb2de27d41f696ae893507c7db224436abe83ee25356f5c1"}, - {file = "regex-2023.6.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:61474f0b41fe1a80e8dfa70f70ea1e047387b7cd01c85ec88fa44f5d7561d787"}, - {file = "regex-2023.6.3-cp36-cp36m-win32.whl", hash = "sha256:0b71e63226e393b534105fcbdd8740410dc6b0854c2bfa39bbda6b0d40e59a54"}, - {file = "regex-2023.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bbb02fd4462f37060122e5acacec78e49c0fbb303c30dd49c7f493cf21fc5b27"}, - {file = "regex-2023.6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b862c2b9d5ae38a68b92e215b93f98d4c5e9454fa36aae4450f61dd33ff48487"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976d7a304b59ede34ca2921305b57356694f9e6879db323fd90a80f865d355a3"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:83320a09188e0e6c39088355d423aa9d056ad57a0b6c6381b300ec1a04ec3d16"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9427a399501818a7564f8c90eced1e9e20709ece36be701f394ada99890ea4b3"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178bbc1b2ec40eaca599d13c092079bf529679bf0371c602edaa555e10b41c3"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:837328d14cde912af625d5f303ec29f7e28cdab588674897baafaf505341f2fc"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2d44dc13229905ae96dd2ae2dd7cebf824ee92bc52e8cf03dcead37d926da019"}, - {file = "regex-2023.6.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d54af539295392611e7efbe94e827311eb8b29668e2b3f4cadcfe6f46df9c777"}, - {file = "regex-2023.6.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7117d10690c38a622e54c432dfbbd3cbd92f09401d622902c32f6d377e2300ee"}, - {file = "regex-2023.6.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bb60b503ec8a6e4e3e03a681072fa3a5adcbfa5479fa2d898ae2b4a8e24c4591"}, - {file = "regex-2023.6.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:65ba8603753cec91c71de423a943ba506363b0e5c3fdb913ef8f9caa14b2c7e0"}, - {file = "regex-2023.6.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:271f0bdba3c70b58e6f500b205d10a36fb4b58bd06ac61381b68de66442efddb"}, - {file = "regex-2023.6.3-cp37-cp37m-win32.whl", hash = "sha256:9beb322958aaca059f34975b0df135181f2e5d7a13b84d3e0e45434749cb20f7"}, - {file = "regex-2023.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fea75c3710d4f31389eed3c02f62d0b66a9da282521075061ce875eb5300cf23"}, - {file = "regex-2023.6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8f56fcb7ff7bf7404becdfc60b1e81a6d0561807051fd2f1860b0d0348156a07"}, - {file = "regex-2023.6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d2da3abc88711bce7557412310dfa50327d5769a31d1c894b58eb256459dc289"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a99b50300df5add73d307cf66abea093304a07eb017bce94f01e795090dea87c"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5708089ed5b40a7b2dc561e0c8baa9535b77771b64a8330b684823cfd5116036"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:687ea9d78a4b1cf82f8479cab23678aff723108df3edeac098e5b2498879f4a7"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d3850beab9f527f06ccc94b446c864059c57651b3f911fddb8d9d3ec1d1b25d"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8915cc96abeb8983cea1df3c939e3c6e1ac778340c17732eb63bb96247b91d2"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:841d6e0e5663d4c7b4c8099c9997be748677d46cbf43f9f471150e560791f7ff"}, - {file = "regex-2023.6.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9edce5281f965cf135e19840f4d93d55b3835122aa76ccacfd389e880ba4cf82"}, - {file = "regex-2023.6.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b956231ebdc45f5b7a2e1f90f66a12be9610ce775fe1b1d50414aac1e9206c06"}, - {file = "regex-2023.6.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:36efeba71c6539d23c4643be88295ce8c82c88bbd7c65e8a24081d2ca123da3f"}, - {file = "regex-2023.6.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:cf67ca618b4fd34aee78740bea954d7c69fdda419eb208c2c0c7060bb822d747"}, - {file = "regex-2023.6.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b4598b1897837067a57b08147a68ac026c1e73b31ef6e36deeeb1fa60b2933c9"}, - {file = "regex-2023.6.3-cp38-cp38-win32.whl", hash = "sha256:f415f802fbcafed5dcc694c13b1292f07fe0befdb94aa8a52905bd115ff41e88"}, - {file = "regex-2023.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:d4f03bb71d482f979bda92e1427f3ec9b220e62a7dd337af0aa6b47bf4498f72"}, - {file = "regex-2023.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ccf91346b7bd20c790310c4147eee6ed495a54ddb6737162a36ce9dbef3e4751"}, - {file = "regex-2023.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b28f5024a3a041009eb4c333863d7894d191215b39576535c6734cd88b0fcb68"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0bb18053dfcfed432cc3ac632b5e5e5c5b7e55fb3f8090e867bfd9b054dbcbf"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5bfb3004f2144a084a16ce19ca56b8ac46e6fd0651f54269fc9e230edb5e4a"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c6b48d0fa50d8f4df3daf451be7f9689c2bde1a52b1225c5926e3f54b6a9ed1"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:051da80e6eeb6e239e394ae60704d2b566aa6a7aed6f2890a7967307267a5dc6"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4c3b7fa4cdaa69268748665a1a6ff70c014d39bb69c50fda64b396c9116cf77"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:457b6cce21bee41ac292d6753d5e94dcbc5c9e3e3a834da285b0bde7aa4a11e9"}, - {file = "regex-2023.6.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aad51907d74fc183033ad796dd4c2e080d1adcc4fd3c0fd4fd499f30c03011cd"}, - {file = "regex-2023.6.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0385e73da22363778ef2324950e08b689abdf0b108a7d8decb403ad7f5191938"}, - {file = "regex-2023.6.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c6a57b742133830eec44d9b2290daf5cbe0a2f1d6acee1b3c7b1c7b2f3606df7"}, - {file = "regex-2023.6.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3e5219bf9e75993d73ab3d25985c857c77e614525fac9ae02b1bebd92f7cecac"}, - {file = "regex-2023.6.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e5087a3c59eef624a4591ef9eaa6e9a8d8a94c779dade95d27c0bc24650261cd"}, - {file = "regex-2023.6.3-cp39-cp39-win32.whl", hash = "sha256:20326216cc2afe69b6e98528160b225d72f85ab080cbdf0b11528cbbaba2248f"}, - {file = "regex-2023.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:bdff5eab10e59cf26bc479f565e25ed71a7d041d1ded04ccf9aee1d9f208487a"}, - {file = "regex-2023.6.3.tar.gz", hash = "sha256:72d1a25bf36d2050ceb35b517afe13864865268dfb45910e2e17a84be6cbfeb0"}, + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "rich" +version = "13.6.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.7.0" + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "setuptools" +version = "68.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "smmap" +version = "5.0.1" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "syrupy" +version = "3.0.6" +description = "Pytest Snapshot Test Utility" +category = "dev" +optional = false +python-versions = ">=3.7,<4" + +[package.dependencies] +colored = ">=1.3.92,<2.0.0" +pytest = ">=5.1.0,<8.0.0" + +[[package]] +name = "textual-dev" +version = "1.2.1" +description = "Development tools for working with Textual" +category = "dev" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +aiohttp = ">=3.8.1" +click = ">=8.1.2" +msgpack = ">=1.0.3" +textual = ">=0.33.0" +typing-extensions = ">=4.4.0,<5.0.0" + +[[package]] +name = "time-machine" +version = "2.10.0" +description = "Travel through time in your tests." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +python-dateutil = "*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tree-sitter" +version = "0.20.2" +description = "Python bindings for the Tree-Sitter parsing library" +category = "main" +optional = true +python-versions = ">=3.3" + +[[package]] +name = "tree-sitter-languages" +version = "1.7.0" +description = "Binary Python wheels for all tree sitter languages." +category = "main" +optional = true +python-versions = "*" + +[package.dependencies] +tree-sitter = "*" + +[[package]] +name = "typed-ast" +version = "1.5.5" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "types-setuptools" +version = "67.8.0.0" +description = "Typing stubs for setuptools" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "types-tree-sitter" +version = "0.20.1.5" +description = "Typing stubs for tree-sitter" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "types-tree-sitter-languages" +version = "1.7.0.1" +description = "Typing stubs for tree-sitter-languages" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +types-tree-sitter = "*" + +[[package]] +name = "typing-extensions" +version = "4.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tzdata" +version = "2022.7" +description = "Provider of IANA time zone data" +category = "dev" +optional = false +python-versions = ">=2" + +[[package]] +name = "uc-micro-py" +version = "1.0.2" +description = "Micro subset of unicode data files for linkify-it-py projects." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["coverage", "pytest", "pytest-cov"] + +[[package]] +name = "urllib3" +version = "2.0.6" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.24.5" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""} +platformdirs = ">=3.9.1,<4" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "watchdog" +version = "3.0.0" +description = "Filesystem events monitoring" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "yarl" +version = "1.9.2" +description = "Yet another URL library" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} + +[[package]] +name = "zipp" +version = "3.15.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[extras] +syntax = ["tree-sitter", "tree_sitter_languages"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "fdb89c91b37a0d781f9cbfb7bd8cffd737e369e21761d67113c1a91882f28158" + +[metadata.files] +aiohttp = [ + {file = "aiohttp-3.8.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41d55fc043954cddbbd82503d9cc3f4814a40bcef30b3569bc7b5e34130718c1"}, + {file = "aiohttp-3.8.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1d84166673694841d8953f0a8d0c90e1087739d24632fe86b1a08819168b4566"}, + {file = "aiohttp-3.8.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:253bf92b744b3170eb4c4ca2fa58f9c4b87aeb1df42f71d4e78815e6e8b73c9e"}, + {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fd194939b1f764d6bb05490987bfe104287bbf51b8d862261ccf66f48fb4096"}, + {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c5f938d199a6fdbdc10bbb9447496561c3a9a565b43be564648d81e1102ac22"}, + {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2817b2f66ca82ee699acd90e05c95e79bbf1dc986abb62b61ec8aaf851e81c93"}, + {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fa375b3d34e71ccccf172cab401cd94a72de7a8cc01847a7b3386204093bb47"}, + {file = "aiohttp-3.8.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9de50a199b7710fa2904be5a4a9b51af587ab24c8e540a7243ab737b45844543"}, + {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e1d8cb0b56b3587c5c01de3bf2f600f186da7e7b5f7353d1bf26a8ddca57f965"}, + {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8e31e9db1bee8b4f407b77fd2507337a0a80665ad7b6c749d08df595d88f1cf5"}, + {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7bc88fc494b1f0311d67f29fee6fd636606f4697e8cc793a2d912ac5b19aa38d"}, + {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ec00c3305788e04bf6d29d42e504560e159ccaf0be30c09203b468a6c1ccd3b2"}, + {file = "aiohttp-3.8.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ad1407db8f2f49329729564f71685557157bfa42b48f4b93e53721a16eb813ed"}, + {file = "aiohttp-3.8.6-cp310-cp310-win32.whl", hash = "sha256:ccc360e87341ad47c777f5723f68adbb52b37ab450c8bc3ca9ca1f3e849e5fe2"}, + {file = "aiohttp-3.8.6-cp310-cp310-win_amd64.whl", hash = "sha256:93c15c8e48e5e7b89d5cb4613479d144fda8344e2d886cf694fd36db4cc86865"}, + {file = "aiohttp-3.8.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e2f9cc8e5328f829f6e1fb74a0a3a939b14e67e80832975e01929e320386b34"}, + {file = "aiohttp-3.8.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e6a00ffcc173e765e200ceefb06399ba09c06db97f401f920513a10c803604ca"}, + {file = "aiohttp-3.8.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:41bdc2ba359032e36c0e9de5a3bd00d6fb7ea558a6ce6b70acedf0da86458321"}, + {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14cd52ccf40006c7a6cd34a0f8663734e5363fd981807173faf3a017e202fec9"}, + {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d5b785c792802e7b275c420d84f3397668e9d49ab1cb52bd916b3b3ffcf09ad"}, + {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1bed815f3dc3d915c5c1e556c397c8667826fbc1b935d95b0ad680787896a358"}, + {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96603a562b546632441926cd1293cfcb5b69f0b4159e6077f7c7dbdfb686af4d"}, + {file = "aiohttp-3.8.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d76e8b13161a202d14c9584590c4df4d068c9567c99506497bdd67eaedf36403"}, + {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e3f1e3f1a1751bb62b4a1b7f4e435afcdade6c17a4fd9b9d43607cebd242924a"}, + {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:76b36b3124f0223903609944a3c8bf28a599b2cc0ce0be60b45211c8e9be97f8"}, + {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a2ece4af1f3c967a4390c284797ab595a9f1bc1130ef8b01828915a05a6ae684"}, + {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:16d330b3b9db87c3883e565340d292638a878236418b23cc8b9b11a054aaa887"}, + {file = "aiohttp-3.8.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:42c89579f82e49db436b69c938ab3e1559e5a4409eb8639eb4143989bc390f2f"}, + {file = "aiohttp-3.8.6-cp311-cp311-win32.whl", hash = "sha256:efd2fcf7e7b9d7ab16e6b7d54205beded0a9c8566cb30f09c1abe42b4e22bdcb"}, + {file = "aiohttp-3.8.6-cp311-cp311-win_amd64.whl", hash = "sha256:3b2ab182fc28e7a81f6c70bfbd829045d9480063f5ab06f6e601a3eddbbd49a0"}, + {file = "aiohttp-3.8.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fdee8405931b0615220e5ddf8cd7edd8592c606a8e4ca2a00704883c396e4479"}, + {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d25036d161c4fe2225d1abff2bd52c34ed0b1099f02c208cd34d8c05729882f0"}, + {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d791245a894be071d5ab04bbb4850534261a7d4fd363b094a7b9963e8cdbd31"}, + {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0cccd1de239afa866e4ce5c789b3032442f19c261c7d8a01183fd956b1935349"}, + {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f13f60d78224f0dace220d8ab4ef1dbc37115eeeab8c06804fec11bec2bbd07"}, + {file = "aiohttp-3.8.6-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a9b5a0606faca4f6cc0d338359d6fa137104c337f489cd135bb7fbdbccb1e39"}, + {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:13da35c9ceb847732bf5c6c5781dcf4780e14392e5d3b3c689f6d22f8e15ae31"}, + {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:4d4cbe4ffa9d05f46a28252efc5941e0462792930caa370a6efaf491f412bc66"}, + {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:229852e147f44da0241954fc6cb910ba074e597f06789c867cb7fb0621e0ba7a"}, + {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:713103a8bdde61d13490adf47171a1039fd880113981e55401a0f7b42c37d071"}, + {file = "aiohttp-3.8.6-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:45ad816b2c8e3b60b510f30dbd37fe74fd4a772248a52bb021f6fd65dff809b6"}, + {file = "aiohttp-3.8.6-cp36-cp36m-win32.whl", hash = "sha256:2b8d4e166e600dcfbff51919c7a3789ff6ca8b3ecce16e1d9c96d95dd569eb4c"}, + {file = "aiohttp-3.8.6-cp36-cp36m-win_amd64.whl", hash = "sha256:0912ed87fee967940aacc5306d3aa8ba3a459fcd12add0b407081fbefc931e53"}, + {file = "aiohttp-3.8.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e2a988a0c673c2e12084f5e6ba3392d76c75ddb8ebc6c7e9ead68248101cd446"}, + {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebf3fd9f141700b510d4b190094db0ce37ac6361a6806c153c161dc6c041ccda"}, + {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3161ce82ab85acd267c8f4b14aa226047a6bee1e4e6adb74b798bd42c6ae1f80"}, + {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95fc1bf33a9a81469aa760617b5971331cdd74370d1214f0b3109272c0e1e3c"}, + {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c43ecfef7deaf0617cee936836518e7424ee12cb709883f2c9a1adda63cc460"}, + {file = "aiohttp-3.8.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca80e1b90a05a4f476547f904992ae81eda5c2c85c66ee4195bb8f9c5fb47f28"}, + {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:90c72ebb7cb3a08a7f40061079817133f502a160561d0675b0a6adf231382c92"}, + {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bb54c54510e47a8c7c8e63454a6acc817519337b2b78606c4e840871a3e15349"}, + {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:de6a1c9f6803b90e20869e6b99c2c18cef5cc691363954c93cb9adeb26d9f3ae"}, + {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:a3628b6c7b880b181a3ae0a0683698513874df63783fd89de99b7b7539e3e8a8"}, + {file = "aiohttp-3.8.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fc37e9aef10a696a5a4474802930079ccfc14d9f9c10b4662169671ff034b7df"}, + {file = "aiohttp-3.8.6-cp37-cp37m-win32.whl", hash = "sha256:f8ef51e459eb2ad8e7a66c1d6440c808485840ad55ecc3cafefadea47d1b1ba2"}, + {file = "aiohttp-3.8.6-cp37-cp37m-win_amd64.whl", hash = "sha256:b2fe42e523be344124c6c8ef32a011444e869dc5f883c591ed87f84339de5976"}, + {file = "aiohttp-3.8.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9e2ee0ac5a1f5c7dd3197de309adfb99ac4617ff02b0603fd1e65b07dc772e4b"}, + {file = "aiohttp-3.8.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01770d8c04bd8db568abb636c1fdd4f7140b284b8b3e0b4584f070180c1e5c62"}, + {file = "aiohttp-3.8.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3c68330a59506254b556b99a91857428cab98b2f84061260a67865f7f52899f5"}, + {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89341b2c19fb5eac30c341133ae2cc3544d40d9b1892749cdd25892bbc6ac951"}, + {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71783b0b6455ac8f34b5ec99d83e686892c50498d5d00b8e56d47f41b38fbe04"}, + {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f628dbf3c91e12f4d6c8b3f092069567d8eb17814aebba3d7d60c149391aee3a"}, + {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b04691bc6601ef47c88f0255043df6f570ada1a9ebef99c34bd0b72866c217ae"}, + {file = "aiohttp-3.8.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ee912f7e78287516df155f69da575a0ba33b02dd7c1d6614dbc9463f43066e3"}, + {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9c19b26acdd08dd239e0d3669a3dddafd600902e37881f13fbd8a53943079dbc"}, + {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:99c5ac4ad492b4a19fc132306cd57075c28446ec2ed970973bbf036bcda1bcc6"}, + {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f0f03211fd14a6a0aed2997d4b1c013d49fb7b50eeb9ffdf5e51f23cfe2c77fa"}, + {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:8d399dade330c53b4106160f75f55407e9ae7505263ea86f2ccca6bfcbdb4921"}, + {file = "aiohttp-3.8.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ec4fd86658c6a8964d75426517dc01cbf840bbf32d055ce64a9e63a40fd7b771"}, + {file = "aiohttp-3.8.6-cp38-cp38-win32.whl", hash = "sha256:33164093be11fcef3ce2571a0dccd9041c9a93fa3bde86569d7b03120d276c6f"}, + {file = "aiohttp-3.8.6-cp38-cp38-win_amd64.whl", hash = "sha256:bdf70bfe5a1414ba9afb9d49f0c912dc524cf60141102f3a11143ba3d291870f"}, + {file = "aiohttp-3.8.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d52d5dc7c6682b720280f9d9db41d36ebe4791622c842e258c9206232251ab2b"}, + {file = "aiohttp-3.8.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ac39027011414dbd3d87f7edb31680e1f430834c8cef029f11c66dad0670aa5"}, + {file = "aiohttp-3.8.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3f5c7ce535a1d2429a634310e308fb7d718905487257060e5d4598e29dc17f0b"}, + {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b30e963f9e0d52c28f284d554a9469af073030030cef8693106d918b2ca92f54"}, + {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:918810ef188f84152af6b938254911055a72e0f935b5fbc4c1a4ed0b0584aed1"}, + {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:002f23e6ea8d3dd8d149e569fd580c999232b5fbc601c48d55398fbc2e582e8c"}, + {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fcf3eabd3fd1a5e6092d1242295fa37d0354b2eb2077e6eb670accad78e40e1"}, + {file = "aiohttp-3.8.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:255ba9d6d5ff1a382bb9a578cd563605aa69bec845680e21c44afc2670607a95"}, + {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d67f8baed00870aa390ea2590798766256f31dc5ed3ecc737debb6e97e2ede78"}, + {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:86f20cee0f0a317c76573b627b954c412ea766d6ada1a9fcf1b805763ae7feeb"}, + {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:39a312d0e991690ccc1a61f1e9e42daa519dcc34ad03eb6f826d94c1190190dd"}, + {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e827d48cf802de06d9c935088c2924e3c7e7533377d66b6f31ed175c1620e05e"}, + {file = "aiohttp-3.8.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bd111d7fc5591ddf377a408ed9067045259ff2770f37e2d94e6478d0f3fc0c17"}, + {file = "aiohttp-3.8.6-cp39-cp39-win32.whl", hash = "sha256:caf486ac1e689dda3502567eb89ffe02876546599bbf915ec94b1fa424eeffd4"}, + {file = "aiohttp-3.8.6-cp39-cp39-win_amd64.whl", hash = "sha256:3f0e27e5b733803333bb2371249f41cf42bae8884863e8e8965ec69bebe53132"}, + {file = "aiohttp-3.8.6.tar.gz", hash = "sha256:b0cf2a4501bff9330a8a5248b4ce951851e415bdcce9dc158e76cfd55e15085c"}, +] +aiosignal = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] +anyio = [ + {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, + {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, +] +async-timeout = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] +asynctest = [ + {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, + {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, +] +attrs = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] +babel = [ + {file = "Babel-2.13.0-py3-none-any.whl", hash = "sha256:fbfcae1575ff78e26c7449136f1abbefc3c13ce542eeb13d43d50d8b047216ec"}, + {file = "Babel-2.13.0.tar.gz", hash = "sha256:04c3e2d28d2b7681644508f836be388ae49e0cfe91465095340395b60d00f210"}, +] +black = [ + {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, + {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, + {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, + {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, + {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, + {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, + {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, + {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, + {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, + {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, + {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, + {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, + {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, + {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, +] +cached-property = [ + {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, + {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, +] +certifi = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] +cfgv = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] +charset-normalizer = [ + {file = "charset-normalizer-3.3.0.tar.gz", hash = "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-win32.whl", hash = "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d"}, + {file = "charset_normalizer-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-win32.whl", hash = "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786"}, + {file = "charset_normalizer-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-win32.whl", hash = "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df"}, + {file = "charset_normalizer-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-win32.whl", hash = "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c"}, + {file = "charset_normalizer-3.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-win32.whl", hash = "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e"}, + {file = "charset_normalizer-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-win32.whl", hash = "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a"}, + {file = "charset_normalizer-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884"}, + {file = "charset_normalizer-3.3.0-py3-none-any.whl", hash = "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2"}, +] +click = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] +colorama = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +colored = [ + {file = "colored-1.4.4.tar.gz", hash = "sha256:04ff4d4dd514274fe3b99a21bb52fb96f2688c01e93fba7bef37221e7cb56ce0"}, +] +coverage = [ + {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, + {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, + {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, + {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, + {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, + {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, + {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, + {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, + {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, + {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, + {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, + {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, + {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, + {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, + {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, + {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, + {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, + {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, + {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, + {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, + {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, + {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, + {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, + {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, + {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, + {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, + {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, + {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, + {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, + {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, + {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, + {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, + {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, +] +distlib = [ + {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, + {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, +] +exceptiongroup = [ + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, +] +filelock = [ + {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, + {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, +] +frozenlist = [ + {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4"}, + {file = "frozenlist-1.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0"}, + {file = "frozenlist-1.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd"}, + {file = "frozenlist-1.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f"}, + {file = "frozenlist-1.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420"}, + {file = "frozenlist-1.3.3-cp310-cp310-win32.whl", hash = "sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642"}, + {file = "frozenlist-1.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678"}, + {file = "frozenlist-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b"}, + {file = "frozenlist-1.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4"}, + {file = "frozenlist-1.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81"}, + {file = "frozenlist-1.3.3-cp311-cp311-win32.whl", hash = "sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8"}, + {file = "frozenlist-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32"}, + {file = "frozenlist-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d"}, + {file = "frozenlist-1.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb"}, + {file = "frozenlist-1.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401"}, + {file = "frozenlist-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a"}, + {file = "frozenlist-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5"}, + {file = "frozenlist-1.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2"}, + {file = "frozenlist-1.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13"}, + {file = "frozenlist-1.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3"}, + {file = "frozenlist-1.3.3-cp38-cp38-win32.whl", hash = "sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b"}, + {file = "frozenlist-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1"}, + {file = "frozenlist-1.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b"}, + {file = "frozenlist-1.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab"}, + {file = "frozenlist-1.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1"}, + {file = "frozenlist-1.3.3-cp39-cp39-win32.whl", hash = "sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38"}, + {file = "frozenlist-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9"}, + {file = "frozenlist-1.3.3.tar.gz", hash = "sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a"}, +] +ghp-import = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] +gitdb = [ + {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, + {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"}, +] +gitpython = [ + {file = "GitPython-3.1.37-py3-none-any.whl", hash = "sha256:5f4c4187de49616d710a77e98ddf17b4782060a1788df441846bddefbb89ab33"}, + {file = "GitPython-3.1.37.tar.gz", hash = "sha256:f9b9ddc0761c125d5780eab2d64be4873fc6817c2899cbcb34b02344bdc7bc54"}, +] +griffe = [ + {file = "griffe-0.30.1-py3-none-any.whl", hash = "sha256:b2f3df6952995a6bebe19f797189d67aba7c860755d3d21cc80f64d076d0154c"}, + {file = "griffe-0.30.1.tar.gz", hash = "sha256:007cc11acd20becf1bb8f826419a52b9d403bbad9d8c8535699f5440ddc0a109"}, +] +h11 = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] +httpcore = [ + {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, + {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, +] +httpx = [ + {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, + {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, +] +identify = [ + {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, + {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, +] +idna = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] +importlib-metadata = [ + {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, + {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, +] +iniconfig = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] +jinja2 = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] +linkify-it-py = [ + {file = "linkify-it-py-2.0.2.tar.gz", hash = "sha256:19f3060727842c254c808e99d465c80c49d2c7306788140987a1a7a29b0d6ad2"}, + {file = "linkify_it_py-2.0.2-py3-none-any.whl", hash = "sha256:a3a24428f6c96f27370d7fe61d2ac0be09017be5190d68d8658233171f1b6541"}, +] +markdown = [ + {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"}, + {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"}, +] +markdown-it-py = [ + {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, + {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, +] +markupsafe = [ + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, + {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, + {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, + {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, + {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, + {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, + {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, +] +mdit-py-plugins = [ + {file = "mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a"}, + {file = "mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e"}, +] +mdurl = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] +mergedeep = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] +mkdocs = [ + {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"}, + {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"}, +] +mkdocs-autorefs = [ + {file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"}, + {file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"}, +] +mkdocs-exclude = [ + {file = "mkdocs-exclude-1.0.2.tar.gz", hash = "sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51"}, +] +mkdocs-material = [ + {file = "mkdocs_material-9.2.7-py3-none-any.whl", hash = "sha256:92e4160d191cc76121fed14ab9f14638e43a6da0f2e9d7a9194d377f0a4e7f18"}, + {file = "mkdocs_material-9.2.7.tar.gz", hash = "sha256:b44da35b0d98cd762d09ef74f1ddce5b6d6e35c13f13beb0c9d82a629e5f229e"}, +] +mkdocs-material-extensions = [ + {file = "mkdocs_material_extensions-1.2-py3-none-any.whl", hash = "sha256:c767bd6d6305f6420a50f0b541b0c9966d52068839af97029be14443849fb8a1"}, + {file = "mkdocs_material_extensions-1.2.tar.gz", hash = "sha256:27e2d1ed2d031426a6e10d5ea06989d67e90bb02acd588bc5673106b5ee5eedf"}, +] +mkdocs-rss-plugin = [ + {file = "mkdocs-rss-plugin-1.5.0.tar.gz", hash = "sha256:4178b3830dcbad9b53b12459e315b1aad6b37d1e7e5c56c686866a10f99878a4"}, + {file = "mkdocs_rss_plugin-1.5.0-py2.py3-none-any.whl", hash = "sha256:2ab14c20bf6b7983acbe50181e7e4a0778731d9c2d5c38107ca7047a7abd2165"}, +] +mkdocstrings = [ + {file = "mkdocstrings-0.20.0-py3-none-any.whl", hash = "sha256:f17fc2c4f760ec302b069075ef9e31045aa6372ca91d2f35ded3adba8e25a472"}, + {file = "mkdocstrings-0.20.0.tar.gz", hash = "sha256:c757f4f646d4f939491d6bc9256bfe33e36c5f8026392f49eaa351d241c838e5"}, +] +mkdocstrings-python = [ + {file = "mkdocstrings_python-0.10.1-py3-none-any.whl", hash = "sha256:ef239cee2c688e2b949a0a47e42a141d744dd12b7007311b3309dc70e3bafc5c"}, + {file = "mkdocstrings_python-0.10.1.tar.gz", hash = "sha256:b72301fff739070ec517b5b36bf2f7c49d1360a275896a64efb97fc17d3f3968"}, +] +msgpack = [ + {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"}, + {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"}, + {file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"}, + {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7"}, + {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3"}, + {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b"}, + {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c"}, + {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd"}, + {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a"}, + {file = "msgpack-1.0.5-cp310-cp310-win32.whl", hash = "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea"}, + {file = "msgpack-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a"}, + {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f5ae84c5c8a857ec44dc180a8b0cc08238e021f57abdf51a8182e915e6299f0"}, + {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e6ca5d5699bcd89ae605c150aee83b5321f2115695e741b99618f4856c50898"}, + {file = "msgpack-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5494ea30d517a3576749cad32fa27f7585c65f5f38309c88c6d137877fa28a5a"}, + {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab2f3331cb1b54165976a9d976cb251a83183631c88076613c6c780f0d6e45a"}, + {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28592e20bbb1620848256ebc105fc420436af59515793ed27d5c77a217477705"}, + {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe5c63197c55bce6385d9aee16c4d0641684628f63ace85f73571e65ad1c1e8d"}, + {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed40e926fa2f297e8a653c954b732f125ef97bdd4c889f243182299de27e2aa9"}, + {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b2de4c1c0538dcb7010902a2b97f4e00fc4ddf2c8cda9749af0e594d3b7fa3d7"}, + {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf22a83f973b50f9d38e55c6aade04c41ddda19b00c4ebc558930d78eecc64ed"}, + {file = "msgpack-1.0.5-cp311-cp311-win32.whl", hash = "sha256:c396e2cc213d12ce017b686e0f53497f94f8ba2b24799c25d913d46c08ec422c"}, + {file = "msgpack-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c4c68d87497f66f96d50142a2b73b97972130d93677ce930718f68828b382e2"}, + {file = "msgpack-1.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a2b031c2e9b9af485d5e3c4520f4220d74f4d222a5b8dc8c1a3ab9448ca79c57"}, + {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f837b93669ce4336e24d08286c38761132bc7ab29782727f8557e1eb21b2080"}, + {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1d46dfe3832660f53b13b925d4e0fa1432b00f5f7210eb3ad3bb9a13c6204a6"}, + {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:366c9a7b9057e1547f4ad51d8facad8b406bab69c7d72c0eb6f529cf76d4b85f"}, + {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4c075728a1095efd0634a7dccb06204919a2f67d1893b6aa8e00497258bf926c"}, + {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:f933bbda5a3ee63b8834179096923b094b76f0c7a73c1cfe8f07ad608c58844b"}, + {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:36961b0568c36027c76e2ae3ca1132e35123dcec0706c4b7992683cc26c1320c"}, + {file = "msgpack-1.0.5-cp36-cp36m-win32.whl", hash = "sha256:b5ef2f015b95f912c2fcab19c36814963b5463f1fb9049846994b007962743e9"}, + {file = "msgpack-1.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:288e32b47e67f7b171f86b030e527e302c91bd3f40fd9033483f2cacc37f327a"}, + {file = "msgpack-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:137850656634abddfb88236008339fdaba3178f4751b28f270d2ebe77a563b6c"}, + {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c05a4a96585525916b109bb85f8cb6511db1c6f5b9d9cbcbc940dc6b4be944b"}, + {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a62ec00b636583e5cb6ad313bbed36bb7ead5fa3a3e38938503142c72cba4f"}, + {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef8108f8dedf204bb7b42994abf93882da1159728a2d4c5e82012edd92c9da9f"}, + {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1835c84d65f46900920b3708f5ba829fb19b1096c1800ad60bae8418652a951d"}, + {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e57916ef1bd0fee4f21c4600e9d1da352d8816b52a599c46460e93a6e9f17086"}, + {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17358523b85973e5f242ad74aa4712b7ee560715562554aa2134d96e7aa4cbbf"}, + {file = "msgpack-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:cb5aaa8c17760909ec6cb15e744c3ebc2ca8918e727216e79607b7bbce9c8f77"}, + {file = "msgpack-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:ab31e908d8424d55601ad7075e471b7d0140d4d3dd3272daf39c5c19d936bd82"}, + {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c"}, + {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d"}, + {file = "msgpack-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb"}, + {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba"}, + {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1"}, + {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87"}, + {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb"}, + {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48"}, + {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0"}, + {file = "msgpack-1.0.5-cp38-cp38-win32.whl", hash = "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e"}, + {file = "msgpack-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1"}, + {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025"}, + {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5"}, + {file = "msgpack-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd"}, + {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437"}, + {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f"}, + {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282"}, + {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d"}, + {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8"}, + {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11"}, + {file = "msgpack-1.0.5-cp39-cp39-win32.whl", hash = "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc"}, + {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"}, + {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"}, +] +multidict = [ + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, + {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, + {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, + {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, + {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, + {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, + {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, + {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, + {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, + {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, + {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, + {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, + {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, +] +mypy = [ + {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, + {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, + {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, + {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, + {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, + {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, + {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, + {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, + {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, + {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, + {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, + {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, + {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, + {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, + {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, + {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, + {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, + {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, + {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, + {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, +] +mypy-extensions = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] +nodeenv = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] +packaging = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] +paginate = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] +pathspec = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] +platformdirs = [ + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, +] +pluggy = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] +pre-commit = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] +pygments = [ + {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, + {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, +] +pymdown-extensions = [ + {file = "pymdown_extensions-10.2.1-py3-none-any.whl", hash = "sha256:bded105eb8d93f88f2f821f00108cb70cef1269db6a40128c09c5f48bfc60ea4"}, + {file = "pymdown_extensions-10.2.1.tar.gz", hash = "sha256:d0c534b4a5725a4be7ccef25d65a4c97dba58b54ad7c813babf0eb5ba9c81591"}, +] +pytest = [ + {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, + {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, +] +pytest-aiohttp = [ + {file = "pytest-aiohttp-1.0.5.tar.gz", hash = "sha256:880262bc5951e934463b15e3af8bb298f11f7d4d3ebac970aab425aff10a780a"}, + {file = "pytest_aiohttp-1.0.5-py3-none-any.whl", hash = "sha256:63a5360fd2f34dda4ab8e6baee4c5f5be4cd186a403cabd498fced82ac9c561e"}, +] +pytest-asyncio = [ + {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, + {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, +] +pytest-cov = [ + {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, + {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, +] +pytest-textual-snapshot = [ + {file = "pytest_textual_snapshot-0.4.0-py3-none-any.whl", hash = "sha256:879cc5de29cdd31cfe1b6daeb1dc5e42682abebcf4f88e7e3375bd5200683fc0"}, + {file = "pytest_textual_snapshot-0.4.0.tar.gz", hash = "sha256:63782e053928a925d88ff7359dd640f2900e23bc708b3007f8b388e65f2527cb"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +pytz = [ + {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, + {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, +] +pyyaml = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {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-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"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {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-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {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"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {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-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"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {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-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"}, ] - -[[package]] -name = "requests" -version = "2.31.0" -description = "Python HTTP for Humans." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +pyyaml-env-tag = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] +regex = [ + {file = "regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f"}, + {file = "regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66"}, + {file = "regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1"}, + {file = "regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5"}, + {file = "regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe"}, + {file = "regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7"}, + {file = "regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af"}, + {file = "regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61"}, + {file = "regex-2022.10.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4"}, + {file = "regex-2022.10.31-cp36-cp36m-win32.whl", hash = "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066"}, + {file = "regex-2022.10.31-cp36-cp36m-win_amd64.whl", hash = "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6"}, + {file = "regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95"}, + {file = "regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394"}, + {file = "regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0"}, + {file = "regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d"}, + {file = "regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c"}, + {file = "regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc"}, + {file = "regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453"}, + {file = "regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49"}, + {file = "regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892"}, + {file = "regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1"}, + {file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"}, + {file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"}, +] +requests = [ {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "rfc3986" -version = "1.5.0" -description = "Validating URI References per RFC 3986" -category = "dev" -optional = false -python-versions = "*" -files = [ +rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] - -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} - -[package.extras] -idna2008 = ["idna"] - -[[package]] -name = "rich" -version = "13.5.2" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -category = "main" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.5.2-py3-none-any.whl", hash = "sha256:146a90b3b6b47cac4a73c12866a499e9817426423f57c5a66949c086191a8808"}, - {file = "rich-13.5.2.tar.gz", hash = "sha256:fb9d6c0a0f643c99eed3875b5377a184132ba9be4d61516a55273d3554d75a39"}, +rich = [ + {file = "rich-13.6.0-py3-none-any.whl", hash = "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245"}, + {file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"}, ] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "setuptools" -version = "68.0.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +setuptools = [ {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, ] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ +six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] - -[[package]] -name = "smmap" -version = "5.0.0" -description = "A pure Python implementation of a sliding window memory map manager" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, - {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, +smmap = [ + {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, + {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, ] - -[[package]] -name = "sniffio" -version = "1.3.0" -description = "Sniff out which async library your code is running under" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +sniffio = [ {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, ] - -[[package]] -name = "syrupy" -version = "3.0.6" -description = "Pytest Snapshot Test Utility" -category = "dev" -optional = false -python-versions = ">=3.7,<4" -files = [ +syrupy = [ {file = "syrupy-3.0.6-py3-none-any.whl", hash = "sha256:9c18e22264026b34239bcc87ab7cc8d893eb17236ea7dae634217ea4f22a848d"}, {file = "syrupy-3.0.6.tar.gz", hash = "sha256:583aa5ca691305c27902c3e29a1ce9da50ff9ab5f184c54b1dc124a16e4a6cf4"}, ] - -[package.dependencies] -colored = ">=1.3.92,<2.0.0" -pytest = ">=5.1.0,<8.0.0" - -[[package]] -name = "textual-dev" -version = "1.1.0" -description = "Development tools for working with Textual" -category = "dev" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "textual_dev-1.1.0-py3-none-any.whl", hash = "sha256:c57320636098e31fa5d5c29fc3bc60829bb420da3c76bfed24db6eacf178dbc6"}, - {file = "textual_dev-1.1.0.tar.gz", hash = "sha256:e2f8ce4e1c18a16b80282f3257cd2feb49a7ede289a78908c9063ce071bb77ce"}, +textual-dev = [ + {file = "textual_dev-1.2.1-py3-none-any.whl", hash = "sha256:a96ff43841cadf853dd689d68c2fc920a23ad71cfa9a33917ca53e96d1cc81f3"}, + {file = "textual_dev-1.2.1.tar.gz", hash = "sha256:0bda11adfc541e0cc9e49bdf37a8b852281dc2387bb6ff3d01f40c7a3f841684"}, ] - -[package.dependencies] -aiohttp = ">=3.8.1" -click = ">=8.1.2" -msgpack = ">=1.0.3" -textual = ">=0.32.0" -typing-extensions = ">=4.4.0,<5.0.0" - -[[package]] -name = "time-machine" -version = "2.10.0" -description = "Travel through time in your tests." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +time-machine = [ {file = "time_machine-2.10.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2d5e93c14b935d802a310c1d4694a9fe894b48a733ebd641c9a570d6f9e1f667"}, {file = "time_machine-2.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c0dda6b132c0180941944ede357109016d161d840384c2fb1096a3a2ef619f4"}, {file = "time_machine-2.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:900517e4a4121bf88527343d6aea2b5c99df134815bb8271ef589ec792502a71"}, @@ -1996,42 +2224,124 @@ files = [ {file = "time_machine-2.10.0-cp39-cp39-win_arm64.whl", hash = "sha256:c1775a949dd830579d1af5a271ec53d920dc01657035ad305f55c5a1ac9b9f1e"}, {file = "time_machine-2.10.0.tar.gz", hash = "sha256:64fd89678cf589fc5554c311417128b2782222dd65f703bf248ef41541761da0"}, ] - -[package.dependencies] -python-dateutil = "*" - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ +toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] - -[[package]] -name = "typed-ast" -version = "1.5.5" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ +tree-sitter = [ + {file = "tree_sitter-0.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1a151ccf9233b0b84850422654247f68a4d78f548425c76520402ea6fb6cdb24"}, + {file = "tree_sitter-0.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52ca2738c3c4c660c83054ac3e44a49cbecb9f89dc26bb8e154d6ca288aa06b0"}, + {file = "tree_sitter-0.20.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8d51478ea078da7cc6f626e9e36f131bbc5fac036cf38ea4b5b81632cbac37d"}, + {file = "tree_sitter-0.20.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0b2b59e1633efbf19cd2ed1ceb8d51b2c44a278153b1113998c70bc1570b750"}, + {file = "tree_sitter-0.20.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7f691c57d2a65d6e53e2f3574153c9cd0c157ff938b8d6f252edd5e619811403"}, + {file = "tree_sitter-0.20.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba72a363387eebaff9a0b788f864fe47da425136cbd4cac6cd125051f043c296"}, + {file = "tree_sitter-0.20.2-cp310-cp310-win32.whl", hash = "sha256:55e33eb206446d5046d3b5fe36ab300840f5a8a844246adb0ccc68c55c30b722"}, + {file = "tree_sitter-0.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:24ce9d14daba0a71a778417d9d61dd4038ca96981ddec19e1e8990881469321c"}, + {file = "tree_sitter-0.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:942dbfb8bc380f09b0e323d3884de07d19022930516f33b7503a6eb5f6e18979"}, + {file = "tree_sitter-0.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ee5651c11924d426f8d6858a40fd5090ae31574f81ef180bef2055282f43bf62"}, + {file = "tree_sitter-0.20.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fb6982b480031628dad7f229c4c8d90b17d4c281ba97848d3b100666d7fa45f"}, + {file = "tree_sitter-0.20.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:067609c6c7cb6e5a6c4be50076a380fe52b6e8f0641ee9d0da33b24a5b972e82"}, + {file = "tree_sitter-0.20.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:849d7e6b66fe7ded08a633943b30e0ed807eee76104288e6c6841433f4a9651b"}, + {file = "tree_sitter-0.20.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e85689573797e49f86e2d7cf48b9dd23bc044c477df074a78546e666d6990a29"}, + {file = "tree_sitter-0.20.2-cp311-cp311-win32.whl", hash = "sha256:098906148e44ea391a91b019d584dd8d0ea1437af62a9744e280e93163fd35ca"}, + {file = "tree_sitter-0.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:2753a87094b72fe7f02276b3948155618f53aa14e1ca20588f0eeed510f68512"}, + {file = "tree_sitter-0.20.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5de192cb9e7b1c882d45418decb7899f1547f7056df756bcae186bbf4966d96e"}, + {file = "tree_sitter-0.20.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3a77e663293a73a97edbf2a2e05001de08933eb5d311a16bdc25b9b2fac54f3"}, + {file = "tree_sitter-0.20.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:415da4a70c56a003758537517fe9e60b8b0c5f70becde54cc8b8f3ba810adc70"}, + {file = "tree_sitter-0.20.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:707fb4d7a6123b8f9f2b005d61194077c3168c0372556e7418802280eddd4892"}, + {file = "tree_sitter-0.20.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:75fcbfb0a61ad64e7f787eb3f8fbf29b8e2b858dc011897ad039d838a06cee02"}, + {file = "tree_sitter-0.20.2-cp36-cp36m-win32.whl", hash = "sha256:622926530895d939fa6e1e2487e71a311c71d3b09f4c4f19301695ea866304a4"}, + {file = "tree_sitter-0.20.2-cp36-cp36m-win_amd64.whl", hash = "sha256:5c0712f031271d9bc462f1db7623d23703ed9fbcbaa6dc19ba535f58d6110774"}, + {file = "tree_sitter-0.20.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2dfdf680ecf5619447243c4c20e4040a7b5e7afca4e1569f03c814e86bfda248"}, + {file = "tree_sitter-0.20.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79650ee23a15559b69542c71ed9eb3297dce21932a7c5c148be384dd0f2cd49d"}, + {file = "tree_sitter-0.20.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d63059746b4b2f2f87dd19c208141c69452694aae32459b7a4ebca8539d13bf4"}, + {file = "tree_sitter-0.20.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9398d1e214d4915032cf68a678de7eb803f64d25ef04724d70b88db7bb7746e9"}, + {file = "tree_sitter-0.20.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b506fb2e2bd7a5a1603c644bbb90401fe488f86bbca39706addaa8d2bfc80815"}, + {file = "tree_sitter-0.20.2-cp37-cp37m-win32.whl", hash = "sha256:405e83804ba60ca1c3dbd258adbe0d7b0f1bdce948e5eec5587a2ebedcf930ba"}, + {file = "tree_sitter-0.20.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a1e66d211c04144484e223922ac094a2367476e6f57000f986c5560dc5a83c6e"}, + {file = "tree_sitter-0.20.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f8adc325c74c042204ed47d095e0ec86f83de3c7ec4979645f86b58514f60297"}, + {file = "tree_sitter-0.20.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beb49c861e1d111e0df119ecbfaa409e6413b8d91e8f56bcdb15f07fbc35594e"}, + {file = "tree_sitter-0.20.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e17ee83409b01fdd09021997b0c747be2f773bb2bb140ba6fb48b7e12fdd039a"}, + {file = "tree_sitter-0.20.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:475ab841647a0d1bc1266c8978279f8e4f7b9520b9a7336d532e5dfc8910214d"}, + {file = "tree_sitter-0.20.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:222350189675d9814966a5c88c6c1378a2ee2f3041c439a6f1d1ff2006f403aa"}, + {file = "tree_sitter-0.20.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:31ea52f0deee70f2cb00aff01e40aae325a34ebe1661de274c9107322fb95f54"}, + {file = "tree_sitter-0.20.2-cp38-cp38-win32.whl", hash = "sha256:cceaf7287137cbca707006624a4a8d4b5ccbfec025793fde84d90524c2bb0946"}, + {file = "tree_sitter-0.20.2-cp38-cp38-win_amd64.whl", hash = "sha256:25b9669911f21ec2b3727bb2f4dfeff6ddb6f81898c3e968d378a660e0d7f90e"}, + {file = "tree_sitter-0.20.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce30a17f46a6b39a04a599dea88c127a19e3e1f43a2ad0ced71b5c032d585077"}, + {file = "tree_sitter-0.20.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9576e8b2e663639527e01ab251b87f0bd370bfdd40515588689ebc424aec786"}, + {file = "tree_sitter-0.20.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d03731a498f624ce3536c821ef23b03d1ad569b3845b326a5b7149ef189d732c"}, + {file = "tree_sitter-0.20.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0116ecb163573ebaa0fc04cc99c90bd94c0be5cc4d0a1ebeb102de9cc9a054"}, + {file = "tree_sitter-0.20.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0943b00d3700f253c3ee6a53a71b9a6ca46defd9c0a33edb07a9388e70dc3a9e"}, + {file = "tree_sitter-0.20.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cb566b6f0b5457148cb8310a1ca3d764edf28e47fcccfe0b167861ecaa50c12"}, + {file = "tree_sitter-0.20.2-cp39-cp39-win32.whl", hash = "sha256:4544204a24c2b4d25d1731b0df83f7c819ce87c4f2538a19724b8753815ef388"}, + {file = "tree_sitter-0.20.2-cp39-cp39-win_amd64.whl", hash = "sha256:9517b204e471d6aa59ee2232f6220f315ed5336079034d5c861a24660d6511d6"}, + {file = "tree_sitter-0.20.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:84343678f58cb354d22ed14b627056ffb33c540cf16c35a83db4eeee8827b935"}, + {file = "tree_sitter-0.20.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:611a80171d8fa6833dd0c8b022714d2ea789de15a955ec42ec4fd5fcc1032edb"}, + {file = "tree_sitter-0.20.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bacecfb61694c95ccee462742b3fcea50ba1baf115c42e60adf52b549ef642ce"}, + {file = "tree_sitter-0.20.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f344ae94a268479456f19712736cc7398de5822dc74cca7d39538c28085721d0"}, + {file = "tree_sitter-0.20.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:221784d7f326fe81ce7174ac5972800f58b9a7c5c48a03719cad9830c22e5a76"}, + {file = "tree_sitter-0.20.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64210ed8d2a1b7e2951f6576aa0cb7be31ad06d87da26c52961318fc54c7fe77"}, + {file = "tree_sitter-0.20.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2634ac73b39ceacfa431d6d95692eae7465977fa0b9e9f7ae6cb445991e829a5"}, + {file = "tree_sitter-0.20.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:71663a0e8230dae99d9c55e6895bd2c9e42534ec861b255775f704ae2db70c1d"}, + {file = "tree_sitter-0.20.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:32c3e0f30b45a58d36bf6a0ec982ca3eaa23c7f924628da499b7ad22a8abad71"}, + {file = "tree_sitter-0.20.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b02e4ab2158c25f6f520c93318d562da58fa4ba53e1dbd434be008f48104980"}, + {file = "tree_sitter-0.20.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10e567eb6961a1e86aebbe26a9ca07d324f8529bca90937a924f8aa0ea4dc127"}, + {file = "tree_sitter-0.20.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:63f8e8e69f5f25c2b565449e1b8a2aa7b6338b4f37c8658c5fbdec04858c30be"}, + {file = "tree_sitter-0.20.2.tar.gz", hash = "sha256:0a6c06abaa55de174241a476b536173bba28241d2ea85d198d33aa8bf009f028"}, +] +tree-sitter-languages = [ + {file = "tree_sitter_languages-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fd8b856c224a74c395ed9495761c3ef8ba86014dbf6037d73634436ae683c808"}, + {file = "tree_sitter_languages-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:277d1bec6e101a26a4445cd7cb1eb8f8cf5a9bbad1ca80692bfae1af63568272"}, + {file = "tree_sitter_languages-1.7.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0473bd896799ccc87f428766813ddedd3506cad8430dbe863b663c81d7387680"}, + {file = "tree_sitter_languages-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6799419bc7e3029112f2a3f8b77b6c299f94f03bb70e5c31a437b3180486be"}, + {file = "tree_sitter_languages-1.7.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e5b705c8ce6ef47fc461484878956ecd42a67cbeb0a17e323b86a4439a8fdc3d"}, + {file = "tree_sitter_languages-1.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:28a732be6fced2f70184c1b34f64961e3b6259fe6d5f7540c91028c2a43a7109"}, + {file = "tree_sitter_languages-1.7.0-cp310-cp310-win32.whl", hash = "sha256:f5cdb1ec88f0b8c617330c953555a20cc7e96ca6b1f5c68ab6db347e869cfeeb"}, + {file = "tree_sitter_languages-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:26cb344a75798fce1a73b690504d8e7789f6ba25a178efcd203444d7868caf38"}, + {file = "tree_sitter_languages-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:433b56cb3dca02b30f21c596f431a2cff90905326be1f8913c3515acb984b21e"}, + {file = "tree_sitter_languages-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96686390e1a01af44aedef7b33d6be82de3cf674a98a5c7b417e540e6afa62cc"}, + {file = "tree_sitter_languages-1.7.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25a4b6d559fbd76c6ec1b73cf03d09f53aaa5a1b61078a3f518b162866d9d97e"}, + {file = "tree_sitter_languages-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e504f199c7a4c8b1b1efb05a063450aa23234feea6fa6c06f4077f7248ea9c98"}, + {file = "tree_sitter_languages-1.7.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6b29856e9314b5f68f05dfa45e6674f47535229dda32294ba6d129077a97759c"}, + {file = "tree_sitter_languages-1.7.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:786fdaf3d2120eef9384b0f22d7e2e42a561073ba753c7b438e90a1e7b351650"}, + {file = "tree_sitter_languages-1.7.0-cp311-cp311-win32.whl", hash = "sha256:a55a7007056d0927b78481b437d79ea0487cc991c7f9c19d67adcceac3d47f53"}, + {file = "tree_sitter_languages-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:4b01d3bdf7ce2aeee4d0df62071a0ca91e618a29845686a5bd714d93c5ef3b36"}, + {file = "tree_sitter_languages-1.7.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b603f1ad01bfb9d178f965125e2528cb7da9666d180f4a9a1acfaedbf5862ea"}, + {file = "tree_sitter_languages-1.7.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70610aa26dd985d2fb9eb07ea8eacc3ceb0cc9c2e91416f51305120cfd919e28"}, + {file = "tree_sitter_languages-1.7.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0444ebc8bdb7dc0d66a816050cfd52376c4e62a94a9c54fde90b29acf3e4bab1"}, + {file = "tree_sitter_languages-1.7.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:7eeb5a3307ff1c0994ffff5ea37ec656a716a728b8c9359374104da521a76ded"}, + {file = "tree_sitter_languages-1.7.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:6c319cef16f2df667f1c165fe4eee160f2b51a0c4b61db1e70de2ab86420ca9a"}, + {file = "tree_sitter_languages-1.7.0-cp36-cp36m-win32.whl", hash = "sha256:b216650126d95d494f927393903e836a7ef5f0c4db0834f3a0b576f97c13abaf"}, + {file = "tree_sitter_languages-1.7.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6c96e5785d164a205962a10256808b3d12dccee9827ec88a46899063a2a2d28"}, + {file = "tree_sitter_languages-1.7.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:adafeabbd8d47b80122fad18bb61c25ed3da04f5347b7d774b53826accb27b7a"}, + {file = "tree_sitter_languages-1.7.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50e2bc5d2da770ecd5af94f9d716faa4764f890fd61bc0a488e9269653d9fb71"}, + {file = "tree_sitter_languages-1.7.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac773097cff7de6cf265c5be9990b4c6690161452da1d9fc41021d4bf7e8c73a"}, + {file = "tree_sitter_languages-1.7.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b233bfc48cf0f16436200afc7d7643cd87101c321de25b919b61f21f1693aa52"}, + {file = "tree_sitter_languages-1.7.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:eab3caedf50467045ed5cab776a57b494332616376d387c6600fd7ea4f5483cf"}, + {file = "tree_sitter_languages-1.7.0-cp37-cp37m-win32.whl", hash = "sha256:d533f743a22f5696494d3a5a60adb4cfbef63d58b8b5622993d93d6d0a602444"}, + {file = "tree_sitter_languages-1.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:aab96f64be30c9f73d6dc958ec22bb1a9fe70e90b2d2a3d233d537b347cea729"}, + {file = "tree_sitter_languages-1.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1bf89d771621e28847036b377f865f947e555a6654356d21beab738bb2531a69"}, + {file = "tree_sitter_languages-1.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b2f171089ec3c4f1de275edc8f0722e1e3dc7a54e83107098315ea2f0952cfcd"}, + {file = "tree_sitter_languages-1.7.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a091577d3a8454c40f813ee2834314c73cc504522f70f9e33d7c2268d33973f9"}, + {file = "tree_sitter_languages-1.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8287efa87d080b340b583a6e81266cc3d8266deb61b8f3312649a9d1562e665a"}, + {file = "tree_sitter_languages-1.7.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9c5080c06a2df7a59c69d2422a6ae83a5e37e92d57c4bd5e572d0eb5226ab3b0"}, + {file = "tree_sitter_languages-1.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ca8f629cfb406a2f9b9f8a3a5c804d4d1ba4cdca41cccba63f51fc1bab13e5de"}, + {file = "tree_sitter_languages-1.7.0-cp38-cp38-win32.whl", hash = "sha256:fd3561b37a99c9d501719819a8736529ae3a6d597128c15be432d1855f3cb0d9"}, + {file = "tree_sitter_languages-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:377ad60f7a7bf27315676c4fa84cc766aa0019c1e556083763136ed951e934c0"}, + {file = "tree_sitter_languages-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1dc71b68e48f58cd5b6a9ab7a541714201815629a6554a969cfc579a6ee6e53"}, + {file = "tree_sitter_languages-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fb1521367b14c275bef70997ea90526e7049f840ba1bbd3ef56c72f5b15596e9"}, + {file = "tree_sitter_languages-1.7.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f73651f7e78371dc3d455e8aba510cc6fb9e1ac1d648c3334157950781eb295"}, + {file = "tree_sitter_languages-1.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:049b0dd63be721fe3f9642a2b5a044bea2852de2b35818467996242ae4b7f01f"}, + {file = "tree_sitter_languages-1.7.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c428a8e1f5ecc4eb5c79abff3eb2881123446cde16fd1d8866d527470a6fdd2f"}, + {file = "tree_sitter_languages-1.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:40fb3fc11ff90caf65b4713feeb6c4852e5d2a04ef8ae6a2ac734a702a6a6c7e"}, + {file = "tree_sitter_languages-1.7.0-cp39-cp39-win32.whl", hash = "sha256:f28e9904833b7a909f8227c4560401049bd3310cebe3e0a884d9461f783b9af2"}, + {file = "tree_sitter_languages-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ea47ee390ec2e1c9bf96d7b418775263766021a834910c9f2d578f95a3e27d0f"}, +] +typed-ast = [ {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"}, @@ -2074,106 +2384,39 @@ files = [ {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"}, {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, ] - -[[package]] -name = "types-setuptools" -version = "67.8.0.0" -description = "Typing stubs for setuptools" -category = "dev" -optional = false -python-versions = "*" -files = [ +types-setuptools = [ {file = "types-setuptools-67.8.0.0.tar.gz", hash = "sha256:95c9ed61871d6c0e258433373a4e1753c0a7c3627a46f4d4058c7b5a08ab844f"}, {file = "types_setuptools-67.8.0.0-py3-none-any.whl", hash = "sha256:6df73340d96b238a4188b7b7668814b37e8018168aef1eef94a3b1872e3f60ff"}, ] - -[[package]] -name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +types-tree-sitter = [ + {file = "types-tree-sitter-0.20.1.5.tar.gz", hash = "sha256:94f971599548b90b9bbb6af651d235ad795a094a07651bc565a4b8856caebab1"}, + {file = "types_tree_sitter-0.20.1.5-py3-none-any.whl", hash = "sha256:8d7f9961febbad29789ce5c65f79b95b0702f3d34a7c12fabcd69c36c2bbe184"}, +] +types-tree-sitter-languages = [ + {file = "types-tree-sitter-languages-1.7.0.1.tar.gz", hash = "sha256:eadbbfa13f3fcad0711ac8f866cf87692f3c0cfeee72e979a5202b797588d57d"}, + {file = "types_tree_sitter_languages-1.7.0.1-py3-none-any.whl", hash = "sha256:818ec7824ed1bb5bcdbe21022340e0df3930199eb969ea1e08eb03a92440bce2"}, +] +typing-extensions = [ {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] - -[[package]] -name = "tzdata" -version = "2022.7" -description = "Provider of IANA time zone data" -category = "dev" -optional = false -python-versions = ">=2" -files = [ +tzdata = [ {file = "tzdata-2022.7-py2.py3-none-any.whl", hash = "sha256:2b88858b0e3120792a3c0635c23daf36a7d7eeeca657c323da299d2094402a0d"}, {file = "tzdata-2022.7.tar.gz", hash = "sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"}, ] - -[[package]] -name = "uc-micro-py" -version = "1.0.2" -description = "Micro subset of unicode data files for linkify-it-py projects." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +uc-micro-py = [ {file = "uc-micro-py-1.0.2.tar.gz", hash = "sha256:30ae2ac9c49f39ac6dce743bd187fcd2b574b16ca095fa74cd9396795c954c54"}, {file = "uc_micro_py-1.0.2-py3-none-any.whl", hash = "sha256:8c9110c309db9d9e87302e2f4ad2c3152770930d88ab385cd544e7a7e75f3de0"}, ] - -[package.extras] -test = ["coverage", "pytest", "pytest-cov"] - -[[package]] -name = "urllib3" -version = "2.0.4" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, - {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, +urllib3 = [ + {file = "urllib3-2.0.6-py3-none-any.whl", hash = "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2"}, + {file = "urllib3-2.0.6.tar.gz", hash = "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564"}, ] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "virtualenv" -version = "20.24.2" -description = "Virtual Python Environment builder" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "virtualenv-20.24.2-py3-none-any.whl", hash = "sha256:43a3052be36080548bdee0b42919c88072037d50d56c28bd3f853cbe92b953ff"}, - {file = "virtualenv-20.24.2.tar.gz", hash = "sha256:fd8a78f46f6b99a67b7ec5cf73f92357891a7b3a40fd97637c27f854aae3b9e0"}, +virtualenv = [ + {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"}, + {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"}, ] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""} -platformdirs = ">=3.9.1,<4" - -[package.extras] -docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] - -[[package]] -name = "watchdog" -version = "3.0.0" -description = "Filesystem events monitoring" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +watchdog = [ {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41"}, {file = "watchdog-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397"}, {file = "watchdog-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96"}, @@ -2202,18 +2445,7 @@ files = [ {file = "watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759"}, {file = "watchdog-3.0.0.tar.gz", hash = "sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9"}, ] - -[package.extras] -watchmedo = ["PyYAML (>=3.10)"] - -[[package]] -name = "yarl" -version = "1.9.2" -description = "Yet another URL library" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ +yarl = [ {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"}, {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"}, {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"}, @@ -2289,29 +2521,7 @@ files = [ {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"}, {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"}, ] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} - -[[package]] -name = "zipp" -version = "3.15.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ +zipp = [ {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, ] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.7" -content-hash = "5ac8aef69083d16bc38af16f22cc94ad14b8b70b5cff61e0c7d462c1d1a8a42c" diff --git a/pyproject.toml b/pyproject.toml index 50d10b2121..ab7e050fd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,10 @@ [tool.poetry] name = "textual" -version = "0.32.0" +version = "0.40.0" homepage = "https://github.com/Textualize/textual" +repository = "https://github.com/Textualize/textual" +documentation = "https://textual.textualize.io/" + description = "Modern Text User Interface framework" authors = ["Will McGugan "] license = "MIT" @@ -19,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Typing :: Typed", ] include = [ @@ -30,16 +34,24 @@ include = [ # it also seems like exclude trumps include. So here we specify that we # want to package up the content of the docs-offline directory in a way # that works around that. - { path = "docs-offline/**/*", format = "sdist" } + { path = "docs-offline/**/*", format = "sdist" }, ] +[tool.poetry.urls] +"Bug Tracker" = "https://github.com/Textualize/textual/issues" + [tool.poetry.dependencies] python = "^3.7" rich = ">=13.3.3" -markdown-it-py = {extras = ["plugins", "linkify"], version = ">=2.1.0"} +markdown-it-py = { extras = ["plugins", "linkify"], version = ">=2.1.0" } #rich = {path="../rich", develop=true} importlib-metadata = ">=4.11.3" typing-extensions = "^4.4.0" +tree-sitter = { version = "^0.20.1", optional = true } +tree_sitter_languages = { version = ">=1.7.0", optional = true } + +[tool.poetry.extras] +syntax = ["tree-sitter", "tree_sitter_languages"] [tool.poetry.group.dev.dependencies] pytest = "^7.1.3" @@ -47,7 +59,7 @@ black = "^23.1.0" mypy = "^1.0.0" pytest-cov = "^2.12.1" mkdocs = "^1.3.0" -mkdocstrings = {extras = ["python"], version = "^0.20.0"} +mkdocstrings = { extras = ["python"], version = "^0.20.0" } mkdocstrings-python = "0.10.1" mkdocs-material = "^9.0.11" mkdocs-exclude = "^1.0.2" @@ -57,9 +69,11 @@ time-machine = "^2.6.0" mkdocs-rss-plugin = "^1.5.0" httpx = "^0.23.1" types-setuptools = "^67.2.0.1" -textual-dev = "^1.1.0" +textual-dev = "^1.2.0" pytest-asyncio = "*" -pytest-textual-snapshot = "0.2.0" +pytest-textual-snapshot = ">=0.4.0" +types-tree-sitter = "^0.20.1.4" +types-tree-sitter-languages = "^1.7.0.1" [tool.black] includes = "src" diff --git a/questions/align-center-middle.question.md b/questions/align-center-middle.question.md index 25e6bd1f84..a33ff239be 100644 --- a/questions/align-center-middle.question.md +++ b/questions/align-center-middle.question.md @@ -9,6 +9,11 @@ alt_titles: - "centre controls" --- +!!! tip + + See [*How To Center Things*](https://textual.textualize.io/how-to/center-things/) in the + Textual documentation for a more comprensive answer to this question. + To center a widget within a container use [`align`](https://textual.textualize.io/styles/align/). But remember that `align` works on the *children* of a container, it isn't something you use diff --git a/src/textual/__init__.py b/src/textual/__init__.py index 46fcf3ec3f..103f9db2fe 100644 --- a/src/textual/__init__.py +++ b/src/textual/__init__.py @@ -64,12 +64,6 @@ def __rich_repr__(self) -> rich.repr.Result: yield self._verbosity, LogVerbosity.NORMAL def __call__(self, *args: object, **kwargs) -> None: - try: - app = active_app.get() - except LookupError: - print_args = (*args, *[f"{key}={value!r}" for key, value in kwargs.items()]) - print(*print_args) - return if constants.LOG_FILE: output = " ".join(str(arg) for arg in args) if kwargs: @@ -80,6 +74,12 @@ def __call__(self, *args: object, **kwargs) -> None: with open(constants.LOG_FILE, "a") as log_file: print(output, file=log_file) + try: + app = active_app.get() + except LookupError: + print_args = (*args, *[f"{key}={value!r}" for key, value in kwargs.items()]) + print(*print_args) + return if app.devtools is None or not app.devtools.is_connected: return diff --git a/src/textual/_ansi_sequences.py b/src/textual/_ansi_sequences.py index fc3b8de624..9d0c4a601b 100644 --- a/src/textual/_ansi_sequences.py +++ b/src/textual/_ansi_sequences.py @@ -221,6 +221,8 @@ "\x1b[1;5B": (Keys.ControlDown,), # Cursor Mode "\x1b[1;5C": (Keys.ControlRight,), # Cursor Mode "\x1b[1;5D": (Keys.ControlLeft,), # Cursor Mode + "\x1bf": (Keys.ControlRight,), # iTerm natural editing keys + "\x1bb": (Keys.ControlLeft,), # iTerm natural editing keys "\x1b[1;5F": (Keys.ControlEnd,), "\x1b[1;5H": (Keys.ControlHome,), # Tmux sends following keystrokes when control+arrow is pressed, but for diff --git a/src/textual/_cache.py b/src/textual/_cache.py index a4b440e602..79a589ff0e 100644 --- a/src/textual/_cache.py +++ b/src/textual/_cache.py @@ -183,14 +183,22 @@ def discard(self, key: CacheKey) -> None: Args: key: Cache key. """ - link = self._cache.get(key) - if link is None: + if key not in self._cache: return + link = self._cache[key] + # Remove link from list link[0][1] = link[1] # type: ignore[index] link[1][0] = link[0] # type: ignore[index] # Remove link from cache + + if self._head[2] == key: + self._head = self._head[1] # type: ignore[assignment] + if self._head[2] == key: # type: ignore[index] + self._head = [] + del self._cache[key] + self._full = False class FIFOCache(Generic[CacheKey, CacheValue]): diff --git a/src/textual/_fuzzy.py b/src/textual/_fuzzy.py deleted file mode 100644 index 2d9766e054..0000000000 --- a/src/textual/_fuzzy.py +++ /dev/null @@ -1,82 +0,0 @@ -from re import compile, escape - -import rich.repr -from rich.text import Text - -from ._cache import LRUCache - - -@rich.repr.auto -class Matcher: - """A fuzzy matcher.""" - - def __init__(self, query: str) -> None: - """ - Args: - query: A query as typed in by the user. - """ - self.query = query - self._query_regex = ".*?".join(f"({escape(character)})" for character in query) - self._query_regex_compiled = compile(self._query_regex) - self._cache: LRUCache[str, float] = LRUCache(1024 * 4) - - def match(self, input: str) -> float: - """Match the input against the query - - Args: - input: Input string to match against. - - Returns: - Strength of the match from 0 to 1. - """ - cached = self._cache.get(input) - if cached is not None: - return cached - match = self._query_regex_compiled.search(input) - if match is None: - score = 0.0 - else: - assert match.lastindex is not None - offsets = [ - match.span(group_no)[0] for group_no in range(1, match.lastindex + 1) - ] - group_count = 0 - last_offset = -2 - for offset in offsets: - if offset > last_offset + 1: - group_count += 1 - last_offset = offset - - score = 1.0 - ((group_count - 1) / len(input)) - self._cache[input] = score - return score - - def highlight(self, input: str) -> Text: - """Highlight the input with the fuzzy match. - - Args: - input: User input. - - Returns: - A Text object with matched letters in bold. - """ - match = self._query_regex_compiled.search(input) - text = Text(input) - if match is None: - return text - assert match.lastindex is not None - offsets = [ - match.span(group_no)[0] for group_no in range(1, match.lastindex + 1) - ] - for offset in offsets: - text.stylize("bold", offset, offset + 1) - - return text - - -if __name__ == "__main__": - from rich import print - - matcher = Matcher("foo.bar") - print(matcher.match("xz foo.bar sdf")) - print(matcher.highlight("xz foo.bar sdf")) diff --git a/src/textual/_slug.py b/src/textual/_slug.py new file mode 100644 index 0000000000..8d23ca4dab --- /dev/null +++ b/src/textual/_slug.py @@ -0,0 +1,116 @@ +"""Provides a utility function and class for creating Markdown-friendly slugs. + +The approach to creating slugs is designed to be as close to +GitHub-flavoured Markdown as possible. However, because there doesn't appear +to be any actual documentation for this 'standard', the code here involves +some guesswork and also some pragmatic shortcuts. + +Expect this to grow over time. + +The main rules used in here at the moment are: + +1. Strip all leading and trailing whitespace. +2. Remove all non-lingual characters (emoji, etc). +3. Remove all punctuation and whitespace apart from dash and underscore. +""" + +from __future__ import annotations + +from collections import defaultdict +from re import compile +from string import punctuation +from typing import Pattern +from urllib.parse import quote + +from typing_extensions import Final + +WHITESPACE_REPLACEMENT: Final[str] = "-" +"""The character to replace undesirable characters with.""" + +REMOVABLE: Final[str] = punctuation.replace(WHITESPACE_REPLACEMENT, "").replace("_", "") +"""The collection of characters that should be removed altogether.""" + +NONLINGUAL: Final[str] = ( + r"\U000024C2-\U0001F251" + r"\U00002702-\U000027B0" + r"\U0001F1E0-\U0001F1FF" + r"\U0001F300-\U0001F5FF" + r"\U0001F600-\U0001F64F" + r"\U0001F680-\U0001F6FF" + r"\U0001f926-\U0001f937" + r"\u200D" + r"\u2640-\u2642" +) +"""A string that can be used in a regular expression to remove most non-lingual characters.""" + +STRIP_RE: Final[Pattern] = compile(f"[{REMOVABLE}{NONLINGUAL}]+") +"""A regular expression for finding all the characters that should be removed.""" + +WHITESPACE_RE: Final[Pattern] = compile(r"\s") +"""A regular expression for finding all the whitespace and turning it into `REPLACEMENT`.""" + + +def slug(text: str) -> str: + """Create a Markdown-friendly slug from the given text. + + Args: + text: The text to generate a slug from. + + Returns: + A slug for the given text. + + The rules used in generating the slug are based on observations of how + GitHub-flavoured Markdown works. + """ + result = text.strip().lower() + for rule, replacement in ( + (STRIP_RE, ""), + (WHITESPACE_RE, WHITESPACE_REPLACEMENT), + ): + result = rule.sub(replacement, result) + return quote(result) + + +class TrackedSlugs: + """Provides a class for generating tracked slugs. + + While [`slug`][textual._slug.slug] will generate a slug for a given + string, it does not guarantee that it is unique for a given context. If + you want to ensure that the same string generates unique slugs (perhaps + heading slugs within a Markdown document, as an example), use an + instance of this class to generate them. + + Example: + ```python + >>> slug("hello world") + 'hello-world' + >>> slug("hello world") + 'hello-world' + >>> unique = TrackedSlugs() + >>> unique.slug("hello world") + 'hello-world' + >>> unique.slug("hello world") + 'hello-world-1' + ``` + """ + + def __init__(self) -> None: + """Initialise the tracked slug object.""" + self._used: defaultdict[str, int] = defaultdict(int) + """Keeps track of how many times a particular slug has been used.""" + + def slug(self, text: str) -> str: + """Create a Markdown-friendly unique slug from the given text. + + Args: + text: The text to generate a slug from. + + Returns: + A slug for the given text. + """ + slugged = slug(text) + used = self._used[slugged] + self._used[slugged] += 1 + if used: + slugged = f"{slugged}-{used}" + return slugged diff --git a/src/textual/_system_commands.py b/src/textual/_system_commands.py new file mode 100644 index 0000000000..c8b499c395 --- /dev/null +++ b/src/textual/_system_commands.py @@ -0,0 +1,55 @@ +"""A command palette command provider for Textual system commands. + +This is a simple command provider that makes the most obvious application +actions available via the [command palette][textual.command.CommandPalette]. +""" + +from .command import Hit, Hits, Provider + + +class SystemCommands(Provider): + """A [source][textual.command.Provider] of command palette commands that run app-wide tasks. + + Used by default in [`App.COMMANDS`][textual.app.App.COMMANDS]. + """ + + async def search(self, query: str) -> Hits: + """Handle a request to search for system commands that match the query. + + Args: + user_input: The user input to be matched. + + Yields: + Command hits for use in the command palette. + """ + # We're going to use Textual's builtin fuzzy matcher to find + # matching commands. + matcher = self.matcher(query) + + # Loop over all applicable commands, find those that match and offer + # them up to the command palette. + for name, runnable, help_text in ( + ( + "Toggle light/dark mode", + self.app.action_toggle_dark, + "Toggle the application between light and dark mode", + ), + ( + "Quit the application", + self.app.action_quit, + "Quit the application as soon as possible", + ), + ( + "Ring the bell", + self.app.action_bell, + "Ring the terminal's 'bell'", + ), + ): + match = matcher.match(name) + if match > 0: + yield Hit( + match, + matcher.highlight(name), + runnable, + help=help_text, + ) diff --git a/src/textual/_text_area_theme.py b/src/textual/_text_area_theme.py new file mode 100644 index 0000000000..93bad81c85 --- /dev/null +++ b/src/textual/_text_area_theme.py @@ -0,0 +1,353 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +from rich.style import Style + +from textual.app import DEFAULT_COLORS +from textual.color import Color +from textual.design import DEFAULT_DARK_SURFACE + + +@dataclass +class TextAreaTheme: + """A theme for the `TextArea` widget. + + Allows theming the general widget (gutter, selections, cursor, and so on) and + mapping of tree-sitter tokens to Rich styles. + + For example, consider the following snippet from the `markdown.scm` highlight + query file. We've assigned the `heading_content` token type to the name `heading`. + + ``` + (heading_content) @heading + ``` + + Now, we can map this `heading` name to a Rich style, and it will be styled as + such in the `TextArea`, assuming a parser which returns a `heading_content` + node is used (as will be the case when language="markdown"). + + ``` + TextAreaTheme('my_theme', syntax_styles={'heading': Style(color='cyan', bold=True)}) + ``` + + We can register this theme with our `TextArea` using the [`TextArea.register_theme`][textual.widgets._text_area.TextArea.register_theme] method, + and headings in our markdown files will be styled bold cyan. + """ + + name: str + """The name of the theme.""" + + base_style: Style | None = None + """The background style of the text area. If `None` the parent style will be used.""" + + gutter_style: Style | None = None + """The style of the gutter. If `None`, a legible Style will be generated.""" + + cursor_style: Style | None = None + """The style of the cursor. If `None`, a legible Style will be generated.""" + + cursor_line_style: Style | None = None + """The style to apply to the line the cursor is on.""" + + cursor_line_gutter_style: Style | None = None + """The style to apply to the gutter of the line the cursor is on. If `None`, a legible Style will be + generated.""" + + bracket_matching_style: Style | None = None + """The style to apply to matching brackets. If `None`, a legible Style will be generated.""" + + selection_style: Style | None = None + """The style of the selection. If `None` a default selection Style will be generated.""" + + syntax_styles: dict[str, Style] = field(default_factory=dict) + """The mapping of tree-sitter names from the `highlight_query` to Rich styles.""" + + def __post_init__(self) -> None: + """Generate some styles if they haven't been supplied.""" + if self.base_style is None: + self.base_style = Style() + + if self.base_style.color is None: + self.base_style = Style(color="#f3f3f3", bgcolor=self.base_style.bgcolor) + + if self.base_style.bgcolor is None: + self.base_style = Style( + color=self.base_style.color, bgcolor=DEFAULT_DARK_SURFACE + ) + + assert self.base_style is not None + assert self.base_style.color is not None + assert self.base_style.bgcolor is not None + + if self.gutter_style is None: + self.gutter_style = self.base_style.copy() + + background_color = Color.from_rich_color(self.base_style.bgcolor) + if self.cursor_style is None: + self.cursor_style = Style( + color=background_color.rich_color, + bgcolor=background_color.inverse.rich_color, + ) + + if self.cursor_line_gutter_style is None and self.cursor_line_style is not None: + self.cursor_line_gutter_style = self.cursor_line_style.copy() + + if self.bracket_matching_style is None: + bracket_matching_background = background_color.blend( + background_color.inverse, factor=0.05 + ) + self.bracket_matching_style = Style( + bgcolor=bracket_matching_background.rich_color + ) + + if self.selection_style is None: + selection_background_color = background_color.blend( + DEFAULT_COLORS["dark"].primary, factor=0.75 + ) + self.selection_style = Style.from_color( + bgcolor=selection_background_color.rich_color + ) + + @classmethod + def get_builtin_theme(cls, theme_name: str) -> "TextAreaTheme" | None: + """Get a `TextAreaTheme` by name. + + Given a `theme_name`, return the corresponding `TextAreaTheme` object. + + Args: + theme_name: The name of the theme. + + Returns: + The `TextAreaTheme` corresponding to the name or `None` if the theme isn't + found. + """ + return _BUILTIN_THEMES.get(theme_name) + + def get_highlight(self, name: str) -> Style | None: + """Return the Rich style corresponding to the name defined in the tree-sitter + highlight query for the current theme. + + Args: + name: The name of the highlight. + + Returns: + The `Style` to use for this highlight, or `None` if no style. + """ + return self.syntax_styles.get(name) + + @classmethod + def builtin_themes(cls) -> list[TextAreaTheme]: + """Get a list of all builtin TextAreaThemes. + + Returns: + A list of all builtin TextAreaThemes. + """ + return list(_BUILTIN_THEMES.values()) + + @classmethod + def default(cls) -> TextAreaTheme: + """Get the default syntax theme. + + Returns: + The default TextAreaTheme (probably "monokai"). + """ + return _MONOKAI + + +_MONOKAI = TextAreaTheme( + name="monokai", + base_style=Style(color="#f8f8f2", bgcolor="#272822"), + gutter_style=Style(color="#90908a", bgcolor="#272822"), + cursor_style=Style(color="#272822", bgcolor="#f8f8f0"), + cursor_line_style=Style(bgcolor="#3e3d32"), + cursor_line_gutter_style=Style(color="#c2c2bf", bgcolor="#3e3d32"), + bracket_matching_style=Style(bgcolor="#838889", bold=True), + selection_style=Style(bgcolor="#65686a"), + syntax_styles={ + "string": Style(color="#E6DB74"), + "string.documentation": Style(color="#E6DB74"), + "comment": Style(color="#75715E"), + "keyword": Style(color="#F92672"), + "operator": Style(color="#F92672"), + "repeat": Style(color="#F92672"), + "exception": Style(color="#F92672"), + "include": Style(color="#F92672"), + "keyword.function": Style(color="#F92672"), + "keyword.return": Style(color="#F92672"), + "keyword.operator": Style(color="#F92672"), + "conditional": Style(color="#F92672"), + "number": Style(color="#AE81FF"), + "float": Style(color="#AE81FF"), + "class": Style(color="#A6E22E"), + "type.class": Style(color="#A6E22E"), + "function": Style(color="#A6E22E"), + "function.call": Style(color="#A6E22E"), + "method": Style(color="#A6E22E"), + "method.call": Style(color="#A6E22E"), + "boolean": Style(color="#66D9EF", italic=True), + "json.null": Style(color="#66D9EF", italic=True), + "regex.punctuation.bracket": Style(color="#F92672"), + "regex.operator": Style(color="#F92672"), + "html.end_tag_error": Style(color="red", underline=True), + "tag": Style(color="#F92672"), + "yaml.field": Style(color="#F92672", bold=True), + "json.label": Style(color="#F92672", bold=True), + "toml.type": Style(color="#F92672"), + "toml.datetime": Style(color="#AE81FF"), + "heading": Style(color="#F92672", bold=True), + "bold": Style(bold=True), + "italic": Style(italic=True), + "strikethrough": Style(strike=True), + "link": Style(color="#66D9EF", underline=True), + "inline_code": Style(color="#E6DB74"), + }, +) + +_DRACULA = TextAreaTheme( + name="dracula", + base_style=Style(color="#f8f8f2", bgcolor="#1E1F35"), + gutter_style=Style(color="#6272a4"), + cursor_style=Style(color="#282a36", bgcolor="#f8f8f0"), + cursor_line_style=Style(bgcolor="#282b45"), + cursor_line_gutter_style=Style(color="#c2c2bf", bgcolor="#282b45", bold=True), + bracket_matching_style=Style(bgcolor="#99999d", bold=True, underline=True), + selection_style=Style(bgcolor="#44475A"), + syntax_styles={ + "string": Style(color="#f1fa8c"), + "string.documentation": Style(color="#f1fa8c"), + "comment": Style(color="#6272a4"), + "keyword": Style(color="#ff79c6"), + "operator": Style(color="#ff79c6"), + "repeat": Style(color="#ff79c6"), + "exception": Style(color="#ff79c6"), + "include": Style(color="#ff79c6"), + "keyword.function": Style(color="#ff79c6"), + "keyword.return": Style(color="#ff79c6"), + "keyword.operator": Style(color="#ff79c6"), + "conditional": Style(color="#ff79c6"), + "number": Style(color="#bd93f9"), + "float": Style(color="#bd93f9"), + "class": Style(color="#50fa7b"), + "type.class": Style(color="#50fa7b"), + "function": Style(color="#50fa7b"), + "function.call": Style(color="#50fa7b"), + "method": Style(color="#50fa7b"), + "method.call": Style(color="#50fa7b"), + "boolean": Style(color="#bd93f9"), + "json.null": Style(color="#bd93f9"), + "regex.punctuation.bracket": Style(color="#ff79c6"), + "regex.operator": Style(color="#ff79c6"), + "html.end_tag_error": Style(color="#F83333", underline=True), + "tag": Style(color="#ff79c6"), + "yaml.field": Style(color="#ff79c6", bold=True), + "json.label": Style(color="#ff79c6", bold=True), + "toml.type": Style(color="#ff79c6"), + "toml.datetime": Style(color="#bd93f9"), + "heading": Style(color="#ff79c6", bold=True), + "bold": Style(bold=True), + "italic": Style(italic=True), + "strikethrough": Style(strike=True), + "link": Style(color="#bd93f9", underline=True), + "inline_code": Style(color="#f1fa8c"), + }, +) + +_DARK_VS = TextAreaTheme( + name="vscode_dark", + base_style=Style(color="#CCCCCC", bgcolor="#1F1F1F"), + gutter_style=Style(color="#6E7681", bgcolor="#1F1F1F"), + cursor_style=Style(color="#1e1e1e", bgcolor="#f0f0f0"), + cursor_line_style=Style(bgcolor="#2b2b2b"), + bracket_matching_style=Style(bgcolor="#3a3a3a", bold=True), + cursor_line_gutter_style=Style(color="#CCCCCC", bgcolor="#2b2b2b"), + selection_style=Style(bgcolor="#264F78"), + syntax_styles={ + "string": Style(color="#ce9178"), + "string.documentation": Style(color="#ce9178"), + "comment": Style(color="#6A9955"), + "keyword": Style(color="#569cd6"), + "operator": Style(color="#569cd6"), + "conditional": Style(color="#569cd6"), + "keyword.function": Style(color="#569cd6"), + "keyword.return": Style(color="#569cd6"), + "keyword.operator": Style(color="#569cd6"), + "repeat": Style(color="#569cd6"), + "exception": Style(color="#569cd6"), + "include": Style(color="#569cd6"), + "number": Style(color="#b5cea8"), + "float": Style(color="#b5cea8"), + "class": Style(color="#4EC9B0"), + "type.class": Style(color="#4EC9B0"), + "function": Style(color="#4EC9B0"), + "function.call": Style(color="#4EC9B0"), + "method": Style(color="#4EC9B0"), + "method.call": Style(color="#4EC9B0"), + "boolean": Style(color="#7DAF9C"), + "json.null": Style(color="#7DAF9C"), + "tag": Style(color="#EFCB43"), + "yaml.field": Style(color="#569cd6", bold=True), + "json.label": Style(color="#569cd6", bold=True), + "toml.type": Style(color="#569cd6"), + "heading": Style(color="#569cd6", bold=True), + "bold": Style(bold=True), + "italic": Style(italic=True), + "strikethrough": Style(strike=True), + "link": Style(color="#40A6FF", underline=True), + "inline_code": Style(color="#ce9178"), + "info_string": Style(color="#ce9178", bold=True, italic=True), + }, +) + +_GITHUB_LIGHT = TextAreaTheme( + name="github_light", + base_style=Style(color="#24292e", bgcolor="#f0f0f0"), + gutter_style=Style(color="#BBBBBB", bgcolor="#f0f0f0"), + cursor_style=Style(color="#fafbfc", bgcolor="#24292e"), + cursor_line_style=Style(bgcolor="#ebebeb"), + bracket_matching_style=Style(color="#24292e", underline=True), + cursor_line_gutter_style=Style(color="#A4A4A4", bgcolor="#ebebeb"), + selection_style=Style(bgcolor="#c8c8fa"), + syntax_styles={ + "string": Style(color="#093069"), + "string.documentation": Style(color="#093069"), + "comment": Style(color="#6a737d"), + "keyword": Style(color="#d73a49"), + "operator": Style(color="#0450AE"), + "conditional": Style(color="#CF222E"), + "keyword.function": Style(color="#CF222E"), + "keyword.return": Style(color="#CF222E"), + "keyword.operator": Style(color="#CF222E"), + "repeat": Style(color="#CF222E"), + "exception": Style(color="#CF222E"), + "include": Style(color="#CF222E"), + "number": Style(color="#d73a49"), + "float": Style(color="#d73a49"), + "parameter": Style(color="#24292e"), + "class": Style(color="#963800"), + "variable": Style(color="#e36209"), + "function": Style(color="#6639BB"), + "method": Style(color="#6639BB"), + "boolean": Style(color="#7DAF9C"), + "tag": Style(color="#6639BB"), + "yaml.field": Style(color="#6639BB"), + "json.label": Style(color="#6639BB"), + "toml.type": Style(color="#6639BB"), + "heading": Style(color="#24292e", bold=True), + "bold": Style(bold=True), + "italic": Style(italic=True), + "strikethrough": Style(strike=True), + "link": Style(color="#40A6FF", underline=True), + "inline_code": Style(color="#093069"), + }, +) + +_BUILTIN_THEMES = { + "monokai": _MONOKAI, + "dracula": _DRACULA, + "vscode_dark": _DARK_VS, + "github_light": _GITHUB_LIGHT, +} + +DEFAULT_THEME = TextAreaTheme.get_builtin_theme("monokai") +"""The default TextAreaTheme used by Textual.""" diff --git a/src/textual/_tree_sitter.py b/src/textual/_tree_sitter.py new file mode 100644 index 0000000000..01e300115c --- /dev/null +++ b/src/textual/_tree_sitter.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +try: + from tree_sitter import Language, Parser, Tree + from tree_sitter.binding import Query + from tree_sitter_languages import get_language, get_parser + + TREE_SITTER = True +except ImportError: + TREE_SITTER = False diff --git a/src/textual/_types.py b/src/textual/_types.py index 85eb27c421..669950c5a2 100644 --- a/src/textual/_types.py +++ b/src/textual/_types.py @@ -1,6 +1,12 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Union -from typing_extensions import Protocol +from typing_extensions import ( + Literal, + Protocol, + SupportsIndex, + get_args, + runtime_checkable, +) if TYPE_CHECKING: from rich.segment import Segment @@ -26,9 +32,15 @@ def post_message(self, message: "Message") -> bool: ... +class UnusedParameter: + """Helper type for a parameter that isn't specified in a method call.""" + + SegmentLines = List[List["Segment"]] CallbackType = Union[Callable[[], Awaitable[None]], Callable[[], None]] """Type used for arbitrary callables used in callbacks.""" +IgnoreReturnCallbackType = Union[Callable[[], Awaitable[Any]], Callable[[], Any]] +"""A callback which ignores the return type.""" WatchCallbackType = Union[ Callable[[], Awaitable[None]], Callable[[Any], Awaitable[None]], diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index edf587a3f9..d3653dc3d5 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -84,6 +84,13 @@ def parse_mouse_code(self, code: str) -> events.Event | None: return event return None + _reissued_sequence_debug_book: Callable[[str], None] | None = None + """INTERNAL USE ONLY! + + If this property is set to a callable, it will be called *instead* of + the reissued sequence being emitted as key events. + """ + def parse(self, on_token: TokenCallback) -> Generator[Awaitable, str, None]: ESC = "\x1b" read1 = self.read1 @@ -94,6 +101,9 @@ def parse(self, on_token: TokenCallback) -> Generator[Awaitable, str, None]: use_prior_escape = False def reissue_sequence_as_keys(reissue_sequence: str) -> None: + if self._reissued_sequence_debug_book is not None: + self._reissued_sequence_debug_book(reissue_sequence) + return for character in reissue_sequence: key_events = sequence_to_key_events(character) for event in key_events: diff --git a/src/textual/app.py b/src/textual/app.py index f505bdfb62..fda83b14a1 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -53,6 +53,7 @@ import rich.repr from rich import terminal_theme from rich.console import Console, RenderableType +from rich.control import Control from rich.protocol import is_renderable from rich.segment import Segment, Segments from rich.traceback import Traceback @@ -68,11 +69,13 @@ from ._context import message_hook as message_hook_context_var from ._event_broker import NoHandler, extract_handler_actions from ._path import CSSPathType, _css_path_type_as_list, _make_path_object_relative +from ._system_commands import SystemCommands from ._wait import wait_for_idle from ._worker_manager import WorkerManager from .actions import ActionParseResult, SkipAction from .await_remove import AwaitRemove from .binding import Binding, BindingType, _Bindings +from .command import CommandPalette, Provider from .css.query import NoMatches from .css.stylesheet import Stylesheet from .design import ColorSystem @@ -90,16 +93,17 @@ _get_unicode_name_from_key, ) from .messages import CallbackType -from .notifications import Notification, Notifications, SeverityLevel +from .notifications import Notification, Notifications, Notify, SeverityLevel from .reactive import Reactive from .renderables.blank import Blank from .screen import Screen, ScreenResultCallbackType, ScreenResultType from .widget import AwaitMount, Widget from .widgets._toast import ToastRack +from .worker import NoActiveWorker, get_current_worker if TYPE_CHECKING: from textual_dev.client import DevtoolsClient - from typing_extensions import Coroutine, TypeAlias + from typing_extensions import Coroutine, Literal, TypeAlias from ._types import MessageTarget @@ -238,6 +242,10 @@ def isatty(self) -> bool: # TODO: should this be configurable? return True + def fileno(self) -> int: + """Return invalid fileno.""" + return -1 + @rich.repr.auto class App(Generic[ReturnType], DOMNode): @@ -248,17 +256,14 @@ class App(Generic[ReturnType], DOMNode): and therefore takes priority in the event of a specificity clash.""" # Default (the lowest priority) CSS - DEFAULT_CSS: ClassVar[ - str - ] = """ + DEFAULT_CSS: ClassVar[str] + DEFAULT_CSS = """ App { background: $background; color: $text; } - *:disabled:can-focus { opacity: 0.7; - } """ @@ -308,17 +313,29 @@ class MyApp(App[None]): TITLE: str | None = None """A class variable to set the *default* title for the application. - To update the title while the app is running, you can set the [title][textual.app.App.title] attribute + To update the title while the app is running, you can set the [title][textual.app.App.title] attribute. + See also [the `Screen.TITLE` attribute][textual.screen.Screen.TITLE]. """ SUB_TITLE: str | None = None """A class variable to set the default sub-title for the application. To update the sub-title while the app is running, you can set the [sub_title][textual.app.App.sub_title] attribute. + See also [the `Screen.SUB_TITLE` attribute][textual.screen.Screen.SUB_TITLE]. + """ + + ENABLE_COMMAND_PALETTE: ClassVar[bool] = True + """Should the [command palette][textual.command.CommandPalette] be enabled for the application?""" + + COMMANDS: ClassVar[set[type[Provider]]] = {SystemCommands} + """Command providers used by the [command palette](/guide/command_palette). + + Should be a set of [command.Provider][textual.command.Provider] classes. """ BINDINGS: ClassVar[list[BindingType]] = [ - Binding("ctrl+c", "quit", "Quit", show=False, priority=True) + Binding("ctrl+c", "quit", "Quit", show=False, priority=True), + Binding("ctrl+backslash", "command_palette", show=False, priority=True), ] title: Reactive[str] = Reactive("", compute=False) @@ -371,6 +388,7 @@ def __init__( self._filters.append(DimFilter()) self.console = Console( + color_system=constants.COLOR_SYSTEM, file=_NullFile(), markup=True, highlight=False, @@ -399,6 +417,12 @@ def __init__( self._animate = self._animator.bind(self) self.mouse_position = Offset(0, 0) + self.cursor_position = Offset(0, 0) + """The position of the terminal cursor in screen-space. + + This can be set by widgets and is useful for controlling the + positioning of OS IME and emoji popup menus.""" + self._exception: Exception | None = None """The unhandled exception which is leading to the app shutting down, or None if the app is still running with no unhandled exceptions.""" @@ -425,12 +449,20 @@ def __init__( an empty string if it doesn't. Sub-titles are typically used to show the high-level state of the app, such as the current mode, or path to - the file being worker on. + the file being worked on. Assign a new value to this attribute to change the sub-title. The new value is always converted to string. """ + self.use_command_palette: bool = self.ENABLE_COMMAND_PALETTE + """A flag to say if the application should use the command palette. + + If set to `False` any call to + [`action_command_palette`][textual.app.App.action_command_palette] + will be ignored. + """ + self._logger = Logger(self._log) self._refresh_required = False @@ -474,11 +506,14 @@ def __init__( # Dev dependencies not installed pass else: - self.devtools = DevtoolsClient() + self.devtools = DevtoolsClient(constants.DEVTOOLS_HOST) self._devtools_redirector = StdoutRedirector(self.devtools) self._loop: asyncio.AbstractEventLoop | None = None self._return_value: ReturnType | None = None + """Internal attribute used to set the return value for the app.""" + self._return_code: int | None = None + """Internal attribute used to set the return code for the app.""" self._exit = False self._disable_tooltips = False self._disable_notifications = False @@ -497,8 +532,15 @@ def __init__( self._capture_print: WeakKeyDictionary[ MessageTarget, tuple[bool, bool] ] = WeakKeyDictionary() + """Registry of the MessageTargets which are capturing output at any given time.""" self._capture_stdout = _PrintCapture(self, stderr=False) + """File-like object capturing data written to stdout.""" self._capture_stderr = _PrintCapture(self, stderr=True) + """File-like object capturing data written to stderr.""" + self._original_stdout = sys.__stdout__ + """The original stdout stream (before redirection etc).""" + self._original_stderr = sys.__stderr__ + """The original stderr stream (before redirection etc).""" self.set_class(self.dark, "-dark-mode") self.set_class(not self.dark, "-light-mode") @@ -528,6 +570,23 @@ def return_value(self) -> ReturnType | None: """ return self._return_value + @property + def return_code(self) -> int | None: + """The return code with which the app exited. + + Non-zero codes indicate errors. + A value of 1 means the app exited with a fatal error. + If the app wasn't exited yet, this will be `None`. + + Example: + The return code can be used to exit the process via `sys.exit`. + ```py + my_app.run() + sys.exit(my_app.return_code) + ``` + """ + return self._return_code + @property def children(self) -> Sequence["Widget"]: """A view onto the app's immediate children. @@ -647,17 +706,27 @@ def _screen_stack(self) -> list[Screen]: """ return self._screen_stacks[self._current_mode] + @property + def current_mode(self) -> str: + """The name of the currently active mode.""" + return self._current_mode + def exit( - self, result: ReturnType | None = None, message: RenderableType | None = None + self, + result: ReturnType | None = None, + return_code: int = 0, + message: RenderableType | None = None, ) -> None: """Exit the app, and return the supplied result. Args: result: Return value. + return_code: The return code. Use non-zero values for error codes. message: Optional message to display on exit. """ self._exit = True self._return_value = result + self._return_code = return_code self.post_message(messages.ExitApp()) if message: self._exit_renderables.append(message) @@ -1092,14 +1161,27 @@ def _flush(self, stderr: bool = False) -> None: self._devtools_redirector.flush() def _print(self, text: str, stderr: bool = False) -> None: - """Called with capture print. + """Called with captured print. + + Dispatches printed content to appropriate destinations: devtools, + widgets currently capturing output, stdout/stderr. Args: text: Text that has been printed. stderr: True if the print was to stderr, or False for stdout. """ if self._devtools_redirector is not None: - self._devtools_redirector.write(text) + current_frame = inspect.currentframe() + self._devtools_redirector.write( + text, current_frame.f_back if current_frame is not None else None + ) + + # If we're in headless mode, we want printed text to still reach stdout/stderr. + if self.is_headless: + target_stream = self._original_stderr if stderr else self._original_stdout + target_stream.write(text) + + # Send Print events to all widgets that are currently capturing output. for target, (_stdout, _stderr) in self._capture_print.items(): if (_stderr and stderr) or (_stdout and not stderr): target.post_message(events.Print(text, stderr=stderr)) @@ -1109,7 +1191,7 @@ def begin_capture_print( ) -> None: """Capture content that is printed (or written to stdout / stderr). - If printing is captured, the `target` will be send an [events.Print][textual.events.Print] message. + If printing is captured, the `target` will be sent an [events.Print][textual.events.Print] message. Args: target: The widget where print content will be sent. @@ -1139,9 +1221,13 @@ async def run_test( notifications: bool = False, message_hook: Callable[[Message], None] | None = None, ) -> AsyncGenerator[Pilot, None]: - """An asynchronous context manager for testing app. + """An asynchronous context manager for testing apps. + + !!! tip - Use this to run your app in "headless" (no output) mode and driver the app via a [Pilot][textual.pilot.Pilot] object. + See the guide for [testing](/guide/testing) Textual apps. + + Use this to run your app in "headless" mode (no output) and drive the app via a [Pilot][textual.pilot.Pilot] object. Example: @@ -1157,7 +1243,8 @@ async def run_test( or None to auto-detect. tooltips: Enable tooltips when testing. notifications: Enable notifications when testing. - message_hook: An optional callback that will called with every message going through the app. + message_hook: An optional callback that will be called each time any message arrives at any + message pump in the app. """ from .pilot import Pilot @@ -1170,7 +1257,7 @@ def on_app_ready() -> None: """Called when app is ready to process events.""" app_ready_event.set() - async def run_app(app) -> None: + async def run_app(app: App) -> None: if message_hook is not None: message_hook_context_var.set(message_hook) app._loop = asyncio.get_running_loop() @@ -1509,19 +1596,19 @@ def mount_all( return self.mount(*widgets, before=before, after=after) def _init_mode(self, mode: str) -> None: - """Do internal initialisation of a new screen stack mode.""" + """Do internal initialisation of a new screen stack mode. + + Args: + mode: Name of the mode. + """ stack = self._screen_stacks.get(mode, []) if not stack: _screen = self.MODES[mode] - if callable(_screen): - screen, _ = self._get_screen(_screen()) - else: - screen, _ = self._get_screen(self.MODES[mode]) + new_screen: Screen | str = _screen() if callable(_screen) else _screen + screen, _ = self._get_screen(new_screen) stack.append(screen) - self._load_screen_css(screen) - self._screen_stacks[mode] = stack def switch_mode(self, mode: str) -> None: @@ -1676,7 +1763,10 @@ def _load_screen_css(self, screen: Screen): screen_css_path = f"{screen.__class__.__name__}" if not self.stylesheet.has_source(screen_css_path): self.stylesheet.add_source( - screen.CSS, path=screen_css_path, is_default_css=False + screen.CSS, + path=screen_css_path, + is_default_css=False, + scope=screen._css_type_name if screen.SCOPED_CSS else "", ) update = True if update: @@ -1703,37 +1793,83 @@ def _replace_screen(self, screen: Screen) -> Screen: self.log.system(f"{screen} REMOVED") return screen + @overload def push_screen( self, screen: Screen[ScreenResultType] | str, callback: ScreenResultCallbackType[ScreenResultType] | None = None, + wait_for_dismiss: Literal[False] = False, ) -> AwaitMount: + ... + + @overload + def push_screen( + self, + screen: Screen[ScreenResultType] | str, + callback: ScreenResultCallbackType[ScreenResultType] | None = None, + wait_for_dismiss: Literal[True] = True, + ) -> asyncio.Future[ScreenResultType]: + ... + + def push_screen( + self, + screen: Screen[ScreenResultType] | str, + callback: ScreenResultCallbackType[ScreenResultType] | None = None, + wait_for_dismiss: bool = False, + ) -> AwaitMount | asyncio.Future[ScreenResultType]: """Push a new [screen](/guide/screens) on the screen stack, making it the current screen. Args: screen: A Screen instance or the name of an installed screen. callback: An optional callback function that will be called if the screen is [dismissed][textual.screen.Screen.dismiss] with a result. + wait_for_dismiss: If `True`, awaiting this method will return the dismiss value from the screen. When set to `False`, awaiting + this method will wait for the screen to be mounted. Note that `wait_for_dismiss` should only be set to `True` when running in a worker. + + Raises: + NoActiveWorker: If using `wait_for_dismiss` outside of a worker. Returns: - An optional awaitable that awaits the mounting of the screen and its children. + An optional awaitable that awaits the mounting of the screen and its children, or an asyncio Future + to await the result of the screen. """ if not isinstance(screen, (Screen, str)): raise TypeError( f"push_screen requires a Screen instance or str; not {screen!r}" ) + try: + loop = asyncio.get_running_loop() + except RuntimeError: + # Mainly for testing, when push_screen isn't called in an async context + future: asyncio.Future[ScreenResultType] = asyncio.Future() + else: + future = loop.create_future() + if self._screen_stack: self.screen.post_message(events.ScreenSuspend()) self.screen.refresh() next_screen, await_mount = self._get_screen(screen) - next_screen._push_result_callback( - self.screen if self._screen_stack else None, callback - ) + try: + message_pump = active_message_pump.get() + except LookupError: + message_pump = self.app + + next_screen._push_result_callback(message_pump, callback, future) self._load_screen_css(next_screen) self._screen_stack.append(next_screen) + self.stylesheet.update(next_screen) next_screen.post_message(events.ScreenResume()) self.log.system(f"{self.screen} is current (PUSHED)") - return await_mount + if wait_for_dismiss: + try: + get_current_worker() + except NoActiveWorker: + raise NoActiveWorker( + "push_screen must be run from a worker when `wait_for_dismiss` is True" + ) from None + return future + else: + return await_mount def switch_screen(self, screen: Screen | str) -> AwaitMount: """Switch to another [screen](/guide/screens) by replacing the top of the screen stack with a new screen. @@ -1833,7 +1969,6 @@ def pop_screen(self) -> Screen[object]: ) previous_screen = self._replace_screen(screen_stack.pop()) previous_screen._pop_result_callback() - self.screen._screen_resized(self.size) self.screen.post_message(events.ScreenResume()) self.log.system(f"{self.screen} is active") return previous_screen @@ -1914,6 +2049,7 @@ def _handle_exception(self, error: Exception) -> None: Args: error: An exception instance. """ + self._return_code = 1 # If we're running via pilot and this is the first exception encountered, # take note of it so that we can re-raise for test frameworks later. if self.is_headless and self._exception is None: @@ -1982,13 +2118,20 @@ async def _process_messages( 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: + _log_path = os.path.abspath(constants.LOG_FILE) + self.log.system(f"Writing logs to {_log_path!r}") try: if self.css_path: self.stylesheet.read_all(self.css_path) - for path, css, tie_breaker in self._get_default_css(): + for path, css, tie_breaker, scope in self._get_default_css(): self.stylesheet.add_source( - css, path=path, is_default_css=True, tie_breaker=tie_breaker + css, + path=path, + is_default_css=True, + tie_breaker=tie_breaker, + scope=scope, ) if self.CSS: try: @@ -2022,6 +2165,7 @@ async def invoke_ready_callback() -> None: try: try: await self._dispatch_message(events.Compose()) + default_screen = self.screen await self._dispatch_message(events.Mount()) self.check_idle() finally: @@ -2030,7 +2174,8 @@ async def invoke_ready_callback() -> None: Reactive._initialize_object(self) self.stylesheet.update(self) - self.refresh() + if self.screen is not default_screen: + self.stylesheet.update(default_screen) await self.animator.start() @@ -2281,12 +2426,12 @@ async def _shutdown(self) -> None: await self._dispatch_message(events.Unmount()) - if self.devtools is not None and self.devtools.is_connected: - await self._disconnect_devtools() - if self._driver is not None: self._driver.close() + if self.devtools is not None and self.devtools.is_connected: + await self._disconnect_devtools() + self._print_error_renderables() if constants.SHOW_RETURN: @@ -2347,7 +2492,11 @@ def _display(self, screen: Screen, renderable: RenderableType | None) -> None: try: try: if isinstance(renderable, CompositorUpdate): + cursor_x, cursor_y = self.cursor_position terminal_sequence = renderable.render_segments(console) + terminal_sequence += Control.move_to( + cursor_x, cursor_y + ).segment.text else: segments = console.render(renderable) terminal_sequence = console._render_buffer(segments) @@ -2357,7 +2506,9 @@ def _display(self, screen: Screen, renderable: RenderableType | None) -> None: self._driver.write(terminal_sequence) finally: self._end_update() + self._driver.flush() + finally: self.post_display_hook() @@ -2890,18 +3041,20 @@ def notify( title: str = "", severity: SeverityLevel = "information", timeout: float = Notification.timeout, - ) -> Notification: + ) -> None: """Create a notification. + !!! tip + + This method is thread-safe. + + Args: message: The message for the notification. title: The title for the notification. severity: The severity of the notification. timeout: The timeout for the notification. - Returns: - The new notification. - The `notify` method is used to create an application-wide notification, shown in a [`Toast`][textual.widgets._toast.Toast], normally originating in the bottom right corner of the display. @@ -2936,11 +3089,14 @@ def notify( ``` """ notification = Notification(message, title, severity, timeout) - self._notifications.add(notification) + self.post_message(Notify(notification)) + + def _on_notify(self, event: Notify) -> None: + """Handle notification message.""" + self._notifications.add(event.notification) self._refresh_notifications() - return notification - def unnotify(self, notification: Notification, refresh: bool = True) -> None: + def _unnotify(self, notification: Notification, refresh: bool = True) -> None: """Remove a notification from the notification collection. Args: @@ -2955,3 +3111,8 @@ def clear_notifications(self) -> None: """Clear all the current notifications.""" self._notifications.clear() self._refresh_notifications() + + def action_command_palette(self) -> None: + """Show the Textual command palette.""" + if self.use_command_palette and not CommandPalette.is_open(self): + self.push_screen(CommandPalette(), callback=self.call_next) diff --git a/src/textual/command.py b/src/textual/command.py new file mode 100644 index 0000000000..d45d941d85 --- /dev/null +++ b/src/textual/command.py @@ -0,0 +1,999 @@ +"""The Textual command palette. + +See the guide on the [Command Palette](../guide/command_palette.md) for full details. + +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from asyncio import CancelledError, Queue, Task, TimeoutError, wait, wait_for +from dataclasses import dataclass +from functools import total_ordering +from time import monotonic +from typing import TYPE_CHECKING, Any, AsyncGenerator, AsyncIterator, ClassVar + +import rich.repr +from rich.align import Align +from rich.console import Group, RenderableType +from rich.emoji import Emoji +from rich.style import Style +from rich.text import Text +from rich.traceback import Traceback +from typing_extensions import Final, TypeAlias + +from . import on, work +from ._asyncio import create_task +from .binding import Binding, BindingType +from .containers import Horizontal, Vertical +from .events import Click, Mount +from .fuzzy import Matcher +from .reactive import var +from .screen import ModalScreen, Screen +from .timer import Timer +from .types import CallbackType, IgnoreReturnCallbackType +from .widget import Widget +from .widgets import Button, Input, LoadingIndicator, OptionList, Static +from .widgets.option_list import Option +from .worker import get_current_worker + +if TYPE_CHECKING: + from .app import App, ComposeResult + +__all__ = [ + "CommandPalette", + "Hit", + "Hits", + "Matcher", + "Provider", +] + + +@dataclass +class Hit: + """Holds the details of a single command search hit.""" + + score: float + """The score of the command hit. + + The value should be between 0 (no match) and 1 (complete match). + """ + + match_display: RenderableType + """A string or Rich renderable representation of the hit.""" + + command: IgnoreReturnCallbackType + """The function to call when the command is chosen.""" + + text: str | None = None + """The command text associated with the hit, as plain text. + + If `match_display` is not simple text, this attribute should be provided by the + [Provider][textual.command.Provider] object. + """ + + help: str | None = None + """Optional help text for the command.""" + + def __lt__(self, other: object) -> bool: + if isinstance(other, Hit): + return self.score < other.score + return NotImplemented + + def __eq__(self, other: object) -> bool: + if isinstance(other, Hit): + return self.score == other.score + return NotImplemented + + def __post_init__(self) -> None: + """Ensure 'text' is populated.""" + if self.text is None: + if isinstance(self.match_display, str): + self.text = self.match_display + elif isinstance(self.match_display, Text): + self.text = self.match_display.plain + else: + raise ValueError( + "A value for 'text' is required if 'match_display' is not a str or Text" + ) + + +Hits: TypeAlias = AsyncIterator[Hit] +"""Return type for the command provider's `search` method.""" + + +class Provider(ABC): + """Base class for command palette command providers. + + To create new command provider, inherit from this class and implement + [`search`][textual.command.Provider.search]. + """ + + def __init__(self, screen: Screen[Any], match_style: Style | None = None) -> None: + """Initialise the command provider. + + Args: + screen: A reference to the active screen. + """ + self.__screen = screen + self.__match_style = match_style + self._init_task: Task | None = None + self._init_success = False + + @property + def focused(self) -> Widget | None: + """The currently-focused widget in the currently-active screen in the application. + + If no widget has focus this will be `None`. + """ + return self.__screen.focused + + @property + def screen(self) -> Screen[object]: + """The currently-active screen in the application.""" + return self.__screen + + @property + def app(self) -> App[object]: + """A reference to the application.""" + return self.__screen.app + + @property + def match_style(self) -> Style | None: + """The preferred style to use when highlighting matching portions of the [`match_display`][textual.command.Hit.match_display].""" + return self.__match_style + + def matcher(self, user_input: str, case_sensitive: bool = False) -> Matcher: + """Create a [fuzzy matcher][textual.fuzzy.Matcher] for the given user input. + + Args: + user_input: The text that the user has input. + case_sensitive: Should matching be case sensitive? + + Returns: + A [fuzzy matcher][textual.fuzzy.Matcher] object for matching against candidate hits. + """ + return Matcher( + user_input, match_style=self.match_style, case_sensitive=case_sensitive + ) + + def _post_init(self) -> None: + """Internal method to run post init task.""" + + async def post_init_task() -> None: + """Wrapper to post init that runs in a task.""" + try: + await self.startup() + except Exception: + self.app.log.error(Traceback()) + else: + self._init_success = True + + self._init_task = create_task(post_init_task()) + + async def _wait_init(self) -> None: + """Wait for initialization.""" + if self._init_task is not None: + await self._init_task + + async def startup(self) -> None: + """Called after the Provider is initialized, but before any calls to `search`.""" + + async def _search(self, query: str) -> Hits: + """Internal method to perform search. + + Args: + query: The user input to be matched. + + Yields: + Instances of [`Hit`][textual.command.Hit]. + """ + await self._wait_init() + if self._init_success: + hits = self.search(query) + async for hit in hits: + yield hit + + @abstractmethod + async def search(self, query: str) -> Hits: + """A request to search for commands relevant to the given query. + + Args: + query: The user input to be matched. + + Yields: + Instances of [`Hit`][textual.command.Hit]. + """ + yield NotImplemented + + async def _shutdown(self) -> None: + """Internal method to call shutdown and log errors.""" + try: + await self.shutdown() + except Exception: + self.app.log.error(Traceback()) + + async def shutdown(self) -> None: + """Called when the Provider is shutdown. + + Use this method to perform an cleanup, if required. + + """ + + +@rich.repr.auto +@total_ordering +class Command(Option): + """Class that holds a command in the [`CommandList`][textual.command.CommandList].""" + + def __init__( + self, + prompt: RenderableType, + command: Hit, + id: str | None = None, + disabled: bool = False, + ) -> None: + """Initialise the option. + + Args: + prompt: The prompt for the option. + command: The details of the command associated with the option. + id: The optional ID for the option. + disabled: The initial enabled/disabled state. Enabled by default. + """ + super().__init__(prompt, id, disabled) + self.command = command + """The details of the command associated with the option.""" + + def __lt__(self, other: object) -> bool: + if isinstance(other, Command): + return self.command < other.command + return NotImplemented + + def __eq__(self, other: object) -> bool: + if isinstance(other, Command): + return self.command == other.command + return NotImplemented + + +class CommandList(OptionList, can_focus=False): + """The command palette command list.""" + + DEFAULT_CSS = """ + CommandList { + visibility: hidden; + border-top: blank; + border-bottom: hkey $primary; + border-left: none; + border-right: none; + height: auto; + max-height: 70vh; + background: $panel; + } + + CommandList:focus { + border: blank; + } + + CommandList.--visible { + visibility: visible; + } + + CommandList.--populating { + border-bottom: none; + } + + CommandList > .option-list--option-highlighted { + background: $accent; + } + + CommandList > .option-list--option { + padding-left: 1; + } + """ + + +class SearchIcon(Static, inherit_css=False): + """Widget for displaying a search icon before the command input.""" + + DEFAULT_CSS = """ + SearchIcon { + margin-left: 1; + margin-top: 1; + width: 2; + } + """ + + icon: var[str] = var(Emoji.replace(":magnifying_glass_tilted_right:")) + """The icon to display.""" + + def render(self) -> RenderableType: + """Render the icon. + + Returns: + The icon renderable. + """ + return self.icon + + +class CommandInput(Input): + """The command palette input control.""" + + DEFAULT_CSS = """ + CommandInput, CommandInput:focus { + border: blank; + width: 1fr; + background: $panel; + padding-left: 0; + } + """ + + +class CommandPalette(ModalScreen[CallbackType], inherit_css=False): + """The Textual command palette.""" + + COMPONENT_CLASSES: ClassVar[set[str]] = { + "command-palette--help-text", + "command-palette--highlight", + } + """ + | Class | Description | + | :- | :- | + | `command-palette--help-text` | Targets the help text of a matched command. | + | `command-palette--highlight` | Targets the highlights of a matched command. | + """ + + DEFAULT_CSS = """ + CommandPalette { + background: $background 30%; + align-horizontal: center; + } + + CommandPalette > .command-palette--help-text { + background: transparent; + color: $text-muted; + } + + CommandPalette:dark > .command-palette--highlight { + text-style: bold; + color: $warning; + } + CommandPalette > .command-palette--highlight { + text-style: bold; + color: $warning-darken-2; + } + + CommandPalette > Vertical { + margin-top: 3; + width: 90%; + height: 100%; + visibility: hidden; + } + + CommandPalette #--input { + height: auto; + visibility: visible; + border: hkey $primary; + background: $panel; + } + + CommandPalette #--input.--list-visible { + border-bottom: none; + } + + CommandPalette #--input Label { + margin-top: 1; + margin-left: 1; + } + + CommandPalette #--input Button { + min-width: 7; + margin-right: 1; + } + + CommandPalette #--results { + overlay: screen; + height: auto; + } + + CommandPalette LoadingIndicator { + height: auto; + visibility: hidden; + background: $panel; + border-bottom: hkey $primary; + } + + CommandPalette LoadingIndicator.--visible { + visibility: visible; + } + """ + + BINDINGS: ClassVar[list[BindingType]] = [ + Binding("ctrl+end, shift+end", "command_list('last')", show=False), + Binding("ctrl+home, shift+home", "command_list('first')", show=False), + Binding("down", "cursor_down", show=False), + Binding("escape", "escape", "Exit the command palette"), + Binding("pagedown", "command_list('page_down')", show=False), + Binding("pageup", "command_list('page_up')", show=False), + Binding("up", "command_list('cursor_up')", show=False), + ] + """ + | Key(s) | Description | + | :- | :- | + | ctrl+end, shift+end | Jump to the last available commands. | + | ctrl+home, shift+home | Jump to the first available commands. | + | down | Navigate down through the available commands. | + | escape | Exit the command palette. | + | pagedown | Navigate down a page through the available commands. | + | pageup | Navigate up a page through the available commands. | + | up | Navigate up through the available commands. | + """ + + run_on_select: ClassVar[bool] = True + """A flag to say if a command should be run when selected by the user. + + If `True` then when a user hits `Enter` on a command match in the result + list, or if they click on one with the mouse, the command will be + selected and run. If set to `False` the input will be filled with the + command and then `Enter` should be pressed on the keyboard or the 'go' + button should be pressed. + """ + + _list_visible: var[bool] = var(False, init=False) + """Internal reactive to toggle the visibility of the command list.""" + + _show_busy: var[bool] = var(False, init=False) + """Internal reactive to toggle the visibility of the busy indicator.""" + + _calling_screen: var[Screen[Any] | None] = var(None) + """A record of the screen that was active when we were called.""" + + _PALETTE_ID: Final[str] = "--command-palette" + """The internal ID for the command palette.""" + + def __init__(self) -> None: + """Initialise the command palette.""" + super().__init__(id=self._PALETTE_ID) + self._selected_command: Hit | None = None + """The command that was selected by the user.""" + self._busy_timer: Timer | None = None + """Keeps track of if there's a busy indication timer in effect.""" + self._no_matches_timer: Timer | None = None + """Keeps track of if there are 'No matches found' message waiting to be displayed.""" + self._providers: list[Provider] = [] + """List of Provider instances involved in searches.""" + + @staticmethod + def is_open(app: App) -> bool: + """Is the command palette current open? + + Args: + app: The app to test. + + Returns: + `True` if the command palette is currently open, `False` if not. + """ + return app.screen.id == CommandPalette._PALETTE_ID + + @property + def _provider_classes(self) -> set[type[Provider]]: + """The currently available command providers. + + This is a combination of the command providers defined [in the + application][textual.app.App.COMMANDS] and those [defined in + the current screen][textual.screen.Screen.COMMANDS]. + """ + return ( + set() + if self._calling_screen is None + else self.app.COMMANDS | self._calling_screen.COMMANDS + ) + + def compose(self) -> ComposeResult: + """Compose the command palette. + + Returns: + The content of the screen. + """ + with Vertical(): + with Horizontal(id="--input"): + yield SearchIcon() + yield CommandInput(placeholder="Command Palette Search...") + if not self.run_on_select: + yield Button("\u25b6") + with Vertical(id="--results"): + yield CommandList() + yield LoadingIndicator() + + def _on_click(self, event: Click) -> None: + """Handle the click event. + + Args: + event: The click event. + + This method is used to allow clicking on the 'background' as a + method of dismissing the palette. + """ + if self.get_widget_at(event.screen_x, event.screen_y)[0] is self: + self.workers.cancel_all() + self.dismiss() + + def on_mount(self, _: Mount) -> None: + """Capture the calling screen.""" + self._calling_screen = self.app.screen_stack[-2] + + match_style = self.get_component_rich_style( + "command-palette--highlight", partial=True + ) + + assert self._calling_screen is not None + self._providers = [ + provider_class(self._calling_screen, match_style) + for provider_class in self._provider_classes + ] + for provider in self._providers: + provider._post_init() + + async def on_unmount(self) -> None: + """Shutdown providers when command palette is closed.""" + if self._providers: + await wait( + [create_task(provider._shutdown()) for provider in self._providers], + ) + self._providers.clear() + + def _stop_busy_countdown(self) -> None: + """Stop any busy countdown that's in effect.""" + if self._busy_timer is not None: + self._busy_timer.stop() + self._busy_timer = None + + _BUSY_COUNTDOWN: Final[float] = 0.5 + """How many seconds to wait for commands to come in before showing we're busy.""" + + def _start_busy_countdown(self) -> None: + """Start a countdown to showing that we're busy searching.""" + self._stop_busy_countdown() + + def _become_busy() -> None: + if self._list_visible: + self._show_busy = True + + self._busy_timer = self.set_timer(self._BUSY_COUNTDOWN, _become_busy) + + def _stop_no_matches_countdown(self) -> None: + """Stop any 'No matches' countdown that's in effect.""" + if self._no_matches_timer is not None: + self._no_matches_timer.stop() + self._no_matches_timer = None + + _NO_MATCHES_COUNTDOWN: Final[float] = 0.5 + """How many seconds to wait before showing 'No matches found'.""" + + def _start_no_matches_countdown(self) -> None: + """Start a countdown to showing that there are no matches for the query. + + Adds a 'No matches found' option to the command list after `_NO_MATCHES_COUNTDOWN` seconds. + """ + self._stop_no_matches_countdown() + + def _show_no_matches() -> None: + command_list = self.query_one(CommandList) + command_list.add_option( + Option( + Align.center(Text("No matches found")), + disabled=True, + id=self._NO_MATCHES, + ) + ) + self._list_visible = True + + self._no_matches_timer = self.set_timer( + self._NO_MATCHES_COUNTDOWN, + _show_no_matches, + ) + + def _watch__list_visible(self) -> None: + """React to the list visible flag being toggled.""" + self.query_one(CommandList).set_class(self._list_visible, "--visible") + self.query_one("#--input", Horizontal).set_class( + self._list_visible, "--list-visible" + ) + if not self._list_visible: + self._show_busy = False + + async def _watch__show_busy(self) -> None: + """React to the show busy flag being toggled. + + This watcher adds or removes a busy indication depending on the + flag's state. + """ + self.query_one(LoadingIndicator).set_class(self._show_busy, "--visible") + self.query_one(CommandList).set_class(self._show_busy, "--populating") + + @staticmethod + async def _consume(hits: Hits, commands: Queue[Hit]) -> None: + """Consume a source of matching commands, feeding the given command queue. + + Args: + hits: The hits to consume. + commands: The command queue to feed. + """ + async for hit in hits: + await commands.put(hit) + + async def _search_for(self, search_value: str) -> AsyncGenerator[Hit, bool]: + """Search for a given search value amongst all of the command providers. + + Args: + search_value: The value to search for. + + Yields: + The hits made amongst the registered command providers. + """ + + # Set up a queue to stream in the command hits from all the providers. + commands: Queue[Hit] = Queue() + + # Fire up an instance of each command provider, inside a task, and + # have them go start looking for matches. + searches = [ + create_task( + self._consume( + provider._search(search_value), + commands, + ) + ) + for provider in self._providers + ] + + # Set up a delay for showing that we're busy. + self._start_busy_countdown() + + # Assume the search isn't aborted. + aborted = False + + # Now, while there's some task running... + while not aborted and any(not search.done() for search in searches): + try: + # ...briefly wait for something on the stack. If we get + # something yield it up to our caller. + aborted = yield await wait_for(commands.get(), 0.1) + except TimeoutError: + # A timeout is fine. We're just going to go back round again + # and see if anything else has turned up. + pass + except CancelledError: + # A cancelled error means things are being aborted. + aborted = True + else: + # There was no timeout, which means that we managed to yield + # up that command; we're done with it so let the queue know. + commands.task_done() + + # Check through all the finished searches, see if any have + # exceptions, and log them. In most other circumstances we'd + # re-raise the exception and quit the application, but the decision + # has been made to find and log exceptions with command providers. + # + # https://github.com/Textualize/textual/pull/3058#discussion_r1310051855 + for search in searches: + if search.done(): + exception = search.exception() + if exception is not None: + self.log.error( + Traceback.from_exception( + type(exception), exception, exception.__traceback__ + ) + ) + + # Having finished the main processing loop, we're not busy any more. + # Anything left in the queue (see next) will fall out more or less + # instantly. If we're aborted, that means a fresh search is incoming + # and it'll have cleaned up the countdown anyway, so don't do that + # here as they'll be a clash. + if not aborted: + self._stop_busy_countdown() + + # If all the providers are pretty fast it could be that we've reached + # this point but the queue isn't empty yet. So here we flush the + # queue of anything left. + while not aborted and not commands.empty(): + aborted = yield await commands.get() + + # If we were aborted, ensure that all of the searches are cancelled. + if aborted: + for search in searches: + search.cancel() + + @staticmethod + def _sans_background(style: Style) -> Style: + """Returns the given style minus the background color. + + Args: + style: The style to remove the color from. + + Returns: + The given style, minus its background. + """ + # Here we're pulling out all of the styles *minus* the background. + # This should probably turn into a utility method on Style + # eventually. The reason for this is we want the developer to be + # able to style the help text with a component class, but we want + # the background to always be the background at any given moment in + # the context of an OptionList. At the moment this act of copying + # sans bgcolor seems to be the only way to achieve this. + return Style( + blink2=style.blink2, + blink=style.blink, + bold=style.bold, + color=style.color, + conceal=style.conceal, + dim=style.dim, + encircle=style.encircle, + frame=style.frame, + italic=style.italic, + link=style.link, + overline=style.overline, + reverse=style.reverse, + strike=style.strike, + underline2=style.underline2, + underline=style.underline, + ) + + def _refresh_command_list( + self, command_list: CommandList, commands: list[Command], clear_current: bool + ) -> None: + """Refresh the command list. + + Args: + command_list: The widget that shows the list of commands. + commands: The commands to show in the widget. + clear_current: Should the current content of the list be cleared first? + """ + # For the moment, this is a fairly naive approach to populating the + # command list with a sorted list of commands. Every time we add a + # new one we're nuking the list of options and populating them + # again. If this turns out to not be a great approach, we may try + # and get a lot smarter with this (ideally OptionList will grow a + # method to sort its content in an efficient way; but for now we'll + # go with "worse is better" wisdom). + highlighted = ( + command_list.get_option_at_index(command_list.highlighted) + if command_list.highlighted is not None and not clear_current + else None + ) + command_list.clear_options().add_options(sorted(commands, reverse=True)) + if highlighted is not None: + command_list.highlighted = command_list.get_option_index(highlighted.id) + self._list_visible = bool(command_list.option_count) + + _RESULT_BATCH_TIME: Final[float] = 0.25 + """How long to wait before adding commands to the command list.""" + + _NO_MATCHES: Final[str] = "--no-matches" + """The ID to give the disabled option that shows there were no matches.""" + + @work(exclusive=True) + async def _gather_commands(self, search_value: str) -> None: + """Gather up all of the commands that match the search value. + + Args: + search_value: The value to search for. + """ + + # We'll potentially use the help text style a lot so let's grab it + # the once for use in the loop further down. + help_style = self._sans_background( + self.get_component_rich_style("command-palette--help-text") + ) + + # The list to hold on to the commands we've gathered from the + # command providers. + gathered_commands: list[Command] = [] + + # Get a reference to the widget that we're going to drop the + # (display of) commands into. + command_list = self.query_one(CommandList) + + # If there's just one option in the list, and it's the item that + # tells the user there were no matches, let's remove that. We're + # starting a new search so we don't want them thinking there's no + # matches already. + if ( + command_list.option_count == 1 + and command_list.get_option_at_index(0).id == self._NO_MATCHES + ): + command_list.remove_option(self._NO_MATCHES) + + # Each command will receive a sequential ID. This is going to be + # used to find commands back again when we update the visible list + # and want to settle the selection back on the command it was on. + command_id = 0 + + # We're going to be checking in on the worker as we loop around, so + # grab a reference to that. + worker = get_current_worker() + + # Reset busy mode. + self._show_busy = False + + # A flag to keep track of if the current content of the command hit + # list needs to be cleared. The initial clear *should* be in + # `_input`, but doing so caused an unsightly "flash" of the list; so + # here we sacrifice "correct" code for a better-looking UI. + clear_current = True + + # We're going to batch updates over time, so start off pretending + # we've just done an update. + last_update = monotonic() + + # Kick off the search, grabbing the iterator. + search_routine = self._search_for(search_value) + search_results = search_routine.__aiter__() + + # We're going to be doing the send/await dance in this code, so we + # need to grab the first yielded command to start things off. + try: + hit = await search_results.__anext__() + except StopAsyncIteration: + # We've been stopped before we've even really got going, likely + # because the user is very quick on the keyboard. + hit = None + + while hit: + # Turn the command into something for display, and add it to the + # list of commands that have been gathered so far. + prompt = hit.match_display + if hit.help: + prompt = Group(prompt, Text(hit.help, style=help_style)) + gathered_commands.append(Command(prompt, hit, id=str(command_id))) + + # Before we go making any changes to the UI, we do a quick + # double-check that the worker hasn't been cancelled. There's + # little point in doing UI work on a value that isn't needed any + # more. + if worker.is_cancelled: + break + + # Having made it this far, it's safe to update the list of + # commands that match the input. Note that we batch up the + # results and only refresh the list once every so often; this + # helps reduce how much UI work needs to be done, but at the + # same time we keep the update frequency often enough so that it + # looks like things are moving along. + now = monotonic() + if (now - last_update) > self._RESULT_BATCH_TIME: + self._refresh_command_list( + command_list, gathered_commands, clear_current + ) + clear_current = False + last_update = now + + # Bump the ID. + command_id += 1 + + # Finally, get the available command from the incoming queue; + # note that we send the worker cancelled status down into the + # search method. + try: + hit = await search_routine.asend(worker.is_cancelled) + except StopAsyncIteration: + break + + # On the way out, if we're still in play, ensure everything has been + # dropped into the command list. + if not worker.is_cancelled: + self._refresh_command_list(command_list, gathered_commands, clear_current) + + # One way or another, we're not busy any more. + self._show_busy = False + + # If we didn't get any hits, and we're not cancelled, that would + # mean nothing was found. Give the user positive feedback to that + # effect. + if command_list.option_count == 0 and not worker.is_cancelled: + self._start_no_matches_countdown() + + @on(Input.Changed) + def _input(self, event: Input.Changed) -> None: + """React to input in the command palette. + + Args: + event: The input event. + """ + event.stop() + self.workers.cancel_all() + self._stop_no_matches_countdown() + + search_value = event.value.strip() + if search_value: + self._gather_commands(search_value) + else: + self._list_visible = False + self.query_one(CommandList).clear_options() + + @on(OptionList.OptionSelected) + def _select_command(self, event: OptionList.OptionSelected) -> None: + """React to a command being selected from the dropdown. + + Args: + event: The option selection event. + """ + event.stop() + self.workers.cancel_all() + input = self.query_one(CommandInput) + with self.prevent(Input.Changed): + assert isinstance(event.option, Command) + input.value = str(event.option.command.text) + self._selected_command = event.option.command + input.action_end() + self._list_visible = False + self.query_one(CommandList).clear_options() + if self.run_on_select: + self._select_or_command() + + @on(Input.Submitted) + @on(Button.Pressed) + def _select_or_command( + self, event: Input.Submitted | Button.Pressed | None = None + ) -> None: + """Depending on context, select or execute a command.""" + # If the list is visible, that means we're in "pick a command" + # mode... + if event is not None: + event.stop() + if self._list_visible: + # ...so if nothing in the list is highlighted yet... + if self.query_one(CommandList).highlighted is None: + # ...cause the first completion to be highlighted. + self._action_cursor_down() + else: + # The list is visible, something is highlighted, the user + # made a selection "gesture"; let's go select it! + self._action_command_list("select") + else: + # The list isn't visible, which means that if we have a + # command... + if self._selected_command is not None: + # ...we should return it to the parent screen and let it + # decide what to do with it (hopefully it'll run it). + self.workers.cancel_all() + self.dismiss(self._selected_command.command) + + def _action_escape(self) -> None: + """Handle a request to escape out of the command palette.""" + if self._list_visible: + self._list_visible = False + else: + self.workers.cancel_all() + self.dismiss() + + def _action_command_list(self, action: str) -> None: + """Pass an action on to the [`CommandList`][textual.command.CommandList]. + + Args: + action: The action to pass on to the [`CommandList`][textual.command.CommandList]. + """ + try: + command_action = getattr(self.query_one(CommandList), f"action_{action}") + except AttributeError: + return + command_action() + + def _action_cursor_down(self) -> None: + """Handle the cursor down action. + + This allows the cursor down key to either open the command list, if + it's closed but has options, or if it's open with options just + cursor through them. + """ + commands = self.query_one(CommandList) + if commands.option_count and not self._list_visible: + self._list_visible = True + commands.highlighted = 0 + elif ( + commands.option_count + and not commands.get_option_at_index(0).id == self._NO_MATCHES + ): + self._action_command_list("cursor_down") diff --git a/src/textual/constants.py b/src/textual/constants.py index acc3862d76..d47d0d2c15 100644 --- a/src/textual/constants.py +++ b/src/textual/constants.py @@ -56,6 +56,9 @@ def get_environ_int(name: str, default: int) -> int: LOG_FILE: Final[str | None] = get_environ("TEXTUAL_LOG", None) """A last resort log file that appends all logs, when devtools isn't working.""" +DEVTOOLS_HOST: Final[str] = get_environ("TEXTUAL_DEVTOOLS_HOST", "127.0.0.1") +"""The host where textual console is running.""" + DEVTOOLS_PORT: Final[int] = get_environ_int("TEXTUAL_DEVTOOLS_PORT", 8081) """Constant with the port that the devtools will connect to.""" @@ -70,3 +73,6 @@ def get_environ_int(name: str, default: int) -> int: MAX_FPS: Final[int] = get_environ_int("TEXTUAL_FPS", 60) """Maximum frames per second for updates.""" + +COLOR_SYSTEM: Final[str | None] = get_environ("TEXTUAL_COLOR_SYSTEM", "auto") +"""Force color system override""" diff --git a/src/textual/css/_help_text.py b/src/textual/css/_help_text.py index e873bd8a00..bb77fcb940 100644 --- a/src/textual/css/_help_text.py +++ b/src/textual/css/_help_text.py @@ -587,9 +587,7 @@ def scrollbar_size_property_help_text(context: StylingContext) -> HelpText: ), ], ).get_by_context(context), - Bullet( - " and must be positive integers, greater than zero" - ), + Bullet(" and must be non-negative integers."), ], ) diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index a38cc859cc..9667e97c65 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -614,10 +614,10 @@ def __set__(self, obj: StylesBase, layout: str | Layout | None): _rich_traceback_omit = True if layout is None: if obj.clear_rule("layout"): - obj.refresh(layout=True) + obj.refresh(layout=True, children=True) elif isinstance(layout, Layout): if obj.set_rule("layout", layout): - obj.refresh(layout=True) + obj.refresh(layout=True, children=True) else: try: layout_object = get_layout(layout) @@ -627,7 +627,7 @@ def __set__(self, obj: StylesBase, layout: str | Layout | None): help_text=layout_property_help_text(self.name, context="inline"), ) if obj.set_rule("layout", layout_object): - obj.refresh(layout=True) + obj.refresh(layout=True, children=True) class OffsetProperty: @@ -1063,8 +1063,8 @@ def __set__(self, obj: StylesBase, value: float | str | None) -> None: obj.refresh(children=self.children) return - if isinstance(value, float): - float_value = value + if isinstance(value, (int, float)): + float_value = float(value) elif isinstance(value, str) and value.endswith("%"): float_value = float(Scalar.parse(value).value) / 100 else: diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index d5d8048cc6..8dcc4637b4 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -722,8 +722,8 @@ def process_layer(self, name: str, tokens: list[Token]) -> None: def process_layers(self, name: str, tokens: list[Token]) -> None: layers: list[str] = [] for token in tokens: - if token.name != "token": - self.error(name, token, "{token.name} not expected here") + if token.name not in {"token", "string"}: + self.error(name, token, f"{token.name} not expected here") layers.append(token.value) self.styles._rules["layers"] = tuple(layers) @@ -876,11 +876,7 @@ def scrollbar_size_error(name: str, token: Token) -> None: scrollbar_size_error(name, token2) horizontal = int(token1.value) - if horizontal == 0: - scrollbar_size_error(name, token1) vertical = int(token2.value) - if vertical == 0: - scrollbar_size_error(name, token2) self.styles._rules["scrollbar_size_horizontal"] = horizontal self.styles._rules["scrollbar_size_vertical"] = vertical self._distribute_importance("scrollbar_size", ("horizontal", "vertical")) @@ -895,8 +891,6 @@ def process_scrollbar_size_vertical(self, name: str, tokens: list[Token]) -> Non if token.name != "number" or not token.value.isdigit(): self.error(name, token, scrollbar_size_single_axis_help_text(name)) value = int(token.value) - if value == 0: - self.error(name, token, scrollbar_size_single_axis_help_text(name)) self.styles._rules["scrollbar_size_vertical"] = value def process_scrollbar_size_horizontal(self, name: str, tokens: list[Token]) -> None: @@ -909,8 +903,6 @@ def process_scrollbar_size_horizontal(self, name: str, tokens: list[Token]) -> N if token.name != "number" or not token.value.isdigit(): self.error(name, token, scrollbar_size_single_axis_help_text(name)) value = int(token.value) - if value == 0: - self.error(name, token, scrollbar_size_single_axis_help_text(name)) self.styles._rules["scrollbar_size_horizontal"] = value def _process_grid_rows_or_columns(self, name: str, tokens: list[Token]) -> None: @@ -921,6 +913,8 @@ def _process_grid_rows_or_columns(self, name: str, tokens: list[Token]) -> None: scalars.append(Scalar.from_number(float(token.value))) elif token.name == "scalar": scalars.append(Scalar.parse(token.value, percent_unit=percent_unit)) + elif token.name == "token" and token.value == "auto": + scalars.append(Scalar.parse("auto")) else: self.error( name, diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py index 95487e8704..729e8f9010 100644 --- a/src/textual/css/constants.py +++ b/src/textual/css/constants.py @@ -64,11 +64,13 @@ VALID_PSEUDO_CLASSES: Final = { "blur", "can-focus", + "dark", "disabled", "enabled", "focus-within", "focus", "hover", + "light", } VALID_OVERLAY: Final = {"none", "screen"} VALID_CONSTRAIN: Final = {"x", "y", "both", "inflect", "none"} diff --git a/src/textual/css/model.py b/src/textual/css/model.py index 3766606de1..cf67bd9ddd 100644 --- a/src/textual/css/model.py +++ b/src/textual/css/model.py @@ -174,6 +174,8 @@ def _selector_to_css(cls, selectors: list[Selector]) -> str: elif selector.combinator == CombinatorType.CHILD: tokens.append(" > ") tokens.append(selector.css) + for pseudo_class in selector.pseudo_classes: + tokens.append(f":{pseudo_class}") return "".join(tokens).strip() @property diff --git a/src/textual/css/parse.py b/src/textual/css/parse.py index 1b31e8b66b..40305df1cf 100644 --- a/src/textual/css/parse.py +++ b/src/textual/css/parse.py @@ -85,6 +85,7 @@ def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]: def parse_rule_set( + scope: str, tokens: Iterator[Token], token: Token, is_default_rules: bool = False, @@ -127,6 +128,19 @@ def parse_rule_set( token = next(tokens) if selectors: + if scope and selectors[0].name != scope: + scope_selector, scope_specificity = get_selector( + scope, (SelectorType.TYPE, (0, 0, 0)) + ) + selectors.insert( + 0, + Selector( + name=scope, + combinator=CombinatorType.DESCENDENT, + type=scope_selector, + specificity=scope_specificity, + ), + ) rule_selectors.append(selectors[:]) declaration = Declaration(token, "") @@ -328,6 +342,7 @@ def substitute_references( def parse( + scope: str, css: str, path: str | PurePath, variables: dict[str, str] | None = None, @@ -339,6 +354,7 @@ def parse( and generating rule sets from it. Args: + scope: CSS type name css: The input CSS path: Path to the CSS variables: Substitution variables to substitute tokens for. @@ -357,6 +373,7 @@ def parse( break if token.name.startswith("selector_start"): yield from parse_rule_set( + scope, tokens, token, is_default_rules=is_default_rules, diff --git a/src/textual/css/query.py b/src/textual/css/query.py index ce966d6b18..a6af6ae679 100644 --- a/src/textual/css/query.py +++ b/src/textual/css/query.py @@ -49,6 +49,9 @@ class WrongType(QueryError): QueryType = TypeVar("QueryType", bound="Widget") +"""Type variable used to type generic queries.""" +ExpectType = TypeVar("ExpectType") +"""Type variable used to further restrict queries.""" @rich.repr.auto(angular=True) @@ -187,10 +190,8 @@ def exclude(self, selector: str) -> DOMQuery[QueryType]: """ return DOMQuery(self.node, exclude=selector, parent=self) - ExpectType = TypeVar("ExpectType") - @overload - def first(self) -> Widget: + def first(self) -> QueryType: ... @overload @@ -226,7 +227,7 @@ def first( raise NoMatches(f"No nodes match {self!r}") @overload - def only_one(self) -> Widget: + def only_one(self) -> QueryType: ... @overload @@ -235,7 +236,7 @@ def only_one(self, expect_type: type[ExpectType]) -> ExpectType: def only_one( self, expect_type: type[ExpectType] | None = None - ) -> Widget | ExpectType: + ) -> QueryType | ExpectType: """Get the *only* matching node. Args: @@ -253,7 +254,9 @@ def only_one( _rich_traceback_omit = True # Call on first to get the first item. Here we'll use all of the # testing and checking it provides. - the_one = self.first(expect_type) if expect_type is not None else self.first() + the_one: ExpectType | QueryType = ( + self.first(expect_type) if expect_type is not None else self.first() + ) try: # Now see if we can access a subsequent item in the nodes. There # should *not* be anything there, so we *should* get an @@ -268,10 +271,10 @@ def only_one( # The IndexError was got, that's a good thing in this case. So # we return what we found. pass - return cast("Widget", the_one) + return the_one @overload - def last(self) -> Widget: + def last(self) -> QueryType: ... @overload @@ -304,7 +307,7 @@ def last( return last @overload - def results(self) -> Iterator[Widget]: + def results(self) -> Iterator[QueryType]: ... @overload @@ -313,7 +316,7 @@ def results(self, filter_type: type[ExpectType]) -> Iterator[ExpectType]: def results( self, filter_type: type[ExpectType] | None = None - ) -> Iterator[Widget | ExpectType]: + ) -> Iterator[QueryType | ExpectType]: """Get query results, optionally filtered by a given type. Args: diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index 22bfccaa3f..3cafd7fa25 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -111,11 +111,14 @@ class CssSource(NamedTuple): content: The CSS as a string. is_defaults: True if the CSS is default (i.e. that defined at the widget level). False if it's user CSS (which will override the defaults). + tie_breaker: Specificity tie breaker. + scope: Scope of CSS. """ content: str is_defaults: bool tie_breaker: int = 0 + scope: str = "" @rich.repr.auto(angular=True) @@ -196,15 +199,16 @@ def _parse_rules( path: str | PurePath, is_default_rules: bool = False, tie_breaker: int = 0, + scope: str = "", ) -> list[RuleSet]: """Parse CSS and return rules. Args: - is_default_rules: css: String containing Textual CSS. path: Path to CSS or unique identifier is_default_rules: True if the rules we're extracting are default (i.e. in Widget.DEFAULT_CSS) rules. False if they're from user defined CSS. + scope: Scope of rules, or empty string for global scope. Raises: StylesheetError: If the CSS is invalid. @@ -215,6 +219,7 @@ def _parse_rules( try: rules = list( parse( + scope, css, path, variable_tokens=self._variable_tokens, @@ -276,6 +281,7 @@ def add_source( path: str | PurePath | None = None, is_default_css: bool = False, tie_breaker: int = 0, + scope: str = "", ) -> None: """Parse CSS from a string. @@ -285,6 +291,7 @@ def add_source( is_default_css: True if the CSS is defined in the Widget, False if the CSS is defined in a user stylesheet. tie_breaker: Integer representing the priority of this source. + scope: CSS type name to limit scope or empty string for no scope. Raises: StylesheetError: If the CSS could not be read. @@ -297,11 +304,11 @@ def add_source( path = str(css) if path in self.source and self.source[path].content == css: # Path already in source, and CSS is identical - content, is_defaults, source_tie_breaker = self.source[path] + content, is_defaults, source_tie_breaker, scope = self.source[path] if source_tie_breaker > tie_breaker: - self.source[path] = CssSource(content, is_defaults, tie_breaker) + self.source[path] = CssSource(content, is_defaults, tie_breaker, scope) return - self.source[path] = CssSource(css, is_default_css, tie_breaker) + self.source[path] = CssSource(css, is_default_css, tie_breaker, scope) self._require_parse = True def parse(self) -> None: @@ -313,7 +320,7 @@ def parse(self) -> None: rules: list[RuleSet] = [] add_rules = rules.extend - for path, (css, is_default_rules, tie_breaker) in self.source.items(): + for path, (css, is_default_rules, tie_breaker, scope) in self.source.items(): if css in self._invalid_css: continue try: @@ -322,6 +329,7 @@ def parse(self) -> None: path, is_default_rules=is_default_rules, tie_breaker=tie_breaker, + scope=scope, ) except Exception: self._invalid_css.add(css) @@ -343,9 +351,13 @@ def reparse(self) -> None: """ # Do this in a fresh Stylesheet so if there are errors we don't break self. stylesheet = Stylesheet(variables=self._variables) - for path, (css, is_defaults, tie_breaker) in self.source.items(): + for path, (css, is_defaults, tie_breaker, scope) in self.source.items(): stylesheet.add_source( - css, path, is_default_css=is_defaults, tie_breaker=tie_breaker + css, + path, + is_default_css=is_defaults, + tie_breaker=tie_breaker, + scope=scope, ) stylesheet.parse() self._rules = stylesheet.rules diff --git a/src/textual/demo.py b/src/textual/demo.py index df63b53235..2638a33121 100644 --- a/src/textual/demo.py +++ b/src/textual/demo.py @@ -7,6 +7,7 @@ from rich.console import RenderableType from rich.json import JSON from rich.markdown import Markdown +from rich.markup import escape from rich.pretty import Pretty from rich.syntax import Syntax from rich.table import Table @@ -269,23 +270,15 @@ class SubTitle(Static): pass -class Notification(Static): - def on_mount(self) -> None: - self.set_timer(3, self.remove) - - def on_click(self) -> None: - self.remove() - - class DemoApp(App[None]): - CSS_PATH = "demo.css" + CSS_PATH = "demo.tcss" TITLE = "Textual Demo" BINDINGS = [ ("ctrl+b", "toggle_sidebar", "Sidebar"), ("ctrl+t", "app.toggle_dark", "Toggle Dark mode"), ("ctrl+s", "app.screenshot()", "Screenshot"), ("f1", "app.toggle_class('RichLog', '-hidden')", "Notes"), - Binding("ctrl+c,ctrl+q", "app.quit", "Quit", show=True), + Binding("ctrl+q", "app.quit", "Quit", show=True), ] show_sidebar = reactive(False) @@ -390,9 +383,9 @@ def action_screenshot(self, filename: str | None = None, path: str = "./") -> No """ self.bell() path = self.save_screenshot(filename, path) - message = Text.assemble("Screenshot saved to ", (f"'{path}'", "bold green")) - self.add_note(message) - self.screen.mount(Notification(message)) + message = f"Screenshot saved to [bold green]'{escape(str(path))}'[/]" + self.add_note(Text.from_markup(message)) + self.notify(message) app = DemoApp() diff --git a/src/textual/demo.css b/src/textual/demo.tcss similarity index 100% rename from src/textual/demo.css rename to src/textual/demo.tcss diff --git a/src/textual/document/__init__.py b/src/textual/document/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/textual/document/_document.py b/src/textual/document/_document.py new file mode 100644 index 0000000000..5e8e37d8d0 --- /dev/null +++ b/src/textual/document/_document.py @@ -0,0 +1,389 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from functools import lru_cache +from typing import TYPE_CHECKING, NamedTuple, Tuple, overload + +if TYPE_CHECKING: + from tree_sitter import Node + from tree_sitter.binding import Query + +from textual._cells import cell_len +from textual._types import Literal, get_args +from textual.geometry import Size + +Newline = Literal["\r\n", "\n", "\r"] +"""The type representing valid line separators.""" +VALID_NEWLINES = set(get_args(Newline)) +"""The set of valid line separator strings.""" + + +@dataclass +class EditResult: + """Contains information about an edit that has occurred.""" + + end_location: Location + """The new end Location after the edit is complete.""" + replaced_text: str + """The text that was replaced.""" + + +@lru_cache(maxsize=1024) +def _utf8_encode(text: str) -> bytes: + """Encode the input text as utf-8 bytes. + + The returned encoded bytes may be retrieved from a cache. + + Args: + text: The text to encode. + + Returns: + The utf-8 bytes representing the input string. + """ + return text.encode("utf-8") + + +def _detect_newline_style(text: str) -> Newline: + """Return the newline type used in this document. + + Args: + text: The text to inspect. + + Returns: + The NewlineStyle used in the file. + """ + if "\r\n" in text: # Windows newline + return "\r\n" + elif "\n" in text: # Unix/Linux/MacOS newline + return "\n" + elif "\r" in text: # Old MacOS newline + return "\r" + else: + return "\n" # Default to Unix style newline + + +class DocumentBase(ABC): + """Describes the minimum functionality a Document implementation must + provide in order to be used by the TextArea widget.""" + + @abstractmethod + def replace_range(self, start: Location, end: Location, text: str) -> EditResult: + """Replace the text at the given range. + + Args: + start: A tuple (row, column) where the edit starts. + end: A tuple (row, column) where the edit ends. + text: The text to insert between start and end. + + Returns: + The new end location after the edit is complete. + """ + + @property + @abstractmethod + def text(self) -> str: + """The text from the document as a string.""" + + @property + @abstractmethod + def newline(self) -> Newline: + """Return the line separator used in the document.""" + + @abstractmethod + def get_line(self, index: int) -> str: + """Returns the line with the given index from the document. + + This is used in rendering lines, and will be called by the + TextArea for each line that is rendered. + + Args: + index: The index of the line in the document. + + Returns: + The str instance representing the line. + """ + + @abstractmethod + def get_text_range(self, start: Location, end: Location) -> str: + """Get the text that falls between the start and end locations. + + Args: + start: The start location of the selection. + end: The end location of the selection. + + Returns: + The text between start (inclusive) and end (exclusive). + """ + + @abstractmethod + def get_size(self, indent_width: int) -> Size: + """Get the size of the document. + + The height is generally the number of lines, and the width + is generally the maximum cell length of all the lines. + + Args: + indent_width: The width to use for tab characters. + + Returns: + The Size of the document bounding box. + """ + + def query_syntax_tree( + self, + query: "Query", + start_point: tuple[int, int] | None = None, + end_point: tuple[int, int] | None = None, + ) -> list[tuple["Node", str]]: + """Query the tree-sitter syntax tree. + + The default implementation always returns an empty list. + + To support querying in a subclass, this must be implemented. + + Args: + query: The tree-sitter Query to perform. + start_point: The (row, column byte) to start the query at. + end_point: The (row, column byte) to end the query at. + + Returns: + A tuple containing the nodes and text captured by the query. + """ + return [] + + def prepare_query(self, query: str) -> "Query" | None: + return None + + @property + @abstractmethod + def line_count(self) -> int: + """Returns the number of lines in the document.""" + + @overload + def __getitem__(self, line_index: int) -> str: + ... + + @overload + def __getitem__(self, line_index: slice) -> list[str]: + ... + + @abstractmethod + def __getitem__(self, line_index: int | slice) -> str | list[str]: + """Return the content of a line as a string, excluding newline characters. + + Args: + line_index: The index or slice of the line(s) to retrieve. + + Returns: + The line or list of lines requested. + """ + + +class Document(DocumentBase): + """A document which can be opened in a TextArea.""" + + def __init__(self, text: str) -> None: + self._newline = _detect_newline_style(text) + """The type of newline used in the text.""" + self._lines: list[str] = text.splitlines(keepends=False) + """The lines of the document, excluding newline characters. + + If there's a newline at the end of the file, the final line is an empty string. + """ + if text.endswith(tuple(VALID_NEWLINES)) or not text: + self._lines.append("") + + @property + def lines(self) -> list[str]: + """Get the document as a list of strings, where each string represents a line. + + Newline characters are not included in at the end of the strings. + + The newline character used in this document can be found via the `Document.newline` property. + """ + return self._lines + + @property + def text(self) -> str: + """Get the text from the document.""" + return self._newline.join(self._lines) + + @property + def newline(self) -> Newline: + """Get the Newline used in this document (e.g. '\r\n', '\n'. etc.)""" + return self._newline + + def get_size(self, tab_width: int) -> Size: + """The Size of the document, taking into account the tab rendering width. + + Args: + tab_width: The width to use for tab indents. + + Returns: + The size (width, height) of the document. + """ + lines = self._lines + cell_lengths = [cell_len(line.expandtabs(tab_width)) for line in lines] + max_cell_length = max(cell_lengths, default=0) + height = len(lines) + return Size(max_cell_length, height) + + def replace_range(self, start: Location, end: Location, text: str) -> EditResult: + """Replace text at the given range. + + Args: + start: A tuple (row, column) where the edit starts. + end: A tuple (row, column) where the edit ends. + text: The text to insert between start and end. + + Returns: + The EditResult containing information about the completed + replace operation. + """ + top, bottom = sorted((start, end)) + top_row, top_column = top + bottom_row, bottom_column = bottom + + insert_lines = text.splitlines() + if text.endswith(tuple(VALID_NEWLINES)): + # Special case where a single newline character is inserted. + insert_lines.append("") + + lines = self._lines + + replaced_text = self.get_text_range(top, bottom) + if bottom_row >= len(lines): + after_selection = "" + else: + after_selection = lines[bottom_row][bottom_column:] + + if top_row >= len(lines): + before_selection = "" + else: + before_selection = lines[top_row][:top_column] + + if insert_lines: + insert_lines[0] = before_selection + insert_lines[0] + destination_column = len(insert_lines[-1]) + insert_lines[-1] = insert_lines[-1] + after_selection + else: + destination_column = len(before_selection) + insert_lines = [before_selection + after_selection] + + lines[top_row : bottom_row + 1] = insert_lines + destination_row = top_row + len(insert_lines) - 1 + + end_location = (destination_row, destination_column) + return EditResult(end_location, replaced_text) + + def get_text_range(self, start: Location, end: Location) -> str: + """Get the text that falls between the start and end locations. + + Returns the text between `start` and `end`, including the appropriate + line separator character as specified by `Document._newline`. Note that + `_newline` is set automatically to the first line separator character + found in the document. + + Args: + start: The start location of the selection. + end: The end location of the selection. + + Returns: + The text between start (inclusive) and end (exclusive). + """ + if start == end: + return "" + + top, bottom = sorted((start, end)) + top_row, top_column = top + bottom_row, bottom_column = bottom + lines = self._lines + if top_row == bottom_row: + line = lines[top_row] + selected_text = line[top_column:bottom_column] + else: + start_line = lines[top_row] + end_line = lines[bottom_row] if bottom_row <= self.line_count - 1 else "" + selected_text = start_line[top_column:] + for row in range(top_row + 1, bottom_row): + selected_text += self._newline + lines[row] + + if bottom_row < self.line_count: + selected_text += self._newline + selected_text += end_line[:bottom_column] + + return selected_text + + @property + def line_count(self) -> int: + """Returns the number of lines in the document.""" + return len(self._lines) + + def get_line(self, index: int) -> str: + """Returns the line with the given index from the document. + + Args: + index: The index of the line in the document. + + Returns: + The string representing the line. + """ + line_string = self[index] + return line_string + + @overload + def __getitem__(self, line_index: int) -> str: + ... + + @overload + def __getitem__(self, line_index: slice) -> list[str]: + ... + + def __getitem__(self, line_index: int | slice) -> str | list[str]: + """Return the content of a line as a string, excluding newline characters. + + Args: + line_index: The index or slice of the line(s) to retrieve. + + Returns: + The line or list of lines requested. + """ + return self._lines[line_index] + + +Location = Tuple[int, int] +"""A location (row, column) within the document. Indexing starts at 0.""" + + +class Selection(NamedTuple): + """A range of characters within a document from a start point to the end point. + The location of the cursor is always considered to be the `end` point of the selection. + The selection is inclusive of the minimum point and exclusive of the maximum point. + """ + + start: Location = (0, 0) + """The start location of the selection. + + If you were to click and drag a selection inside a text-editor, this is where you *started* dragging. + """ + end: Location = (0, 0) + """The end location of the selection. + + If you were to click and drag a selection inside a text-editor, this is where you *finished* dragging. + """ + + @classmethod + def cursor(cls, location: Location) -> "Selection": + """Create a Selection with the same start and end point - a "cursor". + + Args: + location: The location to create the zero-width Selection. + """ + return cls(location, location) + + @property + def is_empty(self) -> bool: + """Return True if the selection has 0 width, i.e. it's just a cursor.""" + start, end = self + return start == end diff --git a/src/textual/document/_languages.py b/src/textual/document/_languages.py new file mode 100644 index 0000000000..a33f7544e8 --- /dev/null +++ b/src/textual/document/_languages.py @@ -0,0 +1,13 @@ +BUILTIN_LANGUAGES = sorted( + [ + "markdown", + "yaml", + "sql", + "css", + "html", + "json", + "python", + "regex", + "toml", + ] +) diff --git a/src/textual/document/_syntax_aware_document.py b/src/textual/document/_syntax_aware_document.py new file mode 100644 index 0000000000..3fd828ae48 --- /dev/null +++ b/src/textual/document/_syntax_aware_document.py @@ -0,0 +1,268 @@ +from __future__ import annotations + +try: + from tree_sitter import Language, Node, Parser, Tree + from tree_sitter.binding import Query + from tree_sitter_languages import get_language, get_parser + + TREE_SITTER = True +except ImportError: + TREE_SITTER = False + +from textual.document._document import Document, EditResult, Location, _utf8_encode +from textual.document._languages import BUILTIN_LANGUAGES + + +class SyntaxAwareDocumentError(Exception): + """General error raised when SyntaxAwareDocument is used incorrectly.""" + + +class SyntaxAwareDocument(Document): + """A wrapper around a Document which also maintains a tree-sitter syntax + tree when the document is edited. + + The primary reason for this split is actually to keep tree-sitter stuff separate, + since it isn't supported in Python 3.7. By having the tree-sitter code + isolated in this subclass, it makes it easier to conditionally import. However, + it does come with other design flaws (e.g. Document is required to have methods + which only really make sense on SyntaxAwareDocument). + + If you're reading this and Python 3.7 is no longer supported by Textual, + consider merging this subclass into the `Document` superclass. + """ + + def __init__( + self, + text: str, + language: str | Language, + ): + """Construct a SyntaxAwareDocument. + + Args: + text: The initial text contained in the document. + language: The language to use. You can pass a string to use a supported + language, or pass in your own tree-sitter `Language` object. + """ + + if not TREE_SITTER: + raise RuntimeError("SyntaxAwareDocument unavailable.") + + super().__init__(text) + self.language: Language | None = None + """The tree-sitter Language or None if tree-sitter is unavailable.""" + + self._parser: Parser | None = None + """The tree-sitter Parser or None if tree-sitter is unavailable.""" + + # If the language is `None`, then avoid doing any parsing related stuff. + if isinstance(language, str): + if language not in BUILTIN_LANGUAGES: + raise SyntaxAwareDocumentError(f"Invalid language {language!r}") + self.language = get_language(language) + self._parser = get_parser(language) + else: + self.language = language + self._parser = Parser() + self._parser.set_language(language) + + self._syntax_tree: Tree = self._parser.parse(self._read_callable) # type: ignore + """The tree-sitter Tree (syntax tree) built from the document.""" + + @property + def language_name(self) -> str | None: + return self.language.name if self.language else None + + def prepare_query(self, query: str) -> Query | None: + """Prepare a tree-sitter tree query. + + Queries should be prepared once, then reused. + + To execute a query, call `query_syntax_tree`. + + Args: + The string query to prepare. + + Returns: + The prepared query. + """ + if not TREE_SITTER: + raise SyntaxAwareDocumentError( + "Couldn't prepare query - tree-sitter is not available on this architecture." + ) + + if self.language is None: + raise SyntaxAwareDocumentError( + "Couldn't prepare query - no language assigned." + ) + + return self.language.query(query) + + def query_syntax_tree( + self, + query: Query, + start_point: tuple[int, int] | None = None, + end_point: tuple[int, int] | None = None, + ) -> list[tuple["Node", str]]: + """Query the tree-sitter syntax tree. + + The default implementation always returns an empty list. + + To support querying in a subclass, this must be implemented. + + Args: + query: The tree-sitter Query to perform. + start_point: The (row, column byte) to start the query at. + end_point: The (row, column byte) to end the query at. + + Returns: + A tuple containing the nodes and text captured by the query. + """ + + if not TREE_SITTER: + raise SyntaxAwareDocumentError( + "tree-sitter is not available on this architecture." + ) + + captures_kwargs = {} + if start_point is not None: + captures_kwargs["start_point"] = start_point + if end_point is not None: + captures_kwargs["end_point"] = end_point + + captures = query.captures(self._syntax_tree.root_node, **captures_kwargs) + return captures + + def replace_range(self, start: Location, end: Location, text: str) -> EditResult: + """Replace text at the given range. + + Args: + start: A tuple (row, column) where the edit starts. + end: A tuple (row, column) where the edit ends. + text: The text to insert between start and end. + + Returns: + The new end location after the edit is complete. + """ + top, bottom = sorted((start, end)) + + # An optimisation would be finding the byte offsets as a single operation rather + # than doing two passes over the document content. + start_byte = self._location_to_byte_offset(top) + start_point = self._location_to_point(top) + old_end_byte = self._location_to_byte_offset(bottom) + old_end_point = self._location_to_point(bottom) + + replace_result = super().replace_range(start, end, text) + + text_byte_length = len(_utf8_encode(text)) + end_location = replace_result.end_location + assert self._syntax_tree is not None + assert self._parser is not None + self._syntax_tree.edit( + start_byte=start_byte, + old_end_byte=old_end_byte, + new_end_byte=start_byte + text_byte_length, + start_point=start_point, + old_end_point=old_end_point, + new_end_point=self._location_to_point(end_location), + ) + # Incrementally parse the document. + self._syntax_tree = self._parser.parse( + self._read_callable, self._syntax_tree # type: ignore[arg-type] + ) + + return replace_result + + def get_line(self, line_index: int) -> str: + """Return the string representing the line, not including new line characters. + + Args: + line_index: The index of the line. + + Returns: + The string representing the line. + """ + line_string = self[line_index] + return line_string + + def _location_to_byte_offset(self, location: Location) -> int: + """Given a document coordinate, return the byte offset of that coordinate. + This method only does work if tree-sitter was imported, otherwise it returns 0. + + Args: + location: The location to convert. + + Returns: + An integer byte offset for the given location. + """ + lines = self._lines + row, column = location + lines_above = lines[:row] + end_of_line_width = len(self.newline) + bytes_lines_above = sum( + len(_utf8_encode(line)) + end_of_line_width for line in lines_above + ) + if row < len(lines): + bytes_on_left = len(_utf8_encode(lines[row][:column])) + else: + bytes_on_left = 0 + byte_offset = bytes_lines_above + bytes_on_left + return byte_offset + + def _location_to_point(self, location: Location) -> tuple[int, int]: + """Convert a document location (row_index, column_index) to a tree-sitter + point (row_index, byte_offset_from_start_of_row). If tree-sitter isn't available + returns (0, 0). + + Args: + location: A location (row index, column codepoint offset) + + Returns: + The point corresponding to that location (row index, column byte offset). + """ + lines = self._lines + row, column = location + if row < len(lines): + bytes_on_left = len(_utf8_encode(lines[row][:column])) + else: + bytes_on_left = 0 + return row, bytes_on_left + + def _read_callable(self, byte_offset: int, point: tuple[int, int]) -> bytes: + """A callable which informs tree-sitter about the document content. + + This is passed to tree-sitter which will call it frequently to retrieve + the bytes from the document. + + Args: + byte_offset: The number of (utf-8) bytes from the start of the document. + point: A tuple (row index, column *byte* offset). Note that this differs + from our Location tuple which is (row_index, column codepoint offset). + + Returns: + All the utf-8 bytes between the byte_offset/point and the end of the current + line _including_ the line separator character(s). Returns None if the + offset/point requested by tree-sitter doesn't correspond to a byte. + """ + row, column = point + lines = self._lines + newline = self.newline + + row_out_of_bounds = row >= len(lines) + if row_out_of_bounds: + return b"" + else: + row_text = lines[row] + + encoded_row = _utf8_encode(row_text) + encoded_row_length = len(encoded_row) + + if column < encoded_row_length: + return encoded_row[column:] + _utf8_encode(newline) + elif column == encoded_row_length: + return _utf8_encode(newline[0]) + elif column == encoded_row_length + 1: + if newline == "\r\n": + return b"\n" + + return b"" diff --git a/src/textual/dom.py b/src/textual/dom.py index 3cc5051728..bc8ab776bf 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -132,6 +132,10 @@ class DOMNode(MessagePump): # Mapping of key bindings BINDINGS: ClassVar[list[BindingType]] = [] + # Indicates if the CSS should be automatically scoped + SCOPED_CSS: ClassVar[bool] = True + """Should default css be limited to the widget type?""" + # True if this node inherits the CSS from the base class. _inherit_css: ClassVar[bool] = True @@ -144,6 +148,9 @@ class DOMNode(MessagePump): # List of names of base classes that inherit CSS _css_type_names: ClassVar[frozenset[str]] = frozenset() + # Name of the widget in CSS + _css_type_name: str = "" + # Generated list of bindings _merged_bindings: ClassVar[_Bindings | None] = None @@ -304,7 +311,9 @@ def __init_subclass__( cls._inherit_bindings = inherit_bindings cls._inherit_component_classes = inherit_component_classes css_type_names: set[str] = set() - for base in cls._css_bases(cls): + bases = cls._css_bases(cls) + cls._css_type_name = bases[0].__name__ + for base in bases: css_type_names.add(base.__name__) cls._merged_bindings = cls._merge_bindings() cls._css_type_names = frozenset(css_type_names) @@ -399,23 +408,26 @@ def _post_register(self, app: App) -> None: """ def __rich_repr__(self) -> rich.repr.Result: - yield "name", self._name, None - yield "id", self._id, None - if self._classes: + # Being a bit defensive here to guard against errors when calling repr before initialization + if hasattr(self, "_name"): + yield "name", self._name, None + if hasattr(self, "_id"): + yield "id", self._id, None + if hasattr(self, "_classes") and self._classes: yield "classes", " ".join(self._classes) - def _get_default_css(self) -> list[tuple[str, str, int]]: + def _get_default_css(self) -> list[tuple[str, str, int, str]]: """Gets the CSS for this class and inherited from bases. Default CSS is inherited from base classes, unless `inherit_css` is set to `False` when subclassing. Returns: - A list of tuples containing (PATH, SOURCE) for this + A list of tuples containing (PATH, SOURCE, SPECIFICITY, SCOPE) for this and inherited from base classes. """ - css_stack: list[tuple[str, str, int]] = [] + css_stack: list[tuple[str, str, int, str]] = [] def get_path(base: Type[DOMNode]) -> str: """Get a path to the DOM Node""" @@ -425,10 +437,17 @@ def get_path(base: Type[DOMNode]) -> str: return f"{base.__name__}" for tie_breaker, base in enumerate(self._node_bases): - css = base.__dict__.get("DEFAULT_CSS", "").strip() + css: str = base.__dict__.get("DEFAULT_CSS", "").strip() if css: - css_stack.append((get_path(base), css, -tie_breaker)) - + scoped: bool = base.__dict__.get("SCOPED_CSS", True) + css_stack.append( + ( + get_path(base), + css, + -tie_breaker, + base._css_type_name if scoped else "", + ) + ) return css_stack @classmethod @@ -612,13 +631,21 @@ def display(self, new_val: bool | str) -> None: @property def visible(self) -> bool: - """Is the visibility style set to a visible state? + """Is this widget visible in the DOM? + + If a widget hasn't had its visibility set explicitly, then it inherits it from its + DOM ancestors. - May be set to a boolean to make the node visible (`True`) or invisible (`False`), or to any valid value for the `visibility` rule. + This may be set explicitly to override inherited values. + The valid values include the valid values for the `visibility` rule and the booleans + `True` or `False`, to set the widget to be visible or invisible, respectively. - When a node is invisible, Textual will reserve space for it, but won't display anything there. + When a node is invisible, Textual will reserve space for it, but won't display anything. """ - return self.styles.visibility != "hidden" + own_value = self.styles.get_rule("visibility") + if own_value is not None: + return own_value != "hidden" + return self.parent.visible if self.parent else True @visible.setter def visible(self, new_value: bool | str) -> None: @@ -869,7 +896,7 @@ def colors(self) -> tuple[Color, Color, Color, Color]: @property def ancestors_with_self(self) -> list[DOMNode]: - """A list of Nodes by tracing a path all the way back to App. + """A list of ancestor nodes found by tracing a path all the way back to App. Note: This is inclusive of ``self``. @@ -887,7 +914,7 @@ def ancestors_with_self(self) -> list[DOMNode]: @property def ancestors(self) -> list[DOMNode]: - """A list of ancestor nodes Nodes by tracing ancestors all the way back to App. + """A list of ancestor nodes found by tracing a path all the way back to App. Returns: A list of nodes. diff --git a/src/textual/drivers/_byte_stream.py b/src/textual/drivers/_byte_stream.py index a69bf79d04..4c6bb602f0 100644 --- a/src/textual/drivers/_byte_stream.py +++ b/src/textual/drivers/_byte_stream.py @@ -2,7 +2,16 @@ import io from collections import deque -from typing import Callable, Deque, Generator, Generic, Iterable, NamedTuple, TypeVar +from typing import ( + Callable, + Deque, + Generator, + Generic, + Iterable, + NamedTuple, + Tuple, + TypeVar, +) from typing_extensions import TypeAlias @@ -126,7 +135,7 @@ class BytePacket(NamedTuple): payload: bytes -class ByteStream(ByteStreamParser[tuple[str, bytes]]): +class ByteStream(ByteStreamParser[Tuple[str, bytes]]): """A stream of packets in the following format. 1 Byte for the type. @@ -142,7 +151,7 @@ def parse( read = self.read from_bytes = int.from_bytes while not self.is_eof: - packet_type = (yield read1()).decode("utf-8") + packet_type = (yield read1()).decode("utf-8", "ignore") size = from_bytes((yield read(4)), "big") payload = (yield read(size)) if size else b"" on_token(BytePacket(packet_type, payload)) diff --git a/src/textual/drivers/_input_reader.py b/src/textual/drivers/_input_reader.py new file mode 100644 index 0000000000..84c72d3633 --- /dev/null +++ b/src/textual/drivers/_input_reader.py @@ -0,0 +1,10 @@ +import platform + +__all__ = ["InputReader"] + +WINDOWS = platform.system() == "Windows" + +if WINDOWS: + from ._input_reader_windows import InputReader +else: + from ._input_reader_linux import InputReader diff --git a/src/textual/drivers/_input_reader_linux.py b/src/textual/drivers/_input_reader_linux.py new file mode 100644 index 0000000000..82c032e0b6 --- /dev/null +++ b/src/textual/drivers/_input_reader_linux.py @@ -0,0 +1,49 @@ +import os +import selectors +import sys +from threading import Event +from typing import Iterator + +from textual import log + + +class InputReader: + """Read input from stdin.""" + + def __init__(self, timeout: float = 0.1) -> None: + """ + + Args: + timeout: Seconds to block for input. + """ + self._fileno = sys.__stdin__.fileno() + self.timeout = timeout + self._selector = selectors.DefaultSelector() + self._selector.register(self._fileno, selectors.EVENT_READ) + self._exit_event = Event() + + def more_data(self) -> bool: + """Check if there is data pending.""" + EVENT_READ = selectors.EVENT_READ + for _key, events in self._selector.select(0.01): + if events & EVENT_READ: + return True + return False + + def close(self) -> None: + """Close the reader (will exit the iterator).""" + self._exit_event.set() + + def __iter__(self) -> Iterator[bytes]: + """Read input, yield bytes.""" + fileno = self._fileno + read = os.read + exit_set = self._exit_event.is_set + EVENT_READ = selectors.EVENT_READ + while not exit_set(): + for _key, events in self._selector.select(self.timeout): + if events & EVENT_READ: + data = read(fileno, 1024) + if not data: + return + yield data diff --git a/src/textual/drivers/_input_reader_windows.py b/src/textual/drivers/_input_reader_windows.py new file mode 100644 index 0000000000..7f9aeb1ebb --- /dev/null +++ b/src/textual/drivers/_input_reader_windows.py @@ -0,0 +1,37 @@ +import os +import sys +from threading import Event +from typing import Iterator + + +class InputReader: + """Read input from stdin.""" + + def __init__(self, timeout: float = 0.1) -> None: + """ + + Args: + timeout: Seconds to block for input. + """ + self._fileno = sys.__stdin__.fileno() + self.timeout = timeout + self._exit_event = Event() + + def more_data(self) -> bool: + """Check if there is data pending.""" + return True + + def close(self) -> None: + """Close the reader (will exit the iterator).""" + self._exit_event.set() + + def __iter__(self) -> Iterator[bytes]: + """Read input, yield bytes.""" + while not self._exit_event.is_set(): + try: + data = os.read(self._fileno, 1024) or None + except Exception: + break + if not data: + break + yield data diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index d77a12f64b..c64f9dcdde 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any import rich.repr +import rich.traceback from .. import events, log from .._xterm_parser import XTermParser @@ -164,7 +165,7 @@ def on_terminal_resize(signum, stack) -> None: self.write("\x1b[?25l") # Hide cursor self.write("\033[?1003h\n") self.flush() - self._key_thread = Thread(target=self.run_input_thread) + self._key_thread = Thread(target=self._run_input_thread) send_size_event() self._key_thread.start() self._request_terminal_sync_mode_support() @@ -233,6 +234,19 @@ def close(self) -> None: if self._writer_thread is not None: self._writer_thread.stop() + 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: + self._app.call_later( + self._app.panic, + rich.traceback.Traceback(), + ) + def run_input_thread(self) -> None: """Wait for input and dispatch events.""" selector = selectors.DefaultSelector() @@ -242,8 +256,9 @@ def run_input_thread(self) -> None: def more_data() -> bool: """Check if there is more data to parse.""" + EVENT_READ = selectors.EVENT_READ for key, events in selector.select(0.01): - if events: + if events & EVENT_READ: return True return False @@ -259,11 +274,9 @@ def more_data() -> bool: while not self.exit_event.is_set(): selector_events = selector.select(0.1) for _selector_key, mask in selector_events: - if mask | EVENT_READ: + if mask & EVENT_READ: unicode_data = decode(read(fileno, 1024)) for event in feed(unicode_data): self.process_event(event) - except Exception as error: - log(error) finally: selector.close() diff --git a/src/textual/drivers/web_driver.py b/src/textual/drivers/web_driver.py index ee58d08888..7b0976df94 100644 --- a/src/textual/drivers/web_driver.py +++ b/src/textual/drivers/web_driver.py @@ -14,18 +14,26 @@ import asyncio import json import os -import selectors +import platform +import signal import sys from codecs import getincrementaldecoder from functools import partial from threading import Event, Thread -from .. import events, log +from .. import events, log, messages from .._xterm_parser import XTermParser from ..app import App from ..driver import Driver from ..geometry import Size from ._byte_stream import ByteStream +from ._input_reader import InputReader + +WINDOWS = platform.system() == "Windows" + + +class _ExitInput(Exception): + """Internal exception to force exit of input loop.""" class WebDriver(Driver): @@ -34,13 +42,21 @@ class WebDriver(Driver): def __init__( self, app: App, *, debug: bool = False, size: tuple[int, int] | None = None ): + if size is None: + try: + width = int(os.environ.get("COLUMNS", 80)) + height = int(os.environ.get("ROWS", 24)) + except ValueError: + pass + else: + size = width, height super().__init__(app, debug=debug, size=size) self.stdout = sys.__stdout__ self.fileno = sys.__stdout__.fileno() - self.in_fileno = sys.__stdin__.fileno() self._write = partial(os.write, self.fileno) self.exit_event = Event() self._key_thread: Thread = Thread(target=self.run_input_thread) + self._input_reader = InputReader() def write(self, data: str) -> None: """Write data to the output device. @@ -52,6 +68,15 @@ def write(self, data: str) -> None: data_bytes = data.encode("utf-8") self._write(b"D%s%s" % (len(data_bytes).to_bytes(4, "big"), data_bytes)) + def write_meta(self, data: dict[str, object]) -> None: + """Write meta to the controlling process (i.e. textual-web) + + Args: + data: Meta dict. + """ + meta_bytes = json.dumps(data).encode("utf-8", errors="ignore") + self._write(b"M%s%s" % (len(meta_bytes).to_bytes(4, "big"), meta_bytes)) + def flush(self) -> None: pass @@ -88,6 +113,18 @@ def start_application_mode(self) -> None: loop = asyncio.get_running_loop() + def do_exit() -> None: + """Callback to force exit.""" + asyncio.run_coroutine_threadsafe( + self._app._post_message(messages.ExitApp()), loop=loop + ) + + if not WINDOWS: + for _signal in (signal.SIGINT, signal.SIGTERM): + loop.add_signal_handler(_signal, do_exit) + + self._write(b"__GANGLION__\n") + self.write("\x1b[?1049h") # Alt screen self._enable_mouse_support() @@ -112,64 +149,61 @@ def disable_input(self) -> None: def stop_application_mode(self) -> None: """Stop application mode, restore state.""" self.exit_event.set() - self._key_thread.join() + self._input_reader.close() + self.write_meta({"type": "exit"}) def run_input_thread(self) -> None: """Wait for input and dispatch events.""" - selector = selectors.DefaultSelector() - fileno = self.in_fileno - selector.register(fileno, 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: - return True - return False - - parser = XTermParser(more_data, debug=self._debug) - feed = parser.feed - + input_reader = self._input_reader + parser = XTermParser(input_reader.more_data, debug=self._debug) utf8_decoder = getincrementaldecoder("utf-8")().decode decode = utf8_decoder - read = os.read - EVENT_READ = selectors.EVENT_READ - # The server sends us a stream of bytes, which contains the equivalent of stdin, plus # in band data packets. byte_stream = ByteStream() 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: - data = read(fileno, 1024) # raw data - - for packet_type, payload in byte_stream.feed(data): - if packet_type == "D": - # Treat as stdin - for event in feed(decode(payload)): - self.process_event(event) - else: - # Process meta information separately - self._on_meta(packet_type, payload) - except Exception as error: - log(error) + for data in input_reader: + for packet_type, payload in byte_stream.feed(data): + if packet_type == "D": + # Treat as stdin + for event in parser.feed(decode(payload)): + self.process_event(event) + else: + # Process meta information separately + self._on_meta(packet_type, payload) + except _ExitInput: + pass + except Exception: + from traceback import format_exc + + log(format_exc()) finally: - selector.close() + input_reader.close() def _on_meta(self, packet_type: str, payload: bytes) -> None: + """Private method to dispatch meta. + + Args: + packet_type: Packet type (currently always "M") + payload: Meta payload (JSON encoded as bytes). + """ payload_map = json.loads(payload) _type = payload_map.get("type") if isinstance(payload_map, dict): self.on_meta(_type, payload_map) def on_meta(self, packet_type: str, payload: dict) -> None: + """Process meta information. + + Args: + packet_type: The type of the packet. + payload: meta dict. + """ if packet_type == "resize": self._size = (payload["width"], payload["height"]) size = Size(*self._size) - event = events.Resize(size, size) - asyncio.run_coroutine_threadsafe( - self._app._post_message(event), - loop=self._loop, - ) + self._app.post_message(events.Resize(size, size)) + elif packet_type == "quit": + self._app.post_message(messages.ExitApp()) + elif packet_type == "exit": + raise _ExitInput() diff --git a/src/textual/drivers/win32.py b/src/textual/drivers/win32.py index 4942bad92f..5751af2caa 100644 --- a/src/textual/drivers/win32.py +++ b/src/textual/drivers/win32.py @@ -124,7 +124,7 @@ class INPUT_RECORD(Structure): _fields_ = [("EventType", wintypes.WORD), ("Event", InputEvent)] -def _set_console_mode(file: IO, mode: int) -> bool: +def set_console_mode(file: IO, mode: int) -> bool: """Set the console mode for a given file (stdout or stdin). Args: @@ -139,7 +139,7 @@ def _set_console_mode(file: IO, mode: int) -> bool: return success -def _get_console_mode(file: IO) -> int: +def get_console_mode(file: IO) -> int: """Get the console mode for a given file (stdout or stdin) Args: @@ -164,22 +164,22 @@ def enable_application_mode() -> Callable[[], None]: terminal_in = sys.stdin terminal_out = sys.stdout - current_console_mode_in = _get_console_mode(terminal_in) - current_console_mode_out = _get_console_mode(terminal_out) + current_console_mode_in = get_console_mode(terminal_in) + current_console_mode_out = get_console_mode(terminal_out) def restore() -> None: """Restore console mode to previous settings""" - _set_console_mode(terminal_in, current_console_mode_in) - _set_console_mode(terminal_out, current_console_mode_out) + set_console_mode(terminal_in, current_console_mode_in) + set_console_mode(terminal_out, current_console_mode_out) - _set_console_mode( + set_console_mode( terminal_out, current_console_mode_out | ENABLE_VIRTUAL_TERMINAL_PROCESSING ) - _set_console_mode(terminal_in, ENABLE_VIRTUAL_TERMINAL_INPUT) + set_console_mode(terminal_in, ENABLE_VIRTUAL_TERMINAL_INPUT) return restore -def _wait_for_handles(handles: List[HANDLE], timeout: int = -1) -> Optional[HANDLE]: +def wait_for_handles(handles: List[HANDLE], timeout: int = -1) -> Optional[HANDLE]: """ Waits for multiple handles. (Similar to 'select') Returns the handle which is ready. Returns `None` on timeout. @@ -244,7 +244,7 @@ def run(self) -> None: while not exit_requested(): # Wait for new events - if _wait_for_handles([hIn], 200) is None: + if wait_for_handles([hIn], 200) is None: # No new events continue @@ -278,7 +278,12 @@ def run(self) -> None: if keys: # Process keys - for event in parser.feed("".join(keys)): + # + # https://github.com/Textualize/textual/issues/3178 has + # the context for the encode/decode here. + for event in parser.feed( + "".join(keys).encode("utf-16", "surrogatepass").decode("utf-16") + ): self.process_event(event) if new_size is not None: # Process changed size diff --git a/src/textual/errors.py b/src/textual/errors.py index 021bcff0fa..034139e204 100644 --- a/src/textual/errors.py +++ b/src/textual/errors.py @@ -1,3 +1,8 @@ +""" +General exception classes. + +""" + from __future__ import annotations diff --git a/src/textual/events.py b/src/textual/events.py index 5c7ffaa2b5..7cff7d01d0 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -13,6 +13,7 @@ from __future__ import annotations +from dataclasses import dataclass from typing import TYPE_CHECKING, Type, TypeVar import rich.repr @@ -416,6 +417,19 @@ def get_content_offset(self, widget: Widget) -> Offset | None: """ if self.screen_offset not in widget.content_region: return None + return self.get_content_offset_capture(widget) + + def get_content_offset_capture(self, widget: Widget) -> Offset: + """Get offset from a widget's content area. + + This method works even if the offset is outside the widget content region. + + Args: + widget: Widget receiving the event. + + Returns: + An offset where the origin is at the top left of the content area. + """ return self.offset - widget.gutter.top_left def _apply_offset(self, x: int, y: int) -> MouseEvent: @@ -534,7 +548,7 @@ class Leave(Event, bubble=False, verbose=True): class Focus(Event, bubble=False): """Sent when a widget is focussed. - - [X] Bubbles + - [ ] Bubbles - [ ] Verbose """ @@ -542,11 +556,12 @@ class Focus(Event, bubble=False): class Blur(Event, bubble=False): """Sent when a widget is blurred (un-focussed). - - [X] Bubbles + - [ ] Bubbles - [ ] Verbose """ +@dataclass class DescendantFocus(Event, bubble=True, verbose=True): """Sent when a child widget is focussed. @@ -554,7 +569,16 @@ class DescendantFocus(Event, bubble=True, verbose=True): - [X] Verbose """ + widget: Widget + """The widget that was focused.""" + + @property + def control(self) -> Widget: + """The widget that was focused (alias of `widget`).""" + return self.widget + +@dataclass class DescendantBlur(Event, bubble=True, verbose=True): """Sent when a child widget is blurred. @@ -562,6 +586,14 @@ class DescendantBlur(Event, bubble=True, verbose=True): - [X] Verbose """ + widget: Widget + """The widget that was blurred.""" + + @property + def control(self) -> Widget: + """The widget that was blurred (alias of `widget`).""" + return self.widget + @rich.repr.auto class Paste(Event, bubble=True): @@ -619,3 +651,7 @@ def __init__(self, text: str, stderr: bool = False) -> None: super().__init__() self.text = text self.stderr = stderr + + def __rich_repr__(self) -> rich.repr.Result: + yield self.text + yield self.stderr diff --git a/src/textual/expand_tabs.py b/src/textual/expand_tabs.py new file mode 100644 index 0000000000..9227f796c2 --- /dev/null +++ b/src/textual/expand_tabs.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import re + +from rich.cells import cell_len + +_TABS_SPLITTER_RE = re.compile(r"(.*?\t|.+?$)") + + +def expand_tabs_inline(line: str, tab_size: int = 4) -> str: + """Expands tabs, taking into account double cell characters. + + Args: + line: The text to expand tabs in. + tab_size: Number of cells in a tab. + Returns: + New string with tabs replaced with spaces. + """ + if "\t" not in line: + return line + new_line_parts: list[str] = [] + add_part = new_line_parts.append + cell_position = 0 + parts = _TABS_SPLITTER_RE.findall(line) + + for part in parts: + if part.endswith("\t"): + part = f"{part[:-1]} " + cell_position += cell_len(part) + tab_remainder = cell_position % tab_size + if tab_remainder: + spaces = tab_size - tab_remainder + part += spaces * " " + add_part(part) + + return "".join(new_line_parts) + + +if __name__ == "__main__": + print(expand_tabs_inline("\tbar")) + print(expand_tabs_inline("1\tbar")) + print(expand_tabs_inline("12\tbar")) + print(expand_tabs_inline("123\tbar")) + print(expand_tabs_inline("1234\tbar")) + print(expand_tabs_inline("💩\tbar")) + print(expand_tabs_inline("💩💩\tbar")) + print(expand_tabs_inline("💩💩💩\tbar")) + print(expand_tabs_inline("F💩\tbar")) + print(expand_tabs_inline("F💩O\tbar")) diff --git a/src/textual/filter.py b/src/textual/filter.py index 65378818eb..7494d9a52a 100644 --- a/src/textual/filter.py +++ b/src/textual/filter.py @@ -1,3 +1,16 @@ +"""Filter classes. + +!!! note + + Filters are used internally, and not recommended for use by Textual app developers. + +Filters are used internally to process terminal output after it has been rendered. +Currently this is used internally to convert the application to monochrome, when the NO_COLOR env var is set. + +In the future, this system will be used to implement accessibility features. + +""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/src/textual/fuzzy.py b/src/textual/fuzzy.py new file mode 100644 index 0000000000..f2c46259d2 --- /dev/null +++ b/src/textual/fuzzy.py @@ -0,0 +1,165 @@ +""" +Fuzzy matcher. + +This class is used by the [command palette](guide/command_palette) to match search terms. + +""" + +from __future__ import annotations + +from re import IGNORECASE, compile, escape + +import rich.repr +from rich.style import Style +from rich.text import Text + +from ._cache import LRUCache + + +@rich.repr.auto +class Matcher: + """A fuzzy matcher.""" + + def __init__( + self, + query: str, + *, + match_style: Style | None = None, + case_sensitive: bool = False, + ) -> None: + """Initialise the fuzzy matching object. + + Args: + query: A query as typed in by the user. + match_style: The style to use to highlight matched portions of a string. + case_sensitive: Should matching be case sensitive? + """ + self._query = query + self._match_style = Style(reverse=True) if match_style is None else match_style + self._query_regex = compile( + ".*?".join(f"({escape(character)})" for character in query), + flags=0 if case_sensitive else IGNORECASE, + ) + self._cache: LRUCache[str, float] = LRUCache(1024 * 4) + + @property + def query(self) -> str: + """The query string to look for.""" + return self._query + + @property + def match_style(self) -> Style: + """The style that will be used to highlight hits in the matched text.""" + return self._match_style + + @property + def query_pattern(self) -> str: + """The regular expression pattern built from the query.""" + return self._query_regex.pattern + + @property + def case_sensitive(self) -> bool: + """Is this matcher case sensitive?""" + return not bool(self._query_regex.flags & IGNORECASE) + + def match(self, candidate: str) -> float: + """Match the candidate against the query. + + Args: + candidate: Candidate string to match against the query. + + Returns: + Strength of the match from 0 to 1. + """ + cached = self._cache.get(candidate) + if cached is not None: + return cached + match = self._query_regex.search(candidate) + if match is None: + score = 0.0 + else: + assert match.lastindex is not None + offsets = [ + match.span(group_no)[0] for group_no in range(1, match.lastindex + 1) + ] + group_count = 0 + last_offset = -2 + for offset in offsets: + if offset > last_offset + 1: + group_count += 1 + last_offset = offset + + score = 1.0 - ((group_count - 1) / len(candidate)) + self._cache[candidate] = score + return score + + def highlight(self, candidate: str) -> Text: + """Highlight the candidate with the fuzzy match. + + Args: + candidate: The candidate string to match against the query. + + Returns: + A [rich.text.Text][`Text`] object with highlighted matches. + """ + match = self._query_regex.search(candidate) + text = Text(candidate) + if match is None: + return text + assert match.lastindex is not None + offsets = [ + match.span(group_no)[0] for group_no in range(1, match.lastindex + 1) + ] + for offset in offsets: + text.stylize(self._match_style, offset, offset + 1) + + return text + + +if __name__ == "__main__": + from itertools import permutations + from string import ascii_lowercase + from time import monotonic + + from rich import print + from rich.rule import Rule + + matcher = Matcher("foo.bar") + print(Rule()) + print("Query is:", matcher.query) + print("Rule is:", matcher.query_pattern) + print(Rule()) + candidates = ( + "foo.bar", + " foo.bar ", + "Hello foo.bar world", + "f o o . b a r", + "f o o .bar", + "foo. b a r", + "Lots of text before the foo.bar", + "foo.bar up front and then lots of text afterwards", + "This has an o in it but does not have a match", + "Let's find one obvious match. But blat around always roughly.", + ) + results = sorted( + [ + (matcher.match(candidate), matcher.highlight(candidate)) + for candidate in candidates + ], + key=lambda pair: pair[0], + reverse=True, + ) + for score, highlight in results: + print(f"{score:.15f} '", highlight, "'", sep="") + print(Rule()) + + RUNS = 5 + candidates = [ + "".join(permutation) for permutation in permutations(ascii_lowercase[:10]) + ] + matcher = Matcher(ascii_lowercase[:10]) + start = monotonic() + for _ in range(RUNS): + for candidate in candidates: + _ = matcher.match(candidate) + print(f"{RUNS * len(candidates)} matches in {monotonic() - start:.5f} seconds") diff --git a/src/textual/layouts/grid.py b/src/textual/layouts/grid.py index 3b030f30f5..3ef2e315f8 100644 --- a/src/textual/layouts/grid.py +++ b/src/textual/layouts/grid.py @@ -21,7 +21,9 @@ def arrange( self, parent: Widget, children: list[Widget], size: Size ) -> ArrangeResult: styles = parent.styles - row_scalars = styles.grid_rows or [Scalar.parse("1fr")] + row_scalars = styles.grid_rows or ( + [Scalar.parse("1fr")] if size.height else [Scalar.parse("auto")] + ) column_scalars = styles.grid_columns or [Scalar.parse("1fr")] gutter_horizontal = styles.grid_gutter_horizontal gutter_vertical = styles.grid_gutter_vertical @@ -109,24 +111,110 @@ def repeat_scalars(scalars: Iterable[Scalar], count: int) -> list[Scalar]: continue cell_coord = next_coord() - # Resolve columns / rows - columns = resolve( - repeat_scalars(column_scalars, table_size_columns), - size.width, - gutter_vertical, - size, - viewport, - ) - rows = resolve( - repeat_scalars( - row_scalars, table_size_rows if table_size_rows else row + 1 - ), - size.height, - gutter_horizontal, - size, - viewport, + column_scalars = repeat_scalars(column_scalars, table_size_columns) + row_scalars = repeat_scalars( + row_scalars, table_size_rows if table_size_rows else row + 1 ) + def apply_width_limits(widget: Widget, width: int) -> int: + """Apply min and max widths to dimension. + + Args: + widget: A Widget. + width: A width. + + Returns: + New width. + """ + styles = widget.styles + if styles.min_width is not None: + width = max( + width, + int(styles.min_width.resolve(size, viewport, Fraction(width))), + ) + if styles.max_width is not None: + width = min( + width, + int(styles.max_width.resolve(size, viewport, Fraction(width))), + ) + return width + + def apply_height_limits(widget: Widget, height: int) -> int: + """Apply min and max height to a dimension. + + Args: + widget: A widget. + height: A height. + + Returns: + New height + """ + styles = widget.styles + if styles.min_height is not None: + height = max( + height, + int(styles.min_height.resolve(size, viewport, Fraction(height))), + ) + if styles.max_height is not None: + height = min( + height, + int(styles.max_height.resolve(size, viewport, Fraction(height))), + ) + return height + + # Handle any auto columns + for column, scalar in enumerate(column_scalars): + if scalar.is_auto: + width = 0.0 + for row in range(len(row_scalars)): + coord = (column, row) + try: + widget, _ = cell_map[coord] + except KeyError: + pass + else: + if widget.styles.column_span != 1: + continue + width = max( + width, + apply_width_limits( + widget, + widget.get_content_width(size, viewport) + + widget.styles.gutter.width, + ), + ) + column_scalars[column] = Scalar.from_number(width) + + columns = resolve(column_scalars, size.width, gutter_vertical, size, viewport) + + # Handle any auto rows + for row, scalar in enumerate(row_scalars): + if scalar.is_auto: + height = 0.0 + for column in range(len(column_scalars)): + coord = (column, row) + try: + widget, _ = cell_map[coord] + except KeyError: + pass + else: + if widget.styles.row_span != 1: + continue + column_width = columns[column][1] + widget_height = apply_height_limits( + widget, + widget.get_content_height( + size, + viewport, + column_width - parent.styles.grid_gutter_vertical, + ) + + widget.styles.gutter.height, + ) + height = max(height, widget_height) + row_scalars[row] = Scalar.from_number(height) + + rows = resolve(row_scalars, size.height, gutter_horizontal, size, viewport) + placements: list[WidgetPlacement] = [] add_placement = placements.append widgets: list[Widget] = [] diff --git a/src/textual/message.py b/src/textual/message.py index d80d512e8c..931c5aa21b 100644 --- a/src/textual/message.py +++ b/src/textual/message.py @@ -11,7 +11,6 @@ from . import _time from ._context import active_message_pump -from ._types import MessageTarget from .case import camel_to_snake if TYPE_CHECKING: diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index affdf08431..3d49080a6a 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -1,6 +1,11 @@ """ -A message pump is a base class for any object which processes messages, which includes Widget, Screen, and App. +A `MessagePump` is a base class for any object which processes messages, which includes Widget, Screen, and App. + +!!! tip + + Most of the method here are useful in general app development. + """ from __future__ import annotations @@ -118,7 +123,7 @@ def __init__(self, parent: MessagePump | None = None) -> None: self._last_idle: float = time() self._max_idle: float | None = None self._mounted_event = asyncio.Event() - self._next_callbacks: list[CallbackType] = [] + self._next_callbacks: list[events.Callback] = [] self._thread_id: int = threading.get_ident() @property @@ -417,7 +422,9 @@ def call_next(self, callback: Callback, *args: Any, **kwargs: Any) -> None: *args: Positional arguments to pass to the callable. **kwargs: Keyword arguments to pass to the callable. """ - self._next_callbacks.append(partial(callback, *args, **kwargs)) + callback_message = events.Callback(callback=partial(callback, *args, **kwargs)) + callback_message._prevent.update(self._get_prevented_messages()) + self._next_callbacks.append(callback_message) self.check_idle() def _on_invoke_later(self, message: messages.InvokeLater) -> None: @@ -562,7 +569,7 @@ async def _flush_next_callbacks(self) -> None: self._next_callbacks.clear() for callback in callbacks: try: - await invoke(callback) + await self._dispatch_message(callback) except Exception as error: self.app._handle_exception(error) break diff --git a/src/textual/notifications.py b/src/textual/notifications.py index 242ba895e4..e1a9fbae44 100644 --- a/src/textual/notifications.py +++ b/src/textual/notifications.py @@ -10,10 +10,19 @@ from rich.repr import Result from typing_extensions import Literal, Self, TypeAlias +from .message import Message + SeverityLevel: TypeAlias = Literal["information", "warning", "error"] """The severity level for a notification.""" +@dataclass +class Notify(Message, bubble=False): + """Message to show a notification.""" + + notification: Notification + + @dataclass class Notification: """Holds the details of a notification.""" diff --git a/src/textual/pilot.py b/src/textual/pilot.py index a94b41a908..9069f61a31 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -1,6 +1,9 @@ """ The pilot object is used by [App.run_test][textual.app.App.run_test] to programmatically operate an app. + +See the guide on how to [test Textual apps](/guide/testing). + """ from __future__ import annotations @@ -19,7 +22,7 @@ def _get_mouse_message_arguments( target: Widget, - offset: Offset = Offset(), + offset: tuple[int, int] = (0, 0), button: int = 0, shift: bool = False, meta: bool = False, @@ -42,8 +45,15 @@ def _get_mouse_message_arguments( return message_arguments +class OutOfBounds(Exception): + """Raised when the pilot mouse target is outside of the (visible) screen.""" + + class WaitForScreenTimeout(Exception): - pass + """Exception raised if messages aren't being processed quickly enough. + + If this occurs, the most likely explanation is some kind of deadlock in the app code. + """ @rich.repr.auto(angular=True) @@ -74,70 +84,115 @@ async def press(self, *keys: str) -> None: async def click( self, selector: type[Widget] | str | None = None, - offset: Offset = Offset(), + offset: tuple[int, int] = (0, 0), shift: bool = False, meta: bool = False, control: bool = False, - ) -> None: - """Simulate clicking with the mouse. + ) -> bool: + """Simulate clicking with the mouse at a specified position. + + The final position to be clicked is computed based on the selector provided and + the offset specified and it must be within the visible area of the screen. Args: - selector: The widget that should be clicked. If None, then the click - will occur relative to the screen. Note that this simply causes - a click to occur at the location of the widget. If the widget is - currently hidden or obscured by another widget, then the click may - not land on it. - offset: The offset to click within the selected widget. + selector: A selector to specify a widget that should be used as the reference + for the click offset. If this is not specified, the offset is interpreted + relative to the screen. You can use this parameter to try to click on a + specific widget. However, if the widget is currently hidden or obscured by + another widget, the click may not land on the widget you specified. + offset: The offset to click. The offset is relative to the selector provided + or to the screen, if no selector is provided. shift: Click with the shift key held down. meta: Click with the meta key held down. control: Click with the control key held down. + + Raises: + OutOfBounds: If the position to be clicked is outside of the (visible) screen. + + Returns: + True if no selector was specified or if the click landed on the selected + widget, False otherwise. """ app = self.app screen = app.screen if selector is not None: - target_widget = screen.query_one(selector) + target_widget = app.query_one(selector) else: target_widget = screen message_arguments = _get_mouse_message_arguments( target_widget, offset, button=1, shift=shift, meta=meta, control=control ) + + click_offset = Offset(message_arguments["x"], message_arguments["y"]) + if click_offset not in screen.region: + raise OutOfBounds( + "Target offset is outside of currently-visible screen region." + ) + app.post_message(MouseDown(**message_arguments)) - await self.pause(0.1) + await self.pause() app.post_message(MouseUp(**message_arguments)) - await self.pause(0.1) + await self.pause() + + # Figure out the widget under the click before we click because the app + # might react to the click and move things. + widget_at, _ = app.get_widget_at(*click_offset) app.post_message(Click(**message_arguments)) - await self.pause(0.1) + await self.pause() + + return selector is None or widget_at is target_widget async def hover( self, selector: type[Widget] | str | None | None = None, - offset: Offset = Offset(), - ) -> None: - """Simulate hovering with the mouse cursor. + offset: tuple[int, int] = (0, 0), + ) -> bool: + """Simulate hovering with the mouse cursor at a specified position. + + The final position to be hovered is computed based on the selector provided and + the offset specified and it must be within the visible area of the screen. Args: - selector: The widget that should be hovered. If None, then the click - will occur relative to the screen. Note that this simply causes - a hover to occur at the location of the widget. If the widget is - currently hidden or obscured by another widget, then the hover may - not land on it. - offset: The offset to hover over within the selected widget. + selector: A selector to specify a widget that should be used as the reference + for the hover offset. If this is not specified, the offset is interpreted + relative to the screen. You can use this parameter to try to hover a + specific widget. However, if the widget is currently hidden or obscured by + another widget, the hover may not land on the widget you specified. + offset: The offset to hover. The offset is relative to the selector provided + or to the screen, if no selector is provided. + + Raises: + OutOfBounds: If the position to be hovered is outside of the (visible) screen. + + Returns: + True if no selector was specified or if the hover landed on the selected + widget, False otherwise. """ app = self.app screen = app.screen if selector is not None: - target_widget = screen.query_one(selector) + target_widget = app.query_one(selector) else: target_widget = screen message_arguments = _get_mouse_message_arguments( target_widget, offset, button=0 ) + + hover_offset = Offset(message_arguments["x"], message_arguments["y"]) + if hover_offset not in screen.region: + raise OutOfBounds( + "Target offset is outside of currently-visible screen region." + ) + await self.pause() app.post_message(MouseMove(**message_arguments)) await self.pause() + widget_at, _ = app.get_widget_at(*hover_offset) + return selector is None or widget_at is target_widget + async def _wait_for_screen(self, timeout: float = 30.0) -> bool: """Wait for the current screen and its children to have processed all pending events. diff --git a/src/textual/reactive.py b/src/textual/reactive.py index 328a458329..d361bd0049 100644 --- a/src/textual/reactive.py +++ b/src/textual/reactive.py @@ -220,11 +220,15 @@ async def await_watcher(awaitable: Awaitable) -> None: obj.post_message(events.Callback(callback=partial(Reactive._compute, obj))) def invoke_watcher( - watch_function: Callable, old_value: object, value: object + watcher_object: Reactable, + watch_function: Callable, + old_value: object, + value: object, ) -> None: """Invoke a watch function. Args: + watcher_object: The object watching for the changes. watch_function: A watch function, which may be sync or async. old_value: The old value of the attribute. value: The new value of the attribute. @@ -239,17 +243,15 @@ def invoke_watcher( watch_result = watch_function() if isawaitable(watch_result): # Result is awaitable, so we need to await it within an async context - obj.post_message( - events.Callback(callback=partial(await_watcher, watch_result)) - ) + watcher_object.call_next(partial(await_watcher, watch_result)) private_watch_function = getattr(obj, f"_watch_{name}", None) if callable(private_watch_function): - invoke_watcher(private_watch_function, old_value, value) + invoke_watcher(obj, private_watch_function, old_value, value) public_watch_function = getattr(obj, f"watch_{name}", None) if callable(public_watch_function): - invoke_watcher(public_watch_function, old_value, value) + invoke_watcher(obj, public_watch_function, old_value, value) # Process "global" watchers watchers: list[tuple[Reactable, Callable]] @@ -263,7 +265,7 @@ def invoke_watcher( ] for reactable, callback in watchers: with reactable.prevent(*obj._prevent_message_types_stack[-1]): - invoke_watcher(callback, old_value, value) + invoke_watcher(reactable, callback, old_value, value) @classmethod def _compute(cls, obj: Reactable) -> None: diff --git a/src/textual/screen.py b/src/textual/screen.py index 12514bdf3c..be09b66e7c 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -5,9 +5,12 @@ from __future__ import annotations +import asyncio from functools import partial +from operator import attrgetter from typing import ( TYPE_CHECKING, + Any, Awaitable, Callable, ClassVar, @@ -47,6 +50,8 @@ if TYPE_CHECKING: from typing_extensions import Final + from .command import Provider + # Unused & ignored imports are needed for the docs to link to these objects: from .errors import NoWidget # type: ignore # noqa: F401 from .message_pump import MessagePump @@ -68,19 +73,23 @@ class ResultCallback(Generic[ScreenResultType]): def __init__( self, - requester: Widget | None, + requester: MessagePump, callback: ScreenResultCallbackType[ScreenResultType] | None, + future: asyncio.Future[ScreenResultType] | None = None, ) -> None: """Initialise the result callback object. Args: requester: The object making a request for the callback. callback: The callback function. + future: A Future to hold the result. """ - self.requester: Widget | None = requester + self.requester = requester """The object in the DOM that requested the callback.""" self.callback: ScreenResultCallbackType | None = callback """The callback function.""" + self.future = future + """A future for the result""" def __call__(self, result: ScreenResultType) -> None: """Call the callback, passing the given result. @@ -91,6 +100,8 @@ def __call__(self, result: ScreenResultType) -> None: Note: If the requested or the callback are `None` this will be a no-op. """ + if self.future is not None: + self.future.set_result(result) if self.requester is not None and self.callback is not None: self.requester.call_next(self.callback, result) @@ -127,10 +138,37 @@ class Screen(Generic[ScreenResultType], Widget): background: $surface; } """ + + TITLE: ClassVar[str | None] = None + """A class variable to set the *default* title for the screen. + + This overrides the app title. + To update the title while the screen is running, + you can set the [title][textual.screen.Screen.title] attribute. + """ + + SUB_TITLE: ClassVar[str | None] = None + """A class variable to set the *default* sub-title for the screen. + + This overrides the app sub-title. + To update the sub-title while the screen is running, + you can set the [sub_title][textual.screen.Screen.sub_title] attribute. + """ + focused: Reactive[Widget | None] = Reactive(None) """The focused [widget][textual.widget.Widget] or `None` for no focus.""" stack_updates: Reactive[int] = Reactive(0, repaint=False) """An integer that updates when the screen is resumed.""" + sub_title: Reactive[str | None] = Reactive(None, compute=False) + """Screen sub-title to override [the app sub-title][textual.app.App.sub_title].""" + title: Reactive[str | None] = Reactive(None, compute=False) + """Screen title to override [the app title][textual.app.App.title].""" + + COMMANDS: ClassVar[set[type[Provider]]] = set() + """Command providers used by the [command palette](/guide/command_palette), associated with the screen. + + Should be a set of [`command.Provider`][textual.command.Provider] classes. + """ BINDINGS = [ Binding("tab", "focus_next", "Focus Next", show=False), @@ -172,6 +210,9 @@ def __init__( ] self.css_path = css_paths + self.title = self.TITLE + self.sub_title = self.SUB_TITLE + @property def is_modal(self) -> bool: """Is the screen modal?""" @@ -293,18 +334,42 @@ def focus_chain(self) -> list[Widget]: widgets: list[Widget] = [] add_widget = widgets.append - stack: list[Iterator[Widget]] = [iter(self.focusable_children)] - pop = stack.pop - push = stack.append + focus_sorter = attrgetter("_focus_sort_key") + # We traverse the DOM and keep track of where we are at with a node stack. + # Additionally, we manually keep track of the visibility of the DOM + # instead of relying on the property `.visible` to save on DOM traversals. + # node_stack: list[tuple[iterator over node children, node visibility]] + node_stack: list[tuple[Iterator[Widget], bool]] = [ + ( + iter(sorted(self.displayed_children, key=focus_sorter)), + self.visible, + ) + ] + pop = node_stack.pop + push = node_stack.append - while stack: - node = next(stack[-1], None) + while node_stack: + children_iterator, parent_visibility = node_stack[-1] + node = next(children_iterator, None) if node is None: pop() else: + if node.disabled: + continue + node_styles_visibility = node.styles.get_rule("visibility") + node_is_visible = ( + node_styles_visibility != "hidden" + if node_styles_visibility + else parent_visibility # Inherit visibility if the style is unset. + ) if node.is_container and node.can_focus_children: - push(iter(node.focusable_children)) - if node.focusable: + sorted_displayed_children = sorted( + node.displayed_children, key=focus_sorter + ) + push((iter(sorted_displayed_children), node_is_visible)) + # Same check as `if node.focusable`, but we cached inherited visibility + # and we also skipped disabled nodes altogether. + if node_is_visible and node.can_focus: add_widget(node) return widgets @@ -457,7 +522,7 @@ def _reset_focus( chosen = candidate break - # Go with the what was found. + # Go with what was found. self.set_focus(chosen) def _update_focus_styles( @@ -627,17 +692,19 @@ def _invoke_later(self, callback: CallbackType, sender: MessagePump) -> None: def _push_result_callback( self, - requester: Widget | None, + requester: MessagePump, callback: ScreenResultCallbackType[ScreenResultType] | None, + future: asyncio.Future[ScreenResultType] | None = None, ) -> None: """Add a result callback to the screen. Args: requester: The object requesting the callback. callback: The callback. + future: A Future to hold the result. """ self._result_callbacks.append( - ResultCallback[ScreenResultType](requester, callback) + ResultCallback[ScreenResultType](requester, callback, future) ) def _pop_result_callback(self) -> None: @@ -895,7 +962,9 @@ def _forward_event(self, event: events.Event) -> None: except errors.NoWidget: self.set_focus(None) else: - if isinstance(event, events.MouseUp) and widget.focusable: + if isinstance(event, events.MouseDown) and widget.focusable: + self.set_focus(widget) + elif isinstance(event, events.MouseUp) and widget.focusable: if self.focused is not widget: self.set_focus(widget) event.stop() @@ -977,6 +1046,14 @@ def can_view(self, widget: Widget) -> bool: # Failing that fall back to normal checking. return super().can_view(widget) + def validate_title(self, title: Any) -> str | None: + """Ensure the title is a string or `None`.""" + return None if title is None else str(title) + + def validate_sub_title(self, sub_title: Any) -> str | None: + """Ensure the sub-title is a string or `None`.""" + return None if sub_title is None else str(sub_title) + @rich.repr.auto class ModalScreen(Screen[ScreenResultType]): diff --git a/src/textual/suggester.py b/src/textual/suggester.py index 362fe89f6d..505993b43a 100644 --- a/src/textual/suggester.py +++ b/src/textual/suggester.py @@ -1,3 +1,9 @@ +""" + +The `Suggester` class is used by the [Input](/widgets/input) widget. + +""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/src/textual/tree-sitter/highlights/bash.scm b/src/textual/tree-sitter/highlights/bash.scm new file mode 100644 index 0000000000..23bf03e697 --- /dev/null +++ b/src/textual/tree-sitter/highlights/bash.scm @@ -0,0 +1,145 @@ +(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 "^#!/")) diff --git a/src/textual/tree-sitter/highlights/css.scm b/src/textual/tree-sitter/highlights/css.scm new file mode 100644 index 0000000000..b26f0ec96c --- /dev/null +++ b/src/textual/tree-sitter/highlights/css.scm @@ -0,0 +1,91 @@ +[ + "@media" + "@charset" + "@namespace" + "@supports" + "@keyframes" + (at_keyword) + (to) + (from) + ] @keyword + +"@import" @include + +(comment) @comment @spell + +[ + (tag_name) + (nesting_selector) + (universal_selector) + ] @type + +(function_name) @function + +[ + "~" + ">" + "+" + "-" + "*" + "/" + "=" + "^=" + "|=" + "~=" + "$=" + "*=" + "and" + "or" + "not" + "only" + ] @operator + +(important) @type.qualifier + +(attribute_selector (plain_value) @string) +(pseudo_element_selector "::" (tag_name) @property) +(pseudo_class_selector (class_name) @property) + +[ + (class_name) + (id_name) + (property_name) + (feature_name) + (attribute_name) + ] @property + +(namespace_name) @namespace + +((property_name) @type.definition + (#lua-match? @type.definition "^[-][-]")) +((plain_value) @type + (#lua-match? @type "^[-][-]")) + +[ + (string_value) + (color_value) + (unit) + ] @string + +[ + (integer_value) + (float_value) + ] @number + +[ + "#" + "," + "." + ":" + "::" + ";" + ] @punctuation.delimiter + +[ + "{" + ")" + "(" + "}" + ] @punctuation.bracket + +(ERROR) @error diff --git a/src/textual/tree-sitter/highlights/html.scm b/src/textual/tree-sitter/highlights/html.scm new file mode 100644 index 0000000000..15f2adb436 --- /dev/null +++ b/src/textual/tree-sitter/highlights/html.scm @@ -0,0 +1,64 @@ +(tag_name) @tag +(erroneous_end_tag_name) @html.end_tag_error +(comment) @comment +(attribute_name) @tag.attribute +(attribute + (quoted_attribute_value) @string) +(text) @text @spell + +((element (start_tag (tag_name) @_tag) (text) @text.title) + (#eq? @_tag "title")) + +((element (start_tag (tag_name) @_tag) (text) @text.title.1) + (#eq? @_tag "h1")) + +((element (start_tag (tag_name) @_tag) (text) @text.title.2) + (#eq? @_tag "h2")) + +((element (start_tag (tag_name) @_tag) (text) @text.title.3) + (#eq? @_tag "h3")) + +((element (start_tag (tag_name) @_tag) (text) @text.title.4) + (#eq? @_tag "h4")) + +((element (start_tag (tag_name) @_tag) (text) @text.title.5) + (#eq? @_tag "h5")) + +((element (start_tag (tag_name) @_tag) (text) @text.title.6) + (#eq? @_tag "h6")) + +((element (start_tag (tag_name) @_tag) (text) @text.strong) + (#any-of? @_tag "strong" "b")) + +((element (start_tag (tag_name) @_tag) (text) @text.emphasis) + (#any-of? @_tag "em" "i")) + +((element (start_tag (tag_name) @_tag) (text) @text.strike) + (#any-of? @_tag "s" "del")) + +((element (start_tag (tag_name) @_tag) (text) @text.underline) + (#eq? @_tag "u")) + +((element (start_tag (tag_name) @_tag) (text) @text.literal) + (#any-of? @_tag "code" "kbd")) + +((element (start_tag (tag_name) @_tag) (text) @text.uri) + (#eq? @_tag "a")) + +((attribute + (attribute_name) @_attr + (quoted_attribute_value (attribute_value) @text.uri)) + (#any-of? @_attr "href" "src")) + +[ + "<" + ">" + "" +] @tag.delimiter + +"=" @operator + +(doctype) @constant + +"" + "=" + "==" + ">" + ">=" + ">>" + ">>=" + "@" + "@=" + "|" + "|=" + "~" + "->" +] @operator + +; Keywords +[ + "and" + "in" + "is" + "not" + "or" + "del" +] @keyword.operator + +[ + "def" + "lambda" +] @keyword.function + +[ + "assert" + "async" + "await" + "class" + "exec" + "global" + "nonlocal" + "pass" + "print" + "with" + "as" +] @keyword + +[ + "return" + "yield" +] @keyword.return +(yield "from" @keyword.return) + +(future_import_statement + "from" @include + "__future__" @constant.builtin) +(import_from_statement "from" @include) +"import" @include + +(aliased_import "as" @include) + +["if" "elif" "else" "match" "case"] @conditional + +["for" "while" "break" "continue"] @repeat + +[ + "try" + "except" + "raise" + "finally" +] @exception + +(raise_statement "from" @exception) + +(try_statement + (else_clause + "else" @exception)) + +["(" ")" "[" "]" "{" "}"] @punctuation.bracket + +(interpolation + "{" @punctuation.special + "}" @punctuation.special) + +["," "." ":" ";" (ellipsis)] @punctuation.delimiter + +;; Class definitions + +(class_definition name: (identifier) @type.class) + +(class_definition + body: (block + (function_definition + name: (identifier) @method))) + +(class_definition + superclasses: (argument_list + (identifier) @type)) + +((class_definition + body: (block + (expression_statement + (assignment + left: (identifier) @field)))) + (#match? @field "^([A-Z])@!.*$")) +((class_definition + body: (block + (expression_statement + (assignment + left: (_ + (identifier) @field))))) + (#match? @field "^([A-Z])@!.*$")) + +((class_definition + (block + (function_definition + name: (identifier) @constructor))) + (#any-of? @constructor "__new__" "__init__")) + +;; Error +(ERROR) @error diff --git a/src/textual/tree-sitter/highlights/regex.scm b/src/textual/tree-sitter/highlights/regex.scm new file mode 100644 index 0000000000..7c671c2c04 --- /dev/null +++ b/src/textual/tree-sitter/highlights/regex.scm @@ -0,0 +1,34 @@ +;; Forked from tree-sitter-regex +;; The MIT License (MIT) Copyright (c) 2014 Max Brunsfeld +[ + "(" + ")" + "(?" + "(?:" + "(?<" + ">" + "[" + "]" + "{" + "}" +] @regex.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 + +[ + (control_letter_escape) + (character_class_escape) + (control_escape) + (start_assertion) + (end_assertion) + (boundary_assertion) + (non_boundary_assertion) +] @string.escape + +[ "*" "+" "?" "|" "=" "!" ] @regex.operator diff --git a/src/textual/tree-sitter/highlights/sql.scm b/src/textual/tree-sitter/highlights/sql.scm new file mode 100644 index 0000000000..03a15fe381 --- /dev/null +++ b/src/textual/tree-sitter/highlights/sql.scm @@ -0,0 +1,114 @@ +(string) @string +(number) @number +(comment) @comment + +(function_call + function: (identifier) @function) + +[ + (NULL) + (TRUE) + (FALSE) +] @constant.builtin + +([ + (type_cast + (type (identifier) @type.builtin)) + (create_function_statement + (type (identifier) @type.builtin)) + (create_function_statement + (create_function_parameters + (create_function_parameter (type (identifier) @type.builtin)))) + (create_type_statement + (type_spec_composite (type (identifier) @type.builtin))) + (create_table_statement + (table_parameters + (table_column (type (identifier) @type.builtin)))) + ] + (#match? + @type.builtin + "^(bigint|BIGINT|int8|INT8|bigserial|BIGSERIAL|serial8|SERIAL8|bit|BIT|varbit|VARBIT|boolean|BOOLEAN|bool|BOOL|box|BOX|bytea|BYTEA|character|CHARACTER|char|CHAR|varchar|VARCHAR|cidr|CIDR|circle|CIRCLE|date|DATE|float8|FLOAT8|inet|INET|integer|INTEGER|int|INT|int4|INT4|interval|INTERVAL|json|JSON|jsonb|JSONB|line|LINE|lseg|LSEG|macaddr|MACADDR|money|MONEY|numeric|NUMERIC|decimal|DECIMAL|path|PATH|pg_lsn|PG_LSN|point|POINT|polygon|POLYGON|real|REAL|float4|FLOAT4|smallint|SMALLINT|int2|INT2|smallserial|SMALLSERIAL|serial2|SERIAL2|serial|SERIAL|serial4|SERIAL4|text|TEXT|time|TIME|time|TIME|timestamp|TIMESTAMP|tsquery|TSQUERY|tsvector|TSVECTOR|txid_snapshot|TXID_SNAPSHOT|enum|ENUM|range|RANGE)$")) + +(identifier) @variable + +[ + "::" + "<" + "<=" + "<>" + "=" + ">" + ">=" +] @operator + +[ + "(" + ")" + "[" + "]" +] @punctuation.bracket + +[ + ";" + "." +] @punctuation.delimiter + +[ + (type) + (array_type) +] @type + +[ + (primary_key_constraint) + (unique_constraint) + (null_constraint) +] @keyword + +[ + "AND" + "AS" + "AUTO_INCREMENT" + "CREATE" + "CREATE_DOMAIN" + "CREATE_OR_REPLACE_FUNCTION" + "CREATE_SCHEMA" + "TABLE" + "TEMPORARY" + "CREATE_TYPE" + "DATABASE" + "FROM" + "GRANT" + "GROUP_BY" + "IF_NOT_EXISTS" + "INDEX" + "INNER" + "INSERT" + "INTO" + "IN" + "JOIN" + "LANGUAGE" + "LEFT" + "LOCAL" + "NOT" + "ON" + "OR" + "ORDER_BY" + "OUTER" + "PRIMARY_KEY" + "PUBLIC" + "RETURNS" + "SCHEMA" + "SELECT" + "SESSION" + "SET" + "TABLE" + "TIME_ZONE" + "TO" + "UNIQUE" + "UPDATE" + "USAGE" + "VALUES" + "WHERE" + "WITH" + "WITHOUT" +] @keyword diff --git a/src/textual/tree-sitter/highlights/toml.scm b/src/textual/tree-sitter/highlights/toml.scm new file mode 100644 index 0000000000..9228d28072 --- /dev/null +++ b/src/textual/tree-sitter/highlights/toml.scm @@ -0,0 +1,36 @@ +; Properties +;----------- + +(bare_key) @toml.type +(quoted_key) @string +(pair (bare_key)) @property + +; Literals +;--------- + +(boolean) @boolean +(comment) @comment @spell +(string) @string +(integer) @number +(float) @float +(offset_date_time) @toml.datetime +(local_date_time) @toml.datetime +(local_date) @toml.datetime +(local_time) @toml.datetime + +; Punctuation +;------------ + +"." @punctuation.delimiter +"," @punctuation.delimiter + +"=" @toml.operator + +"[" @punctuation.bracket +"]" @punctuation.bracket +"[[" @punctuation.bracket +"]]" @punctuation.bracket +"{" @punctuation.bracket +"}" @punctuation.bracket + +(ERROR) @toml.error diff --git a/src/textual/tree-sitter/highlights/yaml.scm b/src/textual/tree-sitter/highlights/yaml.scm new file mode 100644 index 0000000000..a57f464dfc --- /dev/null +++ b/src/textual/tree-sitter/highlights/yaml.scm @@ -0,0 +1,53 @@ +(boolean_scalar) @boolean +(null_scalar) @constant.builtin +(double_quote_scalar) @string +(single_quote_scalar) @string +((block_scalar) @string (#set! "priority" 99)) +(string_scalar) @string +(escape_sequence) @string.escape +(integer_scalar) @number +(float_scalar) @number +(comment) @comment +(anchor_name) @type +(alias_name) @type +(tag) @type +(ERROR) @error + +[ + (yaml_directive) + (tag_directive) + (reserved_directive) +] @preproc + +(block_mapping_pair + key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @yaml.field)) +(block_mapping_pair + key: (flow_node (plain_scalar (string_scalar) @yaml.field))) + +(flow_mapping + (_ key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @yaml.field))) +(flow_mapping + (_ key: (flow_node (plain_scalar (string_scalar) @yaml.field)))) + +[ + "," + "-" + ":" + ">" + "?" + "|" +] @punctuation.delimiter + +[ + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "*" + "&" + "---" + "..." +] @punctuation.special diff --git a/src/textual/types.py b/src/textual/types.py index 57466874ae..024d388f24 100644 --- a/src/textual/types.py +++ b/src/textual/types.py @@ -5,10 +5,17 @@ from ._animator import Animatable, EasingFunction from ._context import NoActiveAppError from ._path import CSSPathError, CSSPathType -from ._types import CallbackType, MessageTarget, WatchCallbackType +from ._types import ( + CallbackType, + IgnoreReturnCallbackType, + MessageTarget, + UnusedParameter, + WatchCallbackType, +) from .actions import ActionParseResult from .css.styles import RenderStyles from .widgets._data_table import CursorType +from .widgets._input import InputValidationOn __all__ = [ "ActionParseResult", @@ -18,8 +25,11 @@ "CSSPathType", "CursorType", "EasingFunction", + "IgnoreReturnCallbackType", + "InputValidationOn", "MessageTarget", "NoActiveAppError", "RenderStyles", + "UnusedParameter", "WatchCallbackType", ] diff --git a/src/textual/widget.py b/src/textual/widget.py index 6d0673ee4d..0fb94ad2e1 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -12,6 +12,7 @@ from types import TracebackType from typing import ( TYPE_CHECKING, + Awaitable, ClassVar, Collection, Generator, @@ -45,7 +46,6 @@ from ._arrange import DockArrangeResult, arrange from ._asyncio import create_task from ._cache import FIFOCache -from ._callback import invoke from ._compose import compose from ._context import NoActiveAppError, active_app from ._easing import DEFAULT_SCROLL_EASING @@ -256,6 +256,12 @@ class Widget(DOMNode): """ COMPONENT_CLASSES: ClassVar[set[str]] = set() + BORDER_TITLE: ClassVar[str] = "" + """Initial value for border_title attribute.""" + + BORDER_SUBTITLE: ClassVar[str] = "" + """Initial value for border_subtitle attribute.""" + can_focus: bool = False """Widget may receive focus.""" can_focus_children: bool = True @@ -273,6 +279,8 @@ class Widget(DOMNode): """The current hover style (style under the mouse cursor). Read only.""" highlight_link_id: Reactive[str] = Reactive("") """The currently highlighted link id. Read only.""" + loading: Reactive[bool] = Reactive(False) + """If set to `True` this widget will temporarily be replaced with a loading indicator.""" def __init__( self, @@ -349,6 +357,10 @@ def __init__( self._add_children(*children) self.disabled = disabled + if self.BORDER_TITLE: + self.border_title = self.BORDER_TITLE + if self.BORDER_SUBTITLE: + self.border_subtitle = self.BORDER_SUBTITLE virtual_size: Reactive[Size] = Reactive(Size(0, 0), layout=True) """The virtual (scrollable) [size][textual.geometry.Size] of the widget.""" @@ -488,6 +500,29 @@ def __exit__( else: self.app._composed[-1].append(composed) + def set_loading(self, loading: bool) -> Awaitable: + """Set or reset the loading state of this widget. + + A widget in a loading state will display a LoadingIndicator that obscures the widget. + + Args: + loading: `True` to put the widget into a loading state, or `False` to reset the loading state. + + Returns: + An optional awaitable. + """ + from textual.widgets import LoadingIndicator + + if loading: + loading_indicator = LoadingIndicator() + return loading_indicator.apply(self) + else: + return LoadingIndicator.clear(self) + + async def _watch_loading(self, loading: bool) -> None: + """Called when the 'loading' reactive is changed.""" + await self.set_loading(loading) + ExpectType = TypeVar("ExpectType", bound="Widget") @overload @@ -906,9 +941,13 @@ def _post_register(self, app: App) -> None: app: App instance. """ # Parse the Widget's CSS - for path, css, tie_breaker in self._get_default_css(): + for path, css, tie_breaker, scope in self._get_default_css(): self.app.stylesheet.add_source( - css, path=path, is_default_css=True, tie_breaker=tie_breaker + css, + path=path, + is_default_css=True, + tie_breaker=tie_breaker, + scope=scope, ) def _get_box_model( @@ -1207,7 +1246,7 @@ def vertical_scrollbar(self) -> ScrollBar: @property def horizontal_scrollbar(self) -> ScrollBar: - """The a horizontal scrollbar. + """The horizontal scrollbar. Note: This will *create* a scrollbar if one doesn't exist. @@ -1498,17 +1537,7 @@ def _self_or_ancestors_disabled(self) -> bool: @property def focusable(self) -> bool: """Can this widget currently be focused?""" - return self.can_focus and not self._self_or_ancestors_disabled - - @property - def focusable_children(self) -> list[Widget]: - """Get the children which may be focused. - - Returns: - List of widgets that can receive focus. - """ - focusable = [child for child in self._nodes if child.display and child.visible] - return sorted(focusable, key=attrgetter("_focus_sort_key")) + return self.can_focus and self.visible and not self._self_or_ancestors_disabled @property def _focus_sort_key(self) -> tuple[int, int]: @@ -2698,8 +2727,8 @@ def _arrange_scrollbars(self, region: Region) -> Iterable[tuple[Widget, Region]] horizontal_scrollbar_region, scrollbar_corner_gap, ) = region.split( - -scrollbar_size_vertical, - -scrollbar_size_horizontal, + region.width - scrollbar_size_vertical, + region.height - scrollbar_size_horizontal, ) if scrollbar_corner_gap: yield self.scrollbar_corner, scrollbar_corner_gap @@ -2716,7 +2745,7 @@ def _arrange_scrollbars(self, region: Region) -> Iterable[tuple[Widget, Region]] elif show_vertical_scrollbar: window_region, scrollbar_region = region.split_vertical( - -scrollbar_size_vertical + region.width - scrollbar_size_vertical ) if scrollbar_region: scrollbar = self.vertical_scrollbar @@ -2725,7 +2754,7 @@ def _arrange_scrollbars(self, region: Region) -> Iterable[tuple[Widget, Region]] yield scrollbar, scrollbar_region elif show_horizontal_scrollbar: window_region, scrollbar_region = region.split_horizontal( - -scrollbar_size_horizontal + region.height - scrollbar_size_horizontal ) if scrollbar_region: scrollbar = self.horizontal_scrollbar @@ -2758,6 +2787,7 @@ def get_pseudo_classes(self) -> Iterable[str]: except NoScreen: pass else: + yield "dark" if self.app.dark else "light" if focused: node = focused while node is not None: @@ -3187,7 +3217,7 @@ def release_mouse(self) -> None: def begin_capture_print(self, stdout: bool = True, stderr: bool = True) -> None: """Capture text from print statements (or writes to stdout / stderr). - If printing is captured, the widget will be send an [events.Print][textual.events.Print] message. + If printing is captured, the widget will be sent an [events.Print][textual.events.Print] message. Call [end_capture_print][textual.widget.Widget.end_capture_print] to disable print capture. @@ -3281,12 +3311,14 @@ def _on_enter(self, event: events.Enter) -> None: def _on_focus(self, event: events.Focus) -> None: self.has_focus = True self.refresh() - self.post_message(events.DescendantFocus()) + if self.parent is not None: + self.parent.post_message(events.DescendantFocus(self)) def _on_blur(self, event: events.Blur) -> None: self.has_focus = False self.refresh() - self.post_message(events.DescendantBlur()) + if self.parent is not None: + self.parent.post_message(events.DescendantBlur(self)) def _on_mouse_scroll_down(self, event: events.MouseScrollDown) -> None: if event.ctrl or event.shift: @@ -3390,18 +3422,19 @@ def notify( title: str = "", severity: SeverityLevel = "information", timeout: float = Notification.timeout, - ) -> Notification: + ) -> None: """Create a notification. + !!! tip + + This method is thread-safe. + Args: message: The message for the notification. title: The title for the notification. severity: The severity of the notification. timeout: The timeout for the notification. - Returns: - The new notification. - See [`App.notify`][textual.app.App.notify] for the full documentation for this method. """ diff --git a/src/textual/widgets/__init__.py b/src/textual/widgets/__init__.py index 8c71dfa7fd..cd6e21f13b 100644 --- a/src/textual/widgets/__init__.py +++ b/src/textual/widgets/__init__.py @@ -12,6 +12,7 @@ from ..widget import Widget from ._button import Button from ._checkbox import Checkbox + from ._collapsible import Collapsible from ._content_switcher import ContentSwitcher from ._data_table import DataTable from ._digits import Digits @@ -32,6 +33,7 @@ from ._radio_button import RadioButton from ._radio_set import RadioSet from ._rich_log import RichLog + from ._rule import Rule from ._select import Select from ._selection_list import SelectionList from ._sparkline import Sparkline @@ -39,14 +41,15 @@ from ._switch import Switch from ._tabbed_content import TabbedContent, TabPane from ._tabs import Tab, Tabs + from ._text_area import TextArea from ._tooltip import Tooltip from ._tree import Tree from ._welcome import Welcome - __all__ = [ "Button", "Checkbox", + "Collapsible", "ContentSwitcher", "DataTable", "Digits", @@ -67,6 +70,7 @@ "ProgressBar", "RadioButton", "RadioSet", + "Rule", "Select", "SelectionList", "Sparkline", @@ -76,6 +80,7 @@ "TabbedContent", "TabPane", "Tabs", + "TextArea", "RichLog", "Tooltip", "Tree", diff --git a/src/textual/widgets/__init__.pyi b/src/textual/widgets/__init__.pyi index 86f17d13cb..d4db2f8f52 100644 --- a/src/textual/widgets/__init__.pyi +++ b/src/textual/widgets/__init__.pyi @@ -1,6 +1,7 @@ # This stub file must re-export every classes exposed in the __init__.py's `__all__` list: from ._button import Button as Button from ._checkbox import Checkbox as Checkbox +from ._collapsible import Collapsible as Collapsible from ._content_switcher import ContentSwitcher as ContentSwitcher from ._data_table import DataTable as DataTable from ._digits import Digits as Digits @@ -22,6 +23,7 @@ from ._progress_bar import ProgressBar as ProgressBar from ._radio_button import RadioButton as RadioButton from ._radio_set import RadioSet as RadioSet from ._rich_log import RichLog as RichLog +from ._rule import Rule as Rule from ._select import Select as Select from ._selection_list import SelectionList as SelectionList from ._sparkline import Sparkline as Sparkline @@ -31,6 +33,7 @@ from ._tabbed_content import TabbedContent as TabbedContent from ._tabbed_content import TabPane as TabPane from ._tabs import Tab as Tab from ._tabs import Tabs as Tabs +from ._text_area import TextArea as TextArea from ._tooltip import Tooltip as Tooltip from ._tree import Tree as Tree from ._welcome import Welcome as Welcome diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index 40be74adbf..413c3b2258 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -3,6 +3,8 @@ from functools import partial import rich.repr +from rich.console import RenderableType +from rich.padding import Padding from rich.text import Text, TextType from typing_extensions import Literal, Self @@ -33,7 +35,7 @@ class Button(Static, can_focus=True): Button { width: auto; min-width: 16; - height: 3; + height: auto; background: $panel; color: $text; border: none; @@ -158,7 +160,7 @@ class Button(Static, can_focus=True): variant = reactive("default") """The variant name for the button.""" - class Pressed(Message, bubble=True): + class Pressed(Message): """Event sent when a `Button` is pressed. Can be handled using `on_button_pressed` in a subclass of @@ -228,10 +230,8 @@ def validate_label(self, label: TextType) -> TextType: return Text.from_markup(label) return label - def render(self) -> TextType: - label = Text.assemble(" ", self.label, " ") - label.stylize(self.text_style) - return label + def render(self) -> RenderableType: + return Padding(self.label, (0, 1), expand=False) async def _on_click(self, event: events.Click) -> None: event.stop() diff --git a/src/textual/widgets/_collapsible.py b/src/textual/widgets/_collapsible.py new file mode 100644 index 0000000000..5901cbc9de --- /dev/null +++ b/src/textual/widgets/_collapsible.py @@ -0,0 +1,176 @@ +from __future__ import annotations + +from rich.console import RenderableType +from rich.text import Text + +from .. import events +from ..app import ComposeResult +from ..binding import Binding +from ..containers import Container +from ..css.query import NoMatches +from ..message import Message +from ..reactive import reactive +from ..widget import Widget + +__all__ = ["Collapsible", "CollapsibleTitle"] + + +class CollapsibleTitle(Widget, can_focus=True): + """Title and symbol for the Collapsible.""" + + DEFAULT_CSS = """ + CollapsibleTitle { + width: auto; + height: auto; + padding: 0 1 0 1; + } + + CollapsibleTitle:hover { + background: $foreground 10%; + color: $text; + } + + CollapsibleTitle:focus { + background: $accent; + color: $text; + } + """ + + BINDINGS = [Binding("enter", "toggle", "Toggle collapsible", show=False)] + """ + | Key(s) | Description | + | :- | :- | + | enter | Toggle the collapsible. | + """ + + collapsed = reactive(True) + + def __init__( + self, + *, + label: str, + collapsed_symbol: str, + expanded_symbol: str, + collapsed: bool, + ) -> None: + super().__init__() + self.collapsed_symbol = collapsed_symbol + self.expanded_symbol = expanded_symbol + self.label = label + self.collapse = collapsed + + class Toggle(Message): + """Request toggle.""" + + async def _on_click(self, event: events.Click) -> None: + """Inform ancestor we want to toggle.""" + event.stop() + self.post_message(self.Toggle()) + + def action_toggle(self) -> None: + """Toggle the state of the parent collapsible.""" + self.post_message(self.Toggle()) + + def render(self) -> RenderableType: + """Compose right/down arrow and label.""" + if self.collapsed: + return Text(f"{self.collapsed_symbol} {self.label}") + else: + return Text(f"{self.expanded_symbol} {self.label}") + + +class Collapsible(Widget): + """A collapsible container.""" + + collapsed = reactive(True) + + DEFAULT_CSS = """ + Collapsible { + width: 1fr; + height: auto; + background: $boost; + border-top: hkey $background; + padding-bottom: 1; + padding-left: 1; + } + + Collapsible.-collapsed > Contents { + display: none; + } + """ + + class Contents(Container): + DEFAULT_CSS = """ + Contents { + width: 100%; + height: auto; + padding: 1 0 0 3; + } + """ + + def __init__( + self, + *children: Widget, + title: str = "Toggle", + collapsed: bool = True, + collapsed_symbol: str = "▶", + expanded_symbol: str = "▼", + name: str | None = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + ) -> None: + """Initialize a Collapsible widget. + + Args: + *children: Contents that will be collapsed/expanded. + title: Title of the collapsed/expanded contents. + collapsed: Default status of the contents. + collapsed_symbol: Collapsed symbol before the title. + expanded_symbol: Expanded symbol before the title. + name: The name of the collapsible. + id: The ID of the collapsible in the DOM. + classes: The CSS classes of the collapsible. + disabled: Whether the collapsible is disabled or not. + """ + self._title = CollapsibleTitle( + label=title, + collapsed_symbol=collapsed_symbol, + expanded_symbol=expanded_symbol, + collapsed=collapsed, + ) + self._contents_list: list[Widget] = list(children) + super().__init__(name=name, id=id, classes=classes, disabled=disabled) + self.collapsed = collapsed + + def on_collapsible_title_toggle(self, event: CollapsibleTitle.Toggle) -> None: + event.stop() + self.collapsed = not self.collapsed + + def _watch_collapsed(self, collapsed: bool) -> None: + """Update collapsed state when reactive is changed.""" + self._update_collapsed(collapsed) + + def _update_collapsed(self, collapsed: bool) -> None: + """Update children to match collapsed state.""" + try: + self._title.collapsed = collapsed + self.set_class(collapsed, "-collapsed") + except NoMatches: + pass + + def _on_mount(self) -> None: + """Initialise collapsed state.""" + self._update_collapsed(self.collapsed) + + def compose(self) -> ComposeResult: + yield self._title + yield self.Contents(*self._contents_list) + + def compose_add_child(self, widget: Widget) -> None: + """When using the context manager compose syntax, we want to attach nodes to the contents. + + Args: + widget: A Widget to add. + """ + self._contents_list.append(widget) diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index bad0298ed6..2c20da5107 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -33,32 +33,33 @@ from ..widget import PseudoClasses CellCacheKey: TypeAlias = ( - "tuple[RowKey, ColumnKey, Style, bool, bool, int, PseudoClasses]" + "tuple[RowKey, ColumnKey, Style, bool, bool, bool, int, PseudoClasses]" ) LineCacheKey: TypeAlias = "tuple[int, int, int, int, Coordinate, Coordinate, Style, CursorType, bool, int, PseudoClasses]" RowCacheKey: TypeAlias = "tuple[RowKey, int, Style, Coordinate, Coordinate, CursorType, bool, bool, int, PseudoClasses]" CursorType = Literal["cell", "row", "column", "none"] -"""The legal types of cursors for [`DataTable.cursor_type`][textual.widgets.DataTable.cursor_type].""" +"""The valid types of cursors for [`DataTable.cursor_type`][textual.widgets.DataTable.cursor_type].""" CellType = TypeVar("CellType") -CELL_X_PADDING = 2 +_DEFAULT_CELL_X_PADDING = 1 +"""Default padding to use on each side of a column in the data table.""" class CellDoesNotExist(Exception): """The cell key/index was invalid. - Raised when the user supplies coordinates or cell keys which - do not exist in the DataTable.""" + Raised when the coordinates or cell key provided does not exist + in the DataTable (e.g. out of bounds index, invalid key)""" class RowDoesNotExist(Exception): - """Raised when the user supplies a row index or row key which does - not exist in the DataTable (e.g. out of bounds index, invalid key)""" + """Raised when the row index or row key provided does not exist + in the DataTable (e.g. out of bounds index, invalid key)""" class ColumnDoesNotExist(Exception): - """Raised when the user supplies a column index or column key which does - not exist in the DataTable (e.g. out of bounds index, invalid key)""" + """Raised when the column index or column key provided does not exist + in the DataTable (e.g. out of bounds index, invalid key)""" class DuplicateKey(Exception): @@ -170,14 +171,18 @@ class Column: content_width: int = 0 auto_width: bool = False - @property - def render_width(self) -> int: - """Width in cells, required to render a column.""" - # +2 is to account for space padding either side of the cell - if self.auto_width: - return self.content_width + CELL_X_PADDING - else: - return self.width + CELL_X_PADDING + def get_render_width(self, data_table: DataTable[Any]) -> int: + """Width, in cells, required to render the column with padding included. + + Args: + data_table: The data table where the column will be rendered. + + Returns: + The width, in cells, required to render the column with padding included. + """ + return 2 * data_table.cell_padding + ( + self.content_width if self.auto_width else self.width + ) @dataclass @@ -187,6 +192,7 @@ class Row: key: RowKey height: int label: Text | None = None + auto_height: bool = False class RowRenderables(NamedTuple): @@ -244,7 +250,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): """ DEFAULT_CSS = """ - App.-dark DataTable { + DataTable:dark { background:; } DataTable { @@ -290,7 +296,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): background: $secondary 30%; } - .-dark-mode DataTable > .datatable--even-row { + DataTable:dark > .datatable--even-row { background: $primary 15%; } @@ -308,6 +314,8 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): show_cursor = Reactive(True) cursor_type: Reactive[CursorType] = Reactive[CursorType]("cell") """The type of the cursor of the `DataTable`.""" + cell_padding = Reactive(_DEFAULT_CELL_X_PADDING) + """Horizontal padding between cells, applied on each side of each cell.""" cursor_coordinate: Reactive[Coordinate] = Reactive( Coordinate(0, 0), repaint=False, always_update=True @@ -322,7 +330,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): ) """The coordinate of the `DataTable` that is being hovered.""" - class CellHighlighted(Message, bubble=True): + class CellHighlighted(Message): """Posted when the cursor moves to highlight a new cell. This is only relevant when the `cursor_type` is `"cell"`. @@ -359,7 +367,7 @@ def control(self) -> DataTable: """Alias for the data table.""" return self.data_table - class CellSelected(Message, bubble=True): + class CellSelected(Message): """Posted by the `DataTable` widget when a cell is selected. This is only relevant when the `cursor_type` is `"cell"`. Can be handled using @@ -394,7 +402,7 @@ def control(self) -> DataTable: """Alias for the data table.""" return self.data_table - class RowHighlighted(Message, bubble=True): + class RowHighlighted(Message): """Posted when a row is highlighted. This message is only posted when the @@ -423,7 +431,7 @@ def control(self) -> DataTable: """Alias for the data table.""" return self.data_table - class RowSelected(Message, bubble=True): + class RowSelected(Message): """Posted when a row is selected. This message is only posted when the @@ -452,7 +460,7 @@ def control(self) -> DataTable: """Alias for the data table.""" return self.data_table - class ColumnHighlighted(Message, bubble=True): + class ColumnHighlighted(Message): """Posted when a column is highlighted. This message is only posted when the @@ -481,7 +489,7 @@ def control(self) -> DataTable: """Alias for the data table.""" return self.data_table - class ColumnSelected(Message, bubble=True): + class ColumnSelected(Message): """Posted when a column is selected. This message is only posted when the @@ -510,7 +518,7 @@ def control(self) -> DataTable: """Alias for the data table.""" return self.data_table - class HeaderSelected(Message, bubble=True): + class HeaderSelected(Message): """Posted when a column header/label is clicked.""" def __init__( @@ -540,7 +548,7 @@ def control(self) -> DataTable: """Alias for the data table.""" return self.data_table - class RowLabelSelected(Message, bubble=True): + class RowLabelSelected(Message): """Posted when a row label is clicked.""" def __init__( @@ -582,11 +590,44 @@ def __init__( show_cursor: bool = True, cursor_foreground_priority: Literal["renderable", "css"] = "css", cursor_background_priority: Literal["renderable", "css"] = "renderable", + cursor_type: CursorType = "cell", + cell_padding: int = _DEFAULT_CELL_X_PADDING, name: str | None = None, id: str | None = None, classes: str | None = None, disabled: bool = False, ) -> None: + """Initialises a widget to display tabular data. + + Args: + show_header: Whether the table header should be visible or not. + show_row_labels: Whether the row labels should be shown or not. + fixed_rows: The number of rows, counting from the top, that should be fixed + and still visible when the user scrolls down. + fixed_columns: The number of columns, counting from the left, that should be + fixed and still visible when the user scrolls right. + zebra_stripes: Enables or disables a zebra effect applied to the background + color of the rows of the table, where alternate colors are styled + differently to improve the readability of the table. + header_height: The height, in number of cells, of the data table header. + show_cursor: Whether the cursor should be visible when navigating the data + table or not. + cursor_foreground_priority: If the data associated with a cell is an + arbitrary renderable with a set foreground color, this determines whether + that color is prioritized over the cursor component class or not. + cursor_background_priority: If the data associated with a cell is an + arbitrary renderable with a set background color, this determines whether + that color is prioritized over the cursor component class or not. + cursor_type: The type of cursor to be used when navigating the data table + with the keyboard. + cell_padding: The number of cells added on each side of each column. Setting + this value to zero will likely make your table very heard to read. + name: The name of the widget. + id: The ID of the widget in the DOM. + classes: The CSS classes for the widget. + disabled: Whether the widget is disabled or not. + """ + super().__init__(name=name, id=id, classes=classes, disabled=disabled) self._data: dict[RowKey, dict[ColumnKey, CellType]] = {} """Contains the cells of the table, indexed by row key and column key. @@ -669,6 +710,10 @@ def __init__( self.cursor_background_priority = cursor_background_priority """Should we prioritize the cursor component class CSS background or the renderable background in the event where a cell contains a renderable with a background color.""" + self.cursor_type = cursor_type + """The type of cursor of the `DataTable`.""" + self.cell_padding = cell_padding + """Horizontal padding between cells, applied on each side of each cell.""" @property def hover_row(self) -> int: @@ -948,6 +993,7 @@ def _clear_caches(self) -> None: self._styles_cache.clear() self._offset_cache.clear() self._ordered_row_cache.clear() + self._get_styles_to_render_cell.cache_clear() def get_row_height(self, row_key: RowKey) -> int: """Given a row key, return the height of that row in terminal cells. @@ -962,7 +1008,7 @@ def get_row_height(self, row_key: RowKey) -> int: return self.header_height return self.rows[row_key].height - async def _on_styles_updated(self) -> None: + def notify_style_update(self) -> None: self._clear_caches() self.refresh() @@ -991,7 +1037,7 @@ def watch_show_header(self, show: bool) -> None: def watch_show_row_labels(self, show: bool) -> None: width, height = self.virtual_size - column_width = self._label_column.render_width + column_width = self._label_column.get_render_width(self) width_change = column_width if show else -column_width self.virtual_size = Size(width + width_change, height) self._scroll_cursor_into_view() @@ -1006,6 +1052,19 @@ def watch_fixed_columns(self) -> None: def watch_zebra_stripes(self) -> None: self._clear_caches() + def validate_cell_padding(self, cell_padding: int) -> int: + return max(cell_padding, 0) + + def watch_cell_padding(self, old_padding: int, new_padding: int) -> None: + # A single side of a single cell will have its width changed by (new - old), + # so the total width change is double that per column, times the number of + # columns for the whole data table. + width_change = 2 * (new_padding - old_padding) * len(self.columns) + width, height = self.virtual_size + self.virtual_size = Size(width + width_change, height) + self._scroll_cursor_into_view() + self._clear_caches() + def watch_hover_coordinate(self, old: Coordinate, value: Coordinate) -> None: self.refresh_coordinate(old) self.refresh_coordinate(value) @@ -1057,9 +1116,10 @@ def move_cursor( cursor_row = row if column is not None: cursor_column = column + destination = Coordinate(cursor_row, cursor_column) self.cursor_coordinate = destination - self._scroll_cursor_into_view(animate=animate) + self.call_after_refresh(self._scroll_cursor_into_view, animate=animate) def _highlight_coordinate(self, coordinate: Coordinate) -> None: """Apply highlighting to the cell at the coordinate, and post event.""" @@ -1159,7 +1219,11 @@ def _highlight_cursor(self) -> None: @property def _row_label_column_width(self) -> int: """The render width of the column containing row labels""" - return self._label_column.render_width if self._should_render_row_labels else 0 + return ( + self._label_column.get_render_width(self) + if self._should_render_row_labels + else 0 + ) def _update_column_widths(self, updated_cells: set[CellKey]) -> None: """Update the widths of the columns based on the newly updated cell widths.""" @@ -1167,6 +1231,7 @@ def _update_column_widths(self, updated_cells: set[CellKey]) -> None: column = self.columns.get(column_key) if column is None: continue + console = self.app.console label_width = measure(console, column.label, 1) content_width = column.content_width @@ -1187,8 +1252,16 @@ def _update_column_widths(self, updated_cells: set[CellKey]) -> None: self._require_update_dimensions = True def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None: - """Called to recalculate the virtual (scrollable) size.""" + """Called to recalculate the virtual (scrollable) size. + + This recomputes column widths and then checks if any of the new rows need + to have their height computed. + + Args: + new_rows: The new rows that will affect the `DataTable` dimensions. + """ console = self.app.console + auto_height_rows: list[tuple[int, Row, list[RenderableType]]] = [] for row_key in new_rows: row_index = self._row_locations.get(row_key) @@ -1198,6 +1271,7 @@ def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None: continue row = self.rows.get(row_key) + assert row is not None if row.label is not None: self._labelled_row_exists = True @@ -1212,9 +1286,71 @@ def _update_dimensions(self, new_rows: Iterable[RowKey]) -> None: content_width = measure(console, renderable, 1) column.content_width = max(column.content_width, content_width) + if row.auto_height: + auto_height_rows.append((row_index, row, cells_in_row)) + self._clear_caches() - data_cells_width = sum(column.render_width for column in self.columns.values()) + # If there are rows that need to have their height computed, render them correctly + # so that we can cache this rendering for later. + if auto_height_rows: + render_cell = self._render_cell # This method renders & caches. + should_highlight = self._should_highlight + cursor_type = self.cursor_type + cursor_location = self.cursor_coordinate + hover_location = self.hover_coordinate + base_style = self.rich_style + fixed_style = self.get_component_styles( + "datatable--fixed" + ).rich_style + Style.from_meta({"fixed": True}) + ordered_columns = self.ordered_columns + fixed_columns = self.fixed_columns + + for row_index, row, cells_in_row in auto_height_rows: + height = 0 + row_style = self._get_row_style(row_index, base_style) + + # As we go through the cells, save their rendering, height, and + # column width. After we compute the height of the row, go over the cells + # that were rendered with the wrong height and append the missing padding. + rendered_cells: list[tuple[SegmentLines, int, int]] = [] + for column_index, column in enumerate(ordered_columns): + style = fixed_style if column_index < fixed_columns else row_style + cell_location = Coordinate(row_index, column_index) + rendered_cell = render_cell( + row_index, + column_index, + style, + column.get_render_width(self), + cursor=should_highlight( + cursor_location, cell_location, cursor_type + ), + hover=should_highlight( + hover_location, cell_location, cursor_type + ), + ) + cell_height = len(rendered_cell) + rendered_cells.append( + (rendered_cell, cell_height, column.get_render_width(self)) + ) + height = max(height, cell_height) + + row.height = height + # Do surgery on the cache for cells that were rendered with the incorrect + # height during the first pass. + for cell_renderable, cell_height, column_width in rendered_cells: + if cell_height < height: + first_line_space_style = cell_renderable[0][0].style + cell_renderable.extend( + [ + [Segment(" " * column_width, first_line_space_style)] + for _ in range(height - cell_height) + ] + ) + + data_cells_width = sum( + column.get_render_width(self) for column in self.columns.values() + ) total_width = data_cells_width + self._row_label_column_width header_height = self.header_height if self.show_header else 0 self.virtual_size = Size( @@ -1234,11 +1370,14 @@ def _get_cell_region(self, coordinate: Coordinate) -> Region: # The x-coordinate of a cell is the sum of widths of the data cells to the left # plus the width of the render width of the longest row label. x = ( - sum(column.render_width for column in self.ordered_columns[:column_index]) + sum( + column.get_render_width(self) + for column in self.ordered_columns[:column_index] + ) + self._row_label_column_width ) column_key = self._column_locations.get_key(column_index) - width = self.columns[column_key].render_width + width = self.columns[column_key].get_render_width(self) height = row.height y = sum(ordered_row.height for ordered_row in self.ordered_rows[:row_index]) if self.show_header: @@ -1255,7 +1394,7 @@ def _get_row_region(self, row_index: int) -> Region: row_key = self._row_locations.get_key(row_index) row = rows[row_key] row_width = ( - sum(column.render_width for column in self.columns.values()) + sum(column.get_render_width(self) for column in self.columns.values()) + self._row_label_column_width ) y = sum(ordered_row.height for ordered_row in self.ordered_rows[:row_index]) @@ -1271,11 +1410,14 @@ def _get_column_region(self, column_index: int) -> Region: columns = self.columns x = ( - sum(column.render_width for column in self.ordered_columns[:column_index]) + sum( + column.get_render_width(self) + for column in self.ordered_columns[:column_index] + ) + self._row_label_column_width ) column_key = self._column_locations.get_key(column_index) - width = columns[column_key].render_width + width = columns[column_key].get_render_width(self) header_height = self.header_height if self.show_header else 0 height = self._total_row_height + header_height full_column_region = Region(x, 0, width, height) @@ -1370,7 +1512,7 @@ def add_column( def add_row( self, *cells: CellType, - height: int = 1, + height: int | None = 1, key: str | None = None, label: TextType | None = None, ) -> RowKey: @@ -1378,13 +1520,14 @@ def add_row( Args: *cells: Positional arguments should contain cell data. - height: The height of a row (in lines). + height: The height of a row (in lines). Use `None` to auto-detect the optimal + height. key: A key which uniquely identifies this row. If None, it will be generated for you and returned. label: The label for the row. Will be displayed to the left if supplied. Returns: - Uniquely identifies this row. Can be used to retrieve this row regardless + Unique identifier for this row. Can be used to retrieve this row regardless of its current location in the DataTable (it could have moved after being added due to sorting or insertion/deletion of other rows). """ @@ -1404,7 +1547,15 @@ def add_row( for column, cell in zip_longest(self.ordered_columns, cells) } label = Text.from_markup(label) if isinstance(label, str) else label - self.rows[row_key] = Row(row_key, height, label) + # Rows with auto-height get a height of 0 because 1) we need an integer height + # to do some intermediate computations and 2) because 0 doesn't impact the data + # table while we don't figure out how tall this row is. + self.rows[row_key] = Row( + row_key, + height or 0, + label, + height is None, + ) self._new_rows.add(row_key) self._require_update_dimensions = True self.cursor_coordinate = self.cursor_coordinate @@ -1465,9 +1616,11 @@ def remove_row(self, row_key: RowKey | str) -> None: Raises: RowDoesNotExist: If the row key does not exist. """ + if row_key not in self._row_locations: raise RowDoesNotExist(f"Row key {row_key!r} is not valid.") + self._new_rows.discard(row_key) self._require_update_dimensions = True self.check_idle() @@ -1482,6 +1635,10 @@ def remove_row(self, row_key: RowKey | str) -> None: self._row_locations = new_row_locations + # Prevent the removed cells from triggering dimension updates + for column_key in self._data.get(row_key): + self._updated_cells.discard(CellKey(row_key, column_key)) + del self.rows[row_key] del self._data[row_key] @@ -1518,8 +1675,10 @@ def remove_column(self, column_key: ColumnKey | str) -> None: self._column_locations = new_column_locations del self.columns[column_key] - for row in self._data: - del self._data[row][column_key] + + for row_key in self._data: + self._updated_cells.discard(CellKey(row_key, column_key)) + del self._data[row_key][column_key] self.cursor_coordinate = self.cursor_coordinate self.hover_coordinate = self.hover_coordinate @@ -1537,13 +1696,14 @@ async def _on_idle(self, _: events.Idle) -> None: if self._updated_cells: # Cell contents have already been updated at this point. # Now we only need to worry about measuring column widths. - updated_columns = self._updated_cells.copy() + updated_cells = self._updated_cells.copy() self._updated_cells.clear() - self._update_column_widths(updated_columns) + self._update_column_widths(updated_cells) if self._require_update_dimensions: # Add the new rows *before* updating the column widths, since - # cells in a new row may influence the final width of a column + # cells in a new row may influence the final width of a column. + # Only then can we compute optimal height of rows with "auto" height. self._require_update_dimensions = False new_rows = self._new_rows.copy() self._new_rows.clear() @@ -1751,7 +1911,7 @@ def _render_cell( row_key = self._row_locations.get_key(row_index) column_key = self._column_locations.get_key(column_index) - cell_cache_key = ( + cell_cache_key: CellCacheKey = ( row_key, column_key, base_style, @@ -1764,7 +1924,6 @@ def _render_cell( if cell_cache_key not in self._cell_render_cache: base_style += Style.from_meta({"row": row_index, "column": column_index}) - height = self.header_height if is_header_cell else self.rows[row_key].height row_label, row_cells = self._get_row_renderables(row_index) if is_row_label_cell: @@ -1772,50 +1931,104 @@ def _render_cell( else: cell = row_cells[column_index] - get_component = self.get_component_rich_style - show_cursor = self.show_cursor - component_style = Style() - - if hover and show_cursor and self._show_hover_cursor: - component_style += get_component("datatable--hover") - if is_header_cell or is_row_label_cell: - # Apply subtle variation in style for the header/label (blue background by - # default) rows and columns affected by the cursor, to ensure we can - # still differentiate between the labels and the data. - component_style += get_component("datatable--header-hover") - - if cursor and show_cursor: - cursor_style = get_component("datatable--cursor") - component_style += cursor_style - if is_header_cell or is_row_label_cell: - component_style += get_component("datatable--header-cursor") - elif is_fixed_style_cell: - component_style += get_component("datatable--fixed-cursor") - - post_foreground = ( - Style.from_color(color=component_style.color) - if self.cursor_foreground_priority == "css" - else Style.null() - ) - post_background = ( - Style.from_color(bgcolor=component_style.bgcolor) - if self.cursor_background_priority == "css" - else Style.null() + component_style, post_style = self._get_styles_to_render_cell( + is_header_cell, + is_row_label_cell, + is_fixed_style_cell, + hover, + cursor, + self.show_cursor, + self._show_hover_cursor, + self.cursor_foreground_priority == "css", + self.cursor_background_priority == "css", ) + if is_header_cell: + options = self.app.console.options.update_dimensions( + width, self.header_height + ) + else: + row = self.rows[row_key] + # If an auto-height row hasn't had its height calculated, we don't fix + # the value for `height` so that we can measure the height of the cell. + if row.auto_height and row.height == 0: + options = self.app.console.options.update_width(width) + else: + options = self.app.console.options.update_dimensions( + width, row.height + ) lines = self.app.console.render_lines( Styled( - Padding(cell, (0, 1)), + Padding(cell, (0, self.cell_padding)), pre_style=base_style + component_style, - post_style=post_foreground + post_background, + post_style=post_style, ), - self.app.console.options.update_dimensions(width, height), + options, ) self._cell_render_cache[cell_cache_key] = lines return self._cell_render_cache[cell_cache_key] + @functools.lru_cache(maxsize=32) + def _get_styles_to_render_cell( + self, + is_header_cell: bool, + is_row_label_cell: bool, + is_fixed_style_cell: bool, + hover: bool, + cursor: bool, + show_cursor: bool, + show_hover_cursor: bool, + has_css_foreground_priority: bool, + has_css_background_priority: bool, + ) -> tuple[Style, Style]: + """Auxiliary method to compute styles used to render a given cell. + + Args: + is_header_cell: Is this a cell from a header? + is_row_label_cell: Is this the label of any given row? + is_fixed_style_cell: Should this cell be styled like a fixed cell? + hover: Does this cell have the hover pseudo class? + cursor: Is this cell covered by the cursor? + show_cursor: Do we want to show the cursor in the data table? + show_hover_cursor: Do we want to show the mouse hover when using the keyboard + to move the cursor? + has_css_foreground_priority: `self.cursor_foreground_priority == "css"`? + has_css_background_priority: `self.cursor_background_priority == "css"`? + """ + get_component = self.get_component_rich_style + component_style = Style() + + if hover and show_cursor and show_hover_cursor: + component_style += get_component("datatable--hover") + if is_header_cell or is_row_label_cell: + # Apply subtle variation in style for the header/label (blue background by + # default) rows and columns affected by the cursor, to ensure we can + # still differentiate between the labels and the data. + component_style += get_component("datatable--header-hover") + + if cursor and show_cursor: + cursor_style = get_component("datatable--cursor") + component_style += cursor_style + if is_header_cell or is_row_label_cell: + component_style += get_component("datatable--header-cursor") + elif is_fixed_style_cell: + component_style += get_component("datatable--fixed-cursor") + + post_foreground = ( + Style.from_color(color=component_style.color) + if has_css_foreground_priority + else Style.null() + ) + post_background = ( + Style.from_color(bgcolor=component_style.bgcolor) + if has_css_background_priority + else Style.null() + ) + + return component_style, post_foreground + post_background + def _render_line_in_row( self, row_key: RowKey, @@ -1856,29 +2069,9 @@ def _render_line_in_row( if cache_key in self._row_render_cache: return self._row_render_cache[cache_key] - def _should_highlight( - cursor: Coordinate, - target_cell: Coordinate, - type_of_cursor: CursorType, - ) -> bool: - """Determine whether we should highlight a cell given the location - of the cursor, the location of the cell, and the type of cursor that - is currently active.""" - if type_of_cursor == "cell": - return cursor == target_cell - elif type_of_cursor == "row": - cursor_row, _ = cursor - cell_row, _ = target_cell - return cursor_row == cell_row - elif type_of_cursor == "column": - _, cursor_column = cursor - _, cell_column = target_cell - return cursor_column == cell_column - else: - return False - - is_header_row = row_key is self._header_row_key + should_highlight = self._should_highlight render_cell = self._render_cell + header_style = self.get_component_styles("datatable--header").rich_style if row_key in self._row_locations: row_index = self._row_locations.get(row_key) @@ -1887,7 +2080,6 @@ def _should_highlight( # If the row has a label, add it to fixed_row here with correct style. fixed_row = [] - header_style = self.get_component_styles("datatable--header").rich_style if self._labelled_row_exists and self.show_row_labels: # The width of the row label is updated again on idle @@ -1897,14 +2089,17 @@ def _should_highlight( -1, header_style, width=self._row_label_column_width, - cursor=_should_highlight(cursor_location, cell_location, cursor_type), - hover=_should_highlight(hover_location, cell_location, cursor_type), + cursor=should_highlight(cursor_location, cell_location, cursor_type), + hover=should_highlight(hover_location, cell_location, cursor_type), )[line_no] fixed_row.append(label_cell_lines) if self.fixed_columns: - fixed_style = self.get_component_styles("datatable--fixed").rich_style - fixed_style += Style.from_meta({"fixed": True}) + if row_key is self._header_row_key: + fixed_style = header_style # We use the header style either way. + else: + fixed_style = self.get_component_styles("datatable--fixed").rich_style + fixed_style += Style.from_meta({"fixed": True}) for column_index, column in enumerate( self.ordered_columns[: self.fixed_columns] ): @@ -1912,28 +2107,16 @@ def _should_highlight( fixed_cell_lines = render_cell( row_index, column_index, - header_style if is_header_row else fixed_style, - column.render_width, - cursor=_should_highlight( + fixed_style, + column.get_render_width(self), + cursor=should_highlight( cursor_location, cell_location, cursor_type ), - hover=_should_highlight(hover_location, cell_location, cursor_type), + hover=should_highlight(hover_location, cell_location, cursor_type), )[line_no] fixed_row.append(fixed_cell_lines) - is_header_row = row_key is self._header_row_key - if is_header_row: - row_style = self.get_component_styles("datatable--header").rich_style - elif row_index < self.fixed_rows: - row_style = self.get_component_styles("datatable--fixed").rich_style - else: - if self.zebra_stripes: - component_row_style = ( - "datatable--odd-row" if row_index % 2 else "datatable--even-row" - ) - row_style = self.get_component_styles(component_row_style).rich_style - else: - row_style = base_style + row_style = self._get_row_style(row_index, base_style) scrollable_row = [] for column_index, column in enumerate(self.ordered_columns): @@ -1942,9 +2125,9 @@ def _should_highlight( row_index, column_index, row_style, - column.render_width, - cursor=_should_highlight(cursor_location, cell_location, cursor_type), - hover=_should_highlight(hover_location, cell_location, cursor_type), + column.get_render_width(self), + cursor=should_highlight(cursor_location, cell_location, cursor_type), + hover=should_highlight(hover_location, cell_location, cursor_type), )[line_no] scrollable_row.append(cell_lines) @@ -1952,7 +2135,7 @@ def _should_highlight( widget_width = self.size.width table_width = ( sum( - column.render_width + column.get_render_width(self) for column in self.ordered_columns[self.fixed_columns :] ) + self._row_label_column_width @@ -2036,7 +2219,8 @@ def _render_line(self, y: int, x1: int, x2: int, base_style: Style) -> Strip: hover_location=self.hover_coordinate, ) fixed_width = sum( - column.render_width for column in self.ordered_columns[: self.fixed_columns] + column.get_render_width(self) + for column in self.ordered_columns[: self.fixed_columns] ) fixed_line: list[Segment] = list(chain.from_iterable(fixed)) if fixed else [] @@ -2072,6 +2256,63 @@ def render_line(self, y: int) -> Strip: return self._render_line(y, scroll_x, scroll_x + width, self.rich_style) + def _should_highlight( + self, + cursor: Coordinate, + target_cell: Coordinate, + type_of_cursor: CursorType, + ) -> bool: + """Determine if the given cell should be highlighted because of the cursor. + + This auxiliary method takes the cursor position and type into account when + determining whether the cell should be highlighted. + + Args: + cursor: The current position of the cursor. + target_cell: The cell we're checking for the need to highlight. + type_of_cursor: The type of cursor that is currently active. + + Returns: + Whether or not the given cell should be highlighted. + """ + if type_of_cursor == "cell": + return cursor == target_cell + elif type_of_cursor == "row": + cursor_row, _ = cursor + cell_row, _ = target_cell + return cursor_row == cell_row + elif type_of_cursor == "column": + _, cursor_column = cursor + _, cell_column = target_cell + return cursor_column == cell_column + else: + return False + + def _get_row_style(self, row_index: int, base_style: Style) -> Style: + """Gets the Style that should be applied to the row at the given index. + + Args: + row_index: The index of the row to style. + base_style: The base style to use by default. + + Returns: + The appropriate style. + """ + + if row_index == -1: + row_style = self.get_component_styles("datatable--header").rich_style + elif row_index < self.fixed_rows: + row_style = self.get_component_styles("datatable--fixed").rich_style + else: + if self.zebra_stripes: + component_row_style = ( + "datatable--odd-row" if row_index % 2 else "datatable--even-row" + ) + row_style = self.get_component_styles(component_row_style).rich_style + else: + row_style = base_style + return row_style + def _on_mouse_move(self, event: events.MouseMove): """If the hover cursor is visible, display it by extracting the row and column metadata from the segments present in the cells.""" @@ -2098,7 +2339,7 @@ def _get_fixed_offset(self) -> Spacing: top += sum(row.height for row in self.ordered_rows[: self.fixed_rows]) left = ( sum( - column.render_width + column.get_render_width(self) for column in self.ordered_columns[: self.fixed_columns] ) + self._row_label_column_width @@ -2216,9 +2457,8 @@ async def _on_click(self, event: events.Click) -> None: def action_page_down(self) -> None: """Move the cursor one page down.""" self._set_hover_cursor(False) - cursor_type = self.cursor_type - if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"): - height = self.size.height - self.header_height if self.show_header else 0 + if self.show_cursor and self.cursor_type in ("cell", "row"): + height = self.size.height - (self.header_height if self.show_header else 0) # Determine how many rows constitutes a "page" offset = 0 @@ -2239,9 +2479,8 @@ def action_page_down(self) -> None: def action_page_up(self) -> None: """Move the cursor one page up.""" self._set_hover_cursor(False) - cursor_type = self.cursor_type - if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"): - height = self.size.height - self.header_height if self.show_header else 0 + if self.show_cursor and self.cursor_type in ("cell", "row"): + height = self.size.height - (self.header_height if self.show_header else 0) # Determine how many rows constitutes a "page" offset = 0 diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index 1855aa2d90..e85c566f49 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -65,7 +65,7 @@ class DirectoryTree(Tree[DirEntry]): PATH: Callable[[str | Path], Path] = Path """Callable that returns a fresh path object.""" - class FileSelected(Message, bubble=True): + class FileSelected(Message): """Posted when a file is selected. Can be handled using `on_directory_tree_file_selected` in a subclass of @@ -90,6 +90,31 @@ def control(self) -> Tree[DirEntry]: """The `Tree` that had a file selected.""" return self.node.tree + class DirectorySelected(Message): + """Posted when a directory is selected. + + Can be handled using `on_directory_tree_directory_selected` in a + subclass of `DirectoryTree` or in a parent widget in the DOM. + """ + + def __init__(self, node: TreeNode[DirEntry], path: Path) -> None: + """Initialise the DirectorySelected object. + + Args: + node: The tree node for the directory that was selected. + path: The path of the directory that was selected. + """ + super().__init__() + self.node: TreeNode[DirEntry] = node + """The tree node of the directory that was selected.""" + self.path: Path = path + """The path of the directory that was selected.""" + + @property + def control(self) -> Tree[DirEntry]: + """The `Tree` that had a directory selected.""" + return self.node.tree + path: var[str | Path] = var["str | Path"](PATH("."), init=False, always_update=True) """The path that is the root of the directory tree. @@ -414,5 +439,7 @@ def _on_tree_node_selected(self, event: Tree.NodeSelected) -> None: dir_entry = event.node.data if dir_entry is None: return - if not self._safe_is_dir(dir_entry.path): + if self._safe_is_dir(dir_entry.path): + self.post_message(self.DirectorySelected(event.node, dir_entry.path)) + else: self.post_message(self.FileSelected(event.node, dir_entry.path)) diff --git a/src/textual/widgets/_header.py b/src/textual/widgets/_header.py index 668105fa31..ac9aeef649 100644 --- a/src/textual/widgets/_header.py +++ b/src/textual/widgets/_header.py @@ -7,7 +7,7 @@ from rich.text import Text from ..app import RenderResult -from ..events import Mount +from ..events import Click, Mount from ..reactive import Reactive from ..widget import Widget @@ -22,11 +22,20 @@ class HeaderIcon(Widget): width: 8; content-align: left middle; } + + HeaderIcon:hover { + background: $foreground 10%; + } """ icon = Reactive("⭘") """The character to use as the icon within the header.""" + async def on_click(self, event: Click) -> None: + """Launch the command palette when icon is clicked.""" + event.stop() + await self.run_action("command_palette") + def render(self) -> RenderResult: """Render the header icon. @@ -160,12 +169,36 @@ def watch_tall(self, tall: bool) -> None: def _on_click(self): self.toggle_class("-tall") + @property + def screen_title(self) -> str: + """The title that this header will display. + + This depends on [`Screen.title`][textual.screen.Screen.title] and [`App.title`][textual.app.App.title]. + """ + screen_title = self.screen.title + title = screen_title if screen_title is not None else self.app.title + return title + + @property + def screen_sub_title(self) -> str: + """The sub-title that this header will display. + + This depends on [`Screen.sub_title`][textual.screen.Screen.sub_title] and [`App.sub_title`][textual.app.App.sub_title]. + """ + screen_sub_title = self.screen.sub_title + sub_title = ( + screen_sub_title if screen_sub_title is not None else self.app.sub_title + ) + return sub_title + def _on_mount(self, _: Mount) -> None: - def set_title(title: str) -> None: - self.query_one(HeaderTitle).text = title + def set_title() -> None: + self.query_one(HeaderTitle).text = self.screen_title def set_sub_title(sub_title: str) -> None: - self.query_one(HeaderTitle).sub_text = sub_title + self.query_one(HeaderTitle).sub_text = self.screen_sub_title self.watch(self.app, "title", set_title) self.watch(self.app, "sub_title", set_sub_title) + self.watch(self.screen, "title", set_title) + self.watch(self.screen, "sub_title", set_sub_title) diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index daf298e7aa..c21afb690e 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -9,18 +9,24 @@ from rich.highlighter import Highlighter from rich.segment import Segment from rich.text import Text +from typing_extensions import Literal from .. import events from .._segment_tools import line_crop from ..binding import Binding, BindingType from ..events import Blur, Focus, Mount -from ..geometry import Size +from ..geometry import Offset, Size from ..message import Message from ..reactive import reactive from ..suggester import Suggester, SuggestionReady from ..validation import ValidationResult, Validator from ..widget import Widget +InputValidationOn = Literal["blur", "changed", "submitted"] +"""Possible messages that trigger input validation.""" +_POSSIBLE_VALIDATE_ON_VALUES = {"blur", "changed", "submitted"} +"""Set literal with the legal values for the type `InputValidationOn`.""" + class _InputRenderable: """Render the input content.""" @@ -221,6 +227,7 @@ def __init__( *, suggester: Suggester | None = None, validators: Validator | Iterable[Validator] | None = None, + validate_on: Iterable[InputValidationOn] | None = None, name: str | None = None, id: str | None = None, classes: str | None = None, @@ -236,6 +243,9 @@ def __init__( suggester: [`Suggester`][textual.suggester.Suggester] associated with this input instance. validators: An iterable of validators that the Input value will be checked against. + validate_on: Zero or more of the values "blur", "changed", and "submitted", + which determine when to do input validation. The default is to do + validation for all messages. name: Optional name for the input widget. id: Optional ID for the widget. classes: Optional initial classes for the widget. @@ -244,6 +254,7 @@ def __init__( super().__init__(name=name, id=id, classes=classes, disabled=disabled) if value is not None: self.value = value + self.placeholder = placeholder self.highlighter = highlighter self.password = password @@ -254,7 +265,25 @@ def __init__( elif validators is None: self.validators = [] else: - self.validators = list(validators) or [] + self.validators = list(validators) + + self.validate_on = ( + set(validate_on) & _POSSIBLE_VALIDATE_ON_VALUES + if validate_on is not None + else _POSSIBLE_VALIDATE_ON_VALUES + ) + """Set with event names to do input validation on. + + Validation can only be performed on blur, on input changes and on input submission. + + Example: + This creates an `Input` widget that only gets validated when the value + is submitted explicitly: + + ```py + input = Input(validate_on=["submitted"]) + ``` + """ def _position_to_cell(self, position: int) -> int: """Convert an index within the value to cell position.""" @@ -299,6 +328,14 @@ def _watch_cursor_position(self) -> None: else: self.view_position = self.view_position + self.app.cursor_position = self.cursor_screen_offset + + @property + def cursor_screen_offset(self) -> Offset: + """The offset of the cursor of this input in screen-space. (x, y)/(column, row)""" + x, y, _width, _height = self.content_region + return Offset(x + self._cursor_offset - self.view_position, y) + async def _watch_value(self, value: str) -> None: self._suggestion = "" if self.suggester and value: @@ -306,8 +343,9 @@ async def _watch_value(self, value: str) -> None: if self.styles.auto_dimensions: self.refresh(layout=True) - validation_result = self.validate(value) - + validation_result = ( + self.validate(value) if "changed" in self.validate_on else None + ) self.post_message(self.Changed(self, value, validation_result)) def validate(self, value: str) -> ValidationResult | None: @@ -389,11 +427,14 @@ def _on_mount(self, _: Mount) -> None: def _on_blur(self, _: Blur) -> None: self.blink_timer.pause() + if "blur" in self.validate_on: + self.validate(self.value) def _on_focus(self, _: Focus) -> None: self.cursor_position = len(self.value) if self.cursor_blink: self.blink_timer.resume() + self.app.cursor_position = self.cursor_screen_offset async def _on_key(self, event: events.Key) -> None: self._cursor_visible = True @@ -421,10 +462,11 @@ async def _on_click(self, event: events.Click) -> None: cell_offset = 0 _cell_size = get_character_cell_size for index, char in enumerate(self.value): - if cell_offset >= click_x: + cell_width = _cell_size(char) + if cell_offset <= click_x < (cell_offset + cell_width): self.cursor_position = index break - cell_offset += _cell_size(char) + cell_offset += cell_width else: self.cursor_position = len(self.value) @@ -449,6 +491,10 @@ def insert_text_at_cursor(self, text: str) -> None: self.value = f"{before}{text}{after}" self.cursor_position += len(text) + def clear(self) -> None: + """Clear the input.""" + self.value = "" + def action_cursor_left(self) -> None: """Move the cursor one position to the left.""" self.cursor_position -= 1 @@ -576,7 +622,9 @@ def action_delete_left_all(self) -> None: async def action_submit(self) -> None: """Handle a submit action. - Normally triggered by the user pressing Enter. This will also run any validators. + Normally triggered by the user pressing Enter. This may also run any validators. """ - validation_result = self.validate(self.value) + validation_result = ( + self.validate(self.value) if "submitted" in self.validate_on else None + ) self.post_message(self.Submitted(self, self.value, validation_result)) diff --git a/src/textual/widgets/_list_item.py b/src/textual/widgets/_list_item.py index bf3a43e28a..e87b8cf4fc 100644 --- a/src/textual/widgets/_list_item.py +++ b/src/textual/widgets/_list_item.py @@ -16,6 +16,8 @@ class ListItem(Widget, can_focus=False): documentation for more details on use. """ + SCOPED_CSS = False + DEFAULT_CSS = """ ListItem { color: $text; diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py index f9f423b53a..615d37ae5a 100644 --- a/src/textual/widgets/_list_view.py +++ b/src/textual/widgets/_list_view.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import ClassVar, Optional +from typing import ClassVar, Iterable, Optional from textual.await_remove import AwaitRemove from textual.binding import Binding, BindingType @@ -38,7 +38,7 @@ class ListView(VerticalScroll, can_focus=True, can_focus_children=False): index = reactive[Optional[int]](0, always_update=True) - class Highlighted(Message, bubble=True): + class Highlighted(Message): """Posted when the highlighted item changes. Highlighted item is controlled using up/down keys. @@ -65,7 +65,7 @@ def control(self) -> ListView: """ return self.list_view - class Selected(Message, bubble=True): + class Selected(Message): """Posted when a list item is selected, e.g. when you press the enter key on it. Can be handled using `on_list_view_selected` in a subclass of `ListView` or in @@ -172,6 +172,21 @@ def watch_index(self, old_index: int, new_index: int) -> None: self._scroll_highlighted_region() self.post_message(self.Highlighted(self, new_child)) + def extend(self, items: Iterable[ListItem]) -> AwaitMount: + """Append multiple new ListItems to the end of the ListView. + + Args: + items: The ListItems to append. + + Returns: + An awaitable that yields control to the event loop + until the DOM has been updated with the new child items. + """ + await_mount = self.mount(*items) + if len(self) == 1: + self.index = 0 + return await_mount + def append(self, item: ListItem) -> AwaitMount: """Append a new ListItem to the end of the ListView. @@ -182,10 +197,7 @@ def append(self, item: ListItem) -> AwaitMount: An awaitable that yields control to the event loop until the DOM has been updated with the new child item. """ - await_mount = self.mount(item) - if len(self) == 1: - self.index = 0 - return await_mount + return self.extend([item]) def clear(self) -> AwaitRemove: """Clear all items from the ListView. diff --git a/src/textual/widgets/_loading_indicator.py b/src/textual/widgets/_loading_indicator.py index a99d18e62c..bdae0c6a72 100644 --- a/src/textual/widgets/_loading_indicator.py +++ b/src/textual/widgets/_loading_indicator.py @@ -1,14 +1,16 @@ from __future__ import annotations from time import time +from typing import Awaitable from rich.console import RenderableType from rich.style import Style from rich.text import Text from ..color import Gradient +from ..css.query import NoMatches from ..events import Mount -from ..widget import Widget +from ..widget import AwaitMount, Widget class LoadingIndicator(Widget): @@ -18,11 +20,53 @@ class LoadingIndicator(Widget): LoadingIndicator { width: 100%; height: 100%; + min-height: 1; content-align: center middle; color: $accent; } + LoadingIndicator.-overlay { + overlay: screen; + background: $boost; + } """ + def apply(self, widget: Widget) -> AwaitMount: + """Apply the loading indicator to a `widget`. + + This will overlay the given widget with a loading indicator. + + Args: + widget: A widget. + + Returns: + AwaitMount: An awaitable for mounting the indicator. + """ + self.add_class("-overlay") + await_mount = widget.mount(self, before=0) + return await_mount + + @classmethod + def clear(cls, widget: Widget) -> Awaitable: + """Clear any loading indicator from the given widget. + + Args: + widget: Widget to clear the loading indicator from. + + Returns: + Optional awaitable. + """ + try: + await_remove = widget.get_child_by_type(cls).remove() + except NoMatches: + + async def null() -> None: + """Nothing to remove""" + return None + + return null() + + return await_remove + def _on_mount(self, _: Mount) -> None: self._start_time = time() self.auto_refresh = 1 / 16 @@ -30,7 +74,7 @@ def _on_mount(self, _: Mount) -> None: def render(self) -> RenderableType: elapsed = time() - self._start_time speed = 0.8 - dot = "\u25CF" + dot = "\u25cf" _, _, background, color = self.colors gradient = Gradient( diff --git a/src/textual/widgets/_log.py b/src/textual/widgets/_log.py index b80f1c9dea..60e343c6a3 100644 --- a/src/textual/widgets/_log.py +++ b/src/textual/widgets/_log.py @@ -294,6 +294,7 @@ def _render_line_strip(self, y: int, rich_style: Style) -> Strip: line = Strip(line_text.render(self.app.console), cell_len(_line)) else: line = Strip([Segment(_line, rich_style)], cell_len(_line)) + self._render_line_cache[y] = line return line diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index 1569195626..ac6de4f4bc 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -12,6 +12,7 @@ from rich.text import Text from typing_extensions import TypeAlias +from .._slug import TrackedSlugs from ..app import ComposeResult from ..containers import Horizontal, Vertical, VerticalScroll from ..events import Mount @@ -50,6 +51,10 @@ def go(self, path: str | PurePath) -> Path: Returns: New location. """ + location, anchor = Markdown.sanitize_location(str(path)) + if location == Path(".") and anchor: + current_file, _ = Markdown.sanitize_location(str(self.location)) + path = f"{current_file}#{anchor}" new_path = self.location.parent / Path(path) self.stack = self.stack[: self.index + 1] new_path = new_path.absolute() @@ -218,6 +223,7 @@ class MarkdownHorizontalRule(MarkdownBlock): class MarkdownParagraph(MarkdownBlock): """A paragraph Markdown block.""" + SCOPED_CSS = False DEFAULT_CSS = """ Markdown > MarkdownParagraph { margin: 0 0 1 0; @@ -539,7 +545,19 @@ class Markdown(Widget): text-style: bold dim; } """ + COMPONENT_CLASSES = {"em", "strong", "s", "code_inline"} + """ + These component classes target standard inline markdown styles. + Changing these will potentially break the standard markdown formatting. + + | Class | Description | + | :- | :- | + | `code_inline` | Target text that is styled as inline code. | + | `em` | Target text that is emphasized inline. | + | `s` | Target text that is styled inline with strykethrough. | + | `strong` | Target text that is styled inline with strong. | + """ BULLETS = ["\u25CF ", "▪ ", "‣ ", "• ", "⭑ "] @@ -564,8 +582,9 @@ def __init__( super().__init__(name=name, id=id, classes=classes) self._markdown = markdown self._parser_factory = parser_factory + self._table_of_contents: TableOfContentsType | None = None - class TableOfContentsUpdated(Message, bubble=True): + class TableOfContentsUpdated(Message): """The table of contents was updated.""" def __init__( @@ -586,7 +605,7 @@ def control(self) -> Markdown: """ return self.markdown - class TableOfContentsSelected(Message, bubble=True): + class TableOfContentsSelected(Message): """An item in the TOC was selected.""" def __init__(self, markdown: Markdown, block_id: str) -> None: @@ -605,7 +624,7 @@ def control(self) -> Markdown: """ return self.markdown - class LinkClicked(Message, bubble=True): + class LinkClicked(Message): """A link in the document was clicked.""" def __init__(self, markdown: Markdown, href: str) -> None: @@ -628,6 +647,46 @@ def _on_mount(self, _: Mount) -> None: if self._markdown is not None: self.update(self._markdown) + @staticmethod + def sanitize_location(location: str) -> tuple[Path, str]: + """Given a location, break out the path and any anchor. + + Args: + location: The location to sanitize. + + Returns: + A tuple of the path to the location cleaned of any anchor, plus + the anchor (or an empty string if none was found). + """ + location, _, anchor = location.partition("#") + return Path(location), anchor + + def goto_anchor(self, anchor: str) -> bool: + """Try and find the given anchor in the current document. + + Args: + anchor: The anchor to try and find. + + Note: + The anchor is found by looking at all of the headings in the + document and finding the first one whose slug matches the + anchor. + + Note that the slugging method used is similar to that found on + GitHub. + + Returns: + True when the anchor was found in the current document, False otherwise. + """ + if not self._table_of_contents or not isinstance(self.parent, Widget): + return False + unique = TrackedSlugs() + for _, title, header_id in self._table_of_contents: + if unique.slug(title) == anchor: + self.parent.scroll_to_widget(self.query_one(f"#{header_id}"), top=True) + return True + return False + async def load(self, path: Path) -> None: """Load a new Markdown document. @@ -641,7 +700,10 @@ async def load(self, path: Path) -> None: The exceptions that can be raised by this method are all of those that can be raised by calling [`Path.read_text`][pathlib.Path.read_text]. """ + path, anchor = self.sanitize_location(str(path)) await self.update(path.read_text(encoding="utf-8")) + if anchor: + self.goto_anchor(anchor) def unhandled_token(self, token: Token) -> MarkdownBlock | None: """Process an unhandled token. @@ -672,7 +734,7 @@ def update(self, markdown: str) -> AwaitMount: ) block_id: int = 0 - table_of_contents: TableOfContentsType = [] + self._table_of_contents = [] for token in parser.parse(markdown): if token.type == "heading_open": @@ -721,7 +783,7 @@ def update(self, markdown: str) -> AwaitMount: if token.type == "heading_close": heading = block._text.plain level = int(token.tag[1:]) - table_of_contents.append((level, heading, block.id)) + self._table_of_contents.append((level, heading, block.id)) if stack: stack[-1]._blocks.append(block) else: @@ -801,7 +863,9 @@ def update(self, markdown: str) -> AwaitMount: if external is not None: (stack[-1]._blocks if stack else output).append(external) - self.post_message(Markdown.TableOfContentsUpdated(self, table_of_contents)) + self.post_message( + Markdown.TableOfContentsUpdated(self, self._table_of_contents) + ) with self.app.batch_update(): self.query("MarkdownBlock").remove() return self.mount_all(output) @@ -887,6 +951,8 @@ async def _on_tree_node_selected(self, message: Tree.NodeSelected) -> None: class MarkdownViewer(VerticalScroll, can_focus=True, can_focus_children=True): """A Markdown viewer widget.""" + SCOPED_CSS = False + DEFAULT_CSS = """ MarkdownViewer { height: 1fr; @@ -952,7 +1018,13 @@ def _on_mount(self, _: Mount) -> None: async def go(self, location: str | PurePath) -> None: """Navigate to a new document path.""" - await self.document.load(self.navigator.go(location)) + path, anchor = self.document.sanitize_location(str(location)) + if path == Path(".") and anchor: + # We've been asked to go to an anchor but with no file specified. + self.document.goto_anchor(anchor) + else: + # We've been asked to go to a file, optionally with an anchor. + await self.document.load(self.navigator.go(location)) async def back(self) -> None: """Go back one level in the history.""" diff --git a/src/textual/widgets/_option_list.py b/src/textual/widgets/_option_list.py index 9d32806449..538f7029f1 100644 --- a/src/textual/widgets/_option_list.py +++ b/src/textual/widgets/_option_list.py @@ -245,7 +245,6 @@ class OptionList(ScrollView, can_focus=True): background: $accent 60%; } """ - """The default styling for an `OptionList`.""" highlighted: reactive[int | None] = reactive["int | None"](None) """The index of the currently-highlighted option, or `None` if no option is highlighted.""" @@ -541,10 +540,6 @@ def _refresh_content_tracking(self, force: bool = False) -> None: if content.id is not None: # The option has an ID set, create a mapping from that # ID to the option so we can use it later. - if content.id in option_ids: - raise DuplicateID( - f"The option list already has an option with id '{content.id}'" - ) option_ids[content.id] = option option += 1 else: @@ -559,6 +554,30 @@ def _refresh_content_tracking(self, force: bool = False) -> None: # list, set the virtual size. self.virtual_size = Size(self.scrollable_content_region.width, len(self._lines)) + def _duplicate_id_check(self, candidate_items: list[OptionListContent]) -> None: + """Check the items to be added for any duplicates. + + Args: + candidate_items: The items that are going be added. + + Raises: + DuplicateID: If there is an attempt to use a duplicate ID. + """ + # We're only interested in options, and only those that have IDs. + new_options = [ + item + for item in candidate_items + if isinstance(item, Option) and item.id is not None + ] + # Get the set of new IDs that we're being given. + new_option_ids = {option.id for option in new_options} + # Now check for duplicates, both internally amongst the new items + # incoming, and also against all the current known IDs. + if len(new_options) != len(new_option_ids) or not new_option_ids.isdisjoint( + self._option_ids + ): + raise DuplicateID("Attempt made to add options with duplicate IDs.") + def add_options(self, items: Iterable[NewOptionListContent]) -> Self: """Add new options to the end of the option list. @@ -570,12 +589,18 @@ def add_options(self, items: Iterable[NewOptionListContent]) -> Self: Raises: DuplicateID: If there is an attempt to use a duplicate ID. + + Note: + All options are checked for duplicate IDs *before* any option is + added. A duplicate ID will cause none of the passed items to be + added to the option list. """ # Only work if we have items to add; but don't make a fuss out of # zero items to add, just carry on like nothing happened. if items: # Turn any incoming values into valid content for the list. content = [self._make_content(item) for item in items] + self._duplicate_id_check(content) self._contents.extend(content) # Pull out the content that is genuine options and add them to the # list of options. @@ -613,6 +638,7 @@ def _remove_option(self, index: int) -> None: self._refresh_content_tracking(force=True) # Force a re-validation of the highlight. self.highlighted = self.highlighted + self._mouse_hovering_over = None self.refresh() def remove_option(self, option_id: str) -> Self: diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index a6ac37302f..21367631ea 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -120,7 +120,7 @@ def __init__( while next(self._variants_cycle) != self.variant: pass - def on_mount(self) -> None: + def _on_mount(self) -> None: """Set the color for this placeholder.""" colors = Placeholder._COLORS.setdefault( self.app, cycle(_PLACEHOLDER_BACKGROUND_COLORS) diff --git a/src/textual/widgets/_progress_bar.py b/src/textual/widgets/_progress_bar.py index 617d390892..ec8c1b22cb 100644 --- a/src/textual/widgets/_progress_bar.py +++ b/src/textual/widgets/_progress_bar.py @@ -8,16 +8,19 @@ from rich.style import Style -from textual.geometry import clamp - +from .._types import UnusedParameter from ..app import ComposeResult, RenderResult from ..containers import Horizontal +from ..geometry import clamp from ..reactive import reactive from ..renderables.bar import Bar as BarRenderable from ..timer import Timer from ..widget import Widget from ..widgets import Label +UNUSED = UnusedParameter() +"""Sentinel for method signatures.""" + class Bar(Widget, can_focus=False): """The bar portion of the progress bar.""" @@ -276,7 +279,6 @@ class ProgressBar(Widget, can_focus=False): """The total number of steps associated with this progress bar, when known. The value `None` will render an indeterminate progress bar. - Once `total` is set to a numerical value, it cannot be set back to `None`. """ percentage: reactive[float | None] = reactive[Optional[float]](None) """The percentage of progress that has been completed. @@ -398,6 +400,7 @@ def advance(self, advance: float = 1) -> None: ```py progress_bar.advance(10) # Advance 10 steps. ``` + Args: advance: Number of steps to advance progress by. """ @@ -406,30 +409,28 @@ def advance(self, advance: float = 1) -> None: def update( self, *, - total: float | None = None, - progress: float | None = None, - advance: float | None = None, + total: None | float | UnusedParameter = UNUSED, + progress: float | UnusedParameter = UNUSED, + advance: float | UnusedParameter = UNUSED, ) -> None: """Update the progress bar with the given options. - Options only affect the progress bar if they are not `None`. - Example: ```py progress_bar.update( total=200, # Set new total to 200 steps. - progress=None, # This has no effect. + progress=50, # Set the progress to 50 (out of 200). ) ``` Args: - total: New total number of steps (if not `None`). - progress: Set the progress to the given number of steps (if not `None`). - advance: Advance the progress by this number of steps (if not `None`). + total: New total number of steps. + progress: Set the progress to the given number of steps. + advance: Advance the progress by this number of steps. """ - if total is not None: + if not isinstance(total, UnusedParameter): self.total = total - if progress is not None: + if not isinstance(progress, UnusedParameter): self.progress = progress - if advance is not None: + if not isinstance(advance, UnusedParameter): self.progress += advance diff --git a/src/textual/widgets/_radio_set.py b/src/textual/widgets/_radio_set.py index 3a0ee116bf..27581af68d 100644 --- a/src/textual/widgets/_radio_set.py +++ b/src/textual/widgets/_radio_set.py @@ -76,7 +76,7 @@ class RadioSet(Container, can_focus=True, can_focus_children=False): """The index of the currently-selected radio button.""" @rich.repr.auto - class Changed(Message, bubble=True): + class Changed(Message): """Posted when the pressed button in the set changes. This message can be handled using an `on_radio_set_changed` method. @@ -124,7 +124,7 @@ def __init__( """Initialise the radio set. Args: - buttons: A collection of labels or [`RadioButton`][textual.widgets.RadioButton]s to group together. + buttons: The labels or [`RadioButton`][textual.widgets.RadioButton]s to group together. name: The name of the radio set. id: The ID of the radio set in the DOM. classes: The CSS classes of the radio set. diff --git a/src/textual/widgets/_rule.py b/src/textual/widgets/_rule.py new file mode 100644 index 0000000000..f172c0bda5 --- /dev/null +++ b/src/textual/widgets/_rule.py @@ -0,0 +1,217 @@ +from __future__ import annotations + +from rich.text import Text +from typing_extensions import Literal + +from ..app import RenderResult +from ..css._error_tools import friendly_list +from ..reactive import Reactive, reactive +from ..widget import Widget + +RuleOrientation = Literal["horizontal", "vertical"] +"""The valid orientations of the rule widget.""" + +LineStyle = Literal[ + "ascii", + "blank", + "dashed", + "double", + "heavy", + "hidden", + "none", + "solid", + "thick", +] +"""The valid line styles of the rule widget.""" + + +_VALID_RULE_ORIENTATIONS = {"horizontal", "vertical"} + +_VALID_LINE_STYLES = { + "ascii", + "blank", + "dashed", + "double", + "heavy", + "hidden", + "none", + "solid", + "thick", +} + +_HORIZONTAL_LINE_CHARS: dict[LineStyle, str] = { + "ascii": "-", + "blank": " ", + "dashed": "╍", + "double": "═", + "heavy": "━", + "hidden": " ", + "none": " ", + "solid": "─", + "thick": "█", +} + +_VERTICAL_LINE_CHARS: dict[LineStyle, str] = { + "ascii": "|", + "blank": " ", + "dashed": "╏", + "double": "║", + "heavy": "┃", + "hidden": " ", + "none": " ", + "solid": "│", + "thick": "█", +} + + +class InvalidRuleOrientation(Exception): + """Exception raised for an invalid rule orientation.""" + + +class InvalidLineStyle(Exception): + """Exception raised for an invalid rule line style.""" + + +class Rule(Widget, can_focus=False): + """A rule widget to separate content, similar to a `
` HTML tag.""" + + DEFAULT_CSS = """ + Rule { + color: $primary; + } + + Rule.-horizontal { + min-height: 1; + max-height: 1; + margin: 1 0; + } + + Rule.-vertical { + min-width: 1; + max-width: 1; + margin: 0 2; + } + """ + + orientation: Reactive[RuleOrientation] = reactive[RuleOrientation]("horizontal") + """The orientation of the rule.""" + + line_style: Reactive[LineStyle] = reactive[LineStyle]("solid") + """The line style of the rule.""" + + def __init__( + self, + orientation: RuleOrientation = "horizontal", + line_style: LineStyle = "solid", + *, + name: str | None = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + ) -> None: + """Initialize a rule widget. + + Args: + orientation: The orientation of the rule. + line_style: The line style of the rule. + name: The name of the widget. + id: The ID of the widget in the DOM. + classes: The CSS classes of the widget. + disabled: Whether the widget is disabled or not. + """ + super().__init__(name=name, id=id, classes=classes, disabled=disabled) + self.orientation = orientation + self.line_style = line_style + + def render(self) -> RenderResult: + rule_char: str + if self.orientation == "vertical": + rule_char = _VERTICAL_LINE_CHARS[self.line_style] + return Text(rule_char * self.size.height) + elif self.orientation == "horizontal": + rule_char = _HORIZONTAL_LINE_CHARS[self.line_style] + return Text(rule_char * self.size.width) + else: + raise InvalidRuleOrientation( + f"Valid rule orientations are {friendly_list(_VALID_RULE_ORIENTATIONS)}" + ) + + def watch_orientation( + self, old_orientation: RuleOrientation, orientation: RuleOrientation + ) -> None: + self.remove_class(f"-{old_orientation}") + self.add_class(f"-{orientation}") + + def validate_orientation(self, orientation: RuleOrientation) -> RuleOrientation: + if orientation not in _VALID_RULE_ORIENTATIONS: + raise InvalidRuleOrientation( + f"Valid rule orientations are {friendly_list(_VALID_RULE_ORIENTATIONS)}" + ) + return orientation + + def validate_line_style(self, style: LineStyle) -> LineStyle: + if style not in _VALID_LINE_STYLES: + raise InvalidLineStyle( + f"Valid rule line styles are {friendly_list(_VALID_LINE_STYLES)}" + ) + return style + + @classmethod + def horizontal( + cls, + line_style: LineStyle = "solid", + name: str | None = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + ) -> Rule: + """Utility constructor for creating a horizontal rule. + + Args: + line_style: The line style of the rule. + name: The name of the widget. + id: The ID of the widget in the DOM. + classes: The CSS classes of the widget. + disabled: Whether the widget is disabled or not. + + Returns: + A rule widget with horizontal orientation. + """ + return Rule( + orientation="horizontal", + line_style=line_style, + name=name, + id=id, + classes=classes, + disabled=disabled, + ) + + @classmethod + def vertical( + cls, + line_style: LineStyle = "solid", + name: str | None = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + ) -> Rule: + """Utility constructor for creating a vertical rule. + + Args: + line_style: The line style of the rule. + name: The name of the widget. + id: The ID of the widget in the DOM. + classes: The CSS classes of the widget. + disabled: Whether the widget is disabled or not. + + Returns: + A rule widget with vertical orientation. + """ + return Rule( + orientation="vertical", + line_style=line_style, + name=name, + id=id, + classes=classes, + disabled=disabled, + ) diff --git a/src/textual/widgets/_select.py b/src/textual/widgets/_select.py index 90e6f18411..508da0487d 100644 --- a/src/textual/widgets/_select.py +++ b/src/textual/widgets/_select.py @@ -226,7 +226,7 @@ class Select(Generic[SelectType], Vertical, can_focus=True): value: var[SelectType | None] = var[Optional[SelectType]](None) """The value of the select.""" - class Changed(Message, bubble=True): + class Changed(Message): """Posted when the select value was changed. This message can be handled using a `on_select_changed` method. diff --git a/src/textual/widgets/_selection_list.py b/src/textual/widgets/_selection_list.py index a448c5e412..f8dece7142 100644 --- a/src/textual/widgets/_selection_list.py +++ b/src/textual/widgets/_selection_list.py @@ -97,15 +97,15 @@ class SelectionList(Generic[SelectionType], OptionList): height: auto; } - .-light-mode SelectionList:focus > .selection-list--button-selected { + SelectionList:light:focus > .selection-list--button-selected { color: $primary; } - .-light-mode SelectionList > .selection-list--button-selected-highlighted { + SelectionList:light > .selection-list--button-selected-highlighted { color: $primary; } - .-light-mode SelectionList:focus > .selection-list--button-selected-highlighted { + SelectionList:light:focus > .selection-list--button-selected-highlighted { color: $primary; } diff --git a/src/textual/widgets/_switch.py b/src/textual/widgets/_switch.py index eb0568c618..a6114ff3a8 100644 --- a/src/textual/widgets/_switch.py +++ b/src/textual/widgets/_switch.py @@ -80,7 +80,7 @@ class Switch(Widget, can_focus=True): slider_pos = reactive(0.0) """The position of the slider.""" - class Changed(Message, bubble=True): + class Changed(Message): """Posted when the status of the switch changes. Can be handled using `on_switch_changed` in a subclass of `Switch` diff --git a/src/textual/widgets/_tabbed_content.py b/src/textual/widgets/_tabbed_content.py index 53a1b6afa8..3dafb5579f 100644 --- a/src/textual/widgets/_tabbed_content.py +++ b/src/textual/widgets/_tabbed_content.py @@ -1,6 +1,7 @@ from __future__ import annotations from asyncio import gather +from dataclasses import dataclass from itertools import zip_longest from typing import Generator @@ -26,14 +27,15 @@ class ContentTab(Tab): """A Tab with an associated content id.""" - def __init__(self, label: Text, content_id: str): + def __init__(self, label: Text, content_id: str, disabled: bool = False): """Initialize a ContentTab. Args: label: The label to be displayed within the tab. content_id: The id of the content associated with the tab. + disabled: Is the tab disabled? """ - super().__init__(label, id=content_id) + super().__init__(label, id=content_id, disabled=disabled) class TabPane(Widget): @@ -49,6 +51,30 @@ class TabPane(Widget): } """ + @dataclass + class TabPaneMessage(Message): + """Base class for `TabPane` messages.""" + + tab_pane: TabPane + """The `TabPane` that is he object of this message.""" + + @property + def control(self) -> TabPane: + """The tab pane that is the object of this message. + + This is an alias for the attribute `tab_pane` and is used by the + [`on`][textual.on] decorator. + """ + return self.tab_pane + + @dataclass + class Disabled(TabPaneMessage): + """Sent when a tab pane is disabled via its reactive `disabled`.""" + + @dataclass + class Enabled(TabPaneMessage): + """Sent when a tab pane is enabled via its reactive `disabled`.""" + def __init__( self, title: TextType, @@ -73,6 +99,10 @@ def __init__( *children, name=name, id=id, classes=classes, disabled=disabled ) + def _watch_disabled(self, disabled: bool) -> None: + """Notify the parent `TabbedContent` that a tab pane was enabled/disabled.""" + self.post_message(self.Disabled(self) if disabled else self.Enabled(self)) + class AwaitTabbedContent: """An awaitable returned by [`TabbedContent`][textual.widgets.TabbedContent] methods that modify the tabs.""" @@ -235,7 +265,8 @@ def compose(self) -> ComposeResult: ] # Get a tab for each pane tabs = [ - ContentTab(content._title, content.id or "") for content in pane_content + ContentTab(content._title, content.id or "", disabled=content.disabled) + for content in pane_content ] # Yield the tabs yield Tabs(*tabs, active=self._initial or None) @@ -375,3 +406,99 @@ def _watch_active(self, active: str) -> None: def tab_count(self) -> int: """Total number of tabs.""" return self.get_child_by_type(Tabs).tab_count + + def _on_tabs_tab_disabled(self, event: Tabs.TabDisabled) -> None: + """Disable the corresponding tab pane.""" + event.stop() + tab_id = event.tab.id or "" + try: + with self.prevent(TabPane.Disabled): + self.get_child_by_type(ContentSwitcher).get_child_by_id( + tab_id, expect_type=TabPane + ).disabled = True + except NoMatches: + return + + def _on_tab_pane_disabled(self, event: TabPane.Disabled) -> None: + """Disable the corresponding tab.""" + event.stop() + tab_pane_id = event.tab_pane.id or "" + try: + with self.prevent(Tab.Disabled): + self.get_child_by_type(Tabs).query_one( + f"Tab#{tab_pane_id}" + ).disabled = True + except NoMatches: + return + + def _on_tabs_tab_enabled(self, event: Tabs.TabEnabled) -> None: + """Enable the corresponding tab pane.""" + event.stop() + tab_id = event.tab.id or "" + try: + with self.prevent(TabPane.Enabled): + self.get_child_by_type(ContentSwitcher).get_child_by_id( + tab_id, expect_type=TabPane + ).disabled = False + except NoMatches: + return + + def _on_tab_pane_enabled(self, event: TabPane.Enabled) -> None: + """Enable the corresponding tab.""" + event.stop() + tab_pane_id = event.tab_pane.id or "" + try: + with self.prevent(Tab.Enabled): + self.get_child_by_type(Tabs).query_one( + f"Tab#{tab_pane_id}" + ).disabled = False + except NoMatches: + return + + def disable_tab(self, tab_id: str) -> None: + """Disables the tab with the given ID. + + Args: + tab_id: The ID of the [`TabPane`][textual.widgets.TabPane] to disable. + + Raises: + Tabs.TabError: If there are any issues with the request. + """ + + self.get_child_by_type(Tabs).disable(tab_id) + + def enable_tab(self, tab_id: str) -> None: + """Enables the tab with the given ID. + + Args: + tab_id: The ID of the [`TabPane`][textual.widgets.TabPane] to enable. + + Raises: + Tabs.TabError: If there are any issues with the request. + """ + + self.get_child_by_type(Tabs).enable(tab_id) + + def hide_tab(self, tab_id: str) -> None: + """Hides the tab with the given ID. + + Args: + tab_id: The ID of the [`TabPane`][textual.widgets.TabPane] to hide. + + Raises: + Tabs.TabError: If there are any issues with the request. + """ + + self.get_child_by_type(Tabs).hide(tab_id) + + def show_tab(self, tab_id: str) -> None: + """Shows the tab with the given ID. + + Args: + tab_id: The ID of the [`TabPane`][textual.widgets.TabPane] to show. + + Raises: + Tabs.TabError: If there are any issues with the request. + """ + + self.get_child_by_type(Tabs).show(tab_id) diff --git a/src/textual/widgets/_tabs.py b/src/textual/widgets/_tabs.py index 46fecf72bc..ca03433644 100644 --- a/src/textual/widgets/_tabs.py +++ b/src/textual/widgets/_tabs.py @@ -1,5 +1,6 @@ from __future__ import annotations +from dataclasses import dataclass from typing import ClassVar import rich.repr @@ -104,17 +105,42 @@ class Tab(Static): Tab.-active:hover { color: $text; } + Tab:disabled { + color: $text-disabled; + text-opacity: 50%; + } + Tab.-hidden { + display: none; + } """ - class Clicked(Message): - """A tab was clicked.""" + @dataclass + class TabMessage(Message): + """Tab-related messages. + + These are mostly intended for internal use when interacting with `Tabs`. + """ tab: Tab - """The tab that was clicked.""" + """The tab that is the object of this message.""" - def __init__(self, tab: Tab) -> None: - self.tab = tab - super().__init__() + @property + def control(self) -> Tab: + """The tab that is the object of this message. + + This is an alias for the attribute `tab` and is used by the + [`on`][textual.on] decorator. + """ + return self.tab + + class Clicked(TabMessage): + """A tab was clicked.""" + + class Disabled(TabMessage): + """A tab was disabled.""" + + class Enabled(TabMessage): + """A tab was enabled.""" def __init__( self, @@ -122,6 +148,7 @@ def __init__( *, id: str | None = None, classes: str | None = None, + disabled: bool = False, ) -> None: """Initialise a Tab. @@ -129,9 +156,10 @@ def __init__( label: The label to use in the tab. id: Optional ID for the widget. classes: Space separated list of class names. + disabled: Whether the tab is disabled or not. """ self.label = Text.from_markup(label) if isinstance(label, str) else label - super().__init__(id=id, classes=classes) + super().__init__(id=id, classes=classes, disabled=disabled) self.update(label) @property @@ -143,6 +171,10 @@ def _on_click(self): """Inform the message that the tab was clicked.""" self.post_message(self.Clicked(self)) + def _watch_disabled(self, disabled: bool) -> None: + """Notify the parent `Tabs` that a tab was enabled/disabled.""" + self.post_message(self.Disabled(self) if disabled else self.Enabled(self)) + class Tabs(Widget, can_focus=True): """A row of tabs.""" @@ -184,8 +216,8 @@ class Tabs(Widget, can_focus=True): class TabError(Exception): """Exception raised when there is an error relating to tabs.""" - class TabActivated(Message): - """Sent when a new tab is activated.""" + class TabMessage(Message): + """Parent class for all messages that have to do with a specific tab.""" ALLOW_SELECTOR_MATCH = {"tab"} """Additional message attributes that can be used with the [`on` decorator][textual.on].""" @@ -195,20 +227,20 @@ def __init__(self, tabs: Tabs, tab: Tab) -> None: Args: tabs: The Tabs widget. - tab: The tab that was activated. + tab: The tab that is the object of this message. """ self.tabs: Tabs = tabs """The tabs widget containing the tab.""" self.tab: Tab = tab - """The tab that was activated.""" + """The tab that is the object of this message.""" super().__init__() @property def control(self) -> Tabs: - """The tabs widget containing the tab that was activated. + """The tabs widget containing the tab that is the object of this message. - This is an alias for [`TabActivated.tabs`][textual.widgets.Tabs.TabActivated.tabs] - which is used by the [`on`][textual.on] decorator. + This is an alias for the attribute `tabs` and is used by the + [`on`][textual.on] decorator. """ return self.tabs @@ -216,6 +248,21 @@ def __rich_repr__(self) -> rich.repr.Result: yield self.tabs yield self.tab + class TabActivated(TabMessage): + """Sent when a new tab is activated.""" + + class TabDisabled(TabMessage): + """Sent when a tab is disabled.""" + + class TabEnabled(TabMessage): + """Sent when a tab is enabled.""" + + class TabHidden(TabMessage): + """Sent when a tab is hidden.""" + + class TabShown(TabMessage): + """Sent when a tab is shown.""" + class Cleared(Message): """Sent when there are no active tabs.""" @@ -299,10 +346,24 @@ def tab_count(self) -> int: """Total number of tabs.""" return len(self.query("#tabs-list > Tab")) + @property + def _potentially_active_tabs(self) -> list[Tab]: + """List of all tabs that could be active. + + This list is comprised of all tabs that are shown and enabled, + plus the active tab in case it is disabled. + """ + return [ + tab + for tab in self.query("#tabs-list > Tab").results(Tab) + if ((not tab.disabled or tab is self.active_tab) and tab.display) + ] + @property def _next_active(self) -> Tab | None: """Next tab to make active if the active tab is removed.""" - tabs = list(self.query("#tabs-list > Tab").results(Tab)) + active_tab = self.active_tab + tabs = self._potentially_active_tabs if self.active_tab is None: return None try: @@ -412,7 +473,7 @@ def remove_tab(self, tab_or_id: Tab | str | None) -> AwaitRemove: """Remove a tab. Args: - tab_or_id: The Tab's id. + tab_or_id: The Tab to remove or its id. Returns: An awaitable object that waits for the tab to be removed. @@ -590,10 +651,117 @@ def _move_tab(self, direction: int) -> None: active_tab = self.active_tab if active_tab is None: return - tabs = list(self.query(Tab)) + tabs = self._potentially_active_tabs if not tabs: return tab_count = len(tabs) new_tab_index = (tabs.index(active_tab) + direction) % tab_count self.active = tabs[new_tab_index].id or "" self._scroll_active_tab() + + def _on_tab_disabled(self, event: Tab.Disabled) -> None: + """Re-post the disabled message.""" + event.stop() + self.post_message(self.TabDisabled(self, event.tab)) + + def _on_tab_enabled(self, event: Tab.Enabled) -> None: + """Re-post the enabled message.""" + event.stop() + self.post_message(self.TabEnabled(self, event.tab)) + + def disable(self, tab_id: str) -> Tab: + """Disable the indicated tab. + + Args: + tab_id: The ID of the [`Tab`][textual.widgets.Tab] to disable. + + Returns: + The [`Tab`][textual.widgets.Tab] that was targeted. + + Raises: + TabError: If there are any issues with the request. + """ + + try: + tab_to_disable = self.query_one(f"#tabs-list > Tab#{tab_id}", Tab) + except NoMatches: + raise self.TabError( + f"There is no tab with ID {tab_id!r} to disable." + ) from None + + tab_to_disable.disabled = True + return tab_to_disable + + def enable(self, tab_id: str) -> Tab: + """Enable the indicated tab. + + Args: + tab_id: The ID of the [`Tab`][textual.widgets.Tab] to enable. + + Returns: + The [`Tab`][textual.widgets.Tab] that was targeted. + + Raises: + TabError: If there are any issues with the request. + """ + + try: + tab_to_enable = self.query_one(f"#tabs-list > Tab#{tab_id}", Tab) + except NoMatches: + raise self.TabError( + f"There is no tab with ID {tab_id!r} to enable." + ) from None + + tab_to_enable.disabled = False + return tab_to_enable + + def hide(self, tab_id: str) -> Tab: + """Hide the indicated tab. + + Args: + tab_id: The ID of the [`Tab`][textual.widgets.Tab] to hide. + + Returns: + The [`Tab`][textual.widgets.Tab] that was targeted. + + Raises: + TabError: If there are any issues with the request. + """ + + try: + tab_to_hide = self.query_one(f"#tabs-list > Tab#{tab_id}", Tab) + except NoMatches: + raise self.TabError(f"There is no tab with ID {tab_id!r} to hide.") + + if tab_to_hide.has_class("-active"): + next_tab = self._next_active + self.active = next_tab.id or "" if next_tab else "" + tab_to_hide.add_class("-hidden") + self.post_message(self.TabHidden(self, tab_to_hide)) + self.call_after_refresh(self._highlight_active) + return tab_to_hide + + def show(self, tab_id: str) -> Tab: + """Show the indicated tab. + + Args: + tab_id: The ID of the [`Tab`][textual.widgets.Tab] to show. + + Returns: + The [`Tab`][textual.widgets.Tab] that was targeted. + + Raises: + TabError: If there are any issues with the request. + """ + + try: + tab_to_show = self.query_one(f"#tabs-list > Tab#{tab_id}", Tab) + except NoMatches: + raise self.TabError(f"There is no tab with ID {tab_id!r} to show.") + + tab_to_show.remove_class("-hidden") + self.post_message(self.TabShown(self, tab_to_show)) + if not self.active: + self._activate_tab(tab_to_show) + self.call_after_refresh(self._highlight_active) + return tab_to_show diff --git a/src/textual/widgets/_text_area.py b/src/textual/widgets/_text_area.py new file mode 100644 index 0000000000..0fbcba4c34 --- /dev/null +++ b/src/textual/widgets/_text_area.py @@ -0,0 +1,1928 @@ +from __future__ import annotations + +import re +from collections import defaultdict +from dataclasses import dataclass, field +from functools import lru_cache +from pathlib import Path +from typing import TYPE_CHECKING, Any, Iterable, Optional, Tuple + +from rich.style import Style +from rich.text import Text + +from textual._text_area_theme import TextAreaTheme +from textual._tree_sitter import TREE_SITTER +from textual.color import Color +from textual.document._document import ( + Document, + DocumentBase, + EditResult, + Location, + Selection, + _utf8_encode, +) +from textual.document._languages import BUILTIN_LANGUAGES +from textual.document._syntax_aware_document import ( + SyntaxAwareDocument, + SyntaxAwareDocumentError, +) +from textual.expand_tabs import expand_tabs_inline + +if TYPE_CHECKING: + from tree_sitter import Language + from tree_sitter.binding import Query + +from textual import events, log +from textual._cells import cell_len +from textual._types import Literal, Protocol, runtime_checkable +from textual.binding import Binding +from textual.events import Message, MouseEvent +from textual.geometry import Offset, Region, Size, Spacing, clamp +from textual.reactive import Reactive, reactive +from textual.scroll_view import ScrollView +from textual.strip import Strip + +_OPENING_BRACKETS = {"{": "}", "[": "]", "(": ")"} +_CLOSING_BRACKETS = {v: k for k, v in _OPENING_BRACKETS.items()} +_TREE_SITTER_PATH = Path(__file__).parent / "../tree-sitter/" +_HIGHLIGHTS_PATH = _TREE_SITTER_PATH / "highlights/" + +StartColumn = int +EndColumn = Optional[int] +HighlightName = str +Highlight = Tuple[StartColumn, EndColumn, HighlightName] +"""A tuple representing a syntax highlight within one line.""" + + +class ThemeDoesNotExist(Exception): + """Raised when the user tries to use a theme which does not exist. + This means a theme which is not builtin, or has not been registered. + """ + + pass + + +class LanguageDoesNotExist(Exception): + """Raised when the user tries to use a language which does not exist. + This means a language which is not builtin, or has not been registered. + """ + + pass + + +@dataclass +class TextAreaLanguage: + """A container for a language which has been registered with the TextArea. + + Attributes: + name: The name of the language. + language: The tree-sitter Language. + highlight_query: The tree-sitter highlight query corresponding to the language, as a string. + """ + + name: str + language: "Language" + highlight_query: str + + +class TextArea(ScrollView, can_focus=True): + DEFAULT_CSS = """\ +TextArea { + width: 1fr; + height: 1fr; +} +""" + + BINDINGS = [ + Binding("escape", "screen.focus_next", "Shift Focus", show=False), + # Cursor movement + Binding("up", "cursor_up", "cursor up", show=False), + Binding("down", "cursor_down", "cursor down", show=False), + Binding("left", "cursor_left", "cursor left", show=False), + Binding("right", "cursor_right", "cursor right", show=False), + Binding("ctrl+left", "cursor_word_left", "cursor word left", show=False), + Binding("ctrl+right", "cursor_word_right", "cursor word right", show=False), + Binding("home,ctrl+a", "cursor_line_start", "cursor line start", show=False), + Binding("end,ctrl+e", "cursor_line_end", "cursor line end", show=False), + Binding("pageup", "cursor_page_up", "cursor page up", show=False), + Binding("pagedown", "cursor_page_down", "cursor page down", show=False), + # Making selections (generally holding the shift key and moving cursor) + Binding( + "ctrl+shift+left", + "cursor_word_left(True)", + "cursor left word select", + show=False, + ), + Binding( + "ctrl+shift+right", + "cursor_word_right(True)", + "cursor right word select", + show=False, + ), + Binding( + "shift+home", + "cursor_line_start(True)", + "cursor line start select", + show=False, + ), + Binding( + "shift+end", "cursor_line_end(True)", "cursor line end select", show=False + ), + Binding("shift+up", "cursor_up(True)", "cursor up select", show=False), + Binding("shift+down", "cursor_down(True)", "cursor down select", show=False), + Binding("shift+left", "cursor_left(True)", "cursor left select", show=False), + Binding("shift+right", "cursor_right(True)", "cursor right select", show=False), + # Shortcut ways of making selections + # Binding("f5", "select_word", "select word", show=False), + Binding("f6", "select_line", "select line", show=False), + Binding("f7", "select_all", "select all", show=False), + # Deletion + Binding("backspace", "delete_left", "delete left", show=False), + Binding( + "ctrl+w", "delete_word_left", "delete left to start of word", show=False + ), + Binding("delete,ctrl+d", "delete_right", "delete right", show=False), + Binding( + "ctrl+f", "delete_word_right", "delete right to start of word", show=False + ), + Binding("ctrl+x", "delete_line", "delete line", show=False), + Binding( + "ctrl+u", "delete_to_start_of_line", "delete to line start", show=False + ), + Binding("ctrl+k", "delete_to_end_of_line", "delete to line end", show=False), + ] + """ + | Key(s) | Description | + | :- | :- | + | escape | Focus on the next item. | + | up | Move the cursor up. | + | down | Move the cursor down. | + | left | Move the cursor left. | + | ctrl+left | Move the cursor to the start of the word. | + | ctrl+shift+left | Move the cursor to the start of the word and select. | + | right | Move the cursor right. | + | ctrl+right | Move the cursor to the end of the word. | + | ctrl+shift+right | Move the cursor to the end of the word and select. | + | home,ctrl+a | Move the cursor to the start of the line. | + | end,ctrl+e | Move the cursor to the end of the line. | + | shift+home | Move the cursor to the start of the line and select. | + | shift+end | Move the cursor to the end of the line and select. | + | pageup | Move the cursor one page up. | + | pagedown | Move the cursor one page down. | + | shift+up | Select while moving the cursor up. | + | shift+down | Select while moving the cursor down. | + | shift+left | Select while moving the cursor left. | + | shift+right | Select while moving the cursor right. | + | backspace | Delete character to the left of cursor. | + | ctrl+w | Delete from cursor to start of the word. | + | delete,ctrl+d | Delete character to the right of cursor. | + | ctrl+f | Delete from cursor to end of the word. | + | ctrl+x | Delete the current line. | + | ctrl+u | Delete from cursor to the start of the line. | + | ctrl+k | Delete from cursor to the end of the line. | + | f6 | Select the current line. | + | f7 | Select all text in the document. | + """ + + language: Reactive[str | None] = reactive(None, always_update=True, init=False) + """The language to use. + + This must be set to a valid, non-None value for syntax highlighting to work. + + If the value is a string, a built-in language parser will be used if available. + + If you wish to use an unsupported language, you'll have to register + it first using [`TextArea.register_language`][textual.widgets._text_area.TextArea.register_language]. + """ + + theme: Reactive[str | None] = reactive(None, always_update=True, init=False) + """The name of the theme to use. + + Themes must be registered using [`TextArea.register_theme`][textual.widgets._text_area.TextArea.register_theme] before they can be used. + + Syntax highlighting is only possible when the `language` attribute is set. + """ + + selection: Reactive[Selection] = reactive( + Selection(), always_update=True, init=False + ) + """The selection start and end locations (zero-based line_index, offset). + + This represents the cursor location and the current selection. + + The `Selection.end` always refers to the cursor location. + + If no text is selected, then `Selection.end == Selection.start` is True. + + The text selected in the document is available via the `TextArea.selected_text` property. + """ + + show_line_numbers: Reactive[bool] = reactive(True) + """True to show the line number column on the left edge, otherwise False. + + Changing this value will immediately re-render the `TextArea`.""" + + indent_width: Reactive[int] = reactive(4) + """The width of tabs or the multiple of spaces to align to on pressing the `tab` key. + + If the document currently open contains tabs that are currently visible on screen, + altering this value will immediately change the display width of the visible tabs. + """ + + match_cursor_bracket: Reactive[bool] = reactive(True) + """If the cursor is at a bracket, highlight the matching bracket (if found).""" + + cursor_blink: Reactive[bool] = reactive(True) + """True if the cursor should blink.""" + + _cursor_blink_visible: Reactive[bool] = reactive(True, repaint=False) + """Indicates where the cursor is in the blink cycle. If it's currently + not visible due to blinking, this is False.""" + + @dataclass + class Changed(Message): + """Posted when the content inside the TextArea changes. + + Handle this message using the `on` decorator - `@on(TextArea.Changed)` + or a method named `on_text_area_changed`. + """ + + text_area: TextArea + """The `text_area` that sent this message.""" + + @property + def control(self) -> TextArea: + """The `TextArea` that sent this message.""" + return self.text_area + + @dataclass + class SelectionChanged(Message): + """Posted when the selection changes. + + This includes when the cursor moves or when text is selected.""" + + selection: Selection + """The new selection.""" + text_area: TextArea + """The `text_area` that sent this message.""" + + @property + def control(self) -> TextArea: + return self.text_area + + def __init__( + self, + text: str = "", + *, + language: str | None = None, + theme: str | None = None, + name: str | None = None, + id: str | None = None, + classes: str | None = None, + disabled: bool = False, + ) -> None: + """Construct a new `TextArea`. + + Args: + text: The initial text to load into the TextArea. + language: The language to use. + theme: The theme to use. + name: The name of the `TextArea` widget. + id: The ID of the widget, used to refer to it from Textual CSS. + classes: One or more Textual CSS compatible class names separated by spaces. + disabled: True if the widget is disabled. + """ + super().__init__(name=name, id=id, classes=classes, disabled=disabled) + self._initial_text = text + + self._languages: dict[str, TextAreaLanguage] = {} + """Maps language names to TextAreaLanguage.""" + + self._themes: dict[str, TextAreaTheme] = {} + """Maps theme names to TextAreaTheme.""" + + self.indent_type: Literal["tabs", "spaces"] = "spaces" + """Whether to indent using tabs or spaces.""" + + self._word_pattern = re.compile(r"(?<=\W)(?=\w)|(?<=\w)(?=\W)") + """Compiled regular expression for what we consider to be a 'word'.""" + + self._last_intentional_cell_width: int = 0 + """Tracks the last column (measured in terms of cell length, since we care here about where the cursor + visually moves rather than logical characters) the user explicitly navigated to so that we can reset to it + whenever possible.""" + + self._undo_stack: list[Undoable] = [] + """A stack (the end of the list is the top of the stack) for tracking edits.""" + + self._selecting = False + """True if we're currently selecting text using the mouse, otherwise False.""" + + self._matching_bracket_location: Location | None = None + """The location (row, column) of the bracket which matches the bracket the + cursor is currently at. If the cursor is at a bracket, or there's no matching + bracket, this will be `None`.""" + + self._highlights: dict[int, list[Highlight]] = defaultdict(list) + """Mapping line numbers to the set of highlights for that line.""" + + self._highlight_query: "Query" | None = None + """The query that's currently being used for highlighting.""" + + self.document: DocumentBase = Document(text) + """The document this widget is currently editing.""" + + self._theme: TextAreaTheme | None = None + """The `TextAreaTheme` corresponding to the set theme name. When the `theme` + reactive is set as a string, the watcher will update this attribute to the + corresponding `TextAreaTheme` object.""" + + self.language = language + + self.theme = theme + + @staticmethod + def _get_builtin_highlight_query(language_name: str) -> str: + """Get the highlight query for a builtin language. + + Args: + language_name: The name of the builtin language. + + Returns: + The highlight query. + """ + try: + highlight_query_path = ( + Path(_HIGHLIGHTS_PATH.resolve()) / f"{language_name}.scm" + ) + highlight_query = highlight_query_path.read_text() + except OSError as e: + log.warning(f"Unable to load highlight query. {e}") + highlight_query = "" + + return highlight_query + + def _build_highlight_map(self) -> None: + """Query the tree for ranges to highlights, and update the internal highlights mapping.""" + highlights = self._highlights + highlights.clear() + if not self._highlight_query: + return + + captures = self.document.query_syntax_tree(self._highlight_query) + for capture in captures: + node, highlight_name = capture + node_start_row, node_start_column = node.start_point + node_end_row, node_end_column = node.end_point + + if node_start_row == node_end_row: + highlight = (node_start_column, node_end_column, highlight_name) + highlights[node_start_row].append(highlight) + else: + # Add the first line of the node range + highlights[node_start_row].append( + (node_start_column, None, highlight_name) + ) + + # Add the middle lines - entire row of this node is highlighted + for node_row in range(node_start_row + 1, node_end_row): + highlights[node_row].append((0, None, highlight_name)) + + # Add the last line of the node range + highlights[node_end_row].append((0, node_end_column, highlight_name)) + + def _watch_selection(self, selection: Selection) -> None: + """When the cursor moves, scroll it into view.""" + self.scroll_cursor_visible() + cursor_location = selection.end + cursor_row, cursor_column = cursor_location + + try: + character = self.document[cursor_row][cursor_column] + except IndexError: + character = "" + + # Record the location of a matching closing/opening bracket. + match_location = self.find_matching_bracket(character, cursor_location) + self._matching_bracket_location = match_location + if match_location is not None: + match_row, match_column = match_location + if match_row in range(*self._visible_line_indices): + self.refresh_lines(match_row) + + self.app.cursor_position = self.cursor_screen_offset + self.post_message(self.SelectionChanged(selection, self)) + + def find_matching_bracket( + self, bracket: str, search_from: Location + ) -> Location | None: + """If the character is a bracket, find the matching bracket. + + Args: + bracket: The character we're searching for the matching bracket of. + search_from: The location to start the search. + + Returns: + The `Location` of the matching bracket, or `None` if it's not found. + If the character is not available for bracket matching, `None` is returned. + """ + match_location = None + bracket_stack = [] + if bracket in _OPENING_BRACKETS: + for candidate, candidate_location in self._yield_character_locations( + search_from + ): + if candidate in _OPENING_BRACKETS: + bracket_stack.append(candidate) + elif candidate in _CLOSING_BRACKETS: + if ( + bracket_stack + and bracket_stack[-1] == _CLOSING_BRACKETS[candidate] + ): + bracket_stack.pop() + if not bracket_stack: + match_location = candidate_location + break + elif bracket in _CLOSING_BRACKETS: + for ( + candidate, + candidate_location, + ) in self._yield_character_locations_reverse(search_from): + if candidate in _CLOSING_BRACKETS: + bracket_stack.append(candidate) + elif candidate in _OPENING_BRACKETS: + if ( + bracket_stack + and bracket_stack[-1] == _OPENING_BRACKETS[candidate] + ): + bracket_stack.pop() + if not bracket_stack: + match_location = candidate_location + break + + return match_location + + def _validate_selection(self, selection: Selection) -> Selection: + """Clamp the selection to valid locations.""" + start, end = selection + clamp_visitable = self.clamp_visitable + return Selection(clamp_visitable(start), clamp_visitable(end)) + + def _watch_language(self, language: str | None) -> None: + """When the language is updated, update the type of document.""" + if language is not None and language not in self.available_languages: + raise LanguageDoesNotExist( + f"{language!r} is not a builtin language, or it has not been registered. " + f"To use a custom language, register it first using `register_language`, " + f"then switch to it by setting the `TextArea.language` attribute." + ) + + self._set_document( + self.document.text if self.document is not None else self._initial_text, + language, + ) + self._initial_text = "" + + def _watch_show_line_numbers(self) -> None: + """The line number gutter contributes to virtual size, so recalculate.""" + self._refresh_size() + + def _watch_indent_width(self) -> None: + """Changing width of tabs will change document display width.""" + self._refresh_size() + + def _watch_theme(self, theme: str | None) -> None: + """We set the styles on this widget when the theme changes, to ensure that + if padding is applied, the colours match.""" + + if theme is None: + # If the theme is None, use the default. + theme_object = TextAreaTheme.default() + else: + # If the user supplied a string theme name, find it and apply it. + try: + theme_object = self._themes[theme] + except KeyError: + theme_object = TextAreaTheme.get_builtin_theme(theme) + + if theme_object is None: + raise ThemeDoesNotExist( + f"{theme!r} is not a builtin theme, or it has not been registered. " + f"To use a custom theme, register it first using `register_theme`, " + f"then switch to that theme by setting the `TextArea.theme` attribute." + ) + + self._theme = theme_object + if theme_object: + base_style = theme_object.base_style + if base_style: + color = base_style.color + background = base_style.bgcolor + if color: + self.styles.color = Color.from_rich_color(color) + if background: + self.styles.background = Color.from_rich_color(background) + + @property + def available_themes(self) -> set[str]: + """A list of the names of the themes available to the `TextArea`. + + The values in this list can be assigned `theme` reactive attribute of + `TextArea`. + + You can retrieve the full specification for a theme by passing one of + the strings from this list into `TextAreaTheme.get_by_name(theme_name: str)`. + + Alternatively, you can directly retrieve a list of `TextAreaTheme` objects + (which contain the full theme specification) by calling + `TextAreaTheme.builtin_themes()`. + """ + return { + theme.name for theme in TextAreaTheme.builtin_themes() + } | self._themes.keys() + + def register_theme(self, theme: TextAreaTheme) -> None: + """Register a theme for use by the `TextArea`. + + After registering a theme, you can set themes by assigning the theme + name to the `TextArea.theme` reactive attribute. For example + `text_area.theme = "my_custom_theme"` where `"my_custom_theme"` is the + name of the theme you registered. + + If you supply a theme with a name that already exists that theme + will be overwritten. + """ + self._themes[theme.name] = theme + + @property + def available_languages(self) -> set[str]: + """A list of the names of languages available to the `TextArea`. + + The values in this list can be assigned to the `language` reactive attribute + of `TextArea`. + + The returned list contains the builtin languages plus those registered via the + `register_language` method. Builtin languages will be listed before + user-registered languages, but there are no other ordering guarantees. + """ + return set(BUILTIN_LANGUAGES) | self._languages.keys() + + def register_language( + self, + language: str | "Language", + highlight_query: str, + ) -> None: + """Register a language and corresponding highlight query. + + Calling this method does not change the language of the `TextArea`. + On switching to this language (via the `language` reactive attribute), + syntax highlighting will be performed using the given highlight query. + + If a string `name` is supplied for a builtin supported language, then + this method will update the default highlight query for that language. + + Registering a language only registers it to this instance of `TextArea`. + + Args: + language: A string referring to a builtin language or a tree-sitter `Language` object. + highlight_query: The highlight query to use for syntax highlighting this language. + """ + + # If tree-sitter is unavailable, do nothing. + if not TREE_SITTER: + return + + from tree_sitter_languages import get_language + + if isinstance(language, str): + language_name = language + language = get_language(language_name) + else: + language_name = language.name + + # Update the custom languages. When changing the document, + # we should first look in here for a language specification. + # If nothing is found, then we can go to the builtin languages. + self._languages[language_name] = TextAreaLanguage( + name=language_name, + language=language, + highlight_query=highlight_query, + ) + # If we updated the currently set language, rebuild the highlights + # using the newly updated highlights query. + if language_name == self.language: + self._set_document(self.text, language_name) + + def _set_document(self, text: str, language: str | None) -> None: + """Construct and return an appropriate document. + + Args: + text: The text of the document. + language: The name of the language to use. This must either be a + built-in supported language, or a language previously registered + via the `register_language` method. + """ + self._highlight_query = None + if TREE_SITTER and language: + # Attempt to get the override language. + text_area_language = self._languages.get(language, None) + document_language: str | "Language" + if text_area_language: + document_language = text_area_language.language + highlight_query = text_area_language.highlight_query + else: + document_language = language + highlight_query = self._get_builtin_highlight_query(language) + document: DocumentBase + try: + document = SyntaxAwareDocument(text, document_language) + except SyntaxAwareDocumentError: + document = Document(text) + log.warning( + f"Parser not found for language {document_language!r}. Parsing disabled." + ) + else: + self._highlight_query = document.prepare_query(highlight_query) + elif language and not TREE_SITTER: + log.warning( + "tree-sitter not available in this environment. Parsing disabled.\n" + "You may need to install the `syntax` extras alongside textual.\n" + "Try `pip install 'textual[syntax]'` or '`poetry add textual[syntax]'." + ) + document = Document(text) + else: + document = Document(text) + + self.document = document + self._build_highlight_map() + + @property + def _visible_line_indices(self) -> tuple[int, int]: + """Return the visible line indices as a tuple (top, bottom). + + Returns: + A tuple (top, bottom) indicating the top and bottom visible line indices. + """ + _, scroll_offset_y = self.scroll_offset + return scroll_offset_y, scroll_offset_y + self.size.height + + def _watch_scroll_x(self) -> None: + self.app.cursor_position = self.cursor_screen_offset + + def _watch_scroll_y(self) -> None: + self.app.cursor_position = self.cursor_screen_offset + + def load_text(self, text: str) -> None: + """Load text into the TextArea. + + This will replace the text currently in the TextArea. + + Args: + text: The text to load into the TextArea. + """ + self._set_document(text, self.language) + self.move_cursor((0, 0)) + self._refresh_size() + + def load_document(self, document: DocumentBase) -> None: + """Load a document into the TextArea. + + Args: + document: The document to load into the TextArea. + """ + self.document = document + self.move_cursor((0, 0)) + self._refresh_size() + + @property + def is_syntax_aware(self) -> bool: + """True if the TextArea is currently syntax aware - i.e. it's parsing document content.""" + return isinstance(self.document, SyntaxAwareDocument) + + def _yield_character_locations( + self, start: Location + ) -> Iterable[tuple[str, Location]]: + """Yields character locations starting from the given location. + + Does not yield location of line separator characters like `\\n`. + + Args: + start: The location to start yielding from. + + Returns: + Yields tuples of (character, (row, column)). + """ + row, column = start + document = self.document + line_count = document.line_count + + while 0 <= row < line_count: + line = document[row] + while column < len(line): + yield line[column], (row, column) + column += 1 + column = 0 + row += 1 + + def _yield_character_locations_reverse( + self, start: Location + ) -> Iterable[tuple[str, Location]]: + row, column = start + document = self.document + line_count = document.line_count + + while line_count > row >= 0: + line = document[row] + if column == -1: + column = len(line) - 1 + while column >= 0: + yield line[column], (row, column) + column -= 1 + row -= 1 + + def _refresh_size(self) -> None: + """Update the virtual size of the TextArea.""" + width, height = self.document.get_size(self.indent_width) + # +1 width to make space for the cursor resting at the end of the line + self.virtual_size = Size(width + self.gutter_width + 1, height) + + def render_line(self, widget_y: int) -> Strip: + """Render a single line of the TextArea. Called by Textual. + + Args: + widget_y: Y Coordinate of line relative to the widget region. + + Returns: + A rendered line. + """ + document = self.document + scroll_x, scroll_y = self.scroll_offset + + # Account for how much the TextArea is scrolled. + line_index = widget_y + scroll_y + + # Render the lines beyond the valid line numbers + out_of_bounds = line_index >= document.line_count + if out_of_bounds: + return Strip.blank(self.size.width) + + theme = self._theme + + # Get the line from the Document. + line_string = document.get_line(line_index) + line = Text(line_string, end="") + + line_character_count = len(line) + line.tab_size = self.indent_width + virtual_width, virtual_height = self.virtual_size + expanded_length = max(virtual_width, self.size.width) + line.set_length(expanded_length) + + selection = self.selection + start, end = selection + selection_top, selection_bottom = sorted(selection) + selection_top_row, selection_top_column = selection_top + selection_bottom_row, selection_bottom_column = selection_bottom + + highlights = self._highlights + if highlights and theme: + line_bytes = _utf8_encode(line_string) + byte_to_codepoint = build_byte_to_codepoint_dict(line_bytes) + get_highlight_from_theme = theme.syntax_styles.get + line_highlights = highlights[line_index] + for highlight_start, highlight_end, highlight_name in line_highlights: + node_style = get_highlight_from_theme(highlight_name) + if node_style is not None: + line.stylize( + node_style, + byte_to_codepoint.get(highlight_start, 0), + byte_to_codepoint.get(highlight_end) if highlight_end else None, + ) + + cursor_row, cursor_column = end + cursor_line_style = theme.cursor_line_style if theme else None + if cursor_line_style and cursor_row == line_index: + line.stylize(cursor_line_style) + + # Selection styling + if start != end and selection_top_row <= line_index <= selection_bottom_row: + # If this row intersects with the selection range + selection_style = theme.selection_style if theme else None + cursor_row, _ = end + if selection_style: + if line_character_count == 0 and line_index != cursor_row: + # A simple highlight to show empty lines are included in the selection + line = Text("▌", end="", style=Style(color=selection_style.bgcolor)) + line.set_length(self.virtual_size.width) + else: + if line_index == selection_top_row == selection_bottom_row: + # Selection within a single line + line.stylize( + selection_style, + start=selection_top_column, + end=selection_bottom_column, + ) + else: + # Selection spanning multiple lines + if line_index == selection_top_row: + line.stylize( + selection_style, + start=selection_top_column, + end=line_character_count, + ) + elif line_index == selection_bottom_row: + line.stylize(selection_style, end=selection_bottom_column) + else: + line.stylize(selection_style, end=line_character_count) + + # Highlight the cursor + matching_bracket = self._matching_bracket_location + match_cursor_bracket = self.match_cursor_bracket + draw_matched_brackets = ( + match_cursor_bracket and matching_bracket is not None and start == end + ) + + if cursor_row == line_index: + draw_cursor = not self.cursor_blink or ( + self.cursor_blink and self._cursor_blink_visible + ) + if draw_matched_brackets: + matching_bracket_style = theme.bracket_matching_style if theme else None + if matching_bracket_style: + line.stylize( + matching_bracket_style, + cursor_column, + cursor_column + 1, + ) + + if draw_cursor: + cursor_style = theme.cursor_style if theme else None + if cursor_style: + line.stylize(cursor_style, cursor_column, cursor_column + 1) + + # Highlight the partner opening/closing bracket. + if draw_matched_brackets: + # mypy doesn't know matching bracket is guaranteed to be non-None + assert matching_bracket is not None + bracket_match_row, bracket_match_column = matching_bracket + if theme and bracket_match_row == line_index: + matching_bracket_style = theme.bracket_matching_style + if matching_bracket_style: + line.stylize( + matching_bracket_style, + bracket_match_column, + bracket_match_column + 1, + ) + + # Build the gutter text for this line + gutter_width = self.gutter_width + if self.show_line_numbers: + if cursor_row == line_index: + gutter_style = theme.cursor_line_gutter_style if theme else None + else: + gutter_style = theme.gutter_style if theme else None + + gutter_width_no_margin = gutter_width - 2 + gutter = Text( + f"{line_index + 1:>{gutter_width_no_margin}} ", + style=gutter_style or "", + end="", + ) + else: + gutter = Text("", end="") + + # Render the gutter and the text of this line + console = self.app.console + gutter_segments = console.render(gutter) + text_segments = console.render( + line, + console.options.update_width(expanded_length), + ) + + # Crop the line to show only the visible part (some may be scrolled out of view) + gutter_strip = Strip(gutter_segments, cell_length=gutter_width) + text_strip = Strip(text_segments).crop( + scroll_x, scroll_x + virtual_width - gutter_width + ) + + # Stylize the line the cursor is currently on. + if cursor_row == line_index: + text_strip = text_strip.extend_cell_length( + expanded_length, cursor_line_style + ) + else: + text_strip = text_strip.extend_cell_length( + expanded_length, theme.base_style if theme else None + ) + + # Join and return the gutter and the visible portion of this line + strip = Strip.join([gutter_strip, text_strip]).simplify() + + return strip.apply_style( + theme.base_style + if theme and theme.base_style is not None + else self.rich_style + ) + + @property + def text(self) -> str: + """The entire text content of the document.""" + return self.document.text + + @property + def selected_text(self) -> str: + """The text between the start and end points of the current selection.""" + start, end = self.selection + return self.get_text_range(start, end) + + def get_text_range(self, start: Location, end: Location) -> str: + """Get the text between a start and end location. + + Args: + start: The start location. + end: The end location. + + Returns: + The text between start and end. + """ + start, end = sorted((start, end)) + return self.document.get_text_range(start, end) + + def edit(self, edit: Edit) -> Any: + """Perform an Edit. + + Args: + edit: The Edit to perform. + + Returns: + Data relating to the edit that may be useful. The data returned + may be different depending on the edit performed. + """ + result = edit.do(self) + self._refresh_size() + edit.after(self) + self._build_highlight_map() + self.post_message(self.Changed(self)) + return result + + async def _on_key(self, event: events.Key) -> None: + """Handle key presses which correspond to document inserts.""" + key = event.key + insert_values = { + "tab": " " * self._find_columns_to_next_tab_stop(), + "enter": "\n", + } + self._restart_blink() + if event.is_printable or key in insert_values: + event.stop() + event.prevent_default() + insert = insert_values.get(key, event.character) + # `insert` is not None because event.character cannot be + # None because we've checked that it's printable. + assert insert is not None + start, end = self.selection + self.replace(insert, start, end, maintain_selection_offset=False) + + def _find_columns_to_next_tab_stop(self) -> int: + """Get the location of the next tab stop after the cursors position on the current line. + + If the cursor is already at a tab stop, this returns the *next* tab stop location. + + Returns: + The number of cells to the next tab stop from the current cursor column. + """ + cursor_row, cursor_column = self.cursor_location + line_text = self.document[cursor_row] + indent_width = self.indent_width + if not line_text: + return indent_width + + width_before_cursor = self.get_column_width(cursor_row, cursor_column) + spaces_to_insert = indent_width - ( + (indent_width + width_before_cursor) % indent_width + ) + + return spaces_to_insert + + def get_target_document_location(self, event: MouseEvent) -> Location: + """Given a MouseEvent, return the row and column offset of the event in document-space. + + Args: + event: The MouseEvent. + + Returns: + The location of the mouse event within the document. + """ + scroll_x, scroll_y = self.scroll_offset + target_x = event.x - self.gutter_width + scroll_x - self.gutter.left + target_x = max(target_x, 0) + target_row = clamp( + event.y + scroll_y - self.gutter.top, + 0, + self.document.line_count - 1, + ) + target_column = self.cell_width_to_column_index(target_x, target_row) + return target_row, target_column + + # --- Lower level event/key handling + @property + def gutter_width(self) -> int: + """The width of the gutter (the left column containing line numbers). + + Returns: + The cell-width of the line number column. If `show_line_numbers` is `False` returns 0. + """ + # The longest number in the gutter plus two extra characters: `│ `. + gutter_margin = 2 + gutter_width = ( + len(str(self.document.line_count + 1)) + gutter_margin + if self.show_line_numbers + else 0 + ) + return gutter_width + + def _on_mount(self, _: events.Mount) -> None: + self.blink_timer = self.set_interval( + 0.5, + self._toggle_cursor_blink_visible, + pause=not (self.cursor_blink and self.has_focus), + ) + + def _on_blur(self, _: events.Blur) -> None: + self._pause_blink(visible=True) + + def _on_focus(self, _: events.Focus) -> None: + self._restart_blink() + self.app.cursor_position = self.cursor_screen_offset + + def _toggle_cursor_blink_visible(self) -> None: + """Toggle visibility of the cursor for the purposes of 'cursor blink'.""" + self._cursor_blink_visible = not self._cursor_blink_visible + cursor_row, _ = self.cursor_location + self.refresh_lines(cursor_row) + + def _restart_blink(self) -> None: + """Reset the cursor blink timer.""" + if self.cursor_blink: + self._cursor_blink_visible = True + self.blink_timer.reset() + + def _pause_blink(self, visible: bool = True) -> None: + """Pause the cursor blinking but ensure it stays visible.""" + self._cursor_blink_visible = visible + self.blink_timer.pause() + + async def _on_mouse_down(self, event: events.MouseDown) -> None: + """Update the cursor position, and begin a selection using the mouse.""" + target = self.get_target_document_location(event) + self.selection = Selection.cursor(target) + self._selecting = True + # Capture the mouse so that if the cursor moves outside the + # TextArea widget while selecting, the widget still scrolls. + self.capture_mouse() + self._pause_blink(visible=True) + + async def _on_mouse_move(self, event: events.MouseMove) -> None: + """Handles click and drag to expand and contract the selection.""" + if self._selecting: + target = self.get_target_document_location(event) + selection_start, _ = self.selection + self.selection = Selection(selection_start, target) + + async def _on_mouse_up(self, event: events.MouseUp) -> None: + """Finalise the selection that has been made using the mouse.""" + self._selecting = False + self.release_mouse() + self.record_cursor_width() + self._restart_blink() + + async def _on_paste(self, event: events.Paste) -> None: + """When a paste occurs, insert the text from the paste event into the document.""" + self.replace(event.text, *self.selection) + + def cell_width_to_column_index(self, cell_width: int, row_index: int) -> int: + """Return the column that the cell width corresponds to on the given row. + + Args: + cell_width: The cell width to convert. + row_index: The index of the row to examine. + + Returns: + The column corresponding to the cell width on that row. + """ + tab_width = self.indent_width + total_cell_offset = 0 + line = self.document[row_index] + for column_index, character in enumerate(line): + total_cell_offset += cell_len(expand_tabs_inline(character, tab_width)) + if total_cell_offset >= cell_width + 1: + return column_index + return len(line) + + def clamp_visitable(self, location: Location) -> Location: + """Clamp the given location to the nearest visitable location. + + Args: + location: The location to clamp. + + Returns: + The nearest location that we could conceivably navigate to using the cursor. + """ + document = self.document + + row, column = location + try: + line_text = document[row] + except IndexError: + line_text = "" + + row = clamp(row, 0, document.line_count - 1) + column = clamp(column, 0, len(line_text)) + + return row, column + + # --- Cursor/selection utilities + def scroll_cursor_visible( + self, center: bool = False, animate: bool = False + ) -> Offset: + """Scroll the `TextArea` such that the cursor is visible on screen. + + Args: + center: True if the cursor should be scrolled to the center. + animate: True if we should animate while scrolling. + + Returns: + The offset that was scrolled to bring the cursor into view. + """ + row, column = self.selection.end + text = self.document[row][:column] + column_offset = cell_len(expand_tabs_inline(text, self.indent_width)) + scroll_offset = self.scroll_to_region( + Region(x=column_offset, y=row, width=3, height=1), + spacing=Spacing(right=self.gutter_width), + animate=animate, + force=True, + center=center, + ) + return scroll_offset + + def move_cursor( + self, + location: Location, + select: bool = False, + center: bool = False, + record_width: bool = True, + ) -> None: + """Move the cursor to a location. + + Args: + location: The location to move the cursor to. + select: If True, select text between the old and new location. + center: If True, scroll such that the cursor is centered. + record_width: If True, record the cursor column cell width after navigating + so that we jump back to the same width the next time we move to a row + that is wide enough. + """ + if select: + start, end = self.selection + self.selection = Selection(start, location) + else: + self.selection = Selection.cursor(location) + + if record_width: + self.record_cursor_width() + + if center: + self.scroll_cursor_visible(center) + + def move_cursor_relative( + self, + rows: int = 0, + columns: int = 0, + select: bool = False, + center: bool = False, + record_width: bool = True, + ) -> None: + """Move the cursor relative to its current location. + + Args: + rows: The number of rows to move down by (negative to move up) + columns: The number of columns to move right by (negative to move left) + select: If True, select text between the old and new location. + center: If True, scroll such that the cursor is centered. + record_width: If True, record the cursor column cell width after navigating + so that we jump back to the same width the next time we move to a row + that is wide enough. + """ + clamp_visitable = self.clamp_visitable + start, end = self.selection + current_row, current_column = end + target = clamp_visitable((current_row + rows, current_column + columns)) + self.move_cursor(target, select, center, record_width) + + def select_line(self, index: int) -> None: + """Select all the text in the specified line. + + Args: + index: The index of the line to select (starting from 0). + """ + try: + line = self.document[index] + except IndexError: + return + else: + self.selection = Selection((index, 0), (index, len(line))) + self.record_cursor_width() + + def action_select_line(self) -> None: + """Select all the text on the current line.""" + cursor_row, _ = self.cursor_location + self.select_line(cursor_row) + + def select_all(self) -> None: + """Select all of the text in the `TextArea`.""" + last_line = self.document.line_count - 1 + length_of_last_line = len(self.document[last_line]) + selection_start = (0, 0) + selection_end = (last_line, length_of_last_line) + self.selection = Selection(selection_start, selection_end) + self.record_cursor_width() + + def action_select_all(self) -> None: + """Select all the text in the document.""" + self.select_all() + + @property + def cursor_location(self) -> Location: + """The current location of the cursor in the document. + + This is a utility for accessing the `end` of `TextArea.selection`. + """ + return self.selection.end + + @cursor_location.setter + def cursor_location(self, location: Location) -> None: + """Set the cursor_location to a new location. + + If a selection is in progress, the anchor point will remain. + """ + self.move_cursor(location, select=not self.selection.is_empty) + + @property + def cursor_screen_offset(self) -> Offset: + """The offset of the cursor relative to the screen.""" + cursor_row, cursor_column = self.cursor_location + scroll_x, scroll_y = self.scroll_offset + region_x, region_y, _width, _height = self.content_region + + offset_x = ( + region_x + + self.get_column_width(cursor_row, cursor_column) + - scroll_x + + self.gutter_width + ) + offset_y = region_y + cursor_row - scroll_y + + return Offset(offset_x, offset_y) + + @property + def cursor_at_first_line(self) -> bool: + """True if and only if the cursor is on the first line.""" + return self.selection.end[0] == 0 + + @property + def cursor_at_last_line(self) -> bool: + """True if and only if the cursor is on the last line.""" + return self.selection.end[0] == self.document.line_count - 1 + + @property + def cursor_at_start_of_line(self) -> bool: + """True if and only if the cursor is at column 0.""" + return self.selection.end[1] == 0 + + @property + def cursor_at_end_of_line(self) -> bool: + """True if and only if the cursor is at the end of a row.""" + cursor_row, cursor_column = self.selection.end + row_length = len(self.document[cursor_row]) + cursor_at_end = cursor_column == row_length + return cursor_at_end + + @property + def cursor_at_start_of_text(self) -> bool: + """True if and only if the cursor is at location (0, 0)""" + return self.selection.end == (0, 0) + + @property + def cursor_at_end_of_text(self) -> bool: + """True if and only if the cursor is at the very end of the document.""" + return self.cursor_at_last_line and self.cursor_at_end_of_line + + # ------ Cursor movement actions + def action_cursor_left(self, select: bool = False) -> None: + """Move the cursor one location to the left. + + If the cursor is at the left edge of the document, try to move it to + the end of the previous line. + + Args: + select: If True, select the text while moving. + """ + new_cursor_location = self.get_cursor_left_location() + self.move_cursor(new_cursor_location, select=select) + + def get_cursor_left_location(self) -> Location: + """Get the location the cursor will move to if it moves left. + + Returns: + The location of the cursor if it moves left. + """ + if self.cursor_at_start_of_text: + return 0, 0 + cursor_row, cursor_column = self.selection.end + length_of_row_above = len(self.document[cursor_row - 1]) + target_row = cursor_row if cursor_column != 0 else cursor_row - 1 + target_column = cursor_column - 1 if cursor_column != 0 else length_of_row_above + return target_row, target_column + + def action_cursor_right(self, select: bool = False) -> None: + """Move the cursor one location to the right. + + If the cursor is at the end of a line, attempt to go to the start of the next line. + + Args: + select: If True, select the text while moving. + """ + target = self.get_cursor_right_location() + self.move_cursor(target, select=select) + + def get_cursor_right_location(self) -> Location: + """Get the location the cursor will move to if it moves right. + + Returns: + the location the cursor will move to if it moves right. + """ + if self.cursor_at_end_of_text: + return self.selection.end + cursor_row, cursor_column = self.selection.end + target_row = cursor_row + 1 if self.cursor_at_end_of_line else cursor_row + target_column = 0 if self.cursor_at_end_of_line else cursor_column + 1 + return target_row, target_column + + def action_cursor_down(self, select: bool = False) -> None: + """Move the cursor down one cell. + + Args: + select: If True, select the text while moving. + """ + target = self.get_cursor_down_location() + self.move_cursor(target, record_width=False, select=select) + + def get_cursor_down_location(self) -> Location: + """Get the location the cursor will move to if it moves down. + + Returns: + The location the cursor will move to if it moves down. + """ + cursor_row, cursor_column = self.selection.end + if self.cursor_at_last_line: + return cursor_row, len(self.document[cursor_row]) + + target_row = min(self.document.line_count - 1, cursor_row + 1) + # Attempt to snap last intentional cell length + target_column = self.cell_width_to_column_index( + self._last_intentional_cell_width, target_row + ) + target_column = clamp(target_column, 0, len(self.document[target_row])) + return target_row, target_column + + def action_cursor_up(self, select: bool = False) -> None: + """Move the cursor up one cell. + + Args: + select: If True, select the text while moving. + """ + target = self.get_cursor_up_location() + self.move_cursor(target, record_width=False, select=select) + + def get_cursor_up_location(self) -> Location: + """Get the location the cursor will move to if it moves up. + + Returns: + The location the cursor will move to if it moves up. + """ + if self.cursor_at_first_line: + return 0, 0 + cursor_row, cursor_column = self.selection.end + target_row = max(0, cursor_row - 1) + # Attempt to snap last intentional cell length + target_column = self.cell_width_to_column_index( + self._last_intentional_cell_width, target_row + ) + target_column = clamp(target_column, 0, len(self.document[target_row])) + return target_row, target_column + + def action_cursor_line_end(self, select: bool = False) -> None: + """Move the cursor to the end of the line.""" + location = self.get_cursor_line_end_location() + self.move_cursor(location, select=select) + + def get_cursor_line_end_location(self) -> Location: + """Get the location of the end of the current line. + + Returns: + The (row, column) location of the end of the cursors current line. + """ + start, end = self.selection + cursor_row, cursor_column = end + target_column = len(self.document[cursor_row]) + return cursor_row, target_column + + def action_cursor_line_start(self, select: bool = False) -> None: + """Move the cursor to the start of the line.""" + + cursor_row, cursor_column = self.cursor_location + line = self.document[cursor_row] + + first_non_whitespace = 0 + for index, code_point in enumerate(line): + if not code_point.isspace(): + first_non_whitespace = index + break + + if cursor_column <= first_non_whitespace and cursor_column != 0: + target = self.get_cursor_line_start_location() + self.move_cursor(target, select=select) + else: + target = cursor_row, first_non_whitespace + self.move_cursor(target, select=select) + + def get_cursor_line_start_location(self) -> Location: + """Get the location of the start of the current line. + + Returns: + The (row, column) location of the start of the cursors current line. + """ + _start, end = self.selection + cursor_row, _cursor_column = end + return cursor_row, 0 + + def action_cursor_word_left(self, select: bool = False) -> None: + """Move the cursor left by a single word, skipping trailing whitespace. + + Args: + select: Whether to select while moving the cursor. + """ + if self.cursor_at_start_of_text: + return + target = self.get_cursor_word_left_location() + self.move_cursor(target, select=select) + + def get_cursor_word_left_location(self) -> Location: + """Get the location the cursor will jump to if it goes 1 word left. + + Returns: + The location the cursor will jump on "jump word left". + """ + cursor_row, cursor_column = self.cursor_location + if cursor_row > 0 and cursor_column == 0: + # Going to the previous row + return cursor_row - 1, len(self.document[cursor_row - 1]) + + # Staying on the same row + line = self.document[cursor_row][:cursor_column] + search_string = line.rstrip() + matches = list(re.finditer(self._word_pattern, search_string)) + cursor_column = matches[-1].start() if matches else 0 + return cursor_row, cursor_column + + def action_cursor_word_right(self, select: bool = False) -> None: + """Move the cursor right by a single word, skipping leading whitespace.""" + + if self.cursor_at_end_of_text: + return + + target = self.get_cursor_word_right_location() + self.move_cursor(target, select=select) + + def get_cursor_word_right_location(self) -> Location: + """Get the location the cursor will jump to if it goes 1 word right. + + Returns: + The location the cursor will jump on "jump word right". + """ + cursor_row, cursor_column = self.selection.end + line = self.document[cursor_row] + if cursor_row < self.document.line_count - 1 and cursor_column == len(line): + # Moving to the line below + return cursor_row + 1, 0 + + # Staying on the same line + search_string = line[cursor_column:] + pre_strip_length = len(search_string) + search_string = search_string.lstrip() + strip_offset = pre_strip_length - len(search_string) + + matches = list(re.finditer(self._word_pattern, search_string)) + if matches: + cursor_column += matches[0].start() + strip_offset + else: + cursor_column = len(line) + + return cursor_row, cursor_column + + def action_cursor_page_up(self) -> None: + """Move the cursor and scroll up one page.""" + height = self.content_size.height + _, cursor_location = self.selection + row, column = cursor_location + target = (row - height, column) + self.scroll_relative(y=-height, animate=False) + self.move_cursor(target) + + def action_cursor_page_down(self) -> None: + """Move the cursor and scroll down one page.""" + height = self.content_size.height + _, cursor_location = self.selection + row, column = cursor_location + target = (row + height, column) + self.scroll_relative(y=height, animate=False) + self.move_cursor(target) + + def get_column_width(self, row: int, column: int) -> int: + """Get the cell offset of the column from the start of the row. + + Args: + row: The row index. + column: The column index (codepoint offset from start of row). + + Returns: + The cell width of the column relative to the start of the row. + """ + line = self.document[row] + return cell_len(expand_tabs_inline(line[:column], self.indent_width)) + + def record_cursor_width(self) -> None: + """Record the current cell width of the cursor. + + This is used where we navigate up and down through rows. + If we're in the middle of a row, and go down to a row with no + content, then we go down to another row, we want our cursor to + jump back to the same offset that we were originally at. + """ + row, column = self.selection.end + column_cell_length = self.get_column_width(row, column) + self._last_intentional_cell_width = column_cell_length + + # --- Editor operations + def insert( + self, + text: str, + location: Location | None = None, + *, + maintain_selection_offset: bool = True, + ) -> EditResult: + """Insert text into the document. + + Args: + text: The text to insert. + location: The location to insert text, or None to use the cursor location. + maintain_selection_offset: If True, the active Selection will be updated + such that the same text is selected before and after the selection, + if possible. Otherwise, the cursor will jump to the end point of the + edit. + + Returns: + An `EditResult` containing information about the edit. + """ + if location is None: + location = self.cursor_location + return self.edit(Edit(text, location, location, maintain_selection_offset)) + + def delete( + self, + start: Location, + end: Location, + *, + maintain_selection_offset: bool = True, + ) -> EditResult: + """Delete the text between two locations in the document. + + Args: + start: The start location. + end: The end location. + maintain_selection_offset: If True, the active Selection will be updated + such that the same text is selected before and after the selection, + if possible. Otherwise, the cursor will jump to the end point of the + edit. + + Returns: + An `EditResult` containing information about the edit. + """ + top, bottom = sorted((start, end)) + return self.edit(Edit("", top, bottom, maintain_selection_offset)) + + def replace( + self, + insert: str, + start: Location, + end: Location, + *, + maintain_selection_offset: bool = True, + ) -> EditResult: + """Replace text in the document with new text. + + Args: + insert: The text to insert. + start: The start location + end: The end location. + maintain_selection_offset: If True, the active Selection will be updated + such that the same text is selected before and after the selection, + if possible. Otherwise, the cursor will jump to the end point of the + edit. + + Returns: + An `EditResult` containing information about the edit. + """ + return self.edit(Edit(insert, start, end, maintain_selection_offset)) + + def clear(self) -> None: + """Delete all text from the document.""" + document = self.document + last_line = document[-1] + document_end = (document.line_count, len(last_line)) + self.delete((0, 0), document_end, maintain_selection_offset=False) + + def action_delete_left(self) -> None: + """Deletes the character to the left of the cursor and updates the cursor location. + + If there's a selection, then the selected range is deleted.""" + + selection = self.selection + start, end = selection + + if selection.is_empty: + end = self.get_cursor_left_location() + + self.delete(start, end, maintain_selection_offset=False) + + def action_delete_right(self) -> None: + """Deletes the character to the right of the cursor and keeps the cursor at the same location. + + If there's a selection, then the selected range is deleted.""" + + selection = self.selection + start, end = selection + + if selection.is_empty: + end = self.get_cursor_right_location() + + self.delete(start, end, maintain_selection_offset=False) + + def action_delete_line(self) -> None: + """Deletes the lines which intersect with the selection.""" + start, end = self.selection + start, end = sorted((start, end)) + start_row, start_column = start + end_row, end_column = end + + # Generally editors will only delete line the end line of the + # selection if the cursor is not at column 0 of that line. + if start_row != end_row and end_column == 0 and end_row >= 0: + end_row -= 1 + + from_location = (start_row, 0) + to_location = (end_row + 1, 0) + + self.delete(from_location, to_location, maintain_selection_offset=False) + + def action_delete_to_start_of_line(self) -> None: + """Deletes from the cursor location to the start of the line.""" + from_location = self.selection.end + cursor_row, cursor_column = from_location + to_location = (cursor_row, 0) + self.delete(from_location, to_location, maintain_selection_offset=False) + + def action_delete_to_end_of_line(self) -> None: + """Deletes from the cursor location to the end of the line.""" + from_location = self.selection.end + cursor_row, cursor_column = from_location + to_location = (cursor_row, len(self.document[cursor_row])) + self.delete(from_location, to_location, maintain_selection_offset=False) + + def action_delete_word_left(self) -> None: + """Deletes the word to the left of the cursor and updates the cursor location.""" + if self.cursor_at_start_of_text: + return + + # If there's a non-zero selection, then "delete word left" typically only + # deletes the characters within the selection range, ignoring word boundaries. + start, end = self.selection + if start != end: + self.delete(start, end, maintain_selection_offset=False) + return + + to_location = self.get_cursor_word_left_location() + self.delete(self.selection.end, to_location, maintain_selection_offset=False) + + def action_delete_word_right(self) -> None: + """Deletes the word to the right of the cursor and keeps the cursor at the same location. + + Note that the location that we delete to using this action is not the same + as the location we move to when we move the cursor one word to the right. + This action does not skip leading whitespace, whereas cursor movement does. + """ + if self.cursor_at_end_of_text: + return + + start, end = self.selection + if start != end: + self.delete(start, end, maintain_selection_offset=False) + return + + cursor_row, cursor_column = end + + # Check the current line for a word boundary + line = self.document[cursor_row][cursor_column:] + matches = list(re.finditer(self._word_pattern, line)) + + current_row_length = len(self.document[cursor_row]) + if matches: + to_location = (cursor_row, cursor_column + matches[0].end()) + elif ( + cursor_row < self.document.line_count - 1 + and cursor_column == current_row_length + ): + to_location = (cursor_row + 1, 0) + else: + to_location = (cursor_row, current_row_length) + + self.delete(end, to_location, maintain_selection_offset=False) + + +@dataclass +class Edit: + """Implements the Undoable protocol to replace text at some range within a document.""" + + text: str + """The text to insert. An empty string is equivalent to deletion.""" + from_location: Location + """The start location of the insert.""" + to_location: Location + """The end location of the insert""" + maintain_selection_offset: bool + """If True, the selection will maintain its offset to the replacement range.""" + _updated_selection: Selection | None = field(init=False, default=None) + """Where the selection should move to after the replace happens.""" + + def do(self, text_area: TextArea) -> EditResult: + """Perform the edit operation. + + Args: + text_area: The `TextArea` to perform the edit on. + + Returns: + An `EditResult` containing information about the replace operation. + """ + text = self.text + + edit_from = self.from_location + edit_to = self.to_location + + # This code is mostly handling how we adjust TextArea.selection + # when an edit is made to the document programmatically. + # We want a user who is typing away to maintain their relative + # position in the document even if an insert happens before + # their cursor position. + + edit_top, edit_bottom = sorted((edit_from, edit_to)) + edit_bottom_row, edit_bottom_column = edit_bottom + + selection_start, selection_end = text_area.selection + selection_start_row, selection_start_column = selection_start + selection_end_row, selection_end_column = selection_end + + replace_result = text_area.document.replace_range(edit_from, edit_to, text) + + new_edit_to_row, new_edit_to_column = replace_result.end_location + + # TODO: We could maybe improve the situation where the selection + # and the edit range overlap with each other. + column_offset = new_edit_to_column - edit_bottom_column + target_selection_start_column = ( + selection_start_column + column_offset + if edit_bottom_row == selection_start_row + and edit_bottom_column <= selection_start_column + else selection_start_column + ) + target_selection_end_column = ( + selection_end_column + column_offset + if edit_bottom_row == selection_end_row + and edit_bottom_column <= selection_end_column + else selection_end_column + ) + + row_offset = new_edit_to_row - edit_bottom_row + target_selection_start_row = selection_start_row + row_offset + target_selection_end_row = selection_end_row + row_offset + + if self.maintain_selection_offset: + self._updated_selection = Selection( + start=(target_selection_start_row, target_selection_start_column), + end=(target_selection_end_row, target_selection_end_column), + ) + else: + self._updated_selection = Selection.cursor(replace_result.end_location) + + return replace_result + + def undo(self, text_area: TextArea) -> EditResult: + """Undo the edit operation. + + Args: + text_area: The `TextArea` to undo the insert operation on. + + Returns: + An `EditResult` containing information about the replace operation. + """ + raise NotImplementedError() + + def after(self, text_area: TextArea) -> None: + """Possibly update the cursor location after the widget has been refreshed. + + Args: + text_area: The `TextArea` this operation was performed on. + """ + if self._updated_selection is not None: + text_area.selection = self._updated_selection + text_area.record_cursor_width() + + +@runtime_checkable +class Undoable(Protocol): + """Protocol for actions performed in the text editor which can be done and undone. + + These are typically actions which affect the document (e.g. inserting and deleting + text), but they can really be anything. + + To perform an edit operation, pass the Edit to `TextArea.edit()`""" + + def do(self, text_area: TextArea) -> Any: + """Do the action. + + Args: + The `TextArea` to perform the action on. + + Returns: + Anything. This protocol doesn't prescribe what is returned. + """ + + def undo(self, text_area: TextArea) -> Any: + """Undo the action. + + Args: + The `TextArea` to perform the action on. + + Returns: + Anything. This protocol doesn't prescribe what is returned. + """ + + +@lru_cache(maxsize=128) +def build_byte_to_codepoint_dict(data: bytes) -> dict[int, int]: + """Build a mapping of utf-8 byte offsets to codepoint offsets for the given data. + + Args: + data: utf-8 bytes. + + Returns: + A `dict[int, int]` mapping byte indices to codepoint indices within `data`. + """ + byte_to_codepoint = {} + current_byte_offset = 0 + code_point_offset = 0 + + while current_byte_offset < len(data): + byte_to_codepoint[current_byte_offset] = code_point_offset + first_byte = data[current_byte_offset] + + # Single-byte character + if (first_byte & 0b10000000) == 0: + current_byte_offset += 1 + # 2-byte character + elif (first_byte & 0b11100000) == 0b11000000: + current_byte_offset += 2 + # 3-byte character + elif (first_byte & 0b11110000) == 0b11100000: + current_byte_offset += 3 + # 4-byte character + elif (first_byte & 0b11111000) == 0b11110000: + current_byte_offset += 4 + else: + raise ValueError(f"Invalid UTF-8 byte: {first_byte}") + + code_point_offset += 1 + + # Mapping for the end of the string + byte_to_codepoint[current_byte_offset] = code_point_offset + return byte_to_codepoint diff --git a/src/textual/widgets/_toast.py b/src/textual/widgets/_toast.py index a09594189b..dff62b5a5d 100644 --- a/src/textual/widgets/_toast.py +++ b/src/textual/widgets/_toast.py @@ -128,7 +128,7 @@ def _expire(self) -> None: # the notification that caused us to exist. Note that we tell the # app to not bother refreshing the display on our account, we're # about to handle that anyway. - self.app.unnotify(self._notification, refresh=False) + self.app._unnotify(self._notification, refresh=False) # Note that we attempt to remove our parent, because we're wrapped # inside an alignment container. The testing that we are is as much # to keep type checkers happy as anything else. diff --git a/src/textual/widgets/_toggle_button.py b/src/textual/widgets/_toggle_button.py index cb9b959012..90828c7ac1 100644 --- a/src/textual/widgets/_toggle_button.py +++ b/src/textual/widgets/_toggle_button.py @@ -94,16 +94,16 @@ class ToggleButton(Static, can_focus=True): /* Light mode overrides. */ - App.-light-mode ToggleButton > .toggle--button { + ToggleButton:light > .toggle--button { color: $background; background: $foreground 10%; } - App.-light-mode ToggleButton:focus > .toggle--button { + ToggleButton:light:focus > .toggle--button { background: $foreground 25%; } - App.-light-mode ToggleButton.-on > .toggle--button { + ToggleButton:light.-on > .toggle--button { color: $primary; } """ # TODO: https://github.com/Textualize/textual/issues/1780 @@ -232,7 +232,7 @@ async def _on_click(self, _: Click) -> None: """Toggle the value of the widget when clicked with the mouse.""" self.toggle() - class Changed(Message, bubble=True): + class Changed(Message): """Posted when the value of the toggle button changes.""" def __init__(self, toggle_button: ToggleButton, value: bool) -> None: diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 0d7b495eda..c413d79109 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -508,7 +508,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): ), } - class NodeCollapsed(Generic[EventTreeDataType], Message, bubble=True): + class NodeCollapsed(Generic[EventTreeDataType], Message): """Event sent when a node is collapsed. Can be handled using `on_tree_node_collapsed` in a subclass of `Tree` or in a @@ -525,7 +525,7 @@ def control(self) -> Tree[EventTreeDataType]: """The tree that sent the message.""" return self.node.tree - class NodeExpanded(Generic[EventTreeDataType], Message, bubble=True): + class NodeExpanded(Generic[EventTreeDataType], Message): """Event sent when a node is expanded. Can be handled using `on_tree_node_expanded` in a subclass of `Tree` or in a @@ -542,7 +542,7 @@ def control(self) -> Tree[EventTreeDataType]: """The tree that sent the message.""" return self.node.tree - class NodeHighlighted(Generic[EventTreeDataType], Message, bubble=True): + class NodeHighlighted(Generic[EventTreeDataType], Message): """Event sent when a node is highlighted. Can be handled using `on_tree_node_highlighted` in a subclass of `Tree` or in a @@ -559,7 +559,7 @@ def control(self) -> Tree[EventTreeDataType]: """The tree that sent the message.""" return self.node.tree - class NodeSelected(Generic[EventTreeDataType], Message, bubble=True): + class NodeSelected(Generic[EventTreeDataType], Message): """Event sent when a node is selected. Can be handled using `on_tree_node_selected` in a subclass of `Tree` or in a @@ -597,8 +597,6 @@ def __init__( disabled: Whether the tree is disabled or not. """ - super().__init__(name=name, id=id, classes=classes, disabled=disabled) - text_label = self.process_label(label) self._updates = 0 @@ -610,6 +608,8 @@ def __init__( self._tree_lines_cached: list[_TreeLine] | None = None self._cursor_node: TreeNode[TreeDataType] | None = None + super().__init__(name=name, id=id, classes=classes, disabled=disabled) + @property def cursor_node(self) -> TreeNode[TreeDataType] | None: """The currently selected node, or ``None`` if no selection.""" @@ -888,25 +888,29 @@ def watch_show_root(self, show_root: bool) -> None: self.cursor_line = -1 self._invalidate() - def scroll_to_line(self, line: int) -> None: + def scroll_to_line(self, line: int, animate: bool = True) -> None: """Scroll to the given line. Args: line: A line number. + animate: Enable animation. """ region = self._get_label_region(line) if region is not None: - self.scroll_to_region(region) + self.scroll_to_region(region, animate=animate) - def scroll_to_node(self, node: TreeNode[TreeDataType]) -> None: + def scroll_to_node( + self, node: TreeNode[TreeDataType], animate: bool = True + ) -> None: """Scroll to the given node. Args: node: Node to scroll in to view. + animate: Animate scrolling. """ line = node._line if line != -1: - self.scroll_to_line(line) + self.scroll_to_line(line, animate=animate) def refresh_line(self, line: int) -> None: """Refresh (repaint) a given line in the tree. @@ -1156,7 +1160,7 @@ def action_cursor_up(self) -> None: self.cursor_line = self.last_line else: self.cursor_line -= 1 - self.scroll_to_line(self.cursor_line) + self.scroll_to_line(self.cursor_line, animate=False) def action_cursor_down(self) -> None: """Move the cursor down one node.""" @@ -1164,7 +1168,7 @@ def action_cursor_down(self) -> None: self.cursor_line = 0 else: self.cursor_line += 1 - self.scroll_to_line(self.cursor_line) + self.scroll_to_line(self.cursor_line, animate=False) def action_page_down(self) -> None: """Move the cursor down a page's-worth of nodes.""" diff --git a/src/textual/widgets/data_table.py b/src/textual/widgets/data_table.py index 0bb18f87fa..a5eea13542 100644 --- a/src/textual/widgets/data_table.py +++ b/src/textual/widgets/data_table.py @@ -1,5 +1,3 @@ -"""Make non-widget DataTable support classes available.""" - from ._data_table import ( CellDoesNotExist, CellKey, diff --git a/src/textual/widgets/rule.py b/src/textual/widgets/rule.py new file mode 100644 index 0000000000..a9ab5d23e9 --- /dev/null +++ b/src/textual/widgets/rule.py @@ -0,0 +1,8 @@ +from ._rule import InvalidLineStyle, InvalidRuleOrientation, LineStyle, RuleOrientation + +__all__ = [ + "InvalidLineStyle", + "InvalidRuleOrientation", + "LineStyle", + "RuleOrientation", +] diff --git a/src/textual/widgets/text_area.py b/src/textual/widgets/text_area.py new file mode 100644 index 0000000000..82a69e38b3 --- /dev/null +++ b/src/textual/widgets/text_area.py @@ -0,0 +1,37 @@ +from textual._text_area_theme import TextAreaTheme +from textual.document._document import ( + Document, + DocumentBase, + EditResult, + Location, + Selection, +) +from textual.document._languages import BUILTIN_LANGUAGES +from textual.document._syntax_aware_document import SyntaxAwareDocument +from textual.widgets._text_area import ( + Edit, + EndColumn, + Highlight, + HighlightName, + LanguageDoesNotExist, + StartColumn, + ThemeDoesNotExist, +) + +__all__ = [ + "BUILTIN_LANGUAGES", + "Document", + "DocumentBase", + "Edit", + "EditResult", + "EndColumn", + "Highlight", + "HighlightName", + "LanguageDoesNotExist", + "Location", + "Selection", + "StartColumn", + "SyntaxAwareDocument", + "TextAreaTheme", + "ThemeDoesNotExist", +] diff --git a/tests/command_palette/test_click_away.py b/tests/command_palette/test_click_away.py new file mode 100644 index 0000000000..383f39cdb2 --- /dev/null +++ b/tests/command_palette/test_click_away.py @@ -0,0 +1,25 @@ +from textual.app import App +from textual.command import CommandPalette, Hit, Hits, Provider + + +class SimpleSource(Provider): + async def search(self, query: str) -> Hits: + def goes_nowhere_does_nothing() -> None: + pass + + yield Hit(1, query, goes_nowhere_does_nothing, query) + + +class CommandPaletteApp(App[None]): + COMMANDS = {SimpleSource} + + def on_mount(self) -> None: + self.action_command_palette() + + +async def test_clicking_outside_command_palette_closes_it() -> None: + """Clicking 'outside' the command palette should make it go away.""" + async with CommandPaletteApp().run_test() as pilot: + assert len(pilot.app.query(CommandPalette)) == 1 + await pilot.click() + assert len(pilot.app.query(CommandPalette)) == 0 diff --git a/tests/command_palette/test_command_source_environment.py b/tests/command_palette/test_command_source_environment.py new file mode 100644 index 0000000000..af9b691d70 --- /dev/null +++ b/tests/command_palette/test_command_source_environment.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from textual.app import App, ComposeResult +from textual.command import CommandPalette, Hit, Hits, Provider +from textual.screen import Screen +from textual.widget import Widget +from textual.widgets import Input + + +class SimpleSource(Provider): + environment: set[tuple[App, Screen, Widget | None]] = set() + + async def search(self, _: str) -> Hits: + def goes_nowhere_does_nothing() -> None: + pass + + SimpleSource.environment.add((self.app, self.screen, self.focused)) + yield Hit(1, "Hit", goes_nowhere_does_nothing, "Hit") + + +class CommandPaletteApp(App[None]): + COMMANDS = {SimpleSource} + + def compose(self) -> ComposeResult: + yield Input() + + def on_mount(self) -> None: + self.action_command_palette() + + +async def test_command_source_environment() -> None: + """The command source should see the app and default screen.""" + async with CommandPaletteApp().run_test() as pilot: + base_screen = pilot.app.query_one(CommandPalette)._calling_screen + assert base_screen is not None + await pilot.press(*"test") + assert len(SimpleSource.environment) == 1 + assert SimpleSource.environment == { + (pilot.app, base_screen, base_screen.query_one(Input)) + } diff --git a/tests/command_palette/test_declare_sources.py b/tests/command_palette/test_declare_sources.py new file mode 100644 index 0000000000..c5bae17904 --- /dev/null +++ b/tests/command_palette/test_declare_sources.py @@ -0,0 +1,97 @@ +from textual.app import App +from textual.command import CommandPalette, Hit, Hits, Provider +from textual.screen import Screen + + +async def test_sources_with_no_known_screen() -> None: + """A command palette with no known screen should have an empty source set.""" + assert CommandPalette()._provider_classes == set() + + +class ExampleCommandSource(Provider): + async def search(self, _: str) -> Hits: + def goes_nowhere_does_nothing() -> None: + pass + + yield Hit(1, "Hit", goes_nowhere_does_nothing, "Hit") + + +class AppWithActiveCommandPalette(App[None]): + def on_mount(self) -> None: + self.action_command_palette() + + +class AppWithNoSources(AppWithActiveCommandPalette): + pass + + +async def test_no_app_command_sources() -> None: + """An app with no sources declared should work fine.""" + async with AppWithNoSources().run_test() as pilot: + assert pilot.app.query_one(CommandPalette)._provider_classes == App.COMMANDS + + +class AppWithSources(AppWithActiveCommandPalette): + COMMANDS = {ExampleCommandSource} + + +async def test_app_command_sources() -> None: + """Command sources declared on an app should be in the command palette.""" + async with AppWithSources().run_test() as pilot: + assert ( + pilot.app.query_one(CommandPalette)._provider_classes + == AppWithSources.COMMANDS + ) + + +class AppWithInitialScreen(App[None]): + def __init__(self, screen: Screen) -> None: + super().__init__() + self._test_screen = screen + + def on_mount(self) -> None: + self.push_screen(self._test_screen) + + +class ScreenWithNoSources(Screen[None]): + def on_mount(self) -> None: + self.app.action_command_palette() + + +async def test_no_screen_command_sources() -> None: + """An app with a screen with no sources declared should work fine.""" + async with AppWithInitialScreen(ScreenWithNoSources()).run_test() as pilot: + assert pilot.app.query_one(CommandPalette)._provider_classes == App.COMMANDS + + +class ScreenWithSources(ScreenWithNoSources): + COMMANDS = {ExampleCommandSource} + + +async def test_screen_command_sources() -> None: + """Command sources declared on a screen should be in the command palette.""" + async with AppWithInitialScreen(ScreenWithSources()).run_test() as pilot: + assert ( + pilot.app.query_one(CommandPalette)._provider_classes + == App.COMMANDS | ScreenWithSources.COMMANDS + ) + + +class AnotherCommandSource(ExampleCommandSource): + pass + + +class CombinedSourceApp(App[None]): + COMMANDS = {AnotherCommandSource} + + def on_mount(self) -> None: + self.push_screen(ScreenWithSources()) + + +async def test_app_and_screen_command_sources_combine() -> None: + """If an app and the screen have command sources they should combine.""" + async with CombinedSourceApp().run_test() as pilot: + assert ( + pilot.app.query_one(CommandPalette)._provider_classes + == CombinedSourceApp.COMMANDS | ScreenWithSources.COMMANDS + ) diff --git a/tests/command_palette/test_escaping.py b/tests/command_palette/test_escaping.py new file mode 100644 index 0000000000..2ac2013b6c --- /dev/null +++ b/tests/command_palette/test_escaping.py @@ -0,0 +1,50 @@ +from textual.app import App +from textual.command import CommandPalette, Hit, Hits, Provider + + +class SimpleSource(Provider): + async def search(self, query: str) -> Hits: + def goes_nowhere_does_nothing() -> None: + pass + + yield Hit(1, query, goes_nowhere_does_nothing, query) + + +class CommandPaletteApp(App[None]): + COMMANDS = {SimpleSource} + + def on_mount(self) -> None: + self.action_command_palette() + + +async def test_escape_closes_when_no_list_visible() -> None: + """Pressing escape when no list is visible should close the command palette.""" + async with CommandPaletteApp().run_test() as pilot: + assert len(pilot.app.query(CommandPalette)) == 1 + await pilot.press("escape") + assert len(pilot.app.query(CommandPalette)) == 0 + + +async def test_escape_does_not_close_when_list_visible() -> None: + """Pressing escape when a hit list is visible should not close the command palette.""" + async with CommandPaletteApp().run_test() as pilot: + assert len(pilot.app.query(CommandPalette)) == 1 + await pilot.press("a") + await pilot.press("escape") + assert len(pilot.app.query(CommandPalette)) == 1 + await pilot.press("escape") + assert len(pilot.app.query(CommandPalette)) == 0 + + +async def test_down_arrow_should_undo_closing_of_list_via_escape() -> None: + """Down arrow should reopen the hit list if escape closed it before.""" + async with CommandPaletteApp().run_test() as pilot: + assert len(pilot.app.query(CommandPalette)) == 1 + await pilot.press("a") + await pilot.press("escape") + assert len(pilot.app.query(CommandPalette)) == 1 + await pilot.press("down") + await pilot.press("escape") + assert len(pilot.app.query(CommandPalette)) == 1 + await pilot.press("escape") + assert len(pilot.app.query(CommandPalette)) == 0 diff --git a/tests/command_palette/test_interaction.py b/tests/command_palette/test_interaction.py new file mode 100644 index 0000000000..d243a35565 --- /dev/null +++ b/tests/command_palette/test_interaction.py @@ -0,0 +1,66 @@ +from textual.app import App +from textual.command import CommandList, CommandPalette, Hit, Hits, Provider + + +class SimpleSource(Provider): + async def search(self, query: str) -> Hits: + def goes_nowhere_does_nothing() -> None: + pass + + for _ in range(100): + yield Hit(1, query, goes_nowhere_does_nothing, query) + + +class CommandPaletteApp(App[None]): + COMMANDS = {SimpleSource} + + def on_mount(self) -> None: + self.action_command_palette() + + +async def test_initial_list_no_highlight() -> None: + """When the list initially appears, nothing will be highlighted.""" + async with CommandPaletteApp().run_test() as pilot: + assert len(pilot.app.query(CommandPalette)) == 1 + assert pilot.app.query_one(CommandList).visible is False + await pilot.press("a") + assert pilot.app.query_one(CommandList).visible is True + assert pilot.app.query_one(CommandList).highlighted is None + + +async def test_down_arrow_selects_an_item() -> None: + """Typing in a search value then pressing down should select a command.""" + async with CommandPaletteApp().run_test() as pilot: + assert len(pilot.app.query(CommandPalette)) == 1 + assert pilot.app.query_one(CommandList).visible is False + await pilot.press("a") + assert pilot.app.query_one(CommandList).visible is True + assert pilot.app.query_one(CommandList).highlighted is None + await pilot.press("down") + assert pilot.app.query_one(CommandList).highlighted is not None + + +async def test_enter_selects_an_item() -> None: + """Typing in a search value then pressing enter should select a command.""" + async with CommandPaletteApp().run_test() as pilot: + assert len(pilot.app.query(CommandPalette)) == 1 + assert pilot.app.query_one(CommandList).visible is False + await pilot.press("a") + assert pilot.app.query_one(CommandList).visible is True + assert pilot.app.query_one(CommandList).highlighted is None + await pilot.press("enter") + assert pilot.app.query_one(CommandList).highlighted is not None + + +async def test_selection_of_command_closes_command_palette() -> None: + """Selecting a command from the list should close the list.""" + async with CommandPaletteApp().run_test() as pilot: + assert len(pilot.app.query(CommandPalette)) == 1 + assert pilot.app.query_one(CommandList).visible is False + await pilot.press("a") + assert pilot.app.query_one(CommandList).visible is True + assert pilot.app.query_one(CommandList).highlighted is None + await pilot.press("enter") + assert pilot.app.query_one(CommandList).highlighted is not None + await pilot.press("enter") + assert len(pilot.app.query(CommandPalette)) == 0 diff --git a/tests/command_palette/test_no_results.py b/tests/command_palette/test_no_results.py new file mode 100644 index 0000000000..400e69c28c --- /dev/null +++ b/tests/command_palette/test_no_results.py @@ -0,0 +1,25 @@ +from textual.app import App +from textual.command import CommandPalette +from textual.widgets import OptionList + + +class CommandPaletteApp(App[None]): + COMMANDS = set() + + def on_mount(self) -> None: + self.action_command_palette() + + +async def test_no_results() -> None: + """Receiving no results from a search for a command should not be a problem.""" + async with CommandPaletteApp().run_test() as pilot: + assert len(pilot.app.query(CommandPalette)) == 1 + results = pilot.app.screen.query_one(OptionList) + assert results.visible is False + assert results.option_count == 0 + await pilot.press("a") + await pilot.pause(delay=CommandPalette._NO_MATCHES_COUNTDOWN) + assert results.visible is True + assert results.option_count == 1 + assert "No matches found" in str(results.get_option_at_index(0).prompt) + assert results.get_option_at_index(0).disabled is True diff --git a/tests/command_palette/test_run_on_select.py b/tests/command_palette/test_run_on_select.py new file mode 100644 index 0000000000..a652096b56 --- /dev/null +++ b/tests/command_palette/test_run_on_select.py @@ -0,0 +1,67 @@ +from functools import partial + +from textual.app import App +from textual.command import CommandPalette, Hit, Hits, Provider +from textual.widgets import Input + + +class SimpleSource(Provider): + async def search(self, _: str) -> Hits: + def goes_nowhere_does_nothing(selection: int) -> None: + assert isinstance(self.app, CommandPaletteRunOnSelectApp) + self.app.selection = selection + + for n in range(100): + yield Hit( + n + 1 / 100, + str(n), + partial(goes_nowhere_does_nothing, n), + str(n), + f"This is help for {n}", + ) + + +class CommandPaletteRunOnSelectApp(App[None]): + COMMANDS = {SimpleSource} + + def __init__(self) -> None: + super().__init__() + self.selection: int | None = None + + +async def test_with_run_on_select_on() -> None: + """With run on select on, the callable should be instantly run.""" + async with CommandPaletteRunOnSelectApp().run_test() as pilot: + save = CommandPalette.run_on_select + CommandPalette.run_on_select = True + assert isinstance(pilot.app, CommandPaletteRunOnSelectApp) + pilot.app.action_command_palette() + await pilot.press("0") + await pilot.app.query_one(CommandPalette).workers.wait_for_complete() + await pilot.press("down") + await pilot.press("enter") + assert pilot.app.selection is not None + CommandPalette.run_on_select = save + + +class CommandPaletteDoNotRunOnSelectApp(CommandPaletteRunOnSelectApp): + def __init__(self) -> None: + super().__init__() + + +async def test_with_run_on_select_off() -> None: + """With run on select off, the callable should not be instantly run.""" + async with CommandPaletteDoNotRunOnSelectApp().run_test() as pilot: + save = CommandPalette.run_on_select + CommandPalette.run_on_select = False + assert isinstance(pilot.app, CommandPaletteDoNotRunOnSelectApp) + pilot.app.action_command_palette() + await pilot.press("0") + await pilot.app.query_one(CommandPalette).workers.wait_for_complete() + await pilot.press("down") + await pilot.press("enter") + assert pilot.app.selection is None + assert pilot.app.query_one(Input).value != "" + await pilot.press("enter") + assert pilot.app.selection is not None + CommandPalette.run_on_select = save diff --git a/tests/css/test_mega_stylesheet.py b/tests/css/test_mega_stylesheet.py index 6cfaf2868b..ae1e187bd2 100644 --- a/tests/css/test_mega_stylesheet.py +++ b/tests/css/test_mega_stylesheet.py @@ -6,6 +6,6 @@ def test_mega_stylesheet() -> None: """It should be possible to load a known-good stylesheet.""" mega_stylesheet = Stylesheet() - mega_stylesheet.read(Path(__file__).parent / "test_mega_stylesheet.css") + mega_stylesheet.read(Path(__file__).parent / "test_mega_stylesheet.tcss") mega_stylesheet.parse() assert ".---we-made-it-to-the-end---" in mega_stylesheet.css diff --git a/tests/css/test_mega_stylesheet.css b/tests/css/test_mega_stylesheet.tcss similarity index 100% rename from tests/css/test_mega_stylesheet.css rename to tests/css/test_mega_stylesheet.tcss diff --git a/tests/css/test_screen_css.py b/tests/css/test_screen_css.py index 1ce54e515d..54138fb8a5 100644 --- a/tests/css/test_screen_css.py +++ b/tests/css/test_screen_css.py @@ -16,13 +16,14 @@ def compose(self): class ScreenWithCSS(Screen): + SCOPED_CSS = False CSS = """ #screen-css { background: #ff0000; } """ - CSS_PATH = "test_screen_css.css" + CSS_PATH = "test_screen_css.tcss" def compose(self): yield Label("Hello, world!", id="app-css") diff --git a/tests/css/test_screen_css.css b/tests/css/test_screen_css.tcss similarity index 100% rename from tests/css/test_screen_css.css rename to tests/css/test_screen_css.tcss diff --git a/tests/css/test_styles.py b/tests/css/test_styles.py index e21572478a..b31ad46548 100644 --- a/tests/css/test_styles.py +++ b/tests/css/test_styles.py @@ -148,6 +148,13 @@ def test_opacity_set_invalid_type_error(): styles.text_opacity = "invalid value" +def test_opacity_set_allows_integer_value(): + """Regression test for https://github.com/Textualize/textual/issues/3414""" + styles = RenderStyles(DOMNode(), Styles(), Styles()) + styles.text_opacity = 0 + assert styles.text_opacity == 0.0 + + @pytest.mark.parametrize( "size_dimension_input,size_dimension_expected_output", [ diff --git a/tests/css/test_stylesheet.py b/tests/css/test_stylesheet.py index c582bc0d85..abb1afe91a 100644 --- a/tests/css/test_stylesheet.py +++ b/tests/css/test_stylesheet.py @@ -13,7 +13,7 @@ def _make_user_stylesheet(css: str) -> Stylesheet: stylesheet = Stylesheet() - stylesheet.source["test.css"] = CssSource(css, is_defaults=False) + stylesheet.source["test.tcss"] = CssSource(css, is_defaults=False) stylesheet.parse() return stylesheet diff --git a/tests/document/test_document.py b/tests/document/test_document.py new file mode 100644 index 0000000000..b6e9952782 --- /dev/null +++ b/tests/document/test_document.py @@ -0,0 +1,100 @@ +import pytest + +from textual.widgets.text_area import Document + +TEXT = """I must not fear. +Fear is the mind-killer.""" + +TEXT_NEWLINE = TEXT + "\n" +TEXT_WINDOWS = TEXT.replace("\n", "\r\n") +TEXT_WINDOWS_NEWLINE = TEXT_NEWLINE.replace("\n", "\r\n") + + +@pytest.mark.parametrize( + "text", [TEXT, TEXT_NEWLINE, TEXT_WINDOWS, TEXT_WINDOWS_NEWLINE] +) +def test_text(text): + """The text we put in is the text we get out.""" + document = Document(text) + assert document.text == text + + +def test_lines_newline_eof(): + document = Document(TEXT_NEWLINE) + assert document.lines == ["I must not fear.", "Fear is the mind-killer.", ""] + + +def test_lines_no_newline_eof(): + document = Document(TEXT) + assert document.lines == [ + "I must not fear.", + "Fear is the mind-killer.", + ] + + +def test_lines_windows(): + document = Document(TEXT_WINDOWS) + assert document.lines == ["I must not fear.", "Fear is the mind-killer."] + + +def test_lines_windows_newline(): + document = Document(TEXT_WINDOWS_NEWLINE) + assert document.lines == ["I must not fear.", "Fear is the mind-killer.", ""] + + +def test_newline_unix(): + document = Document(TEXT) + assert document.newline == "\n" + + +def test_newline_windows(): + document = Document(TEXT_WINDOWS) + assert document.newline == "\r\n" + + +def test_get_selected_text_no_selection(): + document = Document(TEXT) + selection = document.get_text_range((0, 0), (0, 0)) + assert selection == "" + + +def test_get_selected_text_single_line(): + document = Document(TEXT_WINDOWS) + selection = document.get_text_range((0, 2), (0, 6)) + assert selection == "must" + + +def test_get_selected_text_multiple_lines_unix(): + document = Document(TEXT) + selection = document.get_text_range((0, 2), (1, 2)) + assert selection == "must not fear.\nFe" + + +def test_get_selected_text_multiple_lines_windows(): + document = Document(TEXT_WINDOWS) + selection = document.get_text_range((0, 2), (1, 2)) + assert selection == "must not fear.\r\nFe" + + +def test_get_selected_text_including_final_newline_unix(): + document = Document(TEXT_NEWLINE) + selection = document.get_text_range((0, 0), (2, 0)) + assert selection == TEXT_NEWLINE + + +def test_get_selected_text_including_final_newline_windows(): + document = Document(TEXT_WINDOWS_NEWLINE) + selection = document.get_text_range((0, 0), (2, 0)) + assert selection == TEXT_WINDOWS_NEWLINE + + +def test_get_selected_text_no_newline_at_end_of_file(): + document = Document(TEXT) + selection = document.get_text_range((0, 0), (2, 0)) + assert selection == TEXT + + +def test_get_selected_text_no_newline_at_end_of_file_windows(): + document = Document(TEXT_WINDOWS) + selection = document.get_text_range((0, 0), (2, 0)) + assert selection == TEXT_WINDOWS diff --git a/tests/document/test_document_delete.py b/tests/document/test_document_delete.py new file mode 100644 index 0000000000..d00fa686c9 --- /dev/null +++ b/tests/document/test_document_delete.py @@ -0,0 +1,146 @@ +import pytest + +from textual.widgets.text_area import Document, EditResult + +TEXT = """I must not fear. +Fear is the mind-killer. +I forgot the rest of the quote. +Sorry Will.""" + + +@pytest.fixture +def document(): + document = Document(TEXT) + return document + + +def test_delete_single_character(document): + replace_result = document.replace_range((0, 0), (0, 1), "") + assert replace_result == EditResult(end_location=(0, 0), replaced_text="I") + assert document.lines == [ + " must not fear.", + "Fear is the mind-killer.", + "I forgot the rest of the quote.", + "Sorry Will.", + ] + + +def test_delete_single_newline(document): + """Testing deleting newline from right to left""" + replace_result = document.replace_range((1, 0), (0, 16), "") + assert replace_result == EditResult(end_location=(0, 16), replaced_text="\n") + assert document.lines == [ + "I must not fear.Fear is the mind-killer.", + "I forgot the rest of the quote.", + "Sorry Will.", + ] + + +def test_delete_near_end_of_document(document): + """Test deleting a range near the end of a document.""" + replace_result = document.replace_range((1, 0), (3, 11), "") + assert replace_result == EditResult( + end_location=(1, 0), + replaced_text="Fear is the mind-killer.\n" + "I forgot the rest of the quote.\n" + "Sorry Will.", + ) + assert document.lines == [ + "I must not fear.", + "", + ] + + +def test_delete_clearing_the_document(document): + replace_result = document.replace_range((0, 0), (4, 0), "") + assert replace_result == EditResult( + end_location=(0, 0), + replaced_text=TEXT, + ) + assert document.lines == [""] + + +def test_delete_multiple_characters_on_one_line(document): + replace_result = document.replace_range((0, 2), (0, 7), "") + assert replace_result == EditResult( + end_location=(0, 2), + replaced_text="must ", + ) + assert document.lines == [ + "I not fear.", + "Fear is the mind-killer.", + "I forgot the rest of the quote.", + "Sorry Will.", + ] + + +def test_delete_multiple_lines_partially_spanned(document): + """Deleting a selection that partially spans the first and final lines of the selection.""" + replace_result = document.replace_range((0, 2), (2, 2), "") + assert replace_result == EditResult( + end_location=(0, 2), + replaced_text="must not fear.\nFear is the mind-killer.\nI ", + ) + assert document.lines == [ + "I forgot the rest of the quote.", + "Sorry Will.", + ] + + +def test_delete_end_of_line(document): + """Testing deleting newline from left to right""" + replace_result = document.replace_range((0, 16), (1, 0), "") + assert replace_result == EditResult( + end_location=(0, 16), + replaced_text="\n", + ) + assert document.lines == [ + "I must not fear.Fear is the mind-killer.", + "I forgot the rest of the quote.", + "Sorry Will.", + ] + + +def test_delete_single_line_excluding_newline(document): + """Delete from the start to the end of the line.""" + replace_result = document.replace_range((2, 0), (2, 31), "") + assert replace_result == EditResult( + end_location=(2, 0), + replaced_text="I forgot the rest of the quote.", + ) + assert document.lines == [ + "I must not fear.", + "Fear is the mind-killer.", + "", + "Sorry Will.", + ] + + +def test_delete_single_line_including_newline(document): + """Delete from the start of a line to the start of the line below.""" + replace_result = document.replace_range((2, 0), (3, 0), "") + assert replace_result == EditResult( + end_location=(2, 0), + replaced_text="I forgot the rest of the quote.\n", + ) + assert document.lines == [ + "I must not fear.", + "Fear is the mind-killer.", + "Sorry Will.", + ] + + +TEXT_NEWLINE_EOF = """\ +I must not fear. +Fear is the mind-killer. +""" + + +def test_delete_end_of_file_newline(): + document = Document(TEXT_NEWLINE_EOF) + replace_result = document.replace_range((2, 0), (1, 24), "") + assert replace_result == EditResult(end_location=(1, 24), replaced_text="\n") + assert document.lines == [ + "I must not fear.", + "Fear is the mind-killer.", + ] diff --git a/tests/document/test_document_insert.py b/tests/document/test_document_insert.py new file mode 100644 index 0000000000..ea706c9abf --- /dev/null +++ b/tests/document/test_document_insert.py @@ -0,0 +1,107 @@ +from textual.widgets.text_area import Document + +TEXT = """I must not fear. +Fear is the mind-killer.""" + + +def test_insert_no_newlines(): + document = Document(TEXT) + document.replace_range((0, 1), (0, 1), " really") + assert document.lines == [ + "I really must not fear.", + "Fear is the mind-killer.", + ] + + +def test_insert_empty_string(): + document = Document(TEXT) + document.replace_range((0, 1), (0, 1), "") + assert document.lines == ["I must not fear.", "Fear is the mind-killer."] + + +def test_insert_invalid_column(): + document = Document(TEXT) + document.replace_range((0, 999), (0, 999), " really") + assert document.lines == ["I must not fear. really", "Fear is the mind-killer."] + + +def test_insert_invalid_row_and_column(): + document = Document(TEXT) + document.replace_range((999, 0), (999, 0), " really") + assert document.lines == ["I must not fear.", "Fear is the mind-killer.", " really"] + + +def test_insert_range_newline_file_start(): + document = Document(TEXT) + document.replace_range((0, 0), (0, 0), "\n") + assert document.lines == ["", "I must not fear.", "Fear is the mind-killer."] + + +def test_insert_newline_splits_line(): + document = Document(TEXT) + document.replace_range((0, 1), (0, 1), "\n") + assert document.lines == ["I", " must not fear.", "Fear is the mind-killer."] + + +def test_insert_newline_splits_line_selection(): + document = Document(TEXT) + document.replace_range((0, 1), (0, 6), "\n") + assert document.lines == ["I", " not fear.", "Fear is the mind-killer."] + + +def test_insert_multiple_lines_ends_with_newline(): + document = Document(TEXT) + document.replace_range((0, 1), (0, 1), "Hello,\nworld!\n") + assert document.lines == [ + "IHello,", + "world!", + " must not fear.", + "Fear is the mind-killer.", + ] + + +def test_insert_multiple_lines_ends_with_no_newline(): + document = Document(TEXT) + document.replace_range((0, 1), (0, 1), "Hello,\nworld!") + assert document.lines == [ + "IHello,", + "world! must not fear.", + "Fear is the mind-killer.", + ] + + +def test_insert_multiple_lines_starts_with_newline(): + document = Document(TEXT) + document.replace_range((0, 1), (0, 1), "\nHello,\nworld!\n") + assert document.lines == [ + "I", + "Hello,", + "world!", + " must not fear.", + "Fear is the mind-killer.", + ] + + +def test_insert_range_text_no_newlines(): + """Ensuring we can do a simple replacement of text.""" + document = Document(TEXT) + document.replace_range((0, 2), (0, 6), "MUST") + assert document.lines == [ + "I MUST not fear.", + "Fear is the mind-killer.", + ] + + +TEXT_NEWLINE_EOF = """\ +I must not fear. +Fear is the mind-killer. +""" + + +def test_newline_eof(): + document = Document(TEXT_NEWLINE_EOF) + assert document.lines == [ + "I must not fear.", + "Fear is the mind-killer.", + "", + ] diff --git a/tests/input/test_input_clear.py b/tests/input/test_input_clear.py new file mode 100644 index 0000000000..07abff1a76 --- /dev/null +++ b/tests/input/test_input_clear.py @@ -0,0 +1,16 @@ +from textual.app import App, ComposeResult +from textual.widgets import Input + + +class InputApp(App): + def compose(self) -> ComposeResult: + yield Input("Hello, World!") + + +async def test_input_clear(): + async with InputApp().run_test() as pilot: + input_widget = pilot.app.query_one(Input) + assert input_widget.value == "Hello, World!" + input_widget.clear() + await pilot.pause() + assert input_widget.value == "" diff --git a/tests/input/test_input_mouse.py b/tests/input/test_input_mouse.py index e4bfbb51d6..a5249e5498 100644 --- a/tests/input/test_input_mouse.py +++ b/tests/input/test_input_mouse.py @@ -6,28 +6,61 @@ from textual.geometry import Offset from textual.widgets import Input +# A string containing only single-width characters +TEXT_SINGLE = "That gum you like is going to come back in style" + +# A string containing only double-width characters +TEXT_DOUBLE = "こんにちは" + +# A string containing both single and double-width characters +TEXT_MIXED = "aこんbcにdちeは" + class InputApp(App[None]): - TEST_TEXT = "That gum you like is going to come back in style" + def __init__(self, text): + super().__init__() + self._text = text def compose(self) -> ComposeResult: - yield Input(self.TEST_TEXT) + yield Input(self._text) @pytest.mark.parametrize( - "click_at, should_land", + "text, click_at, should_land", ( - (0, 0), - (1, 1), - (10, 10), - (len(InputApp.TEST_TEXT) - 1, len(InputApp.TEST_TEXT) - 1), - (len(InputApp.TEST_TEXT), len(InputApp.TEST_TEXT)), - (len(InputApp.TEST_TEXT) * 2, len(InputApp.TEST_TEXT)), + # Single-width characters + (TEXT_SINGLE, 0, 0), + (TEXT_SINGLE, 1, 1), + (TEXT_SINGLE, 10, 10), + (TEXT_SINGLE, len(TEXT_SINGLE) - 1, len(TEXT_SINGLE) - 1), + (TEXT_SINGLE, len(TEXT_SINGLE), len(TEXT_SINGLE)), + (TEXT_SINGLE, len(TEXT_SINGLE) + 10, len(TEXT_SINGLE)), + # Double-width characters + (TEXT_DOUBLE, 0, 0), + (TEXT_DOUBLE, 1, 0), + (TEXT_DOUBLE, 2, 1), + (TEXT_DOUBLE, 3, 1), + (TEXT_DOUBLE, 4, 2), + (TEXT_DOUBLE, 5, 2), + (TEXT_DOUBLE, (len(TEXT_DOUBLE) * 2) - 1, len(TEXT_DOUBLE) - 1), + (TEXT_DOUBLE, len(TEXT_DOUBLE) * 2, len(TEXT_DOUBLE)), + (TEXT_DOUBLE, len(TEXT_DOUBLE) * 10, len(TEXT_DOUBLE)), + # Mixed-width characters + (TEXT_MIXED, 0, 0), + (TEXT_MIXED, 1, 1), + (TEXT_MIXED, 2, 1), + (TEXT_MIXED, 3, 2), + (TEXT_MIXED, 4, 2), + (TEXT_MIXED, 5, 3), + (TEXT_MIXED, 13, 9), + (TEXT_MIXED, 14, 9), + (TEXT_MIXED, 15, 10), + (TEXT_MIXED, 60, 10), ), ) -async def test_mouse_clicks_within(click_at, should_land): +async def test_mouse_clicks_within(text, click_at, should_land): """Mouse clicks should result in the cursor going to the right place.""" - async with InputApp().run_test() as pilot: + async with InputApp(text).run_test() as pilot: # Note the offsets to take into account the decoration around an # Input. await pilot.click(Input, Offset(click_at + 3, 1)) @@ -37,7 +70,7 @@ async def test_mouse_clicks_within(click_at, should_land): async def test_mouse_click_outwith(): """Mouse clicks outside the input should not affect cursor position.""" - async with InputApp().run_test() as pilot: + async with InputApp(TEXT_SINGLE).run_test() as pilot: pilot.app.query_one(Input).cursor_position = 3 assert pilot.app.query_one(Input).cursor_position == 3 await pilot.click(Input, Offset(0, 0)) diff --git a/tests/input/test_input_terminal_cursor.py b/tests/input/test_input_terminal_cursor.py new file mode 100644 index 0000000000..b956a29846 --- /dev/null +++ b/tests/input/test_input_terminal_cursor.py @@ -0,0 +1,28 @@ +from textual.app import App, ComposeResult +from textual.geometry import Offset +from textual.widgets import Input + + +class InputApp(App): + # Apply padding to ensure gutter accounted for. + CSS = "Input { padding: 4 8 }" + + def compose(self) -> ComposeResult: + yield Input("こんにちは!") + + +async def test_initial_terminal_cursor_position(): + app = InputApp() + async with app.run_test(): + # The input is focused so the terminal cursor position should update. + assert app.cursor_position == Offset(21, 5) + + +async def test_terminal_cursor_position_update_on_cursor_move(): + app = InputApp() + async with app.run_test(): + input_widget = app.query_one(Input) + input_widget.action_cursor_left() + input_widget.action_cursor_left() + # We went left over two double-width characters + assert app.cursor_position == Offset(17, 5) diff --git a/tests/input/test_input_validation.py b/tests/input/test_input_validation.py index b02b2e6574..cfbdf32928 100644 --- a/tests/input/test_input_validation.py +++ b/tests/input/test_input_validation.py @@ -1,18 +1,23 @@ +import pytest + from textual import on from textual.app import App, ComposeResult +from textual.events import Blur from textual.validation import Number, ValidationResult from textual.widgets import Input class InputApp(App): - def __init__(self): + def __init__(self, validate_on=None): super().__init__() self.messages = [] self.validator = Number(minimum=1, maximum=5) + self.validate_on = validate_on def compose(self) -> ComposeResult: yield Input( validators=self.validator, + validate_on=self.validate_on, ) @on(Input.Changed) @@ -77,3 +82,126 @@ async def test_input_submitted_message_validation_success(): await pilot.pause() assert len(app.messages) == 2 assert app.messages[1].validation_result == ValidationResult.success() + + +async def test_on_blur_triggers_validation(): + app = InputApp() + async with app.run_test() as pilot: + input = app.query_one(Input) + input.focus() + input.value = "3" + input.remove_class("-valid") + app.set_focus(None) + await pilot.pause() + assert input.has_class("-valid") + + +@pytest.mark.parametrize( + "validate_on", + [ + set(), + {"blur"}, + {"submitted"}, + {"blur", "submitted"}, + {"fried", "garbage"}, + ], +) +async def test_validation_on_changed_should_not_happen(validate_on): + app = InputApp(validate_on) + async with app.run_test() as pilot: + # sanity checks + assert len(app.messages) == 0 + input = app.query_one(Input) + assert not input.has_class("-valid") + assert not input.has_class("-invalid") + + input.value = "3" + await pilot.pause() + assert len(app.messages) == 1 + assert app.messages[-1].validation_result is None + assert not input.has_class("-valid") + assert not input.has_class("-invalid") + + +@pytest.mark.parametrize( + "validate_on", + [ + set(), + {"blur"}, + {"changed"}, + {"blur", "changed"}, + {"fried", "garbage"}, + ], +) +async def test_validation_on_submitted_should_not_happen(validate_on): + app = InputApp(validate_on) + async with app.run_test() as pilot: + # sanity checks + assert len(app.messages) == 0 + input = app.query_one(Input) + assert not input.has_class("-valid") + assert not input.has_class("-invalid") + + await input.action_submit() + await pilot.pause() + assert len(app.messages) == 1 + assert app.messages[-1].validation_result is None + assert not input.has_class("-valid") + assert not input.has_class("-invalid") + + +@pytest.mark.parametrize( + "validate_on", + [ + set(), + {"submitted"}, + {"changed"}, + {"submitted", "changed"}, + {"fried", "garbage"}, + ], +) +async def test_validation_on_blur_should_not_happen_unless_specified(validate_on): + app = InputApp(validate_on) + async with app.run_test() as pilot: + # sanity checks + input = app.query_one(Input) + assert not input.has_class("-valid") + assert not input.has_class("-invalid") + + input.focus() + await pilot.pause() + app.set_focus(None) + await pilot.pause() + assert not input.has_class("-valid") + assert not input.has_class("-invalid") + + +async def test_none_validate_on_means_all_validations_happen(): + app = InputApp(None) + async with app.run_test() as pilot: + assert len(app.messages) == 0 # sanity checks + input = app.query_one(Input) + assert not input.has_class("-valid") + assert not input.has_class("-invalid") + + input.value = "3" + await pilot.pause() + assert len(app.messages) == 1 + assert app.messages[-1].validation_result is not None + assert input.has_class("-valid") + + input.remove_class("-valid") + + await input.action_submit() + await pilot.pause() + assert len(app.messages) == 2 + assert app.messages[-1].validation_result is not None + assert input.has_class("-valid") + + input.remove_class("-valid") + + input.focus() + await pilot.pause() + app.set_focus(None) + await pilot.pause() + assert input.has_class("-valid") diff --git a/tests/notifications/test_app_notifications.py b/tests/notifications/test_app_notifications.py index 01f54ae605..608fde812f 100644 --- a/tests/notifications/test_app_notifications.py +++ b/tests/notifications/test_app_notifications.py @@ -17,15 +17,17 @@ async def test_app_with_notifications() -> None: """An app with notifications should have notifications in the list.""" async with NotificationApp().run_test() as pilot: pilot.app.notify("test") + await pilot.pause() assert len(pilot.app._notifications) == 1 async def test_app_with_removing_notifications() -> None: """An app with notifications should have notifications in the list, which can be removed.""" async with NotificationApp().run_test() as pilot: - notification = pilot.app.notify("test") + pilot.app.notify("test") + await pilot.pause() assert len(pilot.app._notifications) == 1 - pilot.app.unnotify(notification) + pilot.app._unnotify(list(pilot.app._notifications)[0]) assert len(pilot.app._notifications) == 0 @@ -34,6 +36,7 @@ async def test_app_with_notifications_that_expire() -> None: async with NotificationApp().run_test() as pilot: for n in range(100): pilot.app.notify("test", timeout=(0.5 if bool(n % 2) else 60)) + await pilot.pause() assert len(pilot.app._notifications) == 100 sleep(0.6) assert len(pilot.app._notifications) == 50 @@ -44,6 +47,7 @@ async def test_app_clearing_notifications() -> None: async with NotificationApp().run_test() as pilot: for _ in range(100): pilot.app.notify("test", timeout=120) + await pilot.pause() assert len(pilot.app._notifications) == 100 pilot.app.clear_notifications() assert len(pilot.app._notifications) == 0 diff --git a/tests/option_list/test_option_list_create.py b/tests/option_list/test_option_list_create.py index 7e0bbc49c2..3310579b81 100644 --- a/tests/option_list/test_option_list_create.py +++ b/tests/option_list/test_option_list_create.py @@ -119,5 +119,37 @@ async def test_add_later() -> None: async def test_create_with_duplicate_id() -> None: """Adding an option with a duplicate ID should be an error.""" async with OptionListApp().run_test() as pilot: + option_list = pilot.app.query_one(OptionList) + assert option_list.option_count == 5 + with pytest.raises(DuplicateID): + option_list.add_option(Option("dupe", id="3")) + assert option_list.option_count == 5 + + +async def test_create_with_duplicate_id_and_subsequent_non_dupes() -> None: + """Adding an option with a duplicate ID should be an error.""" + async with OptionListApp().run_test() as pilot: + option_list = pilot.app.query_one(OptionList) + assert option_list.option_count == 5 with pytest.raises(DuplicateID): - pilot.app.query_one(OptionList).add_option(Option("dupe", id="3")) + option_list.add_option(Option("dupe", id="3")) + assert option_list.option_count == 5 + option_list.add_option(Option("Not a dupe", id="6")) + assert option_list.option_count == 6 + option_list.add_option(Option("Not a dupe", id="7")) + assert option_list.option_count == 7 + + +async def test_adding_multiple_duplicates_at_once() -> None: + """Adding duplicates together than aren't existing duplicates should be an error.""" + async with OptionListApp().run_test() as pilot: + option_list = pilot.app.query_one(OptionList) + assert option_list.option_count == 5 + with pytest.raises(DuplicateID): + option_list.add_options( + [ + Option("dupe", id="42"), + Option("dupe", id="42"), + ] + ) + assert option_list.option_count == 5 diff --git a/tests/option_list/test_option_removal.py b/tests/option_list/test_option_removal.py index fe64543aa2..a26f5af496 100644 --- a/tests/option_list/test_option_removal.py +++ b/tests/option_list/test_option_removal.py @@ -5,6 +5,7 @@ import pytest from textual.app import App, ComposeResult +from textual.geometry import Offset from textual.widgets import OptionList from textual.widgets.option_list import Option, OptionDoesNotExist @@ -99,3 +100,13 @@ async def test_remove_invalid_index() -> None: async with OptionListApp().run_test() as pilot: with pytest.raises(OptionDoesNotExist): pilot.app.query_one(OptionList).remove_option_at_index(23) + + +async def test_remove_with_hover_on_last_option(): + """https://github.com/Textualize/textual/issues/3270""" + async with OptionListApp().run_test() as pilot: + await pilot.hover(OptionList, Offset(1, 1) + Offset(2, 1)) + option_list = pilot.app.query_one(OptionList) + assert option_list._mouse_hovering_over == 1 + option_list.remove_option_at_index(0) + assert option_list._mouse_hovering_over == None diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 1802220c43..4b23ecded8 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -21,138 +21,138 @@ font-weight: 700; } - .terminal-2137082507-matrix { + .terminal-644510384-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2137082507-title { + .terminal-644510384-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2137082507-r1 { fill: #c5c8c6 } - .terminal-2137082507-r2 { fill: #7ae998 } - .terminal-2137082507-r3 { fill: #4ebf71;font-weight: bold } - .terminal-2137082507-r4 { fill: #008139 } - .terminal-2137082507-r5 { fill: #e3dbce } - .terminal-2137082507-r6 { fill: #e1e1e1 } - .terminal-2137082507-r7 { fill: #e76580 } - .terminal-2137082507-r8 { fill: #f5e5e9;font-weight: bold } - .terminal-2137082507-r9 { fill: #780028 } + .terminal-644510384-r1 { fill: #c5c8c6 } + .terminal-644510384-r2 { fill: #7ae998 } + .terminal-644510384-r3 { fill: #4ebf71;font-weight: bold } + .terminal-644510384-r4 { fill: #008139 } + .terminal-644510384-r5 { fill: #e3dbce } + .terminal-644510384-r6 { fill: #e1e1e1 } + .terminal-644510384-r7 { fill: #e76580 } + .terminal-644510384-r8 { fill: #f5e5e9;font-weight: bold } + .terminal-644510384-r9 { fill: #780028 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - AlignContainersApp + AlignContainersApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  center  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  middle  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + center + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + middle + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + @@ -324,6 +324,324 @@ ''' # --- +# name: test_auto_grid + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GridApp + + + + + + + + + + ────────────────────────────────────────────────────────────────────────────── + foo▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + Longer label▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ────────────────────────────────────────────────────────────────────────────── + ────────────────────────────────────────────────────────────────────────────── + foo▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + Longer label▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ────────────────────────────────────────────────────────────────────────────── + ────────────────────────────────────────────────────────────────────────────── + foo bar foo bar foo bar foo ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + bar foo bar foo bar foo bar  + foo bar foo bar foo bar ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + Longer label▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ────────────────────────────────────────────────────────────────────────────── + + + + + ''' +# --- +# name: test_auto_grid_default_height + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GridHeightAuto + + + + + + + + + + GridHeightAuto + Here is some text before the grid + ────────────────────────────────────────────────────────────────────────────── + Cell #0Cell #1Cell #2 + Cell #3Cell #4Cell #5 + Cell #6Cell #7Cell #8 + ────────────────────────────────────────────────────────────────────────────── + Here is some text after the grid + + + + + + + + + + + + + + + +  G  Grid  V  Vertical  H  Horizontal  C  Container  + + + + + ''' +# --- # name: test_auto_table ''' @@ -1051,162 +1369,162 @@ font-weight: 700; } - .terminal-3315449210-matrix { + .terminal-3842397750-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3315449210-title { + .terminal-3842397750-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3315449210-r1 { fill: #e1e1e1 } - .terminal-3315449210-r2 { fill: #c5c8c6 } - .terminal-3315449210-r3 { fill: #e1e1e1;font-weight: bold } - .terminal-3315449210-r4 { fill: #454a50 } - .terminal-3315449210-r5 { fill: #303336 } - .terminal-3315449210-r6 { fill: #24292f;font-weight: bold } - .terminal-3315449210-r7 { fill: #a7a7a7;font-weight: bold } - .terminal-3315449210-r8 { fill: #000000 } - .terminal-3315449210-r9 { fill: #0f0f0f } - .terminal-3315449210-r10 { fill: #507bb3 } - .terminal-3315449210-r11 { fill: #364b66 } - .terminal-3315449210-r12 { fill: #dde6ed;font-weight: bold } - .terminal-3315449210-r13 { fill: #a5a9ac;font-weight: bold } - .terminal-3315449210-r14 { fill: #001541 } - .terminal-3315449210-r15 { fill: #0f192e } - .terminal-3315449210-r16 { fill: #7ae998 } - .terminal-3315449210-r17 { fill: #4a8159 } - .terminal-3315449210-r18 { fill: #0a180e;font-weight: bold } - .terminal-3315449210-r19 { fill: #0e1510;font-weight: bold } - .terminal-3315449210-r20 { fill: #008139 } - .terminal-3315449210-r21 { fill: #0f4e2a } - .terminal-3315449210-r22 { fill: #ffcf56 } - .terminal-3315449210-r23 { fill: #8b7439 } - .terminal-3315449210-r24 { fill: #211505;font-weight: bold } - .terminal-3315449210-r25 { fill: #19140c;font-weight: bold } - .terminal-3315449210-r26 { fill: #b86b00 } - .terminal-3315449210-r27 { fill: #68430f } - .terminal-3315449210-r28 { fill: #e76580 } - .terminal-3315449210-r29 { fill: #80404d } - .terminal-3315449210-r30 { fill: #f5e5e9;font-weight: bold } - .terminal-3315449210-r31 { fill: #b0a8aa;font-weight: bold } - .terminal-3315449210-r32 { fill: #780028 } - .terminal-3315449210-r33 { fill: #4a0f22 } + .terminal-3842397750-r1 { fill: #e1e1e1 } + .terminal-3842397750-r2 { fill: #c5c8c6 } + .terminal-3842397750-r3 { fill: #e1e1e1;font-weight: bold } + .terminal-3842397750-r4 { fill: #454a50 } + .terminal-3842397750-r5 { fill: #303336 } + .terminal-3842397750-r6 { fill: #24292f;font-weight: bold } + .terminal-3842397750-r7 { fill: #a7a7a7;font-weight: bold } + .terminal-3842397750-r8 { fill: #000000 } + .terminal-3842397750-r9 { fill: #0f0f0f } + .terminal-3842397750-r10 { fill: #507bb3 } + .terminal-3842397750-r11 { fill: #364b66 } + .terminal-3842397750-r12 { fill: #dde6ed;font-weight: bold } + .terminal-3842397750-r13 { fill: #a5a9ac;font-weight: bold } + .terminal-3842397750-r14 { fill: #001541 } + .terminal-3842397750-r15 { fill: #0f192e } + .terminal-3842397750-r16 { fill: #7ae998 } + .terminal-3842397750-r17 { fill: #4a8159 } + .terminal-3842397750-r18 { fill: #0a180e;font-weight: bold } + .terminal-3842397750-r19 { fill: #0e1510;font-weight: bold } + .terminal-3842397750-r20 { fill: #008139 } + .terminal-3842397750-r21 { fill: #0f4e2a } + .terminal-3842397750-r22 { fill: #ffcf56 } + .terminal-3842397750-r23 { fill: #8b7439 } + .terminal-3842397750-r24 { fill: #211505;font-weight: bold } + .terminal-3842397750-r25 { fill: #19140c;font-weight: bold } + .terminal-3842397750-r26 { fill: #b86b00 } + .terminal-3842397750-r27 { fill: #68430f } + .terminal-3842397750-r28 { fill: #e76580 } + .terminal-3842397750-r29 { fill: #80404d } + .terminal-3842397750-r30 { fill: #f5e5e9;font-weight: bold } + .terminal-3842397750-r31 { fill: #b0a8aa;font-weight: bold } + .terminal-3842397750-r32 { fill: #780028 } + .terminal-3842397750-r33 { fill: #4a0f22 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ButtonsApp + ButtonsApp - - - - - Standard ButtonsDisabled Buttons - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Default  Default  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Primary!  Primary!  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Success!  Success!  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Warning!  Warning!  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Error!  Error!  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - + + + + + Standard ButtonsDisabled Buttons + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + DefaultDefault + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Primary!Primary! + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Success!Success! + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Warning!Warning! + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Error!Error! + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + @@ -1380,7 +1698,7 @@ ''' # --- -# name: test_columns_height +# name: test_collapsible_collapsed ''' @@ -1403,141 +1721,143 @@ font-weight: 700; } - .terminal-363813734-matrix { + .terminal-658258504-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-363813734-title { + .terminal-658258504-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-363813734-r1 { fill: #ff0000 } - .terminal-363813734-r2 { fill: #c5c8c6 } - .terminal-363813734-r3 { fill: #008000 } - .terminal-363813734-r4 { fill: #e1e1e1 } + .terminal-658258504-r1 { fill: #121212 } + .terminal-658258504-r2 { fill: #c5c8c6 } + .terminal-658258504-r3 { fill: #ddedf9 } + .terminal-658258504-r4 { fill: #e2e2e2 } + .terminal-658258504-r5 { fill: #e1e1e1 } + .terminal-658258504-r6 { fill: #dde8f3;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - HeightApp + CollapsibleApp - - - - ────────────────────────────────────────────────────────────────────────────── - ────────────────────────────────────────────────────────── - As tall as containerThis has defaultI have a static height - height - but a - few lines - ──────────────── - - - - - - - - - - ────────────────────────────────────────── - ────────────────────────────────────────────────────────────────────────────── - - - - - - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▶ Leto + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▶ Jessica + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▶ Paul + + + + + + + + + + + + + + + +  C  Collapse All  E  Expand All  ''' # --- -# name: test_content_switcher_example_initial +# name: test_collapsible_custom_symbol ''' @@ -1560,140 +1880,133 @@ font-weight: 700; } - .terminal-4120014803-matrix { + .terminal-3381030266-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4120014803-title { + .terminal-3381030266-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4120014803-r1 { fill: #c5c8c6 } - .terminal-4120014803-r2 { fill: #e1e1e1 } - .terminal-4120014803-r3 { fill: #454a50 } - .terminal-4120014803-r4 { fill: #24292f;font-weight: bold } - .terminal-4120014803-r5 { fill: #e2e3e3;font-weight: bold } - .terminal-4120014803-r6 { fill: #000000 } - .terminal-4120014803-r7 { fill: #004578 } - .terminal-4120014803-r8 { fill: #dde6ed;font-weight: bold } - .terminal-4120014803-r9 { fill: #dde6ed } - .terminal-4120014803-r10 { fill: #211505 } - .terminal-4120014803-r11 { fill: #e2e3e3 } + .terminal-3381030266-r1 { fill: #121212 } + .terminal-3381030266-r2 { fill: #c5c8c6 } + .terminal-3381030266-r3 { fill: #ddedf9 } + .terminal-3381030266-r4 { fill: #e2e2e2 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ContentSwitcherApp + CollapsibleApp - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  DataTable  Markdown  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ──────────────────────────────────────────────────────────────────── -  Book                                 Year  -  Dune                                 1965  -  Dune Messiah                         1969  -  Children of Dune                     1976  -  God Emperor of Dune                  1981  -  Heretics of Dune                     1984  -  Chapterhouse: Dune                   1985  - - - - - - - - - - - ──────────────────────────────────────────────────────────────────── + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + >>> Togglev Toggle + + Hello, world. + + + + + + + + + + + + + + + + + + + @@ -1701,9 +2014,9 @@ ''' # --- -# name: test_content_switcher_example_switch +# name: test_collapsible_expanded ''' - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - ContentSwitcherApp + CollapsibleApp - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  DataTable  Markdown  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ───────────────────────────────────────── - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - Three Flavours Cornetto - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - The Three Flavours Cornetto  - trilogy is an anthology series  - of British comedic genre films  - directed by Edgar Wright. - -        Shaun of the Dead        - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - UK       - Release  - Flavour Date    Director  -  ━━━━━━━━━━━━━━━━━━━━━━━━━━━  - Strawbe…2004-04…Edgar     - Wright    - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -            Hot Fuzz             - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - UK       - Release  - Flavour Date    Director  -  ━━━━━━━━━━━━━━━━━━━━━━━━━━━  - Classico2007-02…Edgar     - Wright    - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -         The World's End         - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - UK        - Release   - FlavourDate     Director  - ───────────────────────────────────────── - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▼ Leto + + # Duke Leto I Atreides + + Head of House Atreides. + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▼ Jessica + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Lady Jessica + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Bene Gesserit and concubine of Leto, and mother of Paul and Alia. + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▼ Paul + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +  Collapse All  E  Expand All  ''' # --- -# name: test_css_hot_reloading +# name: test_collapsible_nested ''' @@ -1994,131 +2197,134 @@ font-weight: 700; } - .terminal-3709538693-matrix { + .terminal-3855984296-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3709538693-title { + .terminal-3855984296-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3709538693-r1 { fill: #e1e1e1 } - .terminal-3709538693-r2 { fill: #c5c8c6 } + .terminal-3855984296-r1 { fill: #121212 } + .terminal-3855984296-r2 { fill: #c5c8c6 } + .terminal-3855984296-r3 { fill: #ddedf9 } + .terminal-3855984296-r4 { fill: #e3e3e3 } + .terminal-3855984296-r5 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - HotReloadingApp + CollapsibleApp - - - - Hello, world! - - - - - - - - - - - - - - - - - - - - - - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▼ Toggle + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▶ Toggle + + + + + + + + + + + + + + + + + + @@ -2126,7 +2332,7 @@ ''' # --- -# name: test_css_property[align.py] +# name: test_collapsible_render ''' @@ -2149,142 +2355,144 @@ font-weight: 700; } - .terminal-1567237307-matrix { + .terminal-3602827927-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1567237307-title { + .terminal-3602827927-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1567237307-r1 { fill: #e1e1e1 } - .terminal-1567237307-r2 { fill: #c5c8c6 } - .terminal-1567237307-r3 { fill: #ffffff } - .terminal-1567237307-r4 { fill: #e5f2e5 } - .terminal-1567237307-r5 { fill: #e5f2e5;font-weight: bold } + .terminal-3602827927-r1 { fill: #121212 } + .terminal-3602827927-r2 { fill: #c5c8c6 } + .terminal-3602827927-r3 { fill: #ddedf9 } + .terminal-3602827927-r4 { fill: #e2e2e2 } + .terminal-3602827927-r5 { fill: #0053aa } + .terminal-3602827927-r6 { fill: #dde8f3;font-weight: bold } + .terminal-3602827927-r7 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - AlignApp + CollapsibleApp - - - - - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - Vertical alignment with Textual - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - Take note, browsers. - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - - - - - - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▼ Leto + + # Duke Leto I Atreides + + Head of House Atreides. + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▼ Jessica + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Lady Jessica + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Bene Gesserit and concubine of Leto, and mother of Paul and Alia. + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▶ Paul + + +  C  Collapse All  E  Expand All  ''' # --- -# name: test_css_property[align_all.py] +# name: test_columns_height ''' @@ -2307,142 +2515,141 @@ font-weight: 700; } - .terminal-1331556511-matrix { + .terminal-363813734-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1331556511-title { + .terminal-363813734-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1331556511-r1 { fill: #808080 } - .terminal-1331556511-r2 { fill: #e1e1e1 } - .terminal-1331556511-r3 { fill: #c5c8c6 } - .terminal-1331556511-r4 { fill: #ddedf9 } - .terminal-1331556511-r5 { fill: #e2e2e2 } + .terminal-363813734-r1 { fill: #ff0000 } + .terminal-363813734-r2 { fill: #c5c8c6 } + .terminal-363813734-r3 { fill: #008000 } + .terminal-363813734-r4 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - AlignAllApp + HeightApp - - - - ──────────────────────────────────────────────────────────────────────── - left topcenter topright top - - - - - ──────────────────────────────────────────────────────────────────────── - - ──────────────────────────────────────────────────────────────────────── - - - left middlecenter middleright middle - - - ──────────────────────────────────────────────────────────────────────── - - ──────────────────────────────────────────────────────────────────────── - - - - - - left bottomcenter bottomright bottom - ──────────────────────────────────────────────────────────────────────── + + + + ────────────────────────────────────────────────────────────────────────────── + ────────────────────────────────────────────────────────── + As tall as containerThis has defaultI have a static height + height + but a + few lines + ──────────────── + + + + + + + + + + ────────────────────────────────────────── + ────────────────────────────────────────────────────────────────────────────── + + + + + + ''' # --- -# name: test_css_property[background.py] +# name: test_command_palette ''' @@ -2465,131 +2672,136 @@ font-weight: 700; } - .terminal-470351640-matrix { + .terminal-3973201778-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-470351640-title { + .terminal-3973201778-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-470351640-r1 { fill: #c5c8c6 } - .terminal-470351640-r2 { fill: #ffffff } + .terminal-3973201778-r1 { fill: #a2a2a2 } + .terminal-3973201778-r2 { fill: #c5c8c6 } + .terminal-3973201778-r3 { fill: #004578 } + .terminal-3973201778-r4 { fill: #00ff00 } + .terminal-3973201778-r5 { fill: #e2e3e3 } + .terminal-3973201778-r6 { fill: #1e1e1e } + .terminal-3973201778-r7 { fill: #fea62b;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - BackgroundApp + CommandPaletteApp - - - - - - - Widget 1 - - - - - - - - Widget 2 - - - - - - - - Widget 3 - - - + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + 🔎A + + + This is a test of this code 9 + This is a test of this code 8 + This is a test of this code 7 + This is a test of this code 6 + This is a test of this code 5 + This is a test of this code 4 + This is a test of this code 3 + This is a test of this code 2 + This is a test of this code 1 + This is a test of this code 0 + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + @@ -2597,7 +2809,7 @@ ''' # --- -# name: test_css_property[background_transparency.py] +# name: test_content_switcher_example_initial ''' @@ -2620,140 +2832,140 @@ font-weight: 700; } - .terminal-4087461672-matrix { + .terminal-78223076-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4087461672-title { + .terminal-78223076-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4087461672-r1 { fill: #c5c8c6 } - .terminal-4087461672-r2 { fill: #e4e1e1 } - .terminal-4087461672-r3 { fill: #e7e0e0 } - .terminal-4087461672-r4 { fill: #eae0e0 } - .terminal-4087461672-r5 { fill: #ede0e0 } - .terminal-4087461672-r6 { fill: #f0dfdf } - .terminal-4087461672-r7 { fill: #f3dfdf } - .terminal-4087461672-r8 { fill: #f6dfdf } - .terminal-4087461672-r9 { fill: #f9dede } - .terminal-4087461672-r10 { fill: #fcdede } - .terminal-4087461672-r11 { fill: #ffdddd } + .terminal-78223076-r1 { fill: #c5c8c6 } + .terminal-78223076-r2 { fill: #e1e1e1 } + .terminal-78223076-r3 { fill: #454a50 } + .terminal-78223076-r4 { fill: #24292f;font-weight: bold } + .terminal-78223076-r5 { fill: #e2e3e3;font-weight: bold } + .terminal-78223076-r6 { fill: #000000 } + .terminal-78223076-r7 { fill: #004578 } + .terminal-78223076-r8 { fill: #dde6ed;font-weight: bold } + .terminal-78223076-r9 { fill: #dde6ed } + .terminal-78223076-r10 { fill: #211505 } + .terminal-78223076-r11 { fill: #e2e3e3 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - BackgroundTransparencyApp + ContentSwitcherApp - - - - - - - - - - - - - - - 10%20%30%40%50%60%70%80%90%100% - - - - - - - - - - - + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + DataTableMarkdown + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ──────────────────────────────────────────────────────────────────── +  Book                                 Year  +  Dune                                 1965  +  Dune Messiah                         1969  +  Children of Dune                     1976  +  God Emperor of Dune                  1981  +  Heretics of Dune                     1984  +  Chapterhouse: Dune                   1985  + + + + + + + + + + + ──────────────────────────────────────────────────────────────────── @@ -2761,9 +2973,9 @@ ''' # --- -# name: test_css_property[border.py] +# name: test_content_switcher_example_switch ''' - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - BorderApp + ContentSwitcherApp - - - - - ──────────────────────────────────────────────────────────────────────────── - - My border is solid red - - ──────────────────────────────────────────────────────────────────────────── - - ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ - - My border is dashed green - - ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - My border is tall blue - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + DataTableMarkdown + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ───────────────────────────────────────── + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Three Flavours Cornetto + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + The Three Flavours Cornetto  + trilogy is an anthology series  + of British comedic genre films  + directed by Edgar Wright. + +        Shaun of the Dead        + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + UK       + Release  + Flavour Date    Director  +  ━━━━━━━━━━━━━━━━━━━━━━━━━━━  + Strawbe…2004-04…Edgar     + Wright    + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +            Hot Fuzz             + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + UK       + Release  + Flavour Date    Director  +  ━━━━━━━━━━━━━━━━━━━━━━━━━━━  + Classico2007-02…Edgar     + Wright    + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +         The World's End         + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + UK        + Release   + FlavourDate     Director  + ───────────────────────────────────────── @@ -2919,7 +3243,7 @@ ''' # --- -# name: test_css_property[border_all.py] +# name: test_css_hot_reloading ''' @@ -2942,133 +3266,131 @@ font-weight: 700; } - .terminal-1717278065-matrix { + .terminal-3709538693-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1717278065-title { + .terminal-3709538693-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1717278065-r1 { fill: #c5c8c6 } - .terminal-1717278065-r2 { fill: #0178d4 } - .terminal-1717278065-r3 { fill: #e1e1e1 } - .terminal-1717278065-r4 { fill: #1e1e1e } + .terminal-3709538693-r1 { fill: #e1e1e1 } + .terminal-3709538693-r2 { fill: #c5c8c6 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - AllBordersApp + HotReloadingApp - - - - +------------------+╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ - |ascii|blankdashed - +------------------+╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ - - - ══════════════════━━━━━━━━━━━━━━━━━━ - doubleheavyhidden/none - ══════════════════━━━━━━━━━━━━━━━━━━ - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - hkeyinnerouter - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - - - ────────────────────────────────────▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - roundsolidtall - ────────────────────────────────────▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - thickvkeywide - ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + + Hello, world! + + + + + + + + + + + + + + + + + + + + + + @@ -3076,7 +3398,7 @@ ''' # --- -# name: test_css_property[border_sub_title_align_all.py] +# name: test_css_property[align.py] ''' @@ -3099,141 +3421,134 @@ font-weight: 700; } - .terminal-1997861159-matrix { + .terminal-1567237307-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1997861159-title { + .terminal-1567237307-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1997861159-r1 { fill: #e1e1e1 } - .terminal-1997861159-r2 { fill: #c5c8c6 } - .terminal-1997861159-r3 { fill: #fea62b } - .terminal-1997861159-r4 { fill: #fea62b;font-weight: bold } - .terminal-1997861159-r5 { fill: #fea62b;font-weight: bold;font-style: italic; } - .terminal-1997861159-r6 { fill: #cc555a;font-weight: bold } - .terminal-1997861159-r7 { fill: #1e1e1e } - .terminal-1997861159-r8 { fill: #1e1e1e;text-decoration: underline; } - .terminal-1997861159-r9 { fill: #fea62b;text-decoration: underline; } - .terminal-1997861159-r10 { fill: #4b4e55;text-decoration: underline; } - .terminal-1997861159-r11 { fill: #4ebf71 } - .terminal-1997861159-r12 { fill: #b93c5b } + .terminal-1567237307-r1 { fill: #e1e1e1 } + .terminal-1567237307-r2 { fill: #c5c8c6 } + .terminal-1567237307-r3 { fill: #ffffff } + .terminal-1567237307-r4 { fill: #e5f2e5 } + .terminal-1567237307-r5 { fill: #e5f2e5;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - BorderSubTitleAlignAll + AlignApp - - - - - - Border titleLef…▁▁▁▁Left▁▁▁▁ - This is the story ofa Pythondeveloper that - Border subtitleCen…▔▔▔▔@@@▔▔▔▔▔ - - - - - - +--------------+Title───────────────── - |had to fill up|nine labelsand ended up redoing it - +-Left-------+──────────────Subtitle - - - - - Title, but really looo… - Title, but r…Title, but reall… - because the first tryhad some labelsthat were too long. - Subtitle, bu…Subtitle, but re… - Subtitle, but really l… - + + + + + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + Vertical alignment with Textual + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + Take note, browsers. + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + + + + + @@ -3241,7 +3556,7 @@ ''' # --- -# name: test_css_property[border_subtitle_align.py] +# name: test_css_property[align_all.py] ''' @@ -3264,142 +3579,142 @@ font-weight: 700; } - .terminal-1601354540-matrix { + .terminal-1331556511-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1601354540-title { + .terminal-1331556511-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1601354540-r1 { fill: #e1e1e1 } - .terminal-1601354540-r2 { fill: #c5c8c6 } - .terminal-1601354540-r3 { fill: #fea62b } - .terminal-1601354540-r4 { fill: #ffffff } - .terminal-1601354540-r5 { fill: #1e1e1e } + .terminal-1331556511-r1 { fill: #808080 } + .terminal-1331556511-r2 { fill: #e1e1e1 } + .terminal-1331556511-r3 { fill: #c5c8c6 } + .terminal-1331556511-r4 { fill: #ddedf9 } + .terminal-1331556511-r5 { fill: #e2e2e2 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - BorderSubtitleAlignApp + AlignAllApp - - - - - ──────────────────────────────────────────────────────────────────────────── - - My subtitle is on the left. - -  < Left ─────────────────────────────────────────────────────────────────── - - ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ - - My subtitle is centered - - ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ Centered! ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - My subtitle is on the right - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ Right >  - - - - - - + + + + ──────────────────────────────────────────────────────────────────────── + left topcenter topright top + + + + + ──────────────────────────────────────────────────────────────────────── + + ──────────────────────────────────────────────────────────────────────── + + + left middlecenter middleright middle + + + ──────────────────────────────────────────────────────────────────────── + + ──────────────────────────────────────────────────────────────────────── + + + + + + left bottomcenter bottomright bottom + ──────────────────────────────────────────────────────────────────────── ''' # --- -# name: test_css_property[border_title_align.py] +# name: test_css_property[background.py] ''' @@ -3422,134 +3737,131 @@ font-weight: 700; } - .terminal-2047325817-matrix { + .terminal-470351640-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2047325817-title { + .terminal-470351640-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2047325817-r1 { fill: #e1e1e1 } - .terminal-2047325817-r2 { fill: #c5c8c6 } - .terminal-2047325817-r3 { fill: #fea62b } - .terminal-2047325817-r4 { fill: #ffffff } - .terminal-2047325817-r5 { fill: #1e1e1e } + .terminal-470351640-r1 { fill: #c5c8c6 } + .terminal-470351640-r2 { fill: #ffffff } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - BorderTitleAlignApp + BackgroundApp - - - - -  < Left ─────────────────────────────────────────────────────────────────── - - My title is on the left. - - ──────────────────────────────────────────────────────────────────────────── - - ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ Centered! ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ - - My title is centered - - ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Right >  - - My title is on the right - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - + + + + + + + Widget 1 + + + + + + + + Widget 2 + + + + + + + + Widget 3 + + + @@ -3557,7 +3869,7 @@ ''' # --- -# name: test_css_property[border_title_colors.py] +# name: test_css_property[background_transparency.py] ''' @@ -3580,134 +3892,140 @@ font-weight: 700; } - .terminal-2286355719-matrix { + .terminal-4087461672-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2286355719-title { + .terminal-4087461672-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2286355719-r1 { fill: #e1e1e1 } - .terminal-2286355719-r2 { fill: #c5c8c6 } - .terminal-2286355719-r3 { fill: #ff0000 } - .terminal-2286355719-r4 { fill: #008000;font-weight: bold } - .terminal-2286355719-r5 { fill: #ff00ff;font-style: italic; } + .terminal-4087461672-r1 { fill: #c5c8c6 } + .terminal-4087461672-r2 { fill: #e4e1e1 } + .terminal-4087461672-r3 { fill: #e7e0e0 } + .terminal-4087461672-r4 { fill: #eae0e0 } + .terminal-4087461672-r5 { fill: #ede0e0 } + .terminal-4087461672-r6 { fill: #f0dfdf } + .terminal-4087461672-r7 { fill: #f3dfdf } + .terminal-4087461672-r8 { fill: #f6dfdf } + .terminal-4087461672-r9 { fill: #f9dede } + .terminal-4087461672-r10 { fill: #fcdede } + .terminal-4087461672-r11 { fill: #ffdddd } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - BorderTitleApp + BackgroundTransparencyApp - - - - - - - - - -  Textual Rocks ━━━━━━━━━━━━━ - - - - - Hello, World! - - - - - ━━━━━━━━━━━━━ Textual Rocks  - - - - - - + + + + + + + + + + + + + + + 10%20%30%40%50%60%70%80%90%100% + + + + + + + + + + + @@ -3715,7 +4033,7 @@ ''' # --- -# name: test_css_property[box_sizing.py] +# name: test_css_property[border.py] ''' @@ -3738,132 +4056,134 @@ font-weight: 700; } - .terminal-3266307003-matrix { + .terminal-1839441138-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3266307003-title { + .terminal-1839441138-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3266307003-r1 { fill: #000000 } - .terminal-3266307003-r2 { fill: #c5c8c6 } - .terminal-3266307003-r3 { fill: #ccccff } + .terminal-1839441138-r1 { fill: #ffffff } + .terminal-1839441138-r2 { fill: #c5c8c6 } + .terminal-1839441138-r3 { fill: #ff0000 } + .terminal-1839441138-r4 { fill: #008000 } + .terminal-1839441138-r5 { fill: #0000ff } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - BoxSizingApp + BorderApp - - - - - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - I'm using border-box! - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - I'm using content-box! - - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - - - - + + + + + ──────────────────────────────────────────────────────────────────────────── + + My border is solid red + + ──────────────────────────────────────────────────────────────────────────── + + ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ + + My border is dashed green + + ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + My border is tall blue + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + @@ -3871,7 +4191,7 @@ ''' # --- -# name: test_css_property[color.py] +# name: test_css_property[border_all.py] ''' @@ -3894,133 +4214,133 @@ font-weight: 700; } - .terminal-830407627-matrix { + .terminal-1717278065-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-830407627-title { + .terminal-1717278065-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-830407627-r1 { fill: #c5c8c6 } - .terminal-830407627-r2 { fill: #ff0000 } - .terminal-830407627-r3 { fill: #00ff00 } - .terminal-830407627-r4 { fill: #0000ff } + .terminal-1717278065-r1 { fill: #c5c8c6 } + .terminal-1717278065-r2 { fill: #0178d4 } + .terminal-1717278065-r3 { fill: #e1e1e1 } + .terminal-1717278065-r4 { fill: #1e1e1e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ColorApp + AllBordersApp - - - - - - - I'm red! - - - - - - - - I'm rgb(0, 255, 0)! - - - - - - - - I'm hsl(240, 100%, 50%)! - - - + + + + +------------------+╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ + |ascii|blankdashed + +------------------+╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ + + + ══════════════════━━━━━━━━━━━━━━━━━━ + doubleheavyhidden/none + ══════════════════━━━━━━━━━━━━━━━━━━ + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + hkeyinnerouter + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ + + + ────────────────────────────────────▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + roundsolidtall + ────────────────────────────────────▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + thickvkeywide + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ @@ -4028,7 +4348,7 @@ ''' # --- -# name: test_css_property[color_auto.py] +# name: test_css_property[border_sub_title_align_all.py] ''' @@ -4051,135 +4371,141 @@ font-weight: 700; } - .terminal-334364861-matrix { + .terminal-1997861159-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-334364861-title { + .terminal-1997861159-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-334364861-r1 { fill: #c5c8c6 } - .terminal-334364861-r2 { fill: #f6cdcd } - .terminal-334364861-r3 { fill: #2a2a01 } - .terminal-334364861-r4 { fill: #cdcdf6 } - .terminal-334364861-r5 { fill: #2a1f21 } - .terminal-334364861-r6 { fill: #cde1cd } + .terminal-1997861159-r1 { fill: #e1e1e1 } + .terminal-1997861159-r2 { fill: #c5c8c6 } + .terminal-1997861159-r3 { fill: #fea62b } + .terminal-1997861159-r4 { fill: #fea62b;font-weight: bold } + .terminal-1997861159-r5 { fill: #fea62b;font-weight: bold;font-style: italic; } + .terminal-1997861159-r6 { fill: #cc555a;font-weight: bold } + .terminal-1997861159-r7 { fill: #1e1e1e } + .terminal-1997861159-r8 { fill: #1e1e1e;text-decoration: underline; } + .terminal-1997861159-r9 { fill: #fea62b;text-decoration: underline; } + .terminal-1997861159-r10 { fill: #4b4e55;text-decoration: underline; } + .terminal-1997861159-r11 { fill: #4ebf71 } + .terminal-1997861159-r12 { fill: #b93c5b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ColorApp + BorderSubTitleAlignAll - - - - - The quick brown fox jumps over the lazy dog! - - - - - The quick brown fox jumps over the lazy dog! - - - - - The quick brown fox jumps over the lazy dog! - - - - - The quick brown fox jumps over the lazy dog! - - - - - The quick brown fox jumps over the lazy dog! - + + + + + + Border titleLef…▁▁▁▁Left▁▁▁▁ + This is the story ofa Pythondeveloper that + Border subtitleCen…▔▔▔▔@@@▔▔▔▔▔ + + + + + + +--------------+Title───────────────── + |had to fill up|nine labelsand ended up redoing it + +-Left-------+──────────────Subtitle + + + + + Title, but really looo… + Title, but r…Title, but reall… + because the first tryhad some labelsthat were too long. + Subtitle, bu…Subtitle, but re… + Subtitle, but really l… + @@ -4187,7 +4513,7 @@ ''' # --- -# name: test_css_property[column_span.py] +# name: test_css_property[border_subtitle_align.py] ''' @@ -4210,137 +4536,134 @@ font-weight: 700; } - .terminal-3959176494-matrix { + .terminal-1601354540-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3959176494-title { + .terminal-1601354540-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3959176494-r1 { fill: #c5c8c6 } - .terminal-3959176494-r2 { fill: #e8e0e7 } - .terminal-3959176494-r3 { fill: #eae3e5 } - .terminal-3959176494-r4 { fill: #ede6e6 } - .terminal-3959176494-r5 { fill: #efe9e4 } - .terminal-3959176494-r6 { fill: #efeedf } - .terminal-3959176494-r7 { fill: #e9eee5 } - .terminal-3959176494-r8 { fill: #e4eee8 } + .terminal-1601354540-r1 { fill: #e1e1e1 } + .terminal-1601354540-r2 { fill: #c5c8c6 } + .terminal-1601354540-r3 { fill: #fea62b } + .terminal-1601354540-r4 { fill: #ffffff } + .terminal-1601354540-r5 { fill: #1e1e1e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + BorderSubtitleAlignApp - - - - - - #p1 - - - - - - #p2#p3 - - - - - - #p4#p5 - - - - - - #p6#p7 - - + + + + + ──────────────────────────────────────────────────────────────────────────── + + My subtitle is on the left. + +  < Left ─────────────────────────────────────────────────────────────────── + + ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ + + My subtitle is centered + + ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ Centered! ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + My subtitle is on the right + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ Right >  + + + + + @@ -4348,7 +4671,7 @@ ''' # --- -# name: test_css_property[content_align.py] +# name: test_css_property[border_title_align.py] ''' @@ -4371,141 +4694,142 @@ font-weight: 700; } - .terminal-1585086532-matrix { + .terminal-2047325817-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1585086532-title { + .terminal-2047325817-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1585086532-r1 { fill: #c5c8c6 } - .terminal-1585086532-r2 { fill: #ffffff } - .terminal-1585086532-r3 { fill: #ffffff;font-style: italic; } - .terminal-1585086532-r4 { fill: #ffffff;font-weight: bold } + .terminal-2047325817-r1 { fill: #e1e1e1 } + .terminal-2047325817-r2 { fill: #c5c8c6 } + .terminal-2047325817-r3 { fill: #fea62b } + .terminal-2047325817-r4 { fill: #ffffff } + .terminal-2047325817-r5 { fill: #1e1e1e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ContentAlignApp + BorderTitleAlignApp - - - - - With content-align you can... - - - - - - - - - - ...Easily align content... - - - - - - - - - - - ...Horizontally and vertically! - + + + + +  < Left ─────────────────────────────────────────────────────────────────── + + My title is on the left. + + ──────────────────────────────────────────────────────────────────────────── + + ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ Centered! ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ + + My title is centered + + ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ Right >  + + My title is on the right + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + ''' # --- -# name: test_css_property[content_align_all.py] +# name: test_css_property[border_title_colors.py] ''' @@ -4528,140 +4852,142 @@ font-weight: 700; } - .terminal-269337742-matrix { + .terminal-2286355719-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-269337742-title { + .terminal-2286355719-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-269337742-r1 { fill: #dde6ed } - .terminal-269337742-r2 { fill: #c5c8c6 } - .terminal-269337742-r3 { fill: #e1e1e1 } + .terminal-2286355719-r1 { fill: #e1e1e1 } + .terminal-2286355719-r2 { fill: #c5c8c6 } + .terminal-2286355719-r3 { fill: #ff0000 } + .terminal-2286355719-r4 { fill: #008000;font-weight: bold } + .terminal-2286355719-r5 { fill: #ff00ff;font-style: italic; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - AllContentAlignApp + BorderTitleApp - - - - left topcenter topright top - - - - - - - - - - - left middlecenter middleright middle - - - - - - - - - - - - left bottomcenter bottomright bottom + + + + + + + + + +  Textual Rocks ━━━━━━━━━━━━━ + + + + + Hello, World! + + + + + ━━━━━━━━━━━━━ Textual Rocks  + + + + + + + ''' # --- -# name: test_css_property[display.py] +# name: test_css_property[box_sizing.py] ''' @@ -4684,155 +5010,155 @@ font-weight: 700; } - .terminal-3544266701-matrix { + .terminal-3266307003-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3544266701-title { + .terminal-3266307003-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3544266701-r1 { fill: #0000ff } - .terminal-3544266701-r2 { fill: #c5c8c6 } - .terminal-3544266701-r3 { fill: #ddeedd } + .terminal-3266307003-r1 { fill: #000000 } + .terminal-3266307003-r2 { fill: #c5c8c6 } + .terminal-3266307003-r3 { fill: #ccccff } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DisplayApp + BoxSizingApp - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Widget 1 - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Widget 3 - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - - - - - - - - - - - - - - - - - - ''' -# --- -# name: test_css_property[dock_all.py] - ''' - - - + + ''' +# --- +# name: test_css_property[color.py] + ''' + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DockAllApp + ColorApp - - - - - - - ────────────────────────────────────────────────────────── - top - - - - - - - leftright - - - - - - - - bottom - ────────────────────────────────────────────────────────── - - + + + + + + + I'm red! + + + + + + + + I'm rgb(0, 255, 0)! + + + + + + + + I'm hsl(240, 100%, 50%)! + + + @@ -4973,7 +5300,7 @@ ''' # --- -# name: test_css_property[grid.py] +# name: test_css_property[color_auto.py] ''' @@ -4996,141 +5323,143 @@ font-weight: 700; } - .terminal-2927206876-matrix { + .terminal-334364861-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2927206876-title { + .terminal-334364861-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2927206876-r1 { fill: #c5c8c6 } - .terminal-2927206876-r2 { fill: #e1e1e1 } - .terminal-2927206876-r3 { fill: #731077 } - .terminal-2927206876-r4 { fill: #161c1d } + .terminal-334364861-r1 { fill: #c5c8c6 } + .terminal-334364861-r2 { fill: #f6cdcd } + .terminal-334364861-r3 { fill: #2a2a01 } + .terminal-334364861-r4 { fill: #cdcdf6 } + .terminal-334364861-r5 { fill: #2a1f21 } + .terminal-334364861-r6 { fill: #cde1cd } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - GridApp + ColorApp - - - - - Grid cell 1Grid cell 2 - - row-span: 3; - column-span: 2; - - - Grid cell 3 - - - - - - Grid cell 4 - - - - - - Grid cell 5Grid cell 6Grid cell 7 - - - - - + + + + + The quick brown fox jumps over the lazy dog! + + + + + The quick brown fox jumps over the lazy dog! + + + + + The quick brown fox jumps over the lazy dog! + + + + + The quick brown fox jumps over the lazy dog! + + + + + The quick brown fox jumps over the lazy dog! + + + ''' # --- -# name: test_css_property[grid_columns.py] +# name: test_css_property[column_span.py] ''' @@ -5153,140 +5482,145 @@ font-weight: 700; } - .terminal-3216047084-matrix { + .terminal-3959176494-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3216047084-title { + .terminal-3959176494-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3216047084-r1 { fill: #ffffff } - .terminal-3216047084-r2 { fill: #c5c8c6 } - .terminal-3216047084-r3 { fill: #e1e1e1 } + .terminal-3959176494-r1 { fill: #c5c8c6 } + .terminal-3959176494-r2 { fill: #e8e0e7 } + .terminal-3959176494-r3 { fill: #eae3e5 } + .terminal-3959176494-r4 { fill: #ede6e6 } + .terminal-3959176494-r5 { fill: #efe9e4 } + .terminal-3959176494-r6 { fill: #efeedf } + .terminal-3959176494-r7 { fill: #e9eee5 } + .terminal-3959176494-r8 { fill: #e4eee8 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + MyApp - - - - ────────────────────────────────────────────────────────────────────── - 1frwidth = 162fr1frwidth = 16 - - - - - - - - - - ────────────────────────────────────────────────────────────────────── - ────────────────────────────────────────────────────────────────────── - 1frwidth = 162fr1frwidth = 16 - - - - - - - - - - ────────────────────────────────────────────────────────────────────── + + + + + + #p1 + + + + + + #p2#p3 + + + + + + #p4#p5 + + + + + + #p6#p7 + + + ''' # --- -# name: test_css_property[grid_gutter.py] +# name: test_css_property[content_align.py] ''' @@ -5309,150 +5643,151 @@ font-weight: 700; } - .terminal-2490521691-matrix { + .terminal-1585086532-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2490521691-title { + .terminal-1585086532-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2490521691-r1 { fill: #ffffff } - .terminal-2490521691-r2 { fill: #c5c8c6 } - .terminal-2490521691-r3 { fill: #e1e1e1 } + .terminal-1585086532-r1 { fill: #c5c8c6 } + .terminal-1585086532-r2 { fill: #ffffff } + .terminal-1585086532-r3 { fill: #ffffff;font-style: italic; } + .terminal-1585086532-r4 { fill: #ffffff;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + ContentAlignApp - - - - ────────────────────────────────────────────────────────────────────────── - - 12 - - ────────────────────────────────────────────────────────────────────────── - - ────────────────────────────────────────────────────────────────────────── - - 34 - - ────────────────────────────────────────────────────────────────────────── - - ────────────────────────────────────────────────────────────────────────── - - 56 - - ────────────────────────────────────────────────────────────────────────── - - ────────────────────────────────────────────────────────────────────────── - - 78 - - - ────────────────────────────────────────────────────────────────────────── - - - - - ''' -# --- -# name: test_css_property[grid_rows.py] - ''' - - - + + ''' +# --- +# name: test_css_property[content_align_all.py] + ''' + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + AllContentAlignApp - - - - ──────────────────────────────────────────────────────────────────────────── - 1fr1fr - ──────────────────────────────────────────────────────────────────────────── - ──────────────────────────────────────────────────────────────────────────── - - height = 6height = 6 - - - ──────────────────────────────────────────────────────────────────────────── - ──────────────────────────────────────────────────────────────────────────── - - 25%25% - - - ──────────────────────────────────────────────────────────────────────────── - ──────────────────────────────────────────────────────────────────────────── - 1fr1fr - ──────────────────────────────────────────────────────────────────────────── - ──────────────────────────────────────────────────────────────────────────── - - height = 6height = 6 - - - ──────────────────────────────────────────────────────────────────────────── + + + + left topcenter topright top + + + + + + + + + + + left middlecenter middleright middle + + + + + + + + + + + + left bottomcenter bottomright bottom ''' # --- -# name: test_css_property[grid_size_both.py] +# name: test_css_property[display.py] ''' @@ -5621,142 +5956,142 @@ font-weight: 700; } - .terminal-1098633890-matrix { + .terminal-3544266701-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1098633890-title { + .terminal-3544266701-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1098633890-r1 { fill: #ffffff } - .terminal-1098633890-r2 { fill: #c5c8c6 } - .terminal-1098633890-r3 { fill: #e1e1e1 } + .terminal-3544266701-r1 { fill: #0000ff } + .terminal-3544266701-r2 { fill: #c5c8c6 } + .terminal-3544266701-r3 { fill: #ddeedd } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + DisplayApp - - - - ──────────────────────────────────────────────────────────────────────────── - - 12 - - - ──────────────────────────────────────────────────────────────────────────── - ──────────────────────────────────────────────────────────────────────────── - - 34 - - - ──────────────────────────────────────────────────────────────────────────── - ────────────────────────────────────── - - 5 - - - ────────────────────────────────────── - - - - - - - - - - - ''' -# --- -# name: test_css_property[grid_size_columns.py] - ''' - + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Widget 1 + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Widget 3 + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_css_property[dock_all.py] + ''' + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + DockAllApp - - - - ──────────────────────────────────────────────────────────────────────────── - - - 12 - - - - ──────────────────────────────────────────────────────────────────────────── - ──────────────────────────────────────────────────────────────────────────── - - - 34 - - - - ──────────────────────────────────────────────────────────────────────────── - ────────────────────────────────────── - - - 5 - - - - ────────────────────────────────────── + + + + + + + ────────────────────────────────────────────────────────── + top + + + + + + + leftright + + + + + + + + bottom + ────────────────────────────────────────────────────────── + + + ''' # --- -# name: test_css_property[height.py] +# name: test_css_property[grid.py] ''' @@ -5933,149 +6268,150 @@ font-weight: 700; } - .terminal-3614144091-matrix { + .terminal-2927206876-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3614144091-title { + .terminal-2927206876-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3614144091-r1 { fill: #ffffff } - .terminal-3614144091-r2 { fill: #c5c8c6 } - .terminal-3614144091-r3 { fill: #e1e1e1 } + .terminal-2927206876-r1 { fill: #c5c8c6 } + .terminal-2927206876-r2 { fill: #e1e1e1 } + .terminal-2927206876-r3 { fill: #731077 } + .terminal-2927206876-r4 { fill: #161c1d } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - HeightApp + GridApp - - - - Widget - - - - - - - - - - - - - - - - - - - - - - - - - - - - ''' -# --- -# name: test_css_property[height_comparison.py] - ''' - - - + + ''' +# --- +# name: test_css_property[grid_columns.py] + ''' + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - HeightComparisonApp + MyApp - - - - #cells· - · - · - #percent· - - · - #w· - · - · - - #h· - · - · - · - #vw - · - · - · - #vh· - - #auto· - #fr1· - #fr2· - · + + + + ────────────────────────────────────────────────────────────────────── + 1frwidth = 162fr1frwidth = 16 + + + + + + + + + + ────────────────────────────────────────────────────────────────────── + ────────────────────────────────────────────────────────────────────── + 1frwidth = 162fr1frwidth = 16 + + + + + + + + + + ────────────────────────────────────────────────────────────────────── ''' # --- -# name: test_css_property[layout.py] +# name: test_css_property[grid_gutter.py] ''' @@ -6253,142 +6581,140 @@ font-weight: 700; } - .terminal-2838975926-matrix { + .terminal-2490521691-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2838975926-title { + .terminal-2490521691-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2838975926-r1 { fill: #efddef } - .terminal-2838975926-r2 { fill: #c5c8c6 } - .terminal-2838975926-r3 { fill: #000000 } - .terminal-2838975926-r4 { fill: #ddefef } - .terminal-2838975926-r5 { fill: #e1e1e1 } + .terminal-2490521691-r1 { fill: #ffffff } + .terminal-2490521691-r2 { fill: #c5c8c6 } + .terminal-2490521691-r3 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - LayoutApp + MyApp - - - - - Layout - - Is - - Vertical - - - LayoutIsHorizontal - - - - - - - - - - - - - - - + + + + ────────────────────────────────────────────────────────────────────────── + + 12 + + ────────────────────────────────────────────────────────────────────────── + + ────────────────────────────────────────────────────────────────────────── + + 34 + + ────────────────────────────────────────────────────────────────────────── + + ────────────────────────────────────────────────────────────────────────── + + 56 + + ────────────────────────────────────────────────────────────────────────── + + ────────────────────────────────────────────────────────────────────────── + + 78 + + + ────────────────────────────────────────────────────────────────────────── ''' # --- -# name: test_css_property[link_background.py] +# name: test_css_property[grid_rows.py] ''' @@ -6411,142 +6737,140 @@ font-weight: 700; } - .terminal-687058265-matrix { + .terminal-986618502-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-687058265-title { + .terminal-986618502-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-687058265-r1 { fill: #e1e1e1 } - .terminal-687058265-r2 { fill: #c5c8c6 } - .terminal-687058265-r3 { fill: #ffdddd;text-decoration: underline; } - .terminal-687058265-r4 { fill: #121201;text-decoration: underline; } - .terminal-687058265-r5 { fill: #ddedf9;text-decoration: underline; } + .terminal-986618502-r1 { fill: #ffffff } + .terminal-986618502-r2 { fill: #c5c8c6 } + .terminal-986618502-r3 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - LinkBackgroundApp + MyApp - - - - Visit the Textualize website. - Click here for the bell sound. - You can also click here for the bell sound. - Exit this application. - - - - - - - - - - - - - - - - - - - - + + + + ──────────────────────────────────────────────────────────────────────────── + 1fr1fr + ──────────────────────────────────────────────────────────────────────────── + ──────────────────────────────────────────────────────────────────────────── + + height = 6height = 6 + + + ──────────────────────────────────────────────────────────────────────────── + ──────────────────────────────────────────────────────────────────────────── + + 25%25% + + + ──────────────────────────────────────────────────────────────────────────── + ──────────────────────────────────────────────────────────────────────────── + 1fr1fr + ──────────────────────────────────────────────────────────────────────────── + ──────────────────────────────────────────────────────────────────────────── + + height = 6height = 6 + + + ──────────────────────────────────────────────────────────────────────────── ''' # --- -# name: test_css_property[link_color.py] +# name: test_css_property[grid_size_both.py] ''' @@ -6569,155 +6893,153 @@ font-weight: 700; } - .terminal-3021056461-matrix { + .terminal-1098633890-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3021056461-title { + .terminal-1098633890-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3021056461-r1 { fill: #e1e1e1 } - .terminal-3021056461-r2 { fill: #c5c8c6 } - .terminal-3021056461-r3 { fill: #ff0000;text-decoration: underline; } - .terminal-3021056461-r4 { fill: #8e8e0f;text-decoration: underline; } - .terminal-3021056461-r5 { fill: #0178d4;text-decoration: underline; } + .terminal-1098633890-r1 { fill: #ffffff } + .terminal-1098633890-r2 { fill: #c5c8c6 } + .terminal-1098633890-r3 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - LinkColorApp + MyApp - - - - Visit the Textualize website. - Click here for the bell sound. - You can also click here for the bell sound. - Exit this application. - - - - - - - - - - - - - - - - - - - - - - - - - ''' -# --- -# name: test_css_property[link_hover_background.py] - ''' - - - + + ''' +# --- +# name: test_css_property[grid_size_columns.py] + ''' + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - LinkHoverBackgroundApp + MyApp - - - - Visit the Textualize website. - Click here for the bell sound. - You can also click here for the bell sound. - Exit this application. - - - - - - - - - - - - - - - - - - - - + + + + ──────────────────────────────────────────────────────────────────────────── + + + 12 + + + + ──────────────────────────────────────────────────────────────────────────── + ──────────────────────────────────────────────────────────────────────────── + + + 34 + + + + ──────────────────────────────────────────────────────────────────────────── + ────────────────────────────────────── + + + 5 + + + + ────────────────────────────────────── ''' # --- -# name: test_css_property[link_hover_color.py] +# name: test_css_property[height.py] ''' @@ -6883,140 +7205,140 @@ font-weight: 700; } - .terminal-3576933835-matrix { + .terminal-3614144091-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3576933835-title { + .terminal-3614144091-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3576933835-r1 { fill: #e1e1e1 } - .terminal-3576933835-r2 { fill: #c5c8c6 } - .terminal-3576933835-r3 { fill: #e1e1e1;text-decoration: underline; } + .terminal-3614144091-r1 { fill: #ffffff } + .terminal-3614144091-r2 { fill: #c5c8c6 } + .terminal-3614144091-r3 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - LinkHoverColorApp + HeightApp - - - - Visit the Textualize website. - Click here for the bell sound. - You can also click here for the bell sound. - Exit this application. - - - - - - - - - - - - - - - - - - - - - - - - - ''' -# --- -# name: test_css_property[link_hover_style.py] + + + + Widget + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_css_property[height_comparison.py] ''' @@ -7039,140 +7361,148 @@ font-weight: 700; } - .terminal-3588337117-matrix { + .terminal-3573285936-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3588337117-title { + .terminal-3573285936-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3588337117-r1 { fill: #e1e1e1 } - .terminal-3588337117-r2 { fill: #c5c8c6 } - .terminal-3588337117-r3 { fill: #e1e1e1;text-decoration: underline; } + .terminal-3573285936-r1 { fill: #c5c8c6 } + .terminal-3573285936-r2 { fill: #e8e0e7 } + .terminal-3573285936-r3 { fill: #ddedf9 } + .terminal-3573285936-r4 { fill: #eae3e5 } + .terminal-3573285936-r5 { fill: #ede6e6 } + .terminal-3573285936-r6 { fill: #efe9e4 } + .terminal-3573285936-r7 { fill: #efeedf } + .terminal-3573285936-r8 { fill: #e9eee5 } + .terminal-3573285936-r9 { fill: #e4eee8 } + .terminal-3573285936-r10 { fill: #e2edeb } + .terminal-3573285936-r11 { fill: #dfebed } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - LinkHoverStyleApp + HeightComparisonApp - - - - Visit the Textualize website. - Click here for the bell sound. - You can also click here for the bell sound. - Exit this application. - - - - - - - - - - - - - - - - - - - - + + + + #cells· + · + · + #percent· + + · + #w· + · + · + + #h· + · + · + · + #vw + · + · + · + #vh· + + #auto· + #fr1· + #fr2· + · ''' # --- -# name: test_css_property[link_style.py] +# name: test_css_property[layout.py] ''' @@ -7195,142 +7525,142 @@ font-weight: 700; } - .terminal-2410786847-matrix { + .terminal-2838975926-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2410786847-title { + .terminal-2838975926-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2410786847-r1 { fill: #e1e1e1 } - .terminal-2410786847-r2 { fill: #c5c8c6 } - .terminal-2410786847-r3 { fill: #e1e1e1;font-weight: bold;font-style: italic; } - .terminal-2410786847-r4 { fill: #1e1e1e;text-decoration: line-through; } - .terminal-2410786847-r5 { fill: #e1e1e1;font-weight: bold } + .terminal-2838975926-r1 { fill: #efddef } + .terminal-2838975926-r2 { fill: #c5c8c6 } + .terminal-2838975926-r3 { fill: #000000 } + .terminal-2838975926-r4 { fill: #ddefef } + .terminal-2838975926-r5 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - LinkStyleApp + LayoutApp - - - - Visit the Textualize website. - Click here for the bell sound. - You can also click here for the bell sound. - Exit this application. - - - - - - - - - - - - - - - - - - - - - - - - - ''' -# --- -# name: test_css_property[links.py] + + + + + Layout + + Is + + Vertical + + + LayoutIsHorizontal + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_css_property[link_background.py] ''' @@ -7353,133 +7683,134 @@ font-weight: 700; } - .terminal-3461676208-matrix { + .terminal-687058265-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3461676208-title { + .terminal-687058265-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3461676208-r1 { fill: #e1e1e1 } - .terminal-3461676208-r2 { fill: #e1e1e1;text-decoration: underline; } - .terminal-3461676208-r3 { fill: #c5c8c6 } - .terminal-3461676208-r4 { fill: #030e19;font-weight: bold;font-style: italic;;text-decoration: underline; } + .terminal-687058265-r1 { fill: #e1e1e1 } + .terminal-687058265-r2 { fill: #c5c8c6 } + .terminal-687058265-r3 { fill: #ffdddd;text-decoration: underline; } + .terminal-687058265-r4 { fill: #121201;text-decoration: underline; } + .terminal-687058265-r5 { fill: #ddedf9;text-decoration: underline; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - LinksApp + LinkBackgroundApp - - - - Here is a link which you can click! - - Here is a link which you can click! - - - - - - - - - - - - - - - - - - - - + + + + Visit the Textualize website. + Click here for the bell sound. + You can also click here for the bell sound. + Exit this application. + + + + + + + + + + + + + + + + + + + @@ -7487,7 +7818,7 @@ ''' # --- -# name: test_css_property[margin.py] +# name: test_css_property[link_color.py] ''' @@ -7510,148 +7841,149 @@ font-weight: 700; } - .terminal-3234129636-matrix { + .terminal-3021056461-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3234129636-title { + .terminal-3021056461-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3234129636-r1 { fill: #000000 } - .terminal-3234129636-r2 { fill: #c5c8c6 } - .terminal-3234129636-r3 { fill: #0000ff } - .terminal-3234129636-r4 { fill: #ccccff } + .terminal-3021056461-r1 { fill: #e1e1e1 } + .terminal-3021056461-r2 { fill: #c5c8c6 } + .terminal-3021056461-r3 { fill: #ff0000;text-decoration: underline; } + .terminal-3021056461-r4 { fill: #8e8e0f;text-decoration: underline; } + .terminal-3021056461-r5 { fill: #0178d4;text-decoration: underline; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MarginApp + LinkColorApp - - - - - - - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total obliteration. - I will face my fear. - I will permit it to pass over me and through me. - And when it has gone past, I will turn the inner eye to see  - its path. - Where the fear has gone there will be nothing. Only I will  - remain. - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - - - - - - - - - - - - - ''' -# --- -# name: test_css_property[margin_all.py] - ''' - - - + + ''' +# --- +# name: test_css_property[link_hover_background.py] + ''' + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MarginAllApp + LinkHoverBackgroundApp - - - - ────────────────────────────────────────────────────────────────── - - - - marginmargin: 1  - no marginmargin: 1: 1 51 2 6 - - - - - ────────────────────────────────────────────────────────────────── - - ────────────────────────────────────────────────────────────────── - - - margin-bottom: 4 - - margin-right: margin-left: 3 - 3 - margin-top: 4 - - - - ────────────────────────────────────────────────────────────────── + + + + Visit the Textualize website. + Click here for the bell sound. + You can also click here for the bell sound. + Exit this application. + + + + + + + + + + + + + + + + + + + + ''' # --- -# name: test_css_property[max_height.py] +# name: test_css_property[link_hover_color.py] ''' @@ -7831,142 +8155,140 @@ font-weight: 700; } - .terminal-3102345871-matrix { + .terminal-3576933835-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3102345871-title { + .terminal-3576933835-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3102345871-r1 { fill: #c5c8c6 } - .terminal-3102345871-r2 { fill: #e8e0e7 } - .terminal-3102345871-r3 { fill: #efe9e4 } - .terminal-3102345871-r4 { fill: #ede6e6 } - .terminal-3102345871-r5 { fill: #eae3e5 } + .terminal-3576933835-r1 { fill: #e1e1e1 } + .terminal-3576933835-r2 { fill: #c5c8c6 } + .terminal-3576933835-r3 { fill: #e1e1e1;text-decoration: underline; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MaxHeightApp + LinkHoverColorApp - - - - - - - max-height: 10w - max-height: 10 - max-height: 50% - - - - - - max-height: 999 - - - - - - - - - - - - - - + + + + Visit the Textualize website. + Click here for the bell sound. + You can also click here for the bell sound. + Exit this application. + + + + + + + + + + + + + + + + + + + + + + ''' # --- -# name: test_css_property[max_width.py] +# name: test_css_property[link_hover_style.py] ''' @@ -7989,134 +8311,132 @@ font-weight: 700; } - .terminal-1398959741-matrix { + .terminal-3588337117-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1398959741-title { + .terminal-3588337117-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1398959741-r1 { fill: #c5c8c6 } - .terminal-1398959741-r2 { fill: #e8e0e7 } - .terminal-1398959741-r3 { fill: #eae3e5 } - .terminal-1398959741-r4 { fill: #ede6e6 } - .terminal-1398959741-r5 { fill: #efe9e4 } + .terminal-3588337117-r1 { fill: #e1e1e1 } + .terminal-3588337117-r2 { fill: #c5c8c6 } + .terminal-3588337117-r3 { fill: #e1e1e1;text-decoration: underline; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MaxWidthApp + LinkHoverStyleApp - - - - - - max-width:  - 50h - - - - - max-width: 999 - - - - - - max-width: 50% - - - - - - max-width: 30 - - + + + + Visit the Textualize website. + Click here for the bell sound. + You can also click here for the bell sound. + Exit this application. + + + + + + + + + + + + + + + + + + + @@ -8124,7 +8444,7 @@ ''' # --- -# name: test_css_property[min_height.py] +# name: test_css_property[link_style.py] ''' @@ -8147,153 +8467,151 @@ font-weight: 700; } - .terminal-3637655391-matrix { + .terminal-2410786847-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3637655391-title { + .terminal-2410786847-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3637655391-r1 { fill: #c5c8c6 } - .terminal-3637655391-r2 { fill: #e1e1e1 } - .terminal-3637655391-r3 { fill: #e8e0e7 } - .terminal-3637655391-r4 { fill: #eae3e5 } - .terminal-3637655391-r5 { fill: #ede6e6 } - .terminal-3637655391-r6 { fill: #efe9e4 } - .terminal-3637655391-r7 { fill: #14191f } + .terminal-2410786847-r1 { fill: #e1e1e1 } + .terminal-2410786847-r2 { fill: #c5c8c6 } + .terminal-2410786847-r3 { fill: #e1e1e1;font-weight: bold;font-style: italic; } + .terminal-2410786847-r4 { fill: #1e1e1e;text-decoration: line-through; } + .terminal-2410786847-r5 { fill: #e1e1e1;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MinHeightApp + LinkStyleApp - - - - - - - - - min-height: 25% - - - min-height: 75% - - - - - - min-height: 30 - min-height: 40w - - - ▃▃ - - - - - - - - - - ''' -# --- -# name: test_css_property[min_width.py] - ''' - - - + + ''' +# --- +# name: test_css_property[links.py] + ''' + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MinWidthApp + LinksApp - - - - - - min-width: 25% - - - - - min-width: 75% - - - - - - min-width: 100 - - - - - - min-width: 400h - - - + + + + Here is a link which you can click! + + Here is a link which you can click! + + + + + + + + + + + + + + + + + + + + @@ -8443,7 +8759,7 @@ ''' # --- -# name: test_css_property[offset.py] +# name: test_css_property[margin.py] ''' @@ -8466,142 +8782,141 @@ font-weight: 700; } - .terminal-292160688-matrix { + .terminal-3234129636-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-292160688-title { + .terminal-3234129636-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-292160688-r1 { fill: #000000 } - .terminal-292160688-r2 { fill: #0000ff } - .terminal-292160688-r3 { fill: #c5c8c6 } - .terminal-292160688-r4 { fill: #ff0000 } - .terminal-292160688-r5 { fill: #008000 } + .terminal-3234129636-r1 { fill: #000000 } + .terminal-3234129636-r2 { fill: #c5c8c6 } + .terminal-3234129636-r3 { fill: #0000ff } + .terminal-3234129636-r4 { fill: #ccccff } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OffsetApp + MarginApp - - - - - Chani (offset 0  - ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀-3) - - - - Paul (offset 8 2)▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - - - - ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - - - Duncan (offset 4  - 10) - - - - ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - - - - - - - - - ''' + + + + + + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration. + I will face my fear. + I will permit it to pass over me and through me. + And when it has gone past, I will turn the inner eye to see  + its path. + Where the fear has gone there will be nothing. Only I will  + remain. + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + + + + + + + + + + + + ''' # --- -# name: test_css_property[opacity.py] +# name: test_css_property[margin_all.py] ''' @@ -8624,148 +8939,148 @@ font-weight: 700; } - .terminal-2483518667-matrix { + .terminal-2245771963-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2483518667-title { + .terminal-2245771963-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2483518667-r1 { fill: #000000 } - .terminal-2483518667-r2 { fill: #c5c8c6 } - .terminal-2483518667-r3 { fill: #000000;font-weight: bold } - .terminal-2483518667-r4 { fill: #01090f } - .terminal-2483518667-r5 { fill: #373838;font-weight: bold } - .terminal-2483518667-r6 { fill: #07243f } - .terminal-2483518667-r7 { fill: #6f7474;font-weight: bold } - .terminal-2483518667-r8 { fill: #10518f } - .terminal-2483518667-r9 { fill: #a8b3b2;font-weight: bold } - .terminal-2483518667-r10 { fill: #1e90ff } - .terminal-2483518667-r11 { fill: #e2f4f3;font-weight: bold } + .terminal-2245771963-r1 { fill: #ffffff } + .terminal-2245771963-r2 { fill: #c5c8c6 } + .terminal-2245771963-r3 { fill: #e0e0e0 } + .terminal-2245771963-r4 { fill: #ece5e5 } + .terminal-2245771963-r5 { fill: #eee8e3 } + .terminal-2245771963-r6 { fill: #e7e0e6 } + .terminal-2245771963-r7 { fill: #eae2e4 } + .terminal-2245771963-r8 { fill: #e3ede7 } + .terminal-2245771963-r9 { fill: #e8ede4 } + .terminal-2245771963-r10 { fill: #e1eceb } + .terminal-2245771963-r11 { fill: #eeeddf } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OpacityApp + MarginAllApp - - - - ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - opacity: 0% - - ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - - opacity: 25% - - ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - - opacity: 50% - - ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - - opacity: 75% - - ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - - opacity: 100% - - ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ + + + + ────────────────────────────────────────────────────────────────── + + + + marginmargin: 1  + no marginmargin: 1: 1 51 2 6 + + + + + ────────────────────────────────────────────────────────────────── + + ────────────────────────────────────────────────────────────────── + + + margin-bottom: 4 + + margin-right: margin-left: 3 + 3 + margin-top: 4 + + + + ────────────────────────────────────────────────────────────────── ''' # --- -# name: test_css_property[outline.py] +# name: test_css_property[max_height.py] ''' @@ -8788,141 +9103,142 @@ font-weight: 700; } - .terminal-3556982422-matrix { + .terminal-3102345871-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3556982422-title { + .terminal-3102345871-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3556982422-r1 { fill: #000000 } - .terminal-3556982422-r2 { fill: #c5c8c6 } - .terminal-3556982422-r3 { fill: #008000 } - .terminal-3556982422-r4 { fill: #cce5cc } + .terminal-3102345871-r1 { fill: #c5c8c6 } + .terminal-3102345871-r2 { fill: #e8e0e7 } + .terminal-3102345871-r3 { fill: #efe9e4 } + .terminal-3102345871-r4 { fill: #ede6e6 } + .terminal-3102345871-r5 { fill: #eae3e5 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OutlineApp + MaxHeightApp - - - - - - - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ear is the mind-killer. - ear is the little-death that brings total obliteration. -  will face my fear. -  will permit it to pass over me and through me. - nd when it has gone past, I will turn the inner eye to see its - ath. - here the fear has gone there will be nothing. Only I will  - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - - - - - - - - - - + + + + + + + max-height: 10w + max-height: 10 + max-height: 50% + + + + + + max-height: 999 + + + + + + + + + + + + ''' # --- -# name: test_css_property[outline_all.py] +# name: test_css_property[max_width.py] ''' @@ -8945,133 +9261,134 @@ font-weight: 700; } - .terminal-3019471504-matrix { + .terminal-1398959741-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3019471504-title { + .terminal-1398959741-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3019471504-r1 { fill: #c5c8c6 } - .terminal-3019471504-r2 { fill: #0178d4 } - .terminal-3019471504-r3 { fill: #e1e1e1 } - .terminal-3019471504-r4 { fill: #1e1e1e } + .terminal-1398959741-r1 { fill: #c5c8c6 } + .terminal-1398959741-r2 { fill: #e8e0e7 } + .terminal-1398959741-r3 { fill: #eae3e5 } + .terminal-1398959741-r4 { fill: #ede6e6 } + .terminal-1398959741-r5 { fill: #efe9e4 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - AllOutlinesApp + MaxWidthApp - - - - +------------------+╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ - |ascii|blankdashed - +------------------+╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ - - - ══════════════════━━━━━━━━━━━━━━━━━━ - doubleheavyhidden/none - ══════════════════━━━━━━━━━━━━━━━━━━ - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - hkeyinnernone - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - - - ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀──────────────────────────────────── - outerroundsolid - ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄──────────────────────────────────── - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - tallvkeywide - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + + + + max-width:  + 50h + + + + + max-width: 999 + + + + + + max-width: 50% + + + + + + max-width: 30 + + @@ -9079,7 +9396,7 @@ ''' # --- -# name: test_css_property[outline_vs_border.py] +# name: test_css_property[min_height.py] ''' @@ -9102,152 +9419,155 @@ font-weight: 700; } - .terminal-1454950069-matrix { + .terminal-3637655391-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1454950069-title { + .terminal-3637655391-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1454950069-r1 { fill: #b93c5b } - .terminal-1454950069-r2 { fill: #e1e1e1 } - .terminal-1454950069-r3 { fill: #c5c8c6 } - .terminal-1454950069-r4 { fill: #4ebf71 } + .terminal-3637655391-r1 { fill: #c5c8c6 } + .terminal-3637655391-r2 { fill: #e1e1e1 } + .terminal-3637655391-r3 { fill: #e8e0e7 } + .terminal-3637655391-r4 { fill: #eae3e5 } + .terminal-3637655391-r5 { fill: #ede6e6 } + .terminal-3637655391-r6 { fill: #efe9e4 } + .terminal-3637655391-r7 { fill: #14191f } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OutlineBorderApp + MinHeightApp - - - - ─────────────────────────────────────────────────────────────────── - ear is the mind-killer. - ear is the little-death that brings total obliteration. -  will face my fear. -  will permit it to pass over me and through me. - nd when it has gone past, I will turn the inner eye to see its path - here the fear has gone there will be nothing. Only I will remain. - ─────────────────────────────────────────────────────────────────── - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total obliteration. - I will face my fear. - I will permit it to pass over me and through me. - And when it has gone past, I will turn the inner eye to see its path. - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ───────────────────────────────────────────────────────────────────── - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total obliteration. - I will face my fear. - I will permit it to pass over me and through me. - And when it has gone past, I will turn the inner eye to see its path. - ───────────────────────────────────────────────────────────────────── - - - - - ''' -# --- -# name: test_css_property[overflow.py] - ''' - - - + + ''' +# --- +# name: test_css_property[min_width.py] + ''' + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OverflowApp + MinWidthApp - - - - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - I must not fear.I must not fear. - Fear is the mind-killer.Fear is the mind-killer. - Fear is the little-death that Fear is the little-death that  - brings total obliteration.brings total obliteration. - I will face my fear.I will face my fear. - I will permit it to pass over meI will permit it to pass over me  - and through me.and through me. - And when it has gone past, I And when it has gone past, I will  - will turn the inner eye to see turn the inner eye to see its  - its path.▁▁path. - Where the fear has gone there Where the fear has gone there will - will be nothing. Only I will be nothing. Only I will remain. - remain.▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁I must not fear. - I must not fear.Fear is the mind-killer. - Fear is the mind-killer.Fear is the little-death that  - Fear is the little-death that brings total obliteration. - brings total obliteration.I will face my fear. - I will face my fear.I will permit it to pass over me  - I will permit it to pass over meand through me. + + + + + + min-width: 25% + + + + + min-width: 75% + + + + + + min-width: 100 + + + + + + min-width: 400h + + + + ''' # --- -# name: test_css_property[padding.py] +# name: test_css_property[offset.py] ''' @@ -9418,139 +9738,142 @@ font-weight: 700; } - .terminal-3291669704-matrix { + .terminal-292160688-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3291669704-title { + .terminal-292160688-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3291669704-r1 { fill: #c5c8c6 } - .terminal-3291669704-r2 { fill: #0000ff } + .terminal-292160688-r1 { fill: #000000 } + .terminal-292160688-r2 { fill: #0000ff } + .terminal-292160688-r3 { fill: #c5c8c6 } + .terminal-292160688-r4 { fill: #ff0000 } + .terminal-292160688-r5 { fill: #008000 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - PaddingApp + OffsetApp - - - - - - - - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total obliteration. - I will face my fear. - I will permit it to pass over me and through me. - And when it has gone past, I will turn the inner eye to see its  - path. - Where the fear has gone there will be nothing. Only I will  - remain. - - - - - - - - - - - - + + + + + Chani (offset 0  + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀-3) + + + + Paul (offset 8 2)▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ + + + + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ + + + Duncan (offset 4  + 10) + + + + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ + + + + + ''' # --- -# name: test_css_property[padding_all.py] +# name: test_css_property[opacity.py] ''' @@ -9573,146 +9896,148 @@ font-weight: 700; } - .terminal-1642992271-matrix { + .terminal-2483518667-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1642992271-title { + .terminal-2483518667-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1642992271-r1 { fill: #c5c8c6 } - .terminal-1642992271-r2 { fill: #e7e0e6 } - .terminal-1642992271-r3 { fill: #eae2e4 } - .terminal-1642992271-r4 { fill: #ece5e5 } - .terminal-1642992271-r5 { fill: #eee8e3 } - .terminal-1642992271-r6 { fill: #e8ede4 } - .terminal-1642992271-r7 { fill: #e3ede7 } - .terminal-1642992271-r8 { fill: #e1eceb } - .terminal-1642992271-r9 { fill: #eeeddf } + .terminal-2483518667-r1 { fill: #000000 } + .terminal-2483518667-r2 { fill: #c5c8c6 } + .terminal-2483518667-r3 { fill: #000000;font-weight: bold } + .terminal-2483518667-r4 { fill: #01090f } + .terminal-2483518667-r5 { fill: #373838;font-weight: bold } + .terminal-2483518667-r6 { fill: #07243f } + .terminal-2483518667-r7 { fill: #6f7474;font-weight: bold } + .terminal-2483518667-r8 { fill: #10518f } + .terminal-2483518667-r9 { fill: #a8b3b2;font-weight: bold } + .terminal-2483518667-r10 { fill: #1e90ff } + .terminal-2483518667-r11 { fill: #e2f4f3;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - PaddingAllApp + OpacityApp - - - - no padding - padding: 1padding:padding: 1 1 - 1 52 6 - - - - - - - - - - padding-right: 3padding-bottom: 4padding-left: 3 - - - - padding-top: 4 - - - - - - - + + + + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + opacity: 0% + + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + + opacity: 25% + + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + + opacity: 50% + + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + + opacity: 75% + + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + + opacity: 100% + + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ''' # --- -# name: test_css_property[row_span.py] +# name: test_css_property[outline.py] ''' @@ -9735,145 +10060,141 @@ font-weight: 700; } - .terminal-3799423623-matrix { + .terminal-3556982422-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3799423623-title { + .terminal-3556982422-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3799423623-r1 { fill: #c5c8c6 } - .terminal-3799423623-r2 { fill: #efe9e4 } - .terminal-3799423623-r3 { fill: #ede6e6 } - .terminal-3799423623-r4 { fill: #eae3e5 } - .terminal-3799423623-r5 { fill: #e8e0e7 } - .terminal-3799423623-r6 { fill: #efeedf } - .terminal-3799423623-r7 { fill: #e9eee5 } - .terminal-3799423623-r8 { fill: #e4eee8 } + .terminal-3556982422-r1 { fill: #000000 } + .terminal-3556982422-r2 { fill: #c5c8c6 } + .terminal-3556982422-r3 { fill: #008000 } + .terminal-3556982422-r4 { fill: #cce5cc } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + OutlineApp - - - - - - #p4 - - - #p3 - - - #p2 - - - #p1 - - - #p5 - - - #p6 - - - #p7 - - - - - - + + + + + + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ear is the mind-killer. + ear is the little-death that brings total obliteration. +  will face my fear. +  will permit it to pass over me and through me. + nd when it has gone past, I will turn the inner eye to see its + ath. + here the fear has gone there will be nothing. Only I will  + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + + + + + + + + + + + + ''' # --- -# name: test_css_property[scrollbar_corner_color.py] +# name: test_css_property[outline_all.py] ''' @@ -9896,132 +10217,133 @@ font-weight: 700; } - .terminal-3484348706-matrix { + .terminal-3019471504-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3484348706-title { + .terminal-3019471504-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3484348706-r1 { fill: #e1e1e1 } - .terminal-3484348706-r2 { fill: #c5c8c6 } - .terminal-3484348706-r3 { fill: #14191f } + .terminal-3019471504-r1 { fill: #c5c8c6 } + .terminal-3019471504-r2 { fill: #0178d4 } + .terminal-3019471504-r3 { fill: #e1e1e1 } + .terminal-3019471504-r4 { fill: #1e1e1e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ScrollbarCornerColorApp + AllOutlinesApp - - - - I must not fear. Fear is the mind-killer. Fear is the little-death that brings - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total obliteration. - I will face my fear. - I will permit it to pass over me and through me. - And when it has gone past, I will turn the inner eye to see its path. - Where the fear has gone there will be nothing. Only I will remain.▅▅ - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total obliteration. - I will face my fear. - I will permit it to pass over me and through me. - And when it has gone past, I will turn the inner eye to see its path. - Where the fear has gone there will be nothing. Only I will remain. - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total obliteration. - I will face my fear. - I will permit it to pass over me and through me. - And when it has gone past, I will turn the inner eye to see its path. - Where the fear has gone there will be nothing. Only I will remain. - I must not fear. + + + + +------------------+╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ + |ascii|blankdashed + +------------------+╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ + + + ══════════════════━━━━━━━━━━━━━━━━━━ + doubleheavyhidden/none + ══════════════════━━━━━━━━━━━━━━━━━━ + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ + hkeyinnernone + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + + + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀──────────────────────────────────── + outerroundsolid + ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄──────────────────────────────────── + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + tallvkeywide + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ @@ -10029,7 +10351,7 @@ ''' # --- -# name: test_css_property[scrollbar_gutter.py] +# name: test_css_property[outline_vs_border.py] ''' @@ -10052,154 +10374,155 @@ font-weight: 700; } - .terminal-2133906435-matrix { + .terminal-1454950069-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2133906435-title { + .terminal-1454950069-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2133906435-r1 { fill: #fffaf0 } - .terminal-2133906435-r2 { fill: #c5c8c6 } - .terminal-2133906435-r3 { fill: #e1e1e1 } + .terminal-1454950069-r1 { fill: #b93c5b } + .terminal-1454950069-r2 { fill: #e1e1e1 } + .terminal-1454950069-r3 { fill: #c5c8c6 } + .terminal-1454950069-r4 { fill: #4ebf71 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ScrollbarGutterApp + OutlineBorderApp - - - - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total obliteration. - I will face my fear. - I will permit it to pass over me and through me. - And when it has gone past, I will turn the inner eye to see its path. - Where the fear has gone there will be nothing. Only I will remain. - - - - - - - - - - - - - - - - - - - - - - ''' -# --- -# name: test_css_property[scrollbar_size.py] - ''' - - - + + ''' +# --- +# name: test_css_property[overflow.py] + ''' + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ScrollbarApp + OverflowApp - - - - - - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total obliteration.▁▁▁▁ - I will face my fear. - I will permit it to pass over me and through me. - And when it has gone past, I will turn the inner eye to see its path. - Where the fear has gone there will be nothing. Only I will remain. - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total obliteration. - I will face my fear. - - - - - - - - - - - + + + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + I must not fear.I must not fear. + Fear is the mind-killer.Fear is the mind-killer. + Fear is the little-death that Fear is the little-death that  + brings total obliteration.brings total obliteration. + I will face my fear.I will face my fear. + I will permit it to pass over meI will permit it to pass over me  + and through me.and through me. + And when it has gone past, I And when it has gone past, I will  + will turn the inner eye to see turn the inner eye to see its  + its path.▁▁path. + Where the fear has gone there Where the fear has gone there will + will be nothing. Only I will be nothing. Only I will remain. + remain.▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁I must not fear. + I must not fear.Fear is the mind-killer. + Fear is the mind-killer.Fear is the little-death that  + Fear is the little-death that brings total obliteration. + brings total obliteration.I will face my fear. + I will face my fear.I will permit it to pass over me  + I will permit it to pass over meand through me. ''' # --- -# name: test_css_property[scrollbar_size2.py] +# name: test_css_property[padding.py] ''' @@ -10364,143 +10690,139 @@ font-weight: 700; } - .terminal-709381566-matrix { + .terminal-3291669704-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-709381566-title { + .terminal-3291669704-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-709381566-r1 { fill: #e7e0e0 } - .terminal-709381566-r2 { fill: #c5c8c6 } - .terminal-709381566-r3 { fill: #e0e4e0 } - .terminal-709381566-r4 { fill: #e0e0e7 } - .terminal-709381566-r5 { fill: #14191f } - .terminal-709381566-r6 { fill: #23568b } + .terminal-3291669704-r1 { fill: #c5c8c6 } + .terminal-3291669704-r2 { fill: #0000ff } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ScrollbarApp + PaddingApp - - - - I must not fear.I must not fear.I must not fear. - Fear is the mind-killer.Fear is the mind-killer.Fear is the mind-killer. - Fear is the little-death Fear is the little-death tFear is the little-death  - I will face my fear.I will face my fear.I will face my fear. - I will permit it to pass I will permit it to pass oI will permit it to pass  - And when it has gone pastAnd when it has gone past,And when it has gone past - Where the fear has gone tWhere the fear has gone thWhere the fear has gone t - I must not fear.I must not fear.I must not fear. - Fear is the mind-killer.Fear is the mind-killer.Fear is the mind-killer. - Fear is the little-death Fear is the little-death tFear is the little-death  - I will face my fear.I will face my fear.I will face my fear.▇▇ - I will permit it to pass I will permit it to pass oI will permit it to pass  - And when it has gone pastAnd when it has gone past,And when it has gone past - Where the fear has gone tWhere the fear has gone thWhere the fear has gone t - I must not fear.I must not fear.I must not fear. - Fear is the mind-killer.Fear is the mind-killer.Fear is the mind-killer. - Fear is the little-death Fear is the little-death tFear is the little-death  - I will face my fear.I will face my fear.I will face my fear. - I will permit it to pass I will permit it to pass oI will permit it to pass  - And when it has gone past, - Where the fear has gone th - I must not fear. - Fear is the mind-killer. - + + + + + + + + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration. + I will face my fear. + I will permit it to pass over me and through me. + And when it has gone past, I will turn the inner eye to see its  + path. + Where the fear has gone there will be nothing. Only I will  + remain. + + + + + + + + + + + ''' # --- -# name: test_css_property[scrollbars.py] +# name: test_css_property[padding_all.py] ''' @@ -10523,143 +10845,146 @@ font-weight: 700; } - .terminal-3248687298-matrix { + .terminal-1642992271-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3248687298-title { + .terminal-1642992271-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3248687298-r1 { fill: #e1e1e1 } - .terminal-3248687298-r2 { fill: #c5c8c6 } - .terminal-3248687298-r3 { fill: #14191f } - .terminal-3248687298-r4 { fill: #ff0000 } - .terminal-3248687298-r5 { fill: #23568b } - .terminal-3248687298-r6 { fill: #008000 } + .terminal-1642992271-r1 { fill: #c5c8c6 } + .terminal-1642992271-r2 { fill: #e7e0e6 } + .terminal-1642992271-r3 { fill: #eae2e4 } + .terminal-1642992271-r4 { fill: #ece5e5 } + .terminal-1642992271-r5 { fill: #eee8e3 } + .terminal-1642992271-r6 { fill: #e8ede4 } + .terminal-1642992271-r7 { fill: #e3ede7 } + .terminal-1642992271-r8 { fill: #e1eceb } + .terminal-1642992271-r9 { fill: #eeeddf } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ScrollbarApp + PaddingAllApp - - - - I must not fear.I must not fear. - Fear is the mind-killer.Fear is the mind-killer. - Fear is the little-death that brings tFear is the little-death that brings t - I will face my fear.I will face my fear. - I will permit it to pass over me and tI will permit it to pass over me and t - And when it has gone past, I will turnAnd when it has gone past, I will turn - see its path.see its path. - Where the fear has gone there will be Where the fear has gone there will be  - will remain.will remain. - I must not fear.I must not fear. - Fear is the mind-killer.Fear is the mind-killer. - Fear is the little-death that brings tFear is the little-death that brings t - I will face my fear.I will face my fear. - I will permit it to pass over me and tI will permit it to pass over me and t - And when it has gone past, I will turnAnd when it has gone past, I will turn - see its path.▃▃see its path.▃▃ - Where the fear has gone there will be Where the fear has gone there will be  - will remain.will remain. - I must not fear.I must not fear. - Fear is the mind-killer.Fear is the mind-killer. - Fear is the little-death that brings tFear is the little-death that brings t - I will face my fear.I will face my fear. - I will permit it to pass over me and tI will permit it to pass over me and t - + + + + no padding + padding: 1padding:padding: 1 1 + 1 52 6 + + + + + + + + + + padding-right: 3padding-bottom: 4padding-left: 3 + + + + padding-top: 4 + + + + + + + ''' # --- -# name: test_css_property[scrollbars2.py] +# name: test_css_property[row_span.py] ''' @@ -10682,143 +11007,148 @@ font-weight: 700; } - .terminal-3828898399-matrix { + .terminal-3799423623-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3828898399-title { + .terminal-3799423623-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3828898399-r1 { fill: #e1e1e1 } - .terminal-3828898399-r2 { fill: #c5c8c6 } - .terminal-3828898399-r3 { fill: #0000ff } + .terminal-3799423623-r1 { fill: #c5c8c6 } + .terminal-3799423623-r2 { fill: #efe9e4 } + .terminal-3799423623-r3 { fill: #ede6e6 } + .terminal-3799423623-r4 { fill: #eae3e5 } + .terminal-3799423623-r5 { fill: #e8e0e7 } + .terminal-3799423623-r6 { fill: #efeedf } + .terminal-3799423623-r7 { fill: #e9eee5 } + .terminal-3799423623-r8 { fill: #e4eee8 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Scrollbar2App + MyApp - - - - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total obliteration. - I will face my fear. - I will permit it to pass over me and through me. - And when it has gone past, I will turn the inner eye to see its path. - Where the fear has gone there will be nothing. Only I will remain. - I must not fear. - Fear is the mind-killer.▇▇ - Fear is the little-death that brings total obliteration. - I will face my fear. - I will permit it to pass over me and through me. - And when it has gone past, I will turn the inner eye to see its path. - Where the fear has gone there will be nothing. Only I will remain. - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total obliteration. - I will face my fear. - I will permit it to pass over me and through me. - And when it has gone past, I will turn the inner eye to see its path. - Where the fear has gone there will be nothing. Only I will remain. - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total obliteration. - - - - - ''' -# --- -# name: test_css_property[text_align.py] - ''' - - + + + + + + #p4 + + + #p3 + + + #p2 + + + #p1 + + + #p5 + + + #p6 + + + #p7 + + + + + + + + ''' +# --- +# name: test_css_property[scrollbar_corner_color.py] + ''' + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextAlign + ScrollbarCornerColorApp - - - - - Left alignedCenter aligned - I must not fear. Fear is the            I must not fear. Fear is the     - mind-killer. Fear is the                  mind-killer. Fear is the       - little-death that brings total         little-death that brings total    - obliteration. I will face my fear. Iobliteration. I will face my fear. I - will permit it to pass over me and   will permit it to pass over me and  - through me.                                     through me.              - - - - - - Right alignedJustified -         I must not fear. Fear is theI  must  not  fear.  Fear   is   the -             mind-killer. Fear is themind-killer.     Fear     is     the -       little-death that brings totallittle-death   that   brings   total - obliteration. I will face my fear. Iobliteration. I will face my fear. I -   will permit it to pass over me andwill permit it to pass over  me  and -                          through me.through me. - - - + + + + I must not fear. Fear is the mind-killer. Fear is the little-death that brings + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration. + I will face my fear. + I will permit it to pass over me and through me. + And when it has gone past, I will turn the inner eye to see its path. + Where the fear has gone there will be nothing. Only I will remain.▅▅ + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration. + I will face my fear. + I will permit it to pass over me and through me. + And when it has gone past, I will turn the inner eye to see its path. + Where the fear has gone there will be nothing. Only I will remain. + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration. + I will face my fear. + I will permit it to pass over me and through me. + And when it has gone past, I will turn the inner eye to see its path. + Where the fear has gone there will be nothing. Only I will remain. + I must not fear. @@ -10977,7 +11301,7 @@ ''' # --- -# name: test_css_property[text_opacity.py] +# name: test_css_property[scrollbar_gutter.py] ''' @@ -11000,145 +11324,143 @@ font-weight: 700; } - .terminal-248406950-matrix { + .terminal-2133906435-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-248406950-title { + .terminal-2133906435-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-248406950-r1 { fill: #c5c8c6 } - .terminal-248406950-r2 { fill: #4e4e4e;font-weight: bold } - .terminal-248406950-r3 { fill: #7f7f7f;font-weight: bold } - .terminal-248406950-r4 { fill: #b0b0b0;font-weight: bold } - .terminal-248406950-r5 { fill: #e1e1e1;font-weight: bold } + .terminal-2133906435-r1 { fill: #fffaf0 } + .terminal-2133906435-r2 { fill: #c5c8c6 } + .terminal-2133906435-r3 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextOpacityApp + ScrollbarGutterApp - - - - - - - -                                text-opacity: 25%                                 - - - - -                                text-opacity: 50%                                 - - - - -                                text-opacity: 75%                                 - - - - -                                text-opacity: 100%                                - - - - - - - - - ''' -# --- -# name: test_css_property[text_style.py] - ''' - - + + + + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration. + I will face my fear. + I will permit it to pass over me and through me. + And when it has gone past, I will turn the inner eye to see its path. + Where the fear has gone there will be nothing. Only I will remain. + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_css_property[scrollbar_size.py] + ''' + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TextStyleApp + ScrollbarApp - - - - I must not fear.I must not fear.I must not fear. - Fear is the mind-killer.Fear is the mind-killer.Fear is the mind-killer. - Fear is the little-death Fear is the little-death Fear is the little-death  - that brings total that brings total that brings total  - obliteration.obliteration.obliteration. - I will face my fear.I will face my fear.I will face my fear. - I will permit it to pass I will permit it to pass I will permit it to pass  - over me and through me.over me and through me.over me and through me. - And when it has gone past,And when it has gone past, And when it has gone past,  - I will turn the inner eye I will turn the inner eye I will turn the inner eye  - to see its path.to see its path.to see its path. - Where the fear has gone Where the fear has gone Where the fear has gone  - there will be nothing. there will be nothing. Onlythere will be nothing. Only - Only I will remain.I will remain.I will remain. - - - - - - - - - + + + + + + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration.▁▁▁▁ + I will face my fear. + I will permit it to pass over me and through me. + And when it has gone past, I will turn the inner eye to see its path. + Where the fear has gone there will be nothing. Only I will remain. + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration. + I will face my fear. + + + + + + + + + + @@ -11293,7 +11613,7 @@ ''' # --- -# name: test_css_property[text_style_all.py] +# name: test_css_property[scrollbar_size2.py] ''' @@ -11316,146 +11636,143 @@ font-weight: 700; } - .terminal-1979075264-matrix { + .terminal-709381566-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1979075264-title { + .terminal-709381566-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1979075264-r1 { fill: #e1e1e1 } - .terminal-1979075264-r2 { fill: #c5c8c6 } - .terminal-1979075264-r3 { fill: #e1e1e1;font-weight: bold } - .terminal-1979075264-r4 { fill: #e1e1e1;font-style: italic; } - .terminal-1979075264-r5 { fill: #1e1e1e } - .terminal-1979075264-r6 { fill: #e1e1e1;text-decoration: line-through; } - .terminal-1979075264-r7 { fill: #e1e1e1;text-decoration: underline; } - .terminal-1979075264-r8 { fill: #e1e1e1;font-weight: bold;font-style: italic; } - .terminal-1979075264-r9 { fill: #1e1e1e;text-decoration: line-through; } + .terminal-709381566-r1 { fill: #e7e0e0 } + .terminal-709381566-r2 { fill: #c5c8c6 } + .terminal-709381566-r3 { fill: #e0e4e0 } + .terminal-709381566-r4 { fill: #e0e0e7 } + .terminal-709381566-r5 { fill: #14191f } + .terminal-709381566-r6 { fill: #23568b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - AllTextStyleApp + ScrollbarApp - - - - - nonebolditalicreverse - I must not fear.I must not fear.I must not fear.I must not fear. - Fear is the Fear is the Fear is the Fear is the  - mind-killer.mind-killer.mind-killer.mind-killer. - Fear is the Fear is the Fear is the Fear is the  - little-death thatlittle-death that little-death thatlittle-death that  - brings total brings total brings total brings total  - obliteration.obliteration.obliteration.obliteration. - I will face my I will face my I will face my I will face my  - fear.fear.fear.fear. - - strikeunderlinebold italicreverse strike - I must not fear.I must not fear.I must not fear.I must not fear. - Fear is the Fear is the Fear is the Fear is the  - mind-killer.mind-killer.mind-killer.mind-killer. - Fear is the Fear is the Fear is the Fear is the  - little-death thatlittle-death that little-death thatlittle-death that  - brings total brings total brings total brings total  - obliteration.obliteration.obliteration.obliteration. - I will face my I will face my I will face my I will face my  - fear.fear.fear.fear. - I will permit it I will permit it I will permit it I will permit it  - - - - - - ''' -# --- -# name: test_css_property[tint.py] + + + + I must not fear.I must not fear.I must not fear. + Fear is the mind-killer.Fear is the mind-killer.Fear is the mind-killer. + Fear is the little-death Fear is the little-death tFear is the little-death  + I will face my fear.I will face my fear.I will face my fear. + I will permit it to pass I will permit it to pass oI will permit it to pass  + And when it has gone pastAnd when it has gone past,And when it has gone past + Where the fear has gone tWhere the fear has gone thWhere the fear has gone t + I must not fear.I must not fear.I must not fear. + Fear is the mind-killer.Fear is the mind-killer.Fear is the mind-killer. + Fear is the little-death Fear is the little-death tFear is the little-death  + I will face my fear.I will face my fear.I will face my fear.▇▇ + I will permit it to pass I will permit it to pass oI will permit it to pass  + And when it has gone pastAnd when it has gone past,And when it has gone past + Where the fear has gone tWhere the fear has gone thWhere the fear has gone t + I must not fear.I must not fear.I must not fear. + Fear is the mind-killer.Fear is the mind-killer.Fear is the mind-killer. + Fear is the little-death Fear is the little-death tFear is the little-death  + I will face my fear.I will face my fear.I will face my fear. + I will permit it to pass I will permit it to pass oI will permit it to pass  + And when it has gone past, + Where the fear has gone th + I must not fear. + Fear is the mind-killer. + + + + + + ''' +# --- +# name: test_css_property[scrollbars.py] ''' @@ -11478,148 +11795,143 @@ font-weight: 700; } - .terminal-4088965036-matrix { + .terminal-3248687298-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4088965036-title { + .terminal-3248687298-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4088965036-r1 { fill: #c5c8c6 } - .terminal-4088965036-r2 { fill: #e1e1e1 } - .terminal-4088965036-r3 { fill: #000000;font-weight: bold } - .terminal-4088965036-r4 { fill: #000c00;font-weight: bold } - .terminal-4088965036-r5 { fill: #001900;font-weight: bold } - .terminal-4088965036-r6 { fill: #002600;font-weight: bold } - .terminal-4088965036-r7 { fill: #003300;font-weight: bold } - .terminal-4088965036-r8 { fill: #004000;font-weight: bold } - .terminal-4088965036-r9 { fill: #14191f } - .terminal-4088965036-r10 { fill: #004c00;font-weight: bold } - .terminal-4088965036-r11 { fill: #005900;font-weight: bold } + .terminal-3248687298-r1 { fill: #e1e1e1 } + .terminal-3248687298-r2 { fill: #c5c8c6 } + .terminal-3248687298-r3 { fill: #14191f } + .terminal-3248687298-r4 { fill: #ff0000 } + .terminal-3248687298-r5 { fill: #23568b } + .terminal-3248687298-r6 { fill: #008000 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TintApp + ScrollbarApp - - - - - tint: green 0%; - - - tint: green 10%; - - - tint: green 20%; - - - tint: green 30%; - - - tint: green 40%; - - - tint: green 50%; - ▄▄ - - tint: green 60%; - - - tint: green 70%; - + + + + I must not fear.I must not fear. + Fear is the mind-killer.Fear is the mind-killer. + Fear is the little-death that brings tFear is the little-death that brings t + I will face my fear.I will face my fear. + I will permit it to pass over me and tI will permit it to pass over me and t + And when it has gone past, I will turnAnd when it has gone past, I will turn + see its path.see its path. + Where the fear has gone there will be Where the fear has gone there will be  + will remain.will remain. + I must not fear.I must not fear. + Fear is the mind-killer.Fear is the mind-killer. + Fear is the little-death that brings tFear is the little-death that brings t + I will face my fear.I will face my fear. + I will permit it to pass over me and tI will permit it to pass over me and t + And when it has gone past, I will turnAnd when it has gone past, I will turn + see its path.▃▃see its path.▃▃ + Where the fear has gone there will be Where the fear has gone there will be  + will remain.will remain. + I must not fear.I must not fear. + Fear is the mind-killer.Fear is the mind-killer. + Fear is the little-death that brings tFear is the little-death that brings t + I will face my fear.I will face my fear. + I will permit it to pass over me and tI will permit it to pass over me and t + ''' # --- -# name: test_css_property[visibility.py] +# name: test_css_property[scrollbars2.py] ''' @@ -11642,141 +11954,141 @@ font-weight: 700; } - .terminal-398211359-matrix { + .terminal-3828898399-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-398211359-title { + .terminal-3828898399-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-398211359-r1 { fill: #0000ff } - .terminal-398211359-r2 { fill: #c5c8c6 } - .terminal-398211359-r3 { fill: #ddeedd } + .terminal-3828898399-r1 { fill: #e1e1e1 } + .terminal-3828898399-r2 { fill: #c5c8c6 } + .terminal-3828898399-r3 { fill: #0000ff } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - VisibilityApp + Scrollbar2App - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Widget 1 - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Widget 3 - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - - - - - - - - - - - - - ''' -# --- -# name: test_css_property[visibility_containers.py] - ''' + + + + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration. + I will face my fear. + I will permit it to pass over me and through me. + And when it has gone past, I will turn the inner eye to see its path. + Where the fear has gone there will be nothing. Only I will remain. + I must not fear. + Fear is the mind-killer.▇▇ + Fear is the little-death that brings total obliteration. + I will face my fear. + I will permit it to pass over me and through me. + And when it has gone past, I will turn the inner eye to see its path. + Where the fear has gone there will be nothing. Only I will remain. + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration. + I will face my fear. + I will permit it to pass over me and through me. + And when it has gone past, I will turn the inner eye to see its path. + Where the fear has gone there will be nothing. Only I will remain. + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration. + + + + + ''' +# --- +# name: test_css_property[text_align.py] + ''' - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - VisibilityContainersApp + TextAlign - - - - - - - PlaceholderPlaceholderPlaceholder - - - - - - - - - - - - - - - - PlaceholderPlaceholderPlaceholder - - - + + + + + Left alignedCenter aligned + I must not fear. Fear is the            I must not fear. Fear is the     + mind-killer. Fear is the                  mind-killer. Fear is the       + little-death that brings total         little-death that brings total    + obliteration. I will face my fear. Iobliteration. I will face my fear. I + will permit it to pass over me and   will permit it to pass over me and  + through me.                                     through me.              + + + + + + Right alignedJustified +         I must not fear. Fear is theI  must  not  fear.  Fear   is   the +             mind-killer. Fear is themind-killer.     Fear     is     the +       little-death that brings totallittle-death   that   brings   total + obliteration. I will face my fear. Iobliteration. I will face my fear. I +   will permit it to pass over me andwill permit it to pass over  me  and +                          through me.through me. + + + @@ -11935,7 +12249,7 @@ ''' # --- -# name: test_css_property[width.py] +# name: test_css_property[text_opacity.py] ''' @@ -11958,141 +12272,143 @@ font-weight: 700; } - .terminal-2620740165-matrix { + .terminal-248406950-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2620740165-title { + .terminal-248406950-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2620740165-r1 { fill: #ffffff } - .terminal-2620740165-r2 { fill: #c5c8c6 } - .terminal-2620740165-r3 { fill: #e1e1e1 } + .terminal-248406950-r1 { fill: #c5c8c6 } + .terminal-248406950-r2 { fill: #4e4e4e;font-weight: bold } + .terminal-248406950-r3 { fill: #7f7f7f;font-weight: bold } + .terminal-248406950-r4 { fill: #b0b0b0;font-weight: bold } + .terminal-248406950-r5 { fill: #e1e1e1;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - WidthApp + TextOpacityApp - - - - Widget - - - - - - - - - - - - - - - - - - - - - - - - - - - - ''' -# --- -# name: test_css_property[width_comparison.py] - ''' + + + + + + + +                                text-opacity: 25%                                 + + + + +                                text-opacity: 50%                                 + + + + +                                text-opacity: 75%                                 + + + + +                                text-opacity: 100%                                + + + + + + + + + ''' +# --- +# name: test_css_property[text_style.py] + ''' - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - WidthComparisonApp + TextStyleApp - - - - - - - - - - - - - - - #cells#percent#w#h#vw#vh#auto#fr1#fr3 - - - - - - - - - - - - ····•····•····•····•····•····•····•····•····•····•····•····•····•····•····•····• + + + + I must not fear.I must not fear.I must not fear. + Fear is the mind-killer.Fear is the mind-killer.Fear is the mind-killer. + Fear is the little-death Fear is the little-death Fear is the little-death  + that brings total that brings total that brings total  + obliteration.obliteration.obliteration. + I will face my fear.I will face my fear.I will face my fear. + I will permit it to pass I will permit it to pass I will permit it to pass  + over me and through me.over me and through me.over me and through me. + And when it has gone past,And when it has gone past, And when it has gone past,  + I will turn the inner eye I will turn the inner eye I will turn the inner eye  + to see its path.to see its path.to see its path. + Where the fear has gone Where the fear has gone Where the fear has gone  + there will be nothing. there will be nothing. Onlythere will be nothing. Only + Only I will remain.I will remain.I will remain. + + + + + + + + + + ''' # --- -# name: test_datatable_add_column +# name: test_css_property[text_style_all.py] ''' @@ -12278,142 +12588,146 @@ font-weight: 700; } - .terminal-2146794738-matrix { + .terminal-1979075264-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2146794738-title { + .terminal-1979075264-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2146794738-r1 { fill: #dde6ed;font-weight: bold } - .terminal-2146794738-r2 { fill: #dde6ed } - .terminal-2146794738-r3 { fill: #c5c8c6 } - .terminal-2146794738-r4 { fill: #211505 } - .terminal-2146794738-r5 { fill: #e1e1e1 } + .terminal-1979075264-r1 { fill: #e1e1e1 } + .terminal-1979075264-r2 { fill: #c5c8c6 } + .terminal-1979075264-r3 { fill: #e1e1e1;font-weight: bold } + .terminal-1979075264-r4 { fill: #e1e1e1;font-style: italic; } + .terminal-1979075264-r5 { fill: #1e1e1e } + .terminal-1979075264-r6 { fill: #e1e1e1;text-decoration: line-through; } + .terminal-1979075264-r7 { fill: #e1e1e1;text-decoration: underline; } + .terminal-1979075264-r8 { fill: #e1e1e1;font-weight: bold;font-style: italic; } + .terminal-1979075264-r9 { fill: #1e1e1e;text-decoration: line-through; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - AddColumn + AllTextStyleApp - - - -  Movies          No Default  With Default  Long Default          -  Severance       ABC           01234567890123456789  -  Foundation      ABC           01234567890123456789  -  Dark            Hello!      ABC           01234567890123456789  -  The Boys        ABC           01234567890123456789  -  The Last of Us  ABC           01234567890123456789  -  Lost in Space   ABC           01234567890123456789  -  Altered Carbon  ABC           01234567890123456789  - - - - - - - - - - - - - - - - + + + + + nonebolditalicreverse + I must not fear.I must not fear.I must not fear.I must not fear. + Fear is the Fear is the Fear is the Fear is the  + mind-killer.mind-killer.mind-killer.mind-killer. + Fear is the Fear is the Fear is the Fear is the  + little-death thatlittle-death that little-death thatlittle-death that  + brings total brings total brings total brings total  + obliteration.obliteration.obliteration.obliteration. + I will face my I will face my I will face my I will face my  + fear.fear.fear.fear. + + strikeunderlinebold italicreverse strike + I must not fear.I must not fear.I must not fear.I must not fear. + Fear is the Fear is the Fear is the Fear is the  + mind-killer.mind-killer.mind-killer.mind-killer. + Fear is the Fear is the Fear is the Fear is the  + little-death thatlittle-death that little-death thatlittle-death that  + brings total brings total brings total brings total  + obliteration.obliteration.obliteration.obliteration. + I will face my I will face my I will face my I will face my  + fear.fear.fear.fear. + I will permit it I will permit it I will permit it I will permit it  + ''' # --- -# name: test_datatable_column_cursor_render +# name: test_css_property[tint.py] ''' @@ -12436,137 +12750,140 @@ font-weight: 700; } - .terminal-1071832686-matrix { + .terminal-4088965036-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1071832686-title { + .terminal-4088965036-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1071832686-r1 { fill: #dde6ed;font-weight: bold } - .terminal-1071832686-r2 { fill: #1e1201;font-weight: bold } - .terminal-1071832686-r3 { fill: #dde6ed } - .terminal-1071832686-r4 { fill: #c5c8c6 } - .terminal-1071832686-r5 { fill: #dfe4e7 } - .terminal-1071832686-r6 { fill: #1e1405 } - .terminal-1071832686-r7 { fill: #e1e1e1 } - .terminal-1071832686-r8 { fill: #211505 } + .terminal-4088965036-r1 { fill: #c5c8c6 } + .terminal-4088965036-r2 { fill: #e1e1e1 } + .terminal-4088965036-r3 { fill: #000000;font-weight: bold } + .terminal-4088965036-r4 { fill: #000c00;font-weight: bold } + .terminal-4088965036-r5 { fill: #001900;font-weight: bold } + .terminal-4088965036-r6 { fill: #002600;font-weight: bold } + .terminal-4088965036-r7 { fill: #003300;font-weight: bold } + .terminal-4088965036-r8 { fill: #004000;font-weight: bold } + .terminal-4088965036-r9 { fill: #14191f } + .terminal-4088965036-r10 { fill: #004c00;font-weight: bold } + .terminal-4088965036-r11 { fill: #005900;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TableApp + TintApp - - - -  lane  swimmer               country        time   -  4     Joseph Schooling      Singapore      50.39  -  2     Michael Phelps        United States  51.14  -  5     Chad le Clos          South Africa   51.14  -  6     László Cseh           Hungary        51.14  -  3     Li Zhuhao             China          51.26  -  8     Mehdy Metella         France         51.58  -  7     Tom Shields           United States  51.73  -  1     Aleksandr Sadovnikov  Russia         51.84  - - - - - - - - - - - - - - + + + + + tint: green 0%; + + + tint: green 10%; + + + tint: green 20%; + + + tint: green 30%; + + + tint: green 40%; + + + tint: green 50%; + ▄▄ + + tint: green 60%; + + + tint: green 70%; @@ -12574,7 +12891,7 @@ ''' # --- -# name: test_datatable_labels_and_fixed_data +# name: test_css_property[visibility.py] ''' @@ -12597,146 +12914,143 @@ font-weight: 700; } - .terminal-1710966859-matrix { + .terminal-398211359-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1710966859-title { + .terminal-398211359-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1710966859-r1 { fill: #dde6ed;font-weight: bold } - .terminal-1710966859-r2 { fill: #dde6ed } - .terminal-1710966859-r3 { fill: #c5c8c6 } - .terminal-1710966859-r4 { fill: #1e1405 } - .terminal-1710966859-r5 { fill: #dfe4e7 } - .terminal-1710966859-r6 { fill: #e1e1e1 } + .terminal-398211359-r1 { fill: #0000ff } + .terminal-398211359-r2 { fill: #c5c8c6 } + .terminal-398211359-r3 { fill: #ddeedd } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TableApp + VisibilityApp - - - -  lane  swimmer               country        time   -  0  5     Chad le Clos          South Africa   51.14  -  1  4     Joseph Schooling      Singapore      50.39  -  2  2     Michael Phelps        United States  51.14  -  3  6     László Cseh           Hungary        51.14  -  4  3     Li Zhuhao             China          51.26  -  5  8     Mehdy Metella         France         51.58  -  6  7     Tom Shields           United States  51.73  -  7  10    Darren Burns          Scotland       51.84  -  8  1     Aleksandr Sadovnikov  Russia         51.84  - - - - - - - - - - - - - - - - - - - ''' -# --- -# name: test_datatable_remove_row - ''' - - + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Widget 1 + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Widget 3 + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + + + + + + + + + + + + + ''' +# --- +# name: test_css_property[visibility_containers.py] + ''' + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TableApp + VisibilityContainersApp - - - -  lane  swimmer               country        time   -  5     Chad le Clos          South Africa   51.14  -  4     Joseph Schooling      Singapore      50.39  -  6     László Cseh           Hungary        51.14  -  3     Li Zhuhao             China          51.26  -  7     Tom Shields           United States  51.73  -  10    Darren Burns          Scotland       51.84  - - - - - - - - - - - - - - - - + + + + + + + PlaceholderPlaceholderPlaceholder + + + + + + + + + + + + + + + + PlaceholderPlaceholderPlaceholder + + + @@ -12891,7 +13207,7 @@ ''' # --- -# name: test_datatable_render +# name: test_css_property[width.py] ''' @@ -12914,145 +13230,143 @@ font-weight: 700; } - .terminal-2311386745-matrix { + .terminal-2620740165-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2311386745-title { + .terminal-2620740165-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2311386745-r1 { fill: #dde6ed;font-weight: bold } - .terminal-2311386745-r2 { fill: #dde6ed } - .terminal-2311386745-r3 { fill: #c5c8c6 } - .terminal-2311386745-r4 { fill: #e1e1e1 } - .terminal-2311386745-r5 { fill: #211505 } + .terminal-2620740165-r1 { fill: #ffffff } + .terminal-2620740165-r2 { fill: #c5c8c6 } + .terminal-2620740165-r3 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TableApp + WidthApp - - - -  lane  swimmer               country        time   -  4     Joseph Schooling      Singapore      50.39  -  2     Michael Phelps        United States  51.14  -  5     Chad le Clos          South Africa   51.14  -  6     László Cseh           Hungary        51.14  -  3     Li Zhuhao             China          51.26  -  8     Mehdy Metella         France         51.58  -  7     Tom Shields           United States  51.73  -  1     Aleksandr Sadovnikov  Russia         51.84  -  10    Darren Burns          Scotland       51.84  - - - - - - - - - - - - - - - - - - - ''' -# --- -# name: test_datatable_row_cursor_render - ''' - - + + + + Widget + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_css_property[width_comparison.py] + ''' + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TableApp + WidthComparisonApp - - - -  lane  swimmer               country        time   -  4     Joseph Schooling      Singapore      50.39  -  2     Michael Phelps        United States  51.14  -  5     Chad le Clos          South Africa   51.14  -  6     László Cseh           Hungary        51.14  -  3     Li Zhuhao             China          51.26  -  8     Mehdy Metella         France         51.58  -  7     Tom Shields           United States  51.73  -  1     Aleksandr Sadovnikov  Russia         51.84  - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + #cells#percent#w#h#vw#vh#auto#fr1#fr3 + + + + + + + + + + + + ····•····•····•····•····•····•····•····•····•····•····•····•····•····•····•····• ''' # --- -# name: test_datatable_sort_multikey +# name: test_datatable_add_column ''' @@ -13232,142 +13550,142 @@ font-weight: 700; } - .terminal-2683041401-matrix { + .terminal-2146794738-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2683041401-title { + .terminal-2146794738-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2683041401-r1 { fill: #dde6ed;font-weight: bold } - .terminal-2683041401-r2 { fill: #dde6ed } - .terminal-2683041401-r3 { fill: #c5c8c6 } - .terminal-2683041401-r4 { fill: #e1e1e1 } - .terminal-2683041401-r5 { fill: #211505 } + .terminal-2146794738-r1 { fill: #dde6ed;font-weight: bold } + .terminal-2146794738-r2 { fill: #dde6ed } + .terminal-2146794738-r3 { fill: #c5c8c6 } + .terminal-2146794738-r4 { fill: #211505 } + .terminal-2146794738-r5 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - TableApp + AddColumn - - - -  lane  swimmer               country        time   -  4     Joseph Schooling      Singapore      50.39  -  2     Michael Phelps        United States  51.14  -  5     Chad le Clos          South Africa   51.14  -  6     László Cseh           Hungary        51.14  -  3     Li Zhuhao             China          51.26  -  8     Mehdy Metella         France         51.58  -  7     Tom Shields           United States  51.73  -  1     Aleksandr Sadovnikov  Russia         51.84  -  10    Darren Burns          Scotland       51.84  - - - - - - - - - - - - - - + + + +  Movies          No Default  With Default  Long Default          +  Severance       ABC           01234567890123456789  +  Foundation      ABC           01234567890123456789  +  Dark            Hello!      ABC           01234567890123456789  +  The Boys        ABC           01234567890123456789  +  The Last of Us  ABC           01234567890123456789  +  Lost in Space   ABC           01234567890123456789  +  Altered Carbon  ABC           01234567890123456789  + + + + + + + + + + + + + + + + ''' # --- -# name: test_datatable_style_ordering +# name: test_datatable_add_row_auto_height ''' @@ -13390,137 +13708,134 @@ font-weight: 700; } - .terminal-1146140386-matrix { + .terminal-3912008695-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1146140386-title { + .terminal-3912008695-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1146140386-r1 { fill: #e1e1e1 } - .terminal-1146140386-r2 { fill: #c5c8c6 } - .terminal-1146140386-r3 { fill: #dde6ed;font-weight: bold } - .terminal-1146140386-r4 { fill: #dde6ed } - .terminal-1146140386-r5 { fill: #fea62b;font-weight: bold;font-style: italic; } - .terminal-1146140386-r6 { fill: #e1e2e3 } - .terminal-1146140386-r7 { fill: #cc555a } - .terminal-1146140386-r8 { fill: #cc555a;font-weight: bold;font-style: italic; } + .terminal-3912008695-r1 { fill: #dde6ed;font-weight: bold } + .terminal-3912008695-r2 { fill: #dde6ed } + .terminal-3912008695-r3 { fill: #c5c8c6 } + .terminal-3912008695-r4 { fill: #211505 } + .terminal-3912008695-r5 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DataTableCursorStyles + AutoHeightRowsApp - - - - Foreground is 'css', background is 'css': -  Movies      -  Severance   - Foundation - Dark - - Foreground is 'css', background is 'renderable': -  Movies      - Severance - Foundation - Dark - - Foreground is 'renderable', background is 'renderable': -  Movies      - Severance - Foundation - Dark - - Foreground is 'renderable', background is 'css': -  Movies      - Severance - Foundation - Dark + + + +  N  Column      +  3  hey there   +  1  hey there   +  5  long        +  string      +  2  ╭───────╮   +  │ Hello │   +  │ world │   +  ╰───────╯   +  4  1           +  2           +  3           +  4           +  5           +  6           +  7           + + + + + + + @@ -13528,9 +13843,9 @@ ''' # --- -# name: test_demo +# name: test_datatable_add_row_auto_height_sorted ''' - + - - + + - - - - - - - - - - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - Textual Demo + AutoHeightRowsApp - - - - Textual Demo - - - TOP - - ▆▆ - - Widgets - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - Rich contentTextual Demo - - Welcome! Textual is a framework for creating sophisticated - applications with the terminal.                            - CSS - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Start  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - - - - - - - -  CTRL+C  Quit  CTRL+B  Sidebar  CTRL+T  Toggle Dark mode  CTRL+S  Screenshot  F1  Notes  + + + +  N  Column      +  1  hey there   +  2  ╭───────╮   +  │ Hello │   +  │ world │   +  ╰───────╯   +  3  hey there   +  4  1           +  2           +  3           +  4           +  5           +  6           +  7           +  5  long        +  string      + + + + + + + + ''' # --- -# name: test_digits +# name: test_datatable_cell_padding ''' @@ -13742,132 +14024,134 @@ font-weight: 700; } - .terminal-993607346-matrix { + .terminal-1699433504-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-993607346-title { + .terminal-1699433504-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-993607346-r1 { fill: #e1e1e1;font-weight: bold } - .terminal-993607346-r2 { fill: #c5c8c6 } - .terminal-993607346-r3 { fill: #e1e1e1 } + .terminal-1699433504-r1 { fill: #e1e1e1 } + .terminal-1699433504-r2 { fill: #c5c8c6 } + .terminal-1699433504-r3 { fill: #dde6ed;font-weight: bold } + .terminal-1699433504-r4 { fill: #dde6ed } + .terminal-1699433504-r5 { fill: #211505 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DigitApp + TableApp - - - - ╺━┓  ┓ ╻ ╻╺━┓╺━┓ -  ━┫  ┃ ┗━┫┏━┛  ┃ - ╺━┛.╺┻╸  ╹┗━╸  ╹ -    ┏━┓ ┓ ╺━┓╺━┓╻ ╻┏━╸┏━╸╺━┓┏━┓┏━┓         -    ┃ ┃ ┃ ┏━┛ ━┫┗━┫┗━┓┣━┓  ┃┣━┫┗━┫╺╋╸╺━╸   -    ┗━┛╺┻╸┗━╸╺━┛  ╹╺━┛┗━┛  ╹┗━┛╺━┛      ., - ╺━┓    ┓ ┏━┓ ^ ╻ ╻ -  ━┫ ×  ┃ ┃ ┃   ┗━┫ - ╺━┛   ╺┻╸┗━┛     ╹ - - - - - - - - - - - - - - + + + + + one  two  three + valuevalueval   + +  one    two    three  +  value  value  val    + +   one      two      three   +   value    value    val     + +    one        two        three    +    value      value      val      + +     one          two          three     +     value        value        val       + + + + + + + + @@ -13875,7 +14159,7 @@ ''' # --- -# name: test_disabled_widgets +# name: test_datatable_change_cell_padding ''' @@ -13898,169 +14182,142 @@ font-weight: 700; } - .terminal-3864303289-matrix { + .terminal-236473376-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3864303289-title { + .terminal-236473376-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3864303289-r1 { fill: #454a50 } - .terminal-3864303289-r2 { fill: #507bb3 } - .terminal-3864303289-r3 { fill: #7ae998 } - .terminal-3864303289-r4 { fill: #ffcf56 } - .terminal-3864303289-r5 { fill: #e76580 } - .terminal-3864303289-r6 { fill: #c5c8c6 } - .terminal-3864303289-r7 { fill: #24292f;font-weight: bold } - .terminal-3864303289-r8 { fill: #dde6ed;font-weight: bold } - .terminal-3864303289-r9 { fill: #0a180e;font-weight: bold } - .terminal-3864303289-r10 { fill: #211505;font-weight: bold } - .terminal-3864303289-r11 { fill: #f5e5e9;font-weight: bold } - .terminal-3864303289-r12 { fill: #000000 } - .terminal-3864303289-r13 { fill: #001541 } - .terminal-3864303289-r14 { fill: #008139 } - .terminal-3864303289-r15 { fill: #b86b00 } - .terminal-3864303289-r16 { fill: #780028 } - .terminal-3864303289-r17 { fill: #303336 } - .terminal-3864303289-r18 { fill: #364b66 } - .terminal-3864303289-r19 { fill: #4a8159 } - .terminal-3864303289-r20 { fill: #8b7439 } - .terminal-3864303289-r21 { fill: #80404d } - .terminal-3864303289-r22 { fill: #a7a7a7;font-weight: bold } - .terminal-3864303289-r23 { fill: #a5a9ac;font-weight: bold } - .terminal-3864303289-r24 { fill: #0e1510;font-weight: bold } - .terminal-3864303289-r25 { fill: #19140c;font-weight: bold } - .terminal-3864303289-r26 { fill: #b0a8aa;font-weight: bold } - .terminal-3864303289-r27 { fill: #0f0f0f } - .terminal-3864303289-r28 { fill: #0f192e } - .terminal-3864303289-r29 { fill: #0f4e2a } - .terminal-3864303289-r30 { fill: #68430f } - .terminal-3864303289-r31 { fill: #4a0f22 } - .terminal-3864303289-r32 { fill: #e2e3e3;font-weight: bold } + .terminal-236473376-r1 { fill: #e1e1e1 } + .terminal-236473376-r2 { fill: #c5c8c6 } + .terminal-236473376-r3 { fill: #dde6ed;font-weight: bold } + .terminal-236473376-r4 { fill: #dde6ed } + .terminal-236473376-r5 { fill: #211505 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - WidgetDisableTestApp + TableApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Button  Button  Button  Button  Button  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Button  Button  Button  Button  Button  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Button  Button  Button  Button  Button  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Button  Button  Button  Button  Button  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Button  Button  Button  Button  Button  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Button  Button  Button  Button  Button  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Button  Button  Button  Button  Button  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Button  Button  Button  Button  Button  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + one  two  three + valuevalueval   + +  one    two    three  +  value  value  val    + +   one      two      three   +   value    value    val     + +    one        two        three    +    value      value      val      + +           one                      two                      three           +           value                    value                    val             + + + + + + + + + ''' # --- -# name: test_dock_layout_sidebar +# name: test_datatable_column_cursor_render ''' @@ -14083,133 +14340,137 @@ font-weight: 700; } - .terminal-3539432312-matrix { + .terminal-1071832686-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3539432312-title { + .terminal-1071832686-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3539432312-r1 { fill: #0f2b41 } - .terminal-3539432312-r2 { fill: #c5c8c6 } - .terminal-3539432312-r3 { fill: #e1e1e1 } - .terminal-3539432312-r4 { fill: #14191f } + .terminal-1071832686-r1 { fill: #dde6ed;font-weight: bold } + .terminal-1071832686-r2 { fill: #1e1201;font-weight: bold } + .terminal-1071832686-r3 { fill: #dde6ed } + .terminal-1071832686-r4 { fill: #c5c8c6 } + .terminal-1071832686-r5 { fill: #dfe4e7 } + .terminal-1071832686-r6 { fill: #1e1405 } + .terminal-1071832686-r7 { fill: #e1e1e1 } + .terminal-1071832686-r8 { fill: #211505 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DockLayoutExample + TableApp - - - - Sidebar1Docking a widget removes it from the layout and  - fixes its position, aligned to either the top,  - right, bottom, or left edges of a container. - - Docked widgets will not scroll out of view,  - making them ideal for sticky headers, footers,  - and sidebars. - ▇▇ - Docking a widget removes it from the layout and  - fixes its position, aligned to either the top,  - right, bottom, or left edges of a container. - - Docked widgets will not scroll out of view,  - making them ideal for sticky headers, footers,  - and sidebars. - - Docking a widget removes it from the layout and  - fixes its position, aligned to either the top,  - right, bottom, or left edges of a container. - - Docked widgets will not scroll out of view,  - making them ideal for sticky headers, footers,  - and sidebars. + + + +  lane  swimmer               country        time   +  4     Joseph Schooling      Singapore      50.39  +  2     Michael Phelps        United States  51.14  +  5     Chad le Clos          South Africa   51.14  +  6     László Cseh           Hungary        51.14  +  3     Li Zhuhao             China          51.26  +  8     Mehdy Metella         France         51.58  +  7     Tom Shields           United States  51.73  +  1     Aleksandr Sadovnikov  Russia         51.84  + + + + + + + + + + + + + + @@ -14217,9 +14478,9 @@ ''' # --- -# name: test_dock_scroll +# name: test_datatable_hot_reloading ''' - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - TestApp + DataTableHotReloadingApp - - - - TestApp - ───────── - this - is - a - sample - sentence - and - here - are - some - wordsthis - is - a - sample - sentence - and - here - are - some - words -  CTRL+Q  Quit  - - - ▇▇ + + + +  A           B     +  one         two   +  three       four  +  five        six   + + + + + + + + + + + + + + + + + + + + ''' # --- -# name: test_dock_scroll2 +# name: test_datatable_labels_and_fixed_data ''' - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - TestApp + TableApp - - - - TestApp - ───────── - this - is - a - sample - sentence - and - here - are - some - wordsthis - is - a▅▅ - sample - sentence - and - here - are - some - words -  CTRL+Q  Quit  - - - - - - - - ''' + + + +  lane  swimmer               country        time   +  0  5     Chad le Clos          South Africa   51.14  +  1  4     Joseph Schooling      Singapore      50.39  +  2  2     Michael Phelps        United States  51.14  +  3  6     László Cseh           Hungary        51.14  +  4  3     Li Zhuhao             China          51.26  +  5  8     Mehdy Metella         France         51.58  +  6  7     Tom Shields           United States  51.73  +  7  10    Darren Burns          Scotland       51.84  +  8  1     Aleksandr Sadovnikov  Russia         51.84  + + + + + + + + + + + + + + + + + + + ''' # --- -# name: test_dock_scroll_off_by_one +# name: test_datatable_remove_row ''' - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - ScrollOffByOne + TableApp - - - - ▔▔▔▔▔▔▔▔ - X 92 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X 93 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X 94 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X 95 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X 96 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X 97 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X 98 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X 99▁▁ - ▁▁▁▁▁▁▁▁ + + + +  lane  swimmer               country        time   +  5     Chad le Clos          South Africa   51.14  +  4     Joseph Schooling      Singapore      50.39  +  6     László Cseh           Hungary        51.14  +  3     Li Zhuhao             China          51.26  +  7     Tom Shields           United States  51.73  +  10    Darren Burns          Scotland       51.84  + + + + + + + + + + + + + + + + @@ -14710,7 +14956,7 @@ ''' # --- -# name: test_focus_component_class +# name: test_datatable_render ''' @@ -14733,143 +14979,142 @@ font-weight: 700; } - .terminal-3936062011-matrix { + .terminal-2311386745-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3936062011-title { + .terminal-2311386745-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3936062011-r1 { fill: #c5c8c6 } - .terminal-3936062011-r2 { fill: #e3e3e3 } - .terminal-3936062011-r3 { fill: #ffdddd } - .terminal-3936062011-r4 { fill: #e1e1e1 } - .terminal-3936062011-r5 { fill: #14191f } - .terminal-3936062011-r6 { fill: #ddedf9 } + .terminal-2311386745-r1 { fill: #dde6ed;font-weight: bold } + .terminal-2311386745-r2 { fill: #dde6ed } + .terminal-2311386745-r3 { fill: #c5c8c6 } + .terminal-2311386745-r4 { fill: #e1e1e1 } + .terminal-2311386745-r5 { fill: #211505 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - StyleBugApp + TableApp - - - - StyleBugApp - test widget 0 - test widget 1 - test widget 2 - test widget 3 - test widget 4 - test widget 5 - test widget 6 - test widget 7 - test widget 8 - test widget 9 - test widget 10 - test widget 11 - test widget 12▇▇ - test widget 13 - test widget 14 - test widget 15 - test widget 16 - test widget 17 - test widget 18 - test widget 19 - test widget 20 - test widget 21 - - + + + +  lane  swimmer               country        time   +  4     Joseph Schooling      Singapore      50.39  +  2     Michael Phelps        United States  51.14  +  5     Chad le Clos          South Africa   51.14  +  6     László Cseh           Hungary        51.14  +  3     Li Zhuhao             China          51.26  +  8     Mehdy Metella         France         51.58  +  7     Tom Shields           United States  51.73  +  1     Aleksandr Sadovnikov  Russia         51.84  +  10    Darren Burns          Scotland       51.84  + + + + + + + + + + + + + + + ''' # --- -# name: test_footer_render +# name: test_datatable_row_cursor_render ''' @@ -14892,143 +15137,146 @@ font-weight: 700; } - .terminal-1971839132-matrix { + .terminal-3008422431-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1971839132-title { + .terminal-3008422431-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1971839132-r1 { fill: #e1e1e1 } - .terminal-1971839132-r2 { fill: #c5c8c6 } - .terminal-1971839132-r3 { fill: #dde8f3;font-weight: bold } - .terminal-1971839132-r4 { fill: #ddedf9 } + .terminal-3008422431-r1 { fill: #dde6ed;font-weight: bold } + .terminal-3008422431-r2 { fill: #dde6ed } + .terminal-3008422431-r3 { fill: #c5c8c6 } + .terminal-3008422431-r4 { fill: #dfe4e7 } + .terminal-3008422431-r5 { fill: #e1e1e1 } + .terminal-3008422431-r6 { fill: #1e1405 } + .terminal-3008422431-r7 { fill: #211505 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - FooterApp + TableApp - - - - - - - - - - - - - - - - - - - - - - - - - - -  Q  Quit the app  ?  Show help screen  DELETE  Delete the thing  + + + +  lane  swimmer               country        time   +  4     Joseph Schooling      Singapore      50.39  +  2     Michael Phelps        United States  51.14  +  5     Chad le Clos          South Africa   51.14  +  6     László Cseh           Hungary        51.14  +  3     Li Zhuhao             China          51.26  +  8     Mehdy Metella         France         51.58  +  7     Tom Shields           United States  51.73  +  1     Aleksandr Sadovnikov  Russia         51.84  + + + + + + + + + + + + + + + ''' # --- -# name: test_fr_margins +# name: test_datatable_sort_multikey ''' - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - TestApp + TableApp - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - - Hello - - - - - - - World - - - - - - - !! - - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - - - - ''' -# --- -# name: test_fr_unit_with_min + + + +  lane  swimmer               country        time   +  4     Joseph Schooling      Singapore      50.39  +  2     Michael Phelps        United States  51.14  +  5     Chad le Clos          South Africa   51.14  +  6     László Cseh           Hungary        51.14  +  3     Li Zhuhao             China          51.26  +  8     Mehdy Metella         France         51.58  +  7     Tom Shields           United States  51.73  +  1     Aleksandr Sadovnikov  Russia         51.84  +  10    Darren Burns          Scotland       51.84  + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_style_ordering ''' @@ -15210,136 +15455,137 @@ font-weight: 700; } - .terminal-1354725553-matrix { + .terminal-1146140386-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1354725553-title { + .terminal-1146140386-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1354725553-r1 { fill: #c5c8c6 } - .terminal-1354725553-r2 { fill: #e3e3e3 } - .terminal-1354725553-r3 { fill: #ddddff } - .terminal-1354725553-r4 { fill: #e3e4e5 } - .terminal-1354725553-r5 { fill: #e2e3e3 } - .terminal-1354725553-r6 { fill: #14191f } - .terminal-1354725553-r7 { fill: #ddedf9 } + .terminal-1146140386-r1 { fill: #e1e1e1 } + .terminal-1146140386-r2 { fill: #c5c8c6 } + .terminal-1146140386-r3 { fill: #dde6ed;font-weight: bold } + .terminal-1146140386-r4 { fill: #dde6ed } + .terminal-1146140386-r5 { fill: #fea62b;font-weight: bold;font-style: italic; } + .terminal-1146140386-r6 { fill: #e1e2e3 } + .terminal-1146140386-r7 { fill: #cc555a } + .terminal-1146140386-r8 { fill: #cc555a;font-weight: bold;font-style: italic; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ScreenSplitApp + DataTableCursorStyles - - - - ScreenSplitApp - This is content This is content number 0 - number 0This is content number 1 - This is content ▄▄This is content number 2 - number 1This is content number 3 - This is content This is content number 4▁▁ - number 2This is content number 5 - This is content This is content number 6 - number 3This is content number 7 - This is content This is content number 8 - number 4This is content number 9 - This is content This is content number 10 - number 5This is content number 11 - This is content This is content number 12 - number 6This is content number 13 - This is content This is content number 14 - number 7This is content number 15 - This is content This is content number 16 - number 8This is content number 17 - This is content This is content number 18 - number 9This is content number 19 - This is content This is content number 20 - number 10This is content number 21 + + + + Foreground is 'css', background is 'css': +  Movies      +  Severance   + Foundation + Dark + + Foreground is 'css', background is 'renderable': +  Movies      + Severance + Foundation + Dark + + Foreground is 'renderable', background is 'renderable': +  Movies      + Severance + Foundation + Dark + + Foreground is 'renderable', background is 'css': +  Movies      + Severance + Foundation + Dark @@ -15347,9 +15593,9 @@ ''' # --- -# name: test_fr_units +# name: test_demo ''' - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + - FRApp + Textual Demo - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - HEADER - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - foobarbaz - - - - - - - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - FOOTER - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + + + Textual Demo + + + TOP + + ▆▆ + + Widgets + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + Rich contentTextual Demo + + Welcome! Textual is a framework for creating sophisticated + applications with the terminal.                            + CSS + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Start + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + + + + + + +  CTRL+B  Sidebar  CTRL+T  Toggle Dark mode  CTRL+S  Screenshot  F1  Notes  CTRL+Q  Quit  ''' # --- -# name: test_grid_layout_basic +# name: test_digits ''' @@ -15526,140 +15807,140 @@ font-weight: 700; } - .terminal-3077119198-matrix { + .terminal-993607346-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3077119198-title { + .terminal-993607346-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3077119198-r1 { fill: #008000 } - .terminal-3077119198-r2 { fill: #c5c8c6 } - .terminal-3077119198-r3 { fill: #e1e1e1 } + .terminal-993607346-r1 { fill: #e1e1e1;font-weight: bold } + .terminal-993607346-r2 { fill: #c5c8c6 } + .terminal-993607346-r3 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - GridLayoutExample + DigitApp - - - - ────────────────────────────────────────────────────────────────────────── - OneTwoThree - - - - - - - - - - ────────────────────────────────────────────────────────────────────────── - ────────────────────────────────────────────────────────────────────────── - FourFiveSix - - - - - - - - - - ────────────────────────────────────────────────────────────────────────── + + + + ╺━┓  ┓ ╻ ╻╺━┓╺━┓ +  ━┫  ┃ ┗━┫┏━┛  ┃ + ╺━┛.╺┻╸  ╹┗━╸  ╹ +    ┏━┓ ┓ ╺━┓╺━┓╻ ╻┏━╸┏━╸╺━┓┏━┓┏━┓         +    ┃ ┃ ┃ ┏━┛ ━┫┗━┫┗━┓┣━┓  ┃┣━┫┗━┫╺╋╸╺━╸   +    ┗━┛╺┻╸┗━╸╺━┛  ╹╺━┛┗━┛  ╹┗━┛╺━┛      ., + ╺━┓    ┓ ┏━┓ ^ ╻ ╻ +  ━┫ ×  ┃ ┃ ┃   ┗━┫ + ╺━┛   ╺┻╸┗━┛     ╹ + + + + + + + + + + + + + + + ''' # --- -# name: test_grid_layout_basic_overflow +# name: test_disabled_widgets ''' @@ -15682,140 +15963,169 @@ font-weight: 700; } - .terminal-1958232742-matrix { + .terminal-3209943725-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1958232742-title { + .terminal-3209943725-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1958232742-r1 { fill: #008000 } - .terminal-1958232742-r2 { fill: #c5c8c6 } - .terminal-1958232742-r3 { fill: #e1e1e1 } + .terminal-3209943725-r1 { fill: #454a50 } + .terminal-3209943725-r2 { fill: #507bb3 } + .terminal-3209943725-r3 { fill: #7ae998 } + .terminal-3209943725-r4 { fill: #ffcf56 } + .terminal-3209943725-r5 { fill: #e76580 } + .terminal-3209943725-r6 { fill: #c5c8c6 } + .terminal-3209943725-r7 { fill: #24292f;font-weight: bold } + .terminal-3209943725-r8 { fill: #dde6ed;font-weight: bold } + .terminal-3209943725-r9 { fill: #0a180e;font-weight: bold } + .terminal-3209943725-r10 { fill: #211505;font-weight: bold } + .terminal-3209943725-r11 { fill: #f5e5e9;font-weight: bold } + .terminal-3209943725-r12 { fill: #000000 } + .terminal-3209943725-r13 { fill: #001541 } + .terminal-3209943725-r14 { fill: #008139 } + .terminal-3209943725-r15 { fill: #b86b00 } + .terminal-3209943725-r16 { fill: #780028 } + .terminal-3209943725-r17 { fill: #303336 } + .terminal-3209943725-r18 { fill: #364b66 } + .terminal-3209943725-r19 { fill: #4a8159 } + .terminal-3209943725-r20 { fill: #8b7439 } + .terminal-3209943725-r21 { fill: #80404d } + .terminal-3209943725-r22 { fill: #a7a7a7;font-weight: bold } + .terminal-3209943725-r23 { fill: #a5a9ac;font-weight: bold } + .terminal-3209943725-r24 { fill: #0e1510;font-weight: bold } + .terminal-3209943725-r25 { fill: #19140c;font-weight: bold } + .terminal-3209943725-r26 { fill: #b0a8aa;font-weight: bold } + .terminal-3209943725-r27 { fill: #0f0f0f } + .terminal-3209943725-r28 { fill: #0f192e } + .terminal-3209943725-r29 { fill: #0f4e2a } + .terminal-3209943725-r30 { fill: #68430f } + .terminal-3209943725-r31 { fill: #4a0f22 } + .terminal-3209943725-r32 { fill: #e2e3e3;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - GridLayoutExample + WidgetDisableTestApp - - - - ────────────────────────────────────────────────────────────────────────── - OneTwoThree - - - - - - ────────────────────────────────────────────────────────────────────────── - ────────────────────────────────────────────────────────────────────────── - FourFiveSix - - - - - - ────────────────────────────────────────────────────────────────────────── - ──────────────────────── - Seven - - - - - - ──────────────────────── + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ''' # --- -# name: test_grid_layout_gutter +# name: test_dock_layout_sidebar ''' @@ -15838,132 +16148,133 @@ font-weight: 700; } - .terminal-721777988-matrix { + .terminal-3539432312-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-721777988-title { + .terminal-3539432312-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-721777988-r1 { fill: #efddef } - .terminal-721777988-r2 { fill: #c5c8c6 } - .terminal-721777988-r3 { fill: #f0fcf0 } + .terminal-3539432312-r1 { fill: #0f2b41 } + .terminal-3539432312-r2 { fill: #c5c8c6 } + .terminal-3539432312-r3 { fill: #e1e1e1 } + .terminal-3539432312-r4 { fill: #14191f } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - GridLayoutExample + DockLayoutExample - - - - OneTwoThree - - - - - - - - - - - - FourFiveSix - - - - - - - - - - + + + + Sidebar1Docking a widget removes it from the layout and  + fixes its position, aligned to either the top,  + right, bottom, or left edges of a container. + + Docked widgets will not scroll out of view,  + making them ideal for sticky headers, footers,  + and sidebars. + ▇▇ + Docking a widget removes it from the layout and  + fixes its position, aligned to either the top,  + right, bottom, or left edges of a container. + + Docked widgets will not scroll out of view,  + making them ideal for sticky headers, footers,  + and sidebars. + + Docking a widget removes it from the layout and  + fixes its position, aligned to either the top,  + right, bottom, or left edges of a container. + + Docked widgets will not scroll out of view,  + making them ideal for sticky headers, footers,  + and sidebars. @@ -15971,9 +16282,9 @@ ''' # --- -# name: test_header_render +# name: test_dock_scroll ''' - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + - HeaderApp + TestApp - - - - HeaderApp - - - - - - - - - - - - - - - - - - - - - - - + + + + TestApp + ───────── + this + is + a + sample + sentence + and + here + are + some + wordsthis + is + a + sample + sentence + and + here + are + some + words +  CTRL+Q  Quit  + + + ▇▇ ''' # --- -# name: test_horizontal_layout +# name: test_dock_scroll2 ''' - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + - HorizontalLayoutExample + TestApp - - - - ────────────────────────────────────────────────────────────────────────── - OneTwoThree - - - - - - - - - - - - - - - - - - - - - - ────────────────────────────────────────────────────────────────────────── + + + + TestApp + ───────── + this + is + a + sample + sentence + and + here + are + some + wordsthis + is + a▅▅ + sample + sentence + and + here + are + some + words +  CTRL+Q  Quit  + + + ''' # --- -# name: test_horizontal_layout_width_auto_dock +# name: test_dock_scroll_off_by_one ''' - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + - HorizontalAutoWidth + ScrollOffByOne - - - - Docke - Widget 1Widget 2 - left  - 1Docked left 2 - - - - - - - - - - - - - - - - - - - + + + + ▔▔▔▔▔▔▔▔ + X 92 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 93 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 94 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 95 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 96 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 97 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 98 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 99▁▁ + ▁▁▁▁▁▁▁▁ @@ -16441,7 +16775,7 @@ ''' # --- -# name: test_input_and_focus +# name: test_focus_component_class ''' @@ -16464,135 +16798,135 @@ font-weight: 700; } - .terminal-596216952-matrix { + .terminal-3936062011-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-596216952-title { + .terminal-3936062011-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-596216952-r1 { fill: #1e1e1e } - .terminal-596216952-r2 { fill: #121212 } - .terminal-596216952-r3 { fill: #c5c8c6 } - .terminal-596216952-r4 { fill: #e2e2e2 } - .terminal-596216952-r5 { fill: #0178d4 } - .terminal-596216952-r6 { fill: #e1e1e1 } + .terminal-3936062011-r1 { fill: #c5c8c6 } + .terminal-3936062011-r2 { fill: #e3e3e3 } + .terminal-3936062011-r3 { fill: #ffdddd } + .terminal-3936062011-r4 { fill: #e1e1e1 } + .terminal-3936062011-r5 { fill: #14191f } + .terminal-3936062011-r6 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - InputApp + StyleBugApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Darren - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Burns - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - - - + + + + StyleBugApp + test widget 0 + test widget 1 + test widget 2 + test widget 3 + test widget 4 + test widget 5 + test widget 6 + test widget 7 + test widget 8 + test widget 9 + test widget 10 + test widget 11 + test widget 12▇▇ + test widget 13 + test widget 14 + test widget 15 + test widget 16 + test widget 17 + test widget 18 + test widget 19 + test widget 20 + test widget 21 @@ -16600,7 +16934,7 @@ ''' # --- -# name: test_input_suggestions +# name: test_footer_render ''' @@ -16623,146 +16957,143 @@ font-weight: 700; } - .terminal-1319604136-matrix { + .terminal-1971839132-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1319604136-title { + .terminal-1971839132-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1319604136-r1 { fill: #1e1e1e } - .terminal-1319604136-r2 { fill: #0178d4 } - .terminal-1319604136-r3 { fill: #c5c8c6 } - .terminal-1319604136-r4 { fill: #e2e2e2 } - .terminal-1319604136-r5 { fill: #1e1e1e;font-style: italic; } - .terminal-1319604136-r6 { fill: #ff0000;font-style: italic; } - .terminal-1319604136-r7 { fill: #e1e1e1 } + .terminal-1971839132-r1 { fill: #e1e1e1 } + .terminal-1971839132-r2 { fill: #c5c8c6 } + .terminal-1971839132-r3 { fill: #dde8f3;font-weight: bold } + .terminal-1971839132-r4 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - FruitsApp + FooterApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - strawberry - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + +  Q  Quit the app  ?  Show help screen  DELETE  Delete the thing  ''' # --- -# name: test_input_validation +# name: test_fr_margins ''' - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + - InputApp + TestApp - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -2 - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - 3 - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -2 - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Enter a number between 1 and 5 - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + + Hello + + + + + + + World + + + + + + + !! + + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ''' # --- -# name: test_key_display +# name: test_fr_unit_with_min ''' @@ -16945,141 +17275,144 @@ font-weight: 700; } - .terminal-1765381587-matrix { + .terminal-1354725553-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1765381587-title { + .terminal-1354725553-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1765381587-r1 { fill: #e1e1e1 } - .terminal-1765381587-r2 { fill: #c5c8c6 } - .terminal-1765381587-r3 { fill: #dde8f3;font-weight: bold } - .terminal-1765381587-r4 { fill: #ddedf9 } + .terminal-1354725553-r1 { fill: #c5c8c6 } + .terminal-1354725553-r2 { fill: #e3e3e3 } + .terminal-1354725553-r3 { fill: #ddddff } + .terminal-1354725553-r4 { fill: #e3e4e5 } + .terminal-1354725553-r5 { fill: #e2e3e3 } + .terminal-1354725553-r6 { fill: #14191f } + .terminal-1354725553-r7 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - KeyDisplayApp + ScreenSplitApp - - - - - - - - - - - - - - - - - - - - - - - - - - -  ?  Question  ^q  Quit app  Escape!  Escape  A  Letter A  + + + + ScreenSplitApp + This is content This is content number 0 + number 0This is content number 1 + This is content ▄▄This is content number 2 + number 1This is content number 3 + This is content This is content number 4▁▁ + number 2This is content number 5 + This is content This is content number 6 + number 3This is content number 7 + This is content This is content number 8 + number 4This is content number 9 + This is content This is content number 10 + number 5This is content number 11 + This is content This is content number 12 + number 6This is content number 13 + This is content This is content number 14 + number 7This is content number 15 + This is content This is content number 16 + number 8This is content number 17 + This is content This is content number 18 + number 9This is content number 19 + This is content This is content number 20 + number 10This is content number 21 + ''' # --- -# name: test_label_widths +# name: test_fr_units ''' @@ -17102,142 +17435,140 @@ font-weight: 700; } - .terminal-248448564-matrix { + .terminal-230484307-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-248448564-title { + .terminal-230484307-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-248448564-r1 { fill: #1f1f1f } - .terminal-248448564-r2 { fill: #c5c8c6 } - .terminal-248448564-r3 { fill: #00ff00 } - .terminal-248448564-r4 { fill: #1b1b1b } - .terminal-248448564-r5 { fill: #121e12 } + .terminal-230484307-r1 { fill: #ffffff } + .terminal-230484307-r2 { fill: #c5c8c6 } + .terminal-230484307-r3 { fill: #e2e2e2 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - LabelWrap + FRApp - - - - - - - - - - Apple Banana Cherry Mango Fig Guava Pineapple:Dragon Unicorn Centaur Phoenix Ch - - - Apple Banana Cherry Mango Fig Guava Pineapple:Dragon Unicorn Centaur Phoenix  - Chimera Castle - - - ╭────────────────────────────────────────────────────────────────────────────╮ - Apple Banana Cherry Mango Fig Guava Pineapple:Dragon Unicorn Centaur  - Phoenix Chimera Castle - ╰────────────────────────────────────────────────────────────────────────────╯ - - - - - - - - + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + HEADER + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + foobarbaz + + + + + + + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + FOOTER + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + ''' # --- -# name: test_layer_fix +# name: test_grid_layout_basic ''' @@ -17260,143 +17591,140 @@ font-weight: 700; } - .terminal-1675990519-matrix { + .terminal-3077119198-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1675990519-title { + .terminal-3077119198-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1675990519-r1 { fill: #c5c8c6 } - .terminal-1675990519-r2 { fill: #e3e3e3 } - .terminal-1675990519-r3 { fill: #e1e1e1 } - .terminal-1675990519-r4 { fill: #ff0000 } - .terminal-1675990519-r5 { fill: #dde8f3;font-weight: bold } - .terminal-1675990519-r6 { fill: #ddedf9 } + .terminal-3077119198-r1 { fill: #008000 } + .terminal-3077119198-r2 { fill: #c5c8c6 } + .terminal-3077119198-r3 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DialogIssueApp + GridLayoutExample - - - - DialogIssueApp - - - - - - ─────────────────────────────────────── - - - - - - This should not cause a scrollbar to ap - - - - - - ─────────────────────────────────────── - - - - -  D  Toggle the dialog  + + + + ────────────────────────────────────────────────────────────────────────── + OneTwoThree + + + + + + + + + + ────────────────────────────────────────────────────────────────────────── + ────────────────────────────────────────────────────────────────────────── + FourFiveSix + + + + + + + + + + ────────────────────────────────────────────────────────────────────────── ''' # --- -# name: test_layers +# name: test_grid_layout_basic_overflow ''' @@ -17419,153 +17747,152 @@ font-weight: 700; } - .terminal-3301495769-matrix { + .terminal-1958232742-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3301495769-title { + .terminal-1958232742-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3301495769-r1 { fill: #e1e1e1 } - .terminal-3301495769-r2 { fill: #c5c8c6 } - .terminal-3301495769-r3 { fill: #ddefef } - .terminal-3301495769-r4 { fill: #211500 } + .terminal-1958232742-r1 { fill: #008000 } + .terminal-1958232742-r2 { fill: #c5c8c6 } + .terminal-1958232742-r3 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - LayersExample + GridLayoutExample - - - - - - - - - - - - - - - box1 (layer = above) - - - - - - box2 (layer = below) - - - - - - - - - - - ''' -# --- -# name: test_layout_containers - ''' - - - + + ''' +# --- +# name: test_grid_layout_gutter + ''' + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + GridLayoutExample - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Accept  Decline  Accept  Decline  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Accept  Accept  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Decline  Decline  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▆▆ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - 00 - - 10000001000000 + + + + OneTwoThree + + + + + + + + + + + + FourFiveSix + + + + + + + + + + + ''' # --- -# name: test_line_api_scrollbars +# name: test_header_render ''' @@ -17743,132 +18059,132 @@ font-weight: 700; } - .terminal-3512435366-matrix { + .terminal-4077214022-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3512435366-title { + .terminal-4077214022-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3512435366-r1 { fill: #e1e1e1 } - .terminal-3512435366-r2 { fill: #c5c8c6 } - .terminal-3512435366-r3 { fill: #23568b } + .terminal-4077214022-r1 { fill: #c5c8c6 } + .terminal-4077214022-r2 { fill: #e3e3e3 } + .terminal-4077214022-r3 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ScrollViewApp + HeaderApp - - - - - - 11 01234567 - 12 01234567 - 13 01234567 - 14 01234567 - 15 01234567▁▁ - 16 01234567 - 17 01234567 - 18 01234567 - 19 01234567 - - 11 01234567 - 12 01234567 - 13 01234567 - 14 01234567 - 15 01234567▁▁ - 16 01234567 - 17 01234567 - 18 01234567 - 19 01234567 - - + + + + HeaderApp + + + + + + + + + + + + + + + + + + + + + + @@ -17876,7 +18192,7 @@ ''' # --- -# name: test_list_view +# name: test_horizontal_layout ''' @@ -17899,141 +18215,140 @@ font-weight: 700; } - .terminal-3746094688-matrix { + .terminal-1769115774-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3746094688-title { + .terminal-1769115774-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3746094688-r1 { fill: #e1e1e1 } - .terminal-3746094688-r2 { fill: #c5c8c6 } - .terminal-3746094688-r3 { fill: #e4e5e6 } - .terminal-3746094688-r4 { fill: #ddedf9 } + .terminal-1769115774-r1 { fill: #008000 } + .terminal-1769115774-r2 { fill: #c5c8c6 } + .terminal-1769115774-r3 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ListViewExample + HorizontalLayoutExample - - - - - - - - - - - - One - - - Two - - - Three - - - - - - - - - + + + + ────────────────────────────────────────────────────────────────────────── + OneTwoThree + + + + + + + + + + + + + + + + + + + + + + ────────────────────────────────────────────────────────────────────────── ''' # --- -# name: test_log_write +# name: test_horizontal_layout_width_auto_dock ''' @@ -18056,131 +18371,134 @@ font-weight: 700; } - .terminal-383823119-matrix { + .terminal-3689181897-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-383823119-title { + .terminal-3689181897-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-383823119-r1 { fill: #e1e1e1 } - .terminal-383823119-r2 { fill: #c5c8c6 } + .terminal-3689181897-r1 { fill: #e1f0ff } + .terminal-3689181897-r2 { fill: #c5c8c6 } + .terminal-3689181897-r3 { fill: #e1e1e1 } + .terminal-3689181897-r4 { fill: #ebf0e2 } + .terminal-3689181897-r5 { fill: #f7e0ef } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - LogApp + HorizontalAutoWidth - - - - Hello, World! - What's up? - FOO - - - - - - - - - - - - - - - - - - - - + + + + Docke + Widget 1Widget 2 + left  + 1Docked left 2 + + + + + + + + + + + + + + + + + + + @@ -18188,7 +18506,7 @@ ''' # --- -# name: test_log_write_lines +# name: test_input_and_focus ''' @@ -18211,141 +18529,143 @@ font-weight: 700; } - .terminal-1103060635-matrix { + .terminal-596216952-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1103060635-title { + .terminal-596216952-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1103060635-r1 { fill: #e1e1e1 } - .terminal-1103060635-r2 { fill: #c5c8c6 } - .terminal-1103060635-r3 { fill: #14191f } - .terminal-1103060635-r4 { fill: #23568b } + .terminal-596216952-r1 { fill: #1e1e1e } + .terminal-596216952-r2 { fill: #121212 } + .terminal-596216952-r3 { fill: #c5c8c6 } + .terminal-596216952-r4 { fill: #e2e2e2 } + .terminal-596216952-r5 { fill: #0178d4 } + .terminal-596216952-r6 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - LogApp + InputApp - - - - I must not fear.And when it has goHello, WorldFear is the mind-k - Fear is the mind-kWhere the fear hasFear is the little - Fear is the littleI must not fear.I will face my fea - I will face my fea▁▁Fear is the mind-kI will permit it t - I will permit it tFear is the littleAnd when it has go - And when it has goI will face my feaWhere the fear has - Where the fear hasI will permit it t - I must not fear.And when it has go - Fear is the mind-kWhere the fear has - Fear is the littleI must not fear. - I will face my feaFear is the mind-k - I will permit it tFear is the little - And when it has goI will face my fea - Where the fear hasI will permit it t - I must not fear.And when it has go - Fear is the mind-kWhere the fear has - Fear is the littleI must not fear. - I will face my feaFear is the mind-k - I will permit it tFear is the little - And when it has goI will face my fea▇▇ - Where the fear hasI will permit it t - I must not fear.And when it has go - Fear is the mind-kWhere the fear has - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Darren + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Burns + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + ''' # --- -# name: test_markdown_example +# name: test_input_suggestions ''' @@ -18368,140 +18688,136 @@ font-weight: 700; } - .terminal-1909492357-matrix { + .terminal-1319604136-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1909492357-title { + .terminal-1319604136-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1909492357-r1 { fill: #e1e1e1 } - .terminal-1909492357-r2 { fill: #121212 } - .terminal-1909492357-r3 { fill: #c5c8c6 } - .terminal-1909492357-r4 { fill: #0053aa } - .terminal-1909492357-r5 { fill: #dde8f3;font-weight: bold } - .terminal-1909492357-r6 { fill: #939393;font-weight: bold } - .terminal-1909492357-r7 { fill: #24292f } - .terminal-1909492357-r8 { fill: #e2e3e3;font-weight: bold } - .terminal-1909492357-r9 { fill: #4ebf71;font-weight: bold } - .terminal-1909492357-r10 { fill: #e1e1e1;font-style: italic; } - .terminal-1909492357-r11 { fill: #e1e1e1;font-weight: bold } + .terminal-1319604136-r1 { fill: #1e1e1e } + .terminal-1319604136-r2 { fill: #0178d4 } + .terminal-1319604136-r3 { fill: #c5c8c6 } + .terminal-1319604136-r4 { fill: #e2e2e2 } + .terminal-1319604136-r5 { fill: #1e1e1e;font-style: italic; } + .terminal-1319604136-r6 { fill: #ff0000;font-style: italic; } + .terminal-1319604136-r7 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MarkdownExampleApp + FruitsApp - - - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - Markdown Document - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - This is an example of Textual's Markdown widget. - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -                               Features                               - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Markdown syntax and extensions are supported. - - ● Typography emphasisstronginline code etc. - ● Headers - ● Lists (bullet and ordered) - ● Syntax highlighted code blocks - ● Tables! - - - - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + strawberry + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + @@ -18509,7 +18825,7 @@ ''' # --- -# name: test_markdown_viewer_example +# name: test_input_validation ''' @@ -18532,152 +18848,146 @@ font-weight: 700; } - .terminal-1944007215-matrix { + .terminal-922438230-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1944007215-title { + .terminal-922438230-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1944007215-r1 { fill: #c5c8c6 } - .terminal-1944007215-r2 { fill: #24292f } - .terminal-1944007215-r3 { fill: #121212 } - .terminal-1944007215-r4 { fill: #e1e1e1 } - .terminal-1944007215-r5 { fill: #e2e3e3 } - .terminal-1944007215-r6 { fill: #96989b } - .terminal-1944007215-r7 { fill: #0053aa } - .terminal-1944007215-r8 { fill: #008139 } - .terminal-1944007215-r9 { fill: #dde8f3;font-weight: bold } - .terminal-1944007215-r10 { fill: #939393;font-weight: bold } - .terminal-1944007215-r11 { fill: #14191f } - .terminal-1944007215-r12 { fill: #e2e3e3;font-weight: bold } - .terminal-1944007215-r13 { fill: #4ebf71;font-weight: bold } - .terminal-1944007215-r14 { fill: #e1e1e1;font-style: italic; } - .terminal-1944007215-r15 { fill: #e1e1e1;font-weight: bold } + .terminal-922438230-r1 { fill: #e1e1e1 } + .terminal-922438230-r2 { fill: #c5c8c6 } + .terminal-922438230-r3 { fill: #1e1e1e } + .terminal-922438230-r4 { fill: #7b3042 } + .terminal-922438230-r5 { fill: #e2e2e2 } + .terminal-922438230-r6 { fill: #3a7e4f } + .terminal-922438230-r7 { fill: #b93c5b } + .terminal-922438230-r8 { fill: #121212 } + .terminal-922438230-r9 { fill: #787878 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MarkdownExampleApp + InputApp - - - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▼  Markdown Viewer - ├──  FeaturesMarkdown Viewer - ├──  Tables - └──  Code Blocks▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - This is an example of Textual's MarkdownViewer - widget. - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▅▅ - -                  Features                  - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Markdown syntax and extensions are supported. - - ● Typography emphasisstronginline code - etc. - ● Headers - ● Lists (bullet and ordered) - ● Syntax highlighted code blocks - ● Tables! - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + -2 + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + 3 + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + -2 + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Enter a number between 1 and 5 + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + ''' # --- -# name: test_modal_dialog_bindings +# name: test_key_display ''' @@ -18700,142 +19010,141 @@ font-weight: 700; } - .terminal-543315859-matrix { + .terminal-1765381587-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-543315859-title { + .terminal-1765381587-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-543315859-r1 { fill: #c5c8c6 } - .terminal-543315859-r2 { fill: #e3e3e3 } - .terminal-543315859-r3 { fill: #e1e1e1 } - .terminal-543315859-r4 { fill: #dde8f3;font-weight: bold } - .terminal-543315859-r5 { fill: #ddedf9 } + .terminal-1765381587-r1 { fill: #e1e1e1 } + .terminal-1765381587-r2 { fill: #c5c8c6 } + .terminal-1765381587-r3 { fill: #dde8f3;font-weight: bold } + .terminal-1765381587-r4 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ModalApp + KeyDisplayApp - - - - ModalApp - Hello - - - - - - - - - - - - - - - - - - - - - -  ⏎  Open Dialog  + + + + + + + + + + + + + + + + + + + + + + + + + + +  ?  Question  ^q  Quit app  Escape!  Escape  A  Letter A  ''' # --- -# name: test_modal_dialog_bindings_input +# name: test_label_widths ''' @@ -18858,148 +19167,142 @@ font-weight: 700; } - .terminal-764470079-matrix { + .terminal-248448564-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-764470079-title { + .terminal-248448564-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-764470079-r1 { fill: #e0e0e0 } - .terminal-764470079-r2 { fill: #656565 } - .terminal-764470079-r3 { fill: #c5c8c6 } - .terminal-764470079-r4 { fill: #121212 } - .terminal-764470079-r5 { fill: #e1e1e1 } - .terminal-764470079-r6 { fill: #454a50 } - .terminal-764470079-r7 { fill: #646464 } - .terminal-764470079-r8 { fill: #24292f;font-weight: bold } - .terminal-764470079-r9 { fill: #000000 } - .terminal-764470079-r10 { fill: #63676c;font-weight: bold } - .terminal-764470079-r11 { fill: #63696e } + .terminal-248448564-r1 { fill: #1f1f1f } + .terminal-248448564-r2 { fill: #c5c8c6 } + .terminal-248448564-r3 { fill: #00ff00 } + .terminal-248448564-r4 { fill: #1b1b1b } + .terminal-248448564-r5 { fill: #121e12 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ModalApp + LabelWrap - - - - DialogModalApp - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - hi! - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  OK  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - - -  ⏎  Open Dialog  + + + + + + + + + + Apple Banana Cherry Mango Fig Guava Pineapple:Dragon Unicorn Centaur Phoenix Ch + + + Apple Banana Cherry Mango Fig Guava Pineapple:Dragon Unicorn Centaur Phoenix  + Chimera Castle + + + ╭────────────────────────────────────────────────────────────────────────────╮ + Apple Banana Cherry Mango Fig Guava Pineapple:Dragon Unicorn Centaur  + Phoenix Chimera Castle + ╰────────────────────────────────────────────────────────────────────────────╯ + + + + + + + ''' # --- -# name: test_multiple_css +# name: test_layer_fix ''' @@ -19022,141 +19325,143 @@ font-weight: 700; } - .terminal-1292433193-matrix { + .terminal-1675990519-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1292433193-title { + .terminal-1675990519-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1292433193-r1 { fill: #8b0000 } - .terminal-1292433193-r2 { fill: #c5c8c6 } - .terminal-1292433193-r3 { fill: #ff0000 } - .terminal-1292433193-r4 { fill: #e1e1e1 } - - - - - + .terminal-1675990519-r1 { fill: #c5c8c6 } + .terminal-1675990519-r2 { fill: #e3e3e3 } + .terminal-1675990519-r3 { fill: #e1e1e1 } + .terminal-1675990519-r4 { fill: #ff0000 } + .terminal-1675990519-r5 { fill: #dde8f3;font-weight: bold } + .terminal-1675990519-r6 { fill: #ddedf9 } + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MultipleCSSApp + DialogIssueApp - - - - #one - #two - - - - - - - - - - - - - - - - - - - - - - + + + + DialogIssueApp + + + + + + ─────────────────────────────────────── + + + + + + This should not cause a scrollbar to ap + + + + + + ─────────────────────────────────────── + + + + +  D  Toggle the dialog  ''' # --- -# name: test_nested_auto_heights +# name: test_layers ''' @@ -19179,135 +19484,133 @@ font-weight: 700; } - .terminal-3700945997-matrix { + .terminal-3301495769-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3700945997-title { + .terminal-3301495769-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3700945997-r1 { fill: #90ee90 } - .terminal-3700945997-r2 { fill: #c5c8c6 } - .terminal-3700945997-r3 { fill: #add8e6 } - .terminal-3700945997-r4 { fill: #808080 } - .terminal-3700945997-r5 { fill: #dddddd } - .terminal-3700945997-r6 { fill: #ffdddd } + .terminal-3301495769-r1 { fill: #e1e1e1 } + .terminal-3301495769-r2 { fill: #c5c8c6 } + .terminal-3301495769-r3 { fill: #ddefef } + .terminal-3301495769-r4 { fill: #211500 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - NestedAutoApp + LayersExample - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ━━━━━━━━━━━━━━━ - ━━━━━━━━━━━━━ - JUST ONE LINE - ━━━━━━━━━━━━━ - ━━━━━━━━━━━━━━━ - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + box1 (layer = above) + + + + + + box2 (layer = below) + + + + + @@ -19315,7 +19618,7 @@ ''' # --- -# name: test_nested_fr +# name: test_layout_containers ''' @@ -19338,141 +19641,151 @@ font-weight: 700; } - .terminal-4113050056-matrix { + .terminal-3743315821-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4113050056-title { + .terminal-3743315821-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4113050056-r1 { fill: #ffffff } - .terminal-4113050056-r2 { fill: #c5c8c6 } - .terminal-4113050056-r3 { fill: #ffff00 } - .terminal-4113050056-r4 { fill: #002121 } + .terminal-3743315821-r1 { fill: #7ae998 } + .terminal-3743315821-r2 { fill: #e76580 } + .terminal-3743315821-r3 { fill: #1e1e1e } + .terminal-3743315821-r4 { fill: #121212 } + .terminal-3743315821-r5 { fill: #c5c8c6 } + .terminal-3743315821-r6 { fill: #4ebf71;font-weight: bold } + .terminal-3743315821-r7 { fill: #f5e5e9;font-weight: bold } + .terminal-3743315821-r8 { fill: #e2e2e2 } + .terminal-3743315821-r9 { fill: #0a180e;font-weight: bold } + .terminal-3743315821-r10 { fill: #008139 } + .terminal-3743315821-r11 { fill: #780028 } + .terminal-3743315821-r12 { fill: #e1e1e1 } + .terminal-3743315821-r13 { fill: #23568b } + .terminal-3743315821-r14 { fill: #14191f } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - AutoApp + MyApp - - - - ────────────────────────────────────────────────────────────────────────────── - ──────────────────────────────────────────────────────────────────────────── - Hello - World! - foo - - - - - - - - - - - - - - - - - - ──────────────────────────────────────────────────────────────────────────── - ────────────────────────────────────────────────────────────────────────────── + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + AcceptDeclineAcceptDecline + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + AcceptAccept + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + DeclineDecline + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▆▆ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + 00 + + 10000001000000 ''' # --- -# name: test_notifications_example +# name: test_line_api_scrollbars ''' @@ -19495,139 +19808,132 @@ font-weight: 700; } - .terminal-3970684023-matrix { + .terminal-3512435366-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3970684023-title { + .terminal-3512435366-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3970684023-r1 { fill: #e1e1e1 } - .terminal-3970684023-r2 { fill: #c5c8c6 } - .terminal-3970684023-r3 { fill: #56c278 } - .terminal-3970684023-r4 { fill: #2e3339 } - .terminal-3970684023-r5 { fill: #e3e4e4 } - .terminal-3970684023-r6 { fill: #feaa35 } - .terminal-3970684023-r7 { fill: #e89719;font-weight: bold } - .terminal-3970684023-r8 { fill: #e3e4e4;font-weight: bold } - .terminal-3970684023-r9 { fill: #e3e4e4;font-weight: bold;font-style: italic; } - .terminal-3970684023-r10 { fill: #bc4563 } + .terminal-3512435366-r1 { fill: #e1e1e1 } + .terminal-3512435366-r2 { fill: #c5c8c6 } + .terminal-3512435366-r3 { fill: #23568b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ToastApp + ScrollViewApp - - - - - - - - It's an older code, sir, but it  - checks out. - - - - Possible trap detected - Now witness the firepower of this - fully ARMED and OPERATIONAL - battle station! - - - - It's a trap! - - - - It's against my programming to  - impersonate a deity. - + + + + + + 11 01234567 + 12 01234567 + 13 01234567 + 14 01234567 + 15 01234567▁▁ + 16 01234567 + 17 01234567 + 18 01234567 + 19 01234567 + + 11 01234567 + 12 01234567 + 13 01234567 + 14 01234567 + 15 01234567▁▁ + 16 01234567 + 17 01234567 + 18 01234567 + 19 01234567 + + @@ -19635,7 +19941,7 @@ ''' # --- -# name: test_notifications_through_modes +# name: test_list_view ''' @@ -19658,143 +19964,142 @@ font-weight: 700; } - .terminal-2782348326-matrix { + .terminal-3746094688-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2782348326-title { + .terminal-3746094688-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2782348326-r1 { fill: #e1e1e1 } - .terminal-2782348326-r2 { fill: #56c278 } - .terminal-2782348326-r3 { fill: #c5c8c6 } - .terminal-2782348326-r4 { fill: #2e3339 } - .terminal-2782348326-r5 { fill: #e3e4e4 } + .terminal-3746094688-r1 { fill: #e1e1e1 } + .terminal-3746094688-r2 { fill: #c5c8c6 } + .terminal-3746094688-r3 { fill: #e4e5e6 } + .terminal-3746094688-r4 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - NotifyThroughModesApp + ListViewExample - - - - This is a mode screen - 4 - - - - 5 - - - - 6 - - - - 7 - - - - 8 - - - - 9 - - - - - - - ''' -# --- -# name: test_notifications_through_screens - ''' + + + + + + + + + + + + One + + + Two + + + Three + + + + + + + + + + + + + + ''' +# --- +# name: test_log_write + ''' - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - NotifyDownScreensApp + LogApp - - - - Screen 10 - 4 - - - - 5 - - - - 6 - - - - 7 - - - - 8 - - - - 9 - + + + + Hello, World! + What's up? + FOO + + + + + + + + + + + + + + + + + + + + @@ -19951,7 +20253,7 @@ ''' # --- -# name: test_offsets +# name: test_log_write_lines ''' @@ -19974,151 +20276,151 @@ font-weight: 700; } - .terminal-4150241775-matrix { + .terminal-1103060635-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4150241775-title { + .terminal-1103060635-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4150241775-r1 { fill: #e1e1e1 } - .terminal-4150241775-r2 { fill: #c5c8c6 } - .terminal-4150241775-r3 { fill: #ffffff } - .terminal-4150241775-r4 { fill: #ddddef } + .terminal-1103060635-r1 { fill: #e1e1e1 } + .terminal-1103060635-r2 { fill: #c5c8c6 } + .terminal-1103060635-r3 { fill: #14191f } + .terminal-1103060635-r4 { fill: #23568b } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OffsetsApp + LogApp - - - - - - - - - ────────────── - FOO - BAR - BAZ - ────────────── - - - - - - ────────────── - FOO - BAR - BAZ - ────────────── - - - - - - - - - ''' -# --- -# name: test_option_list_build - ''' - - - + + ''' +# --- +# name: test_markdown_example + ''' + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OptionListApp + MarkdownExampleApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - OneOneOne - TwoTwoTwo - ──────────────────────────────────────────────────────────────────── - ThreeThreeThree - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - - - + + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Markdown Document + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + This is an example of Textual's Markdown widget. + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +                               Features                               + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Markdown syntax and extensions are supported. + + ● Typography emphasisstronginline code etc. + ● Headers + ● Lists (bullet and ordered) + ● Syntax highlighted code blocks + ● Tables! + + + + @@ -20269,7 +20574,7 @@ ''' # --- -# name: test_option_list_options +# name: test_markdown_viewer_example ''' @@ -20292,148 +20597,152 @@ font-weight: 700; } - .terminal-371403050-matrix { + .terminal-1944007215-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-371403050-title { + .terminal-1944007215-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-371403050-r1 { fill: #c5c8c6 } - .terminal-371403050-r2 { fill: #e3e3e3 } - .terminal-371403050-r3 { fill: #e1e1e1 } - .terminal-371403050-r4 { fill: #1e1e1e } - .terminal-371403050-r5 { fill: #0178d4 } - .terminal-371403050-r6 { fill: #ddedf9;font-weight: bold } - .terminal-371403050-r7 { fill: #e2e2e2 } - .terminal-371403050-r8 { fill: #434343 } - .terminal-371403050-r9 { fill: #787878 } - .terminal-371403050-r10 { fill: #14191f } - .terminal-371403050-r11 { fill: #ddedf9 } + .terminal-1944007215-r1 { fill: #c5c8c6 } + .terminal-1944007215-r2 { fill: #24292f } + .terminal-1944007215-r3 { fill: #121212 } + .terminal-1944007215-r4 { fill: #e1e1e1 } + .terminal-1944007215-r5 { fill: #e2e3e3 } + .terminal-1944007215-r6 { fill: #96989b } + .terminal-1944007215-r7 { fill: #0053aa } + .terminal-1944007215-r8 { fill: #008139 } + .terminal-1944007215-r9 { fill: #dde8f3;font-weight: bold } + .terminal-1944007215-r10 { fill: #939393;font-weight: bold } + .terminal-1944007215-r11 { fill: #14191f } + .terminal-1944007215-r12 { fill: #e2e3e3;font-weight: bold } + .terminal-1944007215-r13 { fill: #4ebf71;font-weight: bold } + .terminal-1944007215-r14 { fill: #e1e1e1;font-style: italic; } + .terminal-1944007215-r15 { fill: #e1e1e1;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OptionListApp + MarkdownExampleApp - - - - OptionListApp - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Aerilon - Aquaria - ─────────────────────────────────────────────────── - Canceron - Caprica - ─────────────────────────────────────────────────── - Gemenon - ─────────────────────────────────────────────────── - Leonis - Libran - ─────────────────────────────────────────────────── - Picon - ─────────────────────────────────────────────────── - Sagittaron▄▄ - Scorpia - ─────────────────────────────────────────────────── - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - + + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▼  Markdown Viewer + ├──  FeaturesMarkdown Viewer + ├──  Tables + └──  Code Blocks▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + This is an example of Textual's MarkdownViewer + widget. + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▅▅ + +                  Features                  + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Markdown syntax and extensions are supported. + + ● Typography emphasisstronginline code + etc. + ● Headers + ● Lists (bullet and ordered) + ● Syntax highlighted code blocks + ● Tables! + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ''' # --- -# name: test_option_list_replace_prompt_from_single_line_to_single_line +# name: test_modal_dialog_bindings ''' @@ -20456,145 +20765,142 @@ font-weight: 700; } - .terminal-1891202557-matrix { + .terminal-543315859-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1891202557-title { + .terminal-543315859-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1891202557-r1 { fill: #c5c8c6 } - .terminal-1891202557-r2 { fill: #e3e3e3 } - .terminal-1891202557-r3 { fill: #1e1e1e } - .terminal-1891202557-r4 { fill: #0178d4 } - .terminal-1891202557-r5 { fill: #ddedf9;font-weight: bold } - .terminal-1891202557-r6 { fill: #e2e2e2 } - .terminal-1891202557-r7 { fill: #e1e1e1 } - .terminal-1891202557-r8 { fill: #ddedf9 } + .terminal-543315859-r1 { fill: #c5c8c6 } + .terminal-543315859-r2 { fill: #e3e3e3 } + .terminal-543315859-r3 { fill: #e1e1e1 } + .terminal-543315859-r4 { fill: #dde8f3;font-weight: bold } + .terminal-543315859-r5 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OptionListApp + ModalApp - - - - OptionListApp - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - 1. Another single line - 2. Two - lines - 3. Three - lines - of text - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - + + + + ModalApp + Hello + + + + + + + + + + + + + + + + + + + + + +  ⏎  Open Dialog  ''' # --- -# name: test_option_list_replace_prompt_from_single_line_to_two_lines +# name: test_modal_dialog_bindings_input ''' @@ -20617,145 +20923,148 @@ font-weight: 700; } - .terminal-2188746417-matrix { + .terminal-2766044148-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2188746417-title { + .terminal-2766044148-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2188746417-r1 { fill: #c5c8c6 } - .terminal-2188746417-r2 { fill: #e3e3e3 } - .terminal-2188746417-r3 { fill: #1e1e1e } - .terminal-2188746417-r4 { fill: #0178d4 } - .terminal-2188746417-r5 { fill: #ddedf9;font-weight: bold } - .terminal-2188746417-r6 { fill: #e2e2e2 } - .terminal-2188746417-r7 { fill: #e1e1e1 } - .terminal-2188746417-r8 { fill: #ddedf9 } + .terminal-2766044148-r1 { fill: #e0e0e0 } + .terminal-2766044148-r2 { fill: #656565 } + .terminal-2766044148-r3 { fill: #c5c8c6 } + .terminal-2766044148-r4 { fill: #121212 } + .terminal-2766044148-r5 { fill: #e1e1e1 } + .terminal-2766044148-r6 { fill: #454a50 } + .terminal-2766044148-r7 { fill: #646464 } + .terminal-2766044148-r8 { fill: #24292f;font-weight: bold } + .terminal-2766044148-r9 { fill: #000000 } + .terminal-2766044148-r10 { fill: #63676c;font-weight: bold } + .terminal-2766044148-r11 { fill: #63696e } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OptionListApp + ModalApp - - - - OptionListApp - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - 1. Two - lines - 2. Two - lines - 3. Three - lines - of text - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - + + + + DialogModalApp + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + hi! + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + OK + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + +  ⏎  Open Dialog  ''' # --- -# name: test_option_list_replace_prompt_from_two_lines_to_three_lines +# name: test_multiple_css ''' @@ -20778,137 +21087,133 @@ font-weight: 700; } - .terminal-2667681921-matrix { + .terminal-1292433193-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2667681921-title { + .terminal-1292433193-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2667681921-r1 { fill: #c5c8c6 } - .terminal-2667681921-r2 { fill: #e3e3e3 } - .terminal-2667681921-r3 { fill: #1e1e1e } - .terminal-2667681921-r4 { fill: #0178d4 } - .terminal-2667681921-r5 { fill: #ddedf9;font-weight: bold } - .terminal-2667681921-r6 { fill: #e2e2e2 } - .terminal-2667681921-r7 { fill: #e1e1e1 } - .terminal-2667681921-r8 { fill: #ddedf9 } + .terminal-1292433193-r1 { fill: #8b0000 } + .terminal-1292433193-r2 { fill: #c5c8c6 } + .terminal-1292433193-r3 { fill: #ff0000 } + .terminal-1292433193-r4 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OptionListApp + MultipleCSSApp - - - - OptionListApp - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - 1. Single line - 1. Three - lines - of text - 3. Three - lines - of text - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - + + + + #one + #two + + + + + + + + + + + + + + + + + + + + + @@ -20916,7 +21221,7 @@ ''' # --- -# name: test_option_list_strings +# name: test_nested_auto_heights ''' @@ -20939,137 +21244,135 @@ font-weight: 700; } - .terminal-2341816165-matrix { + .terminal-3700945997-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2341816165-title { + .terminal-3700945997-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2341816165-r1 { fill: #c5c8c6 } - .terminal-2341816165-r2 { fill: #e3e3e3 } - .terminal-2341816165-r3 { fill: #e1e1e1 } - .terminal-2341816165-r4 { fill: #1e1e1e } - .terminal-2341816165-r5 { fill: #0178d4 } - .terminal-2341816165-r6 { fill: #ddedf9;font-weight: bold } - .terminal-2341816165-r7 { fill: #e2e2e2 } - .terminal-2341816165-r8 { fill: #ddedf9 } + .terminal-3700945997-r1 { fill: #90ee90 } + .terminal-3700945997-r2 { fill: #c5c8c6 } + .terminal-3700945997-r3 { fill: #add8e6 } + .terminal-3700945997-r4 { fill: #808080 } + .terminal-3700945997-r5 { fill: #dddddd } + .terminal-3700945997-r6 { fill: #ffdddd } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OptionListApp + NestedAutoApp - - - - OptionListApp - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Aerilon - Aquaria - Canceron - Caprica - Gemenon - Leonis - Libran - Picon - Sagittaron - Scorpia - Tauron - Virgon - - - - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + ━━━━━━━━━━━━━━━ + ━━━━━━━━━━━━━ + JUST ONE LINE + ━━━━━━━━━━━━━ + ━━━━━━━━━━━━━━━ + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + + + + + + + + + + + + + + + @@ -21077,7 +21380,7 @@ ''' # --- -# name: test_option_list_tables +# name: test_nested_fr ''' @@ -21100,149 +21403,141 @@ font-weight: 700; } - .terminal-228828675-matrix { + .terminal-4113050056-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-228828675-title { + .terminal-4113050056-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-228828675-r1 { fill: #c5c8c6 } - .terminal-228828675-r2 { fill: #e3e3e3 } - .terminal-228828675-r3 { fill: #e1e1e1 } - .terminal-228828675-r4 { fill: #1e1e1e } - .terminal-228828675-r5 { fill: #0178d4 } - .terminal-228828675-r6 { fill: #ddedf9;font-weight: bold;font-style: italic; } - .terminal-228828675-r7 { fill: #e2e2e2 } - .terminal-228828675-r8 { fill: #ddedf9;font-weight: bold } - .terminal-228828675-r9 { fill: #14191f } - .terminal-228828675-r10 { fill: #e2e2e2;font-style: italic; } - .terminal-228828675-r11 { fill: #e2e2e2;font-weight: bold } - .terminal-228828675-r12 { fill: #ddedf9 } + .terminal-4113050056-r1 { fill: #ffffff } + .terminal-4113050056-r2 { fill: #c5c8c6 } + .terminal-4113050056-r3 { fill: #ffff00 } + .terminal-4113050056-r4 { fill: #002121 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - OptionListApp + AutoApp - - - - OptionListApp - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -                  Data for Aerilon                   - ┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ - Patron God   Population    Capital City   - ┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩▃▃ - Demeter      1.2 Billion   Gaoth          - └───────────────┴────────────────┴────────────────┘ -                  Data for Aquaria                   - ┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓ - Patron God   Population   Capital City    - ┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩ - Hermes       75,000       None            - └───────────────┴───────────────┴─────────────────┘ -                  Data for Canceron                  - ┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ - Patron God   Population    Capital City   - ┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩ - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - + + + + ────────────────────────────────────────────────────────────────────────────── + ──────────────────────────────────────────────────────────────────────────── + Hello + World! + foo + + + + + + + + + + + + + + + + + + ──────────────────────────────────────────────────────────────────────────── + ────────────────────────────────────────────────────────────────────────────── ''' # --- -# name: test_order_independence +# name: test_notifications_example ''' @@ -21265,143 +21560,147 @@ font-weight: 700; } - .terminal-1392305496-matrix { + .terminal-3970684023-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1392305496-title { + .terminal-3970684023-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1392305496-r1 { fill: #ffff00 } - .terminal-1392305496-r2 { fill: #e3e3e3 } - .terminal-1392305496-r3 { fill: #c5c8c6 } - .terminal-1392305496-r4 { fill: #e1e1e1 } - .terminal-1392305496-r5 { fill: #dde8f3;font-weight: bold } - .terminal-1392305496-r6 { fill: #ddedf9 } + .terminal-3970684023-r1 { fill: #e1e1e1 } + .terminal-3970684023-r2 { fill: #c5c8c6 } + .terminal-3970684023-r3 { fill: #56c278 } + .terminal-3970684023-r4 { fill: #2e3339 } + .terminal-3970684023-r5 { fill: #e3e4e4 } + .terminal-3970684023-r6 { fill: #feaa35 } + .terminal-3970684023-r7 { fill: #e89719;font-weight: bold } + .terminal-3970684023-r8 { fill: #e3e4e4;font-weight: bold } + .terminal-3970684023-r9 { fill: #e3e4e4;font-weight: bold;font-style: italic; } + .terminal-3970684023-r10 { fill: #bc4563 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Layers + ToastApp - - - - ──────────────────────────────────Layers - It's full of stars! My God! It's full of sta - - This should float over the top - - - ────────────────────────────────── - - - - - - - - - - - - - - - - -  T  Toggle Screen  + + + + + + + + It's an older code, sir, but it  + checks out. + + + + Possible trap detected + Now witness the firepower of this + fully ARMED and OPERATIONAL + battle station! + + + + It's a trap! + + + + It's against my programming to  + impersonate a deity. + + ''' # --- -# name: test_order_independence_toggle +# name: test_notifications_through_modes ''' @@ -21424,143 +21723,142 @@ font-weight: 700; } - .terminal-3727479996-matrix { + .terminal-2782348326-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3727479996-title { + .terminal-2782348326-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3727479996-r1 { fill: #ffff00 } - .terminal-3727479996-r2 { fill: #e3e3e3 } - .terminal-3727479996-r3 { fill: #c5c8c6 } - .terminal-3727479996-r4 { fill: #ddeedd } - .terminal-3727479996-r5 { fill: #dde8f3;font-weight: bold } - .terminal-3727479996-r6 { fill: #ddedf9 } + .terminal-2782348326-r1 { fill: #e1e1e1 } + .terminal-2782348326-r2 { fill: #56c278 } + .terminal-2782348326-r3 { fill: #c5c8c6 } + .terminal-2782348326-r4 { fill: #2e3339 } + .terminal-2782348326-r5 { fill: #e3e4e4 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Layers + NotifyThroughModesApp - - - - ──────────────────────────────────Layers - It's full of stars! My God! It's full of sta - - This should float over the top - - - ────────────────────────────────── - - - - - - - - - - - - - - - - -  T  Toggle Screen  + + + + This is a mode screen + 4 + + + + 5 + + + + 6 + + + + 7 + + + + 8 + + + + 9 + + ''' # --- -# name: test_placeholder_render +# name: test_notifications_through_screens ''' @@ -21583,142 +21881,134 @@ font-weight: 700; } - .terminal-1570661136-matrix { + .terminal-180633759-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1570661136-title { + .terminal-180633759-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1570661136-r1 { fill: #c5c8c6 } - .terminal-1570661136-r2 { fill: #eae3e5 } - .terminal-1570661136-r3 { fill: #e8e0e7 } - .terminal-1570661136-r4 { fill: #efe9e4 } - .terminal-1570661136-r5 { fill: #ede6e6 } - .terminal-1570661136-r6 { fill: #efeedf } - .terminal-1570661136-r7 { fill: #e9eee5 } - .terminal-1570661136-r8 { fill: #e2edeb } - .terminal-1570661136-r9 { fill: #e4eee8;font-weight: bold } - .terminal-1570661136-r10 { fill: #dfebed;font-weight: bold } - .terminal-1570661136-r11 { fill: #dfe9ed } - .terminal-1570661136-r12 { fill: #e3e6eb;font-weight: bold } - .terminal-1570661136-r13 { fill: #e6e3e9 } + .terminal-180633759-r1 { fill: #e1e1e1 } + .terminal-180633759-r2 { fill: #56c278 } + .terminal-180633759-r3 { fill: #c5c8c6 } + .terminal-180633759-r4 { fill: #2e3339 } + .terminal-180633759-r5 { fill: #e3e4e4 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - PlaceholderApp + NotifyDownScreensApp - - - - - Placeholder p2 here! - This is a custom label for p1. - #p4 - #p3#p5Placeholde - r - - Lorem ipsum dolor sit  - 26 x 6amet, consectetur 27 x 6 - adipiscing elit. Etiam  - feugiat ac elit sit amet  - - - Lorem ipsum dolor sit amet,  - consectetur adipiscing elit. Etiam 40 x 6 - feugiat ac elit sit amet accumsan.  - Suspendisse bibendum nec libero quis  - gravida. Phasellus id eleifend ligula. - Nullam imperdiet sem tellus, sed  - vehicula nisl faucibus sit amet. Lorem ipsum dolor sit amet,  - Praesent iaculis tempor ultricies. Sedconsectetur adipiscing elit. Etiam  - lacinia, tellus id rutrum lacinia, feugiat ac elit sit amet accumsan.  - sapien sapien congue mauris, sit amet Suspendisse bibendum nec libero quis  + + + + Screen 10 + 4 + + + + 5 + + + + 6 + + + + 7 + + + + 8 + + + + 9 + @@ -21726,7 +22016,7 @@ ''' # --- -# name: test_print_capture +# name: test_offsets ''' @@ -21749,131 +22039,133 @@ font-weight: 700; } - .terminal-3935013562-matrix { + .terminal-4150241775-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3935013562-title { + .terminal-4150241775-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3935013562-r1 { fill: #e1e1e1 } - .terminal-3935013562-r2 { fill: #c5c8c6 } + .terminal-4150241775-r1 { fill: #e1e1e1 } + .terminal-4150241775-r2 { fill: #c5c8c6 } + .terminal-4150241775-r3 { fill: #ffffff } + .terminal-4150241775-r4 { fill: #ddddef } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - CaptureApp + OffsetsApp - - - - RichLog - This will be captured! - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + ────────────── + FOO + BAR + BAZ + ────────────── + + + + + + ────────────── + FOO + BAR + BAZ + ────────────── + + + @@ -21881,7 +22173,7 @@ ''' # --- -# name: test_programmatic_scrollbar_gutter_change +# name: test_option_list_build ''' @@ -21904,131 +22196,137 @@ font-weight: 700; } - .terminal-2761669360-matrix { + .terminal-72094857-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2761669360-title { + .terminal-72094857-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2761669360-r1 { fill: #ffdddd } - .terminal-2761669360-r2 { fill: #c5c8c6 } + .terminal-72094857-r1 { fill: #1e1e1e } + .terminal-72094857-r2 { fill: #0178d4 } + .terminal-72094857-r3 { fill: #c5c8c6 } + .terminal-72094857-r4 { fill: #ddedf9;font-weight: bold } + .terminal-72094857-r5 { fill: #e2e2e2;font-weight: bold } + .terminal-72094857-r6 { fill: #e2e2e2 } + .terminal-72094857-r7 { fill: #434343 } + .terminal-72094857-r8 { fill: #cc555a } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ProgrammaticScrollbarGutterChange + OptionListApp - - - - onetwo - - - - - - - - - - - - threefour - - - - - - - - - - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + OneOneOne + TwoTwoTwo + ──────────────────────────────────────────────────────────────────── + ThreeThreeThree + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + @@ -22036,7 +22334,7 @@ ''' # --- -# name: test_progress_bar_completed +# name: test_option_list_options ''' @@ -22059,142 +22357,148 @@ font-weight: 700; } - .terminal-230009450-matrix { + .terminal-371403050-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-230009450-title { + .terminal-371403050-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-230009450-r1 { fill: #c5c8c6 } - .terminal-230009450-r2 { fill: #e1e1e1 } - .terminal-230009450-r3 { fill: #4ebf71 } - .terminal-230009450-r4 { fill: #dde8f3;font-weight: bold } - .terminal-230009450-r5 { fill: #ddedf9 } + .terminal-371403050-r1 { fill: #c5c8c6 } + .terminal-371403050-r2 { fill: #e3e3e3 } + .terminal-371403050-r3 { fill: #e1e1e1 } + .terminal-371403050-r4 { fill: #1e1e1e } + .terminal-371403050-r5 { fill: #0178d4 } + .terminal-371403050-r6 { fill: #ddedf9;font-weight: bold } + .terminal-371403050-r7 { fill: #e2e2e2 } + .terminal-371403050-r8 { fill: #434343 } + .terminal-371403050-r9 { fill: #787878 } + .terminal-371403050-r10 { fill: #14191f } + .terminal-371403050-r11 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - IndeterminateProgressBar + OptionListApp - - - - - - - - - - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━100%--:--:-- - - - - - - - - - - - -  S  Start  + + + + OptionListApp + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Aerilon + Aquaria + ─────────────────────────────────────────────────── + Canceron + Caprica + ─────────────────────────────────────────────────── + Gemenon + ─────────────────────────────────────────────────── + Leonis + Libran + ─────────────────────────────────────────────────── + Picon + ─────────────────────────────────────────────────── + Sagittaron▄▄ + Scorpia + ─────────────────────────────────────────────────── + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + ''' # --- -# name: test_progress_bar_completed_styled +# name: test_option_list_replace_prompt_from_single_line_to_single_line ''' @@ -22217,144 +22521,145 @@ font-weight: 700; } - .terminal-3162092160-matrix { + .terminal-1891202557-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3162092160-title { + .terminal-1891202557-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3162092160-r1 { fill: #c5c8c6 } - .terminal-3162092160-r2 { fill: #e1e1e1 } - .terminal-3162092160-r3 { fill: #b93c5b } - .terminal-3162092160-r4 { fill: #1e1e1e } - .terminal-3162092160-r5 { fill: #e1e1e1;text-decoration: underline; } - .terminal-3162092160-r6 { fill: #dde8f3;font-weight: bold } - .terminal-3162092160-r7 { fill: #ddedf9 } + .terminal-1891202557-r1 { fill: #c5c8c6 } + .terminal-1891202557-r2 { fill: #e3e3e3 } + .terminal-1891202557-r3 { fill: #1e1e1e } + .terminal-1891202557-r4 { fill: #0178d4 } + .terminal-1891202557-r5 { fill: #ddedf9;font-weight: bold } + .terminal-1891202557-r6 { fill: #e2e2e2 } + .terminal-1891202557-r7 { fill: #e1e1e1 } + .terminal-1891202557-r8 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - StyledProgressBar + OptionListApp - - - - - - - - - - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━100%--:--:-- - - - - - - - - - - - -  S  Start  + + + + OptionListApp + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + 1. Another single line + 2. Two + lines + 3. Three + lines + of text + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + ''' # --- -# name: test_progress_bar_halfway +# name: test_option_list_replace_prompt_from_single_line_to_two_lines ''' @@ -22377,143 +22682,145 @@ font-weight: 700; } - .terminal-1630089489-matrix { + .terminal-2188746417-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1630089489-title { + .terminal-2188746417-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1630089489-r1 { fill: #c5c8c6 } - .terminal-1630089489-r2 { fill: #e1e1e1 } - .terminal-1630089489-r3 { fill: #fea62b } - .terminal-1630089489-r4 { fill: #323232 } - .terminal-1630089489-r5 { fill: #dde8f3;font-weight: bold } - .terminal-1630089489-r6 { fill: #ddedf9 } + .terminal-2188746417-r1 { fill: #c5c8c6 } + .terminal-2188746417-r2 { fill: #e3e3e3 } + .terminal-2188746417-r3 { fill: #1e1e1e } + .terminal-2188746417-r4 { fill: #0178d4 } + .terminal-2188746417-r5 { fill: #ddedf9;font-weight: bold } + .terminal-2188746417-r6 { fill: #e2e2e2 } + .terminal-2188746417-r7 { fill: #e1e1e1 } + .terminal-2188746417-r8 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - IndeterminateProgressBar + OptionListApp - - - - - - - - - - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━39%00:00:07 - - - - - - - - - - - -  S  Start  + + + + OptionListApp + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + 1. Two + lines + 2. Two + lines + 3. Three + lines + of text + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + ''' # --- -# name: test_progress_bar_halfway_styled +# name: test_option_list_replace_prompt_from_two_lines_to_three_lines ''' @@ -22536,145 +22843,145 @@ font-weight: 700; } - .terminal-1532901142-matrix { + .terminal-2667681921-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1532901142-title { + .terminal-2667681921-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1532901142-r1 { fill: #c5c8c6 } - .terminal-1532901142-r2 { fill: #e1e1e1 } - .terminal-1532901142-r3 { fill: #004578 } - .terminal-1532901142-r4 { fill: #152939 } - .terminal-1532901142-r5 { fill: #1e1e1e } - .terminal-1532901142-r6 { fill: #e1e1e1;text-decoration: underline; } - .terminal-1532901142-r7 { fill: #dde8f3;font-weight: bold } - .terminal-1532901142-r8 { fill: #ddedf9 } + .terminal-2667681921-r1 { fill: #c5c8c6 } + .terminal-2667681921-r2 { fill: #e3e3e3 } + .terminal-2667681921-r3 { fill: #1e1e1e } + .terminal-2667681921-r4 { fill: #0178d4 } + .terminal-2667681921-r5 { fill: #ddedf9;font-weight: bold } + .terminal-2667681921-r6 { fill: #e2e2e2 } + .terminal-2667681921-r7 { fill: #e1e1e1 } + .terminal-2667681921-r8 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - StyledProgressBar + OptionListApp - - - - - - - - - - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━39%00:00:07 - - - - - - - - - - - -  S  Start  + + + + OptionListApp + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + 1. Single line + 1. Three + lines + of text + 3. Three + lines + of text + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + ''' # --- -# name: test_progress_bar_indeterminate +# name: test_option_list_strings ''' @@ -22697,143 +23004,145 @@ font-weight: 700; } - .terminal-3440292978-matrix { + .terminal-2341816165-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3440292978-title { + .terminal-2341816165-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3440292978-r1 { fill: #c5c8c6 } - .terminal-3440292978-r2 { fill: #e1e1e1 } - .terminal-3440292978-r3 { fill: #323232 } - .terminal-3440292978-r4 { fill: #b93c5b } - .terminal-3440292978-r5 { fill: #dde8f3;font-weight: bold } - .terminal-3440292978-r6 { fill: #ddedf9 } + .terminal-2341816165-r1 { fill: #c5c8c6 } + .terminal-2341816165-r2 { fill: #e3e3e3 } + .terminal-2341816165-r3 { fill: #e1e1e1 } + .terminal-2341816165-r4 { fill: #1e1e1e } + .terminal-2341816165-r5 { fill: #0178d4 } + .terminal-2341816165-r6 { fill: #ddedf9;font-weight: bold } + .terminal-2341816165-r7 { fill: #e2e2e2 } + .terminal-2341816165-r8 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - IndeterminateProgressBar + OptionListApp - - - - - - - - - - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━--%--:--:-- - - - - - - - - - - - -  S  Start  + + + + OptionListApp + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Aerilon + Aquaria + Canceron + Caprica + Gemenon + Leonis + Libran + Picon + Sagittaron + Scorpia + Tauron + Virgon + + + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + ''' # --- -# name: test_progress_bar_indeterminate_styled +# name: test_option_list_tables ''' @@ -22856,145 +23165,149 @@ font-weight: 700; } - .terminal-4046569674-matrix { + .terminal-228828675-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4046569674-title { + .terminal-228828675-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4046569674-r1 { fill: #c5c8c6 } - .terminal-4046569674-r2 { fill: #e1e1e1 } - .terminal-4046569674-r3 { fill: #fea62b } - .terminal-4046569674-r4 { fill: #004578 } - .terminal-4046569674-r5 { fill: #1e1e1e } - .terminal-4046569674-r6 { fill: #e1e1e1;text-decoration: underline; } - .terminal-4046569674-r7 { fill: #dde8f3;font-weight: bold } - .terminal-4046569674-r8 { fill: #ddedf9 } + .terminal-228828675-r1 { fill: #c5c8c6 } + .terminal-228828675-r2 { fill: #e3e3e3 } + .terminal-228828675-r3 { fill: #e1e1e1 } + .terminal-228828675-r4 { fill: #1e1e1e } + .terminal-228828675-r5 { fill: #0178d4 } + .terminal-228828675-r6 { fill: #ddedf9;font-weight: bold;font-style: italic; } + .terminal-228828675-r7 { fill: #e2e2e2 } + .terminal-228828675-r8 { fill: #ddedf9;font-weight: bold } + .terminal-228828675-r9 { fill: #14191f } + .terminal-228828675-r10 { fill: #e2e2e2;font-style: italic; } + .terminal-228828675-r11 { fill: #e2e2e2;font-weight: bold } + .terminal-228828675-r12 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - StyledProgressBar + OptionListApp - - - - - - - - - - - - - - - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━--%--:--:-- - - - - - - - - - - - -  S  Start  + + + + OptionListApp + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +                  Data for Aerilon                   + ┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ + Patron God   Population    Capital City   + ┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩▃▃ + Demeter      1.2 Billion   Gaoth          + └───────────────┴────────────────┴────────────────┘ +                  Data for Aquaria                   + ┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓ + Patron God   Population   Capital City    + ┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩ + Hermes       75,000       None            + └───────────────┴───────────────┴─────────────────┘ +                  Data for Canceron                  + ┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓ + Patron God   Population    Capital City   + ┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + ''' # --- -# name: test_quickly_change_tabs +# name: test_order_independence ''' @@ -23017,143 +23330,143 @@ font-weight: 700; } - .terminal-1484676870-matrix { + .terminal-1392305496-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1484676870-title { + .terminal-1392305496-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1484676870-r1 { fill: #c5c8c6 } - .terminal-1484676870-r2 { fill: #737373 } - .terminal-1484676870-r3 { fill: #e1e1e1;font-weight: bold } - .terminal-1484676870-r4 { fill: #474747 } - .terminal-1484676870-r5 { fill: #0178d4 } - .terminal-1484676870-r6 { fill: #e1e1e1 } + .terminal-1392305496-r1 { fill: #ffff00 } + .terminal-1392305496-r2 { fill: #e3e3e3 } + .terminal-1392305496-r3 { fill: #c5c8c6 } + .terminal-1392305496-r4 { fill: #e1e1e1 } + .terminal-1392305496-r5 { fill: #dde8f3;font-weight: bold } + .terminal-1392305496-r6 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - QuicklyChangeTabsApp + Layers - - - - - onetwothree - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - three - - - - - - - - - - - - - - - - - - - + + + + ──────────────────────────────────Layers + It's full of stars! My God! It's full of sta + + This should float over the top + + + ────────────────────────────────── + + + + + + + + + + + + + + + + +  T  Toggle Screen  ''' # --- -# name: test_radio_button_example +# name: test_order_independence_toggle ''' @@ -23176,147 +23489,143 @@ font-weight: 700; } - .terminal-398709012-matrix { + .terminal-3727479996-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-398709012-title { + .terminal-3727479996-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-398709012-r1 { fill: #e1e1e1 } - .terminal-398709012-r2 { fill: #c5c8c6 } - .terminal-398709012-r3 { fill: #1e1e1e } - .terminal-398709012-r4 { fill: #0178d4 } - .terminal-398709012-r5 { fill: #575757 } - .terminal-398709012-r6 { fill: #262626;font-weight: bold } - .terminal-398709012-r7 { fill: #e2e2e2 } - .terminal-398709012-r8 { fill: #e2e2e2;text-decoration: underline; } - .terminal-398709012-r9 { fill: #434343 } - .terminal-398709012-r10 { fill: #4ebf71;font-weight: bold } + .terminal-3727479996-r1 { fill: #ffff00 } + .terminal-3727479996-r2 { fill: #e3e3e3 } + .terminal-3727479996-r3 { fill: #c5c8c6 } + .terminal-3727479996-r4 { fill: #ddeedd } + .terminal-3727479996-r5 { fill: #dde8f3;font-weight: bold } + .terminal-3727479996-r6 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - RadioChoicesApp + Layers - - - - - - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Battlestar Galactica -  Dune 1984 -  Dune 2021 -  Serenity -  Star Trek: The Motion Picture -  Star Wars: A New Hope -  The Last Starfighter -  Total Recall 👉 🔴 -  Wing Commander - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - + + + + ──────────────────────────────────Layers + It's full of stars! My God! It's full of sta + + This should float over the top + + + ────────────────────────────────── + + + + + + + + + + + + + + + + +  T  Toggle Screen  ''' # --- -# name: test_radio_set_example +# name: test_placeholder_render ''' @@ -23339,140 +23648,142 @@ font-weight: 700; } - .terminal-2369252398-matrix { + .terminal-1570661136-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2369252398-title { + .terminal-1570661136-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2369252398-r1 { fill: #e1e1e1 } - .terminal-2369252398-r2 { fill: #c5c8c6 } - .terminal-2369252398-r3 { fill: #1e1e1e } - .terminal-2369252398-r4 { fill: #0178d4 } - .terminal-2369252398-r5 { fill: #575757 } - .terminal-2369252398-r6 { fill: #262626;font-weight: bold } - .terminal-2369252398-r7 { fill: #e2e2e2 } - .terminal-2369252398-r8 { fill: #e2e2e2;text-decoration: underline; } - .terminal-2369252398-r9 { fill: #434343 } - .terminal-2369252398-r10 { fill: #4ebf71;font-weight: bold } - .terminal-2369252398-r11 { fill: #cc555a;font-weight: bold;font-style: italic; } + .terminal-1570661136-r1 { fill: #c5c8c6 } + .terminal-1570661136-r2 { fill: #eae3e5 } + .terminal-1570661136-r3 { fill: #e8e0e7 } + .terminal-1570661136-r4 { fill: #efe9e4 } + .terminal-1570661136-r5 { fill: #ede6e6 } + .terminal-1570661136-r6 { fill: #efeedf } + .terminal-1570661136-r7 { fill: #e9eee5 } + .terminal-1570661136-r8 { fill: #e2edeb } + .terminal-1570661136-r9 { fill: #e4eee8;font-weight: bold } + .terminal-1570661136-r10 { fill: #dfebed;font-weight: bold } + .terminal-1570661136-r11 { fill: #dfe9ed } + .terminal-1570661136-r12 { fill: #e3e6eb;font-weight: bold } + .terminal-1570661136-r13 { fill: #e6e3e9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - RadioChoicesApp + PlaceholderApp - - - - - - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Battlestar Galactica Amanda -  Dune 1984 Connor MacLeod -  Dune 2021 Duncan MacLeod -  Serenity Heather MacLeod -  Star Trek: The Motion Picture Joe Dawson -  Star Wars: A New Hope Kurgan, The -  The Last Starfighter Methos -  Total Recall 👉 🔴 Rachel Ellenstein -  Wing Commander Ramírez - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - + + + + + Placeholder p2 here! + This is a custom label for p1. + #p4 + #p3#p5Placeholde + r + + Lorem ipsum dolor sit  + 26 x 6amet, consectetur 27 x 6 + adipiscing elit. Etiam  + feugiat ac elit sit amet  + + + Lorem ipsum dolor sit amet,  + consectetur adipiscing elit. Etiam 40 x 6 + feugiat ac elit sit amet accumsan.  + Suspendisse bibendum nec libero quis  + gravida. Phasellus id eleifend ligula. + Nullam imperdiet sem tellus, sed  + vehicula nisl faucibus sit amet. Lorem ipsum dolor sit amet,  + Praesent iaculis tempor ultricies. Sedconsectetur adipiscing elit. Etiam  + lacinia, tellus id rutrum lacinia, feugiat ac elit sit amet accumsan.  + sapien sapien congue mauris, sit amet Suspendisse bibendum nec libero quis  @@ -23480,7 +23791,7 @@ ''' # --- -# name: test_remove_with_auto_height +# name: test_print_capture ''' @@ -23503,144 +23814,139 @@ font-weight: 700; } - .terminal-1869274227-matrix { + .terminal-3935013562-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1869274227-title { + .terminal-3935013562-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1869274227-r1 { fill: #c5c8c6 } - .terminal-1869274227-r2 { fill: #e3e3e3 } - .terminal-1869274227-r3 { fill: #008000 } - .terminal-1869274227-r4 { fill: #ffff00 } - .terminal-1869274227-r5 { fill: #e1e1e1 } - .terminal-1869274227-r6 { fill: #dde8f3;font-weight: bold } - .terminal-1869274227-r7 { fill: #ddedf9 } + .terminal-3935013562-r1 { fill: #e1e1e1 } + .terminal-3935013562-r2 { fill: #c5c8c6 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - VerticalRemoveApp + CaptureApp - - - - VerticalRemoveApp - ────────────────────────────────────────────────────────────────────────────── - ──────────────────── - This is a test label - ──────────────────── - ────────────────────────────────────────────────────────────────────────────── - - - - - - - - - - - - - - - - - -  A  Add  D  Delete  + + + + RichLog + This will be captured! + + + + + + + + + + + + + + + + + + + + + + ''' # --- -# name: test_richlog_max_lines +# name: test_programmatic_scrollbar_gutter_change ''' @@ -23663,142 +23969,142 @@ font-weight: 700; } - .terminal-707514202-matrix { + .terminal-2761669360-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-707514202-title { + .terminal-2761669360-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-707514202-r1 { fill: #e1e1e1 } - .terminal-707514202-r2 { fill: #c5c8c6 } + .terminal-2761669360-r1 { fill: #ffdddd } + .terminal-2761669360-r2 { fill: #c5c8c6 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - RichLogLines + ProgrammaticScrollbarGutterChange - - - - Key press #3 - Key press #4 - Key press #5 - - - - - - - - - - - - - - - - - - - - - - - - - - ''' -# --- -# name: test_richlog_scroll - ''' - - + + + + onetwo + + + + + + + + + + + + threefour + + + + + + + + + + + + + + + + ''' +# --- +# name: test_progress_bar_completed + ''' + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - RichLogScrollApp + IndeterminateProgressBar - - - - Line 0Line 10Line 0 - Line 1Line 11Line 1 - Line 2Line 12Line 2 - Line 3Line 13Line 3 - Line 4Line 14Line 4 - Line 5Line 15Line 5 - Line 6Line 16Line 6 - Line 7Line 17Line 7 - Line 8Line 18Line 8 - Line 9Line 19Line 9 - - - - - - - - - - - - - - + + + + + + + + + + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━100%--:--:-- + + + + + + + + + + + +  S  Start  ''' # --- -# name: test_screen_switch +# name: test_progress_bar_completed_styled ''' @@ -23973,144 +24282,146 @@ font-weight: 700; } - .terminal-1316892474-matrix { + .terminal-3162092160-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1316892474-title { + .terminal-3162092160-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1316892474-r1 { fill: #c5c8c6 } - .terminal-1316892474-r2 { fill: #e3e3e3 } - .terminal-1316892474-r3 { fill: #e1e1e1 } - .terminal-1316892474-r4 { fill: #dde8f3;font-weight: bold } - .terminal-1316892474-r5 { fill: #ddedf9 } + .terminal-3162092160-r1 { fill: #c5c8c6 } + .terminal-3162092160-r2 { fill: #e1e1e1 } + .terminal-3162092160-r3 { fill: #b93c5b } + .terminal-3162092160-r4 { fill: #1e1e1e } + .terminal-3162092160-r5 { fill: #e1e1e1;text-decoration: underline; } + .terminal-3162092160-r6 { fill: #dde8f3;font-weight: bold } + .terminal-3162092160-r7 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ModalApp + StyledProgressBar - - - - ModalApp - B - - - - - - - - - - - - - - - - - - - - - -  A  Push screen A  - - - - - ''' -# --- -# name: test_scroll_to - ''' - + + + + + + + + + + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━100%--:--:-- + + + + + + + + + + + +  S  Start  + + + + + ''' +# --- +# name: test_progress_bar_halfway + ''' + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - ScrollOffByOne + IndeterminateProgressBar - - - - X 43 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X 44 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X 45 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X 46 - ▁▁▁▁▁▁▁▁▃▃ - ▔▔▔▔▔▔▔▔ - X 47▂▂ - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X 48 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X 49 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - X 50 - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - + + + + + + + + + + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━39%00:00:07 + + + + + + + + + + + +  S  Start  ''' # --- -# name: test_scroll_to_center +# name: test_progress_bar_halfway_styled ''' @@ -24297,144 +24601,145 @@ font-weight: 700; } - .terminal-1487675823-matrix { + .terminal-1532901142-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1487675823-title { + .terminal-1532901142-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1487675823-r1 { fill: #e1e1e1 } - .terminal-1487675823-r2 { fill: #c5c8c6 } - .terminal-1487675823-r3 { fill: #004578 } - .terminal-1487675823-r4 { fill: #23568b } - .terminal-1487675823-r5 { fill: #fea62b } - .terminal-1487675823-r6 { fill: #cc555a } - .terminal-1487675823-r7 { fill: #14191f } + .terminal-1532901142-r1 { fill: #c5c8c6 } + .terminal-1532901142-r2 { fill: #e1e1e1 } + .terminal-1532901142-r3 { fill: #004578 } + .terminal-1532901142-r4 { fill: #152939 } + .terminal-1532901142-r5 { fill: #1e1e1e } + .terminal-1532901142-r6 { fill: #e1e1e1;text-decoration: underline; } + .terminal-1532901142-r7 { fill: #dde8f3;font-weight: bold } + .terminal-1532901142-r8 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + StyledProgressBar - - - - SPAM - SPAM - SPAM - ──────────────────────────────────────────────────────────────────────────── - SPAM - SPAM - SPAM - SPAM - SPAM - SPAM▄▄ - SPAM - SPAM - ──────────────────────────────────────────────────────────────────────── - @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@>>bullseye<<@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ - ▇▇ - ▄▄ - - - - - - - - ──────────────────────────────────────────────────────────────────────────── + + + + + + + + + + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━39%00:00:07 + + + + + + + + + + + +  S  Start  ''' # --- -# name: test_scroll_visible +# name: test_progress_bar_indeterminate ''' @@ -24457,140 +24762,143 @@ font-weight: 700; } - .terminal-150767416-matrix { + .terminal-3440292978-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-150767416-title { + .terminal-3440292978-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-150767416-r1 { fill: #e1e1e1 } - .terminal-150767416-r2 { fill: #c5c8c6 } - .terminal-150767416-r3 { fill: #23568b } + .terminal-3440292978-r1 { fill: #c5c8c6 } + .terminal-3440292978-r2 { fill: #e1e1e1 } + .terminal-3440292978-r3 { fill: #323232 } + .terminal-3440292978-r4 { fill: #b93c5b } + .terminal-3440292978-r5 { fill: #dde8f3;font-weight: bold } + .terminal-3440292978-r6 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - MyApp + IndeterminateProgressBar - - - - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |▆▆ - | - | - | - | - SHOULD BE VISIBLE + + + + + + + + + + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━--%--:--:-- + + + + + + + + + + + +  S  Start  ''' # --- -# name: test_scrollbar_thumb_height +# name: test_progress_bar_indeterminate_styled ''' @@ -24613,142 +24921,145 @@ font-weight: 700; } - .terminal-4204360114-matrix { + .terminal-4046569674-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4204360114-title { + .terminal-4046569674-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4204360114-r1 { fill: #c5c8c6 } - .terminal-4204360114-r2 { fill: #e3e3e3 } - .terminal-4204360114-r3 { fill: #ff0000 } - .terminal-4204360114-r4 { fill: #dde2e8 } - .terminal-4204360114-r5 { fill: #ddedf9 } + .terminal-4046569674-r1 { fill: #c5c8c6 } + .terminal-4046569674-r2 { fill: #e1e1e1 } + .terminal-4046569674-r3 { fill: #fea62b } + .terminal-4046569674-r4 { fill: #004578 } + .terminal-4046569674-r5 { fill: #1e1e1e } + .terminal-4046569674-r6 { fill: #e1e1e1;text-decoration: underline; } + .terminal-4046569674-r7 { fill: #dde8f3;font-weight: bold } + .terminal-4046569674-r8 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ScrollViewTester + StyledProgressBar - - - - ScrollViewTester -  1 ────────────────────────────────────────────────────────────────────────── - Welcome to line 980 - Welcome to line 981 - Welcome to line 982 - Welcome to line 983 - Welcome to line 984 - Welcome to line 985 - Welcome to line 986 - Welcome to line 987 - Welcome to line 988 - Welcome to line 989 - Welcome to line 990 - Welcome to line 991 - Welcome to line 992 - Welcome to line 993 - Welcome to line 994 - Welcome to line 995 - Welcome to line 996 - Welcome to line 997 - Welcome to line 998 - Welcome to line 999 - ────────────────────────────────────────────────────────────────────────────── - - - - - - ''' -# --- -# name: test_select + + + + + + + + + + + + + + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━--%--:--:-- + + + + + + + + + + + +  S  Start  + + + + + ''' +# --- +# name: test_quickly_change_tabs ''' @@ -24771,136 +25082,135 @@ font-weight: 700; } - .terminal-1161182100-matrix { + .terminal-1484676870-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1161182100-title { + .terminal-1484676870-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1161182100-r1 { fill: #c5c8c6 } - .terminal-1161182100-r2 { fill: #e3e3e3 } - .terminal-1161182100-r3 { fill: #e1e1e1 } - .terminal-1161182100-r4 { fill: #1e1e1e } - .terminal-1161182100-r5 { fill: #0178d4 } - .terminal-1161182100-r6 { fill: #787878 } - .terminal-1161182100-r7 { fill: #a8a8a8 } + .terminal-1484676870-r1 { fill: #c5c8c6 } + .terminal-1484676870-r2 { fill: #737373 } + .terminal-1484676870-r3 { fill: #e1e1e1;font-weight: bold } + .terminal-1484676870-r4 { fill: #474747 } + .terminal-1484676870-r5 { fill: #0178d4 } + .terminal-1484676870-r6 { fill: #e1e1e1 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SelectApp + QuicklyChangeTabsApp - - - - SelectApp - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Select - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - - - + + + + + onetwothree + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + three + + + + + + + + + + + + + + + + + + @@ -24908,7 +25218,7 @@ ''' # --- -# name: test_select_expanded +# name: test_radio_button_example ''' @@ -24931,140 +25241,139 @@ font-weight: 700; } - .terminal-2035490498-matrix { + .terminal-398709012-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2035490498-title { + .terminal-398709012-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2035490498-r1 { fill: #c5c8c6 } - .terminal-2035490498-r2 { fill: #e3e3e3 } - .terminal-2035490498-r3 { fill: #e1e1e1 } - .terminal-2035490498-r4 { fill: #1e1e1e } - .terminal-2035490498-r5 { fill: #0178d4 } - .terminal-2035490498-r6 { fill: #787878 } - .terminal-2035490498-r7 { fill: #a8a8a8 } - .terminal-2035490498-r8 { fill: #121212 } - .terminal-2035490498-r9 { fill: #ddedf9;font-weight: bold } - .terminal-2035490498-r10 { fill: #85beea;font-weight: bold } - .terminal-2035490498-r11 { fill: #e2e3e3 } + .terminal-398709012-r1 { fill: #e1e1e1 } + .terminal-398709012-r2 { fill: #c5c8c6 } + .terminal-398709012-r3 { fill: #1e1e1e } + .terminal-398709012-r4 { fill: #0178d4 } + .terminal-398709012-r5 { fill: #575757 } + .terminal-398709012-r6 { fill: #262626;font-weight: bold } + .terminal-398709012-r7 { fill: #e2e2e2 } + .terminal-398709012-r8 { fill: #e2e2e2;text-decoration: underline; } + .terminal-398709012-r9 { fill: #434343 } + .terminal-398709012-r10 { fill: #4ebf71;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SelectApp + RadioChoicesApp - - - - SelectApp - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Select - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Select - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total  - obliteration. - I will face my fear. - I will permit it to pass over me and through me. - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Battlestar Galactica +  Dune 1984 +  Dune 2021 +  Serenity +  Star Trek: The Motion Picture +  Star Wars: A New Hope +  The Last Starfighter +  Total Recall 👉 🔴 +  Wing Commander + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + @@ -25072,7 +25381,7 @@ ''' # --- -# name: test_select_expanded_changed +# name: test_radio_set_example ''' @@ -25095,136 +25404,140 @@ font-weight: 700; } - .terminal-4010426174-matrix { + .terminal-2369252398-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4010426174-title { + .terminal-2369252398-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4010426174-r1 { fill: #c5c8c6 } - .terminal-4010426174-r2 { fill: #e3e3e3 } - .terminal-4010426174-r3 { fill: #e1e1e1 } - .terminal-4010426174-r4 { fill: #1e1e1e } - .terminal-4010426174-r5 { fill: #0178d4 } - .terminal-4010426174-r6 { fill: #e2e2e2 } - .terminal-4010426174-r7 { fill: #a8a8a8 } + .terminal-2369252398-r1 { fill: #e1e1e1 } + .terminal-2369252398-r2 { fill: #c5c8c6 } + .terminal-2369252398-r3 { fill: #1e1e1e } + .terminal-2369252398-r4 { fill: #0178d4 } + .terminal-2369252398-r5 { fill: #575757 } + .terminal-2369252398-r6 { fill: #262626;font-weight: bold } + .terminal-2369252398-r7 { fill: #e2e2e2 } + .terminal-2369252398-r8 { fill: #e2e2e2;text-decoration: underline; } + .terminal-2369252398-r9 { fill: #434343 } + .terminal-2369252398-r10 { fill: #4ebf71;font-weight: bold } + .terminal-2369252398-r11 { fill: #cc555a;font-weight: bold;font-style: italic; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SelectApp + RadioChoicesApp - - - - I must not fear. - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - I must not fear. - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - - - + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Battlestar Galactica Amanda +  Dune 1984 Connor MacLeod +  Dune 2021 Duncan MacLeod +  Serenity Heather MacLeod +  Star Trek: The Motion Picture Joe Dawson +  Star Wars: A New Hope Kurgan, The +  The Last Starfighter Methos +  Total Recall 👉 🔴 Rachel Ellenstein +  Wing Commander Ramírez + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + @@ -25232,7 +25545,7 @@ ''' # --- -# name: test_select_rebuild +# name: test_remove_with_auto_height ''' @@ -25255,147 +25568,144 @@ font-weight: 700; } - .terminal-330554958-matrix { + .terminal-1869274227-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-330554958-title { + .terminal-1869274227-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-330554958-r1 { fill: #1e1e1e } - .terminal-330554958-r2 { fill: #0178d4 } - .terminal-330554958-r3 { fill: #c5c8c6 } - .terminal-330554958-r4 { fill: #787878 } - .terminal-330554958-r5 { fill: #a8a8a8 } - .terminal-330554958-r6 { fill: #121212 } - .terminal-330554958-r7 { fill: #ddedf9;font-weight: bold } - .terminal-330554958-r8 { fill: #85beea;font-weight: bold } - .terminal-330554958-r9 { fill: #e2e3e3 } - .terminal-330554958-r10 { fill: #e1e1e1 } + .terminal-1869274227-r1 { fill: #c5c8c6 } + .terminal-1869274227-r2 { fill: #e3e3e3 } + .terminal-1869274227-r3 { fill: #008000 } + .terminal-1869274227-r4 { fill: #ffff00 } + .terminal-1869274227-r5 { fill: #e1e1e1 } + .terminal-1869274227-r6 { fill: #dde8f3;font-weight: bold } + .terminal-1869274227-r7 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SelectRebuildApp + VerticalRemoveApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Select - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Select - This - Should - Be - What - Goes - Into - The - Snapshit - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - + + + + VerticalRemoveApp + ────────────────────────────────────────────────────────────────────────────── + ──────────────────── + This is a test label + ──────────────────── + ────────────────────────────────────────────────────────────────────────────── + + + + + + + + + + + + + + + + + +  A  Add  D  Delete  + + + + ''' # --- -# name: test_selection_list_selected +# name: test_richlog_max_lines ''' @@ -25418,141 +25728,131 @@ font-weight: 700; } - .terminal-4089366530-matrix { + .terminal-707514202-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4089366530-title { + .terminal-707514202-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4089366530-r1 { fill: #c5c8c6 } - .terminal-4089366530-r2 { fill: #e3e3e3 } - .terminal-4089366530-r3 { fill: #e1e1e1 } - .terminal-4089366530-r4 { fill: #0178d4 } - .terminal-4089366530-r5 { fill: #e1e1e1;font-weight: bold } - .terminal-4089366530-r6 { fill: #575757 } - .terminal-4089366530-r7 { fill: #4ebf71;font-weight: bold } - .terminal-4089366530-r8 { fill: #ddedf9;font-weight: bold } - .terminal-4089366530-r9 { fill: #98a84b } - .terminal-4089366530-r10 { fill: #262626;font-weight: bold } - .terminal-4089366530-r11 { fill: #e2e2e2 } - .terminal-4089366530-r12 { fill: #ddedf9 } + .terminal-707514202-r1 { fill: #e1e1e1 } + .terminal-707514202-r2 { fill: #c5c8c6 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SelectionListApp + RichLogLines - - - - SelectionListApp - - -  Shall we play some games? ── Selected games ───────────── - [ - XFalken's Maze'secret_back_door', - XBlack Jack'a_nice_game_of_chess', - XGin Rummy'fighter_combat' - XHearts] - XBridge────────────────────────────── - XCheckers - XChess - XPoker - XFighter Combat - - ────────────────────────────── - - - - - - - + + + + Key press #3 + Key press #4 + Key press #5 + + + + + + + + + + + + + + + + + + + + @@ -25560,7 +25860,7 @@ ''' # --- -# name: test_selection_list_selections +# name: test_richlog_scroll ''' @@ -25583,148 +25883,140 @@ font-weight: 700; } - .terminal-3401996005-matrix { + .terminal-2577369616-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3401996005-title { + .terminal-2577369616-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3401996005-r1 { fill: #c5c8c6 } - .terminal-3401996005-r2 { fill: #e3e3e3 } - .terminal-3401996005-r3 { fill: #e1e1e1 } - .terminal-3401996005-r4 { fill: #0178d4 } - .terminal-3401996005-r5 { fill: #575757 } - .terminal-3401996005-r6 { fill: #4ebf71;font-weight: bold } - .terminal-3401996005-r7 { fill: #ddedf9;font-weight: bold } - .terminal-3401996005-r8 { fill: #262626;font-weight: bold } - .terminal-3401996005-r9 { fill: #e2e2e2 } - .terminal-3401996005-r10 { fill: #ddedf9 } + .terminal-2577369616-r1 { fill: #e1e1e1 } + .terminal-2577369616-r2 { fill: #c5c8c6 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SelectionListApp + RichLogScrollApp - - - - SelectionListApp - - -  Shall we play some games? ────────────────────────────────── - - XFalken's Maze - XBlack Jack - XGin Rummy - XHearts - XBridge - XCheckers - XChess - XPoker - XFighter Combat - - - - - - - ────────────────────────────────────────────────────────────── - - - - - - - - ''' -# --- -# name: test_selection_list_tuples - ''' + + + + Line 0Line 10Line 0 + Line 1Line 11Line 1 + Line 2Line 12Line 2 + Line 3Line 13Line 3 + Line 4Line 14Line 4 + Line 5Line 15Line 5 + Line 6Line 16Line 6 + Line 7Line 17Line 7 + Line 8Line 18Line 8 + Line 9Line 19Line 9 + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_rule_horizontal_rules + ''' - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SelectionListApp + HorizontalRulesApp - - - - SelectionListApp - - -  Shall we play some games? ────────────────────────────────── - - XFalken's Maze - XBlack Jack - XGin Rummy - XHearts - XBridge - XCheckers - XChess - XPoker - XFighter Combat - - - - - - - ────────────────────────────────────────────────────────────── - - + + + +                         solid (default)                          + + ──────────────────────────────────────────────────────────────── + +                              heavy                               + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +                              thick                               + + ████████████████████████████████████████████████████████████████ + +                              dashed                              + + ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ + +                              double                              + + ════════════════════════════════════════════════════════════════ + +                              ascii                               + + ---------------------------------------------------------------- @@ -25886,7 +26171,7 @@ ''' # --- -# name: test_sparkline_component_classes_colors +# name: test_rule_vertical_rules ''' @@ -25909,175 +26194,2424 @@ font-weight: 700; } - .terminal-2491064415-matrix { + .terminal-1162739243-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2491064415-title { + .terminal-1162739243-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2491064415-r1 { fill: #e1e1e1 } - .terminal-2491064415-r2 { fill: #c5c8c6 } - .terminal-2491064415-r3 { fill: #fea62b } - .terminal-2491064415-r4 { fill: #eea831 } - .terminal-2491064415-r5 { fill: #d0ac3c } - .terminal-2491064415-r6 { fill: #c2ae42 } - .terminal-2491064415-r7 { fill: #b4b048 } - .terminal-2491064415-r8 { fill: #9ab452 } - .terminal-2491064415-r9 { fill: #8db557 } - .terminal-2491064415-r10 { fill: #78b860 } - .terminal-2491064415-r11 { fill: #6eba63 } - .terminal-2491064415-r12 { fill: #66bb67 } - .terminal-2491064415-r13 { fill: #59bd6c } - .terminal-2491064415-r14 { fill: #54be6e } - .terminal-2491064415-r15 { fill: #4ebe70 } - .terminal-2491064415-r16 { fill: #50be70 } - .terminal-2491064415-r17 { fill: #57bd6d } - .terminal-2491064415-r18 { fill: #5cbc6b } - .terminal-2491064415-r19 { fill: #63bb68 } - .terminal-2491064415-r20 { fill: #74b961 } - .terminal-2491064415-r21 { fill: #7eb85d } - .terminal-2491064415-r22 { fill: #94b454 } - .terminal-2491064415-r23 { fill: #a1b34f } - .terminal-2491064415-r24 { fill: #aeb14a } - .terminal-2491064415-r25 { fill: #caad3f } - .terminal-2491064415-r26 { fill: #d9ab39 } - .terminal-2491064415-r27 { fill: #f7a62d } - .terminal-2491064415-r28 { fill: #f5a72e } - .terminal-2491064415-r29 { fill: #d7ab3a } - .terminal-2491064415-r30 { fill: #c8ad40 } - .terminal-2491064415-r31 { fill: #baaf45 } - .terminal-2491064415-r32 { fill: #9fb350 } - .terminal-2491064415-r33 { fill: #93b555 } - .terminal-2491064415-r34 { fill: #7cb85e } - .terminal-2491064415-r35 { fill: #72b962 } - .terminal-2491064415-r36 { fill: #6abb65 } - .terminal-2491064415-r37 { fill: #5bbd6b } - .terminal-2491064415-r38 { fill: #56bd6d } - .terminal-2491064415-r39 { fill: #4fbe70 } - .terminal-2491064415-r40 { fill: #55bd6e } - .terminal-2491064415-r41 { fill: #5abd6c } - .terminal-2491064415-r42 { fill: #60bc69 } - .terminal-2491064415-r43 { fill: #70ba63 } - .terminal-2491064415-r44 { fill: #79b85f } - .terminal-2491064415-r45 { fill: #8fb556 } - .terminal-2491064415-r46 { fill: #9bb352 } - .terminal-2491064415-r47 { fill: #a8b24c } - .terminal-2491064415-r48 { fill: #c4ae41 } - .terminal-2491064415-r49 { fill: #d3ac3c } - .terminal-2491064415-r50 { fill: #f1a730 } - .terminal-2491064415-r51 { fill: #fba62b } - .terminal-2491064415-r52 { fill: #ddaa37 } - .terminal-2491064415-r53 { fill: #ceac3d } - .terminal-2491064415-r54 { fill: #c0ae43 } - .terminal-2491064415-r55 { fill: #a5b24e } - .terminal-2491064415-r56 { fill: #98b453 } - .terminal-2491064415-r57 { fill: #81b75c } - .terminal-2491064415-r58 { fill: #76b960 } - .terminal-2491064415-r59 { fill: #6dba64 } - .terminal-2491064415-r60 { fill: #5ebc6a } - .terminal-2491064415-r61 { fill: #58bd6c } - .terminal-2491064415-r62 { fill: #50be6f } - .terminal-2491064415-r63 { fill: #4ebf71 } - .terminal-2491064415-r64 { fill: #53be6e } - .terminal-2491064415-r65 { fill: #58bd6d } - .terminal-2491064415-r66 { fill: #5dbc6a } - .terminal-2491064415-r67 { fill: #6cba64 } - .terminal-2491064415-r68 { fill: #75b961 } - .terminal-2491064415-r69 { fill: #8ab658 } - .terminal-2491064415-r70 { fill: #96b454 } - .terminal-2491064415-r71 { fill: #a3b24f } - .terminal-2491064415-r72 { fill: #beaf44 } - .terminal-2491064415-r73 { fill: #ccac3e } - .terminal-2491064415-r74 { fill: #7bb85f } - .terminal-2491064415-r75 { fill: #89b659 } - .terminal-2491064415-r76 { fill: #97b453 } - .terminal-2491064415-r77 { fill: #b1b049 } - .terminal-2491064415-r78 { fill: #d3ac3b } - .terminal-2491064415-r79 { fill: #ddaa38 } - .terminal-2491064415-r80 { fill: #e5a934 } - .terminal-2491064415-r81 { fill: #f2a72f } - .terminal-2491064415-r82 { fill: #fda62b } - .terminal-2491064415-r83 { fill: #f4a72e } - .terminal-2491064415-r84 { fill: #efa830 } - .terminal-2491064415-r85 { fill: #e8a933 } - .terminal-2491064415-r86 { fill: #cdac3e } - .terminal-2491064415-r87 { fill: #b7b047 } - .terminal-2491064415-r88 { fill: #aab14c } - .terminal-2491064415-r89 { fill: #9db351 } - .terminal-2491064415-r90 { fill: #83b75b } - .terminal-2491064415-r91 { fill: #91b556 } - .terminal-2491064415-r92 { fill: #acb14b } - .terminal-2491064415-r93 { fill: #b8af46 } - .terminal-2491064415-r94 { fill: #cfac3d } - .terminal-2491064415-r95 { fill: #e1a936 } - .terminal-2491064415-r96 { fill: #f0a730 } - .terminal-2491064415-r97 { fill: #fca62b } - .terminal-2491064415-r98 { fill: #f6a72d } - .terminal-2491064415-r99 { fill: #f1a72f } - .terminal-2491064415-r100 { fill: #eba832 } - .terminal-2491064415-r101 { fill: #dbaa38 } - .terminal-2491064415-r102 { fill: #d2ac3c } - .terminal-2491064415-r103 { fill: #bcaf45 } - .terminal-2491064415-r104 { fill: #b0b149 } - .terminal-2491064415-r105 { fill: #87b65a } - .terminal-2491064415-r106 { fill: #78b85f } - .terminal-2491064415-r107 { fill: #5abd6b } - .terminal-2491064415-r108 { fill: #6eba64 } - .terminal-2491064415-r109 { fill: #7db85e } - .terminal-2491064415-r110 { fill: #8bb658 } - .terminal-2491064415-r111 { fill: #a6b24d } - .terminal-2491064415-r112 { fill: #b3b048 } - .terminal-2491064415-r113 { fill: #d5ab3b } - .terminal-2491064415-r114 { fill: #deaa37 } - .terminal-2491064415-r115 { fill: #eda831 } - .terminal-2491064415-r116 { fill: #f3a72f } - .terminal-2491064415-r117 { fill: #fba62c } - .terminal-2491064415-r118 { fill: #f8a62d } - .terminal-2491064415-r119 { fill: #f3a72e } - .terminal-2491064415-r120 { fill: #dfaa37 } - .terminal-2491064415-r121 { fill: #d6ab3a } - .terminal-2491064415-r122 { fill: #c1ae43 } - .terminal-2491064415-r123 { fill: #b5b047 } - .terminal-2491064415-r124 { fill: #7fb85d } - .terminal-2491064415-r125 { fill: #f89c2f } - .terminal-2491064415-r126 { fill: #ec8a37 } - .terminal-2491064415-r127 { fill: #e6823b } - .terminal-2491064415-r128 { fill: #e1793f } - .terminal-2491064415-r129 { fill: #d66946 } - .terminal-2491064415-r130 { fill: #d26249 } - .terminal-2491064415-r131 { fill: #c9554f } - .terminal-2491064415-r132 { fill: #c54f52 } - .terminal-2491064415-r133 { fill: #c24a54 } - .terminal-2491064415-r134 { fill: #bd4257 } - .terminal-2491064415-r135 { fill: #bb4059 } - .terminal-2491064415-r136 { fill: #b93c5a } - .terminal-2491064415-r137 { fill: #b93d5a } - .terminal-2491064415-r138 { fill: #bc4158 } - .terminal-2491064415-r139 { fill: #be4456 } - .terminal-2491064415-r140 { fill: #c14855 } - .terminal-2491064415-r141 { fill: #c75350 } - .terminal-2491064415-r142 { fill: #cb584d } - .terminal-2491064415-r143 { fill: #d46647 } - .terminal-2491064415-r144 { fill: #d96e44 } - .terminal-2491064415-r145 { fill: #de7640 } - .terminal-2491064415-r146 { fill: #e98738 } - .terminal-2491064415-r147 { fill: #ef8f34 } - .terminal-2491064415-r148 { fill: #fba22c } - .terminal-2491064415-r149 { fill: #faa02d } - .terminal-2491064415-r150 { fill: #ee8e35 } - .terminal-2491064415-r151 { fill: #e98539 } - .terminal-2491064415-r152 { fill: #e37d3d } - .terminal-2491064415-r153 { fill: #d86d44 } - .terminal-2491064415-r154 { fill: #d46548 } - .terminal-2491064415-r155 { fill: #cb584e } - .terminal-2491064415-r156 { fill: #c75250 } + .terminal-1162739243-r1 { fill: #e1e1e1 } + .terminal-1162739243-r2 { fill: #c5c8c6 } + .terminal-1162739243-r3 { fill: #004578 } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VerticalRulesApp + + + + + + + + + + + + solid heavy thick dasheddoubleascii | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + + + + + + + + ''' +# --- +# name: test_scoped_css + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MyApp + + + + + + + + + + ────────────────────────────────────────────────────────────────────────────── + ─── + foo + ─── + ─── + bar + ─── + ────────────────────────────────────────────────────────────────────────────── + ────────────────────────────────────────────────────────────────────────────── + ─── + foo + ─── + ─── + bar + ─── + ────────────────────────────────────────────────────────────────────────────── + I should not be styled + + + + + + + + + + + + ''' +# --- +# name: test_screen_switch + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ModalApp + + + + + + + + + + ModalApp + B + + + + + + + + + + + + + + + + + + + + + +  A  Push screen A  + + + + + ''' +# --- +# name: test_scroll_to + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ScrollOffByOne + + + + + + + + + + X 43 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 44 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 45 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 46 + ▁▁▁▁▁▁▁▁▃▃ + ▔▔▔▔▔▔▔▔ + X 47▂▂ + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 48 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 49 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + X 50 + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + + + + + + ''' +# --- +# name: test_scroll_to_center + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MyApp + + + + + + + + + + SPAM + SPAM + SPAM + ──────────────────────────────────────────────────────────────────────────── + SPAM + SPAM + SPAM + SPAM + SPAM + SPAM▄▄ + SPAM + SPAM + ──────────────────────────────────────────────────────────────────────── + @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@>>bullseye<<@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + ▇▇ + ▄▄ + + + + + + + + ──────────────────────────────────────────────────────────────────────────── + + + + + ''' +# --- +# name: test_scroll_visible + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MyApp + + + + + + + + + + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + |▆▆ + | + | + | + | + SHOULD BE VISIBLE + + + + + ''' +# --- +# name: test_scrollbar_thumb_height + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ScrollViewTester + + + + + + + + + + ScrollViewTester +  1 ────────────────────────────────────────────────────────────────────────── + Welcome to line 980 + Welcome to line 981 + Welcome to line 982 + Welcome to line 983 + Welcome to line 984 + Welcome to line 985 + Welcome to line 986 + Welcome to line 987 + Welcome to line 988 + Welcome to line 989 + Welcome to line 990 + Welcome to line 991 + Welcome to line 992 + Welcome to line 993 + Welcome to line 994 + Welcome to line 995 + Welcome to line 996 + Welcome to line 997 + Welcome to line 998 + Welcome to line 999 + ────────────────────────────────────────────────────────────────────────────── + + + + + + ''' +# --- +# name: test_select + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SelectApp + + + + + + + + + + SelectApp + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Select + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_select_expanded + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SelectApp + + + + + + + + + + SelectApp + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Select + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Select + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total  + obliteration. + I will face my fear. + I will permit it to pass over me and through me. + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + ''' +# --- +# name: test_select_expanded_changed + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SelectApp + + + + + + + + + + I must not fear. + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + I must not fear. + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_select_rebuild + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SelectRebuildApp + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Select + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Select + This + Should + Be + What + Goes + Into + The + Snapshit + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + ''' +# --- +# name: test_selection_list_selected + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SelectionListApp + + + + + + + + + + SelectionListApp + + +  Shall we play some games? ── Selected games ───────────── + [ + XFalken's Maze'secret_back_door', + XBlack Jack'a_nice_game_of_chess', + XGin Rummy'fighter_combat' + XHearts] + XBridge────────────────────────────── + XCheckers + XChess + XPoker + XFighter Combat + + ────────────────────────────── + + + + + + + + + + + + + ''' +# --- +# name: test_selection_list_selections + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SelectionListApp + + + + + + + + + + SelectionListApp + + +  Shall we play some games? ────────────────────────────────── + + XFalken's Maze + XBlack Jack + XGin Rummy + XHearts + XBridge + XCheckers + XChess + XPoker + XFighter Combat + + + + + + + ────────────────────────────────────────────────────────────── + + + + + + + + ''' +# --- +# name: test_selection_list_tuples + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SelectionListApp + + + + + + + + + + SelectionListApp + + +  Shall we play some games? ────────────────────────────────── + + XFalken's Maze + XBlack Jack + XGin Rummy + XHearts + XBridge + XCheckers + XChess + XPoker + XFighter Combat + + + + + + + ────────────────────────────────────────────────────────────── + + + + + + + + ''' +# --- +# name: test_sparkline_component_classes_colors + ''' + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SparklineColorsApp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_sparkline_render + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SparklineSummaryFunctionApp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_switches + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SwitchApp + + + + + + + + + + + + + + Example switches + + + ▔▔▔▔▔▔▔▔ + off:      + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + on:       + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + focused:  + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + custom:   + ▁▁▁▁▁▁▁▁ + + + + + + + + + + ''' +# --- +# name: test_tabbed_content + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TabbedApp + + + + + + + + + + + LetoJessicaPaul + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + Lady Jessica + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Bene Gesserit and concubine of Leto, and mother of Paul and Alia. + + + + PaulAlia + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + First child + + + + + + +  L  Leto  J  Jessica  P  Paul  + + + + + ''' +# --- +# name: test_tabbed_content_with_modified_tabs + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FiddleWithTabsApp + + + + + + + + + + + Tab 1Tab 2Tab 4Tab 5 + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Button + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_table_markup + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableStaticApp + + + + + + + + + + ┏━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┓ + FooBar   baz       + ┡━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━┩ + Hello World!ItalicUnderline + └──────────────┴────────┴───────────┘ + + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_tabs_invalidate + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TabApp + + + + + + + + + + + Tab 1Tab 2 + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + ────────────────────────────────────────────────────────────────────────────── + + world + + ────────────────────────────────────────────────────────────────────────────── + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_text_area_language_rendering[css] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + +  1  /* This is a comment in CSS */ +  2   +  3  /* Basic selectors and properties */ +  4  body {                                 +  5      font-family: Arial, sans-serif;    +  6      background-color: #f4f4f4;         +  7      margin: 0;                         +  8      padding: 0;                        +  9  }                                      + 10   + 11  /* Class and ID selectors */ + 12  .header {                              + 13      background-color: #333;            + 14      color: #fff;                       + 15      padding: 10px0;                   + 16      text-align: center;                + 17  }                                      + 18   + 19  #logo {                                + 20      font-size: 24px;                   + 21      font-weight: bold;                 + 22  }                                      + 23   + 24  /* Descendant and child selectors */ + 25  .nav ul {                              + 26      list-style-type: none;             + 27      padding: 0;                        + 28  }                                      + 29   + 30  .nav > li {                            + 31      display: inline-block;             + 32      margin-right: 10px;                + 33  }                                      + 34   + 35  /* Pseudo-classes */ + 36  a:hover {                              + 37      text-decoration: underline;        + 38  }                                      + 39   + 40  input:focus {                          + 41      border-color: #007BFF;             + 42  }                                      + 43   + 44  /* Media query */ + 45  @media (max-width: 768px) {            + 46      body {                             + 47          font-size: 16px;               + 48      }                                  + 49   + 50      .header {                          + 51          padding: 5px0;                + 52      }                                  + 53  }                                      + 54   + 55  /* Keyframes animation */ + 56  @keyframes slideIn {                   + 57  from {                             + 58          transform: translateX(-100%);  + 59      }                                  + 60  to {                               + 61          transform: translateX(0);      + 62      }                                  + 63  }                                      + 64   + 65  .slide-in-element {                    + 66      animation: slideIn 0.5s forwards;  + 67  }                                      + 68   + + + + + + ''' +# --- +# name: test_text_area_language_rendering[html] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + +  1  <!DOCTYPE html>                                                              +  2  <html lang="en">                                                            +  3   +  4  <head>                                                                      +  5  <!-- Meta tags --> +  6      <meta charset="UTF-8">                                                  +  7      <meta name="viewport" content="width=device-width, initial-scale=1.0" +  8  <!-- Title --> +  9      <title>HTML Test Page</title>                                           + 10  <!-- Link to CSS --> + 11      <link rel="stylesheet" href="styles.css">                               + 12  </head>                                                                     + 13   + 14  <body>                                                                      + 15  <!-- Header section --> + 16      <header class="header">                                                 + 17          <h1 id="logo">HTML Test Page</h1>                                   + 18      </header>                                                               + 19   + 20  <!-- Navigation --> + 21      <nav class="nav">                                                       + 22          <ul>                                                                + 23              <li><a href="#">Home</a></li>                                   + 24              <li><a href="#">About</a></li>                                  + 25              <li><a href="#">Contact</a></li>                                + 26          </ul>                                                               + 27      </nav>                                                                  + 28   + 29  <!-- Main content area --> + 30      <main>                                                                  + 31          <article>                                                           + 32              <h2>Welcome to the Test Page</h2>                               + 33              <p>This is a paragraph to test the HTML structure.</p>          + 34              <img src="test-image.jpg" alt="Test Image" width="300">         + 35          </article>                                                          + 36      </main>                                                                 + 37   + 38  <!-- Form --> + 39      <section>                                                               + 40          <form action="/submit" method="post">                               + 41              <label for="name">Name:</label>                                 + 42              <input type="text" id="name" name="name">                       + 43              <input type="submit" value="Submit">                            + 44          </form>                                                             + 45      </section>                                                              + 46   + 47  <!-- Footer --> + 48      <footer>                                                                + 49          <p>&copy; 2023 HTML Test Page</p>                                   + 50      </footer>                                                               + 51   + 52  <!-- Script tag --> + 53      <script src="scripts.js"></script>                                      + 54  </body>                                                                     + 55   + 56  </html>                                                                     + 57   + + + + + + ''' +# --- +# name: test_text_area_language_rendering[json] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + +  1  { +  2  "name""John Doe",                            +  3  "age"30,                                     +  4  "isStudent"false,                            +  5  "address": {                                   +  6  "street""123 Main St",                   +  7  "city""Anytown",                         +  8  "state""CA",                             +  9  "zip""12345" + 10      },                                             + 11  "phoneNumbers": [                              + 12          {                                          + 13  "type""home",                        + 14  "number""555-555-1234" + 15          },                                         + 16          {                                          + 17  "type""work",                        + 18  "number""555-555-5678" + 19          }                                          + 20      ],                                             + 21  "hobbies": ["reading""hiking""swimming"],  + 22  "pets": [                                      + 23          {                                          + 24  "type""dog",                         + 25  "name""Fido" + 26          },                                         + 27      ],                                             + 28  "graduationYear"null + 29  } + 30   + 31   + + + + + + ''' +# --- +# name: test_text_area_language_rendering[markdown] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + +  1  Heading +  2  =======                                                                      +  3   +  4  Sub-heading +  5  -----------                                                                  +  6   +  7  ### Heading +  8   +  9  #### H4 Heading + 10   + 11  ##### H5 Heading + 12   + 13  ###### H6 Heading + 14   + 15   + 16  Paragraphs are separated                                                     + 17  by a blank line.                                                             + 18   + 19  Two spaces at the end of a line                                              + 20  produces a line break.                                                       + 21   + 22  Text attributes _italic_,                                                    + 23  **bold**`monospace`.                                                       + 24   + 25  Horizontal rule:                                                             + 26   + 27  ---                                                                          + 28   + 29  Bullet list:                                                                 + 30   + 31  * apples                                                                   + 32  * oranges                                                                  + 33  * pears                                                                    + 34   + 35  Numbered list:                                                               + 36   + 37  1. lather                                                                  + 38  2. rinse                                                                   + 39  3. repeat                                                                  + 40   + 41  An [example](http://example.com).                                            + 42   + 43  > Markdown uses email-style > characters for blockquoting.                   + 44  >                                                                            + 45  > Lorem ipsum                                                                + 46   + 47  ![progress](https://github.com/textualize/rich/raw/master/imgs/progress.gif) + 48   + 49   + 50  ```                                                                          + 51  a=1                                                                          + 52  ```                                                                          + 53   + 54  ```python                                                                    + 55  import this                                                                  + 56  ```                                                                          + 57   + 58  ```somelang                                                                  + 59  foobar                                                                       + 60  ```                                                                          + 61   + 62      import this                                                              + 63   + 64   + 65  1. List item                                                                 + 66   + 67         Code block                                                            + 68   + + + + + + ''' +# --- +# name: test_text_area_language_rendering[python] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + +  1  import math                                                                  +  2  from os import path                                                          +  3   +  4  # I'm a comment :) +  5   +  6  string_var ="Hello, world!" +  7  int_var =42 +  8  float_var =3.14 +  9  complex_var =1+2j + 10   + 11  list_var = [12345]                                                   + 12  tuple_var = (12345)                                                  + 13  set_var = {12345}                                                    + 14  dict_var = {"a"1"b"2"c"3}                                          + 15   + 16  deffunction_no_args():                                                      + 17  return"No arguments" + 18   + 19  deffunction_with_args(a, b):                                                + 20  return a + b                                                             + 21   + 22  deffunction_with_default_args(a=0, b=0):                                    + 23  return a * b                                                             + 24   + 25  lambda_func =lambda x: x**2 + 26   + 27  if int_var ==42:                                                            + 28  print("It's the answer!")                                                + 29  elif int_var <42:                                                           + 30  print("Less than the answer.")                                           + 31  else:                                                                        + 32  print("Greater than the answer.")                                        + 33   + 34  for index, value inenumerate(list_var):                                     + 35  print(f"Index: {index}, Value: {value}")                                 + 36   + 37  counter =0 + 38  while counter <5:                                                           + 39  print(f"Counter value: {counter}")                                       + 40      counter +=1 + 41   + 42  squared_numbers = [x**2for x inrange(10if x %2==0]                    + 43   + 44  try:                                                                         + 45      result =10/0 + 46  except ZeroDivisionError:                                                    + 47  print("Cannot divide by zero!")                                          + 48  finally:                                                                     + 49  print("End of try-except block.")                                        + 50   + 51  classAnimal:                                                                + 52  def__init__(self, name):                                                + 53          self.name = name                                                     + 54   + 55  defspeak(self):                                                         + 56  raiseNotImplementedError("Subclasses must implement this method." + 57   + 58  classDog(Animal):                                                           + 59  defspeak(self):                                                         + 60  returnf"{self.name} says Woof!" + 61   + 62  deffibonacci(n):                                                            + 63      a, b =01 + 64  for _ inrange(n):                                                       + 65  yield a                                                              + 66          a, b = b, a + b                                                      + 67   + 68  for num infibonacci(5):                                                     + 69  print(num)                                                               + 70   + 71  withopen('test.txt''w'as f:                                             + 72      f.write("Testing with statement.")                                       + 73   + 74  @my_decorator                                                                + 75  defsay_hello():                                                             + 76  print("Hello!")                                                          + 77   + 78  say_hello()                                                                  + 79   + + + + + + ''' +# --- +# name: test_text_area_language_rendering[regex] + ''' + + + + + + + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 "b" +  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 break + 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 "a"   + 22  (?<!a)b         # Negative lookbehind: matches "b" that is not preceded by " + 23  a(?=b)          # Positive lookahead: matches "a" that is followed by "b"    + 24  a(?!b)          # Negative lookahead: matches "a" that is not followed by "b + 25   + + + + + + ''' +# --- +# name: test_text_area_language_rendering[sql] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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''U + 20   + 21  INSERTINTO Books (BookID, Title, AuthorID, PublishedDate) VALUES (1'1984' + 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   + + + + + + ''' +# --- +# name: test_text_area_language_rendering[toml] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + - SparklineColorsApp + TextAreaSnapshot - - - - - - - - - - - - - - - - - - - - - - - - - - + + + +  1  # This is a comment in TOML +  2   +  3  string = "Hello, world!" +  4  integer = 42 +  5  float = 3.14 +  6  boolean = true +  7  datetime = 1979-05-27T07:32:00Z +  8   +  9  fruits = ["apple""banana""cherry" + 10   + 11  [address]                               + 12  street = "123 Main St" + 13  city = "Anytown" + 14  state = "CA" + 15  zip = "12345" + 16   + 17  [person.john]                           + 18  name = "John Doe" + 19  age = 28 + 20  is_student = false + 21   + 22   + 23  [[animals]]                             + 24  name = "Fido" + 25  type = "dog" + 26   @@ -26592,9 +32330,9 @@ ''' # --- -# name: test_sparkline_render +# name: test_text_area_language_rendering[yaml] ''' - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + +  1  # This is a comment in YAML +  2   +  3  # Scalars +  4  string"Hello, world!" +  5  integer42 +  6  float3.14 +  7  booleantrue +  8   +  9  # Sequences (Arrays) + 10  fruits:                                               + 11    - Apple + 12    - Banana + 13    - Cherry + 14   + 15  # Nested sequences + 16  persons:                                              + 17    - nameJohn + 18  age28 + 19  is_studentfalse + 20    - nameJane + 21  age22 + 22  is_studenttrue + 23   + 24  # Mappings (Dictionaries) + 25  address:                                              + 26  street123 Main St + 27  cityAnytown + 28  stateCA + 29  zip'12345' + 30   + 31  # Multiline string + 32  description|                                        + 33    This is a multiline                                 + 34    string in YAML. + 35   + 36  # Inline and nested collections + 37  colors: { redFF0000green00FF00blue0000FF }  + 38   + + + + + + ''' +# --- +# name: test_text_area_selection_rendering[selection0] + ''' + + + + + + + + + + + + + + + + + + + + + + + TextAreaSnapshot + + + + + + + + + + I am a line. + ▌                     + I am another line.             + + I am the final line.  + + + + + ''' +# --- +# name: test_text_area_selection_rendering[selection1] + ''' + + + + + + + + + + + + + - - + + - - + + - SparklineSummaryFunctionApp + TextAreaSnapshot - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + I am a line. + ▌                     + I am another line.    + + I am the final line.  ''' # --- -# name: test_switches +# name: test_text_area_selection_rendering[selection2] ''' - + - - + + - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - + + - - + + - - + + + TextAreaSnapshot + + + + + + + + + + I am a line. + ▌                     + I am another line. + ▌                     + I am the final line.  + + + + + ''' +# --- +# name: test_text_area_selection_rendering[selection3] + ''' + + + + + + + + + + - - + + - - + + - - + + - - + + + TextAreaSnapshot + + + + + + + + + + I am a line. + ▌                     + I am another line. + ▌                     + I am the final line. + + + + + ''' +# --- +# name: test_text_area_selection_rendering[selection4] + ''' + + + + + + + + + + - - + + - - + + - - + + - - + + + TextAreaSnapshot + + + + + + + + + + I am a line.          + + I am another line.    + + I am the final line.  + + + + + ''' +# --- +# name: test_text_area_selection_rendering[selection5] + ''' + + + + + + + + + + - - + + - - + + - - + + - SwitchApp + TextAreaSnapshot - - - - - - - - Example switches - - - ▔▔▔▔▔▔▔▔ - off:      - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - on:       - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - focused:  - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - custom:   - ▁▁▁▁▁▁▁▁ - - - - - + + + + I am a line.          + + I am another line.             + + I am the final line.  ''' # --- -# name: test_tabbed_content +# name: test_text_area_themes[dracula] ''' - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - TabbedApp + TextAreaSnapshot - - - - - LetoJessicaPaul - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - Lady Jessica - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Bene Gesserit and concubine of Leto, and mother of Paul and Alia. - - - - PaulAlia - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - First child - - - - - - -  L  Leto  J  Jessica  P  Paul  + + + + 1  defhello(name): + 2      x =123 + 3  whilenotFalse:            + 4  print("hello "+ name)  + 5  continue + 6   + ''' # --- -# name: test_table_markup +# name: test_text_area_themes[github_light] ''' - + - - + + - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + + TextAreaSnapshot + + + + + + + + + + 1  defhello(name): + 2  x=123 + 3  whilenotFalse:            + 4  print("hello "+name + 5  continue + 6   + + + + + + ''' +# --- +# name: test_text_area_themes[monokai] + ''' + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - TableStaticApp + TextAreaSnapshot - - - - ┏━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┓ - FooBar   baz       - ┡━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━┩ - Hello World!ItalicUnderline - └──────────────┴────────┴───────────┘ - - - - - - - - - - - - - - - - - - + + + + 1  defhello(name): + 2      x =123 + 3  whilenotFalse:            + 4  print("hello "+ name)  + 5  continue + 6   @@ -27317,9 +33323,9 @@ ''' # --- -# name: test_tabs_invalidate +# name: test_text_area_themes[vscode_dark] ''' - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - TabApp + TextAreaSnapshot - - - - - Tab 1Tab 2 - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - ────────────────────────────────────────────────────────────────────────────── - - world - - ────────────────────────────────────────────────────────────────────────────── - - - - - - - - - - - - - - - + + + + 1  defhello(name): + 2      x =123 + 3  whilenotFalse:            + 4  print("hello "+ name)  + 5  continue + 6   @@ -27655,141 +33595,141 @@ font-weight: 700; } - .terminal-4254142758-matrix { + .terminal-1791594084-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4254142758-title { + .terminal-1791594084-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4254142758-r1 { fill: #05080f } - .terminal-4254142758-r2 { fill: #e1e1e1 } - .terminal-4254142758-r3 { fill: #c5c8c6 } - .terminal-4254142758-r4 { fill: #1e2226;font-weight: bold } - .terminal-4254142758-r5 { fill: #35393d } - .terminal-4254142758-r6 { fill: #454a50 } - .terminal-4254142758-r7 { fill: #fea62b } - .terminal-4254142758-r8 { fill: #e2e3e3;font-weight: bold } - .terminal-4254142758-r9 { fill: #000000 } - .terminal-4254142758-r10 { fill: #e2e3e3 } - .terminal-4254142758-r11 { fill: #14191f } + .terminal-1791594084-r1 { fill: #05080f } + .terminal-1791594084-r2 { fill: #e1e1e1 } + .terminal-1791594084-r3 { fill: #c5c8c6 } + .terminal-1791594084-r4 { fill: #1e2226;font-weight: bold } + .terminal-1791594084-r5 { fill: #35393d } + .terminal-1791594084-r6 { fill: #454a50 } + .terminal-1791594084-r7 { fill: #fea62b } + .terminal-1791594084-r8 { fill: #e2e3e3;font-weight: bold } + .terminal-1791594084-r9 { fill: #000000 } + .terminal-1791594084-r10 { fill: #e2e3e3 } + .terminal-1791594084-r11 { fill: #14191f } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - BorderApp + BorderApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  ascii  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔+------------------- ascii --------------------+ -  blank || - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|| - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|I must not fear.| -  dashed |Fear is the mind-killer.| - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|Fear is the little-death that brings | - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|total obliteration.| -  double |I will face my fear.| - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▅▅|I will permit it to pass over me and | - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|through me.| -  heavy |And when it has gone past, I will turn| - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|the inner eye to see its path.| - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|Where the fear has gone there will be | -  hidden |nothing. Only I will remain.| - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|| - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|| -  hkey +----------------------------------------------+ - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  inner  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ascii + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔+------------------- ascii --------------------+ + blank|| + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|| + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|I must not fear.| + dashed|Fear is the mind-killer.| + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|Fear is the little-death that brings | + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|total obliteration.| + double|I will face my fear.| + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▅▅|I will permit it to pass over me and | + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|through me.| + heavy|And when it has gone past, I will turn| + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|the inner eye to see its path.| + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|Where the fear has gone there will be | + hidden|nothing. Only I will remain.| + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁|| + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔|| + hkey+----------------------------------------------+ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + inner + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ @@ -27819,152 +33759,152 @@ font-weight: 700; } - .terminal-1131328884-matrix { + .terminal-1491806047-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1131328884-title { + .terminal-1491806047-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1131328884-r1 { fill: #c5c8c6 } - .terminal-1131328884-r2 { fill: #e1e1e1;font-weight: bold } - .terminal-1131328884-r3 { fill: #737373 } - .terminal-1131328884-r4 { fill: #474747 } - .terminal-1131328884-r5 { fill: #0178d4 } - .terminal-1131328884-r6 { fill: #454a50 } - .terminal-1131328884-r7 { fill: #e1e1e1 } - .terminal-1131328884-r8 { fill: #e0e0e0 } - .terminal-1131328884-r9 { fill: #e2e3e3;font-weight: bold } - .terminal-1131328884-r10 { fill: #14191f } - .terminal-1131328884-r11 { fill: #000000 } - .terminal-1131328884-r12 { fill: #1e1e1e } - .terminal-1131328884-r13 { fill: #dde0e6 } - .terminal-1131328884-r14 { fill: #99a1b3 } - .terminal-1131328884-r15 { fill: #dde2e8 } - .terminal-1131328884-r16 { fill: #99a7b9 } - .terminal-1131328884-r17 { fill: #dde4ea } - .terminal-1131328884-r18 { fill: #99adc1 } - .terminal-1131328884-r19 { fill: #dde6ed } - .terminal-1131328884-r20 { fill: #99b4c9 } - .terminal-1131328884-r21 { fill: #dde8f3;font-weight: bold } - .terminal-1131328884-r22 { fill: #ddedf9 } + .terminal-1491806047-r1 { fill: #c5c8c6 } + .terminal-1491806047-r2 { fill: #e1e1e1;font-weight: bold } + .terminal-1491806047-r3 { fill: #737373 } + .terminal-1491806047-r4 { fill: #474747 } + .terminal-1491806047-r5 { fill: #0178d4 } + .terminal-1491806047-r6 { fill: #454a50 } + .terminal-1491806047-r7 { fill: #e1e1e1 } + .terminal-1491806047-r8 { fill: #e0e0e0 } + .terminal-1491806047-r9 { fill: #e2e3e3;font-weight: bold } + .terminal-1491806047-r10 { fill: #000000 } + .terminal-1491806047-r11 { fill: #1e1e1e } + .terminal-1491806047-r12 { fill: #dde0e6 } + .terminal-1491806047-r13 { fill: #99a1b3 } + .terminal-1491806047-r14 { fill: #dde2e8 } + .terminal-1491806047-r15 { fill: #99a7b9 } + .terminal-1491806047-r16 { fill: #dde4ea } + .terminal-1491806047-r17 { fill: #99adc1 } + .terminal-1491806047-r18 { fill: #dde6ed } + .terminal-1491806047-r19 { fill: #99b4c9 } + .terminal-1491806047-r20 { fill: #23568b } + .terminal-1491806047-r21 { fill: #dde8f3;font-weight: bold } + .terminal-1491806047-r22 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ColorsApp + ColorsApp - - - - - Theme ColorsNamed Colors - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  primary ▇▇ - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  secondary "primary" - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  background $primary-darken-3$t - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  primary-background $primary-darken-2$t - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▆▆ -  secondary-background $primary-darken-1$t - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  surface $primary$t - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  D  Toggle dark mode  + + + + + Theme ColorsNamed Colors + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + primary + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + secondary"primary" + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + background$primary-darken-3$t + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + primary-background$primary-darken-2$t + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + secondary-background$primary-darken-1$t + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + surface$primary$t + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +  D  Toggle dark mode  @@ -27994,148 +33934,148 @@ font-weight: 700; } - .terminal-456227705-matrix { + .terminal-740882655-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-456227705-title { + .terminal-740882655-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-456227705-r1 { fill: #454a50 } - .terminal-456227705-r2 { fill: #e1e1e1 } - .terminal-456227705-r3 { fill: #c5c8c6 } - .terminal-456227705-r4 { fill: #24292f;font-weight: bold } - .terminal-456227705-r5 { fill: #262626 } - .terminal-456227705-r6 { fill: #000000 } - .terminal-456227705-r7 { fill: #e2e2e2 } - .terminal-456227705-r8 { fill: #e3e3e3 } - .terminal-456227705-r9 { fill: #e2e3e3;font-weight: bold } - .terminal-456227705-r10 { fill: #14191f } - .terminal-456227705-r11 { fill: #b93c5b } - .terminal-456227705-r12 { fill: #121212 } - .terminal-456227705-r13 { fill: #1e1e1e } - .terminal-456227705-r14 { fill: #fea62b } - .terminal-456227705-r15 { fill: #211505;font-weight: bold } - .terminal-456227705-r16 { fill: #211505 } - .terminal-456227705-r17 { fill: #dde8f3;font-weight: bold } - .terminal-456227705-r18 { fill: #ddedf9 } + .terminal-740882655-r1 { fill: #454a50 } + .terminal-740882655-r2 { fill: #e1e1e1 } + .terminal-740882655-r3 { fill: #c5c8c6 } + .terminal-740882655-r4 { fill: #24292f;font-weight: bold } + .terminal-740882655-r5 { fill: #262626 } + .terminal-740882655-r6 { fill: #000000 } + .terminal-740882655-r7 { fill: #e2e2e2 } + .terminal-740882655-r8 { fill: #e3e3e3 } + .terminal-740882655-r9 { fill: #e2e3e3;font-weight: bold } + .terminal-740882655-r10 { fill: #14191f } + .terminal-740882655-r11 { fill: #b93c5b } + .terminal-740882655-r12 { fill: #121212 } + .terminal-740882655-r13 { fill: #1e1e1e } + .terminal-740882655-r14 { fill: #fea62b } + .terminal-740882655-r15 { fill: #211505;font-weight: bold } + .terminal-740882655-r16 { fill: #211505 } + .terminal-740882655-r17 { fill: #dde8f3;font-weight: bold } + .terminal-740882655-r18 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - EasingApp + EasingApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  round ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Animation Duration:1.0 - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -  out_sine  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -  out_quint  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Welcome to Textual! - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  out_quart I must not fear. - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Fear is the  - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔mind-killer. -  out_quad Fear is the  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁little-death that  - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔brings total  -  out_expo obliteration. - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁I will face my fear. - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔I will permit it to  -  out_elastic pass over me and  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁through me. - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔And when it has gone  -  out_cubic  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ CTRL+P  Focus: Duration Input  CTRL+B  Toggle Dark  + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + round▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Animation Duration:1.0 + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + out_sine + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + out_quint + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Welcome to Textual! + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + out_quartI must not fear. + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁Fear is the  + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔mind-killer. + out_quadFear is the  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁little-death that  + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔brings total  + out_expoobliteration. + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁I will face my fear. + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔I will permit it to  + out_elasticpass over me and  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁through me. + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔And when it has gone  + out_cubic + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ CTRL+P  Focus: Duration Input  CTRL+B  Toggle Dark  @@ -28165,146 +34105,146 @@ font-weight: 700; } - .terminal-391476017-matrix { + .terminal-4085160594-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-391476017-title { + .terminal-4085160594-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-391476017-r1 { fill: #c5c8c6 } - .terminal-391476017-r2 { fill: #e3e3e3 } - .terminal-391476017-r3 { fill: #e1e1e1 } - .terminal-391476017-r4 { fill: #e1e1e1;text-decoration: underline; } - .terminal-391476017-r5 { fill: #e1e1e1;font-weight: bold } - .terminal-391476017-r6 { fill: #e1e1e1;font-style: italic; } - .terminal-391476017-r7 { fill: #98729f;font-weight: bold } - .terminal-391476017-r8 { fill: #d0b344 } - .terminal-391476017-r9 { fill: #98a84b } - .terminal-391476017-r10 { fill: #00823d;font-style: italic; } - .terminal-391476017-r11 { fill: #ffcf56 } - .terminal-391476017-r12 { fill: #e76580 } - .terminal-391476017-r13 { fill: #fea62b;font-weight: bold } - .terminal-391476017-r14 { fill: #f5e5e9;font-weight: bold } - .terminal-391476017-r15 { fill: #b86b00 } - .terminal-391476017-r16 { fill: #780028 } + .terminal-4085160594-r1 { fill: #c5c8c6 } + .terminal-4085160594-r2 { fill: #e3e3e3 } + .terminal-4085160594-r3 { fill: #e1e1e1 } + .terminal-4085160594-r4 { fill: #e1e1e1;text-decoration: underline; } + .terminal-4085160594-r5 { fill: #e1e1e1;font-weight: bold } + .terminal-4085160594-r6 { fill: #e1e1e1;font-style: italic; } + .terminal-4085160594-r7 { fill: #98729f;font-weight: bold } + .terminal-4085160594-r8 { fill: #d0b344 } + .terminal-4085160594-r9 { fill: #98a84b } + .terminal-4085160594-r10 { fill: #00823d;font-style: italic; } + .terminal-4085160594-r11 { fill: #ffcf56 } + .terminal-4085160594-r12 { fill: #e76580 } + .terminal-4085160594-r13 { fill: #fea62b;font-weight: bold } + .terminal-4085160594-r14 { fill: #f5e5e9;font-weight: bold } + .terminal-4085160594-r15 { fill: #b86b00 } + .terminal-4085160594-r16 { fill: #780028 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Textual Keys + Textual Keys - - - - Textual Keys - ╭────────────────────────────────────────────────────────────────────────────╮ - Press some keys! - - To quit the app press ctrl+ctwice or press the Quit button below. - ╰────────────────────────────────────────────────────────────────────────────╯ - Key(key='a'character='a'name='a'is_printable=True) - Key(key='b'character='b'name='b'is_printable=True) - - - - - - - - - - - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -  Clear  Quit  - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + Textual Keys + ╭────────────────────────────────────────────────────────────────────────────╮ + Press some keys! + + To quit the app press ctrl+ctwice or press the Quit button below. + ╰────────────────────────────────────────────────────────────────────────────╯ + Key(key='a'character='a'name='a'is_printable=True) + Key(key='b'character='b'name='b'is_printable=True) + + + + + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ClearQuit + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ @@ -28626,6 +34566,163 @@ ''' # --- +# name: test_unscoped_css + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MyApp + + + + + + + + + + ────────────────────────────────────────────────────────────────────────────── + ─── + foo + ─── + ─── + bar + ─── + ────────────────────────────────────────────────────────────────────────────── + ────────────────────────────────────────────────────────────────────────────── + ─── + foo + ─── + ─── + bar + ─── + ────────────────────────────────────────────────────────────────────────────── + ─────────────────── + This will be styled + ─────────────────── + + + + + + + + + + ''' +# --- # name: test_vertical_layout ''' diff --git a/tests/snapshot_tests/language_snippets.py b/tests/snapshot_tests/language_snippets.py new file mode 100644 index 0000000000..6b55775159 --- /dev/null +++ b/tests/snapshot_tests/language_snippets.py @@ -0,0 +1,465 @@ +PYTHON = """\ +import math +from os import path + +# I'm a comment :) + +string_var = "Hello, world!" +int_var = 42 +float_var = 3.14 +complex_var = 1 + 2j + +list_var = [1, 2, 3, 4, 5] +tuple_var = (1, 2, 3, 4, 5) +set_var = {1, 2, 3, 4, 5} +dict_var = {"a": 1, "b": 2, "c": 3} + +def function_no_args(): + return "No arguments" + +def function_with_args(a, b): + return a + b + +def function_with_default_args(a=0, b=0): + return a * b + +lambda_func = lambda x: x**2 + +if int_var == 42: + print("It's the answer!") +elif int_var < 42: + print("Less than the answer.") +else: + print("Greater than the answer.") + +for index, value in enumerate(list_var): + print(f"Index: {index}, Value: {value}") + +counter = 0 +while counter < 5: + print(f"Counter value: {counter}") + counter += 1 + +squared_numbers = [x**2 for x in range(10) if x % 2 == 0] + +try: + result = 10 / 0 +except ZeroDivisionError: + print("Cannot divide by zero!") +finally: + print("End of try-except block.") + +class Animal: + def __init__(self, name): + self.name = name + + def speak(self): + raise NotImplementedError("Subclasses must implement this method.") + +class Dog(Animal): + def speak(self): + return f"{self.name} says Woof!" + +def fibonacci(n): + a, b = 0, 1 + for _ in range(n): + yield a + a, b = b, a + b + +for num in fibonacci(5): + print(num) + +with open('test.txt', 'w') as f: + f.write("Testing with statement.") + +@my_decorator +def say_hello(): + print("Hello!") + +say_hello() +""" + + +MARKDOWN = """\ +Heading +======= + +Sub-heading +----------- + +### Heading + +#### H4 Heading + +##### H5 Heading + +###### H6 Heading + + +Paragraphs are separated +by a blank line. + +Two spaces at the end of a line +produces a line break. + +Text attributes _italic_, +**bold**, `monospace`. + +Horizontal rule: + +--- + +Bullet list: + + * apples + * oranges + * pears + +Numbered list: + + 1. lather + 2. rinse + 3. repeat + +An [example](http://example.com). + +> Markdown uses email-style > characters for blockquoting. +> +> Lorem ipsum + +![progress](https://github.com/textualize/rich/raw/master/imgs/progress.gif) + + +``` +a=1 +``` + +```python +import this +``` + +```somelang +foobar +``` + + import this + + +1. List item + + Code block +""" + +YAML = """\ +# This is a comment in YAML + +# Scalars +string: "Hello, world!" +integer: 42 +float: 3.14 +boolean: true + +# Sequences (Arrays) +fruits: + - Apple + - Banana + - Cherry + +# Nested sequences +persons: + - name: John + age: 28 + is_student: false + - name: Jane + age: 22 + is_student: true + +# Mappings (Dictionaries) +address: + street: 123 Main St + city: Anytown + state: CA + zip: '12345' + +# Multiline string +description: | + This is a multiline + string in YAML. + +# Inline and nested collections +colors: { red: FF0000, green: 00FF00, blue: 0000FF } +""" + +TOML = """\ +# This is a comment in TOML + +string = "Hello, world!" +integer = 42 +float = 3.14 +boolean = true +datetime = 1979-05-27T07:32:00Z + +fruits = ["apple", "banana", "cherry"] + +[address] +street = "123 Main St" +city = "Anytown" +state = "CA" +zip = "12345" + +[person.john] +name = "John Doe" +age = 28 +is_student = false + + +[[animals]] +name = "Fido" +type = "dog" +""" + +SQL = """\ +-- This is a comment in SQL + +-- Create tables +CREATE TABLE Authors ( + AuthorID INT PRIMARY KEY, + Name VARCHAR(255) NOT NULL, + Country VARCHAR(50) +); + +CREATE TABLE Books ( + BookID INT PRIMARY KEY, + Title VARCHAR(255) NOT NULL, + AuthorID INT, + PublishedDate DATE, + FOREIGN KEY (AuthorID) REFERENCES Authors(AuthorID) +); + +-- Insert data +INSERT INTO Authors (AuthorID, Name, Country) VALUES (1, 'George Orwell', 'UK'); + +INSERT INTO Books (BookID, Title, AuthorID, PublishedDate) VALUES (1, '1984', 1, '1949-06-08'); + +-- Update data +UPDATE Authors SET Country = 'United Kingdom' WHERE Country = 'UK'; + +-- Select data with JOIN +SELECT Books.Title, Authors.Name +FROM Books +JOIN Authors ON Books.AuthorID = Authors.AuthorID; + +-- Delete data (commented to preserve data for other examples) +-- DELETE FROM Books WHERE BookID = 1; + +-- Alter table structure +ALTER TABLE Authors ADD COLUMN BirthDate DATE; + +-- Create index +CREATE INDEX idx_author_name ON Authors(Name); + +-- Drop index (commented to avoid actually dropping it) +-- DROP INDEX idx_author_name ON Authors; + +-- End of script +""" + +CSS = """\ +/* This is a comment in CSS */ + +/* Basic selectors and properties */ +body { + font-family: Arial, sans-serif; + background-color: #f4f4f4; + margin: 0; + padding: 0; +} + +/* Class and ID selectors */ +.header { + background-color: #333; + color: #fff; + padding: 10px 0; + text-align: center; +} + +#logo { + font-size: 24px; + font-weight: bold; +} + +/* Descendant and child selectors */ +.nav ul { + list-style-type: none; + padding: 0; +} + +.nav > li { + display: inline-block; + margin-right: 10px; +} + +/* Pseudo-classes */ +a:hover { + text-decoration: underline; +} + +input:focus { + border-color: #007BFF; +} + +/* Media query */ +@media (max-width: 768px) { + body { + font-size: 16px; + } + + .header { + padding: 5px 0; + } +} + +/* Keyframes animation */ +@keyframes slideIn { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } +} + +.slide-in-element { + animation: slideIn 0.5s forwards; +} +""" + +HTML = """\ + + + + + + + + + HTML Test Page + + + + + + +
+

HTML Test Page

+
+ + + + + +
+
+

Welcome to the Test Page

+

This is a paragraph to test the HTML structure.

+ Test Image +
+
+ + +
+
+ + + +
+
+ + +
+

© 2023 HTML Test Page

+
+ + + + + + +""" + +JSON = """\ +{ + "name": "John Doe", + "age": 30, + "isStudent": false, + "address": { + "street": "123 Main St", + "city": "Anytown", + "state": "CA", + "zip": "12345" + }, + "phoneNumbers": [ + { + "type": "home", + "number": "555-555-1234" + }, + { + "type": "work", + "number": "555-555-5678" + } + ], + "hobbies": ["reading", "hiking", "swimming"], + "pets": [ + { + "type": "dog", + "name": "Fido" + }, + ], + "graduationYear": null +} + +""" + +REGEX = r"""^abc # Matches any string that starts with "abc" +abc$ # Matches any string that ends with "abc" +^abc$ # Matches the string "abc" and nothing else +a.b # Matches any string containing "a", any character, then "b" +a[.]b # Matches the string "a.b" +a|b # Matches either "a" or "b" +a{2} # Matches "aa" +a{2,} # Matches two or more consecutive "a" characters +a{2,5} # Matches between 2 and 5 consecutive "a" characters +a? # Matches "a" or nothing (0 or 1 occurrence of "a") +a* # Matches zero or more consecutive "a" characters +a+ # Matches one or more consecutive "a" characters +\d # Matches any digit (equivalent to [0-9]) +\D # Matches any non-digit +\w # Matches any word character (equivalent to [a-zA-Z0-9_]) +\W # Matches any non-word character +\s # Matches any whitespace character (spaces, tabs, line breaks) +\S # Matches any non-whitespace character +(?i)abc # Case-insensitive match for "abc" +(?:a|b) # Non-capturing group for either "a" or "b" +(?<=a)b # Positive lookbehind: matches "b" that is preceded by "a" +(? ComposeResult: + with Container(id="c1"): + yield Label("foo") + yield Input() + yield Label("Longer label") + yield Input() + with Container(id="c2"): + yield Label("foo") + yield Input() + yield Label("Longer label") + yield Input() + with Container(id="c3"): + yield Label("foo bar " * 10) + yield Input() + yield Label("Longer label") + yield Input() + + +if __name__ == "__main__": + app = GridApp() + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/auto_grid_default_height.py b/tests/snapshot_tests/snapshot_apps/auto_grid_default_height.py new file mode 100644 index 0000000000..1b5883466e --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/auto_grid_default_height.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import Type +from textual.app import App, ComposeResult +from textual.containers import Container, Horizontal, Vertical, Grid +from textual.widgets import Header, Footer, Label +from textual.binding import Binding + + +class GridHeightAuto(App[None]): + CSS = """ + #test-area { + + border: solid red; + height: auto; + } + + Grid { + grid-size: 3; + # grid-rows: auto; + } + """ + + BINDINGS = [ + Binding("g", "grid", "Grid"), + Binding("v", "vertical", "Vertical"), + Binding("h", "horizontal", "Horizontal"), + Binding("c", "container", "Container"), + ] + + def compose(self) -> ComposeResult: + yield Header() + yield Vertical(Label("Select a container to test (see footer)"), id="sandbox") + yield Footer() + + def build(self, out_of: Type[Container | Grid | Horizontal | Vertical]) -> None: + self.query("#sandbox > *").remove() + self.query_one("#sandbox", Vertical).mount( + Label("Here is some text before the grid"), + out_of(*[Label(f"Cell #{n}") for n in range(9)], id="test-area"), + Label("Here is some text after the grid"), + ) + + def action_grid(self): + self.build(Grid) + + def action_vertical(self): + self.build(Vertical) + + def action_horizontal(self): + self.build(Horizontal) + + def action_container(self): + self.build(Container) + + +if __name__ == "__main__": + GridHeightAuto().run() diff --git a/tests/snapshot_tests/snapshot_apps/command_palette.py b/tests/snapshot_tests/snapshot_apps/command_palette.py new file mode 100644 index 0000000000..631aaf4060 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/command_palette.py @@ -0,0 +1,29 @@ +from textual.app import App +from textual.command import Hit, Hits, Provider + + +class TestSource(Provider): + def goes_nowhere_does_nothing(self) -> None: + pass + + async def search(self, query: str) -> Hits: + matcher = self.matcher(query) + for n in range(10): + command = f"This is a test of this code {n}" + yield Hit( + n / 10, + matcher.highlight(command), + self.goes_nowhere_does_nothing, + command, + ) + + +class CommandPaletteApp(App[None]): + COMMANDS = {TestSource} + + def on_mount(self) -> None: + self.action_command_palette() + + +if __name__ == "__main__": + CommandPaletteApp().run() diff --git a/tests/snapshot_tests/snapshot_apps/data_table_add_row_auto_height.py b/tests/snapshot_tests/snapshot_apps/data_table_add_row_auto_height.py new file mode 100644 index 0000000000..23a224b4ff --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/data_table_add_row_auto_height.py @@ -0,0 +1,25 @@ +from rich.panel import Panel +from rich.text import Text + +from textual.app import App +from textual.widgets import DataTable + + +class AutoHeightRowsApp(App[None]): + def compose(self): + table = DataTable() + self.column = table.add_column("N") + table.add_column("Column", width=10) + table.add_row(3, "hey there", height=None) + table.add_row(1, Text("hey there"), height=None) + table.add_row(5, Text("long string", overflow="fold"), height=None) + table.add_row(2, Panel.fit("Hello\nworld"), height=None) + table.add_row(4, "1\n2\n3\n4\n5\n6\n7", height=None) + yield table + + def key_s(self): + self.query_one(DataTable).sort(self.column) + + +if __name__ == "__main__": + AutoHeightRowsApp().run() diff --git a/tests/snapshot_tests/snapshot_apps/data_table_cell_padding.py b/tests/snapshot_tests/snapshot_apps/data_table_cell_padding.py new file mode 100644 index 0000000000..df7283abb3 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/data_table_cell_padding.py @@ -0,0 +1,28 @@ +from textual.app import App, ComposeResult +from textual.widgets import DataTable + + +class TableApp(App): + CSS = """ + DataTable { + margin: 1; + } + """ + + def compose(self) -> ComposeResult: + for cell_padding in range(5): + dt = DataTable(cell_padding=cell_padding) + dt.add_columns("one", "two", "three") + dt.add_row("value", "value", "val") + yield dt + + def key_a(self): + self.query(DataTable).last().cell_padding = 20 + + def key_b(self): + self.query(DataTable).last().cell_padding = 10 + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/data_table_style_order.py b/tests/snapshot_tests/snapshot_apps/data_table_style_order.py index 40844053da..2558c79e51 100644 --- a/tests/snapshot_tests/snapshot_apps/data_table_style_order.py +++ b/tests/snapshot_tests/snapshot_apps/data_table_style_order.py @@ -10,10 +10,14 @@ ] -def make_datatable(foreground_priority: Literal["css", "renderable"], - background_priority: Literal["css", "renderable"]) -> DataTable: - table = DataTable(cursor_foreground_priority=foreground_priority, - cursor_background_priority=background_priority) +def make_datatable( + foreground_priority: Literal["css", "renderable"], + background_priority: Literal["css", "renderable"], +) -> DataTable: + table = DataTable( + cursor_foreground_priority=foreground_priority, + cursor_background_priority=background_priority, + ) table.zebra_stripes = True table.add_column("Movies") for row in data: @@ -30,15 +34,17 @@ class DataTableCursorStyles(App): CSS = """ DataTable {margin-bottom: 1;} -DataTable > .datatable--cursor { - color: $secondary; - background: $success; - text-style: bold italic; -} + DataTable > .datatable--cursor { + color: $secondary; + background: $success; + text-style: bold italic; + } """ def compose(self) -> ComposeResult: - priorities: list[tuple[Literal["css", "renderable"], Literal["css", "renderable"]]] = [ + priorities: list[ + tuple[Literal["css", "renderable"], Literal["css", "renderable"]] + ] = [ ("css", "css"), ("css", "renderable"), ("renderable", "renderable"), @@ -52,5 +58,5 @@ def compose(self) -> ComposeResult: app = DataTableCursorStyles() -if __name__ == '__main__': +if __name__ == "__main__": app.run() diff --git a/tests/snapshot_tests/snapshot_apps/datatable_hot_reloading.py b/tests/snapshot_tests/snapshot_apps/datatable_hot_reloading.py new file mode 100644 index 0000000000..7e3a803155 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/datatable_hot_reloading.py @@ -0,0 +1,58 @@ +from pathlib import Path + +from textual.app import App, ComposeResult +from textual.widgets import DataTable + +CSS_PATH = (Path(__file__) / "../datatable_hot_reloading.tcss").resolve() + +# Write some CSS to the file before the app loads. +# Then, the test will clear all the CSS to see if the +# hot reloading applies the changes correctly. +CSS_PATH.write_text( + """\ +DataTable > .datatable--cursor { + background: purple; +} + +DataTable > .datatable--fixed { + background: red; +} + +DataTable > .datatable--fixed-cursor { + background: blue; +} + +DataTable > .datatable--header { + background: yellow; +} + +DataTable > .datatable--odd-row { + background: pink; +} + +DataTable > .datatable--even-row { + background: brown; +} +""" +) + + +class DataTableHotReloadingApp(App[None]): + CSS_PATH = CSS_PATH + + def compose(self) -> ComposeResult: + yield DataTable(zebra_stripes=True, cursor_type="row") + + def on_mount(self) -> None: + dt = self.query_one(DataTable) + dt.add_column("A", width=10) + self.c = dt.add_column("B") + dt.fixed_columns = 1 + dt.add_row("one", "two") + dt.add_row("three", "four") + dt.add_row("five", "six") + + +if __name__ == "__main__": + app = DataTableHotReloadingApp() + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/hot_reloading_app.css b/tests/snapshot_tests/snapshot_apps/datatable_hot_reloading.tcss similarity index 100% rename from tests/snapshot_tests/snapshot_apps/hot_reloading_app.css rename to tests/snapshot_tests/snapshot_apps/datatable_hot_reloading.tcss diff --git a/tests/snapshot_tests/snapshot_apps/horizontal_auto_width.py b/tests/snapshot_tests/snapshot_apps/horizontal_auto_width.py index ef0528fa9b..2f01abbbe4 100644 --- a/tests/snapshot_tests/snapshot_apps/horizontal_auto_width.py +++ b/tests/snapshot_tests/snapshot_apps/horizontal_auto_width.py @@ -7,6 +7,7 @@ class HorizontalAutoWidth(App): """ Checks that the auto width of the parent Horizontal is correct. """ + def compose(self) -> ComposeResult: yield Horizontal( Static("Docked left 1", id="dock-1"), @@ -17,6 +18,6 @@ def compose(self) -> ComposeResult: ) -app = HorizontalAutoWidth(css_path="horizontal_auto_width.css") -if __name__ == '__main__': +app = HorizontalAutoWidth(css_path="horizontal_auto_width.tcss") +if __name__ == "__main__": app.run() diff --git a/tests/snapshot_tests/snapshot_apps/horizontal_auto_width.css b/tests/snapshot_tests/snapshot_apps/horizontal_auto_width.tcss similarity index 100% rename from tests/snapshot_tests/snapshot_apps/horizontal_auto_width.css rename to tests/snapshot_tests/snapshot_apps/horizontal_auto_width.tcss diff --git a/tests/snapshot_tests/snapshot_apps/hot_reloading_app.py b/tests/snapshot_tests/snapshot_apps/hot_reloading_app.py index d7fc82f220..5f75b9aee4 100644 --- a/tests/snapshot_tests/snapshot_apps/hot_reloading_app.py +++ b/tests/snapshot_tests/snapshot_apps/hot_reloading_app.py @@ -5,7 +5,7 @@ from textual.widgets import Label -CSS_PATH = (Path(__file__) / "../hot_reloading_app.css").resolve() +CSS_PATH = (Path(__file__) / "../hot_reloading_app.tcss").resolve() # Write some CSS to the file before the app loads. # Then, the test will clear all the CSS to see if the diff --git a/tests/snapshot_tests/snapshot_apps/hot_reloading_app.tcss b/tests/snapshot_tests/snapshot_apps/hot_reloading_app.tcss new file mode 100644 index 0000000000..5e9ee82eb7 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/hot_reloading_app.tcss @@ -0,0 +1 @@ +/* This file is purposefully empty. */ diff --git a/tests/snapshot_tests/snapshot_apps/modified_tabs.py b/tests/snapshot_tests/snapshot_apps/modified_tabs.py new file mode 100644 index 0000000000..48bb66d567 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/modified_tabs.py @@ -0,0 +1,27 @@ +from textual.app import App, ComposeResult +from textual.widgets import Button, TabbedContent + + +class FiddleWithTabsApp(App[None]): + CSS = """ + TabPane:disabled { + background: red; + } + """ + + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Button() + yield Button() + yield Button() + yield Button() + yield Button() + + def on_mount(self) -> None: + self.query_one(TabbedContent).disable_tab(f"tab-1") + self.query_one(TabbedContent).disable_tab(f"tab-2") + self.query_one(TabbedContent).hide_tab(f"tab-3") + + +if __name__ == "__main__": + FiddleWithTabsApp().run() diff --git a/tests/snapshot_tests/snapshot_apps/multiple_css/first.css b/tests/snapshot_tests/snapshot_apps/multiple_css/first.tcss similarity index 100% rename from tests/snapshot_tests/snapshot_apps/multiple_css/first.css rename to tests/snapshot_tests/snapshot_apps/multiple_css/first.tcss diff --git a/tests/snapshot_tests/snapshot_apps/multiple_css/multiple_css.py b/tests/snapshot_tests/snapshot_apps/multiple_css/multiple_css.py index 5f97223c17..ed76001d80 100644 --- a/tests/snapshot_tests/snapshot_apps/multiple_css/multiple_css.py +++ b/tests/snapshot_tests/snapshot_apps/multiple_css/multiple_css.py @@ -5,13 +5,13 @@ classvar CSS and two separate CSS files. The background ends up red because classvar CSS wins. The `color` rule tests a clash between loading two external CSS files. -The color ends up as darkred (from 'second.css'), because that file is loaded +The color ends up as darkred (from 'second.tcss'), because that file is loaded second and wins. -- element #two This element tests that separate rules applied to the same widget are mixed -correctly. The color is set to cadetblue in 'first.css', and the background is -darkolivegreen in 'second.css'. Both of these should apply. +correctly. The color is set to cadetblue in 'first.tcss', and the background is +darkolivegreen in 'second.tcss'. Both of these should apply. """ from textual.app import App, ComposeResult from textual.widgets import Static @@ -29,6 +29,6 @@ def compose(self) -> ComposeResult: yield Static("#two", id="two") -app = MultipleCSSApp(css_path=["first.css", "second.css"]) -if __name__ == '__main__': +app = MultipleCSSApp(css_path=["first.tcss", "second.tcss"]) +if __name__ == "__main__": app.run() diff --git a/tests/snapshot_tests/snapshot_apps/multiple_css/second.css b/tests/snapshot_tests/snapshot_apps/multiple_css/second.tcss similarity index 100% rename from tests/snapshot_tests/snapshot_apps/multiple_css/second.css rename to tests/snapshot_tests/snapshot_apps/multiple_css/second.tcss diff --git a/tests/snapshot_tests/snapshot_apps/scoped_css.py b/tests/snapshot_tests/snapshot_apps/scoped_css.py new file mode 100644 index 0000000000..3ee46c108f --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/scoped_css.py @@ -0,0 +1,34 @@ +from textual.app import App, ComposeResult +from textual.widget import Widget +from textual.widgets import Label + + +class MyWidget(Widget): + DEFAULT_CSS = """ + MyWidget { + height: auto; + border: magenta; + } + Label { + border: solid green; + } + """ + + def compose(self) -> ComposeResult: + yield Label("foo") + yield Label("bar") + + def on_mount(self) -> None: + self.log(self.app.stylesheet.css) + + +class MyApp(App): + def compose(self) -> ComposeResult: + yield MyWidget() + yield MyWidget() + yield Label("I should not be styled") + + +if __name__ == "__main__": + app = MyApp() + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/text_area.py b/tests/snapshot_tests/snapshot_apps/text_area.py new file mode 100644 index 0000000000..da6bd06992 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/text_area.py @@ -0,0 +1,15 @@ +"""Tests the rendering of the TextArea for all supported languages.""" +from textual.app import App, ComposeResult +from textual.widgets import TextArea + + +class TextAreaSnapshot(App): + def compose(self) -> ComposeResult: + text_area = TextArea() + text_area.cursor_blink = False + yield text_area + + +app = TextAreaSnapshot() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/text_area_unfocus.py b/tests/snapshot_tests/snapshot_apps/text_area_unfocus.py new file mode 100644 index 0000000000..e092f16721 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/text_area_unfocus.py @@ -0,0 +1,17 @@ +"""Tests the rendering of the TextArea for all supported languages.""" +from textual.app import App, ComposeResult +from textual.widgets import TextArea + + +class TextAreaUnfocusSnapshot(App): + AUTO_FOCUS = None + + def compose(self) -> ComposeResult: + text_area = TextArea() + text_area.cursor_blink = False + yield text_area + + +app = TextAreaUnfocusSnapshot() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/unscoped_css.py b/tests/snapshot_tests/snapshot_apps/unscoped_css.py new file mode 100644 index 0000000000..f0cecbadff --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/unscoped_css.py @@ -0,0 +1,35 @@ +from textual.app import App, ComposeResult +from textual.widget import Widget +from textual.widgets import Label + + +class MyWidget(Widget): + SCOPED_CSS = False + DEFAULT_CSS = """ + MyWidget { + height: auto; + border: magenta; + } + Label { + border: solid green; + } + """ + + def compose(self) -> ComposeResult: + yield Label("foo") + yield Label("bar") + + def on_mount(self) -> None: + self.log(self.app.stylesheet.css) + + +class MyApp(App): + def compose(self) -> ComposeResult: + yield MyWidget() + yield MyWidget() + yield Label("This will be styled") + + +if __name__ == "__main__": + app = MyApp() + app.run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 9d2257f667..7797a8c2bd 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -1,7 +1,13 @@ +import sys from pathlib import Path import pytest +from tests.snapshot_tests.language_snippets import SNIPPETS +from textual.widgets.text_area import Selection, BUILTIN_LANGUAGES +from textual.widgets import TextArea +from textual.widgets.text_area import TextAreaTheme + # These paths should be relative to THIS directory. WIDGET_EXAMPLES_DIR = Path("../../docs/examples/widgets") LAYOUT_EXAMPLES_DIR = Path("../../docs/examples/guide/layout") @@ -89,7 +95,8 @@ def test_input_validation(snap_compare): "tab", "3", # This is valid, so -valid should be applied "tab", - *"-2", # -2 is invalid, so -invalid should be applied (and :focus, since we stop here) + *"-2", + # -2 is invalid, so -invalid should be applied (and :focus, since we stop here) ] assert snap_compare(SNAPSHOT_APPS_DIR / "input_validation.py", press=press) @@ -148,6 +155,30 @@ def test_datatable_add_column(snap_compare): assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_add_column.py") +def test_datatable_add_row_auto_height(snap_compare): + # Check that rows added with auto height computation look right. + assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_add_row_auto_height.py") + + +def test_datatable_add_row_auto_height_sorted(snap_compare): + # Check that rows added with auto height computation look right. + assert snap_compare( + SNAPSHOT_APPS_DIR / "data_table_add_row_auto_height.py", press=["s"] + ) + + +def test_datatable_cell_padding(snap_compare): + # Check that horizontal cell padding is respected. + assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_cell_padding.py") + + +def test_datatable_change_cell_padding(snap_compare): + # Check that horizontal cell padding is respected. + assert snap_compare( + SNAPSHOT_APPS_DIR / "data_table_cell_padding.py", press=["a", "b"] + ) + + def test_footer_render(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "footer.py") @@ -222,6 +253,11 @@ def test_tabbed_content(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "tabbed_content.py") +def test_tabbed_content_with_modified_tabs(snap_compare): + # Tabs enabled and hidden. + assert snap_compare(SNAPSHOT_APPS_DIR / "modified_tabs.py") + + def test_option_list_strings(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "option_list_strings.py") @@ -280,6 +316,14 @@ def test_progress_bar_completed_styled(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "progress_bar_styled_.py", press=["u"]) +def test_rule_horizontal_rules(snap_compare): + assert snap_compare(WIDGET_EXAMPLES_DIR / "horizontal_rules.py") + + +def test_rule_vertical_rules(snap_compare): + assert snap_compare(WIDGET_EXAMPLES_DIR / "vertical_rules.py") + + def test_select(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "select_widget.py") @@ -317,6 +361,26 @@ def test_sparkline_component_classes_colors(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "sparkline_colors.py") +def test_collapsible_render(snap_compare): + assert snap_compare(WIDGET_EXAMPLES_DIR / "collapsible.py") + + +def test_collapsible_collapsed(snap_compare): + assert snap_compare(WIDGET_EXAMPLES_DIR / "collapsible.py", press=["c"]) + + +def test_collapsible_expanded(snap_compare): + assert snap_compare(WIDGET_EXAMPLES_DIR / "collapsible.py", press=["e"]) + + +def test_collapsible_nested(snap_compare): + assert snap_compare(WIDGET_EXAMPLES_DIR / "collapsible_nested.py") + + +def test_collapsible_custom_symbol(snap_compare): + assert snap_compare(WIDGET_EXAMPLES_DIR / "collapsible_custom_symbol.py") + + # --- CSS properties --- # We have a canonical example for each CSS property that is shown in their docs. # If any of these change, something has likely broken, so snapshot each of them. @@ -478,6 +542,20 @@ async def run_before(pilot): ) +def test_datatable_hot_reloading(snap_compare): + """Regression test for https://github.com/Textualize/textual/issues/3312.""" + + async def run_before(pilot): + css_file = pilot.app.CSS_PATH + with open(css_file, "w") as f: + f.write("/* This file is purposefully empty. */\n") # Clear all the CSS. + await pilot.app._on_css_change() + + assert snap_compare( + SNAPSHOT_APPS_DIR / "datatable_hot_reloading.py", run_before=run_before + ) + + def test_layer_fix(snap_compare): # Check https://github.com/Textualize/textual/issues/1358 assert snap_compare(SNAPSHOT_APPS_DIR / "layer_fix.py", press=["d"]) @@ -579,6 +657,7 @@ def test_blur_on_disabled(snap_compare): def test_tooltips_in_compound_widgets(snap_compare): # https://github.com/Textualize/textual/issues/2641 async def run_before(pilot) -> None: + await pilot.pause() await pilot.hover("ProgressBar") await pilot.pause(0.3) await pilot.pause() @@ -586,6 +665,17 @@ async def run_before(pilot) -> None: assert snap_compare(SNAPSHOT_APPS_DIR / "tooltips.py", run_before=run_before) +def test_command_palette(snap_compare) -> None: + from textual.command import CommandPalette + + async def run_before(pilot) -> None: + await pilot.press("ctrl+backslash") + await pilot.press("A") + await pilot.app.query_one(CommandPalette).workers.wait_for_complete() + + assert snap_compare(SNAPSHOT_APPS_DIR / "command_palette.py", run_before=run_before) + + # --- textual-dev library preview tests --- @@ -630,5 +720,107 @@ def test_nested_fr(snap_compare) -> None: assert snap_compare(SNAPSHOT_APPS_DIR / "nested_fr.py") +@pytest.mark.skipif( + sys.version_info < (3, 8), reason="tree-sitter requires python3.8 or higher" +) +@pytest.mark.parametrize("language", BUILTIN_LANGUAGES) +def test_text_area_language_rendering(language, snap_compare): + # This test will fail if we're missing a snapshot test for a valid + # language. We should have a snapshot test for each language we support + # as the syntax highlighting will be completely different for each of them. + + snippet = SNIPPETS.get(language) + + def setup_language(pilot) -> None: + text_area = pilot.app.query_one(TextArea) + text_area.load_text(snippet) + text_area.language = language + + assert snap_compare( + SNAPSHOT_APPS_DIR / "text_area.py", + run_before=setup_language, + terminal_size=(80, snippet.count("\n") + 2), + ) + + +@pytest.mark.parametrize( + "selection", + [ + Selection((0, 0), (2, 8)), + Selection((1, 0), (0, 0)), + Selection((5, 2), (0, 0)), + Selection((0, 0), (4, 20)), + Selection.cursor((1, 0)), + Selection.cursor((2, 6)), + ], +) +def test_text_area_selection_rendering(snap_compare, selection): + text = """I am a line. + +I am another line. + +I am the final line.""" + + def setup_selection(pilot): + text_area = pilot.app.query_one(TextArea) + text_area.load_text(text) + text_area.show_line_numbers = False + text_area.selection = selection + + assert snap_compare( + SNAPSHOT_APPS_DIR / "text_area.py", + run_before=setup_selection, + terminal_size=(30, text.count("\n") + 1), + ) + + +@pytest.mark.skipif( + sys.version_info < (3, 8), reason="tree-sitter requires python3.8 or higher" +) +@pytest.mark.parametrize( + "theme_name", [theme.name for theme in TextAreaTheme.builtin_themes()] +) +def test_text_area_themes(snap_compare, theme_name): + """Each theme should have its own snapshot with at least some Python + to check that the rendering is sensible. This also ensures that theme + switching results in the display changing correctly.""" + text = """\ +def hello(name): + x = 123 + while not False: + print("hello " + name) + continue +""" + + def setup_theme(pilot): + text_area = pilot.app.query_one(TextArea) + text_area.load_text(text) + text_area.language = "python" + text_area.selection = Selection((0, 1), (1, 9)) + text_area.theme = theme_name + + assert snap_compare( + SNAPSHOT_APPS_DIR / "text_area.py", + run_before=setup_theme, + terminal_size=(48, text.count("\n") + 2), + ) + + def test_digits(snap_compare) -> None: assert snap_compare(SNAPSHOT_APPS_DIR / "digits.py") + + +def test_auto_grid(snap_compare) -> None: + assert snap_compare(SNAPSHOT_APPS_DIR / "auto_grid.py") + + +def test_auto_grid_default_height(snap_compare) -> None: + assert snap_compare(SNAPSHOT_APPS_DIR / "auto_grid_default_height.py", press=["g"]) + + +def test_scoped_css(snap_compare) -> None: + assert snap_compare(SNAPSHOT_APPS_DIR / "scoped_css.py") + + +def test_unscoped_css(snap_compare) -> None: + assert snap_compare(SNAPSHOT_APPS_DIR / "unscoped_css.py") diff --git a/tests/test_app.py b/tests/test_app.py index 268eebe7b7..1fe20a46a4 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,3 +1,5 @@ +import contextlib + from textual.app import App, ComposeResult from textual.widgets import Button, Input @@ -28,14 +30,14 @@ async def test_hover_update_styles(): app = MyApp() async with app.run_test() as pilot: button = app.query_one(Button) - assert button.pseudo_classes == {"enabled", "can-focus"} + assert button.pseudo_classes == {"enabled", "can-focus", "dark"} # Take note of the initial background colour initial_background = button.styles.background await pilot.hover(Button) # We've hovered, so ensure the pseudoclass is present and background changed - assert button.pseudo_classes == {"enabled", "hover", "can-focus"} + assert button.pseudo_classes == {"enabled", "hover", "can-focus", "dark"} assert button.styles.background != initial_background @@ -67,3 +69,40 @@ def test_setting_sub_title(): app.sub_title = [True, False, 2] assert app.sub_title == "[True, False, 2]" + + +async def test_default_return_code_is_zero(): + app = App() + async with app.run_test(): + app.exit() + assert app.return_code == 0 + + +async def test_return_code_is_one_after_crash(): + class MyApp(App): + def key_p(self): + 1 / 0 + + app = MyApp() + with contextlib.suppress(ZeroDivisionError): + async with app.run_test() as pilot: + await pilot.press("p") + assert app.return_code == 1 + + +async def test_set_return_code(): + app = App() + async with app.run_test(): + app.exit(return_code=42) + assert app.return_code == 42 + + +def test_no_return_code_before_running(): + app = App() + assert app.return_code is None + + +async def test_no_return_code_while_running(): + app = App() + async with app.run_test(): + assert app.return_code is None diff --git a/tests/test_binding_inheritance.py b/tests/test_binding_inheritance.py index 6e4c59b0f0..431920422c 100644 --- a/tests/test_binding_inheritance.py +++ b/tests/test_binding_inheritance.py @@ -39,7 +39,7 @@ class NoBindings(App[None]): async def test_just_app_no_bindings() -> None: """An app with no bindings should have no bindings, other than ctrl+c.""" async with NoBindings().run_test() as pilot: - assert list(pilot.app._bindings.keys.keys()) == ["ctrl+c"] + assert list(pilot.app._bindings.keys.keys()) == ["ctrl+c", "ctrl+backslash"] assert pilot.app._bindings.get_key("ctrl+c").priority is True @@ -60,7 +60,9 @@ class AlphaBinding(App[None]): async def test_just_app_alpha_binding() -> None: """An app with a single binding should have just the one binding.""" async with AlphaBinding().run_test() as pilot: - assert sorted(pilot.app._bindings.keys.keys()) == sorted(["ctrl+c", "a"]) + assert sorted(pilot.app._bindings.keys.keys()) == sorted( + ["ctrl+c", "ctrl+backslash", "a"] + ) assert pilot.app._bindings.get_key("ctrl+c").priority is True assert pilot.app._bindings.get_key("a").priority is True @@ -82,7 +84,9 @@ class LowAlphaBinding(App[None]): async def test_just_app_low_priority_alpha_binding() -> None: """An app with a single low-priority binding should have just the one binding.""" async with LowAlphaBinding().run_test() as pilot: - assert sorted(pilot.app._bindings.keys.keys()) == sorted(["ctrl+c", "a"]) + assert sorted(pilot.app._bindings.keys.keys()) == sorted( + ["ctrl+c", "ctrl+backslash", "a"] + ) assert pilot.app._bindings.get_key("ctrl+c").priority is True assert pilot.app._bindings.get_key("a").priority is False diff --git a/tests/test_border_subtitle.py b/tests/test_border_subtitle.py new file mode 100644 index 0000000000..bdbeb2b3c4 --- /dev/null +++ b/tests/test_border_subtitle.py @@ -0,0 +1,18 @@ +from textual.app import App, ComposeResult +from textual.widget import Widget + + +async def test_border_subtitle(): + class BorderWidget(Widget): + BORDER_TITLE = "foo" + BORDER_SUBTITLE = "bar" + + class SimpleApp(App): + def compose(self) -> ComposeResult: + yield BorderWidget() + + empty_app = SimpleApp() + async with empty_app.run_test() as pilot: + widget = empty_app.query_one(BorderWidget) + assert widget.border_title == "foo" + assert widget.border_subtitle == "bar" diff --git a/tests/test_cache.py b/tests/test_cache.py index ed2cb028f8..935c5dbfbe 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -259,3 +259,29 @@ def test_discard(): cache.discard("key4") # key that does not exist assert len(cache) == 2 # size should not change + + +def test_discard_regression(): + """Regression test for https://github.com/Textualize/textual/issues/3537""" + + cache = LRUCache(maxsize=3) + cache[1] = "foo" + cache[2] = "bar" + cache[3] = "baz" + cache[4] = "egg" + + assert cache.keys() == {2, 3, 4} + + cache.discard(2) + assert cache.keys() == {3, 4} + + cache[5] = "bob" + assert cache.keys() == {3, 4, 5} + + cache.discard(5) + assert cache.keys() == {3, 4} + + cache.discard(4) + cache.discard(3) + + assert cache.keys() == set() diff --git a/tests/test_call_later.py b/tests/test_call_x_schedulers.py similarity index 100% rename from tests/test_call_later.py rename to tests/test_call_x_schedulers.py diff --git a/tests/test_collapsible.py b/tests/test_collapsible.py new file mode 100644 index 0000000000..db6f4c2147 --- /dev/null +++ b/tests/test_collapsible.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +from textual.app import App, ComposeResult +from textual.widgets import Collapsible, Label +from textual.widgets._collapsible import CollapsibleTitle + +COLLAPSED_CLASS = "-collapsed" + + +def get_title(collapsible: Collapsible) -> CollapsibleTitle: + return collapsible.get_child_by_type(CollapsibleTitle) + + +def get_contents(collapsible: Collapsible) -> Collapsible.Contents: + return collapsible.get_child_by_type(Collapsible.Contents) + + +async def test_collapsible(): + """It should be possible to access title and collapsed.""" + collapsible = Collapsible(title="Pilot", collapsed=True) + assert collapsible._title.label == "Pilot" + assert collapsible.collapsed + + +async def test_compose_default_collapsible(): + """Test default settings of Collapsible with 1 widget in contents.""" + + class CollapsibleApp(App[None]): + def compose(self) -> ComposeResult: + yield Collapsible(Label("Some Contents")) + + async with CollapsibleApp().run_test() as pilot: + collapsible = pilot.app.query_one(Collapsible) + assert get_title(collapsible).label == "Toggle" + assert len(get_contents(collapsible).children) == 1 + + +async def test_compose_empty_collapsible(): + """It should be possible to create an empty Collapsible.""" + + class CollapsibleApp(App[None]): + def compose(self) -> ComposeResult: + yield Collapsible() + + async with CollapsibleApp().run_test() as pilot: + collapsible = pilot.app.query_one(Collapsible) + assert len(get_contents(collapsible).children) == 0 + + +async def test_compose_nested_collapsible(): + """Children Collapsibles are independent from parents Collapsibles.""" + + class CollapsibleApp(App[None]): + def compose(self) -> ComposeResult: + with Collapsible(Label("Outer"), id="outer", collapsed=False): + yield Collapsible(Label("Inner"), id="inner", collapsed=False) + + async with CollapsibleApp().run_test() as pilot: + outer: Collapsible = pilot.app.get_child_by_id("outer") + inner: Collapsible = get_contents(outer).get_child_by_id("inner") + outer.collapsed = True + assert not inner.collapsed + + +async def test_compose_expanded_collapsible(): + """It should be possible to create a Collapsible with expanded contents.""" + + class CollapsibleApp(App[None]): + def compose(self) -> ComposeResult: + yield Collapsible(collapsed=False) + + async with CollapsibleApp().run_test() as pilot: + collapsible = pilot.app.query_one(Collapsible) + assert not get_title(collapsible).has_class(COLLAPSED_CLASS) + assert not get_contents(collapsible).has_class(COLLAPSED_CLASS) + + +async def test_collapsible_collapsed_contents_display_false(): + """Test default settings of Collapsible with 1 widget in contents.""" + + class CollapsibleApp(App[None]): + def compose(self) -> ComposeResult: + yield Collapsible(Label("Some Contents"), collapsed=True) + + async with CollapsibleApp().run_test() as pilot: + collapsible = pilot.app.query_one(Collapsible) + assert not get_contents(collapsible).display + + +async def test_collapsible_expanded_contents_display_true(): + """Test default settings of Collapsible with 1 widget in contents.""" + + class CollapsibleApp(App[None]): + def compose(self) -> ComposeResult: + yield Collapsible(Label("Some Contents"), collapsed=False) + + async with CollapsibleApp().run_test() as pilot: + collapsible = pilot.app.query_one(Collapsible) + assert get_contents(collapsible).display + + +async def test_toggle_title(): + """Clicking title should update ``collapsed``.""" + + class CollapsibleApp(App[None]): + def compose(self) -> ComposeResult: + yield Collapsible(collapsed=False) + + async with CollapsibleApp().run_test() as pilot: + collapsible = pilot.app.query_one(Collapsible) + assert not collapsible.collapsed + + await pilot.click(CollapsibleTitle) + assert collapsible.collapsed + + await pilot.click(CollapsibleTitle) + assert not collapsible.collapsed diff --git a/tests/test_containers.py b/tests/test_containers.py index 22a4ad0ec7..52fddf1a71 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -98,3 +98,24 @@ def compose(self) -> ComposeResult: middle = app.query_one(Middle) assert middle.size.width == 4 assert middle.size.height == app.size.height + + +async def test_scrollbar_zero_thickness(): + """Ensuring that scrollbars can be set to zero thickness.""" + + class ScrollbarZero(App): + CSS = """* { + scrollbar-size: 0 0; + scrollbar-size-vertical: 0; /* just exercising the parser */ + scrollbar-size-horizontal: 0; /* exercise the parser */ + } + """ + + def compose(self) -> ComposeResult: + with Vertical(): + for _ in range(10): + yield Label("Hello, world!") + + app = ScrollbarZero() + async with app.run_test(size=(8, 6)): + pass diff --git a/tests/test_data_table.py b/tests/test_data_table.py index 1c943ee287..cdfb705895 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -3,15 +3,17 @@ from operator import itemgetter import pytest +from rich.panel import Panel from rich.text import Text from textual._wait import wait_for_idle from textual.actions import SkipAction -from textual.app import App +from textual.app import App, RenderableType from textual.coordinate import Coordinate from textual.geometry import Offset from textual.message import Message from textual.widgets import DataTable +from textual.widgets._data_table import _DEFAULT_CELL_X_PADDING from textual.widgets.data_table import ( CellDoesNotExist, CellKey, @@ -175,11 +177,13 @@ async def test_empty_table_interactions(): assert app.message_names == [] -async def test_cursor_movement_with_home_pagedown_etc(): +@pytest.mark.parametrize("show_header", [True, False]) +async def test_cursor_movement_with_home_pagedown_etc(show_header): app = DataTableApp() async with app.run_test() as pilot: table = app.query_one(DataTable) + table.show_header = show_header table.add_columns("A", "B") table.add_rows(ROWS) await pilot.press("right", "pagedown") @@ -270,7 +274,10 @@ async def test_add_column_with_width(): row = table.add_row("123") assert table.get_cell(row, column) == "123" assert table.columns[column].width == 10 - assert table.columns[column].render_width == 12 # 10 + (2 padding) + assert ( + table.columns[column].get_render_width(table) + == 10 + 2 * _DEFAULT_CELL_X_PADDING + ) async def test_add_columns(): @@ -304,6 +311,19 @@ async def test_remove_row(): assert len(table.rows) == 2 +async def test_remove_row_and_update(): + """Regression test for https://github.com/Textualize/textual/issues/3470 - + Crash when attempting to remove and update the same cell.""" + app = DataTableApp() + async with app.run_test() as pilot: + table: DataTable = app.query_one(DataTable) + table.add_column("A", key="A") + table.add_row("1", key="1") + table.update_cell("1", "A", "X", update_width=True) + table.remove_row("1") + await pilot.pause() + + async def test_remove_column(): app = DataTableApp() async with app.run_test(): @@ -318,6 +338,19 @@ async def test_remove_column(): assert table.get_row_at(2) == ["2/1"] +async def test_remove_column_and_update(): + """Regression test for https://github.com/Textualize/textual/issues/3470 - + Crash when attempting to remove and update the same cell.""" + app = DataTableApp() + async with app.run_test() as pilot: + table: DataTable = app.query_one(DataTable) + table.add_column("A", key="A") + table.add_row("1", key="1") + table.update_cell("1", "A", "X", update_width=True) + table.remove_column("A") + await pilot.pause() + + async def test_clear(): app = DataTableApp() async with app.run_test(): @@ -688,7 +721,10 @@ async def test_update_cell_at_column_width(label, new_value, new_content_width): table.update_cell_at(Coordinate(0, 0), new_value, update_width=True) await wait_for_idle() assert first_column.content_width == new_content_width - assert first_column.render_width == new_content_width + 2 + assert ( + first_column.get_render_width(table) + == new_content_width + 2 * _DEFAULT_CELL_X_PADDING + ) async def test_coordinate_to_cell_key(): @@ -814,7 +850,7 @@ async def test_hover_mouse_leave(): await pilot.hover(DataTable, offset=Offset(1, 1)) assert table._show_hover_cursor # Move our cursor away from the DataTable, and the hover cursor is hidden - await pilot.hover(DataTable, offset=Offset(-1, -1)) + await pilot.hover(DataTable, offset=Offset(20, 20)) assert not table._show_hover_cursor @@ -1328,3 +1364,83 @@ def custom_sort(row_data): sorted_row_data = (row_data[4], row_data[2], row_data[3], row_data[1]) for i, row in enumerate(sorted_row_data): assert table.get_row_at(i) == row +@pytest.mark.parametrize( + ["cell", "height"], + [ + ("hey there", 1), + (Text("hey there"), 1), + (Text("long string", overflow="fold"), 2), + (Panel.fit("Hello\nworld"), 4), + ("1\n2\n3\n4\n5\n6\n7", 7), + ], +) +async def test_add_row_auto_height(cell: RenderableType, height: int): + app = DataTableApp() + async with app.run_test() as pilot: + table = app.query_one(DataTable) + table.add_column("C", width=10) + row_key = table.add_row(cell, height=None) + row = table.rows.get(row_key) + await pilot.pause() + assert row.height == height + + +async def test_add_row_expands_column_widths(): + """Regression test for https://github.com/Textualize/textual/issues/1026.""" + app = DataTableApp() + + async with app.run_test() as pilot: + table = app.query_one(DataTable) + table.add_column("First") + table.add_column("Second", width=10) + await pilot.pause() + assert ( + table.ordered_columns[0].get_render_width(table) + == 5 + 2 * _DEFAULT_CELL_X_PADDING + ) + assert ( + table.ordered_columns[1].get_render_width(table) + == 10 + 2 * _DEFAULT_CELL_X_PADDING + ) + + table.add_row("a" * 20, "a" * 20) + await pilot.pause() + assert ( + table.ordered_columns[0].get_render_width(table) + == 20 + 2 * _DEFAULT_CELL_X_PADDING + ) + assert ( + table.ordered_columns[1].get_render_width(table) + == 10 + 2 * _DEFAULT_CELL_X_PADDING + ) + + +async def test_cell_padding_updates_virtual_size(): + app = DataTableApp() + + async with app.run_test() as pilot: + table = app.query_one(DataTable) + table.add_column("First") + table.add_column("Second", width=10) + table.add_column("Third") + + width = table.virtual_size.width + + table.cell_padding += 5 + assert width + 5 * 2 * 3 == table.virtual_size.width + + table.cell_padding -= 2 + assert width + 3 * 2 * 3 == table.virtual_size.width + + table.cell_padding += 10 + assert width + 13 * 2 * 3 == table.virtual_size.width + + +async def test_cell_padding_cannot_be_negative(): + app = DataTableApp() + async with app.run_test(): + table = app.query_one(DataTable) + table.cell_padding = -3 + assert table.cell_padding == 0 + table.cell_padding = -1234 + assert table.cell_padding == 0 diff --git a/tests/test_dom.py b/tests/test_dom.py index 1b354067ce..6f06fb8091 100644 --- a/tests/test_dom.py +++ b/tests/test_dom.py @@ -150,8 +150,8 @@ class E(D): assert c_css[0][2] == d_css[0][2] + 1 == 0 # The CSS on the stack is the correct one. - assert e_css[0][1:] == ("E", 0) - assert e_css[1][1:] == ("C", -2) + assert e_css[0][1:] == ("E", 0, "E") + assert e_css[1][1:] == ("C", -2, "C") def test_component_classes_inheritance(): diff --git a/tests/test_focus.py b/tests/test_focus.py index a03b9b53cd..489d808c26 100644 --- a/tests/test_focus.py +++ b/tests/test_focus.py @@ -1,8 +1,10 @@ import pytest from textual.app import App +from textual.containers import Container from textual.screen import Screen from textual.widget import Widget +from textual.widgets import Button class Focusable(Widget, can_focus=True): @@ -201,3 +203,109 @@ def test_focus_next_and_previous_with_str_selector_without_self(screen: Screen): assert screen.focus_previous(".a").id == "foo" assert screen.focus_previous(".a").id == "foo" assert screen.focus_previous(".b").id == "baz" + + +async def test_focus_does_not_move_to_invisible_widgets(): + """Make sure invisible widgets don't get focused by accident. + + This is kind of a regression test for https://github.com/Textualize/textual/issues/3053, + but not really. + """ + + class MyApp(App): + CSS = "#inv { visibility: hidden; }" + + def compose(self): + yield Button("one", id="one") + yield Button("two", id="inv") + yield Button("three", id="three") + + app = MyApp() + async with app.run_test(): + assert app.focused.id == "one" + assert app.screen.focus_next().id == "three" + + +async def test_focus_moves_to_visible_widgets_inside_invisible_containers(): + """Regression test for https://github.com/Textualize/textual/issues/3053.""" + + class MyApp(App): + CSS = """ + #inv { visibility: hidden; } + #three { visibility: visible; } + """ + + def compose(self): + yield Button(id="one") + with Container(id="inv"): + yield Button(id="three") + + app = MyApp() + async with app.run_test(): + assert app.focused.id == "one" + assert app.screen.focus_next().id == "three" + + +async def test_focus_chain_handles_inherited_visibility(): + """Regression test for https://github.com/Textualize/textual/issues/3053 + + This is more or less a test for the interactions between #3053 and #3071. + We want to make sure that the focus chain is computed correctly when going through + a DOM with containers with all sorts of visibilities set. + """ + + class W(Widget): + can_focus = True + + w1 = W(id="one") + c2 = Container(id="two") + w3 = W(id="three") + c4 = Container(id="four") + w5 = W(id="five") + c6 = Container(id="six") + w7 = W(id="seven") + c8 = Container(id="eight") + w9 = W(id="nine") + w10 = W(id="ten") + w11 = W(id="eleven") + w12 = W(id="twelve") + w13 = W(id="thirteen") + + class InheritedVisibilityApp(App[None]): + CSS = """ + #four, #eight, #ten { + visibility: visible; + } + + #six, #thirteen { + visibility: hidden; + } + """ + + def compose(self): + yield w1 # visible, inherited + with c2: # visible, inherited + yield w3 # visible, inherited + with c4: # visible, set + yield w5 # visible, inherited + with c6: # hidden, set + yield w7 # hidden, inherited + with c8: # visible, set + yield w9 # visible, inherited + yield w10 # visible, set + yield w11 # visible, inherited + yield w12 # visible, inherited + yield w13 # invisible, set + + app = InheritedVisibilityApp() + async with app.run_test(): + focus_chain = app.screen.focus_chain + assert focus_chain == [ + w1, + w3, + w5, + w9, + w10, + w11, + w12, + ] diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 71c073d9f2..d2ab460c9a 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -1,6 +1,7 @@ +from rich.style import Style from rich.text import Span -from textual._fuzzy import Matcher +from textual.fuzzy import Matcher def test_match(): @@ -28,13 +29,12 @@ def test_highlight(): matcher = Matcher("foo.bar") spans = matcher.highlight("foo/egg.bar").spans - print(spans) assert spans == [ - Span(0, 1, "bold"), - Span(1, 2, "bold"), - Span(2, 3, "bold"), - Span(7, 8, "bold"), - Span(8, 9, "bold"), - Span(9, 10, "bold"), - Span(10, 11, "bold"), + Span(0, 1, Style(reverse=True)), + Span(1, 2, Style(reverse=True)), + Span(2, 3, Style(reverse=True)), + Span(7, 8, Style(reverse=True)), + Span(8, 9, Style(reverse=True)), + Span(9, 10, Style(reverse=True)), + Span(10, 11, Style(reverse=True)), ] diff --git a/tests/test_header.py b/tests/test_header.py new file mode 100644 index 0000000000..45df30fa20 --- /dev/null +++ b/tests/test_header.py @@ -0,0 +1,151 @@ +from textual.app import App +from textual.screen import Screen +from textual.widgets import Header + + +async def test_screen_title_none_is_ignored(): + class MyScreen(Screen): + def compose(self): + yield Header() + + class MyApp(App): + TITLE = "app title" + + def on_mount(self): + self.push_screen(MyScreen()) + + app = MyApp() + async with app.run_test(): + assert app.query_one("HeaderTitle").text == "app title" + + +async def test_screen_title_overrides_app_title(): + class MyScreen(Screen): + TITLE = "screen title" + + def compose(self): + yield Header() + + class MyApp(App): + TITLE = "app title" + + def on_mount(self): + self.push_screen(MyScreen()) + + app = MyApp() + async with app.run_test(): + assert app.query_one("HeaderTitle").text == "screen title" + + +async def test_screen_title_reactive_updates_title(): + class MyScreen(Screen): + TITLE = "screen title" + + def compose(self): + yield Header() + + class MyApp(App): + TITLE = "app title" + + def on_mount(self): + self.push_screen(MyScreen()) + + app = MyApp() + async with app.run_test() as pilot: + app.screen.title = "new screen title" + await pilot.pause() + assert app.query_one("HeaderTitle").text == "new screen title" + + +async def test_app_title_reactive_does_not_update_title_when_screen_title_is_set(): + class MyScreen(Screen): + TITLE = "screen title" + + def compose(self): + yield Header() + + class MyApp(App): + TITLE = "app title" + + def on_mount(self): + self.push_screen(MyScreen()) + + app = MyApp() + async with app.run_test() as pilot: + app.title = "new app title" + await pilot.pause() + assert app.query_one("HeaderTitle").text == "screen title" + + +async def test_screen_sub_title_none_is_ignored(): + class MyScreen(Screen): + def compose(self): + yield Header() + + class MyApp(App): + SUB_TITLE = "app sub-title" + + def on_mount(self): + self.push_screen(MyScreen()) + + app = MyApp() + async with app.run_test(): + assert app.query_one("HeaderTitle").sub_text == "app sub-title" + + +async def test_screen_sub_title_overrides_app_sub_title(): + class MyScreen(Screen): + SUB_TITLE = "screen sub-title" + + def compose(self): + yield Header() + + class MyApp(App): + SUB_TITLE = "app sub-title" + + def on_mount(self): + self.push_screen(MyScreen()) + + app = MyApp() + async with app.run_test(): + assert app.query_one("HeaderTitle").sub_text == "screen sub-title" + + +async def test_screen_sub_title_reactive_updates_sub_title(): + class MyScreen(Screen): + SUB_TITLE = "screen sub-title" + + def compose(self): + yield Header() + + class MyApp(App): + SUB_TITLE = "app sub-title" + + def on_mount(self): + self.push_screen(MyScreen()) + + app = MyApp() + async with app.run_test() as pilot: + app.screen.sub_title = "new screen sub-title" + await pilot.pause() + assert app.query_one("HeaderTitle").sub_text == "new screen sub-title" + + +async def test_app_sub_title_reactive_does_not_update_sub_title_when_screen_sub_title_is_set(): + class MyScreen(Screen): + SUB_TITLE = "screen sub-title" + + def compose(self): + yield Header() + + class MyApp(App): + SUB_TITLE = "app sub-title" + + def on_mount(self): + self.push_screen(MyScreen()) + + app = MyApp() + async with app.run_test() as pilot: + app.sub_title = "new app sub-title" + await pilot.pause() + assert app.query_one("HeaderTitle").sub_text == "screen sub-title" diff --git a/tests/test_markdown.py b/tests/test_markdown.py index 5002430d7d..da4c016aab 100644 --- a/tests/test_markdown.py +++ b/tests/test_markdown.py @@ -125,3 +125,18 @@ async def test_load_non_existing_file() -> None: await pilot.app.query_one(Markdown).load( Path("---this-does-not-exist---.it.is.not.a.md") ) + + +@pytest.mark.parametrize( + ("anchor", "found"), + [ + ("hello-world", False), + ("hello-there", True), + ] +) +async def test_goto_anchor(anchor: str, found: bool) -> None: + """Going to anchors should return a boolean: whether the anchor was found.""" + document = "# Hello There\n\nGeneral.\n" + async with MarkdownApp(document).run_test() as pilot: + markdown = pilot.app.query_one(Markdown) + assert markdown.goto_anchor(anchor) is found diff --git a/tests/test_markdownviewer.py b/tests/test_markdownviewer.py new file mode 100644 index 0000000000..27ccf0da99 --- /dev/null +++ b/tests/test_markdownviewer.py @@ -0,0 +1,64 @@ +from pathlib import Path + +import pytest + +from textual.app import App, ComposeResult +from textual.geometry import Offset +from textual.widgets import Markdown, MarkdownViewer + +TEST_MARKDOWN = """\ +* [First]({{file}}#first) +* [Second](#second) + +# First + +The first. + +# Second + +The second. +""" + + +class MarkdownFileViewerApp(App[None]): + def __init__(self, markdown_file: Path) -> None: + super().__init__() + self.markdown_file = markdown_file + markdown_file.write_text(TEST_MARKDOWN.replace("{{file}}", markdown_file.name)) + + def compose(self) -> ComposeResult: + yield MarkdownViewer() + + async def on_mount(self) -> None: + self.query_one(MarkdownViewer).show_table_of_contents = False + await self.query_one(MarkdownViewer).go(self.markdown_file) + + +@pytest.mark.parametrize("link", [0, 1]) +async def test_markdown_file_viewer_anchor_link(tmp_path, link: int) -> None: + """Test https://github.com/Textualize/textual/issues/3094""" + async with MarkdownFileViewerApp(Path(tmp_path) / "test.md").run_test() as pilot: + # There's not really anything to test *for* here, but the lack of an + # exception is the win (before the fix this is testing it would have + # been FileNotFoundError). + await pilot.click(Markdown, Offset(2, link)) + + +class MarkdownStringViewerApp(App[None]): + def compose(self) -> ComposeResult: + yield MarkdownViewer(TEST_MARKDOWN.replace("{{file}}", "")) + + async def on_mount(self) -> None: + self.query_one(MarkdownViewer).show_table_of_contents = False + + +@pytest.mark.parametrize("link", [0, 1]) +async def test_markdown_string_viewer_anchor_link(link: int) -> None: + """Test https://github.com/Textualize/textual/issues/3094 + + Also https://github.com/Textualize/textual/pull/3244#issuecomment-1710278718.""" + async with MarkdownStringViewerApp().run_test() as pilot: + # There's not really anything to test *for* here, but the lack of an + # exception is the win (before the fix this is testing it would have + # been FileNotFoundError). + await pilot.click(Markdown, Offset(2, link)) diff --git a/tests/test_message_pump.py b/tests/test_message_pump.py index c02978e690..c6f9d921ca 100644 --- a/tests/test_message_pump.py +++ b/tests/test_message_pump.py @@ -87,3 +87,39 @@ async def test_prevent() -> None: await pilot.pause() assert len(app.input_changed_events) == 1 assert app.input_changed_events[0].value == "foo" + + +async def test_prevent_with_call_next() -> None: + """Test for https://github.com/Textualize/textual/issues/3166. + + Does a callback scheduled with `call_next` respect messages that + were prevented when it was scheduled? + """ + + hits = 0 + + class PreventTestApp(App[None]): + def compose(self) -> ComposeResult: + yield Input() + + def change_input(self) -> None: + self.query_one(Input).value += "a" + + def on_input_changed(self) -> None: + nonlocal hits + hits += 1 + + app = PreventTestApp() + async with app.run_test() as pilot: + app.call_next(app.change_input) + await pilot.pause() + assert hits == 1 + + with app.prevent(Input.Changed): + app.call_next(app.change_input) + await pilot.pause() + assert hits == 1 + + app.call_next(app.change_input) + await pilot.pause() + assert hits == 2 diff --git a/tests/test_path.py b/tests/test_path.py index 501ab0523c..d7088f8be7 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -10,33 +10,33 @@ class RelativePathObjectApp(App[None]): - CSS_PATH = Path("test.css") + CSS_PATH = Path("test.tcss") class RelativePathStrApp(App[None]): - CSS_PATH = "test.css" + CSS_PATH = "test.tcss" class AbsolutePathObjectApp(App[None]): - CSS_PATH = Path("/tmp/test.css") + CSS_PATH = Path("/tmp/test.tcss") class AbsolutePathStrApp(App[None]): - CSS_PATH = "/tmp/test.css" + CSS_PATH = "/tmp/test.tcss" class ListPathApp(App[None]): - CSS_PATH = ["test.css", Path("/another/path.css")] + CSS_PATH = ["test.tcss", Path("/another/path.tcss")] @pytest.mark.parametrize( "app,expected_css_path_attribute", [ - (RelativePathObjectApp(), [APP_DIR / "test.css"]), - (RelativePathStrApp(), [APP_DIR / "test.css"]), - (AbsolutePathObjectApp(), [Path("/tmp/test.css")]), - (AbsolutePathStrApp(), [Path("/tmp/test.css")]), - (ListPathApp(), [APP_DIR / "test.css", Path("/another/path.css")]), + (RelativePathObjectApp(), [APP_DIR / "test.tcss"]), + (RelativePathStrApp(), [APP_DIR / "test.tcss"]), + (AbsolutePathObjectApp(), [Path("/tmp/test.tcss")]), + (AbsolutePathStrApp(), [Path("/tmp/test.tcss")]), + (ListPathApp(), [APP_DIR / "test.tcss", Path("/another/path.tcss")]), ], ) def test_css_paths_of_various_types(app, expected_css_path_attribute): diff --git a/tests/test_pilot.py b/tests/test_pilot.py index d631146c77..43789bb203 100644 --- a/tests/test_pilot.py +++ b/tests/test_pilot.py @@ -5,12 +5,43 @@ from textual import events from textual.app import App, ComposeResult from textual.binding import Binding -from textual.widgets import Label +from textual.containers import Center, Middle +from textual.pilot import OutOfBounds +from textual.widgets import Button, Label KEY_CHARACTERS_TO_TEST = "akTW03" + punctuation """Test some "simple" characters (letters + digits) and all punctuation.""" +class CenteredButtonApp(App): + CSS = """ # Ensure the button is 16 x 3 + Button { + min-width: 16; + max-width: 16; + width: 16; + min-height: 3; + max-height: 3; + height: 3; + } + """ + + def compose(self): + with Center(): + with Middle(): + yield Button() + + +class ManyLabelsApp(App): + """Auxiliary app with a button following many labels.""" + + AUTO_FOCUS = None # So that there's no auto-scrolling. + + def compose(self): + for idx in range(100): + yield Label(f"label {idx}", id=f"label{idx}") + yield Button() + + async def test_pilot_press_ascii_chars(): """Test that the pilot can press most ASCII characters as keys.""" keys_pressed = [] @@ -52,3 +83,197 @@ def action_beep(self) -> None: with pytest.raises(ZeroDivisionError): async with FailingApp().run_test() as pilot: await pilot.press("b") + + +async def test_pilot_click_screen(): + """Regression test for https://github.com/Textualize/textual/issues/3395. + + Check we can use `Screen` as a selector for a click.""" + + async with App().run_test() as pilot: + await pilot.click("Screen") + + +async def test_pilot_hover_screen(): + """Regression test for https://github.com/Textualize/textual/issues/3395. + + Check we can use `Screen` as a selector for a hover.""" + + async with App().run_test() as pilot: + await pilot.hover("Screen") + + +@pytest.mark.parametrize( + ["method", "screen_size", "offset"], + [ + # + ("click", (80, 24), (100, 12)), # Right of screen. + ("click", (80, 24), (100, 36)), # Bottom-right of screen. + ("click", (80, 24), (50, 36)), # Under screen. + ("click", (80, 24), (-10, 36)), # Bottom-left of screen. + ("click", (80, 24), (-10, 12)), # Left of screen. + ("click", (80, 24), (-10, -2)), # Top-left of screen. + ("click", (80, 24), (50, -2)), # Above screen. + ("click", (80, 24), (100, -2)), # Top-right of screen. + # + ("click", (5, 5), (7, 3)), # Right of screen. + ("click", (5, 5), (7, 7)), # Bottom-right of screen. + ("click", (5, 5), (3, 7)), # Under screen. + ("click", (5, 5), (-1, 7)), # Bottom-left of screen. + ("click", (5, 5), (-1, 3)), # Left of screen. + ("click", (5, 5), (-1, -1)), # Top-left of screen. + ("click", (5, 5), (3, -1)), # Above screen. + ("click", (5, 5), (7, -1)), # Top-right of screen. + # + ("hover", (80, 24), (100, 12)), # Right of screen. + ("hover", (80, 24), (100, 36)), # Bottom-right of screen. + ("hover", (80, 24), (50, 36)), # Under screen. + ("hover", (80, 24), (-10, 36)), # Bottom-left of screen. + ("hover", (80, 24), (-10, 12)), # Left of screen. + ("hover", (80, 24), (-10, -2)), # Top-left of screen. + ("hover", (80, 24), (50, -2)), # Above screen. + ("hover", (80, 24), (100, -2)), # Top-right of screen. + # + ("hover", (5, 5), (7, 3)), # Right of screen. + ("hover", (5, 5), (7, 7)), # Bottom-right of screen. + ("hover", (5, 5), (3, 7)), # Under screen. + ("hover", (5, 5), (-1, 7)), # Bottom-left of screen. + ("hover", (5, 5), (-1, 3)), # Left of screen. + ("hover", (5, 5), (-1, -1)), # Top-left of screen. + ("hover", (5, 5), (3, -1)), # Above screen. + ("hover", (5, 5), (7, -1)), # Top-right of screen. + ], +) +async def test_pilot_target_outside_screen_errors(method, screen_size, offset): + """Make sure that targeting a click/hover completely outside of the screen errors.""" + app = App() + async with app.run_test(size=screen_size) as pilot: + pilot_method = getattr(pilot, method) + with pytest.raises(OutOfBounds): + await pilot_method(offset=offset) + + +@pytest.mark.parametrize( + ["method", "offset"], + [ + ("click", (0, 0)), # Top-left corner. + ("click", (40, 0)), # Top edge. + ("click", (79, 0)), # Top-right corner. + ("click", (79, 12)), # Right edge. + ("click", (79, 23)), # Bottom-right corner. + ("click", (40, 23)), # Bottom edge. + ("click", (40, 23)), # Bottom-left corner. + ("click", (0, 12)), # Left edge. + ("click", (40, 12)), # Right in the middle. + # + ("hover", (0, 0)), # Top-left corner. + ("hover", (40, 0)), # Top edge. + ("hover", (79, 0)), # Top-right corner. + ("hover", (79, 12)), # Right edge. + ("hover", (79, 23)), # Bottom-right corner. + ("hover", (40, 23)), # Bottom edge. + ("hover", (40, 23)), # Bottom-left corner. + ("hover", (0, 12)), # Left edge. + ("hover", (40, 12)), # Right in the middle. + ], +) +async def test_pilot_target_inside_screen_is_fine_with_correct_coordinate_system( + method, offset +): + """Make sure that the coordinate system for the click is the correct one. + + Especially relevant because I kept getting confused about the way it works. + """ + app = ManyLabelsApp() + async with app.run_test(size=(80, 24)) as pilot: + app.query_one("#label99").scroll_visible(animate=False) + await pilot.pause() + + pilot_method = getattr(pilot, method) + await pilot_method(offset=offset) + + +@pytest.mark.parametrize( + ["method", "target"], + [ + ("click", "#label0"), + ("click", "#label90"), + ("click", Button), + # + ("hover", "#label0"), + ("hover", "#label90"), + ("hover", Button), + ], +) +async def test_pilot_target_on_widget_that_is_not_visible_errors(method, target): + """Make sure that clicking a widget that is not scrolled into view raises an error.""" + app = ManyLabelsApp() + async with app.run_test(size=(80, 5)) as pilot: + app.query_one("#label50").scroll_visible(animate=False) + await pilot.pause() + + pilot_method = getattr(pilot, method) + with pytest.raises(OutOfBounds): + await pilot_method(target) + + +@pytest.mark.parametrize("method", ["click", "hover"]) +async def test_pilot_target_widget_under_another_widget(method): + """The targeting method should return False when the targeted widget is covered.""" + + class ObscuredButton(App): + CSS = """ + Label { + width: 30; + height: 5; + } + """ + + def compose(self): + yield Button() + yield Label() + + def on_mount(self): + self.query_one(Label).styles.offset = (0, -3) + + app = ObscuredButton() + async with app.run_test() as pilot: + await pilot.pause() + pilot_method = getattr(pilot, method) + assert (await pilot_method(Button)) is False + + +@pytest.mark.parametrize("method", ["click", "hover"]) +async def test_pilot_target_visible_widget(method): + """The targeting method should return True when the targeted widget is hit.""" + + class ObscuredButton(App): + def compose(self): + yield Button() + + app = ObscuredButton() + async with app.run_test() as pilot: + await pilot.pause() + pilot_method = getattr(pilot, method) + assert (await pilot_method(Button)) is True + + +@pytest.mark.parametrize( + ["method", "offset"], + [ + ("click", (0, 0)), + ("click", (2, 0)), + ("click", (10, 23)), + ("click", (70, 0)), + # + ("hover", (0, 0)), + ("hover", (2, 0)), + ("hover", (10, 23)), + ("hover", (70, 0)), + ], +) +async def test_pilot_target_screen_always_true(method, offset): + app = ManyLabelsApp() + async with app.run_test(size=(80, 24)) as pilot: + pilot_method = getattr(pilot, method) + assert (await pilot_method(offset=offset)) is True diff --git a/tests/test_progress_bar.py b/tests/test_progress_bar.py index 64b034817d..bc7f799196 100644 --- a/tests/test_progress_bar.py +++ b/tests/test_progress_bar.py @@ -79,7 +79,7 @@ def test_update_total(): assert pb.total == 1000 pb.update(total=None) - assert pb.total == 1000 + assert pb.total is None pb.update(total=100) assert pb.total == 100 @@ -119,6 +119,15 @@ def test_update(): assert pb.progress == 50 +def test_go_back_to_indeterminate(): + pb = ProgressBar() + + pb.total = 100 + assert pb.percentage == 0 + pb.total = None + assert pb.percentage is None + + @pytest.mark.parametrize( ["show_bar", "show_percentage", "show_eta"], [ diff --git a/tests/test_query.py b/tests/test_query.py index 60dced9716..d09599cdf2 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1,9 +1,17 @@ import pytest from textual.app import App, ComposeResult +from textual.color import Color from textual.containers import Container -from textual.css.query import InvalidQueryFormat, NoMatches, TooManyMatches, WrongType +from textual.css.query import ( + DeclarationError, + InvalidQueryFormat, + NoMatches, + TooManyMatches, + WrongType, +) from textual.widget import Widget +from textual.widgets import Label def test_query(): @@ -230,3 +238,78 @@ def compose(self) -> ComposeResult: results = list(query.results()) assert len(results) == 5 assert not any(node.id == "root-container" for node in results) + + +async def test_query_set_styles_invalid_css_raises_error(): + app = App() + async with app.run_test(): + with pytest.raises(DeclarationError): + app.query(Widget).set_styles(css="random_rule: 1fr;") + + +async def test_query_set_styles_kwds(): + class LabelApp(App): + def compose(self): + yield Label("Some text") + + app = LabelApp() + async with app.run_test(): + # Sanity check. + assert app.query_one(Label).styles.color != Color(255, 0, 0) + app.query(Label).set_styles(color="red") + assert app.query_one(Label).styles.color == Color(255, 0, 0) + + +async def test_query_set_styles_css_and_kwds(): + class LabelApp(App): + def compose(self): + yield Label("Some text") + + app = LabelApp() + async with app.run_test(): + # Sanity checks. + lbl = app.query_one(Label) + assert lbl.styles.color != Color(255, 0, 0) + assert lbl.styles.background != Color(255, 0, 0) + + app.query(Label).set_styles(css="background: red;", color="red") + assert app.query_one(Label).styles.color == Color(255, 0, 0) + assert app.query_one(Label).styles.background == Color(255, 0, 0) + + +async def test_query_set_styles_css(): + class LabelApp(App): + def compose(self): + yield Label("Some text") + + app = LabelApp() + async with app.run_test(): + # Sanity checks. + lbl = app.query_one(Label) + assert lbl.styles.color != Color(255, 0, 0) + assert lbl.styles.background != Color(255, 0, 0) + + app.query(Label).set_styles(css="background: red; color: red;") + assert app.query_one(Label).styles.color == Color(255, 0, 0) + assert app.query_one(Label).styles.background == Color(255, 0, 0) + + +@pytest.mark.parametrize( + "args", [(False, False), (True, False), (True, True), (False, True)] +) +async def test_query_refresh(args): + refreshes = [] + + class MyWidget(Widget): + def refresh(self, *, repaint=None, layout=None): + super().refresh(repaint=repaint, layout=layout) + refreshes.append((repaint, layout)) + + class MyApp(App): + def compose(self): + yield MyWidget() + + app = MyApp() + async with app.run_test() as pilot: + app.query(MyWidget).refresh(repaint=args[0], layout=args[1]) + assert refreshes[-1] == args diff --git a/tests/test_reactive.py b/tests/test_reactive.py index cb5a6b5f2f..9ab1af192c 100644 --- a/tests/test_reactive.py +++ b/tests/test_reactive.py @@ -499,3 +499,81 @@ def _compute_double(self) -> int: async with PrivateComputeTest().run_test() as pilot: pilot.app.base = 5 assert pilot.app.double == 10 + + +async def test_async_reactive_watch_callbacks_go_on_the_watcher(): + """Regression test for https://github.com/Textualize/textual/issues/3036. + + This makes sure that async callbacks are called. + See the next test for sync callbacks. + """ + + from_app = False + from_holder = False + + class Holder(Widget): + attr = var(None) + + def watch_attr(self): + nonlocal from_holder + from_holder = True + + class MyApp(App): + def __init__(self): + super().__init__() + self.holder = Holder() + + def on_mount(self): + self.watch(self.holder, "attr", self.callback) + + def update(self): + self.holder.attr = "hello world" + + async def callback(self): + nonlocal from_app + from_app = True + + async with MyApp().run_test() as pilot: + pilot.app.update() + await pilot.pause() + assert from_holder + assert from_app + + +async def test_sync_reactive_watch_callbacks_go_on_the_watcher(): + """Regression test for https://github.com/Textualize/textual/issues/3036. + + This makes sure that sync callbacks are called. + See the previous test for async callbacks. + """ + + from_app = False + from_holder = False + + class Holder(Widget): + attr = var(None) + + def watch_attr(self): + nonlocal from_holder + from_holder = True + + class MyApp(App): + def __init__(self): + super().__init__() + self.holder = Holder() + + def on_mount(self): + self.watch(self.holder, "attr", self.callback) + + def update(self): + self.holder.attr = "hello world" + + def callback(self): + nonlocal from_app + from_app = True + + async with MyApp().run_test() as pilot: + pilot.app.update() + await pilot.pause() + assert from_holder + assert from_app diff --git a/tests/test_rule.py b/tests/test_rule.py new file mode 100644 index 0000000000..f754eaf3ff --- /dev/null +++ b/tests/test_rule.py @@ -0,0 +1,26 @@ +import pytest + +from textual.widgets import Rule +from textual.widgets.rule import InvalidLineStyle, InvalidRuleOrientation + + +def test_invalid_rule_orientation(): + with pytest.raises(InvalidRuleOrientation): + Rule(orientation="invalid orientation!") + + +def test_invalid_rule_line_style(): + with pytest.raises(InvalidLineStyle): + Rule(line_style="invalid line style!") + + +def test_invalid_reactive_rule_orientation_change(): + rule = Rule() + with pytest.raises(InvalidRuleOrientation): + rule.orientation = "invalid orientation!" + + +def test_invalid_reactive_rule_line_style_change(): + rule = Rule() + with pytest.raises(InvalidLineStyle): + rule.line_style = "invalid line style!" diff --git a/tests/test_screens.py b/tests/test_screens.py index 5f587fd0ee..e5ddacb4d7 100644 --- a/tests/test_screens.py +++ b/tests/test_screens.py @@ -4,11 +4,13 @@ import pytest -from textual.app import App, ScreenStackError, ComposeResult +from textual import work +from textual.app import App, ComposeResult, ScreenStackError from textual.events import MouseMove from textual.geometry import Offset from textual.screen import Screen from textual.widgets import Button, Input, Label +from textual.worker import NoActiveWorker skip_py310 = pytest.mark.skipif( sys.version_info.minor == 10 and sys.version_info.major == 3, @@ -407,4 +409,71 @@ def on_mount(self): assert len(MouseMoveRecordingScreen.mouse_events) == 1 mouse_event = MouseMoveRecordingScreen.mouse_events[0] - assert mouse_event.x, mouse_event.y == (label_offset.x + mouse_offset.x, label_offset.y + mouse_offset.y) + assert mouse_event.x, mouse_event.y == ( + label_offset.x + mouse_offset.x, + label_offset.y + mouse_offset.y, + ) + + +async def test_push_screen_wait_for_dismiss() -> None: + """Test push_screen returns result.""" + + class QuitScreen(Screen[bool]): + BINDINGS = [ + ("y", "quit(True)"), + ("n", "quit(False)"), + ] + + def action_quit(self, quit: bool) -> None: + self.dismiss(quit) + + results: list[bool] = [] + + class ScreensApp(App): + BINDINGS = [("x", "exit")] + + @work + async def action_exit(self) -> None: + result = await self.push_screen(QuitScreen(), wait_for_dismiss=True) + results.append(result) + + app = ScreensApp() + # Press X to exit, then Y to dismiss, expect True result + async with app.run_test() as pilot: + await pilot.press("x", "y") + assert results == [True] + + results.clear() + app = ScreensApp() + # Press X to exit, then N to dismiss, expect False result + async with app.run_test() as pilot: + await pilot.press("x", "n") + assert results == [False] + + +async def test_push_screen_wait_for_dismiss_no_worker() -> None: + """Test wait_for_dismiss raises NoActiveWorker when not using workers.""" + + class QuitScreen(Screen[bool]): + BINDINGS = [ + ("y", "quit(True)"), + ("n", "quit(False)"), + ] + + def action_quit(self, quit: bool) -> None: + self.dismiss(quit) + + results: list[bool] = [] + + class ScreensApp(App): + BINDINGS = [("x", "exit")] + + async def action_exit(self) -> None: + result = await self.push_screen(QuitScreen(), wait_for_dismiss=True) + results.append(result) + + app = ScreensApp() + # using `wait_for_dismiss` outside of a worker should raise NoActiveWorker + with pytest.raises(NoActiveWorker): + async with app.run_test() as pilot: + await pilot.press("x", "y") diff --git a/tests/test_slug.py b/tests/test_slug.py new file mode 100644 index 0000000000..0486966e83 --- /dev/null +++ b/tests/test_slug.py @@ -0,0 +1,62 @@ +import pytest + +from textual._slug import TrackedSlugs, slug + + +@pytest.mark.parametrize( + "text, expected", + [ + ("test", "test"), + ("Test", "test"), + (" Test ", "test"), + ("-test-", "-test-"), + ("!test!", "test"), + ("test!!test", "testtest"), + ("test! !test", "test-test"), + ("test test", "test-test"), + ("test test", "test--test"), + ("test test", "test----------test"), + ("--test", "--test"), + ("test--", "test--"), + ("--test--test--", "--test--test--"), + ("test!\"#$%&'()*+,-./:;<=>?@[]^_`{|}~test", "test-_test"), + ("tëst", "t%C3%ABst"), + ("test🙂test", "testtest"), + ("test🤷test", "testtest"), + ("test🤷🏻‍♀️test", "testtest"), + ], +) +def test_simple_slug(text: str, expected: str) -> None: + """The simple slug function should produce the expected slug.""" + assert slug(text) == expected + + +@pytest.fixture(scope="module") +def tracker() -> TrackedSlugs: + return TrackedSlugs() + + +@pytest.mark.parametrize( + "text, expected", + [ + ("test", "test"), + ("test", "test-1"), + ("test", "test-2"), + ("-test-", "-test-"), + ("-test-", "-test--1"), + ("test!\"#$%&'()*+,-./:;<=>?@[]^_`{|}~test", "test-_test"), + ("test!\"#$%&'()*+,-./:;<=>?@[]^_`{|}~test", "test-_test-1"), + ("tëst", "t%C3%ABst"), + ("tëst", "t%C3%ABst-1"), + ("tëst", "t%C3%ABst-2"), + ("test🙂test", "testtest"), + ("test🤷test", "testtest-1"), + ("test🤷🏻‍♀️test", "testtest-2"), + ("test", "test-3"), + ("test", "test-4"), + (" test ", "test-5"), + ], +) +def test_tracked_slugs(tracker: TrackedSlugs, text: str, expected: str) -> None: + """The tracked slugging class should produce the expected slugs.""" + assert tracker.slug(text) == expected diff --git a/tests/test_tabbed_content.py b/tests/test_tabbed_content.py index 882dd11523..f0b029a7f6 100644 --- a/tests/test_tabbed_content.py +++ b/tests/test_tabbed_content.py @@ -427,3 +427,379 @@ def on_tabbed_content_cleared(self) -> None: assert tabbed_content.tab_count == 0 assert tabbed_content.active == "" assert pilot.app.cleared == 1 + + +async def test_disabling_does_not_deactivate_tab(): + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + + def on_mount(self) -> None: + self.query_one("Tab#tab-1").disabled = True + + app = TabbedApp() + async with app.run_test(): + assert app.query_one(Tabs).active == "tab-1" + + +async def test_disabled_tab_cannot_be_clicked(): + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + yield Label("tab-2") + + def on_mount(self) -> None: + self.query_one("Tab#tab-2").disabled = True + + app = TabbedApp() + async with app.run_test() as pilot: + await pilot.click("Tab#tab-2") + assert app.query_one(Tabs).active == "tab-1" + + +async def test_disabling_via_tabbed_content(): + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + yield Label("tab-2") + + def on_mount(self) -> None: + self.query_one(TabbedContent).disable_tab("tab-2") + + app = TabbedApp() + async with app.run_test() as pilot: + await pilot.click("Tab#tab-2") + assert app.query_one(Tabs).active == "tab-1" + + +async def test_disabling_via_tab_pane(): + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + yield Label("tab-2") + + def on_mount(self) -> None: + self.query_one("TabPane#tab-2").disabled = True + + app = TabbedApp() + async with app.run_test() as pilot: + await pilot.pause() + await pilot.click("Tab#tab-2") + assert app.query_one(Tabs).active == "tab-1" + + +async def test_creating_disabled_tab(): + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + with TabPane("first"): + yield Label("hello") + with TabPane("second", disabled=True): + yield Label("world") + + app = TabbedApp() + async with app.run_test() as pilot: + await pilot.click("Tab#tab-2") + assert app.query_one(Tabs).active == "tab-1" + + +async def test_navigation_around_disabled_tabs(): + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + yield Label("tab-2") + yield Label("tab-3") + yield Label("tab-4") + + def on_mount(self) -> None: + self.query_one("Tab#tab-1").disabled = True + self.query_one("Tab#tab-3").disabled = True + + app = TabbedApp() + async with app.run_test(): + tabs = app.query_one(Tabs) + assert tabs.active == "tab-1" + tabs.action_next_tab() + assert tabs.active == "tab-2" + tabs.action_next_tab() + assert tabs.active == "tab-4" + tabs.action_next_tab() + assert tabs.active == "tab-2" + tabs.action_previous_tab() + assert tabs.active == "tab-4" + + +async def test_reenabling_tab(): + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + yield Label("tab-2") + + def on_mount(self) -> None: + self.query_one("Tab#tab-2").disabled = True + + def reenable(self) -> None: + app.query_one("Tab#tab-2").disabled = False + + app = TabbedApp() + async with app.run_test() as pilot: + await pilot.click("Tab#tab-2") + assert app.query_one(Tabs).active == "tab-1" + app.reenable() + await pilot.click("Tab#tab-2") + assert app.query_one(Tabs).active == "tab-2" + + +async def test_reenabling_via_tabbed_content(): + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + yield Label("tab-2") + + def on_mount(self) -> None: + self.query_one(TabbedContent).disable_tab("tab-2") + + def reenable(self) -> None: + self.query_one(TabbedContent).enable_tab("tab-2") + + app = TabbedApp() + async with app.run_test() as pilot: + await pilot.click("Tab#tab-2") + assert app.query_one(Tabs).active == "tab-1" + app.reenable() + await pilot.click("Tab#tab-2") + assert app.query_one(Tabs).active == "tab-2" + + +async def test_reenabling_via_tab_pane(): + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + yield Label("tab-2") + + def on_mount(self) -> None: + self.query_one("TabPane#tab-2").disabled = True + + def reenable(self) -> None: + self.query_one("TabPane#tab-2").disabled = False + + app = TabbedApp() + async with app.run_test() as pilot: + await pilot.pause() + await pilot.click("Tab#tab-2") + assert app.query_one(Tabs).active == "tab-1" + app.reenable() + await pilot.click("Tab#tab-2") + assert app.query_one(Tabs).active == "tab-2" + + +async def test_disabling_unknown_tab(): + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + + app = TabbedApp() + async with app.run_test(): + with pytest.raises(Tabs.TabError): + app.query_one(TabbedContent).disable_tab("foo") + + +async def test_enabling_unknown_tab(): + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + + app = TabbedApp() + async with app.run_test(): + with pytest.raises(Tabs.TabError): + app.query_one(TabbedContent).enable_tab("foo") + + +async def test_hide_unknown_tab(): + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + + app = TabbedApp() + async with app.run_test(): + with pytest.raises(Tabs.TabError): + app.query_one(TabbedContent).hide_tab("foo") + + +async def test_show_unknown_tab(): + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + + app = TabbedApp() + async with app.run_test(): + with pytest.raises(Tabs.TabError): + app.query_one(TabbedContent).show_tab("foo") + + +async def test_hide_show_messages(): + hide_msg = False + show_msg = False + + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + + def on_tabs_tab_hidden(self) -> None: + nonlocal hide_msg + hide_msg = True + + def on_tabs_tab_shown(self) -> None: + nonlocal show_msg + show_msg = True + + app = TabbedApp() + async with app.run_test() as pilot: + app.query_one(TabbedContent).hide_tab("tab-1") + await pilot.pause() + assert hide_msg + app.query_one(TabbedContent).show_tab("tab-1") + await pilot.pause() + assert show_msg + + +async def test_hide_last_tab_means_no_tab_active(): + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + + app = TabbedApp() + async with app.run_test() as pilot: + tabbed_content = app.query_one(TabbedContent) + tabbed_content.hide_tab("tab-1") + await pilot.pause() + assert not tabbed_content.active + + +async def test_hiding_tabs_moves_active_to_next_tab(): + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + yield Label("tab-2") + yield Label("tab-3") + + app = TabbedApp() + async with app.run_test() as pilot: + tabbed_content = app.query_one(TabbedContent) + tabbed_content.hide_tab("tab-1") + await pilot.pause() + assert tabbed_content.active == "tab-2" + tabbed_content.hide_tab("tab-2") + await pilot.pause() + assert tabbed_content.active == "tab-3" + + +async def test_showing_tabs_does_not_change_active_tab(): + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + yield Label("tab-2") + yield Label("tab-3") + + app = TabbedApp() + async with app.run_test() as pilot: + tabbed_content = app.query_one(TabbedContent) + tabbed_content.hide_tab("tab-1") + tabbed_content.hide_tab("tab-2") + await pilot.pause() + # sanity check + assert tabbed_content.active == "tab-3" + + tabbed_content.show_tab("tab-1") + tabbed_content.show_tab("tab-2") + assert tabbed_content.active == "tab-3" + + +@pytest.mark.parametrize("tab_id", ["tab-1", "tab-2"]) +async def test_showing_first_tab_activates_tab(tab_id: str): + class TabbedApp(App[None]): + def compose(self) -> ComposeResult: + with TabbedContent(): + yield Label("tab-1") + yield Label("tab-2") + + app = TabbedApp() + async with app.run_test() as pilot: + tabbed_content = app.query_one(TabbedContent) + tabbed_content.hide_tab("tab-1") + tabbed_content.hide_tab("tab-2") + await pilot.pause() + # sanity check + assert not tabbed_content.active + + tabbed_content.show_tab(tab_id) + await pilot.pause() + assert tabbed_content.active == tab_id + + +async def test_disabling_nested_tabs(): + """Regression test for https://github.com/Textualize/textual/issues/3145.""" + + class TabbedApp(App): + def compose(self) -> ComposeResult: + with TabbedContent(id="tabbed-content"): + with TabPane("Tab Pane 1"): + yield Label("foo") + with TabPane("Tab Pane 2"): + yield Label("bar") + with TabPane("Tab Pane 3"): + with TabbedContent(): + with TabPane("Inner Pane 1"): + yield Label("fizz") + with TabPane("Inner Pane 2"): + yield Label("bang") + + app = TabbedApp() + async with app.run_test() as pilot: + tabber = app.query_one("#tabbed-content", expect_type=TabbedContent) + tabber.disable_tab("tab-1") + await pilot.pause() + tabber.enable_tab("tab-1") + await pilot.pause() + + +async def test_hiding_nested_tabs(): + """Regression test for https://github.com/Textualize/textual/issues/3145.""" + + class TabbedApp(App): + def compose(self) -> ComposeResult: + with TabbedContent(id="tabbed-content"): + with TabPane("Tab Pane 1"): + yield Label("foo") + with TabPane("Tab Pane 2"): + yield Label("bar") + with TabPane("Tab Pane 3"): + with TabbedContent(): + with TabPane("Inner Pane 1"): + yield Label("fizz") + with TabPane("Inner Pane 2"): + yield Label("bang") + + app = TabbedApp() + async with app.run_test() as pilot: + tabber = app.query_one("#tabbed-content", expect_type=TabbedContent) + tabber.hide_tab("tab-1") + await pilot.pause() + tabber.show_tab("tab-1") + await pilot.pause() diff --git a/tests/test_visibility_change.py b/tests/test_visibility_change.py deleted file mode 100644 index 7006827ee2..0000000000 --- a/tests/test_visibility_change.py +++ /dev/null @@ -1,43 +0,0 @@ -"""See https://github.com/Textualize/textual/issues/1355 as the motivation for these tests.""" - -from textual.app import App, ComposeResult -from textual.containers import VerticalScroll -from textual.widget import Widget - - -class VisibleTester(App[None]): - """An app for testing visibility changes.""" - - CSS = """ - Widget { - height: 1fr; - } - .hidden { - visibility: hidden; - } - """ - - def compose(self) -> ComposeResult: - yield VerticalScroll( - Widget(id="keep"), Widget(id="hide-via-code"), Widget(id="hide-via-css") - ) - - -async def test_visibility_changes() -> None: - """Test changing visibility via code and CSS.""" - async with VisibleTester().run_test() as pilot: - assert pilot.app.query_one("#keep").visible is True - assert pilot.app.query_one("#hide-via-code").visible is True - assert pilot.app.query_one("#hide-via-css").visible is True - - pilot.app.query_one("#hide-via-code").styles.visibility = "hidden" - await pilot.pause(0) - assert pilot.app.query_one("#keep").visible is True - assert pilot.app.query_one("#hide-via-code").visible is False - assert pilot.app.query_one("#hide-via-css").visible is True - - pilot.app.query_one("#hide-via-css").set_class(True, "hidden") - await pilot.pause(0) - assert pilot.app.query_one("#keep").visible is True - assert pilot.app.query_one("#hide-via-code").visible is False - assert pilot.app.query_one("#hide-via-css").visible is False diff --git a/tests/test_visible.py b/tests/test_visible.py new file mode 100644 index 0000000000..3d991d8588 --- /dev/null +++ b/tests/test_visible.py @@ -0,0 +1,78 @@ +from textual.app import App, ComposeResult +from textual.containers import VerticalScroll +from textual.widget import Widget + + +async def test_visibility_changes() -> None: + """Test changing visibility via code and CSS. + + See https://github.com/Textualize/textual/issues/1355 as the motivation for these tests. + """ + + class VisibleTester(App[None]): + """An app for testing visibility changes.""" + + CSS = """ + Widget { + height: 1fr; + } + .hidden { + visibility: hidden; + } + """ + + def compose(self) -> ComposeResult: + yield VerticalScroll( + Widget(id="keep"), Widget(id="hide-via-code"), Widget(id="hide-via-css") + ) + + async with VisibleTester().run_test() as pilot: + assert pilot.app.query_one("#keep").visible is True + assert pilot.app.query_one("#hide-via-code").visible is True + assert pilot.app.query_one("#hide-via-css").visible is True + + pilot.app.query_one("#hide-via-code").styles.visibility = "hidden" + await pilot.pause(0) + assert pilot.app.query_one("#keep").visible is True + assert pilot.app.query_one("#hide-via-code").visible is False + assert pilot.app.query_one("#hide-via-css").visible is True + + pilot.app.query_one("#hide-via-css").set_class(True, "hidden") + await pilot.pause(0) + assert pilot.app.query_one("#keep").visible is True + assert pilot.app.query_one("#hide-via-code").visible is False + assert pilot.app.query_one("#hide-via-css").visible is False + + +async def test_visible_is_inherited() -> None: + """Regression test for https://github.com/Textualize/textual/issues/3071""" + + class InheritedVisibilityApp(App[None]): + CSS = """ + #four { + visibility: visible; + } + + #six { + visibility: hidden; + } + """ + + def compose(self): + yield Widget(id="one") + with VerticalScroll(id="two"): + yield Widget(id="three") + with VerticalScroll(id="four"): + yield Widget(id="five") + with VerticalScroll(id="six"): + yield Widget(id="seven") + + app = InheritedVisibilityApp() + async with app.run_test(): + assert app.query_one("#one").visible + assert app.query_one("#two").visible + assert app.query_one("#three").visible + assert app.query_one("#four").visible + assert app.query_one("#five").visible + assert not app.query_one("#six").visible + assert not app.query_one("#seven").visible diff --git a/tests/test_widget.py b/tests/test_widget.py index fb5fdb52e1..4b0058aafe 100644 --- a/tests/test_widget.py +++ b/tests/test_widget.py @@ -10,7 +10,7 @@ from textual.geometry import Offset, Size from textual.message import Message from textual.widget import MountError, PseudoClasses, Widget -from textual.widgets import Label +from textual.widgets import Label, LoadingIndicator @pytest.mark.parametrize( @@ -355,3 +355,31 @@ def test_get_set_tooltip(): assert widget.tooltip == "This is a tooltip." +async def test_loading(): + """Test setting the loading reactive.""" + + class LoadingApp(App): + def compose(self) -> ComposeResult: + yield Label("Hello, World") + + async with LoadingApp().run_test() as pilot: + app = pilot.app + label = app.query_one(Label) + assert label.loading == False + assert len(label.query(LoadingIndicator)) == 0 + + label.loading = True + await pilot.pause() + assert len(label.query(LoadingIndicator)) == 1 + + label.loading = True # Setting to same value is a null-op + await pilot.pause() + assert len(label.query(LoadingIndicator)) == 1 + + label.loading = False + await pilot.pause() + assert len(label.query(LoadingIndicator)) == 0 + + label.loading = False # Setting to same value is a null-op + await pilot.pause() + assert len(label.query(LoadingIndicator)) == 0 diff --git a/tests/text_area/test_edit_via_api.py b/tests/text_area/test_edit_via_api.py new file mode 100644 index 0000000000..4cf8602e0a --- /dev/null +++ b/tests/text_area/test_edit_via_api.py @@ -0,0 +1,522 @@ +"""Tests editing the document using the API (replace etc.) + +The tests in this module directly call the edit APIs on the TextArea rather +than going via bindings. + +Note that more extensive testing for editing is done at the Document level. +""" +import pytest + +from textual.app import App, ComposeResult +from textual.widgets import TextArea +from textual.widgets.text_area import EditResult, Selection + +TEXT = """\ +I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +""" + +SIMPLE_TEXT = """\ +ABCDE +FGHIJ +KLMNO +PQRST +UVWXY +Z +""" + + +class TextAreaApp(App): + def compose(self) -> ComposeResult: + text_area = TextArea() + text_area.load_text(TEXT) + yield text_area + + +async def test_insert_text_start_maintain_selection_offset(): + """Ensure that we can maintain the offset between the location + an insert happens and the location of the selection.""" + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.move_cursor((0, 5)) + text_area.insert("Hello", location=(0, 0)) + assert text_area.text == "Hello" + TEXT + assert text_area.selection == Selection.cursor((0, 10)) + + +async def test_insert_text_start(): + """The document is correctly updated on inserting at the start. + If we don't maintain the selection offset, the cursor jumps + to the end of the edit and the selection is empty.""" + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.move_cursor((0, 5)) + text_area.insert("Hello", location=(0, 0), maintain_selection_offset=False) + assert text_area.text == "Hello" + TEXT + assert text_area.selection == Selection.cursor((0, 5)) + + +async def test_insert_empty_string(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("0123456789") + + text_area.insert("", location=(0, 3)) + + assert text_area.text == "0123456789" + + +async def test_replace_empty_string(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("0123456789") + + text_area.replace("", start=(0, 3), end=(0, 7)) + + assert text_area.text == "012789" + + +@pytest.mark.parametrize( + "cursor_location,insert_location,cursor_destination", + [ + ((0, 3), (0, 2), (0, 4)), # API insert just before cursor + ((0, 3), (0, 3), (0, 4)), # API insert at cursor location + ((0, 3), (0, 4), (0, 3)), # API insert just after cursor + ((0, 3), (0, 5), (0, 3)), # API insert just after cursor + ], +) +async def test_insert_character_near_cursor_maintain_selection_offset( + cursor_location, + insert_location, + cursor_destination, +): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("012345") + text_area.move_cursor(cursor_location) + text_area.insert("X", location=insert_location) + assert text_area.selection == Selection.cursor(cursor_destination) + + +async def test_insert_newlines_start(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.insert("\n\n\n") + assert text_area.text == "\n\n\n" + TEXT + assert text_area.selection == Selection.cursor((3, 0)) + + +async def test_insert_newlines_end(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.insert("\n\n\n", location=(4, 0)) + assert text_area.text == TEXT + "\n\n\n" + + +async def test_insert_windows_newlines(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + # Although we're inserting windows newlines, the configured newline on + # the Document inside the TextArea will be "\n", so when we check TextArea.text + # we expect to see "\n". + text_area.insert("\r\n\r\n\r\n") + assert text_area.text == "\n\n\n" + TEXT + + +async def test_insert_old_mac_newlines(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.insert("\r\r\r") + assert text_area.text == "\n\n\n" + TEXT + + +async def test_insert_text_non_cursor_location(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.insert("Hello", location=(4, 0)) + assert text_area.text == TEXT + "Hello" + assert text_area.selection == Selection.cursor((0, 0)) + + +async def test_insert_text_non_cursor_location_dont_maintain_offset(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.selection = Selection((2, 3), (3, 5)) + + result = text_area.insert( + "Hello", + location=(4, 0), + maintain_selection_offset=False, + ) + + assert result == EditResult( + end_location=(4, 5), + replaced_text="", + ) + assert text_area.text == TEXT + "Hello" + + # Since maintain_selection_offset is False, the selection + # is reset to a cursor and goes to the end of the insert. + assert text_area.selection == Selection.cursor((4, 5)) + + +async def test_insert_multiline_text(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.move_cursor((2, 5)) + text_area.insert("Hello,\nworld!", maintain_selection_offset=False) + expected_content = """\ +I must not fear. +Fear is the mind-killer. +Fear Hello, +world!is the little-death that brings total obliteration. +I will face my fear. +""" + assert text_area.cursor_location == (3, 6) # Cursor moved to end of insert + assert text_area.text == expected_content + + +async def test_insert_multiline_text_maintain_offset(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.move_cursor((2, 5)) + result = text_area.insert("Hello,\nworld!") + + assert result == EditResult( + end_location=(3, 6), + replaced_text="", + ) + + # The insert happens at the cursor (default location) + # Offset is maintained - we inserted 1 line so cursor shifts + # down 1 line, and along by the length of the last insert line. + assert text_area.cursor_location == (3, 6) + expected_content = """\ +I must not fear. +Fear is the mind-killer. +Fear Hello, +world!is the little-death that brings total obliteration. +I will face my fear. +""" + assert text_area.text == expected_content + + +async def test_replace_multiline_text(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + # replace "Fear is the mind-killer\nFear is the little death...\n" + # with "Hello,\nworld!\n" + result = text_area.replace("Hello,\nworld!\n", start=(1, 0), end=(3, 0)) + expected_replaced_text = """\ +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +""" + assert result == EditResult( + end_location=(3, 0), + replaced_text=expected_replaced_text, + ) + + expected_content = """\ +I must not fear. +Hello, +world! +I will face my fear. +""" + assert text_area.selection == Selection.cursor((0, 0)) # cursor didnt move + assert text_area.text == expected_content + + +async def test_replace_multiline_text_maintain_selection(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + + # To begin with, the user selects the word "face" + text_area.selection = Selection((3, 7), (3, 11)) + assert text_area.selected_text == "face" + + # Text is inserted via the API in a way that shifts + # the start and end locations of the word "face" in + # both the horizontal and vertical directions. + text_area.replace( + "Hello,\nworld!\n123\n456", + start=(1, 0), + end=(3, 0), + ) + expected_content = """\ +I must not fear. +Hello, +world! +123 +456I will face my fear. +""" + # Despite this insert, the selection locations are updated + # and the word face is still highlighted. This ensures that + # if text is insert programmatically, a user that is typing + # won't lose their place - the cursor will maintain the same + # relative position in the document as before. + assert text_area.selected_text == "face" + assert text_area.selection == Selection((4, 10), (4, 14)) + assert text_area.text == expected_content + + +async def test_delete_within_line(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.selection = Selection((0, 11), (0, 15)) + assert text_area.selected_text == "fear" + + # Delete some text before the selection location. + result = text_area.delete((0, 6), (0, 10)) + + # Even though the word has 'shifted' left, it's still selected. + assert text_area.selection == Selection((0, 7), (0, 11)) + assert text_area.selected_text == "fear" + + # We've recorded exactly what text was replaced in the EditResult + assert result == EditResult( + end_location=(0, 6), + replaced_text=" not", + ) + + expected_text = """\ +I must fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +""" + assert text_area.text == expected_text + + +async def test_delete_within_line_dont_maintain_offset(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.delete((0, 6), (0, 10), maintain_selection_offset=False) + expected_text = """\ +I must fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +""" + assert text_area.selection == Selection.cursor((0, 6)) # cursor moved + assert text_area.text == expected_text + + +async def test_delete_multiple_lines_selection_above(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + + # User has selected text on the first line... + text_area.selection = Selection((0, 2), (0, 6)) + assert text_area.selected_text == "must" + + # Some lines below are deleted... + result = text_area.delete((1, 0), (3, 0)) + + # The selection is not affected at all. + assert text_area.selection == Selection((0, 2), (0, 6)) + + # We've recorded the text that was deleted in the ReplaceResult. + # Lines of index 1 and 2 were deleted. Since the end + # location of the selection is (3, 0), the newline + # marker is included in the deletion. + expected_replaced_text = """\ +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +""" + assert result == EditResult( + end_location=(1, 0), + replaced_text=expected_replaced_text, + ) + assert ( + text_area.text + == """\ +I must not fear. +I will face my fear. +""" + ) + + +async def test_delete_empty_document(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("") + result = text_area.delete((0, 0), (1, 0)) + assert result.replaced_text == "" + assert text_area.text == "" + + +async def test_clear(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.clear() + + +async def test_clear_empty_document(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("") + text_area.clear() + + +@pytest.mark.parametrize( + "select_from,select_to", + [ + [(0, 3), (2, 1)], + [(2, 1), (0, 3)], # Ensuring independence from selection direction. + ], +) +async def test_insert_text_multiline_selection_top(select_from, select_to): + """ + An example to attempt to explain what we're testing here... + + X = edit range, * = character in TextArea, S = selection + + *********XX + XXXXX***SSS + SSSSSSSSSSS + SSSS******* + + If an edit happens at XXXX, we need to ensure that the SSS on the + same line is adjusted appropriately so that it's still highlighting + the same characters as before. + """ + app = TextAreaApp() + async with app.run_test(): + # ABCDE + # FGHIJ + # KLMNO + # PQRST + # UVWXY + # Z + text_area = app.query_one(TextArea) + text_area.load_text(SIMPLE_TEXT) + text_area.selection = Selection(select_from, select_to) + + # Check what text is selected. + expected_selected_text = "DE\nFGHIJ\nK" + assert text_area.selected_text == expected_selected_text + + result = text_area.replace( + "Hello", + start=(0, 0), + end=(0, 2), + ) + + assert result == EditResult(end_location=(0, 5), replaced_text="AB") + + # The edit range has grown from width 2 to width 5, so the + # top line of the selection was adjusted (column+=3) such that the + # same characters are highlighted: + # ... the selection is not changed after programmatic insert + # ... the same text is selected as before. + assert text_area.selected_text == expected_selected_text + + # The resulting text in the TextArea is correct. + assert text_area.text == "HelloCDE\nFGHIJ\nKLMNO\nPQRST\nUVWXY\nZ\n" + + +@pytest.mark.parametrize( + "select_from,select_to", + [ + [(0, 3), (2, 5)], + [(2, 5), (0, 3)], # Ensuring independence from selection direction. + ], +) +async def test_insert_text_multiline_selection_bottom(select_from, select_to): + """ + The edited text is within the selected text on the bottom line + of the selection. The bottom of the selection should be adjusted + such that any text that was previously selected is still selected. + """ + app = TextAreaApp() + async with app.run_test(): + # ABCDE + # FGHIJ + # KLMNO + # PQRST + # UVWXY + # Z + + text_area = app.query_one(TextArea) + text_area.load_text(SIMPLE_TEXT) + text_area.selection = Selection(select_from, select_to) + + # Check what text is selected. + assert text_area.selected_text == "DE\nFGHIJ\nKLMNO" + + result = text_area.replace( + "*", + start=(2, 0), + end=(2, 3), + ) + assert result == EditResult(end_location=(2, 1), replaced_text="KLM") + + # The 'NO' from the selection is still available on the + # bottom selection line, however the 'KLM' is replaced + # with '*'. Since 'NO' is still available, it's maintained + # within the selection. + assert text_area.selected_text == "DE\nFGHIJ\n*NO" + + # The resulting text in the TextArea is correct. + # 'KLM' replaced with '*' + assert text_area.text == "ABCDE\nFGHIJ\n*NO\nPQRST\nUVWXY\nZ\n" + + +async def test_delete_fully_within_selection(): + """User-facing selection should be best-effort adjusted when a programmatic + replacement is made to the document.""" + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("0123456789") + text_area.selection = Selection((0, 2), (0, 7)) + assert text_area.selected_text == "23456" + + result = text_area.delete((0, 4), (0, 6)) + assert result == EditResult( + replaced_text="45", + end_location=(0, 4), + ) + # We deleted 45, but the other characters are still available + assert text_area.selected_text == "236" + assert text_area.text == "01236789" + + +async def test_replace_fully_within_selection(): + """Adjust the selection when a replacement happens inside it.""" + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("0123456789") + text_area.selection = Selection((0, 2), (0, 7)) + assert text_area.selected_text == "23456" + + result = text_area.replace("XX", start=(0, 2), end=(0, 5)) + assert result == EditResult( + replaced_text="234", + end_location=(0, 4), + ) + assert text_area.selected_text == "XX56" diff --git a/tests/text_area/test_edit_via_bindings.py b/tests/text_area/test_edit_via_bindings.py new file mode 100644 index 0000000000..aa99a63ad9 --- /dev/null +++ b/tests/text_area/test_edit_via_bindings.py @@ -0,0 +1,418 @@ +"""Tests some edits using the keyboard. + +All tests in this module should press keys on the keyboard which edit the document, +and check that the document content is updated as expected, as well as the cursor +location. + +Note that more extensive testing for editing is done at the Document level. +""" +import pytest + +from textual.app import App, ComposeResult +from textual.widgets import TextArea +from textual.widgets.text_area import Selection + +TEXT = """I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +""" + +SIMPLE_TEXT = """\ +ABCDE +FGHIJ +KLMNO +PQRST +UVWXY +Z""" + + +class TextAreaApp(App): + def compose(self) -> ComposeResult: + text_area = TextArea() + text_area.load_text(TEXT) + yield text_area + + +async def test_single_keypress_printable_character(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.press("x") + assert text_area.text == "x" + TEXT + + +async def test_single_keypress_enter(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.press("enter") + assert text_area.text == "\n" + TEXT + + +@pytest.mark.parametrize( + "content,cursor_column,cursor_destination", + [ + ("", 0, 4), + ("x", 0, 4), + ("x", 1, 4), + ("xxx", 3, 4), + ("xxxx", 4, 8), + ("xxxxx", 5, 8), + ("xxxxxx", 6, 8), + ("💩", 1, 3), + ("💩💩", 2, 6), + ], +) +async def test_tab_with_spaces_goes_to_tab_stop( + content, cursor_column, cursor_destination +): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.indent_width = 4 + text_area.load_text(content) + text_area.cursor_location = (0, cursor_column) + + await pilot.press("tab") + + assert text_area.cursor_location[1] == cursor_destination + + +async def test_delete_left(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("Hello, world!") + text_area.move_cursor((0, 6)) + await pilot.press("backspace") + assert text_area.text == "Hello world!" + assert text_area.selection == Selection.cursor((0, 5)) + + +async def test_delete_left_start(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("Hello, world!") + await pilot.press("backspace") + assert text_area.text == "Hello, world!" + assert text_area.selection == Selection.cursor((0, 0)) + + +async def test_delete_left_end(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("Hello, world!") + text_area.move_cursor((0, 13)) + await pilot.press("backspace") + assert text_area.text == "Hello, world" + assert text_area.selection == Selection.cursor((0, 12)) + + +@pytest.mark.parametrize( + "key,selection", + [ + ("delete", Selection((1, 2), (3, 4))), + ("delete", Selection((3, 4), (1, 2))), + ("backspace", Selection((1, 2), (3, 4))), + ("backspace", Selection((3, 4), (1, 2))), + ], +) +async def test_deletion_with_non_empty_selection(key, selection): + """When there's a selection, pressing backspace or delete should delete everything + that is selected and reset the selection to a cursor at the appropriate location.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text(SIMPLE_TEXT) + text_area.selection = selection + await pilot.press(key) + assert text_area.selection == Selection.cursor((1, 2)) + assert ( + text_area.text + == """\ +ABCDE +FGT +UVWXY +Z""" + ) + + +async def test_delete_right(): + """Pressing 'delete' deletes the character to the right of the cursor.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("Hello, world!") + text_area.move_cursor((0, 13)) + await pilot.press("delete") + assert text_area.text == "Hello, world!" + assert text_area.selection == Selection.cursor((0, 13)) + + +async def test_delete_right_end_of_line(): + """Pressing 'delete' at the end of the line merges this line with the line below.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("hello\nworld!") + end_of_line = text_area.get_cursor_line_end_location() + text_area.move_cursor(end_of_line) + await pilot.press("delete") + assert text_area.selection == Selection.cursor((0, 5)) + assert text_area.text == "helloworld!" + + +@pytest.mark.parametrize( + "selection,expected_result", + [ + (Selection.cursor((0, 0)), ""), + (Selection.cursor((0, 4)), ""), + (Selection.cursor((0, 10)), ""), + (Selection((0, 2), (0, 4)), ""), + (Selection((0, 4), (0, 2)), ""), + ], +) +async def test_delete_line(selection, expected_result): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("0123456789") + text_area.selection = selection + + await pilot.press("ctrl+x") + + assert text_area.selection == Selection.cursor((0, 0)) + assert text_area.text == expected_result + + +@pytest.mark.parametrize( + "selection,expected_result", + [ + # Cursors + (Selection.cursor((0, 0)), "345\n678\n9\n"), + (Selection.cursor((0, 2)), "345\n678\n9\n"), + (Selection.cursor((3, 1)), "012\n345\n678\n"), + (Selection.cursor((4, 0)), "012\n345\n678\n9\n"), + # Selections + (Selection((1, 1), (1, 2)), "012\n678\n9\n"), # non-empty single line selection + (Selection((1, 2), (2, 1)), "012\n9\n"), # delete lines selection touches + ( + Selection((1, 2), (3, 0)), + "012\n9\n", + ), # cursor at column 0 of line 3, should not be deleted! + ( + Selection((3, 0), (1, 2)), + "012\n9\n", + ), # opposite direction + (Selection((0, 0), (4, 0)), ""), # delete all lines + ], +) +async def test_delete_line_multiline_document(selection, expected_result): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("012\n345\n678\n9\n") + text_area.selection = selection + + await pilot.press("ctrl+x") + + cursor_row, _ = text_area.cursor_location + assert text_area.selection == Selection.cursor((cursor_row, 0)) + assert text_area.text == expected_result + + +@pytest.mark.parametrize( + "selection,expected_result", + [ + # Cursors + (Selection.cursor((0, 0)), ""), + (Selection.cursor((0, 5)), "01234"), + (Selection.cursor((0, 9)), "012345678"), + (Selection.cursor((0, 10)), "0123456789"), + # Selections + (Selection((0, 0), (0, 9)), "012345678"), + (Selection((0, 0), (0, 10)), "0123456789"), + (Selection((0, 2), (0, 5)), "01234"), + (Selection((0, 5), (0, 2)), "01"), + ], +) +async def test_delete_to_end_of_line(selection, expected_result): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("0123456789") + text_area.selection = selection + + await pilot.press("ctrl+k") + + assert text_area.selection == Selection.cursor(selection.end) + assert text_area.text == expected_result + + +@pytest.mark.parametrize( + "selection,expected_result", + [ + # Cursors + (Selection.cursor((0, 0)), "0123456789"), + (Selection.cursor((0, 5)), "56789"), + (Selection.cursor((0, 9)), "9"), + (Selection.cursor((0, 10)), ""), + # Selections + (Selection((0, 0), (0, 9)), "9"), + (Selection((0, 0), (0, 10)), ""), + (Selection((0, 2), (0, 5)), "56789"), + (Selection((0, 5), (0, 2)), "23456789"), + ], +) +async def test_delete_to_start_of_line(selection, expected_result): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("0123456789") + text_area.selection = selection + + await pilot.press("ctrl+u") + + assert text_area.selection == Selection.cursor((0, 0)) + assert text_area.text == expected_result + + +@pytest.mark.parametrize( + "selection,expected_result,final_selection", + [ + (Selection.cursor((0, 0)), " 012 345 6789", Selection.cursor((0, 0))), + (Selection.cursor((0, 4)), " 2 345 6789", Selection.cursor((0, 2))), + (Selection.cursor((0, 5)), " 345 6789", Selection.cursor((0, 2))), + ( + Selection.cursor((0, 6)), + " 345 6789", + Selection.cursor((0, 2)), + ), + (Selection.cursor((0, 14)), " 012 345 ", Selection.cursor((0, 10))), + # When there's a selection and you "delete word left", it just deletes the selection + (Selection((0, 4), (0, 11)), " 01789", Selection.cursor((0, 4))), + ], +) +async def test_delete_word_left(selection, expected_result, final_selection): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text(" 012 345 6789") + text_area.selection = selection + + await pilot.press("ctrl+w") + + assert text_area.text == expected_result + assert text_area.selection == final_selection + + +@pytest.mark.parametrize( + "selection,expected_result,final_selection", + [ + (Selection.cursor((0, 0)), "\t012 \t 345\t6789", Selection.cursor((0, 0))), + (Selection.cursor((0, 4)), "\t \t 345\t6789", Selection.cursor((0, 1))), + (Selection.cursor((0, 5)), "\t\t 345\t6789", Selection.cursor((0, 1))), + ( + Selection.cursor((0, 6)), + "\t 345\t6789", + Selection.cursor((0, 1)), + ), + (Selection.cursor((0, 15)), "\t012 \t 345\t", Selection.cursor((0, 11))), + # When there's a selection and you "delete word left", it just deletes the selection + (Selection((0, 4), (0, 11)), "\t0126789", Selection.cursor((0, 4))), + ], +) +async def test_delete_word_left_with_tabs(selection, expected_result, final_selection): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("\t012 \t 345\t6789") + text_area.selection = selection + + await pilot.press("ctrl+w") + + assert text_area.text == expected_result + assert text_area.selection == final_selection + + +async def test_delete_word_left_to_start_of_line(): + """If no word boundary found when we 'delete word left', then + the deletion happens to the start of the line.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("0123\n 456789") + text_area.selection = Selection.cursor((1, 3)) + + await pilot.press("ctrl+w") + + assert text_area.text == "0123\n456789" + assert text_area.selection == Selection.cursor((1, 0)) + + +async def test_delete_word_left_at_line_start(): + """If we're at the start of a line and we 'delete word left', the + line merges with the line above (if possible).""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("0123\n 456789") + text_area.selection = Selection.cursor((1, 0)) + + await pilot.press("ctrl+w") + + assert text_area.text == "0123 456789" + assert text_area.selection == Selection.cursor((0, 4)) + + +@pytest.mark.parametrize( + "selection,expected_result,final_selection", + [ + (Selection.cursor((0, 0)), "012 345 6789", Selection.cursor((0, 0))), + (Selection.cursor((0, 4)), " 01 345 6789", Selection.cursor((0, 4))), + (Selection.cursor((0, 5)), " 012345 6789", Selection.cursor((0, 5))), + (Selection.cursor((0, 14)), " 012 345 6789", Selection.cursor((0, 14))), + # When non-empty selection, "delete word right" just deletes the selection + (Selection((0, 4), (0, 11)), " 01789", Selection.cursor((0, 4))), + ], +) +async def test_delete_word_right(selection, expected_result, final_selection): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text(" 012 345 6789") + text_area.selection = selection + + await pilot.press("ctrl+f") + + assert text_area.text == expected_result + assert text_area.selection == final_selection + + +async def test_delete_word_right_delete_to_end_of_line(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("01234\n56789") + text_area.selection = Selection.cursor((0, 3)) + + await pilot.press("ctrl+f") + + assert text_area.text == "012\n56789" + assert text_area.selection == Selection.cursor((0, 3)) + + +async def test_delete_word_right_at_end_of_line(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("01234\n56789") + text_area.selection = Selection.cursor((0, 5)) + + await pilot.press("ctrl+f") + + assert text_area.text == "0123456789" + assert text_area.selection == Selection.cursor((0, 5)) diff --git a/tests/text_area/test_languages.py b/tests/text_area/test_languages.py new file mode 100644 index 0000000000..6124da0cbd --- /dev/null +++ b/tests/text_area/test_languages.py @@ -0,0 +1,100 @@ +import sys + +import pytest + +from textual.app import App, ComposeResult +from textual.widgets import TextArea +from textual.widgets.text_area import LanguageDoesNotExist + + +class TextAreaApp(App): + def compose(self) -> ComposeResult: + yield TextArea("print('hello')", language="python") + + +async def test_setting_builtin_language_via_constructor(): + class MyTextAreaApp(App): + def compose(self) -> ComposeResult: + yield TextArea("print('hello')", language="python") + + app = MyTextAreaApp() + + async with app.run_test(): + text_area = app.query_one(TextArea) + assert text_area.language == "python" + + text_area.language = "markdown" + assert text_area.language == "markdown" + + +async def test_setting_builtin_language_via_attribute(): + class MyTextAreaApp(App): + def compose(self) -> ComposeResult: + text_area = TextArea("print('hello')") + text_area.language = "python" + yield text_area + + app = MyTextAreaApp() + + async with app.run_test(): + text_area = app.query_one(TextArea) + assert text_area.language == "python" + + text_area.language = "markdown" + assert text_area.language == "markdown" + + +async def test_setting_language_to_none(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.language = None + assert text_area.language is None + + +async def test_setting_unknown_language(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + + with pytest.raises(LanguageDoesNotExist): + text_area.language = "this-language-doesnt-exist" + + +@pytest.mark.skipif(sys.version_info < (3, 8), reason="tree-sitter requires python3.8 or higher") +async def test_register_language(): + app = TextAreaApp() + + async with app.run_test(): + text_area = app.query_one(TextArea) + + from tree_sitter_languages import get_language + + language = get_language("elm") + + # ...and register it with no highlights + text_area.register_language(language, "") + + # Ensure that registered language is now available. + assert "elm" in text_area.available_languages + + # Switch to the newly registered language + text_area.language = "elm" + + assert text_area.language == "elm" + + +@pytest.mark.skipif(sys.version_info < (3, 8), reason="tree-sitter requires python3.8 or higher") +async def test_register_language_existing_language(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + + # Before registering the language, we have highlights as expected. + assert len(text_area._highlights) > 0 + + # Overwriting the highlight query for Python... + text_area.register_language("python", "") + + # We've overridden the highlight query with a blank one, so there are no highlights. + assert text_area._highlights == {} diff --git a/tests/text_area/test_messages.py b/tests/text_area/test_messages.py new file mode 100644 index 0000000000..c6ddbe5a4d --- /dev/null +++ b/tests/text_area/test_messages.py @@ -0,0 +1,91 @@ +from typing import List + +from textual import on +from textual.app import App, ComposeResult +from textual.events import Event +from textual.message import Message +from textual.widgets import TextArea + + +class TextAreaApp(App): + def __init__(self): + super().__init__() + self.messages = [] + + @on(TextArea.Changed) + @on(TextArea.SelectionChanged) + def message_received(self, message: Message): + self.messages.append(message) + + def compose(self) -> ComposeResult: + yield TextArea("123") + + +def get_changed_messages(messages: List[Event]) -> List[TextArea.Changed]: + return [message for message in messages if isinstance(message, TextArea.Changed)] + + +def get_selection_changed_messages( + messages: List[Event], +) -> List[TextArea.SelectionChanged]: + return [ + message + for message in messages + if isinstance(message, TextArea.SelectionChanged) + ] + + +async def test_changed_message_edit_via_api(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + assert get_changed_messages(app.messages) == [] + + text_area.insert("A") + await pilot.pause() + + assert get_changed_messages(app.messages) == [TextArea.Changed(text_area)] + assert get_selection_changed_messages(app.messages) == [ + TextArea.SelectionChanged(text_area.selection, text_area) + ] + + +async def test_changed_message_via_typing(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + assert get_changed_messages(app.messages) == [] + + await pilot.press("a") + + assert get_changed_messages(app.messages) == [TextArea.Changed(text_area)] + assert get_selection_changed_messages(app.messages) == [ + TextArea.SelectionChanged(text_area.selection, text_area) + ] + + +async def test_selection_changed_via_api(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + assert get_selection_changed_messages(app.messages) == [] + + text_area.cursor_location = (0, 1) + await pilot.pause() + + assert get_selection_changed_messages(app.messages) == [ + TextArea.SelectionChanged(text_area.selection, text_area) + ] + + +async def test_selection_changed_via_typing(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + assert get_selection_changed_messages(app.messages) == [] + + await pilot.press("a") + + assert get_selection_changed_messages(app.messages) == [ + TextArea.SelectionChanged(text_area.selection, text_area) + ] diff --git a/tests/text_area/test_selection.py b/tests/text_area/test_selection.py new file mode 100644 index 0000000000..bbc70e476e --- /dev/null +++ b/tests/text_area/test_selection.py @@ -0,0 +1,336 @@ +import pytest + +from textual.app import App, ComposeResult +from textual.widgets import TextArea +from textual.widgets.text_area import Selection + +TEXT = """I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +""" + + +class TextAreaApp(App): + def compose(self) -> ComposeResult: + text_area = TextArea() + text_area.load_text(TEXT) + yield text_area + + +async def test_default_selection(): + """The cursor starts at (0, 0) in the document.""" + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + assert text_area.selection == Selection.cursor((0, 0)) + + +async def test_cursor_location_get(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.selection = Selection((1, 1), (2, 2)) + assert text_area.cursor_location == (2, 2) + + +async def test_cursor_location_set(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + target = (1, 2) + text_area.cursor_location = target + assert text_area.selection == Selection.cursor(target) + + +async def test_cursor_location_set_while_selecting(): + """If you set the cursor_location while a selection is in progress, + the start/anchor point of the selection will remain where it is.""" + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.selection = Selection((0, 0), (0, 2)) + target = (1, 2) + text_area.cursor_location = target + assert text_area.selection == Selection((0, 0), target) + + +async def test_move_cursor_select(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.selection = Selection((1, 1), (2, 2)) + text_area.move_cursor((2, 3), select=True) + assert text_area.selection == Selection((1, 1), (2, 3)) + + +async def test_move_cursor_relative(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + + text_area.move_cursor_relative(rows=1, columns=2) + assert text_area.selection == Selection.cursor((1, 2)) + + text_area.move_cursor_relative(rows=-1, columns=-2) + assert text_area.selection == Selection.cursor((0, 0)) + + text_area.move_cursor_relative(rows=1000, columns=1000) + assert text_area.selection == Selection.cursor((4, 0)) + + +async def test_selected_text_forward(): + """Selecting text from top to bottom results in the correct selected_text.""" + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.selection = Selection((0, 0), (2, 0)) + assert ( + text_area.selected_text + == """\ +I must not fear. +Fear is the mind-killer. +""" + ) + + +async def test_selected_text_backward(): + """Selecting text from bottom to top results in the correct selected_text.""" + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.selection = Selection((2, 0), (0, 0)) + assert ( + text_area.selected_text + == """\ +I must not fear. +Fear is the mind-killer. +""" + ) + + +async def test_selected_text_multibyte(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("こんにちは") + text_area.selection = Selection((0, 1), (0, 3)) + assert text_area.selected_text == "んに" + + +async def test_selection_clamp(): + """When you set the selection reactive, it's clamped to within the document bounds.""" + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.selection = Selection((99, 99), (100, 100)) + assert text_area.selection == Selection(start=(4, 0), end=(4, 0)) + + +@pytest.mark.parametrize( + "start,end", + [ + ((0, 0), (0, 0)), + ((0, 4), (0, 3)), + ((1, 0), (0, 16)), + ], +) +async def test_get_cursor_left_location(start, end): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.move_cursor(start) + assert text_area.get_cursor_left_location() == end + + +@pytest.mark.parametrize( + "start,end", + [ + ((0, 0), (0, 1)), + ((0, 16), (1, 0)), + ((3, 20), (4, 0)), + ((4, 0), (4, 0)), + ], +) +async def test_get_cursor_right_location(start, end): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.move_cursor(start) + assert text_area.get_cursor_right_location() == end + + +@pytest.mark.parametrize( + "start,end", + [ + ((0, 4), (0, 0)), # jump to start + ((1, 2), (0, 2)), # go to column above + ((2, 56), (1, 24)), # snap to end of row above + ], +) +async def test_get_cursor_up_location(start, end): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.move_cursor(start) + # This is required otherwise the cursor will snap back to the + # last location navigated to (0, 0) + text_area.record_cursor_width() + assert text_area.get_cursor_up_location() == end + + +@pytest.mark.parametrize( + "start,end", + [ + ((3, 4), (4, 0)), # jump to end + ((1, 2), (2, 2)), # go to column above + ((2, 56), (3, 20)), # snap to end of row below + ], +) +async def test_get_cursor_down_location(start, end): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.move_cursor(start) + # This is required otherwise the cursor will snap back to the + # last location navigated to (0, 0) + text_area.record_cursor_width() + assert text_area.get_cursor_down_location() == end + + +@pytest.mark.parametrize( + "start,end", + [ + ((0, 0), (0, 0)), + ((0, 1), (0, 0)), + ((0, 2), (0, 0)), + ((0, 3), (0, 0)), + ((0, 4), (0, 3)), + ((0, 5), (0, 3)), + ((0, 6), (0, 3)), + ((0, 7), (0, 3)), + ((0, 10), (0, 7)), + ((1, 0), (0, 10)), + ((1, 2), (1, 0)), + ((1, 4), (1, 0)), + ((1, 7), (1, 4)), + ((1, 8), (1, 7)), + ((1, 13), (1, 11)), + ((1, 14), (1, 11)), + ], +) +async def test_cursor_word_left_location(start, end): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("AB CD EFG\n HI\tJK LM ") + text_area.move_cursor(start) + assert text_area.get_cursor_word_left_location() == end + + +@pytest.mark.parametrize( + "start,end", + [ + ((0, 0), (0, 2)), + ((0, 1), (0, 2)), + ((0, 2), (0, 5)), + ((0, 3), (0, 5)), + ((0, 4), (0, 5)), + ((0, 5), (0, 10)), + ((0, 6), (0, 10)), + ((0, 7), (0, 10)), + ((0, 10), (1, 0)), + ((1, 0), (1, 6)), + ((1, 2), (1, 6)), + ((1, 4), (1, 6)), + ((1, 7), (1, 9)), + ((1, 8), (1, 9)), + ((1, 13), (1, 14)), + ((1, 14), (1, 14)), + ], +) +async def test_cursor_word_right_location(start, end): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text("AB CD EFG\n HI\tJK LM ") + text_area.move_cursor(start) + assert text_area.get_cursor_word_right_location() == end + + +@pytest.mark.parametrize( + "content,expected_selection", + [ + ("123\n456\n789", Selection((0, 0), (2, 3))), + ("123\n456\n789\n", Selection((0, 0), (3, 0))), + ("", Selection((0, 0), (0, 0))), + ], +) +async def test_select_all(content, expected_selection): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text(content) + + text_area.select_all() + + assert text_area.selection == expected_selection + + +@pytest.mark.parametrize( + "index,content,expected_selection", + [ + (1, "123\n456\n789\n", Selection((1, 0), (1, 3))), + (2, "123\n456\n789\n", Selection((2, 0), (2, 3))), + (3, "123\n456\n789\n", Selection((3, 0), (3, 0))), + (1000, "123\n456\n789\n", Selection.cursor((0, 0))), + (0, "", Selection((0, 0), (0, 0))), + ], +) +async def test_select_line(index, content, expected_selection): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.load_text(content) + + text_area.select_line(index) + + assert text_area.selection == expected_selection + + +async def test_cursor_screen_offset_and_terminal_cursor_position_update(): + class TextAreaCursorScreenOffset(App): + def compose(self) -> ComposeResult: + yield TextArea("abc\ndef") + + app = TextAreaCursorScreenOffset() + async with app.run_test(): + text_area = app.query_one(TextArea) + + assert app.cursor_position == (3, 0) + + text_area.cursor_location = (1, 1) + + assert text_area.cursor_screen_offset == (4, 1) + + # Also ensure that this update has been reported back to the app + # for the benefit of IME/emoji popups. + assert app.cursor_position == (4, 1) + + +async def test_cursor_screen_offset_and_terminal_cursor_position_scrolling(): + class TextAreaCursorScreenOffset(App): + def compose(self) -> ComposeResult: + yield TextArea("AB\nAB\nAB\nAB\nAB\nAB\n") + + app = TextAreaCursorScreenOffset() + async with app.run_test(size=(80, 2)) as pilot: + text_area = app.query_one(TextArea) + + assert app.cursor_position == (3, 0) + + text_area.cursor_location = (5, 0) + await pilot.pause() + + assert text_area.cursor_screen_offset == (3, 1) + assert app.cursor_position == (3, 1) diff --git a/tests/text_area/test_selection_bindings.py b/tests/text_area/test_selection_bindings.py new file mode 100644 index 0000000000..76d4586df4 --- /dev/null +++ b/tests/text_area/test_selection_bindings.py @@ -0,0 +1,318 @@ +import pytest + +from textual.app import App, ComposeResult +from textual.geometry import Offset +from textual.widgets import TextArea +from textual.widgets.text_area import Document, Selection + +TEXT = """I must not fear. +Fear is the mind-killer. +Fear is the little-death that brings total obliteration. +I will face my fear. +""" + + +class TextAreaApp(App): + def compose(self) -> ComposeResult: + text_area = TextArea() + text_area.load_text(TEXT) + yield text_area + + +async def test_mouse_click(): + """When you click the TextArea, the cursor moves to the expected location.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.click(TextArea, Offset(x=5, y=2)) + assert text_area.selection == Selection.cursor((2, 2)) + + +async def test_mouse_click_clamp_from_right(): + """When you click to the right of the document bounds, the cursor is clamped + to within the document bounds.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.click(TextArea, Offset(x=8, y=20)) + assert text_area.selection == Selection.cursor((4, 0)) + + +async def test_mouse_click_gutter_clamp(): + """When you click the gutter, it selects the start of the line.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.click(TextArea, Offset(x=0, y=3)) + assert text_area.selection == Selection.cursor((3, 0)) + + +async def test_cursor_movement_basic(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("01234567\n012345\n0123456789") + + await pilot.press("right") + assert text_area.selection == Selection.cursor((0, 1)) + + await pilot.press("down") + assert text_area.selection == Selection.cursor((1, 1)) + + await pilot.press("left") + assert text_area.selection == Selection.cursor((1, 0)) + + await pilot.press("up") + assert text_area.selection == Selection.cursor((0, 0)) + + +async def test_cursor_selection_right(): + """When you press shift+right the selection is updated correctly.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + await pilot.press(*["shift+right"] * 3) + assert text_area.selection == Selection((0, 0), (0, 3)) + + +async def test_cursor_selection_right_to_previous_line(): + """When you press shift+right resulting in the cursor moving to the next line, + the selection is updated correctly.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.selection = Selection.cursor((0, 15)) + await pilot.press(*["shift+right"] * 4) + assert text_area.selection == Selection((0, 15), (1, 2)) + + +async def test_cursor_selection_left(): + """When you press shift+left the selection is updated correctly.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.selection = Selection.cursor((2, 5)) + await pilot.press(*["shift+left"] * 3) + assert text_area.selection == Selection((2, 5), (2, 2)) + + +async def test_cursor_selection_left_to_previous_line(): + """When you press shift+left resulting in the cursor moving back to the previous line, + the selection is updated correctly.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.selection = Selection.cursor((2, 2)) + await pilot.press(*["shift+left"] * 3) + + # The cursor jumps up to the end of the line above. + end_of_previous_line = len(TEXT.splitlines()[1]) + assert text_area.selection == Selection((2, 2), (1, end_of_previous_line)) + + +async def test_cursor_selection_up(): + """When you press shift+up the selection is updated correctly.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.move_cursor((2, 3)) + + await pilot.press("shift+up") + assert text_area.selection == Selection((2, 3), (1, 3)) + + +async def test_cursor_selection_up_when_cursor_on_first_line(): + """When you press shift+up the on the first line, it selects to the start.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.move_cursor((0, 4)) + + await pilot.press("shift+up") + assert text_area.selection == Selection((0, 4), (0, 0)) + await pilot.press("shift+up") + assert text_area.selection == Selection((0, 4), (0, 0)) + + +async def test_cursor_selection_down(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.move_cursor((2, 5)) + + await pilot.press("shift+down") + assert text_area.selection == Selection((2, 5), (3, 5)) + + +async def test_cursor_selection_down_when_cursor_on_last_line(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("ABCDEF\nGHIJK") + text_area.move_cursor((1, 2)) + + await pilot.press("shift+down") + assert text_area.selection == Selection((1, 2), (1, 5)) + await pilot.press("shift+down") + assert text_area.selection == Selection((1, 2), (1, 5)) + + +async def test_cursor_word_right(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("ABC DEF\nGHIJK") + + await pilot.press("ctrl+right") + + assert text_area.selection == Selection.cursor((0, 3)) + + +async def test_cursor_word_right_select(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("ABC DEF\nGHIJK") + + await pilot.press("ctrl+shift+right") + + assert text_area.selection == Selection((0, 0), (0, 3)) + + +async def test_cursor_word_left(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("ABC DEF\nGHIJK") + text_area.move_cursor((0, 7)) + + await pilot.press("ctrl+left") + + assert text_area.selection == Selection.cursor((0, 4)) + + +async def test_cursor_word_left_select(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("ABC DEF\nGHIJK") + text_area.move_cursor((0, 7)) + + await pilot.press("ctrl+shift+left") + + assert text_area.selection == Selection((0, 7), (0, 4)) + + +@pytest.mark.parametrize("key", ["end", "ctrl+e"]) +async def test_cursor_to_line_end(key): + """You can use the keyboard to jump the cursor to the end of the current line.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.selection = Selection.cursor((2, 2)) + await pilot.press(key) + eol_index = len(TEXT.splitlines()[2]) + assert text_area.cursor_location == (2, eol_index) + assert text_area.selection.is_empty + + +@pytest.mark.parametrize("key", ["home", "ctrl+a"]) +async def test_cursor_to_line_home_basic_behaviour(key): + """You can use the keyboard to jump the cursor to the start of the current line.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.selection = Selection.cursor((2, 2)) + await pilot.press(key) + assert text_area.cursor_location == (2, 0) + assert text_area.selection.is_empty + + +@pytest.mark.parametrize( + "cursor_start,cursor_destination", + [ + ((0, 0), (0, 4)), + ((0, 2), (0, 0)), + ((0, 4), (0, 0)), + ((0, 5), (0, 4)), + ((0, 9), (0, 4)), + ((0, 15), (0, 4)), + ], +) +async def test_cursor_line_home_smart_home(cursor_start, cursor_destination): + """If the line begins with whitespace, pressing home firstly goes + to the start of the (non-whitespace) content. Pressing it again takes you to column + 0. If you press it again, it goes back to the first non-whitespace column.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text(" hello world") + text_area.move_cursor(cursor_start) + await pilot.press("home") + assert text_area.selection == Selection.cursor(cursor_destination) + + +async def test_cursor_page_down(): + """Pagedown moves the cursor down 1 page, retaining column index.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("XXX\n" * 200) + text_area.selection = Selection.cursor((0, 1)) + await pilot.press("pagedown") + assert text_area.selection == Selection.cursor((app.console.height - 1, 1)) + + +async def test_cursor_page_up(): + """Pageup moves the cursor up 1 page, retaining column index.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_text("XXX\n" * 200) + text_area.selection = Selection.cursor((100, 1)) + await pilot.press("pageup") + assert text_area.selection == Selection.cursor( + (100 - app.console.height + 1, 1) + ) + + +async def test_cursor_vertical_movement_visual_alignment_snapping(): + """When you move the cursor vertically, it should stay vertically + aligned even when double-width characters are used.""" + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.load_document(Document("こんにちは\n012345")) + text_area.move_cursor((1, 3), record_width=True) + + # The '3' is aligned with ん at (0, 1) + # こんにちは + # 012345 + # Pressing `up` takes us from (1, 3) to (0, 1) because record_width=True. + await pilot.press("up") + assert text_area.selection == Selection.cursor((0, 1)) + + # Pressing `down` takes us from (0, 1) to (1, 3) + await pilot.press("down") + assert text_area.selection == Selection.cursor((1, 3)) + + +async def test_select_line_binding(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + text_area.move_cursor((2, 2)) + + await pilot.press("f6") + + assert text_area.selection == Selection((2, 0), (2, 56)) + + +async def test_select_all_binding(): + app = TextAreaApp() + async with app.run_test() as pilot: + text_area = app.query_one(TextArea) + + await pilot.press("f7") + + assert text_area.selection == Selection((0, 0), (4, 0)) diff --git a/tests/text_area/test_setting_themes.py b/tests/text_area/test_setting_themes.py new file mode 100644 index 0000000000..8d165a98a9 --- /dev/null +++ b/tests/text_area/test_setting_themes.py @@ -0,0 +1,67 @@ +import pytest + +from textual._text_area_theme import TextAreaTheme +from textual.app import App, ComposeResult +from textual.widgets import TextArea +from textual.widgets._text_area import ThemeDoesNotExist + + +class TextAreaApp(App): + def compose(self) -> ComposeResult: + yield TextArea("print('hello')", language="python") + + +async def test_default_theme(): + app = TextAreaApp() + + async with app.run_test(): + text_area = app.query_one(TextArea) + assert text_area.theme is None + + +async def test_setting_builtin_themes(): + class MyTextAreaApp(App): + def compose(self) -> ComposeResult: + yield TextArea("print('hello')", language="python", theme="vscode_dark") + + app = MyTextAreaApp() + + async with app.run_test(): + text_area = app.query_one(TextArea) + assert text_area.theme == "vscode_dark" + + text_area.theme = "monokai" + assert text_area.theme == "monokai" + + +async def test_setting_theme_to_none(): + app = TextAreaApp() + + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.theme = None + assert text_area.theme is None + # When theme is None, we use the default theme. + assert text_area._theme.name == TextAreaTheme.default().name + + +async def test_setting_unknown_theme_raises_exception(): + app = TextAreaApp() + async with app.run_test(): + text_area = app.query_one(TextArea) + with pytest.raises(ThemeDoesNotExist): + text_area.theme = "this-theme-doesnt-exist" + + +async def test_registering_and_setting_theme(): + app = TextAreaApp() + + async with app.run_test(): + text_area = app.query_one(TextArea) + text_area.register_theme(TextAreaTheme("my-theme")) + + assert "my-theme" in text_area.available_themes + + text_area.theme = "my-theme" + + assert text_area.theme == "my-theme" diff --git a/tests/tree/test_directory_tree_reload_node.py b/tests/tree/test_directory_tree.py similarity index 60% rename from tests/tree/test_directory_tree_reload_node.py rename to tests/tree/test_directory_tree.py index 07ea048f7e..c56bf48d52 100644 --- a/tests/tree/test_directory_tree_reload_node.py +++ b/tests/tree/test_directory_tree.py @@ -1,19 +1,83 @@ -from rich.text import Text +from __future__ import annotations +from rich.text import Text +from textual import on from textual.app import App, ComposeResult from textual.widgets import DirectoryTree -class DirectoryTreeReloadApp(App[None]): - """DirectoryTree reloading test app.""" +class DirectoryTreeApp(App[None]): + """DirectoryTree test app.""" def __init__(self, path): super().__init__() self._tmp_path = path + self.messages = [] def compose(self) -> ComposeResult: yield DirectoryTree(self._tmp_path) + @on(DirectoryTree.FileSelected) + @on(DirectoryTree.DirectorySelected) + def record( + self, event: DirectoryTree.FileSelected | DirectoryTree.DirectorySelected + ) -> None: + self.messages.append(event.__class__.__name__) + + +async def test_directory_tree_file_selected_message(tmp_path) -> None: + """Selecting a file should result in a file selected message being emitted.""" + + FILE_NAME = "hello.txt" + + # Creating one file under root + file = tmp_path / FILE_NAME + file.touch() + async with DirectoryTreeApp(tmp_path).run_test() as pilot: + tree = pilot.app.query_one(DirectoryTree) + await pilot.pause() + + # Sanity check - file is the only child of root + assert len(tree.root.children) == 1 + node = tree.root.children[0] + assert node.label == Text(FILE_NAME) + + # Navigate to the file and select it + await pilot.press("down", "enter") + await pilot.pause() + assert pilot.app.messages == ["FileSelected"] + + +async def test_directory_tree_directory_selected_message(tmp_path) -> None: + """Selecting a directory should result in a directory selected message being emitted.""" + + SUBDIR = "subdir" + FILE_NAME = "hello.txt" + + # Creating node with one file as its child + subdir = tmp_path / SUBDIR + subdir.mkdir() + file = subdir / FILE_NAME + file.touch() + async with DirectoryTreeApp(tmp_path).run_test() as pilot: + tree = pilot.app.query_one(DirectoryTree) + await pilot.pause() + + # Sanity check - subdirectory is the only child of root + assert len(tree.root.children) == 1 + node = tree.root.children[0] + assert node.label == Text(SUBDIR) + + # Navigate to the subdirectory and select it + await pilot.press("down", "enter") + await pilot.pause() + assert pilot.app.messages == ["DirectorySelected"] + + # Select the subdirectory again + await pilot.press("enter") + await pilot.pause() + assert pilot.app.messages == ["DirectorySelected", "DirectorySelected"] + async def test_directory_tree_reload_node(tmp_path) -> None: """Reloading a node of a directory tree should display newly created file inside the directory.""" @@ -28,7 +92,7 @@ async def test_directory_tree_reload_node(tmp_path) -> None: file1 = reloaded_dir / FILE1_NAME file1.touch() - async with DirectoryTreeReloadApp(tmp_path).run_test() as pilot: + async with DirectoryTreeApp(tmp_path).run_test() as pilot: tree = pilot.app.query_one(DirectoryTree) await pilot.pause() @@ -78,7 +142,7 @@ async def test_directory_tree_reload_other_node(tmp_path) -> None: file3 = non_reloaded_dir / NOT_RELOADED_FILE3_NAME file3.touch() - async with DirectoryTreeReloadApp(tmp_path).run_test() as pilot: + async with DirectoryTreeApp(tmp_path).run_test() as pilot: tree = pilot.app.query_one(DirectoryTree) await pilot.pause() diff --git a/tests/tree/test_tree_availability.py b/tests/tree/test_tree_availability.py new file mode 100644 index 0000000000..c3f509446e --- /dev/null +++ b/tests/tree/test_tree_availability.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from typing import Any + +from textual import on +from textual.app import App, ComposeResult +from textual.widgets import Tree + + +class TreeApp(App[None]): + """Test tree app.""" + + def __init__(self, disabled: bool, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.disabled = disabled + self.messages: list[tuple[str, str]] = [] + + def compose(self) -> ComposeResult: + """Compose the child widgets.""" + yield Tree("Root", disabled=self.disabled, id="test-tree") + + def on_mount(self) -> None: + self.query_one(Tree).root.add("Child") + self.query_one(Tree).focus() + + def record( + self, + event: Tree.NodeSelected[None] + | Tree.NodeExpanded[None] + | Tree.NodeCollapsed[None] + | Tree.NodeHighlighted[None], + ) -> None: + self.messages.append( + (event.__class__.__name__, event.node.tree.id or "Unknown") + ) + + @on(Tree.NodeSelected) + def node_selected(self, event: Tree.NodeSelected[None]) -> None: + self.record(event) + + @on(Tree.NodeExpanded) + def node_expanded(self, event: Tree.NodeExpanded[None]) -> None: + self.record(event) + + @on(Tree.NodeCollapsed) + def node_collapsed(self, event: Tree.NodeCollapsed[None]) -> None: + self.record(event) + + @on(Tree.NodeHighlighted) + def node_highlighted(self, event: Tree.NodeHighlighted[None]) -> None: + self.record(event) + + +async def test_creating_disabled_tree(): + """Mounting a disabled `Tree` should result in the base `Widget` + having a `disabled` property equal to `True`""" + app = TreeApp(disabled=True) + async with app.run_test() as pilot: + tree = app.query_one(Tree) + assert not tree.focusable + assert tree.disabled + assert tree.cursor_line == 0 + await pilot.click("#test-tree") + await pilot.pause() + await pilot.press("down") + await pilot.pause() + assert tree.cursor_line == 0 + + +async def test_creating_enabled_tree(): + """Mounting an enabled `Tree` should result in the base `Widget` + having a `disabled` property equal to `False`""" + app = TreeApp(disabled=False) + async with app.run_test() as pilot: + tree = app.query_one(Tree) + assert tree.focusable + assert not tree.disabled + assert tree.cursor_line == 0 + await pilot.click("#test-tree") + await pilot.pause() + await pilot.press("down") + await pilot.pause() + assert tree.cursor_line == 1 + + +async def test_disabled_tree_node_selected_message() -> None: + """Clicking the root node disclosure triangle on a disabled tree + should result in no messages being emitted.""" + app = TreeApp(disabled=True) + async with app.run_test() as pilot: + tree = app.query_one(Tree) + # try clicking on a disabled tree + await pilot.click("#test-tree") + await pilot.pause() + assert not pilot.app.messages + # make sure messages DO flow after enabling a disabled tree + tree.disabled = False + await pilot.click("#test-tree") + await pilot.pause() + assert pilot.app.messages == [("NodeExpanded", "test-tree")] + + +async def test_enabled_tree_node_selected_message() -> None: + """Clicking the root node disclosure triangle on an enabled tree + should result in an `NodeExpanded` message being emitted.""" + app = TreeApp(disabled=False) + async with app.run_test() as pilot: + tree = app.query_one(Tree) + # try clicking on an enabled tree + await pilot.click("#test-tree") + await pilot.pause() + assert pilot.app.messages == [("NodeExpanded", "test-tree")] + tree.disabled = True + # make sure messages DO NOT flow after disabling an enabled tree + app.messages = [] + await pilot.click("#test-tree") + await pilot.pause() + assert not pilot.app.messages