binocular/ui/preview/
mod.rs1mod plain_text;
2
3use crate::app::{InputMode, Mode};
4use crate::preview::{self, PreviewContent, PreviewSource};
5use ratatui::{
6 layout::Rect,
7 style::{Color, Modifier, Style},
8 text::{Line, Span},
9 widgets::{Block, BorderType, Borders},
10 Frame,
11};
12
13use crate::ui::indicators::mode_indicator;
14use crate::ui::shortcuts::{preview_hints, render_hints_line};
15
16pub struct PreviewView<'a> {
17 pub app_mode: Mode,
18 pub preview_mode: InputMode,
19 pub source: Option<&'a PreviewSource>,
20 pub status_message: Option<(&'a str, std::time::Instant)>,
21 pub command_buffer: Option<&'a str>,
22 pub highlight_line: Option<usize>,
23 pub search_query: &'a str,
24 pub selection_start: Option<(usize, usize)>,
25 pub cursor_line: usize,
26 pub cursor_char: usize,
27 pub scroll: u16,
28 pub scroll_char: u16,
29 pub area_height: u16,
30}
31
32pub fn render_preview(
33 f: &mut Frame,
34 view: &PreviewView<'_>,
35 content: Option<&mut PreviewContent>,
36 area: Rect,
37) {
38 let is_read_only = is_read_only_preview(content.as_deref());
39 let preview_style = if is_read_only {
40 Style::default().fg(Color::Yellow)
41 } else if view.app_mode == Mode::Preview {
42 Style::default().fg(Color::Blue)
43 } else {
44 Style::default()
45 };
46
47 let status_line = build_status_line(view, content.as_deref());
48 let hints = render_hints_line(preview_hints(view.app_mode, view.preview_mode));
49 let preview_block = Block::default()
50 .borders(Borders::ALL)
51 .border_type(BorderType::Rounded)
52 .title(build_preview_title(view.source).centered())
53 .title_bottom(status_line)
54 .title_bottom(hints.right_aligned())
55 .border_style(preview_style);
56
57 f.render_widget(ratatui::widgets::Clear, area);
58
59 match content {
60 Some(PreviewContent::RichText(text_file)) => {
61 preview::rich_text::ui::render_rich_text_preview(
62 f,
63 area,
64 preview_block,
65 text_file,
66 view,
67 );
68 }
69 Some(PreviewContent::Diff(diff)) => {
70 plain_text::render_plain_text_preview(f, area, preview_block, diff.text.clone(), view);
71 }
72 Some(PreviewContent::PlainText(content)) => {
73 plain_text::render_plain_text_preview(f, area, preview_block, content.clone(), view);
74 }
75 Some(PreviewContent::Image(image)) => {
76 preview::image::ui::render_image_preview(f, area, preview_block, image, view);
77 }
78 Some(PreviewContent::Media(media)) => {
79 preview::media::ui::render_media_preview(f, area, preview_block, media, view);
80 }
81 Some(PreviewContent::StructuredLog(lp)) => {
82 let inner = preview_block.inner(area);
83 f.render_widget(preview_block, area);
84 preview::structured_log::ui::render_structured_log(f, inner, lp);
85 }
86 None => {
87 f.render_widget(preview_block, area);
88 }
89 }
90}
91
92fn build_preview_title(source: Option<&PreviewSource>) -> Line<'static> {
93 let Some(source) = source else {
94 return Line::from("Preview");
95 };
96 let title = source.title();
97 Line::from(vec![
98 Span::raw(" "),
99 Span::styled(
100 shorten_path(title.as_ref()),
101 Style::default().add_modifier(Modifier::BOLD),
102 ),
103 Span::raw(" "),
104 ])
105}
106
107fn shorten_path(path: &str) -> String {
109 use std::path::Path;
110 let stripped = path.strip_prefix("./").unwrap_or(path);
111 let p = Path::new(stripped);
112 let name = match p.file_name() {
113 Some(n) => n.to_string_lossy().into_owned(),
114 None => return stripped.to_string(),
115 };
116 match p.parent().filter(|par| *par != Path::new("")) {
117 None => name,
118 Some(parent) => {
119 let parent_name = parent
120 .file_name()
121 .map(|n| n.to_string_lossy().into_owned())
122 .unwrap_or_else(|| parent.to_string_lossy().into_owned());
123 format!("{}/{}", parent_name, name)
124 }
125 }
126}
127
128fn build_status_line(view: &PreviewView<'_>, content: Option<&PreviewContent>) -> Line<'static> {
129 if is_read_only_preview(content) {
130 return Line::from(vec![Span::styled(
131 " READ-ONLY ",
132 Style::default().fg(Color::Yellow),
133 )]);
134 }
135
136 if let Some((msg, time)) = view.status_message {
137 if time.elapsed().as_secs() < 3 {
138 return Line::from(vec![Span::styled(
139 format!(" {} ", msg),
140 Style::default().fg(Color::Green),
141 )]);
142 }
143 }
144
145 Line::from(vec![
146 Span::raw(" "),
147 mode_indicator(&view.preview_mode, view.command_buffer),
148 ])
149}
150
151fn is_read_only_preview(content: Option<&PreviewContent>) -> bool {
152 matches!(
153 content,
154 Some(PreviewContent::Diff(_))
155 | Some(PreviewContent::PlainText(_))
156 | Some(PreviewContent::Image(_))
157 | Some(PreviewContent::Media(_))
158 )
159}