//use std::cmp::{min, max}; //use std::io::Write as IoWrite; use std::collections::HashMap; use std::fmt::Write as FmtWrite; use std::io::{Seek, SeekFrom}; use std::fs; use std::io::Read; use std::env; use std::iter::FromIterator; //use std::path::Path; extern crate termios; use termios::{Termios, TCSANOW, ECHO, ICANON, tcsetattr}; extern crate termion; use termion::color; //use termion::{color, input}; use termion::input::TermRead; use termion::event::{Event, Key}; use termion::screen::AlternateScreen; use termion::raw::IntoRawMode; use termion::clear; use termion::cursor; fn main() { match termion::terminal_size() { Ok((w, h)) => { if let Some(arg1) = env::args().nth(1) { println!("You would like to work with {}.", arg1); launch_interface(w, h, arg1.clone()); } else { usage(); } } Err(e) => { println!("{}", e); } } } fn usage() { println!("usage: this_thing filename"); } struct CachingFileView { file: std::fs::File, filelen: u64, cache_seek: u64, cache_size: usize, cache_len: usize, cache: Vec } impl FileView for CachingFileView { fn get_bytes(&mut self, offset: u64, len: u64) -> Vec { if len > self.cache_size as u64 { panic!("Caching view does not support get_bytes() larger than cache size"); } // check if we have to update the cache if offset <= self.cache_seek as u64 || offset + len > self.cache_seek + self.cache_len as u64 { // we do... self.cache.clear(); self.cache.resize(self.cache_size as usize, 0); // Seek such that the requested bytes are the middle of the cache, if possible // theory is this is a decent compromise for not knowing which direction // to predict motion towards self.cache_seek = std::cmp::max(offset as i64 - (self.cache_size as i64 / 2), 0) as u64; self.file.seek(SeekFrom::Start(self.cache_seek)).unwrap(); self.file.read(&mut self.cache).unwrap(); self.cache_len = self.cache.len(); } // ok now that's ``cached``... let cached_start = (offset - self.cache_seek) as usize; let cached_end = cached_start + len as usize; Vec::from_iter(self.cache[cached_start..cached_end].iter().cloned()) } fn size(&self) -> u64 { self.filelen } } impl CachingFileView { fn new(filepath: String) -> Result { let f = try!(fs::File::open(&filepath)); let metadata = try!(f.metadata()); // modified() is probably how we should check concurrent modifications if metadata.permissions().readonly() { return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "File is read-only")); } Ok(CachingFileView { file: f, filelen: metadata.len(), cache_seek: 0, cache_size: 64 * 1024, cache_len: 0, cache: Vec::with_capacity(64 * 1024) }) } } /* struct NoCacheFileView { file: std::fs::File, filelen: u64 } impl NoCacheFileView { fn new(filepath: String) -> Result { let f = try!(fs::File::open(&filepath)); let metadata = try!(f.metadata()); // modified() is probably how we should check concurrent modifications if metadata.permissions().readonly() { return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "File is read-only")); } Ok(NoCacheFileView { file: f, filelen: metadata.len() }) } } impl FileView for NoCacheFileView { fn get_bytes(&mut self, offset: u64, len: u64) -> Vec { let mut vec = Vec::with_capacity(len as usize); vec.resize(len as usize, 0); self.file.seek(SeekFrom::Start(offset)).unwrap(); self.file.read(&mut vec).unwrap(); vec } fn size(&self) -> u64 { self.filelen } } */ trait FileView { fn get_bytes(&mut self, offset: u64, len: u64) -> Vec; fn size(&self) -> u64; } const MODAL_WIDTH: usize = 24; // super naive, no idea how to track inserts/deletions yet struct Edits { edit_list: HashMap } impl Edits { fn new() -> Edits { Edits { edit_list: std::collections::HashMap::new() } } // TODO: this should accept the various kinds of edits and // positions down to bit-level, then figure it out. // // consider: // Bit(location, value) // Base64(location, value) // Byte(location, value) fn remove(&mut self, cursor: u64) { self.edit_list.remove(&cursor); } fn record(&mut self, cursor: u64, value: u8) { self.edit_list.insert(cursor, value); } } trait EditMode { fn render_bytes(&self, cursor: u64, width: u16, start: u64, bytes: std::slice::Iter) -> String; // translates from the width of the display (terminal) // to the number of bytes this edit mode can display fn element_width(&self, display_width: u16) -> u64; fn dec_sub_elem(&mut self, amount: u64); fn inc_sub_elem(&mut self, amount: u64); } struct BinaryMode { sub_elem_idx: u8 } impl EditMode for BinaryMode { fn render_bytes(&self, cursor: u64, width: u16, start: u64, bytes: std::slice::Iter) -> String { let mut text = "".to_owned(); // let mut ascii_text = "".to_owned(); let col_size = 1; let mut i: usize = 0; for b in bytes { if cursor == start + i as u64 { text.push_str(&format!("{}", color::Fg(color::Yellow))); // ascii_text.push_str(&format!("{}", color::Fg(color::Yellow))); text.push_str(&format!("{:08b}", b)); // ascii_text.push_str(&format!("{}", ascii_or_dot(b.to_owned()) as char)); text.push_str(&format!("{}", color::Fg(color::Reset))); // ascii_text.push_str(&format!("{}", color::Fg(color::Reset))); } else { text.push_str(&format!("{:08b}", b)); // ascii_text.push_str(&format!("{}", ascii_or_dot(b.to_owned()) as char)); } if i % col_size == (col_size - 1) { text.push_str(" "); } else { text.push_str(" "); } i = i + 1; } format!(" {}", text).to_owned() } fn element_width(&self, display_width: u16) -> u64 { // left pad by 12, 9 chars wide per byte (display_width as u64 - 12 - 12) / 9 } fn dec_sub_elem(&mut self, amount: u64) { self.sub_elem_idx = self.sub_elem_idx - amount as u8; } fn inc_sub_elem(&mut self, amount: u64) { self.sub_elem_idx = self.sub_elem_idx + amount as u8; } } struct HexMode { sub_elem_idx: u8 } impl EditMode for HexMode { fn render_bytes(&self, cursor: u64, width: u16, start: u64, bytes: std::slice::Iter) -> String { let mut text = "".to_owned(); let mut ascii_text = "".to_owned(); let col_size = 4; let mut i: usize = 0; let selected = true; for b in bytes { if cursor == start + i as u64 { text.push_str(&format!("{}", color::Fg(color::Yellow))); ascii_text.push_str(&format!("{}", color::Fg(color::Yellow))); if selected { text.push_str(&format!("{}", termion::style::Bold)); } text.push_str(&format!("{:02x}", b)); ascii_text.push_str(&format!("{}", ascii_or_dot(b.to_owned()) as char)); if selected { text.push_str(&format!("{}", termion::style::Reset)); } text.push_str(&format!("{}", color::Fg(color::Reset))); ascii_text.push_str(&format!("{}", color::Fg(color::Reset))); } else { text.push_str(&format!("{:02x}", b)); ascii_text.push_str(&format!("{}", ascii_or_dot(b.to_owned()) as char)); } if i % col_size == (col_size - 1) { text.push_str(" "); } else { text.push_str(" "); } i = i + 1; } /* * 12 == "
: " * i / col_size for the extra space between columns * i * 3 for 3 characters per byte * + 1 for padding.. */ let hex_line_width = 12 + i / col_size + i * 3 + 1; let padding = format!("{: >line_width$}", "", line_width = width as usize - hex_line_width - i); format!("0x{:08x}: {}{}{}", start, text, padding, ascii_text).to_owned() } fn element_width(&self, display_width: u16) -> u64 { (display_width as u64 - 13) / 17 * 4 } fn dec_sub_elem(&mut self, amount: u64) { self.sub_elem_idx = self.sub_elem_idx - amount as u8; } fn inc_sub_elem(&mut self, amount: u64) { self.sub_elem_idx = self.sub_elem_idx + amount as u8; } } struct Program<'a, 'b> { filename: String, width: u16, height: u16, seek: u64, cursor: u64, old_term: Termios, new_term: Termios, screen: &'a mut std::io::Write, view: &'b mut FileView, input_buf: Vec, state: Mode, edits: Edits, status: String, edit_views: Vec<&'b mut EditMode>, current_edit_idx: usize //screen: AlternateScreen> } impl <'a, 'b> Program<'a, 'b> { fn lines_to_draw(&self) -> u64 { self.view_byte_height() / self.edit_views.len() as u64 } fn seek_to(&mut self, dest: u64) { self.cursor = dest; self.recalculate_seek(); } fn inc_cursor(&mut self, amount: u64) { if self.view.size() - self.cursor < amount { self.cursor = self.view.size() - 1; } else { self.cursor += amount; } self.recalculate_seek(); } fn dec_cursor(&mut self, amount: u64) { if self.cursor < amount { self.cursor = 0; } else { self.cursor -= amount; } self.recalculate_seek(); } fn view_interface_height(&self) -> u64 { 2 } fn view_byte_height(&self) -> u64 { self.height as u64 - self.view_interface_height() } fn bytes_per_line(&self) -> u64 { let mut min = self.width as u64; for view in self.edit_views.iter() { min = std::cmp::min(min, view.element_width(self.width)); } min } fn recalculate_seek(&mut self) { /* * * file: * xXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxX * seek: * |-----------------------| * render_lim: | * cursor: | * */ let hex_view_height = self.lines_to_draw(); let bytes_per_line = self.bytes_per_line(); let render_lim = bytes_per_line * hex_view_height; if self.cursor >= self.seek + render_lim { // know a priori that buf size is height * width. TODO: fix. // we've sought past the end, so reset seek to what would have been the start of this // display and it'll all work out from here. self.seek = self.cursor - (self.cursor % bytes_per_line) - render_lim + bytes_per_line; } else if self.cursor < self.seek { // we've sought before the start, so reset seek to just before. self.seek = self.cursor - (self.cursor % bytes_per_line); } } } fn launch_interface(w: u16, h: u16, filename: String) { match fs::metadata(&filename) { Ok(fs_meta) => { println!("{} is {} bytes, and is it readable? {}", filename, fs_meta.len(), true); let termios = Termios::from_fd(0).unwrap(); let mut new_termios = termios.clone(); // fix terminal to not echo, thanks new_termios.c_lflag &= !(ICANON | ECHO); //let initial_buf = populate_buf(&filename, 0, w as u64 * h as u64); let mut hexmode = HexMode { sub_elem_idx: 0 }; let mut binmode = BinaryMode { sub_elem_idx: 0 }; let mut state = Program { filename: filename.to_string(), width: w, height: h, old_term: termios, new_term: new_termios, cursor: 0, seek: 0, // screen: &mut std::io::stdout(), screen: &mut AlternateScreen::from(std::io::stdout().into_raw_mode().unwrap()), view: &mut CachingFileView::new(filename).unwrap(), state: Mode::Edit, status: "".to_string(), input_buf: Vec::with_capacity(0), edits: Edits::new(), edit_views: vec![ &mut hexmode, &mut binmode ], current_edit_idx: 0 }; tcsetattr(0, TCSANOW, &state.new_term).unwrap(); print!("{}", termion::cursor::Hide); interface_loop(&mut state); print!("{}", termion::cursor::Show); tcsetattr(0, TCSANOW, &state.old_term).unwrap(); }, Err(e) => println!("{} does not exist. ({})", filename, e), } } enum Mode { Edit, ReadAddress } fn interface_loop(state: &mut Program) { render_interface(state); let stdin = std::io::stdin(); for input in stdin.events() { match state.state { Mode::Edit => { match input.unwrap() { Event::Key(Key::Char('\n')) => { state.state = Mode::ReadAddress; } Event::Key(Key::Up) => { let amount = state.bytes_per_line(); // why can't this be inlined? state.dec_cursor(amount as u64); } Event::Key(Key::Down) => { let amount = state.bytes_per_line(); state.inc_cursor(amount as u64); } Event::Key(Key::Left) => { state.dec_cursor(1); } Event::Key(Key::Right) => { state.inc_cursor(1); } Event::Key(Key::PageDown) => { let amount = state.bytes_per_line() * state.lines_to_draw(); if state.view.size() - state.cursor < amount { state.cursor = state.view.size() - 1; state.seek = std::cmp::min( state.view.size() - (state.view.size() % state.bytes_per_line()), state.seek + amount ) } else { state.cursor += amount; state.seek += amount; } } Event::Key(Key::PageUp) => { let amount = state.bytes_per_line() * state.lines_to_draw(); if state.cursor < amount { state.cursor = 0; state.seek = 0; } else if state.seek < amount { state.cursor -= amount; state.seek = 0; } else { state.cursor -= amount; state.seek -= amount; } } Event::Key(Key::Char('q')) => { break; } Event::Key(Key::Ctrl('\t')) => { // switch to previous edit mode } Event::Key(Key::Ctrl(x)) => { state.status = format!("ctrl {}", x); } Event::Key(Key::Char('\t')) => { // switch to next edit mode } // would prefer shift+t Event::Unsupported(vec) => { if vec.len() == 3 && vec[0] == 0x1b && vec[1] == 0x4f { match vec[2] { 0x41 => { state.status = "ctrl up".to_owned(); state.edit_views[state.current_edit_idx].inc_sub_elem(1); /* up */ } 0x42 => { state.status = "ctrl down".to_owned(); /* down */ } 0x43 => { state.status = "ctrl right".to_owned(); /* right */ } 0x44 => { state.status = "ctrl left".to_owned(); /* left */ } _ => { state.status = format!("ctrl? {:?}", vec); } } } else { // thought shift+arrow would end up here, but.. nope state.status = format!("ctrl? {:?}", vec); } } Event::Key(Key::Char(x)) => { // TODO: how does this work on non-US keyboards? keyboards without a-fA-F? if (x >= '0' && x <= '9') || (x >= 'a' && x <= 'f') || (x >= 'A' && x <= 'F') { state.edits.record(state.cursor, x as u8); // edit the nibble under the cursor } } Event::Key(Key::Backspace) => { state.edits.remove(state.cursor); state.dec_cursor(1); } _ => { } } } Mode::ReadAddress => { match input.unwrap() { Event::Key(Key::Char('\n')) => { let buf_str = state.input_buf.iter().cloned().collect::(); if buf_str.len() == 0 { state.input_buf = Vec::with_capacity(0); state.state = Mode::Edit; state.status = " ".to_string(); } else { let addr_str = u64::from_str_radix(&buf_str, 16); match addr_str { Ok(addr) => { if addr < state.view.size() { state.seek_to(addr); state.input_buf = Vec::with_capacity(0); state.state = Mode::Edit; state.status = " ".to_string(); } else { state.status = format!("Address out of bounds: 0x{:x}", addr); state.input_buf = Vec::with_capacity(0); state.state = Mode::Edit; } } Err(s) => { state.status = format!("Unable to parse address '{}', error: {}", buf_str, s); state.input_buf = Vec::with_capacity(0); state.state = Mode::Edit; } } } } Event::Key(Key::Esc) => { state.input_buf = Vec::with_capacity(0); state.state = Mode::Edit; } Event::Key(Key::Backspace) => { if state.input_buf.len() > 0 { state.input_buf.pop(); } } Event::Key(Key::Char(x)) => { if (x >= '0' && x <= '9') || (x >= 'a' && x <= 'f') || (x >= 'A' && x <= 'F') { if state.input_buf.len() < MODAL_WIDTH - 6 { state.input_buf.push(x); } } } _ => { } } } } render_interface(state); } } fn ascii_or_dot(c: u8) -> u8 { let u = c as char; if u >= 'a' && u <= 'z' || u >= '0' && u <= '9' || u >= 'A' && u <= 'Z' || u == '~' || u == '!' || u == '@' || u == '#' || u == '$' || u == '%' || u == '^' || u == '&' || u == '*' || u == '(' || u == ')' || u == '_' || u == '+' || u == '{' || u == '}' || u == '|' || u == ':' || u == '"' || u == '>' || u == '?' || u == '-' || u == '=' || u == '[' || u == ']' || u == '\\' || u == ';' || u == '\'' || u == '.' || u == '/' || u == '<' || u == ',' || u == '`' { c } else { '.' as u8 } } fn render_interface(state: &mut Program) { // println!("{}", clear::All); write!(state.screen, "{}{}", cursor::Goto(1, 1), clear::CurrentLine).unwrap(); let bufsz = state.lines_to_draw() * state.bytes_per_line(); writeln!(state.screen, "Dimensions: {}x{} Seek: 0x{:x}, Cursor: 0x{:x} - file: {}, 0x{:x} bytes", state.width, state.height, state.seek, state.cursor, state.filename, state.view.size()).unwrap(); if state.status.len() > 0 { writeln!(state.screen, "{}{} {}", cursor::Goto(1, 2), clear::CurrentLine, state.status).unwrap(); state.status = "".to_string(); } let buffer = state.view.get_bytes(state.seek, bufsz); // buffer is one screen of bytes, starting at seek. let start = (state.seek) as usize; let mut iface = String::new(); let width = state.bytes_per_line(); for i in 0..state.lines_to_draw() { for j in 0..state.edit_views.len() { let view = &state.edit_views[j]; let slice_start = width * i; let slice_end = slice_start + width; write!(iface, "{}", view.render_bytes(state.cursor, state.width, start as u64 + slice_start, buffer[(slice_start as usize)..(slice_end as usize)].iter())).unwrap(); if i < state.lines_to_draw() - 1 || j < (state.edit_views.len() - 1) { write!(iface, "\n").unwrap(); } } } let height = state.view_interface_height() as u16; write!(state.screen, "{}{}", cursor::Goto(1, height + 1), iface).unwrap(); match state.state { Mode::ReadAddress => { let xmid = state.width as u16 / 2; let ymid = state.height as u16 / 2; write!(state.screen, "{}{:-^2$}", cursor::Goto(xmid-12, ymid-2), "", MODAL_WIDTH).unwrap(); write!(state.screen, "{}|{: ^2$}|", cursor::Goto(xmid-12, ymid-1), "Enter new addresss", MODAL_WIDTH - 2).unwrap(); write!(state.screen, "{}| >{:_^2$} |", cursor::Goto(xmid-12, ymid-0), "", MODAL_WIDTH - 6).unwrap(); write!(state.screen, "{}|{: ^2$}|", cursor::Goto(xmid-12, ymid+1), "", MODAL_WIDTH - 2).unwrap(); write!(state.screen, "{}{:-^2$}", cursor::Goto(xmid-12, ymid+2), "", MODAL_WIDTH).unwrap(); write!(state.screen, "{}{}", cursor::Goto(xmid-9, ymid-0), state.input_buf.iter().cloned().collect::()).unwrap(); } _ => {} } state.screen.flush().unwrap(); }