From 854f76e1c8f698dd362f3c90c5ddf26e27e01f6e Mon Sep 17 00:00:00 2001 From: iximeow Date: Sat, 14 Oct 2017 17:02:25 -0700 Subject: rewrite a lot of things this commit should have been a lot of smaller commits. 1: draws to an alternate screen now 2: better behavior around buffering the currently viewed region 3: sketched out something to track edits (one day it'll be a real hex *editor*) 4: start moving to sanely declare layout sizes view_interface_height(), view_byte_height(), bytes_per_line()... 5: support accepting an address to seek to. this is the start of proper UI states 6: extract logic to draw a particular line of bytes out eventually support base64 lines, bit-level edits currently just mirrors the original hex layout. --- main.rs | 536 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 452 insertions(+), 84 deletions(-) diff --git a/main.rs b/main.rs index 1b935c8..80a2a1e 100644 --- a/main.rs +++ b/main.rs @@ -1,22 +1,31 @@ -use std::cmp::{min, max}; -use std::io; +#![feature(slice_patterns)] +// eh, whatever, unstable features... + +//use std::cmp::{min, max}; +//use std::io::Write as IoWrite; +use std::fmt::Write as FmtWrite; use std::io::{Seek, SeekFrom}; use std::fs; -use std::fmt::Write; 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, input}; +//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() { - println!("hello"); +// println!("hello"); +// let (w, h) = try!(termion::terminal_size()); match termion::terminal_size() { Ok((w, h)) => { if let Some(arg1) = env::args().nth(1) { @@ -36,9 +45,164 @@ fn usage() { println!("usage: this_thing filename"); } -struct Program { - filename: String, +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")); + } + +// let f2: &'a fs::File = &f; + + Ok(NoCacheFileView { + file: f, + filelen: metadata.len() + }) + } +} + +trait FileView { + fn get_bytes(&mut self, offset: u64, len: u64) -> Vec; + fn size(&self) -> u64; +} + +const modal_width: usize = 24; + +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 } +} + +struct Edits { + +} + +impl Edits { + fn new() -> Edits { + Edits { + } + } + fn remove(&mut self, cursor: u64, sub_elem: u8) { + + } + fn record(&mut self, cursor: u64, sub_elem: u8, value: u8) { + + } +} + +trait EditMode { + fn render_bytes(&self, 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 HexMode { + sub_elem_idx: u8 +} + +impl EditMode for HexMode { + fn render_bytes(&self, 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; + for b in bytes { + 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; + } + format!("0x{:08x}: {} {}", start, text, 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, row_size: u16, height: u16, @@ -46,13 +210,29 @@ struct Program { cursor: u64, old_term: Termios, new_term: Termios, - buf: Vec + screen: &'a mut std::io::Write, + view: &'b mut FileView, + input_buf: Vec, + state: Mode, + sub_elem: u8, + edits: Edits, + status: String, + edit_views: Vec<&'b mut EditMode>, + current_edit_idx: usize + //screen: AlternateScreen> } -impl Program { +impl <'a, 'b> Program<'a, 'b> { + fn lines_to_draw(&self) -> u64 { + self.view_byte_height() / (self.edit_views.len() as u64 + 1) + } + fn seek_to(&mut self, dest: u64) { + self.cursor = dest; + self.recalculate_seek(); + } fn inc_cursor(&mut self, amount: u64) { - if self.filelen - self.cursor < amount { - self.cursor = self.filelen - 1; + if self.view.size() - self.cursor < amount { + self.cursor = self.view.size() - 1; } else { self.cursor += amount; } @@ -66,25 +246,48 @@ impl Program { } 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 view_byte_width(&self) -> u64 { + self.row_size as u64 + } + + fn bytes_per_line(&self) -> u64 { + let mut min = self.view_byte_width(); + for view in self.edit_views.iter() { + min = std::cmp::min(min, view.element_width(self.width)); + } + min + } + fn recalculate_seek(&mut self) { - let old_seek = self.seek; - let bufsize = self.height as u64 * self.width as u64; - let render_lim = (self.row_size as u64) * (self.height as u64 - 1); // this shouldn't be decided here... - if self.cursor + render_lim > self.seek + bufsize { // know a priori that buf size is height * width. TODO: fix. - println!("aaaa"); + /* + * + * file: + * xXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxX + * seek: + * |-----------------------| + * render_lim: | + * cursor: | + * + */ + let hex_view_height = self.lines_to_draw(); + let bytes_per_line = self.view_byte_width(); + 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 % self.row_size as u64); + self.seek = self.cursor - (self.cursor % bytes_per_line) - render_lim + bytes_per_line; } else if self.cursor < self.seek { - println!("bbbb"); - // we've sought before the start, so reset seek to just before. ideally, populate a - // buffer for like 1k before, but lazy. - self.seek = self.cursor - (self.cursor % self.row_size as u64); - } - - // repopulate buf.. - if self.seek != old_seek { - self.buf = populate_buf(&self.filename, self.seek, bufsize); + // we've sought before the start, so reset seek to just before. + self.seek = self.cursor - (self.cursor % bytes_per_line); } } } @@ -100,11 +303,12 @@ fn launch_interface(w: u16, h: u16, filename: String) { // 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 initial_buf = populate_buf(&filename, 0, w as u64 * h as u64); + + let mut hexmode = HexMode { sub_elem_idx: 0 }; let mut state = Program { - filename: filename, - filelen: fs_meta.len(), + filename: filename.to_string(), width: w, // let col_width = 4; // w = 10 + 1 + 1 + 4 * 3 * x + x + 1 + 4 * x @@ -116,12 +320,25 @@ fn launch_interface(w: u16, h: u16, filename: String) { new_term: new_termios, cursor: 0, seek: 0, - buf: initial_buf +// 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), + sub_elem: 0, + edits: Edits::new(), + edit_views: vec![ + &mut hexmode + ], + 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(); }, @@ -130,40 +347,170 @@ fn launch_interface(w: u16, h: u16, filename: String) { } } +enum Mode { + Edit, + ReadAddress +} + fn interface_loop(state: &mut Program) { // let buffer = populate_buf(&state.filename, state.cursor, state.width as u64 * state.height as u64); // sw*h over-estimates by about a factor of.. 4? render_interface(state); - let stdin = io::stdin(); + let stdin = std::io::stdin(); // for input in KeyboardInput::from_stdin(&stdin.lock().bytes()) { for input in stdin.events() { - match input.unwrap() { - Event::Key(Key::Up) => { - let amount = state.row_size; // why can't this be inlined? - state.dec_cursor(amount as u64); - } - Event::Key(Key::Down) => { - let amount = state.row_size; - 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.row_size as u64) * (state.height as u64 - 1); - state.inc_cursor(amount); - } - Event::Key(Key::PageUp) => { - let amount = (state.row_size as u64) * (state.height as u64 - 1); - state.dec_cursor(amount); + match state.state { + Mode::Edit => { + match input.unwrap() { + Event::Key(Key::Char('\n')) => { + state.state = Mode::ReadAddress; + } + Event::Key(Key::Up) => { + let amount = state.row_size; // why can't this be inlined? + state.dec_cursor(amount as u64); + } + Event::Key(Key::Down) => { + let amount = state.row_size; + 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.view_byte_width() * 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.view_byte_width()), + state.seek + amount + ) + } else { + state.cursor += amount; + state.seek += amount; + } + } + Event::Key(Key::PageUp) => { + let amount = state.view_byte_width() * 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(x)) => { + state.status = format!("ctrl {}", x); + } + Event::Key(Key::Char('\t')) => { + // switch to next edit mode + } + // would prefer shift+t + Event::Key(Key::Ctrl('\t')) => { + // switch to previous edit mode + } + 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, state.sub_elem, x as u8); + // edit the nibble under the cursor + } + } + Event::Key(Key::Backspace) => { + state.edits.remove(state.cursor, state.sub_elem); + state.dec_cursor(1); + } + _ => { } + } } - u => { - println!("Got {:?}", u); - break; + 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(); + continue; + } + let addr_str = u64::from_str_radix(&buf_str, 16); + match addr_str { + Ok(addr) => { + if addr >= 0 && 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 '{}'", buf_str); + 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); @@ -218,22 +565,38 @@ fn ascii_or_dot(c: u8) -> u8 { } } -fn render_interface(state: &Program) { - println!("Dimensions: {}x{} Seek: {}, Cursor: {} - file: {}, {} bytes", state.width, state.height, state.seek, state.cursor, state.filename, state.filelen); +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.view_byte_width(); + 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(); + } +// writeln!(state.screen, "Read {} bytes at {} ", bufsz, state.seek).unwrap(); //let addr_width = 8; // let ascii = bytes_width * cols; - let buffer = &state.buf; + let buffer = state.view.get_bytes(state.seek, bufsz); // &state.buf; - let start = (state.cursor - state.seek) as usize; - let mut idx: usize = start; + // buffer is one screen of bytes, starting at seek. + + let start = (state.seek) as usize; + let mut idx: usize = 0; let col_width = 4; - for i in 1..(state.height-1) { + + let mut iface = String::new(); + + // +1 here is a total hack to intermix typical line rendering.. + let width = state.bytes_per_line(); + + for i in 0..state.lines_to_draw() { let mut hex_line = String::new(); let mut ascii_line = String::new(); - for _ in 0..(state.row_size / 4) { + for _ in 0..(state.view_byte_width() / col_width) { for _ in 0..col_width { - if idx < buffer.len() { + if idx < buffer.len() && ((idx + start) as u64) < state.view.size() { let chr = buffer[idx]; if idx as u64 + state.seek == state.cursor { write!(&mut hex_line, "\x1b[33m").unwrap(); @@ -254,31 +617,36 @@ fn render_interface(state: &Program) { write!(&mut hex_line, " ").unwrap(); } - let lineaddr = start as u64 + ((i as u64) - 1) * (state.row_size as u64); - let addr = if (lineaddr as usize) < buffer.len() { + let lineaddr = start as u64 + i * state.view_byte_width(); + let addr = if lineaddr < state.view.size() { format!("0x{:08x}:", lineaddr) } else { " ".to_string() }; - println!("{} {} {}", addr, hex_line, ascii_line); + write!(iface, "{} {} {}", addr, hex_line, ascii_line).unwrap(); + for view in state.edit_views.iter() { + let slice_start = (width * i); + let slice_end = slice_start + width; + write!(iface, "\n{}", view.render_bytes(start as u64 + slice_start, buffer[(slice_start as usize)..(slice_end as usize)].iter())); + } + if i < state.lines_to_draw() - 1 { + write!(iface, "\n").unwrap(); + } } -} - -fn populate_buf(arg: &String, seek: u64, count: u64) -> Vec { - match fs::File::open(&arg) { - Ok(mut fd) => { - /*..*/ - fd.seek(SeekFrom::Start(seek)).unwrap(); - fd - .bytes() - .take(count as usize) - .map(|r| r.unwrap()) // lazy - .collect() - }, - Err(_) => - { - println!("Failed to open {}", arg); - panic!(); + 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); + write!(state.screen, "{}|{: ^2$}|", cursor::Goto(xmid-12, ymid-1), "Enter new addresss", modal_width - 2); + write!(state.screen, "{}| >{:_^2$} |", cursor::Goto(xmid-12, ymid-0), "", modal_width - 6); + write!(state.screen, "{}|{: ^2$}|", cursor::Goto(xmid-12, ymid+1), "", modal_width - 2); + write!(state.screen, "{}{:-^2$}", cursor::Goto(xmid-12, ymid+2), "", modal_width); + write!(state.screen, "{}{}", cursor::Goto(xmid-9, ymid-0), state.input_buf.iter().cloned().collect::()); } + _ => {} } + state.screen.flush().unwrap(); } -- cgit v1.1