ruff-sync 0.1.5.dev1__tar.gz → 0.1.5.dev3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ruff_sync-0.1.5.dev3/.agents/DEPENDENCIES.md +47 -0
- ruff_sync-0.1.5.dev3/.agents/skills/mike/SKILL.md +150 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/mkdocs-generation/templates/mkdocs.yml +4 -1
- ruff_sync-0.1.5.dev3/.agents/skills/textual/SKILL.md +52 -0
- ruff_sync-0.1.5.dev3/.agents/skills/textual/examples/basic_app.py +62 -0
- ruff_sync-0.1.5.dev3/.agents/skills/textual/examples/reactive_example.py +51 -0
- ruff_sync-0.1.5.dev3/.agents/skills/textual/references/events.md +84 -0
- ruff_sync-0.1.5.dev3/.agents/skills/textual/references/styling.md +54 -0
- ruff_sync-0.1.5.dev3/.agents/skills/textual/references/testing.md +58 -0
- ruff_sync-0.1.5.dev3/.agents/skills/textual/references/widgets.md +85 -0
- ruff_sync-0.1.5.dev3/.agents/tui_design.md +149 -0
- ruff_sync-0.1.5.dev3/.agents/tui_requirements.md +69 -0
- ruff_sync-0.1.5.dev3/.agents/tui_rule_browsing.md +44 -0
- ruff_sync-0.1.5.dev3/.agents/tui_rule_browsing_design.md +66 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.github/workflows/ci.yaml +24 -1
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/PKG-INFO +3 -1
- ruff_sync-0.1.5.dev3/docs/overrides/main.html +6 -0
- ruff_sync-0.1.5.dev3/docs/overrides/partials/version_warning.html +27 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/mkdocs.yml +5 -3
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/pyproject.toml +6 -1
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/src/ruff_sync/__init__.py +10 -2
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/src/ruff_sync/cli.py +36 -0
- ruff_sync-0.1.5.dev3/src/ruff_sync/config_io.py +136 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/src/ruff_sync/core.py +1 -73
- ruff_sync-0.1.5.dev3/src/ruff_sync/dependencies.py +64 -0
- ruff_sync-0.1.5.dev3/src/ruff_sync/system.py +176 -0
- ruff_sync-0.1.5.dev3/src/ruff_sync/tui/__init__.py +26 -0
- ruff_sync-0.1.5.dev3/src/ruff_sync/tui/app.py +304 -0
- ruff_sync-0.1.5.dev3/src/ruff_sync/tui/constants.py +9 -0
- ruff_sync-0.1.5.dev3/src/ruff_sync/tui/screens.py +185 -0
- ruff_sync-0.1.5.dev3/src/ruff_sync/tui/themes.py +21 -0
- ruff_sync-0.1.5.dev3/src/ruff_sync/tui/widgets.py +293 -0
- ruff_sync-0.1.5.dev3/tests/conftest.py +123 -0
- ruff_sync-0.1.5.dev3/tests/test_config_io.py +153 -0
- ruff_sync-0.1.5.dev3/tests/test_dependencies.py +59 -0
- ruff_sync-0.1.5.dev3/tests/test_minimal_imports.sh +54 -0
- ruff_sync-0.1.5.dev3/tests/test_rule_logic.py +99 -0
- ruff_sync-0.1.5.dev3/tests/test_system.py +54 -0
- ruff_sync-0.1.5.dev3/tests/test_themes.py +44 -0
- ruff_sync-0.1.5.dev3/tests/test_tui.py +299 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/uv.lock +99 -1
- ruff_sync-0.1.5.dev1/.agents/skills/mike/SKILL.md +0 -64
- ruff_sync-0.1.5.dev1/tests/conftest.py +0 -64
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/TESTING.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/formatters-architecture.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/gitlab-reports.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/issue-102-context.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/dirty-equals/SKILL.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/dirty-equals/references/common-matchers.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/dirty-equals/references/toml-matching.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/dirty-equals/trigger_eval.json +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/gh-issues/SKILL.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/mike/references/commands.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/mkdocs-generation/SKILL.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/mkdocs-generation/examples.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/mkdocs-generation/templates/api-reference.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/mkdocs-generation/templates/getting-started.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/mkdocs-generation/templates/index.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/release-notes-generation/SKILL.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/ruff-sync-usage/SKILL.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/ruff-sync-usage/references/ci-integration.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/ruff-sync-usage/references/configuration.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/ruff-sync-usage/references/troubleshooting.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/workflows/add-test-case.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.git-blame-ignore-revs +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.github/dependabot.yml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.github/workflows/complexity.yaml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.github/workflows/docs.yaml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.gitignore +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.pre-commit-config.yaml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.pre-commit-hooks.yaml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/AGENTS.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/CONTRIBUTING.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/LICENSE.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/README.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/codecov.yml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/configs/data-science-engineering/ruff.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/configs/fastapi/ruff.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/configs/kitchen-sink/ruff.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/agent-skill.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/assets/favicon.png +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/assets/github-pr-annotation.png +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/assets/logo.png +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/assets/ruff_sync_banner.png +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/best-practices.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/ci-integration.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/configuration.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/contributing.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/examples/advanced-config.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/examples/basic-config.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/gen_ref_pages.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/index.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/installation.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/pre-commit.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/pre-defined-configs.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/troubleshooting.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/url-resolution.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/docs/usage.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/scripts/check_dogfood.sh +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/scripts/gitclone_dogfood.sh +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/scripts/pull_dogfood.sh +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/skills-lock.json +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/src/ruff_sync/__main__.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/src/ruff_sync/constants.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/src/ruff_sync/formatters.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/src/ruff_sync/pre_commit.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tasks.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/__init__.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/multi_upstream_final.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/multi_upstream_initial.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/multi_upstream_up1.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/multi_upstream_up2.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/no_changes_final.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/no_changes_initial.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/no_changes_upstream.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/no_dotted_keys_final.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/no_dotted_keys_initial.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/no_dotted_keys_upstream.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/no_ruff_cfg_final.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/no_ruff_cfg_initial.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/no_ruff_cfg_upstream.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/readme_excludes_final.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/readme_excludes_initial.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/readme_excludes_upstream.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/standard_final.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/standard_initial.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/lifecycle_tomls/standard_upstream.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/ruff.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_basic.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_check.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_ci_integration.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_ci_validation.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_config_validation.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_constants.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_corner_cases.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_deprecation.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_e2e.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_formatters.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_git_fetch.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_pre_commit.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_project.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_scaffold.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_serialization.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_toml_operations.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_url_handling.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/test_whitespace.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/w_ruff_sync_cfg/pyproject.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/wo_ruff_cfg/pyproject.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/tests/wo_ruff_sync_cfg/pyproject.toml +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Optional Dependencies & Lazy Loading
|
|
2
|
+
|
|
3
|
+
To keep the `ruff-sync` CLI fast and lightweight, while still allowing for extensible features (like a TUI or specialized formatters), we use a standardized pattern for **Optional Dependencies**.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
1. **Defensive Coding**: The application must never crash at startup if an optional dependency is missing. Instead, it should fail gracefully ONLY when the specific feature requiring that dependency is invoked.
|
|
8
|
+
2. **Delayed Import Cycles**: We must avoid expensive imports of optional dependencies during the initial CLI application boot.
|
|
9
|
+
3. **User-Friendly Errors**: If a dependency is missing, we must provide clear instructions on how the user can install it using `ruff-sync` extras.
|
|
10
|
+
|
|
11
|
+
## Standard Pattern
|
|
12
|
+
|
|
13
|
+
All code that relies on an optional dependency MUST follow this pattern:
|
|
14
|
+
|
|
15
|
+
### 1. Define the Extra in `pyproject.toml`
|
|
16
|
+
|
|
17
|
+
Add the dependency to the `[project.optional-dependencies]` section.
|
|
18
|
+
|
|
19
|
+
```toml
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
tui = ["textual>=8.2.2"]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 2. Check and Lazy-Import (Locally)
|
|
25
|
+
|
|
26
|
+
Never import optional dependencies at the top level of a module. All imports must happen inside the function or method that requires them, AFTER a defensive check.
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
def run_tui_feature():
|
|
30
|
+
# 1. First, check availability (fast, lightweight)
|
|
31
|
+
from ruff_sync.dependencies import require_dependency
|
|
32
|
+
require_dependency("textual", extra_name="tui")
|
|
33
|
+
|
|
34
|
+
# 2. Then, perform local import (delayed expensive cycle)
|
|
35
|
+
from textual.app import App
|
|
36
|
+
...
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## The Dependency Helper (`ruff_sync.dependencies`)
|
|
40
|
+
|
|
41
|
+
Use the utilities in `src/ruff_sync/dependencies.py` to handle these checks.
|
|
42
|
+
|
|
43
|
+
- `is_installed(package_name: str) -> bool`: A fast check using `importlib.util.find_spec` that doesn't trigger the package initialization.
|
|
44
|
+
- `require_dependency(package_name: str, extra_name: str) -> None`: Checks if a package is installed and raises a helpful `ImportError` if it is not.
|
|
45
|
+
|
|
46
|
+
### Example ImportError
|
|
47
|
+
> "The 'textual' package is required for this feature. Install it with: pip install 'ruff-sync[tui]'"
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mike
|
|
3
|
+
description: Use this skill to manage multiple versions of documentation for MkDocs-powered sites. Deploy new versions, update aliases (like 'latest' or 'stable'), set the default version for the site root, and manage versioned subdirectories in the deployment branch. Use this whenever the user wants to publish, version, or alias documentation, even if they don't explicitly mention "mike". Also use this when the user wants to troubleshoot or debug versioned documentation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# mike: MkDocs Versioning
|
|
7
|
+
|
|
8
|
+
`mike` is used in this project to manage a versioned documentation site, allowing side-by-side availability of `dev` (main branch) and stable release docs (e.g., `0.1.4`).
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
`mike` is included in the `docs` dependency group.
|
|
13
|
+
```bash
|
|
14
|
+
# Sync documentation dependencies
|
|
15
|
+
uv sync --group docs
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Project Strategy
|
|
19
|
+
|
|
20
|
+
This project follows a specific versioning strategy:
|
|
21
|
+
1. **`dev`**: Represents the current `main` branch.
|
|
22
|
+
2. **Stable Releases**: Versioned documentation (e.g., `0.1.4`) created upon release.
|
|
23
|
+
3. **`stable`**: An alias always pointing to the most recent non-dev release.
|
|
24
|
+
4. **`latest`**: An alias pointing to the most recent release (including dev, if applicable).
|
|
25
|
+
|
|
26
|
+
## Theme Overrides
|
|
27
|
+
|
|
28
|
+
To support the version switcher and custom banners, the project uses a `custom_dir` override in `mkdocs.yml`:
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
theme:
|
|
32
|
+
name: material
|
|
33
|
+
custom_dir: docs/overrides
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Version switcher
|
|
37
|
+
|
|
38
|
+
The version switcher is enabled via `extra.version.provider: mike`:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
extra:
|
|
42
|
+
version:
|
|
43
|
+
provider: mike
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Versioning Banner
|
|
47
|
+
|
|
48
|
+
A custom banner is displayed when users are viewing the `dev` documentation. This is handled via `docs/overrides/main.html` and `docs/overrides/partials/version_warning.html`.
|
|
49
|
+
|
|
50
|
+
### How it works:
|
|
51
|
+
- `main.html`: Extends the base template and includes the `version_warning.html` partial at the start of the `content` block.
|
|
52
|
+
- `version_warning.html`: Contains an HTML snippet that is hidden by default and shown via JavaScript if the URL path contains `/dev/`.
|
|
53
|
+
|
|
54
|
+
Example in `version_warning.html`:
|
|
55
|
+
```html
|
|
56
|
+
<div id="version-warning" style="display: none;">
|
|
57
|
+
<div class="admonition warning">
|
|
58
|
+
<p class="admonition-title">Warning</p>
|
|
59
|
+
<p>
|
|
60
|
+
You are viewing the documentation for the <strong>development</strong> version.
|
|
61
|
+
The latest stable release can be found at <a href="https://kilo59.github.io/ruff-sync/">kilo59.github.io/ruff-sync</a>.
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<script>
|
|
66
|
+
(function() {
|
|
67
|
+
const version_warning = document.getElementById("version-warning");
|
|
68
|
+
if (!version_warning) return;
|
|
69
|
+
|
|
70
|
+
// mike provides a 'mike' object with some metadata if available
|
|
71
|
+
// Otherwise fall back to checking the pathname
|
|
72
|
+
const isDev = window.location.pathname.includes("/dev/") ||
|
|
73
|
+
(window.mike && typeof window.mike.version === 'string' && window.mike.version === "dev");
|
|
74
|
+
|
|
75
|
+
if (isDev) {
|
|
76
|
+
version_warning.style.display = "block";
|
|
77
|
+
}
|
|
78
|
+
})();
|
|
79
|
+
</script>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 1. Deploying Development Docs
|
|
83
|
+
Run this from the `main` branch to update the `dev` version:
|
|
84
|
+
```bash
|
|
85
|
+
mike deploy dev --push --update-aliases
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 2. Deploying a Stable Release
|
|
89
|
+
When a new version is released (e.g., `0.1.4`), deploy it and update the `stable` alias:
|
|
90
|
+
```bash
|
|
91
|
+
# Deploy the specific version and update 'stable'
|
|
92
|
+
mike deploy 0.1.4 stable --push --update-aliases
|
|
93
|
+
|
|
94
|
+
# Set 'stable' as the default version for the site root
|
|
95
|
+
mike set-default --push stable
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Reference Commands
|
|
99
|
+
|
|
100
|
+
| Action | Command |
|
|
101
|
+
| :--- | :--- |
|
|
102
|
+
| **Deploy** | `mike deploy <version> [alias]` |
|
|
103
|
+
| **List** | `mike list` |
|
|
104
|
+
| **Set Default** | `mike set-default <version>` |
|
|
105
|
+
| **Alias** | `mike alias <version> <alias>` |
|
|
106
|
+
| **Delete** | `mike delete <identifier>` |
|
|
107
|
+
|
|
108
|
+
## CI/CD Integration
|
|
109
|
+
|
|
110
|
+
The deployment logic is automated in [.github/workflows/ci.yaml](.github/workflows/ci.yaml). It automatically:
|
|
111
|
+
- Extracts the version from `pyproject.toml`.
|
|
112
|
+
- Deploys to `dev` if the version contains `.dev`.
|
|
113
|
+
- Deploys to `<version>` and updates `stable` for official releases.
|
|
114
|
+
|
|
115
|
+
> [!IMPORTANT]
|
|
116
|
+
> **Do NOT** use `mike install-gh-pages`. It is deprecated and removed in the version used by this project. `mike deploy` handles branch initialization automatically.
|
|
117
|
+
|
|
118
|
+
> [!TIP]
|
|
119
|
+
> Use `mike serve` locally to preview the version switcher before pushing changes.
|
|
120
|
+
|
|
121
|
+
## Troubleshooting
|
|
122
|
+
|
|
123
|
+
### Version Selector Not Appearing
|
|
124
|
+
- **Missing `versions.json`**: Ensure `mike deploy` or `mike update-aliases` has been run. The file must exist at the site root.
|
|
125
|
+
- **Incomplete `versions.json`**: If the current page's version (e.g., `stable`) is not listed in `versions.json`, some themes (like Material) may hide the selector.
|
|
126
|
+
- **`site_url` Case Sensitivity**: On GitHub Pages, ensure `site_url` in `mkdocs.yml` matches the actual deployment URL (usually lowercase). Discrepancies can cause the switcher to fail to find `versions.json` due to 404s.
|
|
127
|
+
- **Redundant Config**: Ensure `theme.version` is NOT set in `mkdocs.yml`. Use `extra.version.provider: mike` instead.
|
|
128
|
+
- **`canonical_version` Missing**: If the switcher is hidden on the root page, adding `canonical_version: stable` (or your main alias) to the `mike` plugin configuration can help the theme associate the root with the switcher metadata.
|
|
129
|
+
|
|
130
|
+
### 404 for `versions.json`
|
|
131
|
+
- If you see a 404 for `/versions.json` but `https://<user>.github.io/<repo>/versions.json` exists, the switcher is looking at the domain root instead of the project root. Verify `site_url` includes the repository name and has a trailing slash.
|
|
132
|
+
|
|
133
|
+
## Post-Mortem & Known Issues
|
|
134
|
+
|
|
135
|
+
> [!CAUTION]
|
|
136
|
+
> **Current Status**: Documentation versioning is currently **BROKEN** on the live site (`kilo59.github.io/ruff-sync`).
|
|
137
|
+
|
|
138
|
+
### Failed Repair History
|
|
139
|
+
The following fixes have been attempted and **FAILED** to resolve the issue:
|
|
140
|
+
1. **Lowercasing `site_url`**: Normalizing the repository name in the URL (e.g., `ruff-sync` instead of `Ruff-Sync`) did not fix the 404s for `versions.json`.
|
|
141
|
+
2. **Removing `theme.version`**: Removing the redundant Material 9.x config did not restore the switcher.
|
|
142
|
+
3. **Adding `canonical_version: stable`**: Adding this to the `mike` plugin in `mkdocs.yml` was intended to fix path resolution but has not fixed the root page 404.
|
|
143
|
+
4. **CI Restoration Logic**: Adding `mike alias --push stable stable` to the CI to manually repair `versions.json` hasn't restored the picker on the root page.
|
|
144
|
+
|
|
145
|
+
### Root Cause Suspicions
|
|
146
|
+
- **GitHub Pages Subfolder Pathing**: The site is served from a subfolder (`/ruff-sync/`). `mike`'s JavaScript for the version switcher frequently struggles with calculating relative paths to `versions.json` when served from a subfolder if `site_url` or base paths are not perfectly aligned with the deployment environment.
|
|
147
|
+
- **`versions.json` Drift**: The `versions.json` file on the `gh-pages` branch frequently becomes desynchronized or loses the `stable` entry, which triggers `mkdocs-material` to hide the switcher entirely.
|
|
148
|
+
|
|
149
|
+
### Guidance for Future Agents
|
|
150
|
+
Before attempting another "fix," you **MUST** verify the current state of `versions.json` on the `gh-pages` branch and check the browser console on the live site for 404 paths. Do not assume standard configurations will work without manual verification of the deployed assets.
|
{ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev3}/.agents/skills/mkdocs-generation/templates/mkdocs.yml
RENAMED
|
@@ -10,10 +10,13 @@ repo_name: [GITHUB_USER]/[REPO_NAME]
|
|
|
10
10
|
extra:
|
|
11
11
|
version:
|
|
12
12
|
provider: mike
|
|
13
|
-
|
|
13
|
+
social:
|
|
14
|
+
- icon: fontawesome/brands/github
|
|
15
|
+
link: https://github.com/[GITHUB_USER]/[REPO_NAME]
|
|
14
16
|
|
|
15
17
|
theme:
|
|
16
18
|
name: material
|
|
19
|
+
custom_dir: docs/overrides
|
|
17
20
|
features:
|
|
18
21
|
- navigation.sections
|
|
19
22
|
- navigation.top
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: textual
|
|
3
|
+
description: Build sophisticated Terminal User Interfaces (TUIs) in Python using an async, CSS-inspired framework.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Textual TUI Framework (>=8.2.2)
|
|
7
|
+
|
|
8
|
+
Textual is a Python framework for creating interactive, beautiful Terminal User Interfaces (TUIs). It uses an asynchronous engine and a layout system inspired by modern web development (Flexbox/Grid and CSS).
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
from textual.app import App, ComposeResult
|
|
15
|
+
from textual.widgets import Header, Footer, Static
|
|
16
|
+
|
|
17
|
+
class SimpleApp(App[None]):
|
|
18
|
+
"""A minimal Textual app."""
|
|
19
|
+
BINDINGS = [("q", "quit", "Quit")]
|
|
20
|
+
|
|
21
|
+
def compose(self) -> ComposeResult:
|
|
22
|
+
yield Header()
|
|
23
|
+
yield Static("Hello, [bold blue]Textual[/bold blue]!")
|
|
24
|
+
yield Footer()
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
SimpleApp().run()
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Core Workflow
|
|
31
|
+
|
|
32
|
+
1. **`compose()`**: Define the UI structure by `yield`-ing widgets.
|
|
33
|
+
2. **Styling**: Use **TCSS** (Textual Cascading Style Sheets) for layout and design.
|
|
34
|
+
3. **Event Handlers**: Handle interaction via specially named methods (e.g., `on_button_pressed`).
|
|
35
|
+
4. **Reactivity**: Use `reactive` attributes to automatically update the UI when data changes.
|
|
36
|
+
|
|
37
|
+
## Progressive Disclosure (Detailed References)
|
|
38
|
+
|
|
39
|
+
- [**Styling & Layout**](references/styling.md): TCSS, Flexbox, Grid, and Units.
|
|
40
|
+
- [**Events & Reactivity**](references/events.md): Message passing, watchers, and state management.
|
|
41
|
+
- [**Widget Library**](references/widgets.md): Common components (DataTable, Input, ListView).
|
|
42
|
+
- [**Testing**](references/testing.md): Unit testing apps with `pilot` and `App.run_test`.
|
|
43
|
+
|
|
44
|
+
## Gotchas & Breaking Changes (>=8.2.2)
|
|
45
|
+
|
|
46
|
+
> [!WARNING]
|
|
47
|
+
> - **8.2.2 Breaking Changes**:
|
|
48
|
+
> - `Static.renderable` and `Label.renderable` are now **`Static.content`** and **`Label.content`**.
|
|
49
|
+
> - `Select.BLANK` is now **`Select.NULL`**.
|
|
50
|
+
> - **Fractional Units**: Use `fr` for fractional units (e.g. `width: 1fr`). A common typo is `rf`, which is invalid.
|
|
51
|
+
> - **Async Handlers**: Event handlers can be `async def` or `def`. Use `async` if you need to `await` I/O or `post_message`.
|
|
52
|
+
> - **Main Thread**: Do not block the main thread with long-running synchronous code. Use `self.run_worker()` for background tasks.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""A minimal Textual app boilerplate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from textual.app import App, ComposeResult
|
|
8
|
+
from textual.containers import Vertical
|
|
9
|
+
from textual.widgets import Button, Footer, Header, Static
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BasicApp(App[None]):
|
|
13
|
+
"""A minimal Textual app boilerplate."""
|
|
14
|
+
|
|
15
|
+
# v8.x.x: Specify a default theme
|
|
16
|
+
theme: ClassVar[str] = "nord"
|
|
17
|
+
|
|
18
|
+
BINDINGS: ClassVar[list[tuple[str, str, str]]] = [
|
|
19
|
+
("q", "quit", "Quit application"),
|
|
20
|
+
("d", "toggle_dark", "Toggle Dark Mode"),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
CSS = """
|
|
24
|
+
Screen {
|
|
25
|
+
align: center middle;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Vertical {
|
|
29
|
+
width: 30;
|
|
30
|
+
height: auto;
|
|
31
|
+
border: solid $accent;
|
|
32
|
+
padding: 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Static {
|
|
36
|
+
text-align: center;
|
|
37
|
+
width: 100%;
|
|
38
|
+
margin-bottom: 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* v8.x.x: New pointer rule */
|
|
42
|
+
Button {
|
|
43
|
+
pointer: pointer;
|
|
44
|
+
}
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def compose(self) -> ComposeResult:
|
|
48
|
+
"""Compose the UI."""
|
|
49
|
+
yield Header()
|
|
50
|
+
with Vertical():
|
|
51
|
+
yield Static("Basic Textual App")
|
|
52
|
+
yield Button("Click Me!", id="hello-btn", variant="primary")
|
|
53
|
+
yield Footer()
|
|
54
|
+
|
|
55
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
56
|
+
"""Handle button press events."""
|
|
57
|
+
if event.button.id == "hello-btn":
|
|
58
|
+
self.notify("Action Triggered!")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
BasicApp().run()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Demonstrating Textual's reactive system."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import App, ComposeResult
|
|
6
|
+
from textual.containers import Container
|
|
7
|
+
from textual.reactive import reactive
|
|
8
|
+
from textual.widgets import Footer, Header, Input, Static
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ReactiveApp(App[None]):
|
|
12
|
+
"""Demonstrating Textual's reactive system."""
|
|
13
|
+
|
|
14
|
+
# Reactive attribute: UI can respond to changes automatically
|
|
15
|
+
search_query: reactive[str] = reactive("")
|
|
16
|
+
|
|
17
|
+
CSS = """
|
|
18
|
+
#status {
|
|
19
|
+
background: $primary;
|
|
20
|
+
color: $text;
|
|
21
|
+
/* v8.x.x: Simplified padding for text content */
|
|
22
|
+
text-padding: 1 2;
|
|
23
|
+
margin-top: 1;
|
|
24
|
+
width: 100%;
|
|
25
|
+
text-align: center;
|
|
26
|
+
}
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def compose(self) -> ComposeResult:
|
|
30
|
+
"""Compose the UI."""
|
|
31
|
+
yield Header()
|
|
32
|
+
with Container():
|
|
33
|
+
yield Input(placeholder="Type to search...", id="search-input")
|
|
34
|
+
yield Static("Waiting for input...", id="status")
|
|
35
|
+
yield Footer()
|
|
36
|
+
|
|
37
|
+
def watch_search_query(self, query: str) -> None:
|
|
38
|
+
"""Called automatically when search_query changes."""
|
|
39
|
+
status = self.query_one("#status")
|
|
40
|
+
if query:
|
|
41
|
+
status.update(f"Searching for: [bold]{query}[/bold]")
|
|
42
|
+
else:
|
|
43
|
+
status.update("Waiting for input...")
|
|
44
|
+
|
|
45
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
46
|
+
"""Update reactive state on input."""
|
|
47
|
+
self.search_query = event.value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
ReactiveApp().run()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Events & Reactivity
|
|
2
|
+
|
|
3
|
+
Textual uses a powerful event-driven architecture and a reactive state system to keep the UI in sync with your data.
|
|
4
|
+
|
|
5
|
+
## Event Handlers
|
|
6
|
+
|
|
7
|
+
Naming follows the convention: `on_<widget_name>_<event_type>`.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
class MySidebar(Vertical):
|
|
11
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
12
|
+
"""Handle any button press within the sidebar."""
|
|
13
|
+
self.log(f"Button {event.button.id} was pressed!")
|
|
14
|
+
|
|
15
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
16
|
+
"""Handle specifically an Input widget change."""
|
|
17
|
+
self.app.notify(f"Searching for: {event.value}")
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Theming (v8.x.x)
|
|
21
|
+
|
|
22
|
+
Textual v8.x.x introduced a robust theming system.
|
|
23
|
+
|
|
24
|
+
- **`App.theme`**: Set the active theme (e.g., `self.theme = "nord"`).
|
|
25
|
+
- **`App.register_theme(theme)`**: Add custom themes to the app.
|
|
26
|
+
- **`App.search_themes()`**: Bring up the theme switcher in the command palette.
|
|
27
|
+
- **`variant`**: Use `Button(variant="primary")` or `Label(variant="warning")` for themed styles.
|
|
28
|
+
|
|
29
|
+
## Reactive Attributes
|
|
30
|
+
|
|
31
|
+
Define `reactive` attributes to automatically trigger updates. Use `watch_<attribute_name>` to side-effect when a value changes.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from textual.reactive import reactive
|
|
35
|
+
|
|
36
|
+
class Counter(Static):
|
|
37
|
+
# Reactive state: UI updates whenever 'count' is modified
|
|
38
|
+
count: reactive[int] = reactive(0)
|
|
39
|
+
|
|
40
|
+
def watch_count(self, old_value: int, new_value: int) -> None:
|
|
41
|
+
"""Called automatically when count changes."""
|
|
42
|
+
self.update(f"Current Count: {new_value}")
|
|
43
|
+
|
|
44
|
+
def on_click(self) -> None:
|
|
45
|
+
self.count += 1
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Message Passing
|
|
49
|
+
|
|
50
|
+
Widgets can communicate with parents via custom messages:
|
|
51
|
+
|
|
52
|
+
1. **Define a Message**:
|
|
53
|
+
```python
|
|
54
|
+
from textual.message import Message
|
|
55
|
+
class DataLoaded(Message):
|
|
56
|
+
def __init__(self, data: list[str]) -> None:
|
|
57
|
+
self.data = data
|
|
58
|
+
super().__init__()
|
|
59
|
+
```
|
|
60
|
+
2. **Post it**: `self.post_message(DataLoaded(my_data))`
|
|
61
|
+
3. **Handle it in Parent**:
|
|
62
|
+
```python
|
|
63
|
+
def on_data_loaded(self, event: DataLoaded) -> None:
|
|
64
|
+
self.query_one(DataTable).add_rows(event.data)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Procedure: Implementing a Search Feature
|
|
68
|
+
|
|
69
|
+
1. **Reactive Search Query**:
|
|
70
|
+
```python
|
|
71
|
+
search_query: reactive[str] = reactive("")
|
|
72
|
+
```
|
|
73
|
+
2. **Watch the Query**:
|
|
74
|
+
```python
|
|
75
|
+
def watch_search_query(self, query: str) -> None:
|
|
76
|
+
"""Perform search whenever the query changes."""
|
|
77
|
+
results = self.search_database(query)
|
|
78
|
+
self.results_container.update_results(results)
|
|
79
|
+
```
|
|
80
|
+
3. **Bridge from UI**:
|
|
81
|
+
```python
|
|
82
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
83
|
+
self.search_query = event.value
|
|
84
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Styling & Layout (TCSS)
|
|
2
|
+
|
|
3
|
+
Textual uses **TCSS** (Textual Cascading Style Sheets) for design. It is similar to web CSS but optimized for the terminal grid.
|
|
4
|
+
|
|
5
|
+
## Core Units
|
|
6
|
+
|
|
7
|
+
- **`fr`**: Fractional units (e.g., `1fr`, `2fr`). Divides available space proportionally.
|
|
8
|
+
- **`%`**: Percentage of the parent container's dimension.
|
|
9
|
+
- **`vh` / `vw`**: Percentage of the entire terminal window height/width.
|
|
10
|
+
- **Integers**: Represent exact terminal character cells (e.g., `width: 20;`).
|
|
11
|
+
|
|
12
|
+
- **`Vertical`**: Stack widgets top-to-bottom.
|
|
13
|
+
- **`Horizontal`**: Align widgets left-to-right.
|
|
14
|
+
- **`Grid`**: A flexible 2D grid layout. Define `grid-size`, `grid-columns`, and `grid-rows`.
|
|
15
|
+
|
|
16
|
+
## New CSS Rules (v8.x.x)
|
|
17
|
+
|
|
18
|
+
- **`pointer`**: Change the mouse cursor style (e.g., `pointer: pointer;`, `pointer: text;`).
|
|
19
|
+
- **`background-tint`**: Apply a translucent color over the background (e.g., `background-tint: $primary 20%;`).
|
|
20
|
+
- **`text-padding`**: Simplified padding for text content within a widget (e.g., `text-padding: 1 2;`).
|
|
21
|
+
- **`scroll-bar-visibility`**: Control when scrollbars are shown (`auto`, `visible`, `hidden`).
|
|
22
|
+
- **`position`**: Support for `relative` and `absolute` positioning.
|
|
23
|
+
|
|
24
|
+
## Procedure: Rapid Styling
|
|
25
|
+
|
|
26
|
+
1. **Assign IDs or Classes**:
|
|
27
|
+
```python
|
|
28
|
+
yield Button("Save", id="save-btn", classes="primary-action")
|
|
29
|
+
```
|
|
30
|
+
2. **Define TCSS**:
|
|
31
|
+
```css
|
|
32
|
+
.primary-action {
|
|
33
|
+
background: $accent;
|
|
34
|
+
color: $text;
|
|
35
|
+
text-style: bold;
|
|
36
|
+
width: 100%;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#save-btn {
|
|
40
|
+
border: heavy $success;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
3. **Reference CSS in App**:
|
|
44
|
+
```python
|
|
45
|
+
class MyApp(App):
|
|
46
|
+
CSS_PATH = "styles.tcss" # Relative to the Python file
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Built-in Design System
|
|
50
|
+
|
|
51
|
+
Textual includes a set of semantic variables (`$primary`, `$secondary`, `$accent`, `$error`, `$success`) that automatically adapt to the user's terminal theme.
|
|
52
|
+
|
|
53
|
+
> [!TIP]
|
|
54
|
+
> Use `textual console` or `textual run --dev` to see live styling updates without restarting the application.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Testing & Debugging
|
|
2
|
+
|
|
3
|
+
Textual apps are inherently asynchronous. Testing requires simulating user input and observing the UI state.
|
|
4
|
+
|
|
5
|
+
## Unit Testing with Pilot
|
|
6
|
+
|
|
7
|
+
Use the `pilot` object to interact with your app in a headless state.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import pytest
|
|
11
|
+
from my_app import SimpleApp
|
|
12
|
+
|
|
13
|
+
@pytest.mark.asyncio
|
|
14
|
+
async def test_button_click():
|
|
15
|
+
app = SimpleApp()
|
|
16
|
+
async with app.run_test() as pilot:
|
|
17
|
+
# Simulate a button click by CSS selector or ID
|
|
18
|
+
await pilot.click("#say-hello-btn")
|
|
19
|
+
|
|
20
|
+
# New in v8.x.x: Rapid clicks (double click)
|
|
21
|
+
await pilot.click("#say-hello-btn", times=2)
|
|
22
|
+
# OR use dedicated methods:
|
|
23
|
+
await pilot.double_click("#say-hello-btn")
|
|
24
|
+
await pilot.triple_click("#say-hello-btn")
|
|
25
|
+
|
|
26
|
+
# Verify state
|
|
27
|
+
assert app.notification_count >= 1
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Developer Tools
|
|
31
|
+
|
|
32
|
+
Textual includes powerful tools for live development.
|
|
33
|
+
|
|
34
|
+
### Textual Devtools
|
|
35
|
+
In one terminal, run:
|
|
36
|
+
```bash
|
|
37
|
+
textual console
|
|
38
|
+
```
|
|
39
|
+
In another, run your app with the `--dev` flag:
|
|
40
|
+
```bash
|
|
41
|
+
textual run --dev my_app.py
|
|
42
|
+
```
|
|
43
|
+
Logs and tracebacks will stream to the console, allowing you to see `self.log()` output and `print()` statements without breaking the UI.
|
|
44
|
+
|
|
45
|
+
## Procedures
|
|
46
|
+
|
|
47
|
+
### Simulating Text Entry
|
|
48
|
+
```python
|
|
49
|
+
async with app.run_test() as pilot:
|
|
50
|
+
await pilot.press("h", "e", "l", "l", "o", "enter")
|
|
51
|
+
assert app.query_one(Input).value == "hello"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Waiting for Animations/I/O
|
|
55
|
+
If your UI has transitions or performs background work, use `pilot.wait_for_scheduled_animations()` or simply `await pilot.pause(0.1)`.
|
|
56
|
+
|
|
57
|
+
> [!IMPORTANT]
|
|
58
|
+
> Always use `pytest-asyncio` with the `@pytest.mark.asyncio` decorator for Textual tests. The boilerplate provided in `app.run_test()` handles the event loop lifecycle for you.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Widget Library
|
|
2
|
+
|
|
3
|
+
Textual includes a rich set of built-in widgets. Always check `textual.widgets` before building your own.
|
|
4
|
+
|
|
5
|
+
## Content & Navigation
|
|
6
|
+
|
|
7
|
+
- **`Header`**: Standard title bar with optional clock.
|
|
8
|
+
- **`Footer`**: Keybinding bar (automatically populated from `BINDINGS`).
|
|
9
|
+
- **`Static`**: Pure text or basic content. Use **`content`** (v8.x.x) for raw renderable or `markup=True`.
|
|
10
|
+
- **`Link`**: (v8.x.x) New widget for clickable URLs.
|
|
11
|
+
- **`ProgressBar`**: Real-time progress tracking.
|
|
12
|
+
|
|
13
|
+
## Interaction
|
|
14
|
+
|
|
15
|
+
- **`Button`**: Standard clickable button. Variants: `success`, `error`, `primary`, `warning`.
|
|
16
|
+
- **`Input`**: Text entry field. Events: `Changed`, `Submitted`.
|
|
17
|
+
- **`MaskedInput`**: (v8.x.x) New widget for formatted inputs (e.g. phones, CC, etc.).
|
|
18
|
+
- **`Checkbox`** / **`Switch`**: Boolean state inputs.
|
|
19
|
+
- **`Select`**: Dropdown selection. Sentinel: **`Select.NULL`** (v8.x.x).
|
|
20
|
+
|
|
21
|
+
## Data & Selection
|
|
22
|
+
|
|
23
|
+
### `DataTable`
|
|
24
|
+
High-performance grid for tabular data.
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
table = self.query_one(DataTable)
|
|
28
|
+
table.add_columns("ID", "Name", "Score")
|
|
29
|
+
table.add_row("1", "Alice", "100")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### `ListView` & `ListItem`
|
|
33
|
+
Scrollable lists of items.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
list_view = self.query_one(ListView)
|
|
37
|
+
list_view.append(ListItem(Static("Item One")))
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Procedures
|
|
41
|
+
|
|
42
|
+
### Adding Data to a Table
|
|
43
|
+
|
|
44
|
+
1. **Clear existing rows**: `table.clear()`
|
|
45
|
+
2. **Batch add rows**: `table.add_rows(data_generator_or_list)`
|
|
46
|
+
3. **Control selection**: `table.cursor_type = "row"` (default is `"cell"`)
|
|
47
|
+
|
|
48
|
+
### Handling Keyboard Input
|
|
49
|
+
|
|
50
|
+
Use the `on_key` handler for low-level input:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
def on_key(self, event: events.Key) -> None:
|
|
54
|
+
if event.key == "ctrl+s":
|
|
55
|
+
self.save_data()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> [!TIP]
|
|
59
|
+
> Use `BINDINGS` in your `App` or `Screen` class for most navigation tasks. Textual manages the labels and shortcuts in the `Footer` for you.
|
|
60
|
+
|
|
61
|
+
### `ModalScreen` & Overlays
|
|
62
|
+
Modals are screens with a transparent or dim background that overlay the main app.
|
|
63
|
+
```python
|
|
64
|
+
from textual.screen import ModalScreen
|
|
65
|
+
from textual.app import App
|
|
66
|
+
|
|
67
|
+
class OmniboxScreen(ModalScreen[str]):
|
|
68
|
+
# A modal screen that returns a `str` when dismissed.
|
|
69
|
+
def compose(self) -> ComposeResult:
|
|
70
|
+
# yield your input/search widgets here
|
|
71
|
+
yield Input(placeholder="Search...")
|
|
72
|
+
|
|
73
|
+
# Dismiss the screen and return data
|
|
74
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
75
|
+
self.dismiss(event.value)
|
|
76
|
+
|
|
77
|
+
# In the main App or Screen:
|
|
78
|
+
def on_key(self, event: events.Key) -> None:
|
|
79
|
+
if event.key == "ctrl+p":
|
|
80
|
+
self.push_screen(OmniboxScreen(), self.handle_omnibox_result)
|
|
81
|
+
|
|
82
|
+
def handle_omnibox_result(self, result: str | None) -> None:
|
|
83
|
+
if result:
|
|
84
|
+
print(f"Selected: {result}")
|
|
85
|
+
```
|