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
10pub 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 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 let close_line = Paragraph::new(Line::from(vec![
46 Span::styled("Shift + O", key_style),
47 Span::styled(" close", desc_style),
48 ]));
49
50 let sections = Layout::default()
52 .direction(Direction::Vertical)
53 .constraints([
54 Constraint::Length(1), Constraint::Length(1), Constraint::Length(9), Constraint::Length(1), Constraint::Length(8), Constraint::Length(1), Constraint::Length(8), Constraint::Length(1), Constraint::Length(10), Constraint::Length(1), Constraint::Length(7), Constraint::Length(1), Constraint::Length(10), Constraint::Min(0), ])
69 .split(inner_area);
70
71 frame.render_widget(close_line, sections[0]);
72
73 {
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 {
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 {
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 {
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 {
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 {
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}