Skip to content

Commit 56e49e2

Browse files
Merge pull request #3 from AlbertArakelyan/1-fix-and-ctrlr-keys
1 fix and ctrlr keys
2 parents bbce42a + ed25f04 commit 56e49e2

16 files changed

Lines changed: 909 additions & 34 deletions

File tree

.claude/settings.local.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
"Bash(cd /c/Users/alber/Desktop/forge && cargo check 2>&1)",
55
"Bash(cd /c/Users/alber/Desktop/forge && cargo test 2>&1)",
66
"Bash(echo No tokio-util in lock yet:*)",
7-
"WebFetch(domain:docs.rs)"
7+
"WebFetch(domain:docs.rs)",
8+
"Bash(cd \"C:\\\\Users\\\\alber\\\\Desktop\\\\forge\" && cargo check 2>&1)"
89
]
910
},
11+
"enableAllProjectMcpServers": true,
1012
"enabledMcpjsonServers": [
1113
"context7"
12-
],
13-
"enableAllProjectMcpServers": true
14+
]
1415
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
---
2+
name: tui-sections-focusing-best-practices
3+
description: >
4+
Best practices for implementing focusable sections in a Ratatui/crossterm TUI app.
5+
Use when adding a new focusable widget, wiring a focus cycle, showing focus indicators,
6+
or deciding how keybindings should be scoped to focused sections.
7+
---
8+
9+
# TUI Section Focus — Best Practices
10+
11+
## Core model
12+
13+
Focus is a property of `AppState`, not of individual widgets.
14+
One enum variant per focusable section (e.g. `Sidebar`, `UrlBar`, `TabBar`, `Editor`, `ResponseViewer`).
15+
16+
```rust
17+
#[derive(Debug, Clone, PartialEq, Eq, Default)]
18+
pub enum Focus {
19+
Sidebar,
20+
#[default]
21+
UrlBar,
22+
TabBar,
23+
Editor,
24+
ResponseViewer,
25+
}
26+
```
27+
28+
`AppState.focus: Focus` is the single source of truth.
29+
Widgets read it; they never write it.
30+
31+
---
32+
33+
## Focus cycle
34+
35+
Add `next()` and `prev()` methods to `Focus`.
36+
Document the order in the comment — it's the canonical reference.
37+
38+
```rust
39+
/// Cycle: Sidebar → UrlBar → TabBar → Editor → ResponseViewer → Sidebar
40+
pub fn next(&self) -> Focus { ... }
41+
pub fn prev(&self) -> Focus { ... }
42+
```
43+
44+
- `Tab` calls `self.state.focus = self.state.focus.next()`.
45+
- `BackTab` calls `.prev()`.
46+
- Numeric shortcuts (`1``N`) jump directly to a section.
47+
48+
When inserting a new section, update *both* `next()` and `prev()`, update
49+
the doc comment, and add a direct shortcut if the section is frequently used.
50+
51+
---
52+
53+
## Keybindings scoped to focus
54+
55+
Guard every section-specific keybinding with a focus check.
56+
Use a `if` guard on the match arm — never rely on mode alone.
57+
58+
```rust
59+
KeyCode::Left if self.state.focus == Focus::TabBar => {
60+
self.state.active_tab = self.state.active_tab.prev();
61+
}
62+
KeyCode::Right if self.state.focus == Focus::TabBar => {
63+
self.state.active_tab = self.state.active_tab.next();
64+
}
65+
```
66+
67+
This prevents key collisions between sections that share the same key
68+
(e.g. `Left`/`Right` in UrlBar for cursor vs. TabBar for tab switch).
69+
70+
---
71+
72+
## Visual focus indicator
73+
74+
Each widget is responsible for rendering its own focus state.
75+
Receive `state: &AppState` and check `state.focus == Focus::MySection`.
76+
77+
**Border colour** (most common):
78+
```rust
79+
let border_style = if state.focus == Focus::Editor {
80+
Style::default().fg(ACCENT_BLUE)
81+
} else {
82+
Style::default().fg(BORDER_INACTIVE)
83+
};
84+
Block::default().borders(Borders::ALL).border_style(border_style)
85+
```
86+
87+
**Bracket label** (for tab bars / inline items):
88+
```rust
89+
let label = if is_active && state.focus == Focus::TabBar {
90+
format!("[{name}]")
91+
} else {
92+
name.to_string()
93+
};
94+
```
95+
96+
Avoid flashing: indicator should be based entirely on `state.focus` — no
97+
animation or timer needed.
98+
99+
---
100+
101+
## Sub-selection within a section
102+
103+
When a section contains multiple selectable items (tabs, list rows, etc.),
104+
store the selection in `AppState` alongside `focus`.
105+
106+
```rust
107+
pub active_tab: ActiveTab, // which tab is selected
108+
pub focus: Focus, // which section owns keyboard input
109+
```
110+
111+
Provide `next()`/`prev()` on the sub-selection enum too.
112+
Switch sub-selection only when `focus == Focus::ThatSection`.
113+
114+
---
115+
116+
## Dirty flag discipline
117+
118+
Set `AppState.dirty = true` only when visible state actually changes.
119+
120+
- Key events: set dirty in the event dispatcher (always safe).
121+
- Sub-selection change: already covered by the key event path.
122+
- Focus change: covered by Tab/BackTab handler.
123+
- Tick: set dirty **only** during loading spinner (`RequestStatus::Loading`).
124+
125+
---
126+
127+
## Checklist for a new focusable section
128+
129+
- [ ] Add variant to `Focus` enum
130+
- [ ] Update `Focus::next()` and `Focus::prev()`
131+
- [ ] Update doc comment on `next()`
132+
- [ ] Add direct numeric shortcut in `handle_normal_key()` if needed
133+
- [ ] Render focus indicator inside the widget's `render()` function
134+
- [ ] Guard section-specific keybindings with `focus == Focus::MySection`
135+
- [ ] If section has sub-selection: add sub-enum + `next()`/`prev()` + store in `AppState`

.idea/indexLayout.xml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/projectSettingsUpdater.xml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)