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