use std::io::{self, Write};
use crossterm::{
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
execute, terminal,
};
use regex::Regex;
use crate::buffer::LineBuffer;
use crate::flags::Flags;
use crate::layout::Layout;
use crate::render::{self, Viewport};
use crate::search::Search;
pub fn dump(buf: &LineBuffer) -> anyhow::Result<()> {
let stdout = io::stdout();
let mut out = stdout.lock();
let n = buf.lines().len();
for (i, line) in buf.lines().iter().enumerate() {
out.write_all(line.as_bytes())?;
if i + 1 < n || buf.final_newline() {
out.write_all(b"\n")?;
}
}
Ok(())
}
pub fn run(flags: Flags, buf: LineBuffer) -> anyhow::Result<()> {
let _guard = TerminalGuard::enter(!flags.no_init)?;
let (cols, rows) = terminal::size()?;
let mut vp = Viewport { top: 0, rows, cols };
let mut chop = flags.chop_long_lines;
let mut layout = Layout::build(&buf, cols, chop);
let mut search: Option<Search> = None;
let mut search_input: Option<String> = None;
let mut preview: Option<Regex> = None;
let mut info: Option<String> = None;
loop {
preview = match search_input.as_deref() {
None | Some("") => None,
Some(input) => Regex::new(input).ok().or(preview),
};
let status = compute_status(&vp, &layout, search_input.as_deref(), info.as_deref());
let highlight: Option<&Regex> = match preview.as_ref() {
Some(re) => Some(re),
None => search.as_ref().map(|s| &s.pattern),
};
render::draw(&buf, &layout, &vp, &flags, &status, highlight)?;
let event = event::read()?;
info = None;
match event {
Event::Key(key) => {
let in_search = search_input.is_some();
if in_search {
let action = handle_search_key(key, search_input.as_mut().unwrap());
match action {
SearchAction::Continue => {}
SearchAction::Exit => search_input = None,
SearchAction::Commit(pattern) => {
search_input = None;
commit_search(&pattern, &mut search, &mut vp, &buf, &layout, &mut info);
}
}
} else {
match handle_normal_key(key, &mut vp, &buf, &layout, &mut search, &mut info) {
NormalAction::Quit => break,
NormalAction::EnterSearch => search_input = Some(String::new()),
NormalAction::ToggleChop => {
chop = !chop;
rebuild_layout(&mut layout, &buf, &mut vp, chop);
}
NormalAction::Continue => {}
}
}
}
Event::Resize(w, h) => {
vp.cols = w;
vp.rows = h;
rebuild_layout(&mut layout, &buf, &mut vp, chop);
}
_ => {}
}
}
Ok(())
}
#[derive(PartialEq, Eq)]
enum NormalAction {
Continue,
EnterSearch,
ToggleChop,
Quit,
}
enum SearchAction {
Continue,
Exit,
Commit(String),
}
fn compute_status(
vp: &Viewport,
layout: &Layout,
search_input: Option<&str>,
info: Option<&str>,
) -> String {
if let Some(msg) = info {
return msg.to_string();
}
if let Some(input) = search_input {
return format!("/{}", input);
}
let at_end = vp.top + vp.content_rows() >= layout.len();
if at_end { "(END)".into() } else { ":".into() }
}
fn handle_normal_key(
key: KeyEvent,
vp: &mut Viewport,
buf: &LineBuffer,
layout: &Layout,
search: &mut Option<Search>,
info: &mut Option<String>,
) -> NormalAction {
let content_rows = vp.content_rows();
let max_top = layout.len().saturating_sub(content_rows);
match key.code {
KeyCode::Char('q') => return NormalAction::Quit,
KeyCode::Char('/') => return NormalAction::EnterSearch,
KeyCode::Char('S') => return NormalAction::ToggleChop,
KeyCode::Char('n') => match search.as_mut() {
Some(s) => match s.find_next(buf.lines()) {
Some(line) => set_top_to_line(vp, layout, line, max_top),
None => *info = Some("Pattern not found".into()),
},
None => *info = Some("No previous search".into()),
},
KeyCode::Char('j') | KeyCode::Down | KeyCode::Enter => {
vp.top = (vp.top + 1).min(max_top);
}
KeyCode::Char('k') | KeyCode::Up => {
vp.top = vp.top.saturating_sub(1);
}
KeyCode::Char(' ') | KeyCode::PageDown => {
vp.top = (vp.top + content_rows).min(max_top);
}
KeyCode::Char('b') | KeyCode::PageUp => {
vp.top = vp.top.saturating_sub(content_rows);
}
KeyCode::Char('g') | KeyCode::Home => vp.top = 0,
KeyCode::Char('G') | KeyCode::End => vp.top = max_top,
_ => {}
}
NormalAction::Continue
}
fn handle_search_key(key: KeyEvent, input: &mut String) -> SearchAction {
let is_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
match key.code {
KeyCode::Esc => SearchAction::Exit,
KeyCode::Char('c') if is_ctrl => SearchAction::Exit,
KeyCode::Enter => SearchAction::Commit(std::mem::take(input)),
KeyCode::Backspace => {
if input.pop().is_none() {
SearchAction::Exit
} else {
SearchAction::Continue
}
}
KeyCode::Char(c) if !is_ctrl => {
input.push(c);
SearchAction::Continue
}
_ => SearchAction::Continue,
}
}
fn commit_search(
pattern: &str,
search: &mut Option<Search>,
vp: &mut Viewport,
buf: &LineBuffer,
layout: &Layout,
info: &mut Option<String>,
) {
if pattern.is_empty() {
return;
}
match Search::new(pattern) {
Ok(mut s) => {
let max_top = layout.len().saturating_sub(vp.content_rows());
let from_line = layout.segment(vp.top).map(|s| s.line_idx).unwrap_or(0);
match s.find_forward(buf.lines(), from_line) {
Some(line) => set_top_to_line(vp, layout, line, max_top),
None => *info = Some("Pattern not found".into()),
}
*search = Some(s);
}
Err(e) => *info = Some(format!("Invalid regex: {}", e)),
}
}
fn set_top_to_line(vp: &mut Viewport, layout: &Layout, line: usize, max_top: usize) {
if let Some(seg) = layout.first_segment_of(line) {
vp.top = seg.min(max_top);
}
}
fn rebuild_layout(layout: &mut Layout, buf: &LineBuffer, vp: &mut Viewport, chop: bool) {
let anchor_line = layout.segment(vp.top).map(|s| s.line_idx);
layout.rebuild(buf, vp.cols, chop);
let max_top = layout.len().saturating_sub(vp.content_rows());
if let Some(line) = anchor_line {
if let Some(seg_idx) = layout.first_segment_of(line) {
vp.top = seg_idx.min(max_top);
return;
}
}
if vp.top > max_top {
vp.top = max_top;
}
}
struct TerminalGuard {
alt_screen: bool,
}
impl TerminalGuard {
fn enter(alt_screen: bool) -> io::Result<Self> {
terminal::enable_raw_mode()?;
if alt_screen {
execute!(io::stdout(), terminal::EnterAlternateScreen)?;
}
Ok(Self { alt_screen })
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
if self.alt_screen {
let _ = execute!(io::stdout(), terminal::LeaveAlternateScreen);
}
let _ = terminal::disable_raw_mode();
}
}