mod rope; mod term; use rope::Rope; use std::ffi::{OsStr, OsString}; use std::io::{stdout, stdin, Write, Read}; use std::path::Path; use term::Key; struct State { buf: Rope, cursor_row: usize, cursor_col: usize, line_offset: usize, line_cols: usize, } impl State { fn cursor_pos(&mut self) -> Result<(usize, usize), String> { if let Err(err) = write!(stdout(), "\x1b[6n") { return Err(format!("cursor position: status report: {}", err)); } if let Err(err) = stdout().flush() { return Err(format!("cursor position: status report: {}", err)); } let mut buf = vec![0; 20]; let n = match stdin().read(&mut buf) { Ok(n) => n, Err(err) => { return Err(format!("cursor position: read status report: {}", err)); } }; buf.truncate(n); let mut semicolon = 0; for (i, &c) in buf.iter().enumerate() { if c == b';' { semicolon = i; break; } } let row_str = match std::str::from_utf8(&buf[2..semicolon]) { Ok(row_str) => row_str, Err(_) => { return Err(format!("cursor position: invalid response {}", String::from_utf8_lossy(&buf))); } }; let col_str = match std::str::from_utf8(&buf[semicolon+1..buf.len()-1]) { Ok(col_str) => col_str, Err(_) => { return Err(format!("cursor position: invalid response {}", String::from_utf8_lossy(&buf))); } }; let row = match row_str.parse::() { Ok(row) => row, Err(_) => { return Err(format!("cursor position: invalid response {}", String::from_utf8_lossy(&buf))); } }; let col = match col_str.parse::() { Ok(col) => col, Err(_) => { return Err(format!("cursor position: invalid response {}", String::from_utf8_lossy(&buf))); } }; // Yuck... 1 indexing return Ok((row-1, col-1)); } fn repaint_line_full(&mut self) { let _ = write!(stdout(), "\x1b[{}H", self.cursor_row+1); let line = self.buf.line(self.cursor_row); let _ = line.print(&mut stdout()); let (_, col) = self.cursor_pos().expect("asdf"); self.line_cols = col; if self.cursor_col >= self.line_cols { self.line_offset = line.len(); return; } let mut lo = line.char(self.cursor_col).expect("at this point, cursor_col should have some valid offset"); let mut hi = line.len(); // Binary search :D while hi > lo { let x = line.floor_char_boundary(lo + (hi - lo) / 2); let _ = write!(stdout(), "\x1b[G"); let _ = line.slice(0, x).print(&mut stdout()); let (_, col) = self.cursor_pos().expect("ouchie, I should really handle this I guess"); if col <= self.cursor_col { lo = line.ceil_char_boundary(x+1); } else { hi = x; } } self.line_offset = line.floor_char_boundary(lo-1); let _ = write!(stdout(), "\x1b[G\x1b[K"); let _ = line.print(&mut stdout()); let _ = write!(stdout(), "\x1b[{}G", self.cursor_col+1); } } fn edit(file: &OsStr) -> Result<(), String> { let _raw_handle = match term::raw() { Some(h) => h, None => { return Err(String::from("cannot put terminal in raw mode")); } }; let r = match Rope::open(Path::new(file)) { Ok(r) => r, Err(err) => { return Err(format!("open file {}: {}", file.to_string_lossy(), err)); } }; let size = match term::size() { Some(size) => size, None => { return Err(String::from("cannot get terminal size")); } }; let mut state = State{ buf: r, cursor_row: 0, cursor_col: 0, line_offset: 0, line_cols: 0, }; let _ = write!(stdout(), "\x1b[J"); for i in 0..usize::from(size.ws_row) { if i > 0 { let _ = write!(stdout(), "\r\n"); } if i > state.buf.lines() { break; } let line = state.buf.line(i); // TODO: handle line-wrapping. let _ = line.print(&mut stdout()); if i == 0 { let (_, col) = state.cursor_pos()?; state.line_cols = col; } } let _ = write!(stdout(), "\x1b[H"); let _ = stdout().flush(); loop { let c = match term::read_key(&mut stdin()) { Ok(c) => c, Err(err) => { return Err(err); } }; match c { Key::CtrlQ => { break; } Key::CtrlS => { if let Err(err) = state.buf.save(Path::new(file)) { return Err(format!("write file {}: {}", file.to_string_lossy(), err)); } } Key::Up => { if state.cursor_row == 0 { continue; } state.cursor_row -= 1; 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; 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 { continue; } state.cursor_col -= 1; state.repaint_line_full(); let _ = stdout().flush(); } Key::Right => { if state.cursor_col >= state.line_cols { continue; } state.cursor_col += 1; 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; 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); } }