mod rope; mod term; use rope::Rope; use std::ffi::{OsStr, OsString}; use std::io::{stdout, stdin, Write, Read}; use std::error::Error; use std::path::Path; use term::Key; struct State { buf: Rope, cursor_row: usize, cursor_col: usize, line_offset: usize, line_cols: usize, } fn parse_status_report(buf: &[u8]) -> Result<(usize, usize), Box> { let mut semicolon = 0; for (i, &c) in buf.iter().enumerate() { if c == b';' { semicolon = i; break; } } if semicolon < 2 || semicolon == buf.len()-1 { return Err(Box::from("invalid response")); } let row_str = std::str::from_utf8(&buf[2..semicolon])?; let col_str = std::str::from_utf8(&buf[semicolon+1..buf.len()-1])?; let row = row_str.parse::()?; let col = col_str.parse::()?; // Yuck... 1 indexing return Ok((row-1, col-1)); } impl State { fn cursor_pos(&mut self) -> Result<(usize, usize), Box> { write!(stdout(), "\x1b[6n").map_err(|err| format!("cursor position: status report: {}", err))?; stdout().flush().map_err(|err| format!("cursor position: status report: {}", err))?; let mut buf = vec![0; 20]; let n = stdin().read(&mut buf).map_err(|err| format!("cursor position: read status report: {}", err))?; buf.truncate(n); let pos = parse_status_report(&buf).map_err(|_| format!("cursor position: invalid response: {}", String::from_utf8_lossy(&buf)))?; return Ok(pos); } fn repaint_full(&mut self) -> Result<(), Box> { let size = term::size()?; write!(stdout(), "\x1b[H\x1b[J")?; for i in 0..usize::from(size.ws_row) { if i > 0 { write!(stdout(), "\r\n")?; } if i > self.buf.lines() { break; } let line = self.buf.line(i); let max_len = line.char(usize::from(size.ws_col-1)); line.slice(0, max_len).print(&mut stdout())?; if max_len < line.len() { write!(stdout(), "\x1b[30m\x1b[47m>\x1b[m")?; } } return Ok(()); } fn repaint_line_full(&mut self) -> Result<(), Box> { let term_size = term::size()?; write!(stdout(), "\x1b[{}H\x1b[K", self.cursor_row+1)?; let mut line = self.buf.line(self.cursor_row); let max_len = line.char(usize::from(term_size.ws_col-1)); let mut truncated = false; if max_len < line.len() { line = line.slice(0, max_len); truncated = true; } line.print(&mut stdout())?; let (_, col) = self.cursor_pos()?; self.line_cols = col; if self.cursor_col >= self.line_cols { self.line_offset = line.len(); if truncated { write!(stdout(), "\x1b[30m\x1b[47m>\x1b[m\x1b[{}G", self.cursor_col+1)?; } return Ok(()); } let mut lo = line.char(self.cursor_col); 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) = self.cursor_pos()?; 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[30m\x1b[47m>\x1b[m")?; } write!(stdout(), "\x1b[{}G", self.cursor_col+1)?; return Ok(()); } } 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 mut state = State{ buf: r, cursor_row: 0, cursor_col: 0, line_offset: 0, line_cols: 0, }; let _ = state.repaint_full(); let _ = write!(stdout(), "\x1b[H"); let _ = stdout().flush(); loop { let c = term::read_key(&mut stdin()).map_err(|err| format!("read key: {}", err))?; match c { Key::CtrlQ => { break; } Key::CtrlS => { let _ = state.buf.save(Path::new(file)); } Key::Up => { if state.cursor_row == 0 { continue; } state.cursor_row -= 1; let _ = state.repaint_line_full(); let _ = stdout().flush(); } Key::Down => { if state.cursor_row == usize::from(size.ws_row - 1) || state.cursor_row == state.buf.lines() { continue; } state.cursor_row += 1; let _ = state.repaint_line_full(); let _ = stdout().flush(); } Key::Left => { state.cursor_col = std::cmp::min(state.cursor_col, state.line_cols); if state.cursor_col == 0 { state.cursor_row -= 1; let _ = state.repaint_line_full(); state.cursor_col = state.line_cols; let _ = state.repaint_line_full(); } else { state.cursor_col -= 1; let _ = state.repaint_line_full(); } let _ = stdout().flush(); } Key::Right => { if state.cursor_col >= state.line_cols { state.cursor_row += 1; state.cursor_col = 0; } else { state.cursor_col += 1; } let _ = state.repaint_line_full(); let _ = stdout().flush(); } Key::Char(c) => { state.cursor_col = std::cmp::min(state.cursor_col, state.line_cols); state.buf = state.buf.insert(state.buf.line_idx(state.cursor_row)+state.line_offset, c); state.cursor_col += 1; let _ = state.repaint_line_full(); 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); } }