Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
81e87e8
tui
Kilo59 Apr 4, 2026
cf836ed
ruff-inspect entrypoint
Kilo59 Apr 4, 2026
f7d2f77
some tests
Kilo59 Apr 4, 2026
940cd39
refactor(tui): address review feedback and harden configuration loading
Kilo59 Apr 4, 2026
4900e2f
textual dev dependencies
Kilo59 Apr 4, 2026
20c64ba
test optional dependencies are optional
Kilo59 Apr 4, 2026
365adaf
add warnings on version docs
Kilo59 Apr 4, 2026
1ecd848
don't use uv
Kilo59 Apr 4, 2026
58a0a11
display better UI hints
Kilo59 Apr 4, 2026
aad541a
better content panel
Kilo59 Apr 4, 2026
59f1b17
update design
Kilo59 Apr 4, 2026
2b3a7d1
update design
Kilo59 Apr 4, 2026
d47e75b
show config docs
Kilo59 Apr 4, 2026
0476750
strip whitespace and make content panel bigger
Kilo59 Apr 4, 2026
46b277b
make rule inspector full height
Kilo59 Apr 4, 2026
b0b4b83
update design
Kilo59 Apr 4, 2026
4ecbba3
commit better browsing enhancements
Kilo59 Apr 4, 2026
984ab67
widget skill
Kilo59 Apr 4, 2026
84f4abf
design
Kilo59 Apr 4, 2026
6e9f42d
add rule status visibility and global search
Kilo59 Apr 4, 2026
f1eb3f5
remove panel hiding
Kilo59 Apr 4, 2026
d66e2a6
effective only display
Kilo59 Apr 4, 2026
8c9c56a
don't show disabled rules
Kilo59 Apr 4, 2026
e6e61cb
max-width of tree browser
Kilo59 Apr 4, 2026
d7ed117
auto-expand
Kilo59 Apr 4, 2026
3f4ee29
enrich rule status visualization and inspection
Kilo59 Apr 4, 2026
bdde1e8
remove status disabled
Kilo59 Apr 4, 2026
276a2ee
add legend
Kilo59 Apr 4, 2026
e8ab77f
copy-content
Kilo59 Apr 5, 2026
e9d4fb1
custom themes
Kilo59 Apr 5, 2026
66e5167
adaptive status colors
Kilo59 Apr 5, 2026
ed73143
fix test
Kilo59 Apr 5, 2026
e9d2d52
tui refinements
Kilo59 Apr 5, 2026
e55533b
refactor dependency error handling
Kilo59 Apr 5, 2026
0a59823
remove extra themes
Kilo59 Apr 5, 2026
5469149
fix tests
Kilo59 Apr 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .agents/skills/mike/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,22 @@ The deployment logic is automated in [.github/workflows/ci.yaml](.github/workflo

### 404 for `versions.json`
- 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.

## Post-Mortem & Known Issues

> [!CAUTION]
> **Current Status**: Documentation versioning is currently **BROKEN** on the live site (`kilo59.github.io/ruff-sync`).

### Failed Repair History
The following fixes have been attempted and **FAILED** to resolve the issue:
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`.
2. **Removing `theme.version`**: Removing the redundant Material 9.x config did not restore the switcher.
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.
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.

### Root Cause Suspicions
- **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.
- **`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.

### Guidance for Future Agents
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.
26 changes: 26 additions & 0 deletions .agents/skills/textual/references/widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,29 @@ def on_key(self, event: events.Key) -> None:

> [!TIP]
> Use `BINDINGS` in your `App` or `Screen` class for most navigation tasks. Textual manages the labels and shortcuts in the `Footer` for you.

### `ModalScreen` & Overlays
Modals are screens with a transparent or dim background that overlay the main app.
```python
from textual.screen import ModalScreen
from textual.app import App

class OmniboxScreen(ModalScreen[str]):
# A modal screen that returns a `str` when dismissed.
def compose(self) -> ComposeResult:
# yield your input/search widgets here
yield Input(placeholder="Search...")

# Dismiss the screen and return data
def on_input_submitted(self, event: Input.Submitted) -> None:
self.dismiss(event.value)

# In the main App or Screen:
def on_key(self, event: events.Key) -> None:
if event.key == "ctrl+p":
self.push_screen(OmniboxScreen(), self.handle_omnibox_result)

def handle_omnibox_result(self, result: str | None) -> None:
if result:
print(f"Selected: {result}")
```
29 changes: 19 additions & 10 deletions .agents/tui_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ Before we build the TUI feature itself, some of the existing codebase should be
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>`).
**Refactoring Task:**
- Create a generalized, safe abstraction for interacting with the `ruff` executable.
- 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.
- Provide typed async functions:
- `async def get_ruff_rule_markdown(rule_code: str) -> str | None`
- `async def get_ruff_config_markdown(setting_path: str) -> str | None`
- These should use `asyncio.create_subprocess_exec` to fetch and return the text. This prevents raw `subprocess` sprawl across TUI widgets.

### B. Configuration Reader Extraction (`src/ruff_sync/config_io.py` or similar)
Right now, discovering and extracting the local `pyproject.toml` is slightly coupled to the lifecycle of pulling from upstreams (`pull()` / `check()` in `core.py`).
Expand Down Expand Up @@ -59,16 +62,21 @@ Right now, discovering and extracting the local `pyproject.toml` is slightly cou
### [NEW] src/ruff_sync/tui/app.py
- Define `RuffSyncApp` subclassing `textual.app.App`.
- **CSS**: Define embedded TCSS.
- Layout: `Horizontal` split with `Tree` (left) sized `1fr`, and a `Vertical` container (right) sized `2fr`. Left side for Navigation, right side for Content & Inspector.
- Layout: `Horizontal` split with `Tree` (left) sized `1fr`, and a `Vertical` container (`#content-pane`) (right) sized `2fr`.
- **Dynamic Layout**:
- `#category-table` takes **40% height** by default.
- `#inspector` (Markdown) takes **60% height** by default.
- When the table is hidden, the `#inspector` uses a `.full-height` class (**100% height**) to fill the content pane.
- Both widgets utilize `overflow-y: auto` for vertical scrolling only when needed.
- **Compose Method**:
```python
def compose(self) -> ComposeResult:
yield Header()
with Horizontal():
yield ConfigTree(id="config-tree")
with Vertical():
with Vertical(id="content-pane"):
yield CategoryTable(id="category-table")
yield RuleInspector(id="inspector", classes="hidden")
yield RuleInspector(id="inspector")
yield Footer()
```
- **Lifecycle (`on_mount`)**:
Expand All @@ -87,9 +95,9 @@ Contains all custom Textual widgets required for this view.
- Automatically clears and updates columns/rows when a tree node is highlighted.

**3. `RuleInspector` (inherits `textual.widgets.Markdown`)**:
- Displays `ruff rule <CODE>` output.
- Features an `async def fetch_and_display(self, rule_code: str)` method.
- **Background Worker**: Uses Textual `@work(thread=True)` to execute our refactored `get_ruff_rule_markdown()` function.
- Displays documentation for both rules (`ruff rule <CODE>`) and settings (`ruff config <SETTING>`).
- Features an `async def fetch_and_display(self, target: str, is_rule: bool = True)` method.
- **Background Worker**: Uses Textual `@work(thread=True)` to execute the appropriate refactored `get_ruff_*_markdown()` function.

---

Expand All @@ -98,10 +106,11 @@ Contains all custom Textual widgets required for this view.
### [MODIFY] src/ruff_sync/tui/app.py (Reactivity)
- Tie widgets together using Textual's event routing (`@on`):
- `@on(Tree.NodeSelected)`:
- If the node is a configuration section (e.g. `lint.isort`), hide the Inspector and populate the `CategoryTable` with settings.
- If the node represents a list of rules (unwrapped `lint.select`), display the active rule list in the table.
- If the node is a configuration section (e.g. `lint.isort`), ensure the `CategoryTable` is visible (remove "hidden" class), remove the `.full-height` class from the inspector, and populate it with settings. It also triggers `inspector.fetch_and_display(section_path, is_rule=False)` to show section-level docs if available.
- If the node represents a rule code, add the "hidden" class to `CategoryTable` to maximize vertical space, add the `.full-height` class to the `RuleInspector`, and display the rule via `fetch_and_display(code, is_rule=True)`.
- `@on(DataTable.RowSelected)`:
- If the focused row represents a Ruff Rule Code (e.g., `RUF012`), reveal the `RuleInspector` widget and call `inspector.fetch_and_display("RUF012")`.
- If the focused row represents a Ruff Rule Code (e.g., `RUF012`), hide the `CategoryTable`, reveal the `RuleInspector` widget, apply `.full-height`, and call `inspector.fetch_and_display("RUF012", is_rule=True)`.
- If the focused row represents a configuration setting, reveal the `RuleInspector` (at default height if table is shown) and call `inspector.fetch_and_display(setting_key, is_rule=False)`.
- Expose related context natively based on selections.

---
Expand Down
12 changes: 8 additions & 4 deletions .agents/tui_requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,19 @@
- Provide visual grouping for rule categories (e.g., `E` for pycodestyle, `F` for Pyflakes, `TC` for flake8-type-checking).
3. **Contextual Inspector & Documentation**
- When a user highlights a specific rule (e.g., `RUF012`), asynchronously execute `ruff rule <CODE>` to fetch and render the official documentation as Markdown.
- Surface related context depending on the selection: highlighting a rule exposes its documentation AND related configuration settings; highlighting a config setting might expose its structural definition or the specific rules it governs.
4. **Fuzzy Search**
- When a user highlights a configuration setting (e.g., `lint.isort.combine-as-imports`), asynchronously execute `ruff config <SETTING>` to fetch its documentation, default value, and type information.
- Surface related context depending on the selection: highlighting a rule exposes its documentation AND related configuration settings; highlighting a config setting (in the Tree or CategoryTable) exposes its definition and usage examples.
4. **Rich Metadata Rendering**
- The Inspector should distinguish between Rule documentation and Setting documentation.
- Setting documentation should clearly call out **Default Value** and **Type** in a dedicated header or sidebar within the inspector.
5. **Fuzzy Search**
- A search bar to quickly locate a specific configuration key or rule code without manual scrolling.

### 2.2 UX / UI Layout Concept
- **Header:** Application title and current local repository path.
- **Left Sidebar (Navigation):** `Tree` widget for configuration categories (`Global`, `Linting`, `Formatting`, `Rule Index`).
- **Center Area (Main Content):** `DataTable` or `ListView` showing the keys and values for the selected category.
- **Right/Bottom Panel (Inspector):** Context-aware inspector. Because `ruff rule <CODE>` output contains proper Markdown, this panel MUST robustly render Markdown (e.g., utilizing Textual's `Markdown` widget) while dynamically adjoining related setting/rule cross-references around it.
- **Center Area (Main Content):** `DataTable` or `ListView` showing the keys and values for the selected category. This panel is dynamically hidden when inspecting dense documentation to maximize reading space.
- **Right/Bottom Panel (Inspector):** Context-aware scrollable inspector (`overflow-y: auto`). Because `ruff rule <CODE>` output contains proper Markdown, this panel MUST robustly render Markdown (e.g., utilizing Textual's `Markdown` widget) while dynamically adjoining related setting/rule cross-references around it.
- **Footer:** Action key bindings (`q` to Quit, `/` to Search, `?` for Help).

---
Expand Down
44 changes: 44 additions & 0 deletions .agents/tui_rule_browsing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Ruff-Sync TUI: Rule Browsing & Discovery Proposals

This document outlines additional convenience features and alternative navigation paradigms for browsing and exploring specific Ruff rules and groups outside the standard TOML hierarchy.

**Related Documents:**
- [TUI Requirements](tui_requirements.md)
- [TUI Technical Design](tui_design.md)
- [Rule Browsing Detailed Design](tui_rule_browsing_design.md)

## Background
While the TOML hierarchy (as established in the core requirements) provides an exact structural representation (e.g., exposing `tool.ruff.lint.select`), it is not always the most intuitive way to discover or understand the net set of rules actively evaluating the project.

The following proposals are designed to augment the current "Read-Only" TUI mode to support rapid interrogation, global searching, and global discovery of Ruff rules.

---

## 1. The "Effective Rules" Flat Table
Instead of exclusively browsing via the TOML tree (`tool.ruff.lint.select`), we introduce a top-level **"Effective Rules"** dashboard.
- **How it works:** A single, sortable `DataTable` that flattens all configuration vectors (`select`, `extend-select`, `ignore`, `per-file-ignores`) into a definitive list.
- **Columns:** `Code` (e.g., F401), `Name` (e.g., unused-import), `Category` (e.g., Pyflakes), and `Status` (Enabled/Ignored).
- **Benefit:** Gives the user a complete, "at-a-glance" ledger of exactly what the linter is checking without having to manually perform the mental math of `select` minus `ignore`.

## 2. Category / Linter Prefix Grouping
Ruff is conceptually built around "Linters" (e.g., `Pyflakes`, `pycodestyle`, `flake8-bugbear`). The TOML configuration often just specifies a prefix like `select = ["E", "F", "B"]`.
- **How it works:** Add a new root node in the `Tree` called **"Linters & Categories"**.
- **Interaction:** Navigating to "flake8-bugbear (B)" displays a table of all `B` rules, instantly showing which ones are locally enabled, ignored, or inactive.
- **Benefit:** Maps directly to how developers usually think about adding new rulesets to their projects.

## 3. Global Fuzzy Command Palette ("Omnibox")
Navigating a tree or a table can be tedious if the user knows exactly what they are looking for.
- **How it works:** A global keybind (e.g., `Ctrl+P` or `/`) that opens a fuzzy search overlay modal.
- **Interaction:** The user types "unused" and the palette immediately surfaces `F401 (unused-import)`, `F841 (unused-variable)`, etc. Hitting Enter drops them directly into the `RuleInspector` for that specific rule, completely bypassing the Tree hierarchy.
- **Benefit:** The fastest possible way to interrogate a specific rule.

## 4. Quick-Filter State Toggles
When inside any view displaying rules (like the flattened table or the prefix grouping), give the user hotkeys to rapidly shift perspectives.
- **How it works:** Add toggles (e.g., `1: All`, `2: Enabled`, `3: Ignored`).
- **Benefit:** If a user is looking at a massive category like `E` (pycodestyle), they can quickly press `3` to filter the table down to ONLY the rules they explicitly ignored, providing an instant audit trail.

## 5. "Rule Registry" / Discovery Mode
Currently, the user only sees rules they explicitly mention in their `pyproject.toml` or have inherited. How do they discover new rules to adopt?
- **How it works:** By executing `ruff rule --all` under the hood, the TUI could populate a "Discovery" tab. This lists every single rule Ruff supports.
- **Visuals:** Rules that are currently enabled in the local project are highlighted or checked off.
- **Benefit:** Huge value-add for configuration exploration. Users can browse new rules they might want to adopt directly inside the TUI without referring back to the Ruff website.
66 changes: 66 additions & 0 deletions .agents/tui_rule_browsing_design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Detailed Implementation Plan: Effective Rules & Omnibox

This document provides exhaustive technical instructions to implement Features 1 (Effective Rules Flat Table) and 3 (Global Fuzzy Command Palette) from the [Rule Browsing Proposals](tui_rule_browsing.md).

---

## 1. Data Access & Subprocess Upgrades
To present a flat list of rules or allow fuzzy searching across all rules, the TUI must have structural knowledge of every rule Ruff supports.

### A. `get_all_ruff_rules() -> list[dict]`
- In `src/ruff_sync/system.py` (or the equivalent subprocess wrapper module), implement an async function that executes `uv run ruff rule --all --output-format json` (or `ruff rule --all --output-format json` depending on the environment context).
- This returns a JSON array of objects representing all rules (fields include `name`, `code`, `linter`, `summary`, etc.).

### B. `get_ruff_linters() -> list[dict]`
- Implement an async function that executes `ruff linter --output-format json`. Returns categories/prefixes (fields include `prefix`, `name`, `categories`).

### C. Active Rule Evaluation Logic
- Create a pure utility function `compute_effective_rules(all_rules: list[dict], toml_config: dict) -> list[dict]`.
- `toml_config` is the unwrapped TOML dictionary.
- The function iterates over `all_rules`. For each rule, it checks the rule's `code` (e.g. `F401`) against the `[tool.ruff.lint]` keys `select`, `ignore`, `extend-select`.
- **Heuristic**: Length-based prefix matching. If `select = ["F"]` and `ignore = ["F401"]`, `F401` matches both. Since `F401` (len 4) is a longer and more specific prefix match than `F` (len 1), `ignore` wins. The rule is marked with `status="Ignored"`. If it was only partially matched in `select` it gets `status="Enabled"`.
- Return an enriched list of dictionaries mimicking the original rules but adding a `status` key.

---

## 2. The "Effective Rules" Flat Table UI

### A. Tree Hierarchy Updates (`src/ruff_sync/tui/widgets.py`)
- Modify the `ConfigTree` (which parses the `pyproject.toml` hierarchy) to inject a synthetic root node at the very top: **"Effective Active Rules"**.

### B. Table Integration (`src/ruff_sync/tui/app.py` & `widgets.py`)
- When the user selects the "Effective Active Rules" tree node, intercept the event in `@on(Tree.NodeSelected)`.
- Make the `CategoryTable` visible and hide the `RuleInspector` initially.
- Clear existing columns and add `["Code", "Name", "Linter", "Status"]`.
- Fetch the enriched rules list from `compute_effective_rules`.
- Iterate the enriched rules, adding them to the `CategoryTable`. Use Textual Rich markup (e.g., `[green]Enabled[/green]`, `[red]Ignored[/red]`) for the Status column.

### C. Row Selection Linkage
- Ensure `@on(DataTable.RowSelected)` correctly parses the Row Data to extract the "Rule Code" (e.g. `F401`).
- Hiding the Table and revealing the `RuleInspector` should fire seamlessly by calling `self.query_one("#inspector").fetch_and_display(rule_code, is_rule=True)`.

---

## 3. The Global Fuzzy "Omnibox" UI

### A. App-Level Keybind (`src/ruff_sync/tui/app.py`)
- In `RuffSyncApp.BINDINGS`, add `("/", "search", "Search Rules")`.
- Add `def action_search(self) -> None:` to intercept the hotkey.

### B. `OmniboxScreen` Widget (`src/ruff_sync/tui/screens.py`)
- Create a module `screens.py` and define `class OmniboxScreen(ModalScreen[str]):`.
- `compose()` should yield an `Input(placeholder="Search rules (e.g. F401, unused)...")` and optionally an initially-empty `OptionList` or `ListView` beneath it for results.
- **TCSS**: Center the `OmniboxScreen` contents vertically and horizontally. Give the main container a distinct background color and border to pop out over the main App.

### C. Fuzzy Search Logic (`@on(Input.Changed)`)
- Read `all_rules` from the background fetching mechanism.
- For every keystroke (`Input.Changed`), perform a simple substring or fuzzy match against both `rule["code"]` and `rule["name"]`.
- Populate the `ListView`/`OptionList` with the top 10-15 matches.

### D. Submission (`@on(Input.Submitted)` or `OptionList.OptionSelected`)
- When the user hits enter on a search result, call `self.dismiss(result_rule_code)`.

### E. App Callback Integration
- In `action_search`, execute `self.push_screen(OmniboxScreen(), self.handle_omnibox_result)`.
- `def handle_omnibox_result(self, rule_code: str | None) -> None:`
- If `rule_code` is provided, act exactly as if a row was selected in the `CategoryTable`: Hide the table, show the inspector via `self.query_one("#inspector").fetch_and_display(rule_code, is_rule=True)`.
19 changes: 19 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,25 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
slug: Kilo59/ruff-sync

test-no-optional-deps:
name: Test without optional dependencies
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install ruff-sync (no extras)
# We install only the base package without 'dev' or 'tui' groups.
run: pip install .

- name: Run optional dependency validation script
run: bash tests/test_minimal_imports.sh

pre-publish:
name: Test package installation
needs: [static-analysis, tests]
Expand Down
Loading
Loading