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.
Files changed (135) hide show
  1. ruff_sync-0.1.5.dev2/.agents/DEPENDENCIES.md +47 -0
  2. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/mike/SKILL.md +43 -1
  3. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/mkdocs-generation/templates/mkdocs.yml +4 -1
  4. ruff_sync-0.1.5.dev2/.agents/skills/textual/SKILL.md +52 -0
  5. ruff_sync-0.1.5.dev2/.agents/skills/textual/examples/basic_app.py +62 -0
  6. ruff_sync-0.1.5.dev2/.agents/skills/textual/examples/reactive_example.py +51 -0
  7. ruff_sync-0.1.5.dev2/.agents/skills/textual/references/events.md +84 -0
  8. ruff_sync-0.1.5.dev2/.agents/skills/textual/references/styling.md +54 -0
  9. ruff_sync-0.1.5.dev2/.agents/skills/textual/references/testing.md +58 -0
  10. ruff_sync-0.1.5.dev2/.agents/skills/textual/references/widgets.md +59 -0
  11. ruff_sync-0.1.5.dev2/.agents/tui_design.md +140 -0
  12. ruff_sync-0.1.5.dev2/.agents/tui_requirements.md +65 -0
  13. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/PKG-INFO +3 -1
  14. ruff_sync-0.1.5.dev2/docs/overrides/main.html +6 -0
  15. ruff_sync-0.1.5.dev2/docs/overrides/partials/version_warning.html +18 -0
  16. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/mkdocs.yml +3 -0
  17. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/pyproject.toml +4 -1
  18. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/src/ruff_sync/__init__.py +8 -2
  19. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/src/ruff_sync/cli.py +20 -0
  20. ruff_sync-0.1.5.dev2/src/ruff_sync/config_io.py +136 -0
  21. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/src/ruff_sync/core.py +1 -73
  22. ruff_sync-0.1.5.dev2/src/ruff_sync/dependencies.py +38 -0
  23. ruff_sync-0.1.5.dev2/src/ruff_sync/system.py +45 -0
  24. ruff_sync-0.1.5.dev2/tests/test_config_io.py +153 -0
  25. ruff_sync-0.1.5.dev2/tests/test_dependencies.py +35 -0
  26. ruff_sync-0.1.5.dev2/tests/test_system.py +54 -0
  27. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/uv.lock +97 -1
  28. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/TESTING.md +0 -0
  29. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/formatters-architecture.md +0 -0
  30. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/gitlab-reports.md +0 -0
  31. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/issue-102-context.md +0 -0
  32. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/dirty-equals/SKILL.md +0 -0
  33. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/dirty-equals/references/common-matchers.md +0 -0
  34. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/dirty-equals/references/toml-matching.md +0 -0
  35. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/dirty-equals/trigger_eval.json +0 -0
  36. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/gh-issues/SKILL.md +0 -0
  37. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/mike/references/commands.md +0 -0
  38. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/mkdocs-generation/SKILL.md +0 -0
  39. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/mkdocs-generation/examples.md +0 -0
  40. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/mkdocs-generation/templates/api-reference.md +0 -0
  41. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/mkdocs-generation/templates/getting-started.md +0 -0
  42. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/mkdocs-generation/templates/index.md +0 -0
  43. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/release-notes-generation/SKILL.md +0 -0
  44. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/ruff-sync-usage/SKILL.md +0 -0
  45. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/ruff-sync-usage/references/ci-integration.md +0 -0
  46. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/ruff-sync-usage/references/configuration.md +0 -0
  47. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/skills/ruff-sync-usage/references/troubleshooting.md +0 -0
  48. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.agents/workflows/add-test-case.md +0 -0
  49. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.git-blame-ignore-revs +0 -0
  50. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.github/dependabot.yml +0 -0
  51. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.github/workflows/ci.yaml +0 -0
  52. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.github/workflows/complexity.yaml +0 -0
  53. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.github/workflows/docs.yaml +0 -0
  54. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.gitignore +0 -0
  55. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.pre-commit-config.yaml +0 -0
  56. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/.pre-commit-hooks.yaml +0 -0
  57. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/AGENTS.md +0 -0
  58. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/CONTRIBUTING.md +0 -0
  59. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/LICENSE.md +0 -0
  60. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/README.md +0 -0
  61. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/codecov.yml +0 -0
  62. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/configs/data-science-engineering/ruff.toml +0 -0
  63. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/configs/fastapi/ruff.toml +0 -0
  64. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/configs/kitchen-sink/ruff.toml +0 -0
  65. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/agent-skill.md +0 -0
  66. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/assets/favicon.png +0 -0
  67. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/assets/github-pr-annotation.png +0 -0
  68. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/assets/logo.png +0 -0
  69. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/assets/ruff_sync_banner.png +0 -0
  70. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/best-practices.md +0 -0
  71. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/ci-integration.md +0 -0
  72. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/configuration.md +0 -0
  73. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/contributing.md +0 -0
  74. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/examples/advanced-config.toml +0 -0
  75. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/examples/basic-config.toml +0 -0
  76. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/gen_ref_pages.py +0 -0
  77. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/index.md +0 -0
  78. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/installation.md +0 -0
  79. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/pre-commit.md +0 -0
  80. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/pre-defined-configs.md +0 -0
  81. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/troubleshooting.md +0 -0
  82. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/url-resolution.md +0 -0
  83. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/docs/usage.md +0 -0
  84. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/scripts/check_dogfood.sh +0 -0
  85. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/scripts/gitclone_dogfood.sh +0 -0
  86. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/scripts/pull_dogfood.sh +0 -0
  87. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/skills-lock.json +0 -0
  88. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/src/ruff_sync/__main__.py +0 -0
  89. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/src/ruff_sync/constants.py +0 -0
  90. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/src/ruff_sync/formatters.py +0 -0
  91. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/src/ruff_sync/pre_commit.py +0 -0
  92. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tasks.py +0 -0
  93. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/__init__.py +0 -0
  94. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/conftest.py +0 -0
  95. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/multi_upstream_final.toml +0 -0
  96. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/multi_upstream_initial.toml +0 -0
  97. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/multi_upstream_up1.toml +0 -0
  98. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/multi_upstream_up2.toml +0 -0
  99. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_changes_final.toml +0 -0
  100. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_changes_initial.toml +0 -0
  101. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_changes_upstream.toml +0 -0
  102. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_dotted_keys_final.toml +0 -0
  103. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_dotted_keys_initial.toml +0 -0
  104. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_dotted_keys_upstream.toml +0 -0
  105. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_ruff_cfg_final.toml +0 -0
  106. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_ruff_cfg_initial.toml +0 -0
  107. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/no_ruff_cfg_upstream.toml +0 -0
  108. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/readme_excludes_final.toml +0 -0
  109. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/readme_excludes_initial.toml +0 -0
  110. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/readme_excludes_upstream.toml +0 -0
  111. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/standard_final.toml +0 -0
  112. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/standard_initial.toml +0 -0
  113. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/lifecycle_tomls/standard_upstream.toml +0 -0
  114. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/ruff.toml +0 -0
  115. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_basic.py +0 -0
  116. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_check.py +0 -0
  117. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_ci_integration.py +0 -0
  118. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_ci_validation.py +0 -0
  119. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_config_validation.py +0 -0
  120. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_constants.py +0 -0
  121. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_corner_cases.py +0 -0
  122. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_deprecation.py +0 -0
  123. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_e2e.py +0 -0
  124. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_formatters.py +0 -0
  125. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_git_fetch.py +0 -0
  126. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_pre_commit.py +0 -0
  127. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_project.py +0 -0
  128. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_scaffold.py +0 -0
  129. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_serialization.py +0 -0
  130. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_toml_operations.py +0 -0
  131. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_url_handling.py +0 -0
  132. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/test_whitespace.py +0 -0
  133. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/w_ruff_sync_cfg/pyproject.toml +0 -0
  134. {ruff_sync-0.1.5.dev1 → ruff_sync-0.1.5.dev2}/tests/wo_ruff_cfg/pyproject.toml +0 -0
  135. {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
- ## Core Workflows
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:
@@ -10,10 +10,13 @@ repo_name: [GITHUB_USER]/[REPO_NAME]
10
10
  extra:
11
11
  version:
12
12
  provider: mike
13
- default: latest
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.