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, MarkTarget};
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 ShellPrompt { buffer: String, error: Option<String> },
52 MarkSetPending,
54 MarkJumpPending,
56 CtrlXPending,
58 ColonPrompt { buffer: String, error: Option<String> },
61}
62
63#[derive(Debug, Clone, PartialEq)]
64enum ColonCommand {
65 Next,
66 Prev,
67 Edit(std::path::PathBuf),
68 ShowFile,
69 Quit,
70 Delete,
71 First,
72 Last,
73}
74
75#[derive(Debug, Clone, PartialEq)]
76enum ColonParseError {
77 UnknownCommand(String),
78 MissingPath,
79}
80
81impl std::fmt::Display for ColonParseError {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 match self {
84 ColonParseError::UnknownCommand(t) => write!(f, "unknown command: :{t}"),
85 ColonParseError::MissingPath => write!(f, ":e requires a path"),
86 }
87 }
88}
89
90fn parse_colon_command(buf: &str) -> std::result::Result<ColonCommand, ColonParseError> {
91 let buf = buf.trim();
92 if buf.is_empty() {
93 return Err(ColonParseError::UnknownCommand(String::new()));
94 }
95 let mut parts = buf.splitn(2, char::is_whitespace);
96 let cmd = parts.next().unwrap();
97 let rest = parts.next().unwrap_or("").trim();
98 match cmd {
99 "n" | "next" => Ok(ColonCommand::Next),
100 "p" | "prev" => Ok(ColonCommand::Prev),
101 "e" | "edit" => {
102 if rest.is_empty() {
103 Err(ColonParseError::MissingPath)
104 } else {
105 let expanded = if let Some(stripped) = rest.strip_prefix("~/") {
107 if let Some(home) = std::env::var_os("HOME") {
108 let mut p = std::path::PathBuf::from(home);
109 p.push(stripped);
110 p
111 } else {
112 std::path::PathBuf::from(rest)
113 }
114 } else {
115 std::path::PathBuf::from(rest)
116 };
117 Ok(ColonCommand::Edit(expanded))
118 }
119 }
120 "f" => Ok(ColonCommand::ShowFile),
121 "q" | "quit" => Ok(ColonCommand::Quit),
122 "d" | "delete" => Ok(ColonCommand::Delete),
123 "x" | "first" => Ok(ColonCommand::First),
124 "t" | "last" => Ok(ColonCommand::Last),
125 other => Err(ColonParseError::UnknownCommand(other.to_string())),
126 }
127}
128
129enum ColonOutcome {
130 Continue(Option<String>), Quit,
132}
133
134#[allow(clippy::too_many_arguments)]
135fn switch_file(
136 new_path: &std::path::Path,
137 new_file_index: usize,
138 total_files: usize,
139 args: &crate::cli::Args,
140 preprocessor: Option<&crate::preprocess::Preprocessor>,
141 viewport: &mut crate::viewport::Viewport,
142 src: &mut Box<dyn crate::source::Source>,
143 idx: &mut crate::line_index::LineIndex,
144 record_start_regex: Option<®ex::bytes::Regex>,
145) -> crate::error::Result<()> {
146 let (new_src, new_label, new_failure) =
147 crate::open::open_source_for_path(new_path, args, preprocessor)?;
148
149 *src = new_src;
150 let mut new_idx = crate::line_index::LineIndex::new();
151 if let Some(re) = record_start_regex {
152 new_idx.set_record_start(re.clone());
153 }
154 *idx = new_idx;
155
156 viewport.set_source_label(new_label);
157 viewport.set_file_index(new_file_index, total_files);
158 viewport.set_preprocess_failure(new_failure);
159 viewport.goto_top();
160
161 Ok(())
162}
163
164#[allow(clippy::too_many_arguments)]
165fn dispatch_colon_command(
166 cmd: ColonCommand,
167 file_set: &mut crate::file_set::FileSet,
168 current_file_index: &mut usize,
169 args: &crate::cli::Args,
170 preprocessor: Option<&crate::preprocess::Preprocessor>,
171 record_start_regex: Option<®ex::bytes::Regex>,
172 viewport: &mut crate::viewport::Viewport,
173 src: &mut Box<dyn crate::source::Source>,
174 idx: &mut crate::line_index::LineIndex,
175) -> ColonOutcome {
176 match cmd {
177 ColonCommand::Next => {
178 match file_set.next() {
179 Ok(path) => {
180 let path = path.to_path_buf();
181 let new_idx_val = file_set.current_index();
182 if let Err(e) = switch_file(&path, new_idx_val, file_set.len(), args, preprocessor, viewport, src, idx, record_start_regex) {
183 ColonOutcome::Continue(Some(format!("[open: {e}]")))
184 } else {
185 *current_file_index = new_idx_val;
186 ColonOutcome::Continue(None)
187 }
188 }
189 Err(e) => ColonOutcome::Continue(Some(format!("[{e}]"))),
190 }
191 }
192 ColonCommand::Prev => {
193 match file_set.prev() {
194 Ok(path) => {
195 let path = path.to_path_buf();
196 let new_idx_val = file_set.current_index();
197 if let Err(e) = switch_file(&path, new_idx_val, file_set.len(), args, preprocessor, viewport, src, idx, record_start_regex) {
198 ColonOutcome::Continue(Some(format!("[open: {e}]")))
199 } else {
200 *current_file_index = new_idx_val;
201 ColonOutcome::Continue(None)
202 }
203 }
204 Err(e) => ColonOutcome::Continue(Some(format!("[{e}]"))),
205 }
206 }
207 ColonCommand::Edit(path) => {
208 match crate::open::open_source_for_path(&path, args, preprocessor) {
210 Ok(_) => {
211 let final_path = file_set.append_and_switch(path.clone()).to_path_buf();
213 let new_idx_val = file_set.current_index();
214 if let Err(e) = switch_file(&final_path, new_idx_val, file_set.len(), args, preprocessor, viewport, src, idx, record_start_regex) {
215 ColonOutcome::Continue(Some(format!("[open: {e}]")))
216 } else {
217 *current_file_index = new_idx_val;
218 ColonOutcome::Continue(None)
219 }
220 }
221 Err(e) => ColonOutcome::Continue(Some(format!("[open: {}: {e}]", path.display()))),
222 }
223 }
224 ColonCommand::ShowFile => {
225 let label = viewport.source_label_clone();
226 let cur = file_set.current_index() + 1;
227 let total = file_set.len();
228 let top = viewport.top_line() + 1;
229 let total_lines = idx.line_count();
230 let msg = if total > 1 {
231 format!("{label} (file {cur}/{total}): line {top}/{total_lines}")
232 } else {
233 format!("{label}: line {top}/{total_lines}")
234 };
235 ColonOutcome::Continue(Some(msg))
236 }
237 ColonCommand::Quit => ColonOutcome::Quit,
238 ColonCommand::Delete => {
239 match file_set.delete_current() {
240 Ok(path) => {
241 let path = path.to_path_buf();
242 let new_idx_val = file_set.current_index();
243 if let Err(e) = switch_file(&path, new_idx_val, file_set.len(), args, preprocessor, viewport, src, idx, record_start_regex) {
244 ColonOutcome::Continue(Some(format!("[open: {e}]")))
245 } else {
246 *current_file_index = new_idx_val;
247 ColonOutcome::Continue(None)
248 }
249 }
250 Err(e) => ColonOutcome::Continue(Some(format!("[{e}]"))),
251 }
252 }
253 ColonCommand::First => {
254 if file_set.current_index() == 0 {
255 ColonOutcome::Continue(None) } else if let Some(path) = file_set.first() {
257 let path = path.to_path_buf();
258 let new_idx_val = file_set.current_index();
259 if let Err(e) = switch_file(&path, new_idx_val, file_set.len(), args, preprocessor, viewport, src, idx, record_start_regex) {
260 ColonOutcome::Continue(Some(format!("[open: {e}]")))
261 } else {
262 *current_file_index = new_idx_val;
263 ColonOutcome::Continue(None)
264 }
265 } else {
266 ColonOutcome::Continue(None)
267 }
268 }
269 ColonCommand::Last => {
270 if file_set.current_index() + 1 == file_set.len() {
271 ColonOutcome::Continue(None)
272 } else if let Some(path) = file_set.last() {
273 let path = path.to_path_buf();
274 let new_idx_val = file_set.current_index();
275 if let Err(e) = switch_file(&path, new_idx_val, file_set.len(), args, preprocessor, viewport, src, idx, record_start_regex) {
276 ColonOutcome::Continue(Some(format!("[open: {e}]")))
277 } else {
278 *current_file_index = new_idx_val;
279 ColonOutcome::Continue(None)
280 }
281 } else {
282 ColonOutcome::Continue(None)
283 }
284 }
285 }
286}
287
288#[allow(clippy::too_many_arguments, clippy::collapsible_match)]
289pub fn run(
290 mut src: Box<dyn Source>,
291 mut viewport: Viewport,
292 mut idx: LineIndex,
293 sigterm: Arc<AtomicBool>,
294 rebuild_spec: RebuildSpec,
295 keymap: crate::keys::KeyMap,
296 mut file_set: crate::file_set::FileSet,
297 record_start_regex: Option<regex::bytes::Regex>,
298 args: crate::cli::Args,
299 preprocessor: Option<crate::preprocess::Preprocessor>,
300) -> Result<()> {
301 let (mut cols, mut rows) = size().unwrap_or((80, 24));
302 viewport.resize(cols, rows);
303
304 let mut stdout = io::stdout();
305 let timeout = Duration::from_millis(250);
306 let mut last_revision = src.revision();
307
308 if (viewport.filter_active() || viewport.grep_active()) && !viewport.dim_mode() {
313 idx.extend_to_end(src.as_ref());
314 viewport.extend_visible_lines(&idx, src.as_ref());
315 }
316
317 if viewport.follow_mode() {
320 src.pump();
321 viewport.extend_visible_lines(&idx, src.as_ref());
322 viewport.goto_bottom(src.as_ref(), &mut idx);
323 }
324
325 let mut needs_redraw = true;
327 let mut mode = InputMode::Normal;
328 let mut numeric_prefix: Option<usize> = None;
329 let mut marks: HashMap<char, (usize, usize)> = HashMap::new();
330 let mut previous_position: Option<(usize, usize)> = None;
331 let mut current_file_index: usize = file_set.current_index();
332 let mut transient_status: Option<String> = None;
333
334 loop {
335 if sigterm.load(Ordering::SeqCst) {
336 break;
337 }
338
339 if needs_redraw {
340 let mut frame = viewport.frame(src.as_ref(), &mut idx);
341 match &mode {
344 InputMode::SearchPrompt { direction, buffer, error } => {
345 let prefix = if matches!(direction, SearchDirection::Forward) { "/" } else { "?" };
346 frame.status = match error {
347 Some(e) => format!("{prefix}{buffer} [error: {e}]"),
348 None => format!("{prefix}{buffer}"),
349 };
350 }
351 InputMode::ShellPrompt { buffer, error } => {
352 frame.status = match error {
353 Some(e) => format!("!{buffer} [error: {e}]"),
354 None => format!("!{buffer}"),
355 };
356 }
357 InputMode::ColonPrompt { buffer, error } => {
358 frame.status = match error {
359 Some(e) => format!(":{buffer} [error: {e}]"),
360 None => format!(":{buffer}"),
361 };
362 }
363 _ => {
364 if let Some(msg) = transient_status.take() {
365 frame.status = msg;
366 }
367 }
368 }
369 write_frame(&mut stdout, &frame, cols, rows)
370 .map_err(|e| crate::error::Error::Runtime(format!("stdout: {}", e)))?;
371 needs_redraw = false;
372 }
373
374 match poll(timeout) {
376 Ok(true) => {
377 let event = read().map_err(|e| crate::error::Error::Runtime(format!("input: {}", e)))?;
378 match &mut mode {
381 InputMode::SearchPrompt { direction, buffer, error } => {
382 if let Event::Key(KeyEvent { code, .. }) = event {
383 match code {
384 KeyCode::Esc => { mode = InputMode::Normal; needs_redraw = true; }
385 KeyCode::Enter => {
386 if buffer.is_empty() {
387 if viewport.search_active() {
391 let reverse = !matches!(
392 (viewport.search_direction(), *direction),
393 (SearchDirection::Forward, SearchDirection::Forward)
394 | (SearchDirection::Backward, SearchDirection::Backward)
395 );
396 update_prev_position(&mut previous_position, current_file_index, viewport.top_line());
397 viewport.search_repeat(src.as_ref(), &mut idx, reverse);
398 }
399 mode = InputMode::Normal;
400 } else {
401 match viewport.set_search(buffer.clone(), *direction) {
402 Ok(()) => {
403 update_prev_position(&mut previous_position, current_file_index, viewport.top_line());
404 viewport.search_repeat(src.as_ref(), &mut idx, false);
405 mode = InputMode::Normal;
406 }
407 Err(e) => { *error = Some(e); }
408 }
409 }
410 needs_redraw = true;
411 }
412 KeyCode::Backspace => {
413 buffer.pop();
414 *error = None;
415 needs_redraw = true;
416 }
417 KeyCode::Char(c) => {
418 buffer.push(c);
419 *error = None;
420 needs_redraw = true;
421 }
422 _ => {}
423 }
424 }
425 continue;
426 }
427 InputMode::OptionPrefix => {
428 if let Event::Key(KeyEvent { code, .. }) = event {
429 match code {
430 KeyCode::Char('N') | KeyCode::Char('n') => viewport.toggle_line_numbers(),
431 KeyCode::Char('S') | KeyCode::Char('s') => viewport.toggle_chop(),
432 KeyCode::Char('F') | KeyCode::Char('f') => viewport.toggle_follow(),
433 KeyCode::Char('P') | KeyCode::Char('p') => {
434 mode = InputMode::PrettifyPrefix;
436 needs_redraw = true;
437 continue;
438 }
439 _ => {}
440 }
441 }
442 mode = InputMode::Normal;
443 needs_redraw = true;
444 continue;
445 }
446 InputMode::PrettifyPrefix => {
447 if let Event::Key(KeyEvent { code, .. }) = event {
448 let target: Option<PrettifyTarget> = match code {
449 KeyCode::Char('j') | KeyCode::Char('J') => Some(PrettifyTarget::Mode(PrettifyMode::Json)),
450 KeyCode::Char('y') | KeyCode::Char('Y') => Some(PrettifyTarget::Mode(PrettifyMode::Yaml)),
451 KeyCode::Char('t') | KeyCode::Char('T') => Some(PrettifyTarget::Mode(PrettifyMode::Toml)),
452 KeyCode::Char('x') | KeyCode::Char('X') => Some(PrettifyTarget::Mode(PrettifyMode::Xml)),
453 KeyCode::Char('h') | KeyCode::Char('H') => Some(PrettifyTarget::Mode(PrettifyMode::Html)),
454 KeyCode::Char('c') | KeyCode::Char('C') => Some(PrettifyTarget::Mode(PrettifyMode::Csv)),
455 KeyCode::Char('r') | KeyCode::Char('R') => Some(PrettifyTarget::Mode(PrettifyMode::Off)),
456 KeyCode::Char('a') | KeyCode::Char('A') => Some(PrettifyTarget::Auto),
457 _ => None,
458 };
459 if let Some(t) = target {
460 apply_prettify(
461 src.as_ref(),
462 &mut viewport,
463 &mut idx,
464 rebuild_spec,
465 t,
466 );
467 last_revision = src.revision();
468 }
469 }
470 mode = InputMode::Normal;
471 needs_redraw = true;
472 continue;
473 }
474 InputMode::MarkSetPending => {
475 if let Event::Key(KeyEvent { code: KeyCode::Char(c), .. }) = event {
476 if is_valid_mark_name(c) {
477 mark_set(&mut marks, c, current_file_index, viewport.top_line());
478 }
479 }
480 mode = InputMode::Normal;
481 continue;
482 }
483 InputMode::MarkJumpPending => {
484 if let Event::Key(KeyEvent { code: KeyCode::Char(c), .. }) = event {
485 if is_valid_mark_name(c) {
486 match mark_jump(&marks, c, current_file_index, &mut previous_position, viewport.top_line()) {
487 Some(MarkTarget::SameFile { line }) => {
488 let clamped = line.min(idx.line_count().saturating_sub(1));
489 viewport.goto_line(clamped, src.as_ref(), &mut idx);
490 needs_redraw = true;
491 }
492 Some(MarkTarget::OtherFile { file_index, line }) => {
493 if file_index < file_set.len() {
494 file_set.set_current_index(file_index);
495 let path = file_set.current().unwrap().to_path_buf();
496 if let Err(e) = switch_file(
497 &path, file_index, file_set.len(),
498 &args, preprocessor.as_ref(),
499 &mut viewport, &mut src, &mut idx,
500 record_start_regex.as_ref(),
501 ) {
502 transient_status = Some(format!("[open: {e}]"));
503 } else {
504 let clamped = line.min(idx.line_count().saturating_sub(1));
505 viewport.goto_line(clamped, src.as_ref(), &mut idx);
506 current_file_index = file_index;
507 needs_redraw = true;
508 }
509 }
510 }
511 None => {}
512 }
513 }
514 }
515 mode = InputMode::Normal;
516 continue;
517 }
518 InputMode::ShellPrompt { buffer, error } => {
519 if let Event::Key(KeyEvent { code, .. }) = event {
520 match code {
521 KeyCode::Esc => {
522 mode = InputMode::Normal;
523 needs_redraw = true;
524 }
525 KeyCode::Enter => {
526 if buffer.is_empty() {
527 mode = InputMode::Normal;
528 } else {
529 match crate::shell::run_shell_command(buffer) {
530 Ok(()) => {
531 mode = InputMode::Normal;
532 }
533 Err(e) => {
534 *error = Some(e.to_string());
535 }
536 }
537 }
538 needs_redraw = true;
539 }
540 KeyCode::Backspace => {
541 buffer.pop();
542 *error = None;
543 needs_redraw = true;
544 }
545 KeyCode::Char(c) => {
546 buffer.push(c);
547 *error = None;
548 needs_redraw = true;
549 }
550 _ => {}
551 }
552 }
553 continue;
554 }
555 InputMode::CtrlXPending => {
556 let is_ctrl_x = matches!(
557 event,
558 Event::Key(KeyEvent {
559 code: KeyCode::Char('x'),
560 modifiers: KeyModifiers::CONTROL,
561 ..
562 })
563 );
564 if is_ctrl_x {
565 match jump_previous(&mut previous_position, current_file_index, viewport.top_line()) {
566 Some(MarkTarget::SameFile { line }) => {
567 let clamped = line.min(idx.line_count().saturating_sub(1));
568 viewport.goto_line(clamped, src.as_ref(), &mut idx);
569 needs_redraw = true;
570 }
571 Some(MarkTarget::OtherFile { file_index, line }) => {
572 if file_index < file_set.len() {
573 file_set.set_current_index(file_index);
574 let path = file_set.current().unwrap().to_path_buf();
575 if let Err(e) = switch_file(
576 &path, file_index, file_set.len(),
577 &args, preprocessor.as_ref(),
578 &mut viewport, &mut src, &mut idx,
579 record_start_regex.as_ref(),
580 ) {
581 transient_status = Some(format!("[open: {e}]"));
582 } else {
583 let clamped = line.min(idx.line_count().saturating_sub(1));
584 viewport.goto_line(clamped, src.as_ref(), &mut idx);
585 current_file_index = file_index;
586 needs_redraw = true;
587 }
588 }
589 }
590 None => {}
591 }
592 mode = InputMode::Normal;
593 continue;
594 }
595 mode = InputMode::Normal;
597 }
599 InputMode::ColonPrompt { buffer, error } => {
600 if let Event::Key(KeyEvent { code, .. }) = event {
601 match code {
602 KeyCode::Esc => {
603 mode = InputMode::Normal;
604 needs_redraw = true;
605 }
606 KeyCode::Enter => {
607 if buffer.is_empty() {
608 mode = InputMode::Normal;
609 } else {
610 match parse_colon_command(buffer) {
611 Ok(cmd) => {
612 let outcome = dispatch_colon_command(
613 cmd,
614 &mut file_set,
615 &mut current_file_index,
616 &args,
617 preprocessor.as_ref(),
618 record_start_regex.as_ref(),
619 &mut viewport,
620 &mut src,
621 &mut idx,
622 );
623 match outcome {
624 ColonOutcome::Continue(msg) => {
625 transient_status = msg;
626 }
627 ColonOutcome::Quit => break,
628 }
629 mode = InputMode::Normal;
630 }
631 Err(e) => {
632 *error = Some(e.to_string());
633 }
634 }
635 }
636 needs_redraw = true;
637 }
638 KeyCode::Backspace => {
639 buffer.pop();
640 *error = None;
641 needs_redraw = true;
642 }
643 KeyCode::Char(c) => {
644 buffer.push(c);
645 *error = None;
646 needs_redraw = true;
647 }
648 _ => {}
649 }
650 }
651 continue;
652 }
653 InputMode::Normal => {}
654 }
655 let mut cmd: Option<Command> = None;
659 if let InputMode::Normal = mode {
660 if let Event::Key(ke) = &event {
661 if let Some(target) = keymap.lookup(ke) {
662 match target {
663 crate::keys::BindingTarget::Shell(cmd_text) => {
664 let cmd_text = cmd_text.clone();
665 if let Err(e) = crate::shell::run_shell_command(&cmd_text) {
666 let _ = writeln!(std::io::stderr(),
667 "[shell: {e}]");
668 }
669 needs_redraw = true;
670 continue;
671 }
672 crate::keys::BindingTarget::Command(c) => {
673 cmd = Some(c.clone());
674 }
675 }
676 }
677 }
678 }
679 let cmd = cmd.unwrap_or_else(|| translate(event));
680 let prefix_at_cmd = numeric_prefix.take();
683 match cmd {
684 Command::Digit(d) => {
685 let cur = prefix_at_cmd.unwrap_or(0);
686 let next = cur.saturating_mul(10).saturating_add(d as usize);
687 if next <= 99_999_999 {
688 numeric_prefix = Some(next);
689 } else {
690 numeric_prefix = prefix_at_cmd;
692 }
693 continue;
694 }
695 Command::Cancel => {
696 continue;
698 }
699 Command::GotoLine => {
700 update_prev_position(&mut previous_position, current_file_index, viewport.top_line());
701 match prefix_at_cmd {
702 Some(line) if line > 0 => viewport.goto_line(line - 1, src.as_ref(), &mut idx),
703 _ => viewport.goto_top(),
704 }
705 needs_redraw = true;
706 }
707 Command::GotoRecord => {
708 update_prev_position(&mut previous_position, current_file_index, viewport.top_line());
709 match prefix_at_cmd {
710 Some(rec) if rec > 0 => viewport.goto_record(rec - 1, src.as_ref(), &mut idx),
711 _ => viewport.goto_bottom(src.as_ref(), &mut idx),
712 }
713 needs_redraw = true;
714 }
715 Command::GotoPercent => {
716 update_prev_position(&mut previous_position, current_file_index, viewport.top_line());
717 match prefix_at_cmd {
718 Some(p) if p <= 100 => viewport.goto_percent(p as u8, src.as_ref(), &mut idx),
719 _ => viewport.goto_top(),
720 }
721 needs_redraw = true;
722 }
723 Command::Quit => break,
724 Command::Resize(c, r) => {
725 cols = c; rows = r;
726 viewport.resize(c, r);
727 needs_redraw = true;
728 }
729 Command::ScrollLines(n) => {
730 viewport.scroll_lines(n, src.as_ref(), &mut idx);
731 needs_redraw = true;
732 }
733 Command::ScrollLogicalLines(n) => {
734 viewport.scroll_logical_lines(n, src.as_ref(), &mut idx);
735 needs_redraw = true;
736 }
737 Command::PageDown => {
738 viewport.page_down(src.as_ref(), &mut idx);
739 needs_redraw = true;
740 }
741 Command::PageUp => {
742 viewport.page_up(src.as_ref(), &mut idx);
743 needs_redraw = true;
744 }
745 Command::HalfPageDown => {
746 viewport.half_page_down(src.as_ref(), &mut idx);
747 needs_redraw = true;
748 }
749 Command::HalfPageUp => {
750 viewport.half_page_up(src.as_ref(), &mut idx);
751 needs_redraw = true;
752 }
753 Command::Refresh => {
754 needs_redraw = true;
755 }
756 Command::Reload => {
757 src.pump();
760 if src.revision() != last_revision {
761 rebuild_after_replace(
762 src.as_ref(), &mut viewport, &mut idx, rebuild_spec,
763 );
764 last_revision = src.revision();
765 needs_redraw = true;
766 }
767 }
768 Command::TogglePrettify => {
769 apply_prettify(
770 src.as_ref(), &mut viewport, &mut idx, rebuild_spec,
771 PrettifyTarget::Toggle,
772 );
773 last_revision = src.revision();
774 needs_redraw = true;
775 }
776 Command::SetPrettifyMode(m) => {
777 apply_prettify(
778 src.as_ref(), &mut viewport, &mut idx, rebuild_spec,
779 PrettifyTarget::Mode(m),
780 );
781 last_revision = src.revision();
782 needs_redraw = true;
783 }
784 Command::RedetectPrettify => {
785 apply_prettify(
786 src.as_ref(), &mut viewport, &mut idx, rebuild_spec,
787 PrettifyTarget::Auto,
788 );
789 last_revision = src.revision();
790 needs_redraw = true;
791 }
792 Command::ToggleLineNumbers => {
793 viewport.toggle_line_numbers();
794 needs_redraw = true;
795 }
796 Command::ToggleChop => {
797 viewport.toggle_chop();
798 needs_redraw = true;
799 }
800 Command::ToggleFollow => {
801 viewport.toggle_follow();
802 if viewport.follow_mode() {
803 src.pump();
805 idx.notice_new_bytes(src.as_ref());
806 viewport.goto_bottom(src.as_ref(), &mut idx);
807 }
808 needs_redraw = true;
809 }
810 Command::SearchForward => {
811 mode = InputMode::SearchPrompt {
812 direction: SearchDirection::Forward,
813 buffer: String::new(),
814 error: None,
815 };
816 needs_redraw = true;
817 }
818 Command::SearchBackward => {
819 mode = InputMode::SearchPrompt {
820 direction: SearchDirection::Backward,
821 buffer: String::new(),
822 error: None,
823 };
824 needs_redraw = true;
825 }
826 Command::ShellEscape => {
827 mode = InputMode::ShellPrompt {
828 buffer: String::new(),
829 error: None,
830 };
831 needs_redraw = true;
832 }
833 Command::ColonPrompt => {
834 mode = InputMode::ColonPrompt {
835 buffer: String::new(),
836 error: None,
837 };
838 needs_redraw = true;
839 }
840 Command::NextMatch => {
841 update_prev_position(&mut previous_position, current_file_index, viewport.top_line());
842 if viewport.search_repeat(src.as_ref(), &mut idx, false) {
843 needs_redraw = true;
844 }
845 }
846 Command::PreviousMatch => {
847 update_prev_position(&mut previous_position, current_file_index, viewport.top_line());
848 if viewport.search_repeat(src.as_ref(), &mut idx, true) {
849 needs_redraw = true;
850 }
851 }
852 Command::OptionPrefix => {
853 mode = InputMode::OptionPrefix;
854 }
855 Command::MarkSet => {
856 mode = InputMode::MarkSetPending;
857 }
858 Command::MarkJump => {
859 mode = InputMode::MarkJumpPending;
860 }
861 Command::CtrlXPrefix => {
862 mode = InputMode::CtrlXPending;
863 }
864 Command::JumpPrevious => {
865 }
868 Command::Noop => {}
869 }
870 }
871 Ok(false) => {
872 if viewport.live_mode() {
874 let was_at_bottom = viewport.is_at_bottom(&idx);
875 src.pump();
876 if src.revision() != last_revision {
877 rebuild_after_replace(
878 src.as_ref(), &mut viewport, &mut idx, rebuild_spec,
879 );
880 if was_at_bottom {
881 viewport.goto_bottom(src.as_ref(), &mut idx);
882 }
883 last_revision = src.revision();
884 needs_redraw = true;
885 }
886 } else if viewport.follow_mode() {
887 let was_at_bottom = viewport.is_at_bottom(&idx);
888 src.pump();
889 let lines_before = idx.line_count();
890 idx.notice_new_bytes(src.as_ref());
891 viewport.extend_visible_lines(&idx, src.as_ref());
892 if idx.line_count() != lines_before {
893 needs_redraw = true;
894 if was_at_bottom {
895 viewport.goto_bottom(src.as_ref(), &mut idx);
896 }
897 }
898 } else if !src.is_complete() {
899 let lines_before = idx.line_count();
902 idx.notice_new_bytes(src.as_ref());
903 viewport.extend_visible_lines(&idx, src.as_ref());
904 if idx.line_count() != lines_before {
905 needs_redraw = true;
906 }
907 }
908 }
909 Err(_) => {
910 std::thread::sleep(timeout);
912 }
913 }
914 }
915 Ok(())
916}
917
918#[derive(Debug, Clone, Copy)]
920enum PrettifyTarget {
921 Mode(PrettifyMode),
923 Toggle,
925 Auto,
927}
928
929fn apply_prettify(
933 src: &dyn Source,
934 viewport: &mut Viewport,
935 idx: &mut LineIndex,
936 spec: RebuildSpec,
937 target: PrettifyTarget,
938) {
939 if src.prettify_mode().is_none() {
941 return;
942 }
943 match target {
944 PrettifyTarget::Mode(m) => src.set_prettify_mode(m),
945 PrettifyTarget::Toggle => src.toggle_prettify(),
946 PrettifyTarget::Auto => src.redetect_prettify(),
947 }
948 rebuild_after_replace(src, viewport, idx, spec);
949 viewport.set_prettify_label(src.prettify_label());
950}
951
952fn rebuild_after_replace(
958 src: &dyn Source,
959 viewport: &mut Viewport,
960 idx: &mut LineIndex,
961 spec: RebuildSpec,
962) {
963 let new_off = match spec.tail {
964 Some(n) => find_tail_offset(src, n),
965 None => 0,
966 };
967 *idx = LineIndex::new_starting_at(new_off);
968 if let Some(n) = spec.head {
969 idx.set_head_cap(n);
970 }
971 viewport.invalidate_filter_cache();
972 idx.notice_new_bytes(src);
973 viewport.extend_visible_lines(idx, src);
974 viewport.clamp_top_line(idx.line_count());
975}
976
977fn write_frame(out: &mut impl Write, frame: &Frame, cols: u16, rows: u16) -> io::Result<()> {
978 out.queue(SetAttribute(Attribute::Reset))?;
982 out.queue(ResetColor)?;
983 out.queue(Clear(ClearType::All))?;
984 for (i, row) in frame.body.iter().enumerate() {
985 out.queue(MoveTo(0, i as u16))?;
986 out.queue(SetAttribute(Attribute::Reset))?;
989 let style = frame.row_styles.get(i).copied().unwrap_or(RowStyle::Normal);
990 if matches!(style, RowStyle::Dim) {
991 out.queue(SetAttribute(Attribute::Dim))?;
992 }
993 let no_highlights = Vec::new();
994 let highlights = frame.highlights.get(i).unwrap_or(&no_highlights);
995 write_row_with_highlights(out, row, cols, highlights)?;
996 out.queue(SetAttribute(Attribute::Reset))?;
997 }
998 out.queue(MoveTo(0, rows.saturating_sub(1)))?;
1000 out.queue(SetAttribute(Attribute::Reverse))?;
1001 let mut status = frame.status.clone();
1002 if status.len() > cols as usize {
1003 status.truncate(cols as usize);
1004 } else {
1005 let pad = cols as usize - status.len();
1006 status.push_str(&" ".repeat(pad));
1007 }
1008 out.queue(Print(status))?;
1009 out.queue(ResetColor)?;
1010 out.queue(SetAttribute(Attribute::Reset))?;
1011 out.flush()
1012}
1013
1014fn cells_to_string(row: &[Cell], cols: u16) -> String {
1015 let mut s = String::with_capacity(cols as usize);
1016 for cell in row.iter().take(cols as usize) {
1017 match cell {
1018 Cell::Char { ch, .. } => s.push(*ch),
1019 Cell::Continuation => { }
1020 Cell::Empty => s.push(' '),
1021 }
1022 }
1023 s
1024}
1025
1026fn write_row_with_highlights(
1032 out: &mut impl Write,
1033 row: &[Cell],
1034 cols: u16,
1035 highlights: &[std::ops::Range<usize>],
1036) -> io::Result<()> {
1037 let cols_usize = cols as usize;
1038 if highlights.is_empty() {
1039 out.queue(Print(cells_to_string(row, cols)))?;
1040 return Ok(());
1041 }
1042 let mut ranges: Vec<std::ops::Range<usize>> = highlights
1044 .iter()
1045 .filter_map(|r| {
1046 let s = r.start.min(cols_usize);
1047 let e = r.end.min(cols_usize);
1048 if e > s { Some(s..e) } else { None }
1049 })
1050 .collect();
1051 ranges.sort_by_key(|r| r.start);
1052
1053 let mut col = 0usize;
1054 let mut i = 0usize;
1055 while col < cols_usize && i < row.len() {
1056 let active = ranges.iter().find(|r| r.start <= col && col < r.end);
1058 let (segment_end, reversed) = match active {
1059 Some(r) => (r.end.min(cols_usize), true),
1060 None => {
1061 let next = ranges.iter().find(|r| r.start > col).map(|r| r.start);
1063 (next.unwrap_or(cols_usize), false)
1064 }
1065 };
1066 if reversed { out.queue(SetAttribute(Attribute::Reverse))?; }
1067 let mut s = String::new();
1069 while col < segment_end && i < row.len() {
1070 match &row[i] {
1071 Cell::Char { ch, width } => {
1072 s.push(*ch);
1073 col += *width as usize;
1074 }
1075 Cell::Continuation => {
1076 }
1078 Cell::Empty => {
1079 s.push(' ');
1080 col += 1;
1081 }
1082 }
1083 i += 1;
1084 }
1085 out.queue(Print(s))?;
1086 if reversed { out.queue(SetAttribute(Attribute::NoReverse))?; }
1087 }
1088 Ok(())
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093 use super::*;
1094
1095 #[test]
1096 fn parse_colon_n() {
1097 assert_eq!(parse_colon_command("n").unwrap(), ColonCommand::Next);
1098 assert_eq!(parse_colon_command("next").unwrap(), ColonCommand::Next);
1099 }
1100
1101 #[test]
1102 fn parse_colon_p() {
1103 assert_eq!(parse_colon_command("p").unwrap(), ColonCommand::Prev);
1104 assert_eq!(parse_colon_command("prev").unwrap(), ColonCommand::Prev);
1105 }
1106
1107 #[test]
1108 fn parse_colon_e_with_path() {
1109 match parse_colon_command("e /tmp/foo.log").unwrap() {
1110 ColonCommand::Edit(p) => assert_eq!(p, std::path::PathBuf::from("/tmp/foo.log")),
1111 other => panic!("expected Edit, got {other:?}"),
1112 }
1113 }
1114
1115 #[test]
1116 fn parse_colon_e_with_tilde() {
1117 std::env::set_var("HOME", "/home/user");
1118 match parse_colon_command("e ~/foo.log").unwrap() {
1119 ColonCommand::Edit(p) => assert_eq!(p, std::path::PathBuf::from("/home/user/foo.log")),
1120 other => panic!("expected Edit, got {other:?}"),
1121 }
1122 }
1123
1124 #[test]
1125 fn parse_colon_e_missing_path_errors() {
1126 assert_eq!(parse_colon_command("e").unwrap_err(), ColonParseError::MissingPath);
1127 assert_eq!(parse_colon_command("e ").unwrap_err(), ColonParseError::MissingPath);
1128 }
1129
1130 #[test]
1131 fn parse_colon_f_q_d_x_t() {
1132 assert_eq!(parse_colon_command("f").unwrap(), ColonCommand::ShowFile);
1133 assert_eq!(parse_colon_command("q").unwrap(), ColonCommand::Quit);
1134 assert_eq!(parse_colon_command("d").unwrap(), ColonCommand::Delete);
1135 assert_eq!(parse_colon_command("x").unwrap(), ColonCommand::First);
1136 assert_eq!(parse_colon_command("t").unwrap(), ColonCommand::Last);
1137 }
1138
1139 #[test]
1140 fn parse_unknown_command_errors() {
1141 let err = parse_colon_command("bogus").unwrap_err();
1142 match err {
1143 ColonParseError::UnknownCommand(name) => assert_eq!(name, "bogus"),
1144 other => panic!("expected UnknownCommand, got {other:?}"),
1145 }
1146 }
1147
1148 #[test]
1149 fn parse_handles_whitespace() {
1150 assert_eq!(parse_colon_command("n ").unwrap(), ColonCommand::Next);
1152 assert_eq!(parse_colon_command(" n").unwrap(), ColonCommand::Next);
1153 }
1154}