ruff-sync 0.1.5.dev1__tar.gz → 0.1.5.dev2__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.dev2/.agents/DEPENDENCIES.md +47 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/mike/SKILL.md +43 -1
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/mkdocs-generation/templates/mkdocs.yml +4 -1
- ruff_sync-0.1.5.dev2/.agents/skills/textual/SKILL.md +52 -0
- ruff_sync-0.1.5.dev2/.agents/skills/textual/examples/basic_app.py +62 -0
- ruff_sync-0.1.5.dev2/.agents/skills/textual/examples/reactive_example.py +51 -0
- ruff_sync-0.1.5.dev2/.agents/skills/textual/references/events.md +84 -0
- ruff_sync-0.1.5.dev2/.agents/skills/textual/references/styling.md +54 -0
- ruff_sync-0.1.5.dev2/.agents/skills/textual/references/testing.md +58 -0
- ruff_sync-0.1.5.dev2/.agents/skills/textual/references/widgets.md +59 -0
- ruff_sync-0.1.5.dev2/.agents/tui_design.md +140 -0
- ruff_sync-0.1.5.dev2/.agents/tui_requirements.md +65 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/PKG-INFO +3 -1
- ruff_sync-0.1.5.dev2/docs/overrides/main.html +6 -0
- ruff_sync-0.1.5.dev2/docs/overrides/partials/version_warning.html +18 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/mkdocs.yml +3 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/pyproject.toml +4 -1
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/src/ruff_sync/__init__.py +8 -2
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/src/ruff_sync/cli.py +20 -0
- ruff_sync-0.1.5.dev2/src/ruff_sync/config_io.py +136 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/src/ruff_sync/core.py +1 -73
- ruff_sync-0.1.5.dev2/src/ruff_sync/dependencies.py +38 -0
- ruff_sync-0.1.5.dev2/src/ruff_sync/system.py +45 -0
- ruff_sync-0.1.5.dev2/tests/test_config_io.py +153 -0
- ruff_sync-0.1.5.dev2/tests/test_dependencies.py +35 -0
- ruff_sync-0.1.5.dev2/tests/test_system.py +54 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/uv.lock +97 -1
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/TESTING.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/formatters-architecture.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/gitlab-reports.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/issue-102-context.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/dirty-equals/SKILL.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/dirty-equals/references/common-matchers.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/dirty-equals/references/toml-matching.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/dirty-equals/trigger_eval.json +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/gh-issues/SKILL.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/mike/references/commands.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/mkdocs-generation/SKILL.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/mkdocs-generation/examples.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/mkdocs-generation/templates/api-reference.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/mkdocs-generation/templates/getting-started.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/mkdocs-generation/templates/index.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/release-notes-generation/SKILL.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/ruff-sync-usage/SKILL.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/ruff-sync-usage/references/ci-integration.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/ruff-sync-usage/references/configuration.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/ruff-sync-usage/references/troubleshooting.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/workflows/add-test-case.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.git-blame-ignore-revs +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.github/dependabot.yml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.github/workflows/ci.yaml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.github/workflows/complexity.yaml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.github/workflows/docs.yaml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.gitignore +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.pre-commit-config.yaml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.pre-commit-hooks.yaml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/AGENTS.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/CONTRIBUTING.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/LICENSE.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/README.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/codecov.yml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/configs/data-science-engineering/ruff.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/configs/fastapi/ruff.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/configs/kitchen-sink/ruff.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/agent-skill.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/assets/favicon.png +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/assets/github-pr-annotation.png +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/assets/logo.png +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/assets/ruff_sync_banner.png +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/best-practices.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/ci-integration.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/configuration.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/contributing.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/examples/advanced-config.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/examples/basic-config.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/gen_ref_pages.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/index.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/installation.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/pre-commit.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/pre-defined-configs.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/troubleshooting.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/url-resolution.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/usage.md +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/scripts/check_dogfood.sh +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/scripts/gitclone_dogfood.sh +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/scripts/pull_dogfood.sh +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/skills-lock.json +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/src/ruff_sync/__main__.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/src/ruff_sync/constants.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/src/ruff_sync/formatters.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/src/ruff_sync/pre_commit.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tasks.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/__init__.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/conftest.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/multi_upstream_final.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/multi_upstream_initial.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/multi_upstream_up1.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/multi_upstream_up2.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_changes_final.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_changes_initial.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_changes_upstream.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_dotted_keys_final.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_dotted_keys_initial.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_dotted_keys_upstream.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_ruff_cfg_final.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_ruff_cfg_initial.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_ruff_cfg_upstream.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/readme_excludes_final.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/readme_excludes_initial.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/readme_excludes_upstream.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/standard_final.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/standard_initial.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/standard_upstream.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/ruff.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_basic.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_check.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_ci_integration.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_ci_validation.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_config_validation.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_constants.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_corner_cases.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_deprecation.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_e2e.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_formatters.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_git_fetch.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_pre_commit.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_project.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_scaffold.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_serialization.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_toml_operations.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_url_handling.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_whitespace.py +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/w_ruff_sync_cfg/pyproject.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/wo_ruff_cfg/pyproject.toml +0 -0
- {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/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]'"
|
|
@@ -21,8 +21,50 @@ This project follows a specific versioning strategy:
|
|
|
21
21
|
1. **`dev`**: Represents the current `main` branch.
|
|
22
22
|
2. **Stable Releases**: Versioned documentation (e.g., `0.1.4`) created upon release.
|
|
23
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).
|
|
24
25
|
|
|
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>You are viewing the development version of the documentation.</p>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<script>
|
|
63
|
+
if (window.location.pathname.includes("/dev/")) {
|
|
64
|
+
document.getElementById("version-warning").style.display = "block";
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
67
|
+
```
|
|
26
68
|
|
|
27
69
|
### 1. Deploying Development Docs
|
|
28
70
|
Run this from the `main` branch to update the `dev` version:
|
{ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.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,59 @@
|
|
|
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.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Implementation Plan: Ruff-Sync Read-Only TUI (`inspect`)
|
|
2
|
+
|
|
3
|
+
This document provides a thorough, step-by-step technical design for implementing the initial "Read-Only" TUI to interrogate and visualize `ruff` configurations. This follows the requirements established in `.agents/tui_requirements.md`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Pre-Requisite Refactoring & Architecture Upgrades
|
|
8
|
+
|
|
9
|
+
Before we build the TUI feature itself, some of the existing codebase should be refactored to better support isolated Data Access and Subprocess management.
|
|
10
|
+
|
|
11
|
+
### A. Subprocess Wrapper (`src/ruff_sync/system.py` or `ruff_cli.py`)
|
|
12
|
+
Currently, `ruff-sync` is primarily concerned with TOML serialization. The TUI introduces a requirement to execute the system `ruff` binary (e.g. `ruff rule <CODE>`).
|
|
13
|
+
**Refactoring Task:**
|
|
14
|
+
- Create a generalized, safe abstraction for interacting with the `ruff` executable.
|
|
15
|
+
- Provide a typed function `async def get_ruff_rule_markdown(rule_code: str) -> str | None` that uses `asyncio.create_subprocess_exec` (or threads with `subprocess`) to fetch and return the rule text. This prevents raw `subprocess` sprawl across TUI widgets.
|
|
16
|
+
|
|
17
|
+
### B. Configuration Reader Extraction (`src/ruff_sync/config_io.py` or similar)
|
|
18
|
+
Right now, discovering and extracting the local `pyproject.toml` is slightly coupled to the lifecycle of pulling from upstreams (`pull()` / `check()` in `core.py`).
|
|
19
|
+
**Refactoring Task:**
|
|
20
|
+
- Extract a clean Data Access Object or helper (e.g., `def load_local_ruff_config(path: Path) -> dict[str, Any]`) that utilizes `resolve_target_path`, parses via `tomlkit`, and automatically calls `.unwrap()` to return a plain, read-only Python dictionary. This insulates the Textual app from navigating bizarre `tomlkit` proxy tables.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 2. Dependency & CLI Integration
|
|
25
|
+
|
|
26
|
+
### [MODIFY] pyproject.toml
|
|
27
|
+
- Under `[project.optional-dependencies]`, define the `tui` extra explicitly pinning the Textual version framework (v8.x.x):
|
|
28
|
+
```toml
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
tui = ["textual>=8.2.2"]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### [MODIFY] src/ruff_sync/cli.py
|
|
34
|
+
- Add an `inspect` subcommand to `PARSER` inside `_get_cli_parser()`:
|
|
35
|
+
```python
|
|
36
|
+
inspect_parser = subparsers.add_parser(
|
|
37
|
+
"inspect",
|
|
38
|
+
parents=[common_parser],
|
|
39
|
+
help="Open a Terminal UI to explore and interrogate your local ruff configuration."
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
- In `main()`, route the `inspect` command to a lazy-loaded wrapper:
|
|
43
|
+
```python
|
|
44
|
+
if exec_args.command == "inspect":
|
|
45
|
+
from ruff_sync.dependencies import require_dependency
|
|
46
|
+
require_dependency("textual", extra_name="tui")
|
|
47
|
+
|
|
48
|
+
from ruff_sync.tui.app import RuffSyncApp
|
|
49
|
+
return RuffSyncApp(exec_args).run()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 3. TUI Application Structure & Views
|
|
55
|
+
|
|
56
|
+
### [NEW] src/ruff_sync/tui/__init__.py
|
|
57
|
+
- Mark as a Python package.
|
|
58
|
+
|
|
59
|
+
### [NEW] src/ruff_sync/tui/app.py
|
|
60
|
+
- Define `RuffSyncApp` subclassing `textual.app.App`.
|
|
61
|
+
- **CSS**: Define embedded TCSS.
|
|
62
|
+
- Layout: `Horizontal` split with `Tree` (left) sized `1fr`, and a `Vertical` container (right) sized `2fr`. Left side for Navigation, right side for Content & Inspector.
|
|
63
|
+
- **Compose Method**:
|
|
64
|
+
```python
|
|
65
|
+
def compose(self) -> ComposeResult:
|
|
66
|
+
yield Header()
|
|
67
|
+
with Horizontal():
|
|
68
|
+
yield ConfigTree(id="config-tree")
|
|
69
|
+
with Vertical():
|
|
70
|
+
yield CategoryTable(id="category-table")
|
|
71
|
+
yield RuleInspector(id="inspector", classes="hidden")
|
|
72
|
+
yield Footer()
|
|
73
|
+
```
|
|
74
|
+
- **Lifecycle (`on_mount`)**:
|
|
75
|
+
- Automatically load `[tool.ruff]` via our refactored reader function.
|
|
76
|
+
- Populate the `ConfigTree` with top-level keys (`lint`, `format`, etc.).
|
|
77
|
+
|
|
78
|
+
### [NEW] src/ruff_sync/tui/widgets.py
|
|
79
|
+
Contains all custom Textual widgets required for this view.
|
|
80
|
+
|
|
81
|
+
**1. `ConfigTree` (inherits `textual.widgets.Tree`)**:
|
|
82
|
+
- Parses the unwrapped dictionary representation of the Ruff config.
|
|
83
|
+
- Builds interactive nodes for structural hierarchies (e.g. `lint.select`, `format`).
|
|
84
|
+
|
|
85
|
+
**2. `CategoryTable` (inherits `textual.widgets.DataTable`)**:
|
|
86
|
+
- Displays key-value sets dynamically.
|
|
87
|
+
- Automatically clears and updates columns/rows when a tree node is highlighted.
|
|
88
|
+
|
|
89
|
+
**3. `RuleInspector` (inherits `textual.widgets.Markdown`)**:
|
|
90
|
+
- Displays `ruff rule <CODE>` output.
|
|
91
|
+
- Features an `async def fetch_and_display(self, rule_code: str)` method.
|
|
92
|
+
- **Background Worker**: Uses Textual `@work(thread=True)` to execute our refactored `get_ruff_rule_markdown()` function.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 4. Textual Events & Data Flow
|
|
97
|
+
|
|
98
|
+
### [MODIFY] src/ruff_sync/tui/app.py (Reactivity)
|
|
99
|
+
- Tie widgets together using Textual's event routing (`@on`):
|
|
100
|
+
- `@on(Tree.NodeSelected)`:
|
|
101
|
+
- If the node is a configuration section (e.g. `lint.isort`), hide the Inspector and populate the `CategoryTable` with settings.
|
|
102
|
+
- If the node represents a list of rules (unwrapped `lint.select`), display the active rule list in the table.
|
|
103
|
+
- `@on(DataTable.RowSelected)`:
|
|
104
|
+
- If the focused row represents a Ruff Rule Code (e.g., `RUF012`), reveal the `RuleInspector` widget and call `inspector.fetch_and_display("RUF012")`.
|
|
105
|
+
- Expose related context natively based on selections.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 5. Strict Static Typing & mypy Guidelines
|
|
110
|
+
|
|
111
|
+
Because `ruff-sync` operates under strict mypy regulations, the TUI module must explicitly conform to static type safety.
|
|
112
|
+
|
|
113
|
+
**Key Typing Considerations:**
|
|
114
|
+
1. **Textual App Generics:** Subclass `App` with its expected return type. If the app just runs and quits without returning a value, define it as `class RuffSyncApp(App[None]):`.
|
|
115
|
+
2. **Event Handler Signatures:** Utilize specific type hints internally provided by Textual for event payloads. For example:
|
|
116
|
+
```python
|
|
117
|
+
async def on_tree_node_selected(self, event: Tree.NodeSelected[Any]) -> None:
|
|
118
|
+
```
|
|
119
|
+
3. **Handling `tomlkit` Types (NO `cast`):** `tomlkit`'s proxy objects often resolve ambiguously. Because `typing.cast` is forbidden in production code, when calling `.unwrap()` inside the `ConfigReader` refactor, you **must use explicit `isinstance` checks`** or `TypeGuard` functions (e.g. `if not isinstance(unwrapped_data, dict): raise TypeError(...)`) to strictly narrow the type initially.
|
|
120
|
+
4. **Structured Data over Dicts:** Instead of passing around plain `dict[str, Any]` between the Data Access Object and the Textual widgets, deeply recommend defining and using `TypedDict` or `NamedTuple` objects for predictable configuration structures. This provides significantly better type safety down the pipeline.
|
|
121
|
+
5. **Subprocess IO:** The new `ruff_cli.py` utility function `get_ruff_rule_markdown()` must explicitly declare its return values (`-> str | None`) and the subprocess decoding pipeline should clearly handle `bytes` versus `str` boundary conversions.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 6. Verification Plan
|
|
126
|
+
|
|
127
|
+
### Automated Tests
|
|
128
|
+
- Scaffold `tests/test_tui.py`.
|
|
129
|
+
- Mock out `get_ruff_rule_markdown()` to reliably return stub Markdown documents.
|
|
130
|
+
- Use `textual.app.App.run_test()` to:
|
|
131
|
+
1. Boot the application.
|
|
132
|
+
2. Synthetically select a configuration section and assert the `DataTable` correctly hydrates.
|
|
133
|
+
3. Select a documented rule row and assert the `RuleInspector` Markdown widget transitions from hidden to visible and displays the mock documentation.
|
|
134
|
+
4. Ensure `ImportError` gracefully catches environments that attempt to run the feature without installing `ruff-sync[tui]`.
|
|
135
|
+
|
|
136
|
+
### Manual Testing Protocol
|
|
137
|
+
1. Install feature branch `pip install -e '.[tui]'`.
|
|
138
|
+
2. Execute `uv run ruff-sync inspect` locally.
|
|
139
|
+
3. Validate visual tree correctly reflects the active target `pyproject.toml`.
|
|
140
|
+
4. Highlight rules to ensure rendering triggers asynchronously without locking the UI process block.
|