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