//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, Write}; use std::fs; use std::io::Read; use std::env; use std::iter::FromIterator; use std::ops::IndexMut; //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; /* * divides p by q, returns (quotient, remainder) */ fn best_div_rem(p: u64, q: u8) -> (u64, u8) { (p / q as u64, (p % q as u64) as u8) } 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 } struct EditableFileView { file: std::fs::File, filelen: u64, cache: Vec } trait EditableView { fn insert_byte(&mut self, offset: u64, value: u8); fn update_byte(&mut self, offset: u64, value: u8); fn delete_byte(&mut self, offset: u64); fn get_byte(&self, offset: u64) -> u8; fn get_bytes(&mut self, offset: u64, len: u64) -> Vec; fn size(&self) -> u64; } impl EditableView for EditableFileView { /* * TODO: have edits that are applied At Some Point */ fn insert_byte(&mut self, offset: u64, value: u8) { self.cache.insert(offset as usize, value); } fn update_byte(&mut self, offset: u64, value: u8) { self.cache[offset as usize] = value; } fn delete_byte(&mut self, offset: u64) { self.cache.remove(offset as usize); } fn get_byte(&self, offset: u64) -> u8 { self.cache[offset as usize] } fn get_bytes(&mut self, offset: u64, len: u64) -> Vec { let read_start = offset as usize; let read_end = std::cmp::min(offset + len, self.filelen) as usize; Vec::from_iter(self.cache[read_start..read_end].iter().cloned()) } fn size(&self) -> u64 { return self.filelen; } } 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) }) } } impl EditableFileView { fn new(filepath: String) -> Result { let mut f = try!(fs::OpenOptions::new().read(true).write(true).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")); } let size = metadata.len(); let mut buffer = vec![0; size as usize]; let content = f.read(&mut buffer).unwrap(); Ok(EditableFileView { file: f, filelen: size, cache: buffer }) } } /* 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, selected: bool, 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; // the number of steps of sub_element_idx it takes to move one word fn symbol_width(&self) -> u8; /* * These both return the number of words to move the cursor by, after adjusting * self to the appropriate sub-word position */ fn dec_sub_elem(&mut self, amount: u64) -> u64; fn inc_sub_elem(&mut self, amount: u64) -> u64; fn name(&self) -> &str; // TODO: this is the wrong way to do edits. // if the edit spans across a byte, this just.. won't work. fn apply_sym(&self, original: u8, value: u8) -> Option; } struct ASCIIMode { } impl EditMode for ASCIIMode { fn render_bytes(&self, cursor: u64, width: u16, selected: bool, start: u64, bytes: std::slice::Iter) -> String { let mut ascii_text = "".to_owned(); let mut i: usize = 0; for b in bytes { if cursor == start + i as u64 { ascii_text.push_str(&format!("{}", color::Fg(color::Yellow))); if selected { ascii_text.push_str(&format!("{}", termion::style::Bold)); } ascii_text.push_str(&format!("{}", ascii_or_dot(b.to_owned()) as char)); if selected { ascii_text.push_str(&format!("{}", termion::style::Reset)); } ascii_text.push_str(&format!("{}", color::Fg(color::Reset))); } else { ascii_text.push_str(&format!("{}", ascii_or_dot(b.to_owned()) as char)); } i = i + 1; } ascii_text } fn element_width(&self, display_width: u16) -> u64 { display_width as u64 } fn symbol_width(&self) -> u8 { 1 } fn dec_sub_elem(&mut self, amount: u64) -> u64 { let (mut word_adjust, sub_elem_adjust) = best_div_rem(amount, self.symbol_width()); word_adjust } fn inc_sub_elem(&mut self, amount: u64) -> u64 { let (mut word_adjust, sub_elem_adjust) = best_div_rem(amount, self.symbol_width()); word_adjust } fn name(&self) -> &str { &"ASCII" } fn apply_sym(&self, original: u8, value: u8) -> Option { Some(value) } } struct BinaryMode { sub_elem_idx: u8 } impl EditMode for BinaryMode { fn render_bytes(&self, cursor: u64, width: u16, selected: bool, start: u64, bytes: std::slice::Iter) -> String { let mut 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))); if selected { text.push_str(&format!("{}", termion::style::Bold)); } let symbol_str: String = format!("{:08b}", b); let formatted_str = format!( "{}{}{}{}{}", symbol_str[..(self.sub_elem_idx as usize)].chars().collect::(), termion::style::Underline, symbol_str[(self.sub_elem_idx as usize)..(self.sub_elem_idx as usize + 1)].chars().collect::(), termion::style::NoUnderline, symbol_str[(self.sub_elem_idx as usize + 1)..].chars().collect::() ); text.push_str(&formatted_str); if selected { text.push_str(&format!("{}", termion::style::Reset)); } text.push_str(&format!("{}", color::Fg(color::Reset))); } else { text.push_str(&format!("{:08b}", b)); } 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 symbol_width(&self) -> u8 { 8 } fn dec_sub_elem(&mut self, amount: u64) -> u64 { let (mut word_adjust, sub_elem_adjust) = best_div_rem(amount, self.symbol_width()); if (sub_elem_adjust > self.sub_elem_idx) { // we would have to subtract enough to underflow. Instead, bump the prior word back and // put the cursor at the right location there. word_adjust += 1; self.sub_elem_idx = self.sub_elem_idx + self.symbol_width() - sub_elem_adjust as u8; } else { self.sub_elem_idx = self.sub_elem_idx - sub_elem_adjust as u8; } word_adjust } fn inc_sub_elem(&mut self, amount: u64) -> u64 { let (mut word_adjust, sub_elem_adjust) = best_div_rem(amount, self.symbol_width()); if (sub_elem_adjust >= self.symbol_width() - self.sub_elem_idx) { // we would add enough to overflow. Instead, bump up the word position and // put the cursor at the right location there. word_adjust += 1; self.sub_elem_idx = self.sub_elem_idx + (sub_elem_adjust as u8) - self.symbol_width(); } else { self.sub_elem_idx = self.sub_elem_idx + sub_elem_adjust as u8; } word_adjust } fn name(&self) -> &str { &"Binary" } fn apply_sym(&self, original: u8, value: u8) -> Option { let mut result = original; if value == 0x30 || value == 0x31 { // value is ascii 0 or 1 // clear the bit at sub_elem_idx result &= (!(1 << (self.symbol_width() - self.sub_elem_idx - 1))); result |= (value & 1) << (self.symbol_width() - self.sub_elem_idx - 1); // now set it appropriately Some(result) } else { None } } } struct HexMode { sub_elem_idx: u8 } impl EditMode for HexMode { fn render_bytes(&self, cursor: u64, width: u16, selected: bool, start: u64, bytes: std::slice::Iter) -> String { let mut text = "".to_owned(); let mut ascii_text = "".to_owned(); let col_size = 8; let mut i: usize = 0; for b in bytes { if cursor == start + i as u64 { text.push_str(&format!("{}", color::Fg(color::Yellow))); if selected { text.push_str(&format!("{}", termion::style::Bold)); } let symbol_str: String = format!("{:02x}", b); let formatted_str = format!( "{}{}{}{}{}", symbol_str[..(self.sub_elem_idx as usize)].chars().collect::(), termion::style::Underline, symbol_str[(self.sub_elem_idx as usize)..(self.sub_elem_idx as usize + 1)].chars().collect::(), termion::style::NoUnderline, symbol_str[(self.sub_elem_idx as usize + 1)..].chars().collect::() ); text.push_str(&formatted_str); if selected { text.push_str(&format!("{}", termion::style::Reset)); } text.push_str(&format!("{}", color::Fg(color::Reset))); } else { text.push_str(&format!("{:02x}", b)); } if i % col_size == (col_size - 1) { text.push_str(" "); } else { text.push_str(" "); } i = i + 1; } format!("0x{:08x}: {}", start, text).to_owned() } fn element_width(&self, display_width: u16) -> u64 { (display_width as u64 - 13) / 17 * 4 } fn symbol_width(&self) -> u8 { 2 } fn dec_sub_elem(&mut self, amount: u64) -> u64 { let (mut word_adjust, sub_elem_adjust) = best_div_rem(amount, self.symbol_width()); if (sub_elem_adjust > self.sub_elem_idx) { // we would have to subtract enough to underflow. Instead, bump the prior word back and // put the cursor at the right location there. word_adjust += 1; self.sub_elem_idx = self.sub_elem_idx + self.symbol_width() - sub_elem_adjust as u8; } else { self.sub_elem_idx = self.sub_elem_idx - sub_elem_adjust as u8; } word_adjust } fn inc_sub_elem(&mut self, amount: u64) -> u64 { let (mut word_adjust, sub_elem_adjust) = best_div_rem(amount, self.symbol_width()); if (sub_elem_adjust >= self.symbol_width() - self.sub_elem_idx) { // we would add enough to overflow. Instead, bump up the word position and // put the cursor at the right location there. word_adjust += 1; self.sub_elem_idx = self.sub_elem_idx + (sub_elem_adjust as u8) - self.symbol_width(); } else { self.sub_elem_idx = self.sub_elem_idx + sub_elem_adjust as u8; } word_adjust } fn name(&self) -> &str { &"Hex" } fn apply_sym(&self, original: u8, value: u8) -> Option { let new_bits = if value >= 0x30 && value <= 0x39 { // ascii digit Some(value - 0x30) } else if value >= 0x41 && value <= 0x46 { // majiscule ascii A-F Some(value - 0x41 + 0x0a) } else if value >= 0x61 && value <= 0x66 { // miniscule ascii a-f Some(value - 0x61 + 0x0a) } else { None }; new_bits.map(|bits| { // this 4 constant here is kinda silly, it's because that's word_size / symbol_width // and sub_elem_idx is a selector in units of that size. // here it's 4, in the bit case it's 1, and in the ascii case none of this matters. let mask = !(0xf0 >> (4 * (self.sub_elem_idx))); let masked_value = original & mask; let result = masked_value | (bits << (4 * (self.symbol_width() - 1 - self.sub_elem_idx))); result }) } } 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 EditableFileView, input_buf: Vec, state: Mode, edits: Edits, status: String, edit_views: Vec<&'b mut EditMode>, current_edit_idx: usize, ascii_mode: &'b mut EditMode //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) { let word_diff = self.mut_current_edit_view().inc_sub_elem(amount); if self.view.size() - self.cursor <= word_diff { self.cursor = self.view.size() - 1; } else { self.cursor += word_diff; } self.recalculate_seek(); } fn dec_cursor(&mut self, amount: u64) { let word_diff = self.mut_current_edit_view().dec_sub_elem(amount); if self.cursor < word_diff { self.cursor = 0; } else { self.cursor -= word_diff; } 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 mut_current_edit_view(&mut self) -> &mut EditMode { if self.current_edit_idx == self.edit_views.len() { self.ascii_mode } else { // why does this work and not *self.edit_views.index_mut(self.current_edit_idx) // self.edit_views[self.current_edit_idx] } } fn current_edit_view(&self) -> &EditMode { if self.current_edit_idx == self.edit_views.len() { self.ascii_mode } else { self.edit_views[self.current_edit_idx] // *self.edit_views.get_mut(self.current_edit_idx).unwrap() } } } 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 EditableFileView::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, ascii_mode: &mut ASCIIMode {} }; 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, ViewSelection } fn interface_loop(state: &mut Program) { render_interface(state); let stdin = std::io::stdin(); for input in stdin.events() { match state.state { Mode::ViewSelection => { match input.unwrap() { Event::Key(Key::Up) => { } Event::Key(Key::Down) => { } Event::Key(Key::Char(' ')) => { } Event::Key(Key::Esc) => { state.state = Mode::Edit } _ => { } } } Mode::Edit => { match input.unwrap() { Event::Key(Key::Char('\n')) => { state.state = Mode::ReadAddress; } Event::Key(Key::Up) => { // beginner rust tip: non-lexical lifetimes would permit inlining this let amount = state.bytes_per_line() * state.current_edit_view().symbol_width() as u64; state.dec_cursor(amount as u64); } Event::Key(Key::Down) => { // beginner rust tip: non-lexical lifetimes would permit inlining this let amount = state.bytes_per_line() * state.current_edit_view().symbol_width() as u64; 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.cursor + amount > state.view.size() { state.cursor = state.view.size() - 1; state.seek = state.view.size() + (state.view.size() % state.bytes_per_line()) - 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::Insert) => { state.view.insert_byte(state.cursor, 0); } Event::Key(Key::Delete) => { state.view.delete_byte(state.cursor); } Event::Key(Key::Ctrl('a')) => { // ok well, you want to save the file? // your loss state.view.file.seek(SeekFrom::Start(0)).unwrap(); let write_res = state.view.file.write(&state.view.cache); match write_res { Ok(size) => { state.status = format!("Wrote {} bytes", size); let truncate_res = state.view.file.set_len(state.view.cache.len() as u64); match truncate_res { Ok(_) => { /* do nothing */ }, Err(e) => { state.status = format!("{}, but truncate error: {}", state.status, e); } } }, Err(e) => { state.status = format!("Write error: {}", e); } } } Event::Key(Key::Ctrl('x')) => { // TODO warn if pending changes break; } Event::Key(Key::Ctrl('v')) => { state.state = Mode::ViewSelection; } // nahhhh.. Event::Key(Key::Ctrl('c')) => { state.status = "caught ctrl-c?".to_string(); } Event::Key(Key::Ctrl('\t')) => { state.current_edit_idx -= 1; state.current_edit_idx = state.current_edit_idx % (state.edit_views.len() - 1); } Event::Key(Key::Ctrl(x)) => { state.status = format!("ctrl {}", x); } Event::Key(Key::Char('\t')) => { state.current_edit_idx += 1; state.current_edit_idx = state.current_edit_idx % (state.edit_views.len() + 1); } // 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('g')) => { // follow (dword) if state.view.size() < 4 { state.status = "not enough bytes to seek a dword (need at least four)".to_string(); } else { let read_from = if state.view.size() - state.cursor < 4 { state.view.size() - 4 } else { state.cursor }; let dword_bytes = state.view.get_bytes(state.cursor, 4); if dword_bytes.len() == 4 { let dword: u64 = ((dword_bytes[0] as u32) | ((dword_bytes[1] as u32) << 8) | ((dword_bytes[2] as u32) << 16) | ((dword_bytes[3] as u32) << 24)) as u64; if dword < state.view.size() { state.status = format!("Seeked to 0x{:x}", dword); state.seek_to(dword as u64); } else { state.status = format!("Cannot seek to 0x{:x} - invalid address.", dword); } } else { state.status = format!( "not enough bytes to seek a dword (got {}) - should be unreachable since we already checked for this", dword_bytes.len() ); } } } Event::Key(Key::Char(x)) => { // TODO: how does this work on non-US keyboards? keyboards without a-fA-F? let new_value = state.current_edit_view().apply_sym(state.view.get_byte(state.cursor), x as u8); if let Some(new_value) = new_value { state.view.update_byte(state.cursor, new_value); state.inc_cursor(1); } } 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 == '`' || 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), state.status, termion::clear::UntilNewline).unwrap(); state.status = "".to_string(); } else { writeln!(state.screen, "{}{}", cursor::Goto(1, 2), termion::clear::CurrentLine).unwrap(); } 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() { let slice_start = width * i; let slice_end = std::cmp::min(slice_start + width, buffer.len() as u64); let done = slice_end == buffer.len() as u64; for j in 0..state.edit_views.len() { let view = state.edit_views.get(j).unwrap(); let line = view.render_bytes( state.cursor, state.width, view.name() == state.current_edit_view().name(), start as u64 + slice_start, buffer[(slice_start as usize)..(slice_end as usize)].iter() ); write!(iface, "{}", line).unwrap(); if j == 0 { // ripped from HexMode display /* * 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 + width / 4 + width * 3 + 1; let padding = format!("{: >line_width$}", "", line_width = state.width as usize - hex_line_width as usize - width as usize); write!(iface, "{}", padding); write!(iface, " {}", state.ascii_mode.render_bytes( state.cursor, state.width, state.ascii_mode.name() == state.current_edit_view().name(), start as u64 + slice_start, buffer[(slice_start as usize)..(slice_end as usize)].iter() )); } write!(iface, "{}", termion::clear::UntilNewline); if i < state.lines_to_draw() - 1 || j < (state.edit_views.len() - 1) { write!(iface, "\n").unwrap(); } } if done && i < state.lines_to_draw() - 1 { for l in i..state.lines_to_draw() - 2 { for x in 0..state.edit_views.len() { write!(iface, "{}\n", termion::clear::CurrentLine).unwrap(); } } write!(iface, "{}", termion::clear::CurrentLine).unwrap(); break; } } 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(); } Mode::ViewSelection => { let xmid = state.width as u16 / 2; let ymid = state.height as u16 / 2; write!(state.screen, "{}{:-^2$}", cursor::Goto(xmid-12, ymid - 4), "", MODAL_WIDTH).unwrap(); write!(state.screen, "{}|{:-^2$}|", cursor::Goto(xmid-12, ymid - 3), "Select views", MODAL_WIDTH - 2).unwrap(); } _ => {} } state.screen.flush().unwrap(); }