It shows bytes I guess

This commit is contained in:
maxstrb 2026-02-10 17:23:58 +01:00
parent 936c296449
commit 1ed84e6dee
13 changed files with 3986 additions and 2 deletions

19
.direnv/bin/nix-direnv-reload Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -e
if [[ ! -d "/mnt/removable/Projects/Rust/hexconv" ]]; then
echo "Cannot find source directory; Did you move it?"
echo "(Looking for "/mnt/removable/Projects/Rust/hexconv")"
echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
exit 1
fi
# rebuild the cache forcefully
_nix_direnv_force_reload=1 direnv exec "/mnt/removable/Projects/Rust/hexconv" true
# Update the mtime for .envrc.
# This will cause direnv to reload again - but without re-building.
touch "/mnt/removable/Projects/Rust/hexconv/.envrc"
# Also update the timestamp of whatever profile_rc we have.
# This makes sure that we know we are up to date.
touch -r "/mnt/removable/Projects/Rust/hexconv/.envrc" "/mnt/removable/Projects/Rust/hexconv/.direnv"/*.rc

View file

@ -0,0 +1 @@
/nix/store/316cjg3w8hhq1iv11lila4kswkscv833-source

View file

@ -0,0 +1 @@
/nix/store/fjmkkw7yy5da27n94hyk0zcda538gf97-source

View file

@ -0,0 +1 @@
/nix/store/yspm41avnsc4gr8cjamkv0cf10i3spsi-source

View file

@ -0,0 +1 @@
/nix/store/swz4b263qq4jdxs4fk62cnrg1v7jvmgr-nix-shell-env

File diff suppressed because it is too large Load diff

1511
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -3,4 +3,13 @@ name = "hexconv"
version = "0.1.0"
edition = "2024"
[dependencies]
crossterm = "0.29.0"
ratatui = "0.30.0"
[profile.release]
codegen-units = 1
lto = true
opt-level = "s"
strip = true

56
src/app_state.rs Normal file
View file

@ -0,0 +1,56 @@
use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
use ratatui::layout::Rect;
pub struct AppState {
bytes_buffer: Vec<u8>,
top: usize,
}
impl AppState {
pub fn get_bytes(&self, bytes: usize) -> &[u8] {
let offset = (self.bytes_buffer.len() - self.top).min(bytes);
&self.bytes_buffer[self.top..self.top + offset]
}
pub fn get_top(&self) -> usize {
self.top
}
pub fn new(path: &Path) -> io::Result<Self> {
let mut file_buffer = [0; 4096];
let len;
{
let mut file = File::open(path)?;
len = file.read(&mut file_buffer)?;
}
Ok(Self {
bytes_buffer: file_buffer[0..len].to_vec(),
top: 0,
})
}
pub fn scroll_up(&mut self, by: usize) {
self.top =
(self.top + 16 * by).min(self.bytes_buffer.len() - (self.bytes_buffer.len() % 16));
}
pub fn scroll_down(&mut self, by: usize) {
self.top = self.top.saturating_sub(16 * by);
}
pub fn correct_scroll(&mut self, area: Rect) {
let visible_lines = area.height as usize;
let lines = self.get_bytes(visible_lines * 16);
let loaded_lines = lines.len().div_ceil(16);
if loaded_lines < visible_lines {
self.scroll_down(visible_lines - loaded_lines);
}
}
}

46
src/dump.rs Normal file
View file

@ -0,0 +1,46 @@
use ratatui::prelude::*;
use ratatui::widgets::Paragraph;
pub struct DumpWidget<'a>(pub &'a [u8]);
fn bytes_to_str<'a>(bytes: &[u8], out: &'a mut [u8; 49]) -> &'a str {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let len = bytes.len();
let mut min = 8.min(len);
let mut counter = 2 + (len - 1) * 3;
for (index, byte) in (bytes[0..min]).iter().enumerate() {
out[index * 3] = HEX[(byte >> 4) as usize];
out[index * 3 + 1] = HEX[(byte & 0x0f) as usize];
out[index * 3 + 2] = b' ';
}
if bytes.len() > 8 {
counter += 2;
min = 16.min(len);
for (index, byte) in (bytes[8..min]).iter().enumerate() {
out[25 + index * 3] = b' ';
out[25 + index * 3 + 1] = HEX[(byte >> 4) as usize];
out[25 + index * 3 + 2] = HEX[(byte & 0x0f) as usize];
}
out[24] = b'|';
}
unsafe { std::str::from_utf8_unchecked(&out[..counter]) }
}
impl<'a> Widget for DumpWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let visible_lines = area.height as usize;
let constraints = Layout::vertical(vec![Constraint::Length(1); visible_lines]).split(area);
let mut out = [b' '; 49];
for (line, constraint) in self.0.chunks(16).zip(constraints.iter()) {
Paragraph::new(bytes_to_str(line, &mut out)).render(*constraint, buf);
}
}
}

34
src/line_number.rs Normal file
View file

@ -0,0 +1,34 @@
use ratatui::prelude::*;
use ratatui::widgets::Widget;
pub struct LineNumberWidget(pub usize, pub usize);
fn usize_to_hex(mut value: usize, buffer: &mut [u8; 64]) -> &str {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let len = std::mem::size_of_val(&value);
for i in 0..len {
let current_byte = value & 0xff;
buffer[63 - 2 * i] = HEX[current_byte & 0x0f];
buffer[63 - 2 * i - 1] = HEX[current_byte >> 4];
value >>= 8;
}
unsafe { std::str::from_utf8_unchecked(&buffer[64 - len * 2..]) }
}
impl Widget for LineNumberWidget {
fn render(self, area: Rect, buf: &mut Buffer) {
let visible_lines = area.height as usize;
let mut buffer = [b'0'; 64];
let areas =
Layout::vertical(vec![Constraint::Length(1); visible_lines.min(self.1)]).split(area);
for (i, a) in areas.iter().enumerate() {
Line::from(usize_to_hex(self.0 + i * 16, &mut buffer)).render(*a, buf);
}
}
}

View file

@ -1,3 +1,96 @@
fn main() {
println!("Hello, world!");
mod app_state;
mod dump;
mod line_number;
mod text_preview;
use ratatui::{
DefaultTerminal, Frame,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
layout::{Constraint, Direction, Layout},
};
use std::{io, path::Path};
use crate::{
app_state::AppState, dump::DumpWidget, line_number::LineNumberWidget, text_preview::TextPreview,
};
fn main() -> io::Result<()> {
let mut terminal = ratatui::init();
let app_result = App::new()?.run(&mut terminal);
ratatui::restore();
app_result
}
struct App {
app_state: AppState,
exit: bool,
}
impl App {
fn new() -> io::Result<Self> {
Ok(Self {
app_state: AppState::new(Path::new("/home/maxag/ree.txt"))?,
exit: false,
})
}
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
while !self.exit {
terminal.draw(|frame| self.draw(frame))?;
let _ = self.handle_events();
}
Ok(())
}
fn handle_events(&mut self) -> io::Result<()> {
let Event::Key(current_event) = event::read()? else {
return Ok(());
};
if current_event.kind != KeyEventKind::Press {
return Ok(());
}
match current_event.code {
KeyCode::Char('q') => {
self.exit = true;
}
KeyCode::Up => {
self.app_state.scroll_down(1);
}
KeyCode::Down => {
self.app_state.scroll_up(1);
}
_ => (),
}
Ok(())
}
fn draw(&mut self, frame: &mut Frame) {
let main_area = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![
Constraint::Length(18),
Constraint::Length(51),
Constraint::Length(20),
])
.split(frame.area());
if frame.area().width < 10 || frame.area().height < 30 {
self.exit = true;
return;
}
let visible_lines = main_area[1].height as usize;
self.app_state.correct_scroll(main_area[1]);
let bytes = self.app_state.get_bytes(visible_lines * 16);
let top = self.app_state.get_top();
let lines = bytes.len().div_ceil(16);
frame.render_widget(DumpWidget(bytes), main_area[1]);
frame.render_widget(LineNumberWidget(top, lines), main_area[0]);
frame.render_widget(TextPreview(bytes), main_area[2]);
}
}

30
src/text_preview.rs Normal file
View file

@ -0,0 +1,30 @@
use ratatui::{prelude::*, widgets::Paragraph};
pub struct TextPreview<'a>(pub &'a [u8]);
fn bytes_to_str<'a>(bytes: &[u8], out: &'a mut [u8; 19]) -> &'a str {
let len = bytes.len() + 1;
out[0] = b'|';
for (index, byte) in bytes.iter().enumerate() {
out[index + 1] = if *byte > 31 && *byte < 128 {
*byte
} else {
b'.'
};
}
unsafe { std::str::from_utf8_unchecked(&out[..len]) }
}
impl Widget for TextPreview<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut line_buffer = [0; 19];
let visible_lines = area.height as usize;
let constraints = Layout::vertical(vec![Constraint::Length(1); visible_lines]).split(area);
for (line, constraint) in self.0.chunks(16).zip(constraints.iter()) {
Paragraph::new(bytes_to_str(line, &mut line_buffer)).render(*constraint, buf);
}
}
}