1pub mod diff_view;
2pub mod file_tree;
3pub mod preview_view;
4pub mod review_view;
5pub mod summary;
6
7use crate::app::{App, InputMode};
8use crate::theme::Theme;
9use ratatui::layout::{Constraint, Layout, Rect};
10use ratatui::style::{Modifier, Style};
11use ratatui::text::{Line, Span};
12use ratatui::widgets::{Block, Clear, Paragraph, Wrap};
13use ratatui::Frame;
14
15pub fn draw(app: &App, frame: &mut Frame) -> Vec<preview_view::PendingImage> {
18 let area = frame.area();
19 let mut pending_images = Vec::new();
20
21 let bottom_height = 1;
23 let vertical =
24 Layout::vertical([Constraint::Min(1), Constraint::Length(bottom_height)]).split(area);
25
26 let sidebar_width = if area.width < 80 {
28 Constraint::Max(25)
29 } else {
30 Constraint::Max(40)
31 };
32 let horizontal =
33 Layout::horizontal([sidebar_width, Constraint::Min(40)]).split(vertical[0]);
34
35 file_tree::render_tree(app, frame, horizontal[0]);
37
38 if app.active_review_group.is_some() {
40 review_view::render_review_with_diff(app, frame, horizontal[1]);
41 } else if app.preview_mode && preview_view::is_current_file_markdown(app) {
42 pending_images = preview_view::render_preview(app, frame, horizontal[1]);
43 } else {
44 diff_view::render_diff(app, frame, horizontal[1]);
45 }
46
47 match app.input_mode {
49 InputMode::Search => render_search_bar(app, frame, vertical[1]),
50 InputMode::Normal | InputMode::Help | InputMode::Settings => {
51 summary::render_summary(app, frame, vertical[1])
52 }
53 }
54
55 if app.input_mode == InputMode::Help {
57 render_help_overlay(frame, area, &app.theme);
58 }
59
60 if app.input_mode == InputMode::Settings {
62 render_settings_overlay(frame, area, &app.theme);
63 }
64
65 pending_images
66}
67
68fn render_help_overlay(frame: &mut Frame, area: Rect, theme: &Theme) {
70 let shortcuts = vec![
71 ("Navigation", vec![
72 ("j/k, ↑/↓", "Move up/down"),
73 ("g/G", "Jump to top/bottom"),
74 ("Ctrl-d/u", "Half-page down/up"),
75 ("Tab", "Switch sidebar/diff focus"),
76 ]),
77 ("Actions", vec![
78 ("Enter", "Sidebar: select file/group | Diff: toggle collapse"),
79 ("p", "Toggle markdown preview (.md files)"),
80 ("/", "Search files"),
81 ("n/N", "Next/prev search match"),
82 (",", "Settings"),
83 ("Esc", "Clear filter / quit"),
84 ("q", "Quit"),
85 ]),
86 ("Review (on group)", vec![
87 ("R", "Force-refresh review"),
88 ("Esc", "Close review pane"),
89 ]),
90 ];
91
92 let mut lines: Vec<Line> = vec![Line::raw("")];
93 for (section, keys) in &shortcuts {
94 lines.push(Line::from(Span::styled(
95 format!(" {section}"),
96 Style::default()
97 .fg(theme.help_section_fg)
98 .add_modifier(Modifier::BOLD),
99 )));
100 for (key, desc) in keys {
101 lines.push(Line::from(vec![
102 Span::styled(
103 format!(" {key:<14}"),
104 Style::default()
105 .fg(theme.help_key_fg)
106 .add_modifier(Modifier::BOLD),
107 ),
108 Span::styled(*desc, Style::default().fg(theme.help_text_fg)),
109 ]));
110 }
111 lines.push(Line::raw(""));
112 }
113 lines.push(Line::from(Span::styled(
114 " Press any key to close",
115 Style::default().fg(theme.help_dismiss_fg),
116 )));
117
118 let content_width = lines.iter().map(|l| l.spans.iter().map(|s| s.content.chars().count()).sum::<usize>()).max().unwrap_or(0) as u16;
119 let height = (lines.len() + 2).min(area.height as usize) as u16;
120 let width = (content_width + 4).min(area.width.saturating_sub(4)); let x = (area.width.saturating_sub(width)) / 2;
122 let y = (area.height.saturating_sub(height)) / 2;
123 let popup_area = Rect::new(x, y, width, height);
124
125 frame.render_widget(Clear, popup_area);
126 let block = Block::bordered()
127 .title(" Shortcuts ")
128 .border_style(Style::default().fg(theme.help_section_fg))
129 .style(Style::default().bg(theme.help_overlay_bg));
130 let paragraph = Paragraph::new(lines).block(block);
131 frame.render_widget(paragraph, popup_area);
132}
133
134fn render_settings_overlay(frame: &mut Frame, area: Rect, theme: &Theme) {
136 let current_mode = if theme.syntect_theme.contains("dark") {
137 "Dark"
138 } else {
139 "Light"
140 };
141
142 let mut lines: Vec<Line> = vec![Line::raw("")];
143
144 lines.push(Line::from(Span::styled(
146 " Theme",
147 Style::default()
148 .fg(theme.help_section_fg)
149 .add_modifier(Modifier::BOLD),
150 )));
151 lines.push(Line::from(vec![
152 Span::styled(
153 format!(" {:<14}", "d"),
154 Style::default()
155 .fg(theme.help_key_fg)
156 .add_modifier(Modifier::BOLD),
157 ),
158 Span::styled(
159 format!("Toggle dark/light mode [Current: {current_mode}]"),
160 Style::default().fg(theme.help_text_fg),
161 ),
162 ]));
163 lines.push(Line::raw(""));
164
165 lines.push(Line::from(Span::styled(
166 " Esc to close",
167 Style::default().fg(theme.help_dismiss_fg),
168 )));
169
170 let content_width = lines.iter().map(|l| l.spans.iter().map(|s| s.content.chars().count()).sum::<usize>()).max().unwrap_or(0) as u16;
171 let width = (content_width + 4).min(area.width.saturating_sub(4));
172 let height = (lines.len() + 2).min(area.height as usize) as u16;
173 let x = (area.width.saturating_sub(width)) / 2;
174 let y = (area.height.saturating_sub(height)) / 2;
175 let popup_area = Rect::new(x, y, width, height);
176
177 frame.render_widget(Clear, popup_area);
178 let block = Block::bordered()
179 .title(" Settings ")
180 .border_style(Style::default().fg(theme.help_section_fg))
181 .style(Style::default().bg(theme.help_overlay_bg));
182 let paragraph = Paragraph::new(lines).block(block).wrap(Wrap { trim: false });
183 frame.render_widget(paragraph, popup_area);
184}
185
186fn render_search_bar(app: &App, frame: &mut Frame, area: ratatui::layout::Rect) {
188 let line = Line::from(vec![
189 Span::styled(
190 "/ ",
191 Style::default()
192 .fg(app.theme.help_key_fg)
193 .add_modifier(Modifier::BOLD),
194 ),
195 Span::styled(
196 app.search_query.clone(),
197 Style::default().fg(app.theme.help_text_fg),
198 ),
199 Span::styled(
200 "_",
201 Style::default()
202 .fg(app.theme.help_text_fg)
203 .add_modifier(Modifier::SLOW_BLINK),
204 ),
205 ]);
206 let paragraph = ratatui::widgets::Paragraph::new(line);
207 frame.render_widget(paragraph, area);
208}