mod rope; mod term; use rope::Rope; use std::error::Error; use std::ffi::{OsStr, OsString}; use std::io::{stdout, BufReader, Stdin, Write}; use std::ops::{Deref, DerefMut}; use std::path::Path; use term::Key; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Mode { Insert, Paste, Normal, } #[derive(Debug, Clone)] struct State { buf: Rope, row_start: usize, line_offset: usize, cursor_row: u16, cursor_col: u16, line_cols: u16, mode: Mode, } #[derive(Debug)] struct StateHistory { stdin: BufReader, i: usize, history: Vec, curr: State, } impl Deref for StateHistory { type Target = State; fn deref(&self) -> &Self::Target { return &self.curr; } } impl DerefMut for StateHistory { fn deref_mut(&mut self) -> &mut Self::Target { return &mut self.curr; } } impl StateHistory { fn repaint_all(&mut self) -> Result<(), Box> { let size = term::size()?; let mut stdout = stdout().lock(); write!(stdout, "\x1b[H\x1b[J")?; for i in self.row_start..self.row_start + usize::from(size.ws_row) { if i > self.row_start { write!(stdout, "\r\n")?; } if i > self.buf.lines() { break; } self.buf.line(i).print(&mut stdout)?; if let Ok((_, col)) = term::cursor_pos(&mut self.stdin) { if col == size.ws_col - 1 { let _ = write!(stdout, "\x1b[{}G\x1b[30m\x1b[47m>\x1b[m ", size.ws_col - 1); } } } return Ok(()); } fn repaint_line_full(&mut self) -> Result<(), Box> { let term_size = term::size()?; let mut stdout = stdout().lock(); write!(stdout, "\x1b[{}H\x1b[K", self.cursor_row + 1)?; let line = self.buf.line(self.row_start + usize::from(self.cursor_row)); line.print(&mut stdout)?; let (_, col) = match term::cursor_pos(&mut self.stdin) { Ok(x) => x, Err(err) => { let _ = write!(stdout, "\x1b[{}G", self.cursor_col + 1); return Err(err); } }; self.line_cols = col; let mut truncated = false; if col == term_size.ws_col - 1 { truncated = true; self.line_cols = col - 1; } if self.cursor_col >= self.line_cols { self.line_offset = line.len(); if truncated { write!( stdout, "\x1b[{}G\x1b[30m\x1b[47m>\x1b[m \x1b[{}G", term_size.ws_col - 1, self.line_cols + 1, )?; } return Ok(()); } let mut lo = 0; let mut hi = line.len(); // Binary search :D while hi > lo { let x = line.floor_char_boundary(lo + (hi - lo) / 2); write!(stdout, "\x1b[G")?; line.slice(0, x).print(&mut stdout)?; let (_, col) = match term::cursor_pos(&mut self.stdin) { Ok(x) => x, Err(err) => { let _ = write!(stdout, "\x1b[{}G", self.cursor_col + 1); return Err(err); } }; if col <= self.cursor_col { lo = line.ceil_char_boundary(x + 1); } else { hi = x; } } self.line_offset = line.floor_char_boundary(lo - 1); write!(stdout, "\x1b[G")?; line.print(&mut stdout)?; if truncated { write!( stdout, "\x1b[{}G\x1b[30m\x1b[47m>\x1b[m ", term_size.ws_col - 1 )?; } write!(stdout, "\x1b[{}G", self.cursor_col + 1)?; return Ok(()); } fn up(&mut self) -> Result<(), Box> { if self.cursor_row == 0 && self.row_start == 0 { return Ok(()); } if self.cursor_row == 0 { self.row_start -= 1; self.repaint_all()?; } else { self.cursor_row -= 1; } self.repaint_line_full()?; return Ok(()); } fn down(&mut self) -> Result<(), Box> { if self.row_start + usize::from(self.cursor_row) == self.buf.lines() { return Ok(()); } let size = term::size()?; if self.cursor_row == size.ws_row - 1 { self.row_start += 1; self.repaint_all()?; } else { self.cursor_row += 1; } self.repaint_line_full()?; return Ok(()); } fn left(&mut self) -> Result<(), Box> { if self.cursor_col > self.line_cols { self.cursor_col = self.line_cols; } if self.cursor_col == 0 && self.cursor_row == 0 && self.row_start == 0 { return Ok(()); } if self.cursor_col == 0 { self.up()?; self.cursor_col = self.line_cols; } else { self.cursor_col -= 1; } self.repaint_line_full()?; return Ok(()); } fn right(&mut self) -> Result<(), Box> { if self.cursor_col >= self.line_cols && self.row_start + usize::from(self.cursor_row) == self.buf.lines() { return Ok(()); } if self.cursor_col >= self.line_cols { self.down()?; self.cursor_col = 0; } else { self.cursor_col += 1; } self.repaint_line_full()?; return Ok(()); } fn set_mode(&mut self, mode: Mode) { match (self.mode, mode) { (Mode::Normal, Mode::Normal) => (), (Mode::Normal, _) => { self.i += 1; self.history.truncate(self.i); self.history.push(self.curr.clone()); } (Mode::Paste, Mode::Paste) => (), (Mode::Paste, _) => { let Ok((_, col)) = term::cursor_pos(&mut self.stdin) else { return; }; self.cursor_col = col; let _ = self.repaint_all(); let _ = self.repaint_line_full(); let _ = stdout().flush(); } _ => (), } self.mode = mode; } } fn edit(file: &OsStr) -> Result<(), Box> { let _raw_handle = term::raw().map_err(|err| format!("cannot put terminal in raw mode: {}", err))?; let r = Rope::open(Path::new(file))?; let size = term::size()?; let initial_state = State { buf: r, mode: Mode::Normal, row_start: 0, cursor_row: 0, cursor_col: 0, line_offset: 0, line_cols: 0, }; let mut state = StateHistory { stdin: BufReader::new(std::io::stdin()), i: 0, history: vec![initial_state.clone()], curr: initial_state, }; let _ = state.repaint_all(); let _ = write!(stdout(), "\x1b[H"); let _ = stdout().flush(); loop { let c = term::read_key(&mut state.stdin)?; match c { Key::Timeout => { if state.mode != Mode::Paste { continue; } state.set_mode(Mode::Insert); } Key::CtrlQ => { break; } Key::CtrlS => { state.set_mode(Mode::Normal); let _ = state.buf.save(Path::new(file)); } Key::CtrlZ => { state.set_mode(Mode::Normal); if state.i == state.history.len() - 1 { state.history.push(state.curr.clone()); } else if state.i > 0 { state.i -= 1; } state.curr = state.history[state.i].clone(); let _ = state.repaint_all(); let _ = state.repaint_line_full(); let _ = stdout().flush(); } Key::CtrlY => { state.set_mode(Mode::Normal); if state.i == state.history.len() - 1 { continue; } state.i += 1; state.curr = state.history[state.i].clone(); let _ = state.repaint_all(); let _ = state.repaint_line_full(); let _ = stdout().flush(); } Key::Up => { state.set_mode(Mode::Normal); let _ = state.up(); let _ = stdout().flush(); } Key::Down => { state.set_mode(Mode::Normal); let _ = state.down(); let _ = stdout().flush(); } Key::Left => { state.set_mode(Mode::Normal); let _ = state.left(); let _ = stdout().flush(); } Key::Right => { state.set_mode(Mode::Normal); let _ = state.right(); let _ = stdout().flush(); } Key::Backspace => { state.set_mode(Mode::Insert); if state.row_start == 0 && state.cursor_row == 0 && state.line_offset == 0 { continue; } let line_start = state .buf .line_idx(state.row_start + usize::from(state.cursor_row)); let gap_end = line_start + state.line_offset; let gap_start = state.buf.floor_char_boundary(gap_end - 1); if state.line_offset == 0 { let _ = state.left(); } else { let _ = write!(stdout(), "\x1b[G"); let _ = state.buf.slice(line_start, gap_start).print(&mut stdout()); match term::cursor_pos(&mut state.stdin) { Ok((_, col)) => { state.cursor_col = col; } Err(_) => { state.cursor_col -= 1; } } } state.buf = state .buf .slice(0, gap_start) .concat(&state.buf.slice(gap_end, state.buf.len())); let _ = state.repaint_all(); let _ = state.repaint_line_full(); let _ = stdout().flush(); } Key::Char(b'\r') => { state.set_mode(Mode::Paste); state.buf = state.buf.insert( state .buf .line_idx(state.row_start + usize::from(state.cursor_row)) + state.line_offset, b'\n', ); if state.cursor_row == size.ws_row - 1 { state.row_start += 1; } else { state.cursor_row += 1; } state.cursor_col = 0; state.line_offset = 0; let _ = write!(stdout(), "\r\n"); let _ = stdout().flush(); } Key::Char(c) => { state.set_mode(Mode::Paste); state.buf = state.buf.insert( state .buf .line_idx(state.row_start + usize::from(state.cursor_row)) + state.line_offset, c, ); state.line_offset += 1; let _ = stdout().write_all(&vec![c]); let _ = stdout().flush(); } } } return Ok(()); } fn main() { let args: Vec = std::env::args_os().collect(); if args.len() != 2 { println!("Usage: edit "); std::process::exit(1); } if let Err(err) = edit(&args[1]) { println!("FAIL: {}", err); std::process::exit(1); } }