1use std::io::{self, Write};
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::time::Duration;
5
6use crossterm::cursor::MoveTo;
7use crossterm::event::{poll, read, Event, KeyCode, KeyEvent};
8use crossterm::style::{Print, ResetColor, SetAttribute, Attribute};
9use crossterm::terminal::{Clear, ClearType, size};
10use crossterm::QueueableCommand;
11
12use crate::error::Result;
13use crate::input::{translate, Command};
14use crate::line_index::LineIndex;
15use crate::prettify::PrettifyMode;
16use crate::render::Cell;
17use crate::source::{find_tail_offset, Source};
18use crate::viewport::{Frame, RowStyle, SearchDirection, Viewport};
19
20#[derive(Default, Clone, Copy)]
24pub struct RebuildSpec {
25 pub head: Option<usize>,
26 pub tail: Option<usize>,
27}
28
29#[derive(Debug, Clone)]
31enum InputMode {
32 Normal,
33 OptionPrefix,
35 PrettifyPrefix,
38 SearchPrompt {
41 direction: SearchDirection,
42 buffer: String,
43 error: Option<String>,
46 },
47}
48
49pub fn run(
50 src: Box<dyn Source>,
51 mut viewport: Viewport,
52 mut idx: LineIndex,
53 sigterm: Arc<AtomicBool>,
54 rebuild_spec: RebuildSpec,
55) -> Result<()> {
56 let (mut cols, mut rows) = size().unwrap_or((80, 24));
57 viewport.resize(cols, rows);
58
59 let mut stdout = io::stdout();
60 let timeout = Duration::from_millis(250);
61 let mut last_revision = src.revision();
62
63 if viewport.filter_active() && !viewport.dim_mode() {
67 idx.extend_to_end(src.as_ref());
68 viewport.extend_visible_lines(&idx, src.as_ref());
69 }
70
71 if viewport.follow_mode() {
74 src.pump();
75 viewport.extend_visible_lines(&idx, src.as_ref());
76 viewport.goto_bottom(src.as_ref(), &mut idx);
77 }
78
79 let mut needs_redraw = true;
81 let mut mode = InputMode::Normal;
82
83 loop {
84 if sigterm.load(Ordering::SeqCst) {
85 break;
86 }
87
88 if needs_redraw {
89 let mut frame = viewport.frame(src.as_ref(), &mut idx);
90 if let InputMode::SearchPrompt { direction, buffer, error } = &mode {
92 let prefix = if matches!(direction, SearchDirection::Forward) { "/" } else { "?" };
93 frame.status = match error {
94 Some(e) => format!("{prefix}{buffer} [error: {e}]"),
95 None => format!("{prefix}{buffer}"),
96 };
97 }
98 write_frame(&mut stdout, &frame, cols, rows)
99 .map_err(|e| crate::error::Error::Runtime(format!("stdout: {}", e)))?;
100 needs_redraw = false;
101 }
102
103 match poll(timeout) {
105 Ok(true) => {
106 let event = read().map_err(|e| crate::error::Error::Runtime(format!("input: {}", e)))?;
107 match &mut mode {
110 InputMode::SearchPrompt { direction, buffer, error } => {
111 if let Event::Key(KeyEvent { code, .. }) = event {
112 match code {
113 KeyCode::Esc => { mode = InputMode::Normal; needs_redraw = true; }
114 KeyCode::Enter => {
115 if buffer.is_empty() {
116 if viewport.search_active() {
120 let reverse = !matches!(
121 (viewport.search_direction(), *direction),
122 (SearchDirection::Forward, SearchDirection::Forward)
123 | (SearchDirection::Backward, SearchDirection::Backward)
124 );
125 viewport.search_repeat(src.as_ref(), &mut idx, reverse);
126 }
127 mode = InputMode::Normal;
128 } else {
129 match viewport.set_search(buffer.clone(), *direction) {
130 Ok(()) => {
131 viewport.search_repeat(src.as_ref(), &mut idx, false);
132 mode = InputMode::Normal;
133 }
134 Err(e) => { *error = Some(e); }
135 }
136 }
137 needs_redraw = true;
138 }
139 KeyCode::Backspace => {
140 buffer.pop();
141 *error = None;
142 needs_redraw = true;
143 }
144 KeyCode::Char(c) => {
145 buffer.push(c);
146 *error = None;
147 needs_redraw = true;
148 }
149 _ => {}
150 }
151 }
152 continue;
153 }
154 InputMode::OptionPrefix => {
155 if let Event::Key(KeyEvent { code, .. }) = event {
156 match code {
157 KeyCode::Char('N') | KeyCode::Char('n') => viewport.toggle_line_numbers(),
158 KeyCode::Char('S') | KeyCode::Char('s') => viewport.toggle_chop(),
159 KeyCode::Char('F') | KeyCode::Char('f') => viewport.toggle_follow(),
160 KeyCode::Char('P') | KeyCode::Char('p') => {
161 mode = InputMode::PrettifyPrefix;
163 needs_redraw = true;
164 continue;
165 }
166 _ => {}
167 }
168 }
169 mode = InputMode::Normal;
170 needs_redraw = true;
171 continue;
172 }
173 InputMode::PrettifyPrefix => {
174 if let Event::Key(KeyEvent { code, .. }) = event {
175 let target: Option<PrettifyTarget> = match code {
176 KeyCode::Char('j') | KeyCode::Char('J') => Some(PrettifyTarget::Mode(PrettifyMode::Json)),
177 KeyCode::Char('y') | KeyCode::Char('Y') => Some(PrettifyTarget::Mode(PrettifyMode::Yaml)),
178 KeyCode::Char('t') | KeyCode::Char('T') => Some(PrettifyTarget::Mode(PrettifyMode::Toml)),
179 KeyCode::Char('x') | KeyCode::Char('X') => Some(PrettifyTarget::Mode(PrettifyMode::Xml)),
180 KeyCode::Char('h') | KeyCode::Char('H') => Some(PrettifyTarget::Mode(PrettifyMode::Html)),
181 KeyCode::Char('c') | KeyCode::Char('C') => Some(PrettifyTarget::Mode(PrettifyMode::Csv)),
182 KeyCode::Char('r') | KeyCode::Char('R') => Some(PrettifyTarget::Mode(PrettifyMode::Off)),
183 KeyCode::Char('a') | KeyCode::Char('A') => Some(PrettifyTarget::Auto),
184 _ => None,
185 };
186 if let Some(t) = target {
187 apply_prettify(
188 src.as_ref(),
189 &mut viewport,
190 &mut idx,
191 rebuild_spec,
192 t,
193 );
194 last_revision = src.revision();
195 }
196 }
197 mode = InputMode::Normal;
198 needs_redraw = true;
199 continue;
200 }
201 InputMode::Normal => {}
202 }
203 let cmd = translate(event);
204 match cmd {
205 Command::Quit => break,
206 Command::Resize(c, r) => {
207 cols = c; rows = r;
208 viewport.resize(c, r);
209 needs_redraw = true;
210 }
211 Command::ScrollLines(n) => {
212 viewport.scroll_lines(n, src.as_ref(), &mut idx);
213 needs_redraw = true;
214 }
215 Command::ScrollLogicalLines(n) => {
216 viewport.scroll_logical_lines(n, src.as_ref(), &mut idx);
217 needs_redraw = true;
218 }
219 Command::PageDown => {
220 viewport.page_down(src.as_ref(), &mut idx);
221 needs_redraw = true;
222 }
223 Command::PageUp => {
224 viewport.page_up(src.as_ref(), &mut idx);
225 needs_redraw = true;
226 }
227 Command::HalfPageDown => {
228 viewport.half_page_down(src.as_ref(), &mut idx);
229 needs_redraw = true;
230 }
231 Command::HalfPageUp => {
232 viewport.half_page_up(src.as_ref(), &mut idx);
233 needs_redraw = true;
234 }
235 Command::GoTop => {
236 viewport.goto_top();
237 needs_redraw = true;
238 }
239 Command::GoBottom => {
240 viewport.goto_bottom(src.as_ref(), &mut idx);
241 needs_redraw = true;
242 }
243 Command::Refresh => {
244 needs_redraw = true;
245 }
246 Command::Reload => {
247 src.pump();
250 if src.revision() != last_revision {
251 rebuild_after_replace(
252 src.as_ref(), &mut viewport, &mut idx, rebuild_spec,
253 );
254 last_revision = src.revision();
255 needs_redraw = true;
256 }
257 }
258 Command::TogglePrettify => {
259 apply_prettify(
260 src.as_ref(), &mut viewport, &mut idx, rebuild_spec,
261 PrettifyTarget::Toggle,
262 );
263 last_revision = src.revision();
264 needs_redraw = true;
265 }
266 Command::SetPrettifyMode(m) => {
267 apply_prettify(
268 src.as_ref(), &mut viewport, &mut idx, rebuild_spec,
269 PrettifyTarget::Mode(m),
270 );
271 last_revision = src.revision();
272 needs_redraw = true;
273 }
274 Command::RedetectPrettify => {
275 apply_prettify(
276 src.as_ref(), &mut viewport, &mut idx, rebuild_spec,
277 PrettifyTarget::Auto,
278 );
279 last_revision = src.revision();
280 needs_redraw = true;
281 }
282 Command::ToggleLineNumbers => {
283 viewport.toggle_line_numbers();
284 needs_redraw = true;
285 }
286 Command::ToggleChop => {
287 viewport.toggle_chop();
288 needs_redraw = true;
289 }
290 Command::ToggleFollow => {
291 viewport.toggle_follow();
292 if viewport.follow_mode() {
293 src.pump();
295 idx.notice_new_bytes(src.as_ref());
296 viewport.goto_bottom(src.as_ref(), &mut idx);
297 }
298 needs_redraw = true;
299 }
300 Command::SearchForward => {
301 mode = InputMode::SearchPrompt {
302 direction: SearchDirection::Forward,
303 buffer: String::new(),
304 error: None,
305 };
306 needs_redraw = true;
307 }
308 Command::SearchBackward => {
309 mode = InputMode::SearchPrompt {
310 direction: SearchDirection::Backward,
311 buffer: String::new(),
312 error: None,
313 };
314 needs_redraw = true;
315 }
316 Command::NextMatch => {
317 if viewport.search_repeat(src.as_ref(), &mut idx, false) {
318 needs_redraw = true;
319 }
320 }
321 Command::PreviousMatch => {
322 if viewport.search_repeat(src.as_ref(), &mut idx, true) {
323 needs_redraw = true;
324 }
325 }
326 Command::OptionPrefix => {
327 mode = InputMode::OptionPrefix;
328 }
329 Command::Noop => {}
330 }
331 }
332 Ok(false) => {
333 if viewport.live_mode() {
335 let was_at_bottom = viewport.is_at_bottom(&idx);
336 src.pump();
337 if src.revision() != last_revision {
338 rebuild_after_replace(
339 src.as_ref(), &mut viewport, &mut idx, rebuild_spec,
340 );
341 if was_at_bottom {
342 viewport.goto_bottom(src.as_ref(), &mut idx);
343 }
344 last_revision = src.revision();
345 needs_redraw = true;
346 }
347 } else if viewport.follow_mode() {
348 let was_at_bottom = viewport.is_at_bottom(&idx);
349 src.pump();
350 let lines_before = idx.line_count();
351 idx.notice_new_bytes(src.as_ref());
352 viewport.extend_visible_lines(&idx, src.as_ref());
353 if idx.line_count() != lines_before {
354 needs_redraw = true;
355 if was_at_bottom {
356 viewport.goto_bottom(src.as_ref(), &mut idx);
357 }
358 }
359 } else if !src.is_complete() {
360 let lines_before = idx.line_count();
363 idx.notice_new_bytes(src.as_ref());
364 viewport.extend_visible_lines(&idx, src.as_ref());
365 if idx.line_count() != lines_before {
366 needs_redraw = true;
367 }
368 }
369 }
370 Err(_) => {
371 std::thread::sleep(timeout);
373 }
374 }
375 }
376 Ok(())
377}
378
379#[derive(Debug, Clone, Copy)]
381enum PrettifyTarget {
382 Mode(PrettifyMode),
384 Toggle,
386 Auto,
388}
389
390fn apply_prettify(
394 src: &dyn Source,
395 viewport: &mut Viewport,
396 idx: &mut LineIndex,
397 spec: RebuildSpec,
398 target: PrettifyTarget,
399) {
400 if src.prettify_mode().is_none() {
402 return;
403 }
404 match target {
405 PrettifyTarget::Mode(m) => src.set_prettify_mode(m),
406 PrettifyTarget::Toggle => src.toggle_prettify(),
407 PrettifyTarget::Auto => src.redetect_prettify(),
408 }
409 rebuild_after_replace(src, viewport, idx, spec);
410 viewport.set_prettify_label(src.prettify_label());
411}
412
413fn rebuild_after_replace(
419 src: &dyn Source,
420 viewport: &mut Viewport,
421 idx: &mut LineIndex,
422 spec: RebuildSpec,
423) {
424 let new_off = match spec.tail {
425 Some(n) => find_tail_offset(src, n),
426 None => 0,
427 };
428 *idx = LineIndex::new_starting_at(new_off);
429 if let Some(n) = spec.head {
430 idx.set_head_cap(n);
431 }
432 viewport.invalidate_filter_cache();
433 idx.notice_new_bytes(src);
434 viewport.extend_visible_lines(idx, src);
435 viewport.clamp_top_line(idx.line_count());
436}
437
438fn write_frame(out: &mut impl Write, frame: &Frame, cols: u16, rows: u16) -> io::Result<()> {
439 out.queue(SetAttribute(Attribute::Reset))?;
443 out.queue(ResetColor)?;
444 out.queue(Clear(ClearType::All))?;
445 for (i, row) in frame.body.iter().enumerate() {
446 out.queue(MoveTo(0, i as u16))?;
447 out.queue(SetAttribute(Attribute::Reset))?;
450 let style = frame.row_styles.get(i).copied().unwrap_or(RowStyle::Normal);
451 if matches!(style, RowStyle::Dim) {
452 out.queue(SetAttribute(Attribute::Dim))?;
453 }
454 let no_highlights = Vec::new();
455 let highlights = frame.highlights.get(i).unwrap_or(&no_highlights);
456 write_row_with_highlights(out, row, cols, highlights)?;
457 out.queue(SetAttribute(Attribute::Reset))?;
458 }
459 out.queue(MoveTo(0, rows.saturating_sub(1)))?;
461 out.queue(SetAttribute(Attribute::Reverse))?;
462 let mut status = frame.status.clone();
463 if status.len() > cols as usize {
464 status.truncate(cols as usize);
465 } else {
466 let pad = cols as usize - status.len();
467 status.push_str(&" ".repeat(pad));
468 }
469 out.queue(Print(status))?;
470 out.queue(ResetColor)?;
471 out.queue(SetAttribute(Attribute::Reset))?;
472 out.flush()
473}
474
475fn cells_to_string(row: &[Cell], cols: u16) -> String {
476 let mut s = String::with_capacity(cols as usize);
477 for cell in row.iter().take(cols as usize) {
478 match cell {
479 Cell::Char { ch, .. } => s.push(*ch),
480 Cell::Continuation => { }
481 Cell::Empty => s.push(' '),
482 }
483 }
484 s
485}
486
487fn write_row_with_highlights(
493 out: &mut impl Write,
494 row: &[Cell],
495 cols: u16,
496 highlights: &[std::ops::Range<usize>],
497) -> io::Result<()> {
498 let cols_usize = cols as usize;
499 if highlights.is_empty() {
500 out.queue(Print(cells_to_string(row, cols)))?;
501 return Ok(());
502 }
503 let mut ranges: Vec<std::ops::Range<usize>> = highlights
505 .iter()
506 .filter_map(|r| {
507 let s = r.start.min(cols_usize);
508 let e = r.end.min(cols_usize);
509 if e > s { Some(s..e) } else { None }
510 })
511 .collect();
512 ranges.sort_by_key(|r| r.start);
513
514 let mut col = 0usize;
515 let mut i = 0usize;
516 while col < cols_usize && i < row.len() {
517 let active = ranges.iter().find(|r| r.start <= col && col < r.end);
519 let (segment_end, reversed) = match active {
520 Some(r) => (r.end.min(cols_usize), true),
521 None => {
522 let next = ranges.iter().find(|r| r.start > col).map(|r| r.start);
524 (next.unwrap_or(cols_usize), false)
525 }
526 };
527 if reversed { out.queue(SetAttribute(Attribute::Reverse))?; }
528 let mut s = String::new();
530 while col < segment_end && i < row.len() {
531 match &row[i] {
532 Cell::Char { ch, width } => {
533 s.push(*ch);
534 col += *width as usize;
535 }
536 Cell::Continuation => {
537 }
539 Cell::Empty => {
540 s.push(' ');
541 col += 1;
542 }
543 }
544 i += 1;
545 }
546 out.queue(Print(s))?;
547 if reversed { out.queue(SetAttribute(Attribute::NoReverse))?; }
548 }
549 Ok(())
550}