#![doc(html_logo_url = "https://raw.githubusercontent.com/Luz/hexdino/master/logo.png")]
use anyhow::{Context, Error};
use clap::Parser as ArgParser;
use std::cmp;
use std::io::prelude::*;
use std::io::SeekFrom;
use std::path::{Path, PathBuf};
mod draw;
use draw::draw;
mod search;
use search::*;
use memmem::{Searcher, TwoWaySearcher};
use crossterm::event::{read, Event};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
mod keycodes;
use pest::Parser;
use pest_derive::*;
#[derive(Parser)]
#[grammar = "cmd.pest"]
struct CmdParser;
mod cursor;
use cursor::Cursor;
#[derive(ArgParser)]
#[clap(version, long_about = None)]
struct Args {
#[clap(required = true, value_parser)]
filename: PathBuf,
#[clap(short, long, default_value = "")]
autoparse: String,
}
fn main() -> Result<(), Error> {
let args = Args::parse();
let mut buf = Vec::new();
let mut cursor = Cursor::default();
let mut screenoffset: usize = 0;
const COLS: usize = 16;
let mut command = String::new();
let mut lastcommand = String::new();
let mut autoparse = args.autoparse;
let mut infotext = String::new();
let screensize = crossterm::terminal::size()?;
let screenheight: usize = screensize.1 as usize;
let path = Path::new(&args.filename);
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&path)
.context("File could not be opened.")?;
file.read_to_end(&mut buf).expect("File could not be read.");
enable_raw_mode()?;
draw(&buf, COLS, &command, &mut infotext, cursor, screenoffset)?;
let mut quitnow = false;
while quitnow == false {
if autoparse.is_empty() {
let key = read()?;
let mut keycode: char = '\u{00}';
match key {
Event::Key(event) => {
keycode = keycodes::extract(event.code).unwrap_or('\u{00}');
}
Event::Mouse(_event) => (), Event::FocusGained => (), Event::FocusLost => (), Event::Paste(_text) => (), Event::Resize(_width, _height) => (), };
command.push_str(&keycode.clone().to_string());
} else {
command.push(autoparse.chars().nth(0).unwrap());
autoparse.remove(0);
}
let parsethisstring = command.clone();
let cmd = CmdParser::parse(Rule::cmd_list, &parsethisstring)
.unwrap()
.next()
.unwrap();
let mut clear = true;
let mut save = false;
infotext.clear();
match cmd.as_rule() {
Rule::down => {
cursor.add(COLS, buf.len());
}
Rule::up => {
cursor.sub(COLS, 0);
}
Rule::left => {
if cursor.is_over_ascii() {
cursor.sub(1, 0);
} else if cursor.is_over_right_nibble() {
cursor.select_left_nibble();
} else if cursor.is_over_left_nibble() {
if cursor.pos() >= 1 {
cursor.select_right_nibble();
}
cursor.sub(1, 0);
}
}
Rule::right => {
if cursor.is_over_ascii() {
cursor.add(1, buf.len());
} else if cursor.is_over_left_nibble() {
cursor.select_right_nibble();
} else if cursor.is_over_right_nibble() {
if cursor.pos() < buf.len().saturating_sub(1) {
cursor.select_left_nibble();
}
cursor.add(1, buf.len());
}
}
Rule::start => {
cursor.jump_to_start_of_line(COLS);
if cursor.is_over_right_nibble() {
cursor.select_left_nibble();
}
}
Rule::end => {
cursor.jump_to_end_of_line(COLS, buf.len());
if cursor.is_over_left_nibble() {
cursor.select_right_nibble();
}
}
Rule::bottom => {
let pos_on_line = cursor.calculate_pos_on_line(COLS);
let line = buf.len().saturating_sub(1) / COLS;
cursor.jump_to_pos_on_line(line, pos_on_line, COLS, buf.len());
}
Rule::replace => {
clear = false;
}
Rule::replacement => {
let key = command.chars().last().unwrap_or('x');
if cursor.is_over_ascii() {
if cursor.pos() >= buf.len() {
buf.insert(cursor.pos(), 0);
}
buf[cursor.pos()] = key as u8;
} else {
let mask = if cursor.is_over_left_nibble() {
0x0F
} else {
0xF0
};
let shift = if cursor.is_over_left_nibble() { 4 } else { 0 };
if cursor.pos() >= buf.len() {
buf.insert(cursor.pos(), 0);
}
if let Some(c) = key.to_digit(16) {
buf[cursor.pos()] = buf[cursor.pos()] & mask | (c as u8) << shift;
}
}
lastcommand = command.clone();
}
Rule::replaceend => {
}
Rule::remove => {
if buf.len() > 0 && cursor.pos() < buf.len() {
buf.remove(cursor.pos());
}
cursor.trim_to_max_minus_one(buf.len());
lastcommand = command.clone();
}
Rule::dd => {
let amount: usize = cmd.as_str().parse().unwrap_or(1);
if cursor.pos() < buf.len() {
let startofline = cursor.calculate_start_of_line(COLS);
let mut endofline = startofline + (COLS * amount);
endofline = cmp::min(endofline, buf.len());
buf.drain(startofline..endofline);
cursor.trim_to_max_minus_one(buf.len());
}
lastcommand = command.clone();
}
Rule::insert => {
clear = false;
}
Rule::insertstuff => {
let key = command.chars().last().unwrap_or('x');
if cursor.is_over_left_nibble() {
if let Some(c) = key.to_digit(16) {
buf.insert(cursor.pos(), (c as u8) << 4);
cursor.select_right_nibble();
}
} else if cursor.is_over_right_nibble() {
if cursor.pos() == buf.len() {
buf.insert(cursor.pos(), 0);
}
if let Some(c) = key.to_digit(16) {
buf[cursor.pos()] = buf[cursor.pos()] & 0xF0 | c as u8;
cursor.select_left_nibble();
cursor.add(1, buf.len() + 1);
}
} else if cursor.is_over_ascii() {
buf.insert(cursor.pos(), key as u8);
cursor.add(1, buf.len() + 1);
}
clear = false;
}
Rule::insertend => {
lastcommand = command.clone();
}
Rule::jumpascii => {
if cursor.is_over_ascii() {
cursor.select_left_nibble();
} else {
cursor.select_ascii();
}
}
Rule::querry => {
infotext.push_str(&format!("Current byte marked: {}", cursor.pos()));
}
Rule::repeat => {
autoparse = lastcommand.clone();
}
Rule::gg => {
let line: usize = cmd.as_str().parse().unwrap_or(0);
cursor.jump_to_line(line, COLS, buf.len());
}
Rule::searchend => {
if cursor.is_over_ascii() {
let searchstr = cmd.clone().into_inner().as_str();
let search = searchstr.as_bytes();
let foundpos = TwoWaySearcher::new(&search);
cursor.set_pos(foundpos.search_in(&buf).unwrap_or(cursor.pos()));
} else {
infotext.push_str("Ascii-search works, when the cursor is over ascii");
command.pop();
clear = false;
}
}
Rule::hexsearchend => {
if cursor.is_over_ascii() {
let searchstr = cmd.clone().into_inner().as_str();
let search = searchstr.as_bytes();
let foundpos = TwoWaySearcher::new(&search);
cursor.set_pos(foundpos.search_in(&buf).unwrap_or(cursor.pos()));
} else {
let searchbytes = cmd.clone().into_inner().as_str();
let search = searchbytes.as_bytes();
let mut needle = vec![];
for i in 0..search.len() {
let nibble = match search[i] as u8 {
c @ 48..=57 => c - 48, c @ b'a'..=b'f' => c - 87,
c @ b'A'..=b'F' => c - 55,
_ => 0x10, };
needle.push(nibble);
}
cursor.set_pos(buf.search(&needle).unwrap_or(cursor.pos()));
}
}
Rule::backspace => {
command.pop();
command.pop();
clear = false;
}
Rule::saveandexit => {
save = true;
quitnow = true;
}
Rule::exit => quitnow = true,
Rule::save => save = true,
Rule::escape => (),
Rule::gatherall => {
clear = false;
}
_ => (),
}
if save {
if path.exists() {
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&path)
.context("File could not be opened.")?;
file.seek(SeekFrom::Start(0))
.expect("Filepointer could not be set to 0");
file.write_all(&mut buf)
.expect("File could not be written.");
file.set_len(buf.len() as u64)
.expect("File could not be set to correct lenght.");
command.push_str("File saved!");
} else {
command.push_str("Careful, file could not be saved!");
}
}
if clear {
command.clear();
}
if cursor.pos() > (screenheight + screenoffset - 1) * COLS - 1 {
screenoffset = 2 + cursor.pos() / COLS - screenheight;
}
if cursor.pos() < screenoffset * COLS {
screenoffset = cursor.pos() / COLS;
}
draw(&buf, COLS, &command, &mut infotext, cursor, screenoffset)?;
}
disable_raw_mode()?;
Ok(())
}