Skip to main content

semantic_diff/ui/
mod.rs

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};
12use ratatui::Frame;
13
14/// Draw the entire UI. Returns pending images that must be flushed after
15/// terminal.draw() completes (image protocols bypass ratatui's buffer).
16pub 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    // Vertical split: main content area | bottom bar
21    let bottom_height = 1;
22    let vertical =
23        Layout::vertical([Constraint::Min(1), Constraint::Length(bottom_height)]).split(area);
24
25    // Horizontal split: sidebar | diff view
26    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    // Render file tree sidebar in left panel
35    file_tree::render_tree(app, frame, horizontal[0]);
36
37    // Render diff view or preview in right panel
38    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    // Render bottom bar
45    match app.input_mode {
46        InputMode::Search => render_search_bar(app, frame, vertical[1]),
47        InputMode::Normal | InputMode::Help => summary::render_summary(app, frame, vertical[1]),
48    }
49
50    // Render help overlay on top if in Help mode
51    if app.input_mode == InputMode::Help {
52        render_help_overlay(frame, area, &app.theme);
53    }
54
55    pending_images
56}
57
58/// Render the help overlay centered on screen.
59fn render_help_overlay(frame: &mut Frame, area: Rect, theme: &Theme) {
60    let shortcuts = vec![
61        ("Navigation", vec![
62            ("j/k, ↑/↓", "Move up/down"),
63            ("g/G", "Jump to top/bottom"),
64            ("Ctrl-d/u", "Half-page down/up"),
65            ("Tab", "Switch sidebar/diff focus"),
66        ]),
67        ("Actions", vec![
68            ("Enter", "Sidebar: select file/group | Diff: toggle collapse"),
69            ("p", "Toggle markdown preview (.md files)"),
70            ("/", "Search files"),
71            ("n/N", "Next/prev search match"),
72            ("Esc", "Clear filter / quit"),
73            ("q", "Quit"),
74        ]),
75    ];
76
77    let mut lines: Vec<Line> = vec![Line::raw("")];
78    for (section, keys) in &shortcuts {
79        lines.push(Line::from(Span::styled(
80            format!("  {section}"),
81            Style::default()
82                .fg(theme.help_section_fg)
83                .add_modifier(Modifier::BOLD),
84        )));
85        for (key, desc) in keys {
86            lines.push(Line::from(vec![
87                Span::styled(
88                    format!("    {key:<14}"),
89                    Style::default()
90                        .fg(theme.help_key_fg)
91                        .add_modifier(Modifier::BOLD),
92                ),
93                Span::styled(*desc, Style::default().fg(theme.help_text_fg)),
94            ]));
95        }
96        lines.push(Line::raw(""));
97    }
98    lines.push(Line::from(Span::styled(
99        "  Press any key to close",
100        Style::default().fg(theme.help_dismiss_fg),
101    )));
102
103    let height = (lines.len() + 2).min(area.height as usize) as u16;
104    let width = 50u16.min(area.width.saturating_sub(4));
105    let x = (area.width.saturating_sub(width)) / 2;
106    let y = (area.height.saturating_sub(height)) / 2;
107    let popup_area = Rect::new(x, y, width, height);
108
109    frame.render_widget(Clear, popup_area);
110    let block = Block::bordered()
111        .title(" Shortcuts ")
112        .border_style(Style::default().fg(theme.help_section_fg))
113        .style(Style::default().bg(theme.help_overlay_bg));
114    let paragraph = Paragraph::new(lines).block(block);
115    frame.render_widget(paragraph, popup_area);
116}
117
118/// Render the search input bar at the bottom of the screen.
119fn render_search_bar(app: &App, frame: &mut Frame, area: ratatui::layout::Rect) {
120    let line = Line::from(vec![
121        Span::styled(
122            "/ ",
123            Style::default()
124                .fg(app.theme.help_key_fg)
125                .add_modifier(Modifier::BOLD),
126        ),
127        Span::styled(
128            app.search_query.clone(),
129            Style::default().fg(app.theme.help_text_fg),
130        ),
131        Span::styled(
132            "_",
133            Style::default()
134                .fg(app.theme.help_text_fg)
135                .add_modifier(Modifier::SLOW_BLINK),
136        ),
137    ]);
138    let paragraph = ratatui::widgets::Paragraph::new(line);
139    frame.render_widget(paragraph, area);
140}