summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoriximeow <me@iximeow.net>2017-12-24 02:32:35 -0800
committeriximeow <me@iximeow.net>2017-12-24 02:32:35 -0800
commitaba95d527a9cdaea1fe96b12051031b6f8f8ccac (patch)
tree39ee9b7f0fe3d12fdece6c783bc14c1f3cd5a893
parentc35796ef070e6b33c5cf3e961a6c55fe817fb72d (diff)
files are editable now!
-rw-r--r--main.rs313
1 files changed, 273 insertions, 40 deletions
diff --git a/main.rs b/main.rs
index b46cb48..0578d0f 100644
--- a/main.rs
+++ b/main.rs
@@ -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();