Skip to main content

gitkraft_tui/features/options/
view.rs

1use ratatui::layout::{Constraint, Direction, Layout, Rect};
2use ratatui::style::{Modifier, Style};
3use ratatui::text::{Line, Span};
4use ratatui::widgets::{Block, Borders, Padding, Paragraph};
5use ratatui::Frame;
6
7use crate::app::App;
8use crate::utils::pad_right;
9
10/// Render the options panel with grouped sections in bordered inner blocks,
11/// matching the tui-file-explorer options style.
12pub fn render(app: &App, frame: &mut Frame, area: Rect) {
13    let theme = app.theme();
14
15    let key_style = Style::default()
16        .fg(theme.warning)
17        .add_modifier(Modifier::BOLD);
18    let desc_style = Style::default().fg(theme.text_primary);
19    let value_style = Style::default()
20        .fg(theme.accent)
21        .add_modifier(Modifier::BOLD);
22    let section_title_style = Style::default().fg(theme.text_muted);
23    let muted_style = Style::default().fg(theme.text_muted);
24
25    // ── Outer block ───────────────────────────────────────────────────────
26    let outer_block = Block::default()
27        .title(Line::from(vec![
28            Span::styled("⚙ ", Style::default().fg(theme.accent)),
29            Span::styled(
30                "Options",
31                Style::default()
32                    .fg(theme.accent)
33                    .add_modifier(Modifier::BOLD),
34            ),
35        ]))
36        .borders(Borders::ALL)
37        .border_style(Style::default().fg(theme.border_active))
38        .style(Style::default().bg(theme.bg))
39        .padding(Padding::new(1, 1, 0, 0));
40
41    let inner_area = outer_block.inner(area);
42    frame.render_widget(outer_block, area);
43
44    // ── Close hint ────────────────────────────────────────────────────────
45    let close_line = Paragraph::new(Line::from(vec![
46        Span::styled("Shift + O", key_style),
47        Span::styled(" close", desc_style),
48    ]));
49
50    // ── Layout: close hint, then sections ─────────────────────────────────
51    let sections = Layout::default()
52        .direction(Direction::Vertical)
53        .constraints([
54            Constraint::Length(1),  // close hint
55            Constraint::Length(1),  // spacer
56            Constraint::Length(9),  // Settings section
57            Constraint::Length(1),  // spacer
58            Constraint::Length(8),  // Navigation section
59            Constraint::Length(1),  // spacer
60            Constraint::Length(8),  // Staging section
61            Constraint::Length(1),  // spacer
62            Constraint::Length(10), // Git section
63            Constraint::Length(1),  // spacer
64            Constraint::Length(7),  // Branch Actions section
65            Constraint::Length(1),  // spacer
66            Constraint::Length(10), // Commit Actions section
67            Constraint::Min(0),     // remaining
68        ])
69        .split(inner_area);
70
71    frame.render_widget(close_line, sections[0]);
72
73    // ── Settings section ──────────────────────────────────────────────────
74    {
75        let block = Block::default()
76            .title(Span::styled(" Settings ", section_title_style))
77            .borders(Borders::ALL)
78            .border_style(Style::default().fg(theme.border_inactive))
79            .style(Style::default().bg(theme.bg))
80            .padding(Padding::new(1, 1, 0, 0));
81
82        let theme_name = app.current_theme_name();
83        let editor_name = app.editor.to_string();
84        let last_repo = app
85            .tab()
86            .repo_path
87            .as_ref()
88            .and_then(|p| p.file_name())
89            .map(|n| n.to_string_lossy().to_string())
90            .unwrap_or_else(|| "none".to_string());
91
92        let on_style = Style::default()
93            .fg(theme.success)
94            .add_modifier(Modifier::BOLD);
95
96        let settings_path = gitkraft_core::features::persistence::ops::tui_settings_json_path()
97            .ok()
98            .map(|p| p.display().to_string())
99            .unwrap_or_else(|| "~/.config/gitkraft/tui-settings.json".to_string());
100
101        let lines = vec![
102            Line::from(vec![
103                Span::styled(pad_right("t", 14), key_style),
104                Span::styled(pad_right("next theme", 16), desc_style),
105                Span::styled(theme_name, value_style),
106            ]),
107            Line::from(vec![
108                Span::styled(pad_right("Shift + T", 14), key_style),
109                Span::styled(pad_right("theme picker", 16), desc_style),
110                Span::styled("panel", value_style),
111            ]),
112            Line::from(vec![
113                Span::styled(pad_right("Shift + E", 14), key_style),
114                Span::styled(pad_right("editor", 16), desc_style),
115                Span::styled(editor_name, value_style),
116            ]),
117            Line::from(vec![
118                Span::styled(pad_right(",", 14), key_style),
119                Span::styled(pad_right("edit settings", 16), desc_style),
120                Span::styled(settings_path, value_style),
121            ]),
122            Line::from(vec![
123                Span::styled(pad_right("commits", 14), muted_style),
124                Span::styled(pad_right("max loaded", 16), desc_style),
125                Span::styled("500", value_style),
126            ]),
127            Line::from(vec![
128                Span::styled(pad_right("repo", 14), muted_style),
129                Span::styled(pad_right("current", 16), desc_style),
130                Span::styled(last_repo, on_style),
131            ]),
132        ];
133
134        let paragraph = Paragraph::new(lines).block(block);
135        frame.render_widget(paragraph, sections[2]);
136    }
137
138    // ── Navigation section ────────────────────────────────────────────────
139    {
140        let block = Block::default()
141            .title(Span::styled(" Navigation ", section_title_style))
142            .borders(Borders::ALL)
143            .border_style(Style::default().fg(theme.border_inactive))
144            .style(Style::default().bg(theme.bg))
145            .padding(Padding::new(1, 1, 0, 0));
146
147        let lines = vec![
148            Line::from(vec![
149                Span::styled(pad_right("Tab", 14), key_style),
150                Span::styled("cycle pane forward", desc_style),
151            ]),
152            Line::from(vec![
153                Span::styled(pad_right("Shift + Tab", 14), key_style),
154                Span::styled("cycle pane backward", desc_style),
155            ]),
156            Line::from(vec![
157                Span::styled(pad_right("j / ↓", 14), key_style),
158                Span::styled("next item", desc_style),
159            ]),
160            Line::from(vec![
161                Span::styled(pad_right("k / ↑", 14), key_style),
162                Span::styled("previous item", desc_style),
163            ]),
164            Line::from(vec![
165                Span::styled(pad_right("Enter", 14), key_style),
166                Span::styled("select / activate", desc_style),
167            ]),
168            Line::from(vec![
169                Span::styled(pad_right("Esc", 14), key_style),
170                Span::styled("dismiss / cancel", desc_style),
171            ]),
172        ];
173
174        let paragraph = Paragraph::new(lines).block(block);
175        frame.render_widget(paragraph, sections[4]);
176    }
177
178    // ── Staging section ───────────────────────────────────────────────────
179    {
180        let block = Block::default()
181            .title(Span::styled(" Staging ", section_title_style))
182            .borders(Borders::ALL)
183            .border_style(Style::default().fg(theme.border_inactive))
184            .style(Style::default().bg(theme.bg))
185            .padding(Padding::new(1, 1, 0, 0));
186
187        let lines = vec![
188            Line::from(vec![
189                Span::styled(pad_right("s", 14), key_style),
190                Span::styled(pad_right("stage file", 18), desc_style),
191                Span::styled("selected", value_style),
192            ]),
193            Line::from(vec![
194                Span::styled(pad_right("u", 14), key_style),
195                Span::styled(pad_right("unstage file", 18), desc_style),
196                Span::styled("selected", value_style),
197            ]),
198            Line::from(vec![
199                Span::styled(pad_right("S", 14), key_style),
200                Span::styled(pad_right("stage all", 18), desc_style),
201                Span::styled("all files", value_style),
202            ]),
203            Line::from(vec![
204                Span::styled(pad_right("U", 14), key_style),
205                Span::styled(pad_right("unstage all", 18), desc_style),
206                Span::styled("all files", value_style),
207            ]),
208            Line::from(vec![
209                Span::styled(pad_right("d", 14), key_style),
210                Span::styled(pad_right("discard", 18), desc_style),
211                Span::styled("confirm ×2", value_style),
212            ]),
213            Line::from(vec![
214                Span::styled(pad_right("Tab", 14), key_style),
215                Span::styled("toggle focus", desc_style),
216            ]),
217        ];
218
219        let paragraph = Paragraph::new(lines).block(block);
220        frame.render_widget(paragraph, sections[6]);
221    }
222
223    // ── Git section ───────────────────────────────────────────────────────
224    {
225        let block = Block::default()
226            .title(Span::styled(" Git ", section_title_style))
227            .borders(Borders::ALL)
228            .border_style(Style::default().fg(theme.border_inactive))
229            .style(Style::default().bg(theme.bg))
230            .padding(Padding::new(1, 1, 0, 0));
231
232        let lines = vec![
233            Line::from(vec![
234                Span::styled(pad_right("c", 14), key_style),
235                Span::styled(pad_right("commit", 18), desc_style),
236                Span::styled("staged changes", value_style),
237            ]),
238            Line::from(vec![
239                Span::styled(pad_right("b", 14), key_style),
240                Span::styled(pad_right("create branch", 18), desc_style),
241                Span::styled("from HEAD", value_style),
242            ]),
243            Line::from(vec![
244                Span::styled(pad_right("z", 14), key_style),
245                Span::styled(pad_right("stash save", 18), desc_style),
246                Span::styled("working dir", value_style),
247            ]),
248            Line::from(vec![
249                Span::styled(pad_right("Z", 14), key_style),
250                Span::styled(pad_right("stash pop", 18), desc_style),
251                Span::styled("latest", value_style),
252            ]),
253            Line::from(vec![
254                Span::styled(pad_right("f", 14), key_style),
255                Span::styled(pad_right("fetch", 18), desc_style),
256                Span::styled("from origin", value_style),
257            ]),
258            Line::from(vec![
259                Span::styled(pad_right("p", 14), key_style),
260                Span::styled(pad_right("pull (rebase)", 18), desc_style),
261                Span::styled("from origin", value_style),
262            ]),
263            Line::from(vec![
264                Span::styled(pad_right("P", 14), key_style),
265                Span::styled(pad_right("push", 18), desc_style),
266                Span::styled("to origin", value_style),
267            ]),
268            Line::from(vec![
269                Span::styled(pad_right("F", 14), key_style),
270                Span::styled(pad_right("force push", 18), desc_style),
271                Span::styled("--force-with-lease", value_style),
272            ]),
273        ];
274
275        let paragraph = Paragraph::new(lines).block(block);
276        frame.render_widget(paragraph, sections[8]);
277    }
278
279    // ── Branch Actions section ────────────────────────────────────────
280    {
281        let block = Block::default()
282            .title(Span::styled(" Branch Actions ", section_title_style))
283            .borders(Borders::ALL)
284            .border_style(Style::default().fg(theme.border_inactive))
285            .style(Style::default().bg(theme.bg))
286            .padding(Padding::new(1, 1, 0, 0));
287
288        let lines = vec![
289            Line::from(vec![
290                Span::styled(pad_right("Enter", 14), key_style),
291                Span::styled(pad_right("checkout", 18), desc_style),
292                Span::styled("selected branch", value_style),
293            ]),
294            Line::from(vec![
295                Span::styled(pad_right("m", 14), key_style),
296                Span::styled(pad_right("merge", 18), desc_style),
297                Span::styled("selected → HEAD", value_style),
298            ]),
299            Line::from(vec![
300                Span::styled(pad_right("R", 14), key_style),
301                Span::styled(pad_right("rebase onto", 18), desc_style),
302                Span::styled("selected branch", value_style),
303            ]),
304            Line::from(vec![
305                Span::styled(pad_right("D", 14), key_style),
306                Span::styled(pad_right("delete branch", 18), desc_style),
307                Span::styled("selected", value_style),
308            ]),
309        ];
310
311        let paragraph = Paragraph::new(lines).block(block);
312        frame.render_widget(paragraph, sections[10]);
313    }
314
315    // ── Commit Actions section ────────────────────────────────────────
316    {
317        let block = Block::default()
318            .title(Span::styled(" Commit Actions ", section_title_style))
319            .borders(Borders::ALL)
320            .border_style(Style::default().fg(theme.border_inactive))
321            .style(Style::default().bg(theme.bg))
322            .padding(Padding::new(1, 1, 0, 0));
323
324        let lines = vec![
325            Line::from(vec![
326                Span::styled(pad_right("C", 14), key_style),
327                Span::styled(pad_right("cherry-pick", 18), desc_style),
328                Span::styled("onto current", value_style),
329            ]),
330            Line::from(vec![
331                Span::styled(pad_right("n", 14), key_style),
332                Span::styled(pad_right("reset mixed", 18), desc_style),
333                Span::styled("keep files", value_style),
334            ]),
335            Line::from(vec![
336                Span::styled(pad_right("e", 14), key_style),
337                Span::styled(pad_right("revert", 18), desc_style),
338                Span::styled("selected commit", value_style),
339            ]),
340            Line::from(vec![
341                Span::styled(pad_right("x", 14), key_style),
342                Span::styled(pad_right("reset soft", 18), desc_style),
343                Span::styled("to selected", value_style),
344            ]),
345            Line::from(vec![
346                Span::styled(pad_right("X", 14), key_style),
347                Span::styled(pad_right("reset hard", 18), desc_style),
348                Span::styled("to selected", value_style),
349            ]),
350            Line::from(vec![
351                Span::styled(pad_right("a", 14), key_style),
352                Span::styled(pad_right("all actions…", 18), desc_style),
353                Span::styled("popup menu", value_style),
354            ]),
355        ];
356
357        let paragraph = Paragraph::new(lines).block(block);
358        frame.render_widget(paragraph, sections[12]);
359    }
360}