summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/hex.rs48
-rw-r--r--src/input.rs25
-rw-r--r--src/render.rs133
3 files changed, 160 insertions, 46 deletions
diff --git a/src/hex.rs b/src/hex.rs
index 67f692d..7980252 100644
--- a/src/hex.rs
+++ b/src/hex.rs
@@ -1,27 +1,64 @@
use std::{fs, io};
use std::io::stdout;
use ratatui::{backend::CrosstermBackend, Terminal};
+use ratatui::widgets::{ScrollbarState, ScrollDirection};
use crossterm::{execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}};
use crate::{input, render};
-
+
pub struct HexEditor {
filename: String,
data: Vec<u8>,
cursor: usize,
- terminal: Terminal<CrosstermBackend<std::io::Stdout>>,
+ terminal: Terminal<CrosstermBackend<std::io::Stdout>,>,
+ scrollbar_state: ScrollbarState,
+ scroll_position: usize
}
impl HexEditor {
+
pub fn new(filename: String, data: Vec<u8>) -> io::Result<Self> {
enable_raw_mode()?;
execute!(stdout(), EnterAlternateScreen)?;
let terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
- Ok(Self { filename, data, cursor: 0, terminal })
+ let line_count = (data.len() + 15) / 16;
+ let scrollbar_state = ScrollbarState::default()
+ .content_length(line_count)
+ .viewport_content_length(10);
+
+ Ok(Self {
+ filename,
+ data,
+ cursor: 0,
+ terminal,
+ scrollbar_state,
+ scroll_position: 0,
+ })
}
+
pub fn run(&mut self) -> io::Result<()> {
- while input::handle_input(&mut self.cursor, &mut self.data[..])? {
- render::draw(&mut self.terminal, &self.data, self.cursor, &self.filename)?;
+ while input::handle_input(
+ &mut self.cursor,
+ &mut self.data,
+ &mut self.scroll_position
+ )? {
+ let total_lines = (self.data.len() + 15) / 16;
+ let visible_lines = self.terminal.size()?.height as usize;
+
+
+ self.scrollbar_state = ScrollbarState::new(total_lines)
+ .viewport_content_length(visible_lines)
+ .position(self.scroll_position);
+
+
+ render::draw(
+ &mut self.terminal,
+ &self.data,
+ self.cursor,
+ &self.filename,
+ self.scroll_position,
+ &mut self.scrollbar_state
+ )?;
}
self.cleanup()
}
@@ -32,4 +69,5 @@ impl HexEditor {
fs::write(&self.filename, &self.data)?;
Ok(())
}
+
}
diff --git a/src/input.rs b/src/input.rs
index 0db7e16..1258ac0 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -1,20 +1,35 @@
use std::{io, time::Duration};
use crossterm::{event::{self, KeyCode, KeyEvent}};
-pub fn handle_input(cursor: &mut usize, data: &mut [u8]) -> io::Result<bool> {
+pub fn handle_input(
+ cursor: &mut usize,
+ data: &mut [u8],
+ scroll_position: &mut usize
+) -> io::Result<bool> {
if event::poll(Duration::from_millis(100))? {
if let event::Event::Key(KeyEvent { code, .. }) = event::read()? {
match code {
KeyCode::Esc => return Ok(false),
KeyCode::Left if *cursor > 0 => *cursor -= 1,
KeyCode::Right if *cursor < data.len() * 2 - 1 => *cursor += 1,
- KeyCode::Up if *cursor >= 32 => *cursor -= 32,
- KeyCode::Down if *cursor + 32 < data.len() * 2 => *cursor += 32,
+ KeyCode::Up => {
+ if *cursor >= 32 {
+ *cursor -= 32;
+ *scroll_position = scroll_position.saturating_sub(1);
+ }
+ },
+ KeyCode::Down => {
+ if *cursor + 32 < data.len() * 2 {
+ *cursor += 32;
+ *scroll_position = scroll_position.saturating_add(1);
+ }
+ },
KeyCode::Char(c) if c.is_ascii_hexdigit() => {
let byte_index = *cursor / 2;
let shift = if *cursor % 2 == 0 { 4 } else { 0 };
- data[byte_index] = (data[byte_index] & (0x0F << (4 - shift))) | (c.to_digit(16).unwrap() as u8) << shift;
- }
+ data[byte_index] = (data[byte_index] & (0x0f << (4 - shift)))
+ | (c.to_digit(16).unwrap() as u8) << shift;
+ },
_ => {}
}
}
diff --git a/src/render.rs b/src/render.rs
index a0cb234..061dbfe 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -3,12 +3,22 @@ use ratatui::{
layout::{Constraint, Direction, Layout},
style::{Color, Style},
text::{Line, Span},
- widgets::{Block, Borders, Paragraph},
+ widgets::{
+ Block, Borders, Paragraph, ScrollbarState, Scrollbar, ScrollbarOrientation,
+ },
Terminal,
prelude::Alignment,
+ symbols,
};
-pub fn draw(terminal: &mut Terminal<impl ratatui::backend::Backend>, data: &[u8], cursor: usize, filename: &str) -> io::Result<()> {
+pub fn draw(
+ terminal: &mut Terminal<impl ratatui::backend::Backend>,
+ data: &[u8],
+ cursor: usize,
+ filename: &str,
+ scroll_position: usize,
+ scrollbar_state: &mut ScrollbarState,
+) -> io::Result<()> {
terminal.draw(|frame| {
let size = frame.size();
let layout = Layout::default()
@@ -16,52 +26,103 @@ pub fn draw(terminal: &mut Terminal<impl ratatui::backend::Backend>, data: &[u8]
.constraints([Constraint::Min(1), Constraint::Length(1)])
.split(size);
- let lines: Vec<Line> = data.chunks(16).enumerate().map(|(i, chunk)| {
- let addr = Span::raw(format!("{:08x} │ ", i * 16));
- let mut hex: Vec<Span> = Vec::new();
- let mut ascii_repr = String::new();
- for (j, &byte) in chunk.iter().enumerate() {
- let hex_str = format!("{:02x}", byte);
- let index = i * 32 + j * 2;
+ let total_lines = (data.len() + 15) / 16;
+ let visible_height = layout[0].height as usize;
- for (k, ch) in hex_str.chars().enumerate() {
- let cur_idx = index + k;
- let style = if cur_idx == cursor {
- Style::default().bg(Color::White).fg(Color::Black)
+ *scrollbar_state = ScrollbarState::new(total_lines)
+ .viewport_content_length(visible_height)
+ .position(scroll_position);
+
+ let lines: Vec<Line> = data
+ .chunks(16)
+ .enumerate()
+ .skip(scroll_position)
+ .take(visible_height)
+ .map(|(i, chunk)| {
+ let addr = format!("{:08X} │ ", i * 16);
+ let mut hex_spans = Vec::new();
+ let mut ascii = String::new();
+
+ for (j, &byte) in chunk.iter().enumerate() {
+ let global_pos = i * 16 + j;
+ let nibbles = format!("{:02X}", byte);
+
+ for (k, c) in nibbles.chars().enumerate() {
+ let is_cursor = (global_pos * 2 + k) == cursor;
+ let style = if is_cursor {
+ Style::default().bg(Color::White).fg(Color::Black)
+ } else {
+ Style::default()
+ };
+ hex_spans.push(Span::styled(c.to_string(), style));
+ }
+
+
+ if j < 15 {
+ hex_spans.push(Span::raw(if j % 4 == 3 { " " } else { " " }));
+ }
+
+
+ ascii.push(if byte.is_ascii_graphic() || byte == b' ' {
+ byte as char
} else {
- Style::default()
- };
- hex.push(Span::styled(ch.to_string(), style));
+ '.'
+ });
}
- if j < 15 {
- let space = if j % 4 == 3 { " " } else { " " };
- hex.push(Span::raw(space));
+
+ if chunk.len() < 16 {
+ let missing = 16 - chunk.len();
+ hex_spans.push(Span::raw(" ".repeat(missing * 3)));
+ ascii.push_str(&".".repeat(missing));
}
- ascii_repr.push(if byte.is_ascii_graphic() || byte == b' ' { byte as char } else { '.' });
- }
+ Line::from(vec![
+ Span::raw(addr),
+ Span::raw(hex_spans.iter().map(|s| s.content.as_ref()).collect::<String>()),
+ Span::raw(" │ "),
+ Span::raw(ascii),
+ ])
+ })
+ .collect();
+
+
+ frame.render_widget(
+ Paragraph::new(lines)
+ .block(Block::default().borders(Borders::ALL)),
+ layout[0]
+ );
+
- let missing_bytes = 16 - chunk.len();
- let padding = missing_bytes * 3;
- let spaces = " ".repeat(padding);
+ frame.render_stateful_widget(
+ Scrollbar::default()
+ .orientation(ScrollbarOrientation::VerticalRight)
+ .symbols(symbols::scrollbar::VERTICAL),
+ layout[0],
+ scrollbar_state,
+ );
- let ascii = Span::raw(format!("{} │ {}", spaces, ascii_repr));
- Line::from(vec![addr]
- .into_iter()
- .chain(hex)
- .chain(std::iter::once(ascii))
- .collect::<Vec<_>>())
- }).collect();
+ let progress = if data.is_empty() {
+ 0
+ } else {
+ (cursor / 2 * 100) / data.len()
+ };
- frame.render_widget(Paragraph::new(lines).block(Block::default().borders(Borders::ALL)), layout[0]);
+ let status = format!(
+ "{} │ Offset: 0x{:08X} │ {}%",
+ filename,
+ cursor / 2,
+ progress
+ );
- let progress = if data.is_empty() { 100 } else { (cursor / 2 * 100) / data.len() };
- let status_text = format!("{} \t 0x{:08x} \t {:3}%", filename, cursor / 2, progress);
- let status_bar = Paragraph::new(Line::from(Span::raw(status_text))).alignment(Alignment::Center);
- frame.render_widget(status_bar, layout[1]);
+ frame.render_widget(
+ Paragraph::new(status)
+ .alignment(Alignment::Center)
+ .block(Block::default().borders(Borders::TOP)),
+ layout[1]
+ );
})?;
Ok(())
}