1use std::collections::HashMap;
2use std::io::{self, Write};
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::time::Duration;
6
7use crossterm::cursor::MoveTo;
8use crossterm::event::{poll, read, Event, KeyCode, KeyEvent, KeyModifiers};
9use crossterm::style::{Print, ResetColor, SetAttribute, Attribute};
10use crossterm::terminal::{Clear, ClearType, size};
11use crossterm::QueueableCommand;
12
13use crate::error::Result;
14use crate::input::{translate, Command};
15use crate::marks::{mark_set, mark_jump, jump_previous, update_prev_position, is_valid_mark_name};
16use crate::line_index::LineIndex;
17use crate::prettify::PrettifyMode;
18use crate::render::Cell;
19use crate::source::{find_tail_offset, Source};
20use crate::viewport::{Frame, RowStyle, SearchDirection, Viewport};
21
22#[derive(Default, Clone, Copy)]
26pub struct RebuildSpec {
27 pub head: Option<usize>,
28 pub tail: Option<usize>,
29}
30
31#[derive(Debug, Clone)]
33enum InputMode {
34 Normal,
35 OptionPrefix,
37 PrettifyPrefix,
40 SearchPrompt {
43 direction: SearchDirection,
44 buffer: String,
45 error: Option<String>,
48 },
49 MarkSetPending,
51 MarkJumpPending,
53 CtrlXPending,
55}
56
57pub fn run(
58 src: Box<dyn Source>,
59 mut viewport: Viewport,
60 mut idx: LineIndex,
61 sigterm: Arc<AtomicBool>,
62 rebuild_spec: RebuildSpec,
63) -> Result<()> {
64 let (mut cols, mut rows) = size().unwrap_or((80, 24));
65 viewport.resize(cols, rows);
66
67 let mut stdout = io::stdout();
68 let timeout = Duration::from_millis(250);
69 let mut last_revision = src.revision();
70
71 if (viewport.filter_active() || viewport.grep_active()) && !viewport.dim_mode() {
76 idx.extend_to_end(src.as_ref());
77 viewport.extend_visible_lines(&idx, src.as_ref());
78 }
79
80 if viewport.follow_mode() {
83 src.pump();
84 viewport.extend_visible_lines(&idx, src.as_ref());
85 viewport.goto_bottom(src.as_ref(), &mut idx);
86 }
87
88 let mut needs_redraw = true;
90 let mut mode = InputMode::Normal;
91 let mut numeric_prefix: Option<usize> = None;
92 let mut marks: HashMap<char, usize> = HashMap::new();
93 let mut previous_position: Option<usize> = None;
94
95 loop {
96 if sigterm.load(Ordering::SeqCst) {
97 break;
98 }
99
100 if needs_redraw {
101 let mut frame = viewport.frame(src.as_ref(), &mut idx);
102 if let InputMode::SearchPrompt { direction, buffer, error } = &mode {
104 let prefix = if matches!(direction, SearchDirection::Forward) { "/" } else { "?" };
105 frame.status = match error {
106 Some(e) => format!("{prefix}{buffer} [error: {e}]"),
107 None => format!("{prefix}{buffer}"),
108 };
109 }
110 write_frame(&mut stdout, &frame, cols, rows)
111 .map_err(|e| crate::error::Error::Runtime(format!("stdout: {}", e)))?;
112 needs_redraw = false;
113 }
114
115 match poll(timeout) {
117 Ok(true) => {
118 let event = read().map_err(|e| crate::error::Error::Runtime(format!("input: {}", e)))?;
119 match &mut mode {
122 InputMode::SearchPrompt { direction, buffer, error } => {
123 if let Event::Key(KeyEvent { code, .. }) = event {
124 match code {
125 KeyCode::Esc => { mode = InputMode::Normal; needs_redraw = true; }
126 KeyCode::Enter => {
127 if buffer.is_empty() {
128 if viewport.search_active() {
132 let reverse = !matches!(
133 (viewport.search_direction(), *direction),
134 (SearchDirection::Forward, SearchDirection::Forward)
135 | (SearchDirection::Backward, SearchDirection::Backward)
136 );
137 update_prev_position(&mut previous_position, viewport.top_line());
138 viewport.search_repeat(src.as_ref(), &mut idx, reverse);
139 }
140 mode = InputMode::Normal;
141 } else {
142 match viewport.set_search(buffer.clone(), *direction) {
143 Ok(()) => {
144 update_prev_position(&mut previous_position, viewport.top_line());
145 viewport.search_repeat(src.as_ref(), &mut idx, false);
146 mode = InputMode::Normal;
147 }
148 Err(e) => { *error = Some(e); }
149 }
150 }
151 needs_redraw = true;
152 }
153 KeyCode::Backspace => {
154 buffer.pop();
155 *error = None;
156 needs_redraw = true;
157 }
158 KeyCode::Char(c) => {
159 buffer.push(c);
160 *error = None;
161 needs_redraw = true;
162 }
163 _ => {}
164 }
165 }
166 continue;
167 }
168 InputMode::OptionPrefix => {
169 if let Event::Key(KeyEvent { code, .. }) = event {
170 match code {
171 KeyCode::Char('N') | KeyCode::Char('n') => viewport.toggle_line_numbers(),
172 KeyCode::Char('S') | KeyCode::Char('s') => viewport.toggle_chop(),
173 KeyCode::Char('F') | KeyCode::Char('f') => viewport.toggle_follow(),
174 KeyCode::Char('P') | KeyCode::Char('p') => {
175 mode = InputMode::PrettifyPrefix;
177 needs_redraw = true;
178 continue;
179 }
180 _ => {}
181 }
182 }
183 mode = InputMode::Normal;
184 needs_redraw = true;
185 continue;
186 }
187 InputMode::PrettifyPrefix => {
188 if let Event::Key(KeyEvent { code, .. }) = event {
189 let target: Option<PrettifyTarget> = match code {
190 KeyCode::Char('j') | KeyCode::Char('J') => Some(PrettifyTarget::Mode(PrettifyMode::Json)),
191 KeyCode::Char('y') | KeyCode::Char('Y') => Some(PrettifyTarget::Mode(PrettifyMode::Yaml)),
192 KeyCode::Char('t') | KeyCode::Char('T') => Some(PrettifyTarget::Mode(PrettifyMode::Toml)),
193 KeyCode::Char('x') | KeyCode::Char('X') => Some(PrettifyTarget::Mode(PrettifyMode::Xml)),
194 KeyCode::Char('h') | KeyCode::Char('H') => Some(PrettifyTarget::Mode(PrettifyMode::Html)),
195 KeyCode::Char('c') | KeyCode::Char('C') => Some(PrettifyTarget::Mode(PrettifyMode::Csv)),
196 KeyCode::Char('r') | KeyCode::Char('R') => Some(PrettifyTarget::Mode(PrettifyMode::Off)),
197 KeyCode::Char('a') | KeyCode::Char('A') => Some(PrettifyTarget::Auto),
198 _ => None,
199 };
200 if let Some(t) = target {
201 apply_prettify(
202 src.as_ref(),
203 &mut viewport,
204 &mut idx,
205 rebuild_spec,
206 t,
207 );
208 last_revision = src.revision();
209 }
210 }
211 mode = InputMode::Normal;
212 needs_redraw = true;
213 continue;
214 }
215 InputMode::MarkSetPending => {
216 if let Event::Key(KeyEvent { code: KeyCode::Char(c), .. }) = event {
217 if is_valid_mark_name(c) {
218 mark_set(&mut marks, c, viewport.top_line());
219 }
220 }
221 mode = InputMode::Normal;
222 continue;
223 }
224 InputMode::MarkJumpPending => {
225 if let Event::Key(KeyEvent { code: KeyCode::Char(c), .. }) = event {
226 if is_valid_mark_name(c) {
227 if let Some(line) = mark_jump(
228 &marks, c, idx.line_count(),
229 &mut previous_position, viewport.top_line(),
230 ) {
231 viewport.goto_line(line, src.as_ref(), &mut idx);
232 needs_redraw = true;
233 }
234 }
235 }
236 mode = InputMode::Normal;
237 continue;
238 }
239 InputMode::CtrlXPending => {
240 let is_ctrl_x = matches!(
241 event,
242 Event::Key(KeyEvent {
243 code: KeyCode::Char('x'),
244 modifiers: KeyModifiers::CONTROL,
245 ..
246 })
247 );
248 if is_ctrl_x {
249 if let Some(line) = jump_previous(
250 &mut previous_position, viewport.top_line(),
251 ) {
252 let clamped = line.min(idx.line_count().saturating_sub(1));
253 viewport.goto_line(clamped, src.as_ref(), &mut idx);
254 needs_redraw = true;
255 }
256 mode = InputMode::Normal;
257 continue;
258 }
259 mode = InputMode::Normal;
261 }
263 InputMode::Normal => {}
264 }
265 let cmd = translate(event);
266 let prefix_at_cmd = numeric_prefix.take();
269 match cmd {
270 Command::Digit(d) => {
271 let cur = prefix_at_cmd.unwrap_or(0);
272 let next = cur.saturating_mul(10).saturating_add(d as usize);
273 if next <= 99_999_999 {
274 numeric_prefix = Some(next);
275 } else {
276 numeric_prefix = prefix_at_cmd;
278 }
279 continue;
280 }
281 Command::Cancel => {
282 continue;
284 }
285 Command::GotoLine => {
286 update_prev_position(&mut previous_position, viewport.top_line());
287 match prefix_at_cmd {
288 Some(line) if line > 0 => viewport.goto_line(line - 1, src.as_ref(), &mut idx),
289 _ => viewport.goto_top(),
290 }
291 needs_redraw = true;
292 }
293 Command::GotoRecord => {
294 update_prev_position(&mut previous_position, viewport.top_line());
295 match prefix_at_cmd {
296 Some(rec) if rec > 0 => viewport.goto_record(rec - 1, src.as_ref(), &mut idx),
297 _ => viewport.goto_bottom(src.as_ref(), &mut idx),
298 }
299 needs_redraw = true;
300 }
301 Command::GotoPercent => {
302 update_prev_position(&mut previous_position, viewport.top_line());
303 match prefix_at_cmd {
304 Some(p) if p <= 100 => viewport.goto_percent(p as u8, src.as_ref(), &mut idx),
305 _ => viewport.goto_top(),
306 }
307 needs_redraw = true;
308 }
309 Command::Quit => break,
310 Command::Resize(c, r) => {
311 cols = c; rows = r;
312 viewport.resize(c, r);
313 needs_redraw = true;
314 }
315 Command::ScrollLines(n) => {
316 viewport.scroll_lines(n, src.as_ref(), &mut idx);
317 needs_redraw = true;
318 }
319 Command::ScrollLogicalLines(n) => {
320 viewport.scroll_logical_lines(n, src.as_ref(), &mut idx);
321 needs_redraw = true;
322 }
323 Command::PageDown => {
324 viewport.page_down(src.as_ref(), &mut idx);
325 needs_redraw = true;
326 }
327 Command::PageUp => {
328 viewport.page_up(src.as_ref(), &mut idx);
329 needs_redraw = true;
330 }
331 Command::HalfPageDown => {
332 viewport.half_page_down(src.as_ref(), &mut idx);
333 needs_redraw = true;
334 }
335 Command::HalfPageUp => {
336 viewport.half_page_up(src.as_ref(), &mut idx);
337 needs_redraw = true;
338 }
339 Command::Refresh => {
340 needs_redraw = true;
341 }
342 Command::Reload => {
343 src.pump();
346 if src.revision() != last_revision {
347 rebuild_after_replace(
348 src.as_ref(), &mut viewport, &mut idx, rebuild_spec,
349 );
350 last_revision = src.revision();
351 needs_redraw = true;
352 }
353 }
354 Command::TogglePrettify => {
355 apply_prettify(
356 src.as_ref(), &mut viewport, &mut idx, rebuild_spec,
357 PrettifyTarget::Toggle,
358 );
359 last_revision = src.revision();
360 needs_redraw = true;
361 }
362 Command::SetPrettifyMode(m) => {
363 apply_prettify(
364 src.as_ref(), &mut viewport, &mut idx, rebuild_spec,
365 PrettifyTarget::Mode(m),
366 );
367 last_revision = src.revision();
368 needs_redraw = true;
369 }
370 Command::RedetectPrettify => {
371 apply_prettify(
372 src.as_ref(), &mut viewport, &mut idx, rebuild_spec,
373 PrettifyTarget::Auto,
374 );
375 last_revision = src.revision();
376 needs_redraw = true;
377 }
378 Command::ToggleLineNumbers => {
379 viewport.toggle_line_numbers();
380 needs_redraw = true;
381 }
382 Command::ToggleChop => {
383 viewport.toggle_chop();
384 needs_redraw = true;
385 }
386 Command::ToggleFollow => {
387 viewport.toggle_follow();
388 if viewport.follow_mode() {
389 src.pump();
391 idx.notice_new_bytes(src.as_ref());
392 viewport.goto_bottom(src.as_ref(), &mut idx);
393 }
394 needs_redraw = true;
395 }
396 Command::SearchForward => {
397 mode = InputMode::SearchPrompt {
398 direction: SearchDirection::Forward,
399 buffer: String::new(),
400 error: None,
401 };
402 needs_redraw = true;
403 }
404 Command::SearchBackward => {
405 mode = InputMode::SearchPrompt {
406 direction: SearchDirection::Backward,
407 buffer: String::new(),
408 error: None,
409 };
410 needs_redraw = true;
411 }
412 Command::NextMatch => {
413 update_prev_position(&mut previous_position, viewport.top_line());
414 if viewport.search_repeat(src.as_ref(), &mut idx, false) {
415 needs_redraw = true;
416 }
417 }
418 Command::PreviousMatch => {
419 update_prev_position(&mut previous_position, viewport.top_line());
420 if viewport.search_repeat(src.as_ref(), &mut idx, true) {
421 needs_redraw = true;
422 }
423 }
424 Command::OptionPrefix => {
425 mode = InputMode::OptionPrefix;
426 }
427 Command::MarkSet => {
428 mode = InputMode::MarkSetPending;
429 }
430 Command::MarkJump => {
431 mode = InputMode::MarkJumpPending;
432 }
433 Command::CtrlXPrefix => {
434 mode = InputMode::CtrlXPending;
435 }
436 Command::JumpPrevious => {
437 }
440 Command::Noop => {}
441 }
442 }
443 Ok(false) => {
444 if viewport.live_mode() {
446 let was_at_bottom = viewport.is_at_bottom(&idx);
447 src.pump();
448 if src.revision() != last_revision {
449 rebuild_after_replace(
450 src.as_ref(), &mut viewport, &mut idx, rebuild_spec,
451 );
452 if was_at_bottom {
453 viewport.goto_bottom(src.as_ref(), &mut idx);
454 }
455 last_revision = src.revision();
456 needs_redraw = true;
457 }
458 } else if viewport.follow_mode() {
459 let was_at_bottom = viewport.is_at_bottom(&idx);
460 src.pump();
461 let lines_before = idx.line_count();
462 idx.notice_new_bytes(src.as_ref());
463 viewport.extend_visible_lines(&idx, src.as_ref());
464 if idx.line_count() != lines_before {
465 needs_redraw = true;
466 if was_at_bottom {
467 viewport.goto_bottom(src.as_ref(), &mut idx);
468 }
469 }
470 } else if !src.is_complete() {
471 let lines_before = idx.line_count();
474 idx.notice_new_bytes(src.as_ref());
475 viewport.extend_visible_lines(&idx, src.as_ref());
476 if idx.line_count() != lines_before {
477 needs_redraw = true;
478 }
479 }
480 }
481 Err(_) => {
482 std::thread::sleep(timeout);
484 }
485 }
486 }
487 Ok(())
488}
489
490#[derive(Debug, Clone, Copy)]
492enum PrettifyTarget {
493 Mode(PrettifyMode),
495 Toggle,
497 Auto,
499}
500
501fn apply_prettify(
505 src: &dyn Source,
506 viewport: &mut Viewport,
507 idx: &mut LineIndex,
508 spec: RebuildSpec,
509 target: PrettifyTarget,
510) {
511 if src.prettify_mode().is_none() {
513 return;
514 }
515 match target {
516 PrettifyTarget::Mode(m) => src.set_prettify_mode(m),
517 PrettifyTarget::Toggle => src.toggle_prettify(),
518 PrettifyTarget::Auto => src.redetect_prettify(),
519 }
520 rebuild_after_replace(src, viewport, idx, spec);
521 viewport.set_prettify_label(src.prettify_label());
522}
523
524fn rebuild_after_replace(
530 src: &dyn Source,
531 viewport: &mut Viewport,
532 idx: &mut LineIndex,
533 spec: RebuildSpec,
534) {
535 let new_off = match spec.tail {
536 Some(n) => find_tail_offset(src, n),
537 None => 0,
538 };
539 *idx = LineIndex::new_starting_at(new_off);
540 if let Some(n) = spec.head {
541 idx.set_head_cap(n);
542 }
543 viewport.invalidate_filter_cache();
544 idx.notice_new_bytes(src);
545 viewport.extend_visible_lines(idx, src);
546 viewport.clamp_top_line(idx.line_count());
547}
548
549fn write_frame(out: &mut impl Write, frame: &Frame, cols: u16, rows: u16) -> io::Result<()> {
550 out.queue(SetAttribute(Attribute::Reset))?;
554 out.queue(ResetColor)?;
555 out.queue(Clear(ClearType::All))?;
556 for (i, row) in frame.body.iter().enumerate() {
557 out.queue(MoveTo(0, i as u16))?;
558 out.queue(SetAttribute(Attribute::Reset))?;
561 let style = frame.row_styles.get(i).copied().unwrap_or(RowStyle::Normal);
562 if matches!(style, RowStyle::Dim) {
563 out.queue(SetAttribute(Attribute::Dim))?;
564 }
565 let no_highlights = Vec::new();
566 let highlights = frame.highlights.get(i).unwrap_or(&no_highlights);
567 write_row_with_highlights(out, row, cols, highlights)?;
568 out.queue(SetAttribute(Attribute::Reset))?;
569 }
570 out.queue(MoveTo(0, rows.saturating_sub(1)))?;
572 out.queue(SetAttribute(Attribute::Reverse))?;
573 let mut status = frame.status.clone();
574 if status.len() > cols as usize {
575 status.truncate(cols as usize);
576 } else {
577 let pad = cols as usize - status.len();
578 status.push_str(&" ".repeat(pad));
579 }
580 out.queue(Print(status))?;
581 out.queue(ResetColor)?;
582 out.queue(SetAttribute(Attribute::Reset))?;
583 out.flush()
584}
585
586fn cells_to_string(row: &[Cell], cols: u16) -> String {
587 let mut s = String::with_capacity(cols as usize);
588 for cell in row.iter().take(cols as usize) {
589 match cell {
590 Cell::Char { ch, .. } => s.push(*ch),
591 Cell::Continuation => { }
592 Cell::Empty => s.push(' '),
593 }
594 }
595 s
596}
597
598fn write_row_with_highlights(
604 out: &mut impl Write,
605 row: &[Cell],
606 cols: u16,
607 highlights: &[std::ops::Range<usize>],
608) -> io::Result<()> {
609 let cols_usize = cols as usize;
610 if highlights.is_empty() {
611 out.queue(Print(cells_to_string(row, cols)))?;
612 return Ok(());
613 }
614 let mut ranges: Vec<std::ops::Range<usize>> = highlights
616 .iter()
617 .filter_map(|r| {
618 let s = r.start.min(cols_usize);
619 let e = r.end.min(cols_usize);
620 if e > s { Some(s..e) } else { None }
621 })
622 .collect();
623 ranges.sort_by_key(|r| r.start);
624
625 let mut col = 0usize;
626 let mut i = 0usize;
627 while col < cols_usize && i < row.len() {
628 let active = ranges.iter().find(|r| r.start <= col && col < r.end);
630 let (segment_end, reversed) = match active {
631 Some(r) => (r.end.min(cols_usize), true),
632 None => {
633 let next = ranges.iter().find(|r| r.start > col).map(|r| r.start);
635 (next.unwrap_or(cols_usize), false)
636 }
637 };
638 if reversed { out.queue(SetAttribute(Attribute::Reverse))?; }
639 let mut s = String::new();
641 while col < segment_end && i < row.len() {
642 match &row[i] {
643 Cell::Char { ch, width } => {
644 s.push(*ch);
645 col += *width as usize;
646 }
647 Cell::Continuation => {
648 }
650 Cell::Empty => {
651 s.push(' ');
652 col += 1;
653 }
654 }
655 i += 1;
656 }
657 out.queue(Print(s))?;
658 if reversed { out.queue(SetAttribute(Attribute::NoReverse))?; }
659 }
660 Ok(())
661}