diff --git a/Cargo.toml b/Cargo.toml index b2ee1a9..87d5ee2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,6 @@ edition = "2021" [dependencies] clap = { version = "4.3", features = ["derive"] } anyhow = "1" -ratatui = "0.26" +ratatui = "0.29" crossterm = "*" elf = "0.7" \ No newline at end of file diff --git a/src/app/mod.rs b/src/app/mod.rs index b14c7fe..29f8960 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,3 +1,3 @@ pub mod headless; +mod render_tui; pub mod tui; -mod ui; diff --git a/src/app/ui.rs b/src/app/render_tui.rs similarity index 82% rename from src/app/ui.rs rename to src/app/render_tui.rs index 5cb1a38..9148550 100644 --- a/src/app/ui.rs +++ b/src/app/render_tui.rs @@ -3,12 +3,13 @@ use crate::cpu::AddrBus; use crate::cpu::Register; use crate::cpu::CPU; use crate::instructions::Instruction; +use crate::utils::UserInputManager; use anyhow::Error; use crossterm::event::KeyEvent; use crossterm::event::MouseEvent; use ratatui::layout::Margin; +use ratatui::layout::Position; use std::fmt::Write; -use std::sync::mpsc; use crossterm::event::KeyCode; @@ -23,7 +24,7 @@ use super::tui::Job; pub struct ViewState { pub uart: String, - user_input: String, + user_input_manager: UserInputManager, auto_step: bool, show_help: bool, insert_mode: bool, @@ -33,7 +34,7 @@ impl ViewState { pub fn new() -> Self { ViewState { uart: String::new(), - user_input: String::new(), + user_input_manager: UserInputManager::new(), auto_step: false, show_help: true, insert_mode: false, @@ -43,15 +44,27 @@ impl ViewState { pub fn on_key(&mut self, key: KeyEvent) -> Job { if self.insert_mode { match key.code { - KeyCode::Char(ch) => self.user_input.push(ch), + KeyCode::Left => { + self.user_input_manager.move_cursor_left(); + } + KeyCode::Right => { + self.user_input_manager.move_cursor_right(); + } + KeyCode::Up => { + self.user_input_manager.set_to_previous_input(); + } + KeyCode::Down => { + self.user_input_manager.set_to_next_input(); + } + KeyCode::Char(to_insert) => self.user_input_manager.insert_char(to_insert), KeyCode::Backspace => { - _ = self.user_input.pop(); + self.user_input_manager.remove_char(); } KeyCode::Enter => { - self.user_input.push('\n'); - let job = Job::ReadUart(self.user_input.clone()); - self.user_input.clear(); - return job; + if let Some(input) = self.user_input_manager.finish_current_input() { + let job = Job::ReadUart(input); + return job; + } } KeyCode::Esc => { self.insert_mode = false; @@ -190,7 +203,7 @@ impl ViewState { let register_chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(register_block.inner(&Margin { + .split(register_block.inner(Margin { horizontal: 1, vertical: 1, })); @@ -201,17 +214,22 @@ impl ViewState { frame.render_widget(register_file_table, register_block); } - fn render_io(&mut self, io_block: Rect, uart_rx: &mpsc::Receiver, frame: &mut Frame) { + fn render_io(&mut self, io_block: Rect, frame: &mut Frame) { let right_block_down = Block::bordered() - .title(vec![Span::from("I/O")]) + .title(vec![Span::from("UART0 TX")]) .title_alignment(Alignment::Left); - while let Ok(msg) = uart_rx.try_recv() { - self.uart.push(msg as char); - } let text: &str = &self.uart; let text = Text::from(text); - let paragraph = Paragraph::new(text).block(right_block_down); + let text_height = u16::try_from(text.height()).unwrap_or(u16::MAX); + let scroll = if text_height >= io_block.height + 2 { + text_height - io_block.height + 2 + } else { + 0 + }; + let paragraph = Paragraph::new(text) + .block(right_block_down) + .scroll((scroll, 0)); frame.render_widget(paragraph, io_block); } @@ -219,20 +237,27 @@ impl ViewState { let right_block_bottom = { if self.insert_mode { Block::bordered() - .title(vec![Span::from("User Input to UART RX [Insert Mode]")]) + .title(vec![Span::from( + "User Input to UART0 RX [Insert Mode, press `esc` to leave]", + )]) .title_alignment(Alignment::Left) } else { Block::bordered() .title(vec![Span::from( - "User Input to UART RX [Not in insert mode, press `i`]", + "User Input to UART0 RX [Not in insert mode, press `i`]", )]) .title_alignment(Alignment::Left) } }; - let text: &str = &self.user_input; + let text: &str = &self.user_input_manager.user_input; let text = Text::from(text); let paragraph = Paragraph::new(text).block(right_block_bottom); + + let x_pos = + input_block.x + 1 + u16::try_from(self.user_input_manager.cursor_position).unwrap_or(0); + + frame.set_cursor_position(Position::new(x_pos, input_block.y + 1)); frame.render_widget(paragraph, input_block); } @@ -253,13 +278,13 @@ impl ViewState { frame.render_widget(paragraph, current_block); } - pub fn ui(&mut self, f: &mut Frame, cpu: &CPU, uart_rx: &mpsc::Receiver) { - let size = f.size(); + pub fn ui(&mut self, f: &mut Frame, cpu: &CPU) { + let area = f.area(); let chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(25), Constraint::Percentage(75)].as_ref()) - .split(size); + .split(area); let left_chunks = Layout::default() .direction(Direction::Vertical) @@ -302,7 +327,7 @@ impl ViewState { f.render_widget(paragraph, next_block); ViewState::render_registers(register_block, &cpu.register, f); - self.render_io(io_block, uart_rx, f); + self.render_io(io_block, f); self.render_input(input_block, f); if self.show_help { @@ -311,9 +336,9 @@ impl ViewState { "Key shortcuts:\n'a' to enable auto-step\n'h' for help\n's' to step one instruction\n'q' to quit\n'i' to enter insert mode\n 'ENTER' to send your input to the uart\n 'ESC' to leave the insert mode", ) .block(block); - let area = centered_rect(60, 29, size); - f.render_widget(Clear, area); - f.render_widget(help_message, area); + let popup_area = centered_rect(60, 29, area); + f.render_widget(Clear, popup_area); + f.render_widget(help_message, popup_area); } } } diff --git a/src/app/tui.rs b/src/app/tui.rs index 25a9010..1d870b4 100644 --- a/src/app/tui.rs +++ b/src/app/tui.rs @@ -15,7 +15,7 @@ use crossterm::{ }; use ratatui::{prelude::CrosstermBackend, Terminal}; -use super::ui::ViewState; +use super::render_tui::ViewState; use crate::cli; use crate::cpu::{create_cpu_thread, AddrBus, CPU}; use crate::events::{CpuJob, Event}; @@ -72,7 +72,7 @@ fn event_loop_tui( { let cpu = cpu.lock().unwrap(); - terminal.draw(|f| input_app.ui(f, &cpu, uart_rx))?; + terminal.draw(|f| input_app.ui(f, &cpu))?; } loop { @@ -117,9 +117,17 @@ fn event_loop_tui( } } + while let Ok(msg) = uart_rx.try_recv() { + let msg = msg as char; + // Filter out, as it would mess with ratatui + if msg != '\r' && msg != '\t' { + input_app.uart.push(msg); + } + } + { let cpu = cpu.lock().unwrap(); - terminal.draw(|f| input_app.ui(f, &cpu, uart_rx))?; + terminal.draw(|f| input_app.ui(f, &cpu))?; } } diff --git a/src/cpu/memory.rs b/src/cpu/memory.rs index bc9ac56..e6f21fc 100644 --- a/src/cpu/memory.rs +++ b/src/cpu/memory.rs @@ -1,6 +1,4 @@ -//! This file is scoped around the `Memory` struct. -//! If something can not be `impl Memory` it is considered out of scope. - +/// This trait is the default interface for the CPU execution to the rest of the system. pub trait AddrBus { fn set_reservation(&mut self, addr: usize, value: u32); diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 5fec2ec..6aac70e 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -3,9 +3,12 @@ use std::sync::mpsc; mod map_to_unixsocket; mod peekable_channel; mod peekable_reader; +mod user_input_manager; pub use map_to_unixsocket::map_to_unixsocket; pub use peekable_channel::PeekableChannel; use peekable_reader::PeekableReader; +pub use user_input_manager::UserInputManager; + pub type IOChannel = (mpsc::Sender, mpsc::Receiver); diff --git a/src/utils/user_input_manager.rs b/src/utils/user_input_manager.rs new file mode 100644 index 0000000..b0fb065 --- /dev/null +++ b/src/utils/user_input_manager.rs @@ -0,0 +1,103 @@ +// This object is copied from the Jelly project, maybe this can be its own crate? + +// Handle the input textbox in the TUI, e.g. backspace, arrow keys, ... +pub struct UserInputManager { + pub user_input: String, + user_input_history: Vec, + user_input_history_index: usize, + pub cursor_position: usize, +} + +impl UserInputManager { + pub fn new() -> Self { + Self { + user_input: String::new(), + user_input_history: vec![], + user_input_history_index: 0, + cursor_position: 0, + } + } + + pub fn _insert_string(&mut self, string: &str) { + self.user_input.push_str(string); + self.cursor_position += string.len(); + } + + pub fn insert_char(&mut self, chr: char) { + self.user_input.insert(self.cursor_position, chr); + self.cursor_position += 1; + } + + pub fn remove_char(&mut self) { + if self.cursor_position > 0 && self.cursor_position <= self.user_input.len() { + self.cursor_position = self.cursor_position.saturating_sub(1); + self.user_input.remove(self.cursor_position); + } + } + + pub const fn move_cursor_left(&mut self) { + self.cursor_position = self.cursor_position.saturating_sub(1); + } + + pub const fn move_cursor_right(&mut self) { + if self.cursor_position < self.user_input.len() { + self.cursor_position += 1; + } + } + + pub fn set_to_previous_input(&mut self) { + if self.user_input_history_index > 0 { + self.user_input_history_index -= 1; + self.user_input = self.user_input_history[self.user_input_history_index].clone(); + self.cursor_position = self.user_input.len(); + } + } + + pub fn set_to_next_input(&mut self) { + if self.user_input_history_index < self.user_input_history.len() { + self.user_input_history_index += 1; + if self.user_input_history_index == self.user_input_history.len() { + self.user_input.clear(); + self.cursor_position = 0; + } else { + self.user_input = self.user_input_history[self.user_input_history_index].clone(); + self.cursor_position = self.user_input.len(); + } + } + } + + pub fn finish_current_input(&mut self) -> Option { + let result; + // We don't want to store empty inputs + if self.user_input.is_empty() { + result = Some("\n".to_owned()); + } else { + // nor the same command multiple times + let last_input_equals_current = self + .user_input_history + .last() + .is_some_and(|input| *input == self.user_input); + if !last_input_equals_current { + self.user_input_history + .push(self.user_input.clone().trim_end().to_owned()); + } + + if !self.user_input.ends_with('\n') { + self.user_input.push('\n'); + } + + result = Some(self.user_input.clone()); + self.user_input.clear(); + self.cursor_position = 0; + } + // This has to be done even if the input is empty, as the user might have scrolled back + // and deleted all input. + self.user_input_history_index = self.user_input_history.len(); + + result + } + + pub const fn _input_empty(&self) -> bool { + self.user_input.is_empty() + } +}