1use std::borrow::Cow;
2use std::sync::Arc;
3
4use crossterm::event::KeyEvent;
5use pito_footer::{Input, InputBar};
6use pito_list::{Cell, Column, Key, Keys, List, ListView, Row, Source};
7use ratatui::{
8 Frame,
9 buffer::Buffer,
10 layout::{Alignment, Rect},
11 text::Line,
12 widgets::Paragraph,
13};
14
15use crate::filter::matches;
16use crate::palette::Palette;
17
18pub type Lines = Arc<Vec<Line<'static>>>;
19
20const SEARCH: &[Key] = &[Key::Char('/')];
21const NEXT: &[Key] = &[Key::Char('n')];
22const PREVIOUS: &[Key] = &[Key::Char('N')];
23const COPY: &[Key] = &[Key::Char('y')];
24const ALL: &[Key] = &[Key::Char('Y')];
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27#[non_exhaustive]
28pub enum Turn {
29 Pass,
30 Taken,
31 Copy(String),
32}
33
34#[derive(Debug, Clone)]
35pub struct Log {
36 lines: Lines,
37 top: usize,
38 page: usize,
39 input: Input,
40 typing: bool,
41 hits: Vec<usize>,
42 hit: Option<usize>,
43 follow: bool,
44 label: Cow<'static, str>,
45 placeholder: Cow<'static, str>,
46 hint: Cow<'static, str>,
47 keys: Keys,
48 search: &'static [Key],
49 next: &'static [Key],
50 previous: &'static [Key],
51 copy: &'static [Key],
52 all: &'static [Key],
53}
54
55fn plain(line: &Line) -> String {
56 line.spans
57 .iter()
58 .map(|span| span.content.as_ref())
59 .collect()
60}
61
62impl Log {
63 pub fn new(label: impl Into<Cow<'static, str>>) -> Self {
64 Log {
65 lines: Arc::new(Vec::new()),
66 top: 0,
67 page: 1,
68 input: Input::new(),
69 typing: false,
70 hits: Vec::new(),
71 hit: None,
72 follow: true,
73 label: label.into(),
74 placeholder: Cow::Borrowed(""),
75 hint: Cow::Borrowed(""),
76 keys: Keys::VIM,
77 search: SEARCH,
78 next: NEXT,
79 previous: PREVIOUS,
80 copy: COPY,
81 all: ALL,
82 }
83 }
84
85 pub fn placeholder(mut self, text: impl Into<Cow<'static, str>>) -> Self {
86 self.placeholder = text.into();
87 self
88 }
89
90 pub fn hint(mut self, text: impl Into<Cow<'static, str>>) -> Self {
91 self.hint = text.into();
92 self
93 }
94
95 pub fn follow(mut self, follow: bool) -> Self {
96 self.follow = follow;
97 self
98 }
99
100 pub fn keys(mut self, keys: Keys) -> Self {
101 self.keys = keys;
102 self
103 }
104
105 pub fn search_keys(mut self, keys: &'static [Key]) -> Self {
106 self.search = keys;
107 self
108 }
109
110 pub fn hit_keys(mut self, next: &'static [Key], previous: &'static [Key]) -> Self {
111 self.next = next;
112 self.previous = previous;
113 self
114 }
115
116 pub fn copy_keys(mut self, page: &'static [Key], all: &'static [Key]) -> Self {
117 self.copy = page;
118 self.all = all;
119 self
120 }
121
122 pub fn lines(&self) -> &Lines {
123 &self.lines
124 }
125
126 pub fn set_lines(&mut self, lines: Lines) {
127 if Arc::ptr_eq(&self.lines, &lines) {
128 return;
129 }
130 let following = self.follow && self.top >= self.last();
131 self.lines = lines;
132 if following && self.top > 0 {
133 self.top = self.last();
134 }
135 self.top = self.top.min(self.last());
136 self.find(false);
137 }
138
139 pub fn len(&self) -> usize {
140 self.lines.len()
141 }
142
143 pub fn is_empty(&self) -> bool {
144 self.lines.is_empty()
145 }
146
147 pub fn top(&self) -> usize {
148 self.top
149 }
150
151 pub fn hits(&self) -> usize {
152 self.hits.len()
153 }
154
155 pub fn hit(&self) -> Option<usize> {
156 self.hit
157 }
158
159 pub fn query(&self) -> &str {
160 self.input.value()
161 }
162
163 pub fn typing(&self) -> bool {
164 self.typing
165 }
166
167 pub fn bar(&self) -> Option<InputBar<'_>> {
168 self.typing.then(|| {
169 InputBar::new(&self.label, &self.input)
170 .placeholder(&self.placeholder)
171 .hint(&self.hint)
172 })
173 }
174
175 fn last(&self) -> usize {
176 self.lines.len().saturating_sub(self.page)
177 }
178
179 fn show(&mut self, line: usize) {
180 if line < self.top || line >= self.top + self.page {
181 self.top = line.saturating_sub(self.page / 3).min(self.last());
182 }
183 }
184
185 fn find(&mut self, jump: bool) {
186 let query = self.input.value().trim().to_string();
187 self.hits = if query.is_empty() {
188 Vec::new()
189 } else {
190 self.lines
191 .iter()
192 .enumerate()
193 .filter(|(_, line)| matches(&plain(line), &query))
194 .map(|(index, _)| index)
195 .collect()
196 };
197 self.hit = None;
198 if jump && let Some(at) = self.hits.iter().position(|line| *line >= self.top) {
199 self.hit = Some(at);
200 self.show(self.hits[at]);
201 }
202 }
203
204 fn step(&mut self, by: isize) {
205 if self.hits.is_empty() {
206 return;
207 }
208 let count = self.hits.len() as isize;
209 let at = match self.hit {
210 Some(at) => (at as isize + by).rem_euclid(count),
211 None if by > 0 => 0,
212 None => count - 1,
213 } as usize;
214 self.hit = Some(at);
215 self.show(self.hits[at]);
216 }
217
218 fn text(&self, lines: &[Line]) -> String {
219 let mut text: Vec<String> = lines.iter().map(|line| plain(line)).collect();
220 for line in &mut text {
221 line.truncate(line.trim_end().len());
222 }
223 text.join("\n")
224 }
225
226 pub fn key(&mut self, event: KeyEvent) -> Turn {
227 let key: Key = event.into();
228 if self.typing {
229 match key {
230 Key::Enter => self.typing = false,
231 Key::Esc => {
232 self.typing = false;
233 self.input.clear();
234 self.find(false);
235 }
236 _ => {
237 let before = self.input.value().to_string();
238 self.input.key(event.into());
239 if self.input.value() != before {
240 self.find(true);
241 }
242 }
243 }
244 return Turn::Taken;
245 }
246 let page = self.page.max(1);
247 if self.keys.up.contains(&key) {
248 self.top = self.top.saturating_sub(1);
249 } else if self.keys.down.contains(&key) {
250 self.top = (self.top + 1).min(self.last());
251 } else if self.keys.page_up.contains(&key) {
252 self.top = self.top.saturating_sub(page);
253 } else if self.keys.page_down.contains(&key) {
254 self.top = (self.top + page).min(self.last());
255 } else if self.keys.first.contains(&key) {
256 self.top = 0;
257 } else if self.keys.last.contains(&key) {
258 self.top = self.last();
259 } else if self.search.contains(&key) {
260 self.typing = true;
261 } else if self.next.contains(&key) {
262 self.step(1);
263 } else if self.previous.contains(&key) {
264 self.step(-1);
265 } else if self.copy.contains(&key) {
266 let end = (self.top + page).min(self.lines.len());
267 return Turn::Copy(self.text(&self.lines[self.top.min(end)..end]));
268 } else if self.all.contains(&key) {
269 return Turn::Copy(self.text(&self.lines));
270 } else {
271 return Turn::Pass;
272 }
273 Turn::Taken
274 }
275
276 pub fn paste(&mut self, text: &str) -> Turn {
277 if !self.typing {
278 return Turn::Pass;
279 }
280 self.input.paste(text);
281 self.find(true);
282 Turn::Taken
283 }
284
285 pub fn draw(&mut self, frame: &mut Frame, area: Rect, palette: &Palette) {
286 let total = self.lines.len();
287 let over = area.height > 0 && total > usize::from(area.height);
288 let cut = over && palette.count.is_some() && area.height > 1;
289 let bar = over && palette.bar.is_some() && area.width > 1;
290 let area = Rect {
291 width: area.width - u16::from(bar),
292 height: area.height - u16::from(cut),
293 ..area
294 };
295 self.page = usize::from(area.height).max(1);
296 self.top = self.top.min(self.last());
297 if bar {
298 let column = Rect::new(area.right(), area.y, 1, area.height);
299 scrollbar(frame.buffer_mut(), column, self.top, total, palette);
300 }
301 if let Some(count) = palette.count.filter(|_| cut) {
302 let last = (self.top + self.page).min(total);
303 let label = count.label(self.top + 1, last, total).to_string();
304 let line = Rect::new(area.x, area.bottom(), area.width + u16::from(bar), 1);
305 let label = Line::styled(label, palette.muted).alignment(Alignment::Right);
306 frame.render_widget(Paragraph::new(label), line);
307 }
308 let current = self.hit.map(|at| self.hits[at]);
309 let end = (self.top + self.page).min(self.lines.len());
310 let shown: Vec<Line> = (self.top..end)
311 .map(|index| {
312 let line = self.lines[index].clone();
313 if Some(index) == current {
314 line.patch_style(palette.selected)
315 } else {
316 line
317 }
318 })
319 .collect();
320 frame.render_widget(Paragraph::new(shown), area);
321 }
322}
323
324struct Blank(usize);
325
326impl Source for Blank {
327 fn len(&self) -> usize {
328 self.0
329 }
330
331 fn row(&self, _index: usize) -> Cow<'_, Row> {
332 Cow::Owned(Row::new(Vec::<Cell>::new()))
333 }
334
335 fn selectable(&self, _index: usize) -> bool {
336 true
337 }
338}
339
340pub(crate) fn scrollbar(
341 buf: &mut Buffer,
342 column: Rect,
343 top: usize,
344 total: usize,
345 palette: &Palette,
346) {
347 if column.is_empty() {
348 return;
349 }
350 let mut list = List::from_source(Blank(total));
351 list.select(top);
352 list.scroll_to(top);
353 let columns = [Column::new("", 0, 0)];
354 let view = ListView::new(&mut list, &columns)
355 .styles(palette.list())
356 .scrollbar(palette.bar)
357 .cursor("");
358 let mut scratch = Buffer::empty(Rect::new(0, 0, 2, column.height));
359 ratatui::widgets::Widget::render(view, scratch.area, &mut scratch);
360 for y in 0..column.height {
361 let at = (column.x, column.y + y);
362 if buf.area.contains(at.into()) {
363 buf[at] = scratch[(1, y)].clone();
364 }
365 }
366}