summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoriximeow <me@iximeow.net>2017-10-14 17:02:25 -0700
committeriximeow <me@iximeow.net>2017-10-14 17:02:25 -0700
commit854f76e1c8f698dd362f3c90c5ddf26e27e01f6e (patch)
tree2fcc9c0a0d9760e895a5f0753dd1ad5eb720e7e3
parent4fe34429eeeb6d06ba68d084da37474f61a6ff63 (diff)
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.
-rw-r--r--main.rs536
1 files 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<u8>
+}
+
+impl FileView for CachingFileView {
+ fn get_bytes(&mut self, offset: u64, len: u64) -> Vec<u8> {
+ 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<CachingFileView, std::io::Error> {
+ 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<NoCacheFileView, std::io::Error> {
+ 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<u8>;
+ fn size(&self) -> u64;
+}
+
+const modal_width: usize = 24;
+
+impl FileView for NoCacheFileView {
+ fn get_bytes(&mut self, offset: u64, len: u64) -> Vec<u8> {
+ 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<u8>) -> 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<u8>) -> 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<u8>
+ screen: &'a mut std::io::Write,
+ view: &'b mut FileView,
+ input_buf: Vec<char>,
+ state: Mode,
+ sub_elem: u8,
+ edits: Edits,
+ status: String,
+ edit_views: Vec<&'b mut EditMode>,
+ current_edit_idx: usize
+ //screen: AlternateScreen<termion::raw::RawTerminal<std::io::Stdout>>
}
-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::<String>();
+ 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<u8> {
- 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::<String>());
}
+ _ => {}
}
+ state.screen.flush().unwrap();
}