add status line

This commit is contained in:
alice pellerin
2026-03-17 03:51:36 -05:00
parent a8e0a4fdb0
commit 86f6c43651
5 changed files with 94 additions and 38 deletions
+29 -6
View File
@@ -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 crossterm::{event::{self, Event, KeyCode, KeyModifiers}, terminal::window_size};
use ratatui::style::Color;
use crate::{BYTES_PER_LINE, cursor::Cursor}; use crate::{BYTES_PER_LINE, cursor::Cursor};
@@ -7,6 +8,7 @@ mod widget;
#[derive(Debug)] #[derive(Debug)]
pub struct App { pub struct App {
pub file_name: String,
pub contents: Vec<u8>, pub contents: Vec<u8>,
pub window_rows: usize, pub window_rows: usize,
pub scroll_position: usize, pub scroll_position: usize,
@@ -19,7 +21,7 @@ pub struct App {
#[derive(Debug)] #[derive(Debug)]
pub enum Mode { pub enum Mode {
Normal, Visual, Insert Normal, Select, Insert
} }
#[derive(Debug)] #[derive(Debug)]
@@ -27,6 +29,24 @@ pub enum PartialShortcut {
Goto, Zview 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 { impl App {
pub fn init() -> Self { pub fn init() -> Self {
let input_files: Vec<_> = env::args().skip(1).collect(); let input_files: Vec<_> = env::args().skip(1).collect();
@@ -38,15 +58,17 @@ impl App {
assert!(input_files.len() == 1); 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(); let mut contents = Vec::new();
file.unwrap().read_to_end(&mut contents).unwrap(); file.unwrap().read_to_end(&mut contents).unwrap();
Self { Self {
file_name: file_path.file_name().unwrap().to_str().unwrap().to_owned(),
contents, 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, scroll_position: 0,
cursor: Cursor::default(), cursor: Cursor::default(),
should_quit: false, should_quit: false,
@@ -66,7 +88,8 @@ impl App {
#[allow(clippy::collapsible_match)] #[allow(clippy::collapsible_match)]
match (&self.mode, event::read().unwrap(), &self.partial_shortcut) { match (&self.mode, event::read().unwrap(), &self.partial_shortcut) {
(Mode::Normal, Event::Resize(_, height), _) => { (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) (Mode::Normal, Event::Key(key_event), None)
+46 -20
View File
@@ -1,14 +1,13 @@
use std::{cmp::min, iter}; 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}; use crate::{BYTES_PER_LINE, app::App};
impl Widget for &App { impl Widget for &App {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let screen_end = self.scroll_position + BYTES_PER_LINE * (area.height as usize); 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_end = min(screen_end, self.contents.len());
// TODO: bounds check this
let bytes_to_render = &self.contents[self.scroll_position..bytes_end]; let bytes_to_render = &self.contents[self.scroll_position..bytes_end];
let (chunks, remainder) = bytes_to_render let (chunks, remainder) = bytes_to_render
@@ -16,28 +15,28 @@ impl Widget for &App {
assert!(remainder.is_empty()); assert!(remainder.is_empty());
let lines: Vec<_> = chunks let hex_lines = chunks
.iter() .iter()
.zip((self.scroll_position..).step_by(BYTES_PER_LINE)) .zip((self.scroll_position..).step_by(BYTES_PER_LINE))
.map(|(bytes, address)| self.render_line(address, bytes)) .map(|(bytes, address)| self.render_line(address, bytes));
.collect();
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 { impl App {
#[allow(mismatched_lifetime_syntaxes)] #[allow(mismatched_lifetime_syntaxes)]
fn render_line(&self, address: usize, bytes: &[u8; BYTES_PER_LINE]) -> Line { fn render_line(&self, address: usize, bytes: &[u8; BYTES_PER_LINE]) -> Line {
let spans: Vec<Span<'_>> = iter::once(address::render_address(address)) iter::once(address::render_address(address))
.chain(self.render_chunks(address, bytes)) .chain(self.render_chunks(address, bytes))
.chain(iter::once(" ".into())) .chain(iter::once(" ".into()))
.chain(character_panel::render_character_panel(bytes)) .chain(character_panel::render_character_panel(bytes))
.collect(); .collect()
Line::from(spans)
} }
} }
@@ -57,14 +56,14 @@ mod hex {
use itertools::Itertools; use itertools::Itertools;
use ratatui::{style::{Color, Style, Stylize}, text::Span}; 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 { impl App {
pub fn render_chunks( pub fn render_chunks(
&self, &self,
address: usize, address: usize,
bytes: &[u8; BYTES_PER_LINE] bytes: &[u8; BYTES_PER_LINE]
) -> impl IntoIterator<Item=Span<'static>> { ) -> impl Iterator<Item=Span<'static>> {
let (chunks, remainder) = bytes.as_chunks::<BYTES_PER_CHUNK>(); let (chunks, remainder) = bytes.as_chunks::<BYTES_PER_CHUNK>();
assert!(remainder.is_empty()); assert!(remainder.is_empty());
@@ -74,7 +73,7 @@ mod hex {
.iter() .iter()
.copied() .copied()
.zip((address..).step_by(BYTES_PER_CHUNK)) .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( .interleave(
(address..) (address..)
.step_by(BYTES_PER_CHUNK) .step_by(BYTES_PER_CHUNK)
@@ -88,8 +87,8 @@ mod hex {
fn render_chunk( fn render_chunk(
&self, &self,
address: usize, address: usize,
bytes: [u8; BYTES_PER_CHUNK] bytes: &[u8; BYTES_PER_CHUNK]
) -> Vec<Span<'static>> { ) -> impl Iterator<Item=Span<'static>> {
#[allow(unstable_name_collisions)] #[allow(unstable_name_collisions)]
bytes bytes
.iter() .iter()
@@ -102,7 +101,6 @@ mod hex {
.skip(1) .skip(1)
.map(|address| self.render_space_before(address)) .map(|address| self.render_space_before(address))
) )
.collect()
} }
fn render_byte_at( fn render_byte_at(
@@ -203,7 +201,7 @@ mod character_panel {
pub fn render_character_panel( pub fn render_character_panel(
bytes: &[u8; BYTES_PER_LINE] bytes: &[u8; BYTES_PER_LINE]
) -> impl IntoIterator<Item=Span<'static>> { ) -> impl Iterator<Item=Span<'static>> {
bytes bytes
.iter() .iter()
.copied() .copied()
@@ -257,3 +255,31 @@ mod character_panel {
LOOK_UP_TABLE[byte as usize] 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)
}
}
}
+16
View File
@@ -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)
}
}
+3 -1
View File
@@ -5,7 +5,7 @@ use app::App;
mod cardinality; mod cardinality;
mod empty_span; mod empty_span;
mod select_grey; mod custom_greys;
mod app; mod app;
mod cursor; mod cursor;
@@ -16,6 +16,8 @@ const CHUNKS_PER_LINE: usize = BYTES_PER_LINE / BYTES_PER_CHUNK;
// TODO: // TODO:
// - modes // - modes
// - g/v/z // - g/v/z
// - search
// - jumplist
// - modifications // - modifications
// - insert/append // - insert/append
// - replace // - replace
-11
View File
@@ -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)
}
}