From 86f6c4365169235b21225b78c8381a038fd02a43 Mon Sep 17 00:00:00 2001 From: alice pellerin Date: Tue, 17 Mar 2026 03:51:36 -0500 Subject: [PATCH] add status line --- src/app/mod.rs | 35 +++++++++++++++++++----- src/app/widget.rs | 66 +++++++++++++++++++++++++++++++-------------- src/custom_greys.rs | 16 +++++++++++ src/main.rs | 4 ++- src/select_grey.rs | 11 -------- 5 files changed, 94 insertions(+), 38 deletions(-) create mode 100644 src/custom_greys.rs delete mode 100644 src/select_grey.rs diff --git a/src/app/mod.rs b/src/app/mod.rs index a3327f5..b8b986b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,5 +1,6 @@ -use std::{cmp::min, env, fs::File, io::Read, process::exit}; +use std::{cmp::min, env, fs::File, io::Read, path::PathBuf, process::exit}; use crossterm::{event::{self, Event, KeyCode, KeyModifiers}, terminal::window_size}; +use ratatui::style::Color; use crate::{BYTES_PER_LINE, cursor::Cursor}; @@ -7,6 +8,7 @@ mod widget; #[derive(Debug)] pub struct App { + pub file_name: String, pub contents: Vec, pub window_rows: usize, pub scroll_position: usize, @@ -19,7 +21,7 @@ pub struct App { #[derive(Debug)] pub enum Mode { - Normal, Visual, Insert + Normal, Select, Insert } #[derive(Debug)] @@ -27,6 +29,24 @@ pub enum PartialShortcut { Goto, Zview } +impl Mode { + pub const fn label(&self) -> &'static str { + match self { + Self::Normal => " NORMAL ", + Self::Select => " SELECT ", + Self::Insert => " INSERT ", + } + } + + pub const fn color(&self) -> Color { + match self { + Self::Normal => Color::Blue, + Self::Select => Color::Yellow, + Self::Insert => Color::Green, + } + } +} + impl App { pub fn init() -> Self { let input_files: Vec<_> = env::args().skip(1).collect(); @@ -38,15 +58,17 @@ impl App { assert!(input_files.len() == 1); - let file_name = input_files.first().unwrap(); + let file_path: PathBuf = input_files.first().unwrap().into(); - let file = File::open(file_name); + let file = File::open(&file_path); let mut contents = Vec::new(); file.unwrap().read_to_end(&mut contents).unwrap(); Self { + file_name: file_path.file_name().unwrap().to_str().unwrap().to_owned(), contents, - window_rows: window_size().unwrap().rows as usize, + // -1 because of the status line + window_rows: window_size().unwrap().rows as usize - 1, scroll_position: 0, cursor: Cursor::default(), should_quit: false, @@ -66,7 +88,8 @@ impl App { #[allow(clippy::collapsible_match)] match (&self.mode, event::read().unwrap(), &self.partial_shortcut) { (Mode::Normal, Event::Resize(_, height), _) => { - self.window_rows = height as usize; + // -1 because of the status line + self.window_rows = height as usize - 1; } (Mode::Normal, Event::Key(key_event), None) diff --git a/src/app/widget.rs b/src/app/widget.rs index d96c90e..cd1bb13 100644 --- a/src/app/widget.rs +++ b/src/app/widget.rs @@ -1,14 +1,13 @@ use std::{cmp::min, iter}; -use ratatui::{text::{Line, Span, Text}, widgets::Widget}; +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: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { - let screen_end = self.scroll_position + BYTES_PER_LINE * (area.height as usize); + 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()); - // TODO: bounds check this let bytes_to_render = &self.contents[self.scroll_position..bytes_end]; let (chunks, remainder) = bytes_to_render @@ -16,28 +15,28 @@ impl Widget for &App { assert!(remainder.is_empty()); - let lines: Vec<_> = chunks + let hex_lines = chunks .iter() .zip((self.scroll_position..).step_by(BYTES_PER_LINE)) - .map(|(bytes, address)| self.render_line(address, bytes)) - .collect(); + .map(|(bytes, address)| self.render_line(address, bytes)); - let text = Text::from(lines); + let hex_area_text: Text = hex_lines.collect(); + let hex_area = Rect::new(area.x, area.y, area.width, area.height - 1); + hex_area_text.render(hex_area, buf); - text.render(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); } } impl App { #[allow(mismatched_lifetime_syntaxes)] fn render_line(&self, address: usize, bytes: &[u8; BYTES_PER_LINE]) -> Line { - let spans: Vec> = iter::once(address::render_address(address)) + iter::once(address::render_address(address)) .chain(self.render_chunks(address, bytes)) .chain(iter::once(" ".into())) .chain(character_panel::render_character_panel(bytes)) - .collect(); - - Line::from(spans) + .collect() } } @@ -57,14 +56,14 @@ mod hex { use itertools::Itertools; 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, select_grey::SelectGrey}; + 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 IntoIterator> { + ) -> impl Iterator> { let (chunks, remainder) = bytes.as_chunks::(); assert!(remainder.is_empty()); @@ -74,7 +73,7 @@ mod hex { .iter() .copied() .zip((address..).step_by(BYTES_PER_CHUNK)) - .map(|(chunk, address)| self.render_chunk(address, chunk)) + .map(|(chunk, address)| self.render_chunk(address, &chunk).collect()) .interleave( (address..) .step_by(BYTES_PER_CHUNK) @@ -88,8 +87,8 @@ mod hex { fn render_chunk( &self, address: usize, - bytes: [u8; BYTES_PER_CHUNK] - ) -> Vec> { + bytes: &[u8; BYTES_PER_CHUNK] + ) -> impl Iterator> { #[allow(unstable_name_collisions)] bytes .iter() @@ -102,7 +101,6 @@ mod hex { .skip(1) .map(|address| self.render_space_before(address)) ) - .collect() } fn render_byte_at( @@ -203,7 +201,7 @@ mod character_panel { pub fn render_character_panel( bytes: &[u8; BYTES_PER_LINE] - ) -> impl IntoIterator> { + ) -> impl Iterator> { bytes .iter() .copied() @@ -257,3 +255,31 @@ mod character_panel { 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) + } + } +} diff --git a/src/custom_greys.rs b/src/custom_greys.rs new file mode 100644 index 0000000..817257b --- /dev/null +++ b/src/custom_greys.rs @@ -0,0 +1,16 @@ +use ratatui::style::Color; + +pub trait CustomGreys { + fn select_grey() -> Self; + fn ui_grey() -> Self; +} + +impl CustomGreys for Color { + fn select_grey() -> Self { + Self::Indexed(242) + } + + fn ui_grey() -> Self { + Self::Indexed(238) + } +} diff --git a/src/main.rs b/src/main.rs index 2d24dd7..2b9bc39 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use app::App; mod cardinality; mod empty_span; -mod select_grey; +mod custom_greys; mod app; mod cursor; @@ -16,6 +16,8 @@ const CHUNKS_PER_LINE: usize = BYTES_PER_LINE / BYTES_PER_CHUNK; // TODO: // - modes // - g/v/z +// - search +// - jumplist // - modifications // - insert/append // - replace diff --git a/src/select_grey.rs b/src/select_grey.rs deleted file mode 100644 index 9e7d425..0000000 --- a/src/select_grey.rs +++ /dev/null @@ -1,11 +0,0 @@ -use ratatui::style::Color; - -pub trait SelectGrey { - fn select_grey() -> Self; -} - -impl SelectGrey for Color { - fn select_grey() -> Self { - Self::Indexed(242) - } -}