Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion src/app/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
pub mod headless;
mod render_tui;
pub mod tui;
mod ui;
77 changes: 51 additions & 26 deletions src/app/ui.rs → src/app/render_tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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,
}));
Expand All @@ -201,38 +214,50 @@ impl ViewState {
frame.render_widget(register_file_table, register_block);
}

fn render_io(&mut self, io_block: Rect, uart_rx: &mpsc::Receiver<u8>, 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);
}

fn render_input(&self, input_block: Rect, frame: &mut Frame) {
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);
}

Expand All @@ -253,13 +278,13 @@ impl ViewState {
frame.render_widget(paragraph, current_block);
}

pub fn ui<T: AddrBus>(&mut self, f: &mut Frame, cpu: &CPU<T>, uart_rx: &mpsc::Receiver<u8>) {
let size = f.size();
pub fn ui<T: AddrBus>(&mut self, f: &mut Frame, cpu: &CPU<T>) {
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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
}
}
}
Expand Down
14 changes: 11 additions & 3 deletions src/app/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -72,7 +72,7 @@ fn event_loop_tui<T: AddrBus>(

{
let cpu = cpu.lock().unwrap();
terminal.draw(|f| input_app.ui(f, &cpu, uart_rx))?;
terminal.draw(|f| input_app.ui(f, &cpu))?;
}

loop {
Expand Down Expand Up @@ -117,9 +117,17 @@ fn event_loop_tui<T: AddrBus>(
}
}

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))?;
}
}

Expand Down
4 changes: 1 addition & 3 deletions src/cpu/memory.rs
Original file line number Diff line number Diff line change
@@ -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);

Expand Down
3 changes: 3 additions & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>, mpsc::Receiver<u8>);
103 changes: 103 additions & 0 deletions src/utils/user_input_manager.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
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<String> {
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()
}
}