From fa519b8b57ab4df70521d78733bd68087b67cae5 Mon Sep 17 00:00:00 2001 From: TylerBloom Date: Sat, 17 Jan 2026 19:11:51 -0500 Subject: [PATCH 1/7] [tombstone] Added dynamic pane management --- tombstone/src/state.rs | 112 ++++++++++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 24 deletions(-) diff --git a/tombstone/src/state.rs b/tombstone/src/state.rs index a927c28..ae750e6 100644 --- a/tombstone/src/state.rs +++ b/tombstone/src/state.rs @@ -1,6 +1,7 @@ use std::collections::HashSet; use std::fmt::Write as _; use std::io; +use std::rc::Rc; use std::sync::Arc; use std::sync::Mutex; @@ -14,6 +15,7 @@ use ratatui::backend::Backend; use ratatui::layout::Constraint; use ratatui::layout::Direction; use ratatui::layout::Layout; +use ratatui::layout::Rect; use tracing::level_filters::LevelFilter; use tracing_subscriber::fmt::MakeWriter; use tracing_subscriber::fmt::format::FmtSpan; @@ -127,36 +129,98 @@ impl AppState { } fn render_frame(&mut self, frame: &mut Frame) { - let sections = Layout::default() + let Panes { + repl, + cpu, + mem, + ram, + oam_dma, + interrupts, + pc_state, + } = divide_frame(frame.area()); + self.cli.render(frame, repl); + render_mem(&self.inner, frame, mem); + render_ram(&self.inner, frame, ram); + render_cpu(&self.inner, frame, cpu); + render_oam_dma(&self.inner, frame, oam_dma); + render_interrupts(&self.inner, frame, interrupts); + self.pc_state.render(&self.inner, frame, pc_state); + } +} + +struct Panes { + repl: Rect, + cpu: Rect, + mem: Rect, + ram: Rect, + oam_dma: Rect, + interrupts: Rect, + pc_state: Rect, +} + +impl Panes { + // NOTE: All of these add two to account for the border + const CPU_HEIGHT: u16 = 10; + const OAM_DMA_HEIGHT: u16 = 9; + const INTERRUPTS_HEIGHT: u16 = 4; + const RIGHT_COL_WIDTH: u16 = 35; + const MEM_WIDTH: u16 = 60; + const REPL_MIN_WIDTH: u16 = 60; +} + +fn divide_frame(area: Rect) -> Panes { + let area = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Fill(1), + Constraint::Length(Panes::RIGHT_COL_WIDTH), + ]) + .split(area); + let [left, right] = *Rc::<[_; 2]>::try_from(area).unwrap(); + let area = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(Panes::CPU_HEIGHT), + Constraint::Length(Panes::OAM_DMA_HEIGHT), + Constraint::Length(Panes::INTERRUPTS_HEIGHT), + Constraint::Fill(1), + ]) + .split(right); + let [cpu, oam_dma, interrupts, pc_state] = *Rc::<[_; 4]>::try_from(area).unwrap(); + let (repl, [mem, ram]) = if left.width.saturating_sub(Panes::MEM_WIDTH) >= Panes::REPL_MIN_WIDTH + { + let area = Layout::default() .direction(Direction::Horizontal) - .constraints([Constraint::Fill(1), Constraint::Length(40)]) - .split(frame.area()); - let left = sections[0]; - let right = sections[1]; - let left = Layout::default() + .constraints([Constraint::Length(Panes::MEM_WIDTH), Constraint::Fill(1)]) + .split(left); + let [mem, repl] = *Rc::<[_; 2]>::try_from(area).unwrap(); + let area = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Fill(1), Constraint::Fill(1)]) + .split(mem); + let mem = *Rc::<[_; 2]>::try_from(area).unwrap(); + (repl, mem) + } else { + let area = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Fill(1), Constraint::Fill(2)]) .split(left); - let mem = Layout::default() + let [mem, repl] = *Rc::<[_; 2]>::try_from(area).unwrap(); + let area = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Fill(1), Constraint::Fill(1)]) - .split(left[1]); - let right = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(10), - Constraint::Length(20), - Constraint::Length(4), - Constraint::Fill(1), - ]) - .split(right); - self.cli.render(frame, left[0]); - render_mem(&self.inner, frame, mem[0]); - render_ram(&self.inner, frame, mem[1]); - render_cpu(&self.inner, frame, right[0]); - render_oam_dma(&self.inner, frame, right[1]); - render_interrupts(&self.inner, frame, right[2]); - self.pc_state.render(&self.inner, frame, right[3]); + .split(mem); + let mem = *Rc::<[_; 2]>::try_from(area).unwrap(); + (repl, mem) + }; + Panes { + repl, + cpu, + mem, + ram, + oam_dma, + interrupts, + pc_state, } } From 1c30b0c96767f3b4591dd1be77af48967f44c34c Mon Sep 17 00:00:00 2001 From: TylerBloom Date: Sat, 17 Jan 2026 21:09:58 -0500 Subject: [PATCH 2/7] [tombstone] Renamed CLI to REPL --- tombstone/src/main.rs | 2 +- tombstone/src/{cli => repl}/commands.rs | 2 +- tombstone/src/{cli => repl}/history.rs | 0 tombstone/src/{cli => repl}/mod.rs | 8 ++++---- tombstone/src/state.rs | 8 ++++---- 5 files changed, 10 insertions(+), 10 deletions(-) rename tombstone/src/{cli => repl}/commands.rs (99%) rename tombstone/src/{cli => repl}/history.rs (100%) rename tombstone/src/{cli => repl}/mod.rs (97%) diff --git a/tombstone/src/main.rs b/tombstone/src/main.rs index b55ef52..5f780af 100644 --- a/tombstone/src/main.rs +++ b/tombstone/src/main.rs @@ -10,7 +10,7 @@ use crossterm::terminal::enable_raw_mode; use ratatui::Terminal; use ratatui::backend::CrosstermBackend; -pub mod cli; +pub mod repl; pub mod command; pub mod config; pub mod display_windows; diff --git a/tombstone/src/cli/commands.rs b/tombstone/src/repl/commands.rs similarity index 99% rename from tombstone/src/cli/commands.rs rename to tombstone/src/repl/commands.rs index 2b39250..575c489 100644 --- a/tombstone/src/cli/commands.rs +++ b/tombstone/src/repl/commands.rs @@ -15,7 +15,7 @@ use ratatui::layout::Position; use ratatui::prelude::Backend; use crate::Command; -use crate::cli::PROMPT; +use crate::repl::PROMPT; use crate::command::ReplCommand; /// Create a thread to poll for user inputs and forward them to the main thread. diff --git a/tombstone/src/cli/history.rs b/tombstone/src/repl/history.rs similarity index 100% rename from tombstone/src/cli/history.rs rename to tombstone/src/repl/history.rs diff --git a/tombstone/src/cli/mod.rs b/tombstone/src/repl/mod.rs similarity index 97% rename from tombstone/src/cli/mod.rs rename to tombstone/src/repl/mod.rs index 6b61052..8387ed8 100644 --- a/tombstone/src/cli/mod.rs +++ b/tombstone/src/repl/mod.rs @@ -9,14 +9,14 @@ mod commands; mod history; use crate::Command; -use crate::cli::commands::CommandProcessor; -use crate::cli::history::CliHistory; +use crate::repl::commands::CommandProcessor; +use crate::repl::history::CliHistory; /// The text displayed at the start of the shell command input. static PROMPT: &str = "> "; #[derive(Debug, Default)] -pub struct Cli { +pub struct Repl { processor: CommandProcessor, history: CliHistory, /// Tracks where in the CLI history the cursor is at. Used for events like Up and Down. @@ -25,7 +25,7 @@ pub struct Cli { history_index: usize, } -impl Cli { +impl Repl { pub fn new() -> Self { Self { processor: CommandProcessor::new(), diff --git a/tombstone/src/state.rs b/tombstone/src/state.rs index ae750e6..84e21a0 100644 --- a/tombstone/src/state.rs +++ b/tombstone/src/state.rs @@ -36,7 +36,7 @@ use crate::RunFor; use crate::RunLength; use crate::RunUntil; use crate::ViewCommand; -use crate::cli::Cli; +use crate::repl::Repl; use crate::command::BreakpointCommand; use crate::config::Config; use crate::config::GameConfig; @@ -61,7 +61,7 @@ use crate::pc_state::PcState; /// Rc). pub(crate) struct AppState { inner: InnerAppState, - cli: Cli, + cli: Repl, log_buffer: Arc>>, pc_state: PcState, } @@ -95,7 +95,7 @@ impl AppState { Self { inner, log_buffer, - cli: Cli::new(), + cli: Repl::new(), pc_state: PcState::new(), } } @@ -225,7 +225,7 @@ fn divide_frame(area: Rect) -> Panes { } impl InnerAppState { - fn process(&mut self, cli: &mut Cli, pc: &mut PcState, cmd: Command) { + fn process(&mut self, cli: &mut Repl, pc: &mut PcState, cmd: Command) { match cmd { Command::Read { index } => { let val = self.gb.gb().mem.read_byte(index); From 55930d063263de6d99957f5b619d51b11d015fe4 Mon Sep 17 00:00:00 2001 From: TylerBloom Date: Sun, 18 Jan 2026 18:04:58 -0500 Subject: [PATCH 3/7] [tombstone] Impl-ed simple text wrapping in REPL --- tombstone/Cargo.toml | 2 +- tombstone/src/repl/history.rs | 39 ++++++++++++++++++++++++++++------- tombstone/src/repl/mod.rs | 2 +- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/tombstone/Cargo.toml b/tombstone/Cargo.toml index a6b5205..c6c1bea 100644 --- a/tombstone/Cargo.toml +++ b/tombstone/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" ghast = { path = "../ghast" } clap = { version = "4.5.18", features = ["derive"] } crossterm = "0.29.0" -ratatui = { version = "0.30.0", features = ["all-widgets"] } +ratatui = { version = "=0.29.0", features = ["all-widgets"] } spirit = { path = "../spirit" } tracing = "0.1.40" tracing-subscriber = "0.3.18" diff --git a/tombstone/src/repl/history.rs b/tombstone/src/repl/history.rs index 2818243..d8a513f 100644 --- a/tombstone/src/repl/history.rs +++ b/tombstone/src/repl/history.rs @@ -1,5 +1,5 @@ use ratatui::layout::Rect; -use ratatui::text::Text; +use ratatui::text::{Line, Span, Text}; use ratatui::widgets::Paragraph; use super::PROMPT; @@ -29,14 +29,37 @@ impl CliHistory { } pub fn render(&self, rect: Rect) -> (u16, Paragraph<'_>) { - let digest = std::cmp::min(self.visual_history.len() as u16, rect.height - 1); - let iter = self + if self.visual_history.is_empty() { + return (0, Paragraph::default()); + } + // Calcuate the number of lines needed for the visual history, before wrapping. + let index = self .visual_history + .len() + .saturating_sub(rect.height as usize); + let entries = &self.visual_history[index..]; + + // Collect the wrapped lines into in a vec to be trimmed + let width = rect.width as usize; + let lines: Vec<_> = entries .iter() - .rev() - .take((rect.height - 1) as usize) - .rev() - .map(String::as_str); - (digest, Paragraph::new(Text::from_iter(iter))) + .map(String::as_str) + .flat_map(|s| wrap_lines(width, s)) + .map(Line::from) + .collect(); + + // Use only the lines that will fit into the box + let index = lines.len().saturating_sub(rect.height as usize - 1); + let lines = lines[index..].to_vec(); + + (lines.len() as u16, Paragraph::new(Text::from(lines))) } } + +fn wrap_lines(width: usize, s: &str) -> impl Iterator> { + let mut chars = s.chars(); + std::iter::from_fn(move || { + let digest: Span = chars.by_ref().take(width).collect::().into(); + (!digest.content.is_empty()).then_some(digest) + }) +} diff --git a/tombstone/src/repl/mod.rs b/tombstone/src/repl/mod.rs index 8387ed8..3d1cf32 100644 --- a/tombstone/src/repl/mod.rs +++ b/tombstone/src/repl/mod.rs @@ -107,7 +107,7 @@ impl Repl { pub fn render(&self, frame: &mut Frame, rect: Rect) { let block = Block::bordered() - .title(" CLI ") + .title(" REPL ") .title_alignment(ratatui::layout::Alignment::Center); let inner_rect = block.inner(rect); let y = inner_rect.y; From 2e1a08ef2557db5d0be4f6465ebe60b7c7222b8a Mon Sep 17 00:00:00 2001 From: TylerBloom Date: Thu, 22 Jan 2026 17:00:02 -0500 Subject: [PATCH 4/7] [tombstone] GUI launches with TUI --- tombstone/Cargo.toml | 4 ++-- tombstone/src/gui.rs | 34 ++++++++++++++++++++++++++++++++++ tombstone/src/main.rs | 3 ++- tombstone/src/state.rs | 26 ++++++++++++++++++++++++-- 4 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 tombstone/src/gui.rs diff --git a/tombstone/Cargo.toml b/tombstone/Cargo.toml index c6c1bea..47c3c42 100644 --- a/tombstone/Cargo.toml +++ b/tombstone/Cargo.toml @@ -11,9 +11,9 @@ ratatui = { version = "=0.29.0", features = ["all-widgets"] } spirit = { path = "../spirit" } tracing = "0.1.40" tracing-subscriber = "0.3.18" -tokio = { version = "1.40.0", features = ["full"] } +tokio = { version = "1.47.1", features = ["full"] } +tokio-stream = { version = "0.1.17", features = ["sync"] } iced = "0.13.1" -tokio-stream = { version = "0.1.16", features = ["sync"] } indexmap = "2.6.0" heapless = "0.9.1" serde = { version = "1.0.228", features = ["derive"] } diff --git a/tombstone/src/gui.rs b/tombstone/src/gui.rs new file mode 100644 index 0000000..2f09e66 --- /dev/null +++ b/tombstone/src/gui.rs @@ -0,0 +1,34 @@ +use iced::widget::Image; +use iced::{Element, widget::image::Handle}; + +pub struct GuiState { + image: Handle, +} + +#[derive(Debug)] +pub enum GuiMessage { + Render(Handle), +} + +impl GuiState { + pub fn new() -> Self { + Self { + image: Handle::from_bytes(Vec::new()), + } + } + pub fn update(&mut self, msg: GuiMessage) { + match msg { + GuiMessage::Render(handle) => self.image = handle, + } + } + + pub fn view(&self) -> Element<'_, GuiMessage> { + Image::new(self.image.clone()).into() + } +} + +impl Default for GuiState { + fn default() -> Self { + Self::new() + } +} diff --git a/tombstone/src/main.rs b/tombstone/src/main.rs index 5f780af..00c9337 100644 --- a/tombstone/src/main.rs +++ b/tombstone/src/main.rs @@ -10,11 +10,12 @@ use crossterm::terminal::enable_raw_mode; use ratatui::Terminal; use ratatui::backend::CrosstermBackend; -pub mod repl; pub mod command; pub mod config; pub mod display_windows; +pub mod gui; pub mod pc_state; +pub mod repl; pub mod state; use command::*; diff --git a/tombstone/src/state.rs b/tombstone/src/state.rs index 84e21a0..3ba5fb0 100644 --- a/tombstone/src/state.rs +++ b/tombstone/src/state.rs @@ -8,6 +8,8 @@ use std::sync::Mutex; use crossterm::execute; use crossterm::terminal::LeaveAlternateScreen; use ghast::emu_core::Emulator; +use ghast::emu_core::create_image; +use iced::Task; use indexmap::IndexSet; use ratatui::Frame; use ratatui::Terminal; @@ -16,6 +18,8 @@ use ratatui::layout::Constraint; use ratatui::layout::Direction; use ratatui::layout::Layout; use ratatui::layout::Rect; +use tokio::sync::mpsc::UnboundedSender; +use tokio_stream::wrappers::UnboundedReceiverStream; use tracing::level_filters::LevelFilter; use tracing_subscriber::fmt::MakeWriter; use tracing_subscriber::fmt::format::FmtSpan; @@ -36,7 +40,6 @@ use crate::RunFor; use crate::RunLength; use crate::RunUntil; use crate::ViewCommand; -use crate::repl::Repl; use crate::command::BreakpointCommand; use crate::config::Config; use crate::config::GameConfig; @@ -45,7 +48,10 @@ use crate::display_windows::render_interrupts; use crate::display_windows::render_mem; use crate::display_windows::render_oam_dma; use crate::display_windows::render_ram; +use crate::gui::GuiMessage; +use crate::gui::GuiState; use crate::pc_state::PcState; +use crate::repl::Repl; /// This is the app's state which holds all of the CLI data. This includes all previous commands /// that were ran and all data to be displayed in the TUI (prompts, inputs, command outputs). @@ -100,7 +106,20 @@ impl AppState { } } - pub fn run(mut self, mut term: Terminal) { + pub fn run(self, term: Terminal) { + let (send, recv) = tokio::sync::mpsc::unbounded_channel(); + let stream = Box::pin(UnboundedReceiverStream::new(recv)); + std::thread::spawn(move || self.run_inner(term, send)); + iced::application("Specte-rs - Tombstone GBC", GuiState::update, GuiState::view) + .run_with(move || (GuiState::new(), Task::stream(stream))) + .unwrap() + } + + pub fn run_inner( + mut self, + mut term: Terminal, + send: UnboundedSender, + ) { self.draw(&mut term); self.cli.draw_input_line(&mut term); loop { @@ -113,6 +132,9 @@ impl AppState { lock.drain(0..); drop(lock); self.draw(&mut term); + let screen = &self.inner.gb.gb().ppu.screen; + let image = create_image(screen); + send.send(GuiMessage::Render(image)).unwrap(); } self.cli.draw_input_line(&mut term); } From 97ec84fc09ad61810e02f6f7f9ef1181e2bce1ce Mon Sep 17 00:00:00 2001 From: TylerBloom Date: Fri, 23 Jan 2026 16:33:28 -0500 Subject: [PATCH 5/7] [tombstone] Formatted --- tombstone/src/gui.rs | 3 ++- tombstone/src/repl/commands.rs | 2 +- tombstone/src/repl/history.rs | 4 +++- tombstone/src/state.rs | 10 +++++++--- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/tombstone/src/gui.rs b/tombstone/src/gui.rs index 2f09e66..cfc37ea 100644 --- a/tombstone/src/gui.rs +++ b/tombstone/src/gui.rs @@ -1,5 +1,6 @@ +use iced::Element; use iced::widget::Image; -use iced::{Element, widget::image::Handle}; +use iced::widget::image::Handle; pub struct GuiState { image: Handle, diff --git a/tombstone/src/repl/commands.rs b/tombstone/src/repl/commands.rs index 575c489..f0602c5 100644 --- a/tombstone/src/repl/commands.rs +++ b/tombstone/src/repl/commands.rs @@ -15,8 +15,8 @@ use ratatui::layout::Position; use ratatui::prelude::Backend; use crate::Command; -use crate::repl::PROMPT; use crate::command::ReplCommand; +use crate::repl::PROMPT; /// Create a thread to poll for user inputs and forward them to the main thread. /// Originally based off of [bottom's implementation](https://github.com/ClementTsang/bottom/blob/master/src/main.rs). diff --git a/tombstone/src/repl/history.rs b/tombstone/src/repl/history.rs index d8a513f..ed973fc 100644 --- a/tombstone/src/repl/history.rs +++ b/tombstone/src/repl/history.rs @@ -1,5 +1,7 @@ use ratatui::layout::Rect; -use ratatui::text::{Line, Span, Text}; +use ratatui::text::Line; +use ratatui::text::Span; +use ratatui::text::Text; use ratatui::widgets::Paragraph; use super::PROMPT; diff --git a/tombstone/src/state.rs b/tombstone/src/state.rs index 3ba5fb0..ca4e435 100644 --- a/tombstone/src/state.rs +++ b/tombstone/src/state.rs @@ -110,9 +110,13 @@ impl AppState { let (send, recv) = tokio::sync::mpsc::unbounded_channel(); let stream = Box::pin(UnboundedReceiverStream::new(recv)); std::thread::spawn(move || self.run_inner(term, send)); - iced::application("Specte-rs - Tombstone GBC", GuiState::update, GuiState::view) - .run_with(move || (GuiState::new(), Task::stream(stream))) - .unwrap() + iced::application( + "Specte-rs - Tombstone GBC", + GuiState::update, + GuiState::view, + ) + .run_with(move || (GuiState::new(), Task::stream(stream))) + .unwrap() } pub fn run_inner( From 25dd59681ed4e4342bc5f1ad5449ff8fb5555cd8 Mon Sep 17 00:00:00 2001 From: TylerBloom Date: Fri, 23 Jan 2026 18:35:48 -0500 Subject: [PATCH 6/7] tmp --- tombstone/src/repl/history.rs | 98 +++++++++++++++++++++++++++++------ tombstone/src/repl/mod.rs | 4 +- 2 files changed, 84 insertions(+), 18 deletions(-) diff --git a/tombstone/src/repl/history.rs b/tombstone/src/repl/history.rs index ed973fc..d040386 100644 --- a/tombstone/src/repl/history.rs +++ b/tombstone/src/repl/history.rs @@ -1,4 +1,7 @@ +use std::borrow::Cow; + use ratatui::layout::Rect; +use ratatui::style; use ratatui::text::Line; use ratatui::text::Span; use ratatui::text::Text; @@ -9,25 +12,24 @@ use super::PROMPT; #[derive(Debug, Default)] pub struct CliHistory { // The history that is displayed (contains dups) - visual_history: Vec, + visual_history: Vec>, // The "up arrow" history (removes dups and is ordered by last use) pub minimized_history: Vec, } impl CliHistory { pub fn push_command_output(&mut self, value: String) { - self.push_visual_history(value); + self.push_visual_history(value.lines().map(ToOwned::to_owned).map(Line::from)); } pub fn push_command(&mut self, value: String) { - self.visual_history.push(format!("{PROMPT}{value}")); + self.visual_history.push(format!("{PROMPT}{value}").into()); self.minimized_history.retain(|val| val != &value); self.minimized_history.push(value); } - pub fn push_visual_history(&mut self, value: String) { - self.visual_history - .extend(value.lines().map(ToOwned::to_owned)); + pub fn push_visual_history(&mut self, lines: impl Iterator>) { + self.visual_history.extend(lines); } pub fn render(&self, rect: Rect) -> (u16, Paragraph<'_>) { @@ -43,12 +45,21 @@ impl CliHistory { // Collect the wrapped lines into in a vec to be trimmed let width = rect.width as usize; - let lines: Vec<_> = entries - .iter() - .map(String::as_str) - .flat_map(|s| wrap_lines(width, s)) - .map(Line::from) - .collect(); + let lines = Vec::new(); + for line in entries { + let Line { + style, + alignment, + spans, + } = line; + let mut curr_width = 0; + let mut buffer = Vec::new(); + for span in spans { + let (start, end) = split_span(width, span); + loop { + } + } + } // Use only the lines that will fit into the box let index = lines.len().saturating_sub(rect.height as usize - 1); @@ -58,10 +69,65 @@ impl CliHistory { } } -fn wrap_lines(width: usize, s: &str) -> impl Iterator> { - let mut chars = s.chars(); +fn wrap_lines(width: usize, line: &Line<'static>) -> impl Iterator> { + let style = line.style; + let alignment = line.alignment; + let mut spans = line.spans.clone().into_iter(); + let mut curr_width = 0; + let mut buffer = Vec::new(); std::iter::from_fn(move || { - let digest: Span = chars.by_ref().take(width).collect::().into(); - (!digest.content.is_empty()).then_some(digest) + loop { + // After the first trim, the remaining span could be longer than width + if curr_width > width { + let span = buffer.pop().unwrap(); + let (start, end) = split_span(width, span); + buffer.push(start); + let line = Line { + style, + alignment, + spans: std::mem::take(&mut buffer), + }; + curr_width = end.width(); + buffer.push(end); + return Some(line); + } + let Some(span) = spans.next() else { + return buffer + .is_empty() + .then(|| std::mem::take(&mut buffer)) + .map(|spans| Line { + style, + alignment, + spans, + }); + }; + if curr_width + span.width() > width { + let (start, end) = split_span(width - curr_width, span); + buffer.push(start); + let line = Line { + style, + alignment, + spans: std::mem::take(&mut buffer), + }; + curr_width = end.width(); + buffer.push(end); + return Some(line); + } + curr_width += span.width(); + buffer.push(span); + } }) } + +fn split_span(width: usize, span: Span) -> (Span, Span) { + let mut chars = span.content.chars(); + let start = Span { + style: span.style, + content: chars.by_ref().take(width).collect(), + }; + let end = Span { + style: span.style, + content: chars.collect(), + }; + (start, end) +} diff --git a/tombstone/src/repl/mod.rs b/tombstone/src/repl/mod.rs index 3d1cf32..0a097b2 100644 --- a/tombstone/src/repl/mod.rs +++ b/tombstone/src/repl/mod.rs @@ -35,7 +35,7 @@ impl Repl { } pub fn display(&mut self, data: String) { - self.history.push_visual_history(data); + self.history.push_visual_history(data.lines().map(ToOwned::to_owned).map(Into::into)); } pub fn push_to_history(&mut self, data: String) { @@ -91,7 +91,7 @@ impl Repl { if !s.is_empty() { self.history.push_command(s); } else { - self.history.push_visual_history(PROMPT.into()); + self.history.push_visual_history(std::iter::once(PROMPT.into())); } self.history_index = 0; match command { From f366690402c3938d1d2d4941b68881cc91f6f7b2 Mon Sep 17 00:00:00 2001 From: TylerBloom Date: Wed, 8 Jul 2026 12:30:25 -0400 Subject: [PATCH 7/7] [tomb] Formatted --- tombstone/src/repl/history.rs | 3 +-- tombstone/src/repl/mod.rs | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tombstone/src/repl/history.rs b/tombstone/src/repl/history.rs index d040386..2e2ef51 100644 --- a/tombstone/src/repl/history.rs +++ b/tombstone/src/repl/history.rs @@ -56,8 +56,7 @@ impl CliHistory { let mut buffer = Vec::new(); for span in spans { let (start, end) = split_span(width, span); - loop { - } + loop {} } } diff --git a/tombstone/src/repl/mod.rs b/tombstone/src/repl/mod.rs index 0a097b2..f70afd4 100644 --- a/tombstone/src/repl/mod.rs +++ b/tombstone/src/repl/mod.rs @@ -35,7 +35,8 @@ impl Repl { } pub fn display(&mut self, data: String) { - self.history.push_visual_history(data.lines().map(ToOwned::to_owned).map(Into::into)); + self.history + .push_visual_history(data.lines().map(ToOwned::to_owned).map(Into::into)); } pub fn push_to_history(&mut self, data: String) { @@ -91,7 +92,8 @@ impl Repl { if !s.is_empty() { self.history.push_command(s); } else { - self.history.push_visual_history(std::iter::once(PROMPT.into())); + self.history + .push_visual_history(std::iter::once(PROMPT.into())); } self.history_index = 0; match command {