Files
hexapoda/src/app/widget.rs
T
2026-03-17 17:45:16 -05:00

384 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::{cmp::min, iter};
use ratatui::{buffer::Buffer, layout::Rect, text::{Line, Text}, widgets::Widget};
use crate::{BYTES_PER_LINE, app::App};
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
let screen_end = self.scroll_position + BYTES_PER_LINE * (area.height as usize - 1);
let bytes_end = min(screen_end, self.contents.len());
let bytes_to_render = &self.contents[self.scroll_position..bytes_end];
let (chunks, remainder) = bytes_to_render
.as_chunks::<BYTES_PER_LINE>();
let hex_lines = chunks
.iter()
.zip((self.scroll_position..).step_by(BYTES_PER_LINE))
.map(|(bytes, address)| self.render_line(address, bytes));
let remainder_address = bytes_end - remainder.len();
let remainder_line = if !remainder.is_empty() {
Some(self.render_partial_line(remainder_address, remainder))
} else {
None
};
let hex_text: Text = hex_lines
.chain(remainder_line)
.collect();
let hex_area = Rect::new(area.x, area.y, area.width, area.height - 1);
hex_text.render(hex_area, buf);
let status_line_area = Rect::new(area.x, area.bottom() - 1, area.width, 1);
self.render_status_line().render(status_line_area, buf);
self.render_extra_statuses()
.right_aligned()
.render(status_line_area, buf);
}
}
impl App {
#[allow(mismatched_lifetime_syntaxes)]
fn render_line(&self, address: usize, bytes: &[u8; BYTES_PER_LINE]) -> Line {
iter::once(address::render_address(address))
.chain(self.render_chunks(address, bytes))
.chain(iter::once(" ".into()))
.chain(character_panel::render_character_panel(bytes))
.collect()
}
#[allow(mismatched_lifetime_syntaxes)]
fn render_partial_line(&self, address: usize, bytes: &[u8]) -> Line {
iter::once(address::render_address(address))
.chain(self.render_partial_chunks(address, bytes))
.chain(iter::once(" ".into()))
.chain(character_panel::render_character_panel(bytes))
.collect()
}
}
mod address {
use ratatui::{style::{Color, Style}, text::Span};
pub fn render_address(address: usize) -> Span<'static> {
Span {
style: Style::new().fg(Color::Rgb(138, 187, 195)),
content: format!("{address:08x} ").into()
}
}
}
mod hex {
use std::{borrow::Cow, mem};
use itertools::{Itertools, repeat_n};
use ratatui::{style::{Color, Style, Stylize}, text::Span};
use crate::{BYTES_PER_CHUNK, BYTES_PER_LINE, CHUNKS_PER_LINE, app::App, cardinality::HasCardinality, cursor::InCursor, empty_span::empty_span, custom_greys::CustomGreys};
impl App {
pub fn render_chunks(
&self,
address: usize,
bytes: &[u8; BYTES_PER_LINE]
) -> impl Iterator<Item=Span<'static>> {
let (chunks, remainder) = bytes.as_chunks::<BYTES_PER_CHUNK>();
assert!(remainder.is_empty(), "BYTES_PER_LINE should be a multiple of BYTES_PER_CHUNK");
#[allow(unstable_name_collisions)]
chunks
.iter()
.copied()
.zip((address..).step_by(BYTES_PER_CHUNK))
.map(|(chunk, address)| self.render_chunk(address, &chunk).collect())
.interleave(
(address..)
.step_by(BYTES_PER_CHUNK)
.take(CHUNKS_PER_LINE)
.skip(1)
.map(|address| vec![self.render_large_space_before(address)])
)
.flatten()
}
pub fn render_partial_chunks(
&self,
address: usize,
bytes: &[u8]
) -> impl Iterator<Item=Span<'static>> {
let (chunks, remainder) = bytes.as_chunks::<BYTES_PER_CHUNK>();
let remainder_address = address + chunks.len() * BYTES_PER_CHUNK;
let remainder_chunks: Option<Vec<_>> = if !remainder.is_empty() {
Some(self.render_partial_chunk(remainder_address, remainder).collect())
} else {
None
};
let chunks_rendered = chunks.len() + remainder_chunks.iter().len();
let chunks_not_rendered = CHUNKS_PER_LINE - chunks_rendered;
let spaces_per_chunk = BYTES_PER_CHUNK - 1;
let bytes_not_rendered = BYTES_PER_LINE - bytes.len();
let padding_width = 2 * bytes_not_rendered +
spaces_per_chunk * chunks_not_rendered;
#[allow(unstable_name_collisions)]
chunks
.iter()
.copied()
.zip((address..).step_by(BYTES_PER_CHUNK))
.map(|(chunk, address)| self.render_chunk(address, &chunk).collect())
.chain(remainder_chunks)
.interleave(
(address..)
.step_by(BYTES_PER_CHUNK)
.take(CHUNKS_PER_LINE)
.skip(1)
.map(|address| vec![self.render_large_space_before(address)])
)
.flatten()
.chain(repeat_n(" ".into(), padding_width))
}
fn render_chunk(
&self,
address: usize,
bytes: &[u8; BYTES_PER_CHUNK]
) -> impl Iterator<Item=Span<'static>> {
#[allow(unstable_name_collisions)]
bytes
.iter()
.copied()
.zip(address..)
.map(|(byte, address)| self.render_byte_at(address, byte))
.interleave(
(address..)
.take(BYTES_PER_CHUNK)
.skip(1)
.map(|address| self.render_space_before(address))
)
}
fn render_partial_chunk(
&self,
address: usize,
bytes: &[u8]
) -> impl Iterator<Item=Span<'static>> {
#[allow(unstable_name_collisions)]
bytes
.iter()
.copied()
.zip(address..)
.map(|(byte, address)| self.render_byte_at(address, byte))
.interleave(
(address..)
.take(BYTES_PER_CHUNK)
.skip(1)
.map(|address| self.render_space_before(address))
)
}
fn render_byte_at(
&self,
address: usize,
byte: u8
) -> Span<'static> {
const SPAN_FOR_BYTE: [Span; u8::CARDINALITY] = create_byte_lookup_table();
let span = SPAN_FOR_BYTE[byte as usize].clone();
match self.cursor.contains(address) {
Some(InCursor::Head) => span.bg(Color::Gray),
Some(InCursor::Rest) => span.bg(Color::select_grey()),
None => span,
}
}
fn render_large_space_before(&self, address: usize) -> Span<'static> {
if self.cursor.contains_space_before(address) {
Span {
style: Style::new().bg(Color::select_grey()),
content: " ".into()
}
} else {
" ".into()
}
}
fn render_space_before(&self, address: usize) -> Span<'static> {
if self.cursor.contains_space_before(address) {
Span {
style: Style::new().bg(Color::select_grey()),
content: " ".into()
}
} else {
" ".into()
}
}
}
const fn create_byte_lookup_table() -> [Span<'static>; u8::CARDINALITY] {
let mut result = [const { empty_span() }; u8::CARDINALITY];
let mut index = 0;
while index < u8::CARDINALITY {
result[index].style = style_for_byte(index as u8);
mem::forget(mem::replace(&mut result[index].content, content_for_byte(index as u8)));
index += 1;
}
result
}
const fn style_for_byte(byte: u8) -> Style {
Style::new().fg(fg_for_byte(byte))
}
const fn fg_for_byte(byte: u8) -> Color {
match byte {
0x00 => Color::Rgb(0xA0, 0xA0, 0xA0), // grey
0x01..0x10 => Color::Rgb(0xFF, 0x71, 0xA9), // red
0x10..0x20 => Color::Rgb(0xFF, 0x7A, 0x78), // salmon
0x20..0x30 => Color::Rgb(0xFF, 0x81, 0x23), // red-orange
0x30..0x40 => Color::Rgb(0xF7, 0x93, 0x00), // yellow-orange
0x40..0x50 => Color::Rgb(0xE6, 0x9F, 0x00), // yellow
0x50..0x60 => Color::Rgb(0xC1, 0xB2, 0x00), // green-yellow
0x60..0x70 => Color::Rgb(0x82, 0xC6, 0x00), // lime
0x70..0x80 => Color::Rgb(0x00, 0xD5, 0x00), // green
0x80..0x90 => Color::Rgb(0x00, 0xD4, 0x59), // clover
0x90..0xA0 => Color::Rgb(0x00, 0xD0, 0x91), // teal
0xA0..0xB0 => Color::Rgb(0x00, 0xCC, 0xBB), // cyan
0xB0..0xC0 => Color::Rgb(0x00, 0xC7, 0xDE), // light blue
0xC0..0xD0 => Color::Rgb(0x00, 0xBE, 0xFF), // blue
0xD0..0xE0 => Color::Rgb(0x6C, 0xAF, 0xFF), // blurple
0xE0..0xF0 => Color::Rgb(0xB2, 0x98, 0xFF), // purple
0xF0..0xFF => Color::Rgb(0xFF, 0x4D, 0xFF), // pink
0xFF => Color::White
}
}
const fn content_for_byte(byte: u8) -> Cow<'static, str> {
Cow::Borrowed(hex_for_byte(byte))
}
const fn hex_for_byte(byte: u8) -> &'static str {
const LOOK_UP_TABLE: [&str; u8::CARDINALITY] = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0A", "0B", "0C", "0D", "0E", "0F", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1A", "1B", "1C", "1D", "1E", "1F", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2A", "2B", "2C", "2D", "2E", "2F", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3A", "3B", "3C", "3D", "3E", "3F", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4A", "4B", "4C", "4D", "4E", "4F", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5A", "5B", "5C", "5D", "5E", "5F", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6A", "6B", "6C", "6D", "6E", "6F", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7A", "7B", "7C", "7D", "7E", "7F", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8A", "8B", "8C", "8D", "8E", "8F", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9A", "9B", "9C", "9D", "9E", "9F", "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "AA", "AB", "AC", "AD", "AE", "AF", "B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9", "BA", "BB", "BC", "BD", "BE", "BF", "C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "CA", "CB", "CC", "CD", "CE", "CF", "D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "DA", "DB", "DC", "DD", "DE", "DF", "E0", "E1", "E2", "E3", "E4", "E5", "E6", "E7", "E8", "E9", "EA", "EB", "EC", "ED", "EE", "EF", "F0", "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "FA", "FB", "FC", "FD", "FE", "FF"];
LOOK_UP_TABLE[byte as usize]
}
}
mod character_panel {
use std::{borrow::Cow, mem};
use ratatui::{style::{Color, Style}, text::Span};
use crate::{cardinality::HasCardinality, empty_span::empty_span};
pub fn render_character_panel(
bytes: &[u8]
) -> impl Iterator<Item=Span<'static>> {
bytes
.iter()
.copied()
.map(render_character)
}
fn render_character(byte: u8) -> Span<'static> {
const SPAN_FOR_BYTE: [Span; u8::CARDINALITY] = create_character_lookup_table();
SPAN_FOR_BYTE[byte as usize].clone()
}
const fn create_character_lookup_table() -> [Span<'static>; u8::CARDINALITY] {
let mut result = [const { empty_span() }; u8::CARDINALITY];
let mut index = 0;
while index < u8::CARDINALITY {
result[index].style = style_for_character(index as u8);
mem::forget(mem::replace(
&mut result[index].content,
content_for_character(index as u8)
));
index += 1;
}
result
}
const fn style_for_character(byte: u8) -> Style {
Style::new().fg(fg_for_character(byte))
}
const fn fg_for_character(byte: u8) -> Color {
match byte {
b'\0' => Color::Rgb(0xA0, 0xA0, 0xA0), // grey
b'\t' | b'\n' | b'\r' | b' ' => Color::Red,
_ if byte.is_ascii_graphic() => Color::Red,
_ if byte.is_ascii() => Color::Green,
0xFF => Color::White,
_ => Color::Yellow,
}
}
const fn content_for_character(byte: u8) -> Cow<'static, str> {
Cow::Borrowed(character_for_byte(byte))
}
const fn character_for_byte(byte: u8) -> &'static str {
const LOOK_UP_TABLE: [&str; u8::CARDINALITY] = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "~", "", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", "×", ""];
LOOK_UP_TABLE[byte as usize]
}
}
mod status_line {
use crate::{app::App, custom_greys::CustomGreys};
use ratatui::{style::{Color, Stylize}, text::{Line, Span, Text}};
impl App {
pub fn render_status_line(&self) -> Text<'_> {
Text::from(
Line::from_iter([
self.render_mode(),
" ".into(),
self.render_file_name()
])
)
.bg(Color::ui_grey())
}
fn render_mode(&self) -> Span<'static> {
Span::from(self.mode.label())
.fg(Color::Black)
.bg(self.mode.color())
}
fn render_file_name(&self) -> Span<'_> {
Span::from(&self.file_name)
}
}
}
mod extra_statuses {
use crate::app::{App, PartialAction};
use ratatui::text::Line;
impl App {
pub fn render_extra_statuses(&self) -> Line<'_> {
#[allow(clippy::cast_precision_loss)]
let percentage = self.cursor.head as f64 / (self.contents.len() - 1) as f64 * 100.0;
let partial_action = self.partial_action
.as_ref()
.map_or("", PartialAction::label);
format!("{partial_action} {percentage:.0}% ").into()
}
}
}