diff options
-rw-r--r-- | main.rs | 313 |
1 files changed, 273 insertions, 40 deletions
@@ -2,11 +2,12 @@ //use std::io::Write as IoWrite; use std::collections::HashMap; use std::fmt::Write as FmtWrite; -use std::io::{Seek, SeekFrom}; +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; @@ -22,6 +23,13 @@ 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)) => { @@ -51,6 +59,46 @@ struct CachingFileView { cache: Vec<u8> } +struct EditableFileView { + file: std::fs::File, + filelen: u64, + cache: Vec<u8> +} + +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<u8>; + 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<u8> { + 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<u8> { if len > self.cache_size as u64 { @@ -98,6 +146,28 @@ impl CachingFileView { } } +impl EditableFileView { + fn new(filepath: String) -> Result<EditableFileView, std::io::Error> { + 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, @@ -170,9 +240,18 @@ trait EditMode { // 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); + // 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<u8>; } struct ASCIIMode { @@ -203,9 +282,17 @@ impl EditMode for ASCIIMode { fn element_width(&self, display_width: u16) -> u64 { display_width as u64 } - fn dec_sub_elem(&mut self, amount: u64) { } - fn inc_sub_elem(&mut self, amount: 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<u8> { Some(value) } } struct BinaryMode { @@ -223,7 +310,16 @@ impl EditMode for BinaryMode { if selected { text.push_str(&format!("{}", termion::style::Bold)); } - text.push_str(&format!("{:08b}", b)); + let symbol_str: String = format!("{:08b}", b); + let formatted_str = format!( + "{}{}{}{}{}", + symbol_str[..(self.sub_elem_idx as usize)].chars().collect::<String>(), + termion::style::Underline, + symbol_str[(self.sub_elem_idx as usize)..(self.sub_elem_idx as usize + 1)].chars().collect::<String>(), + termion::style::NoUnderline, + symbol_str[(self.sub_elem_idx as usize + 1)..].chars().collect::<String>() + ); + text.push_str(&formatted_str); if selected { text.push_str(&format!("{}", termion::style::Reset)); } @@ -244,13 +340,44 @@ impl EditMode for BinaryMode { // 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; + 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<u8> { + 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 { @@ -269,7 +396,16 @@ impl EditMode for HexMode { if selected { text.push_str(&format!("{}", termion::style::Bold)); } - text.push_str(&format!("{:02x}", b)); + let symbol_str: String = format!("{:02x}", b); + let formatted_str = format!( + "{}{}{}{}{}", + symbol_str[..(self.sub_elem_idx as usize)].chars().collect::<String>(), + termion::style::Underline, + symbol_str[(self.sub_elem_idx as usize)..(self.sub_elem_idx as usize + 1)].chars().collect::<String>(), + termion::style::NoUnderline, + symbol_str[(self.sub_elem_idx as usize + 1)..].chars().collect::<String>() + ); + text.push_str(&formatted_str); if selected { text.push_str(&format!("{}", termion::style::Reset)); } @@ -289,13 +425,55 @@ impl EditMode for HexMode { 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; + 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<u8> { + 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> { @@ -307,7 +485,7 @@ struct Program<'a, 'b> { old_term: Termios, new_term: Termios, screen: &'a mut std::io::Write, - view: &'b mut FileView, + view: &'b mut EditableFileView, input_buf: Vec<char>, state: Mode, edits: Edits, @@ -327,18 +505,20 @@ impl <'a, 'b> Program<'a, 'b> { self.recalculate_seek(); } fn inc_cursor(&mut self, amount: u64) { - if self.view.size() - self.cursor < amount { + 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 += amount; + self.cursor += word_diff; } self.recalculate_seek(); } fn dec_cursor(&mut self, amount: u64) { - if self.cursor < amount { + let word_diff = self.mut_current_edit_view().dec_sub_elem(amount); + if self.cursor < word_diff { self.cursor = 0; } else { - self.cursor -= amount; + self.cursor -= word_diff; } self.recalculate_seek(); } @@ -383,6 +563,16 @@ impl <'a, 'b> Program<'a, 'b> { } } + 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 @@ -419,7 +609,7 @@ fn launch_interface(w: u16, h: u16, filename: String) { seek: 0, // screen: &mut std::io::stdout(), screen: &mut AlternateScreen::from(std::io::stdout().into_raw_mode().unwrap()), - view: &mut CachingFileView::new(filename).unwrap(), + view: &mut EditableFileView::new(filename).unwrap(), state: Mode::Edit, status: "".to_string(), input_buf: Vec::with_capacity(0), @@ -481,11 +671,13 @@ fn interface_loop(state: &mut Program) { state.state = Mode::ReadAddress; } Event::Key(Key::Up) => { - let amount = state.bytes_per_line(); // why can't this be inlined? + // 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) => { - let amount = state.bytes_per_line(); + // 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) => { @@ -496,12 +688,9 @@ fn interface_loop(state: &mut Program) { } Event::Key(Key::PageDown) => { let amount = state.bytes_per_line() * state.lines_to_draw(); - if state.view.size() - state.cursor < amount { + if state.cursor + amount > state.view.size() { state.cursor = state.view.size() - 1; - state.seek = std::cmp::min( - state.view.size() - (state.view.size() % state.bytes_per_line()), - state.seek + amount - ) + state.seek = state.view.size() + (state.view.size() % state.bytes_per_line()) - amount; } else { state.cursor += amount; state.seek += amount; @@ -520,20 +709,48 @@ fn interface_loop(state: &mut Program) { 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')) => { - state.status = "asf".to_string(); - state.state = Mode::ViewSelection; + // 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::Char('q')) => { + Event::Key(Key::Ctrl('x')) => { + // TODO warn if pending changes break; } + Event::Key(Key::Ctrl('a')) => { + state.status = "asf".to_string(); + 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); + state.current_edit_idx = state.current_edit_idx % (state.edit_views.len() - 1); } Event::Key(Key::Ctrl(x)) => { state.status = format!("ctrl {}", x); @@ -574,9 +791,10 @@ fn interface_loop(state: &mut Program) { } 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 + 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) => { @@ -675,8 +893,10 @@ fn render_interface(state: &mut Program) { 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(); + 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); @@ -690,10 +910,12 @@ fn render_interface(state: &mut Program) { 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 slice_start = width * i; - let slice_end = slice_start + width; let line = view.render_bytes( state.cursor, state.width, @@ -721,10 +943,21 @@ fn render_interface(state: &mut Program) { 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(); |