Skip to content

Instantly share code, notes, and snippets.

@icub3d
Last active January 29, 2024 02:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save icub3d/cf42f0cc31247e994f57e6737ec3723d to your computer and use it in GitHub Desktop.
Save icub3d/cf42f0cc31247e994f57e6737ec3723d to your computer and use it in GitHub Desktop.
Advent of Code 2019 - Day 07 // Intcode + Ratatui
[package]
authors = ["icub3d"]
name = "day07"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.79"
clap = { version = "4.4.18", features = ["derive"] }
crossterm = "0.27.0"
icub3d_combinatorics = "0.1.1"
ratatui = "0.25.0"
use std::fmt::Display;
use crate::parameter::Parameter;
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Instruction {
Add(Parameter, Parameter, Parameter),
Multiply(Parameter, Parameter, Parameter),
Input(Parameter),
Output(Parameter),
JumpIfTrue(Parameter, Parameter),
JumpIfFalse(Parameter, Parameter),
LessThan(Parameter, Parameter, Parameter),
Equals(Parameter, Parameter, Parameter),
Halt,
}
impl Instruction {
pub fn parameter_count(&self) -> usize {
// Get the number of parameters for a given instruction. This will be used by the tui to
// highlight the parameters.
match self {
Instruction::Add(_, _, _) => 3,
Instruction::Multiply(_, _, _) => 3,
Instruction::Input(_) => 1,
Instruction::Output(_) => 1,
Instruction::JumpIfTrue(_, _) => 2,
Instruction::JumpIfFalse(_, _) => 2,
Instruction::LessThan(_, _, _) => 3,
Instruction::Equals(_, _, _) => 3,
Instruction::Halt => 0,
}
}
pub fn position_parameters(&self) -> Vec<usize> {
// Get the parameters in position mode for a given instruction. This will be used by the tui
// to highlight the memory locations that are being read from or written to.
let mut positions = Vec::new();
macro_rules! add_positions {
($param:ident) => {
if let Parameter::Position(pos) = $param {
positions.push(*pos);
}
};
($param:ident, $($params:ident),+) => {
add_positions! { $param }
add_positions! { $($params),+ }
};
}
match self {
Instruction::Add(left, right, dest) => {
add_positions! { left, right, dest }
}
Instruction::Multiply(left, right, dest) => {
add_positions! { left, right, dest }
}
Instruction::Input(dest) => {
add_positions! { dest }
}
Instruction::Output(value) => {
add_positions! { value }
}
Instruction::JumpIfTrue(value, dest) => {
add_positions! { value, dest }
}
Instruction::JumpIfFalse(value, dest) => {
add_positions! { value, dest }
}
Instruction::LessThan(left, right, dest) => {
add_positions! { left, right, dest }
}
Instruction::Equals(left, right, dest) => {
add_positions! { left, right, dest }
}
Instruction::Halt => {}
}
positions
}
}
impl Display for Instruction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Display an instruction in a human-readable format. This will be used by the tui to
// display the current instruction.
match self {
Instruction::Add(left, right, dest) => {
write!(f, "ADD {} + {} -> {}", left, right, dest)
}
Instruction::Multiply(left, right, dest) => {
write!(f, "MUL {} * {} -> {}", left, right, dest)
}
Instruction::Input(dest) => write!(f, "INP -> {}", dest),
Instruction::Output(value) => write!(f, "OUT -> {}", value),
Instruction::JumpIfTrue(value, dest) => write!(f, "JIT {} -> {}", value, dest),
Instruction::JumpIfFalse(value, dest) => write!(f, "JIF {} -> {}", value, dest),
Instruction::LessThan(left, right, dest) => {
write!(f, "LST {} < {} -> {}", left, right, dest)
}
Instruction::Equals(left, right, dest) => {
write!(f, "EQL {} == {} -> {}", left, right, dest)
}
Instruction::Halt => write!(f, "HLT"),
}
}
}
use std::thread;
use std::{collections::VecDeque, sync::Arc};
use std::sync::{
mpsc::{self, Receiver, Sender, SyncSender},
Mutex,
};
pub struct Channel {
pub queue: Arc<Mutex<VecDeque<isize>>>,
}
impl Channel {
pub fn new() -> (Self, Sender<isize>, Receiver<isize>) {
// We'll use this channel to get messages to put in the queue.
let (input_sender, input_receiver) = mpsc::channel();
// We'll use this channel to notify the send loop that there's a new message in the queue.
let (internal_sender, internal_receiver) = mpsc::channel();
// We'll use this channel to send messages from the queue. Note the use of sync_channel
// instead of channel. This will keep most messages on the queue until the receiver
// is ready to receive them.
let (output_sender, output_receiver) = mpsc::sync_channel(0);
let ch = Self {
queue: Arc::new(Mutex::new(VecDeque::new())),
};
// Start up our two loops in a thread.
let queue = ch.queue.clone();
thread::spawn(move || Channel::recv_loop(queue, input_receiver, internal_sender));
let queue = ch.queue.clone();
thread::spawn(move || Channel::send_loop(queue, internal_receiver, output_sender));
(ch, input_sender, output_receiver)
}
pub fn queue(&self) -> Vec<isize> {
self.queue.lock().unwrap().iter().copied().collect()
}
pub fn recv_loop(
queue: Arc<Mutex<VecDeque<isize>>>,
input: Receiver<isize>,
notifier: Sender<()>,
) {
// Read messages from the input channel and put them on the queue.
while let Ok(value) = input.recv() {
queue.lock().unwrap().push_back(value);
notifier.send(()).unwrap();
}
}
pub fn send_loop(
queue: Arc<Mutex<VecDeque<isize>>>,
notifier: Receiver<()>,
output: SyncSender<isize>,
) {
// Read messages from the notifier channel and send them on the output channel.
while notifier.recv().is_ok() {
let data = queue.lock().unwrap();
let value = *data.front().unwrap();
// We drop here to "free" the lock on the queue. This will allow the receiver to
// continue to add messages to the queue even if something isn't ready to pick it
// up.
drop(data);
if output.send(value).is_err() {
// If the receiver has hung up, we're done here.
break;
}
// Now that we've sent the message, we can remove it from the queue.
queue.lock().unwrap().pop_front();
}
}
}
// our imports
mod parameter;
mod instruction;
mod ipc;
mod process;
use std::{
sync::{mpsc, Arc, Mutex},
thread,
};
use clap::{command, Parser};
use ipc::Channel;
use process::Process;
mod tui;
use tui::App;
// Create a flag so we can run the tui.
#[derive(Parser)]
#[command(author, about, version)]
struct Cli {
#[arg(short, long)]
tui: bool,
}
fn main() {
let input = include_str!("../input");
let args = Cli::parse();
if args.tui {
run_tui(input)
} else {
part1(input);
part2(input)
}
}
fn part1(input: &'static str) {
let mut p1 = 0;
// Iterate over all the permutations for the phase settings.
for permutation in icub3d_combinatorics::Permutation::new(5) {
// For part one, it's a single chain, so we can just create a vector of amplifiers and run
// them in order once done.
let mut amplifiers = Vec::new();
// Use a channel to send outputs from one amplifier to the next.
let (mut sender, mut receiver) = mpsc::channel();
// We want to track the first sender so we can kick off the process.
let first = sender.clone();
// Create an amplifier for each phase setting.
for p in permutation.iter() {
// We want to send the phase setting down the current channel.
sender.send(*p as isize).unwrap();
// Create a new channel to chain to our next amplifier.
let (new_sender, new_receiver) = mpsc::channel();
// Create a new amplifier and push it to our list. It's receiver is the old receiver
// and it's sender is the new sender.
amplifiers.push(Process::new_with_program_and_input(
input,
true,
receiver,
new_sender.clone(),
));
// Update our sender and receiver for the next amplifier.
(sender, receiver) = (new_sender, new_receiver);
}
// We start the process by sending 0 to the first amplifier.
first.send(0).unwrap();
// Run each amplifier.
for mut computer in amplifiers {
computer.run();
}
// The last receiver will have the output signal for this run. Keep track of the maximum.
p1 = p1.max(receiver.recv().unwrap());
}
println!("p1: {}", p1);
}
fn part2(input: &'static str) {
// We'll use this channel to send the maximum value from each permutation.
let (max_send, max_recv) = mpsc::channel();
for permutation in icub3d_combinatorics::Permutation::new(5) {
let (mut sender, mut receiver) = mpsc::channel();
let first = sender.clone();
for (i, p) in permutation.iter().enumerate() {
sender.send(*p as isize + 5).unwrap();
if i == 0 {
sender.send(0).unwrap();
}
let (new_sender, new_receiver) = mpsc::channel();
// If we're on the last amplifier, we want to send the output back to the first.
let new_sender = if i == 4 { first.clone() } else { new_sender };
// Create our cloned values so we can move them into the thread.
let my_sender = new_sender.clone();
let max = max_send.clone();
thread::spawn(move || {
let mut amplifier =
Process::new_with_program_and_input(input, true, receiver, my_sender);
amplifier.run();
// If we are the first amplifier and we've halted, the last value on the queue is
// our value for this permutation.
if i == 0 {
max.send(amplifier.input.recv().unwrap()).unwrap();
}
});
(sender, receiver) = (new_sender, new_receiver);
}
}
// We drop here because recv will block until the other end is closed. The threads above will
// finish and close their part, but we need to close the original that is no longer being used.
drop(max_send);
// Receive all the values and find the maximum.
let mut max = 0;
for value in max_recv.iter() {
max = max.max(value);
}
println!("p2: {}", max);
}
fn run_tui(input: &'static str) {
// Let's just test with a single permutation for now.
let permutation = [0, 1, 2, 3, 4];
let (channel, mut sender, mut receiver) = Channel::new();
// We want to track all the channels and amplifiers for the tui. Additionally, we want to have
// notifiers so the tui can tell the threads when to perform another step.
let mut channels = vec![channel];
let mut amplifiers = Vec::new();
let mut notifiers = Vec::new();
for (i, p) in permutation.iter().enumerate() {
sender.send(*p).unwrap();
if i == 0 {
sender.send(0).unwrap();
}
let (channel, new_sender, new_receiver) = Channel::new();
channels.push(channel);
let amplifier = Arc::new(Mutex::new(Process::new_with_program_and_input(
input,
false, // We don't want to block on input.
receiver,
new_sender.clone(),
)));
amplifiers.push(amplifier.clone());
// Create a notifier that the thread will listen on so it knows when to perform another
// step.
let (notifier, recv) = mpsc::channel::<()>();
notifiers.push(notifier);
thread::spawn(move || {
// We'll loop until the receiver is closed.
while recv.recv().is_ok() {
let mut amplifier = amplifier.lock().unwrap();
if amplifier.state().halted {
break;
}
amplifier.step();
}
});
(sender, receiver) = (new_sender, new_receiver);
}
// Now that we have all the amplifiers up, let's run it.
let mut app = App::new(amplifiers, channels, notifiers);
let _ = app.run();
}
use std::fmt::Display;
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Parameter {
Position(usize),
Immediate(isize),
}
impl Display for Parameter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Parameter::Position(pos) => write!(f, "P[{}]", pos),
Parameter::Immediate(value) => write!(f, "I[{}]", value),
}
}
}
impl Parameter {
pub fn new(opcode: isize, position: isize, value: isize) -> Self {
let mode = (opcode / 10_isize.pow(position as u32 + 1)) % 10;
match mode {
0 => Self::Position(value as usize),
1 => Self::Immediate(value),
_ => panic!("Invalid parameter mode"),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_parameter_new() {
// Does the math check out?
assert_eq!(Parameter::new(1002, 1, 4), Parameter::Position(4));
assert_eq!(Parameter::new(1002, 2, 3), Parameter::Immediate(3));
assert_eq!(Parameter::new(1002, 3, 2), Parameter::Position(2));
}
}
use std::sync::mpsc::{Receiver, Sender};
use crate::instruction::Instruction;
use crate::parameter::Parameter;
// Separate out the state of the process so we can easily pass it to the tui.
#[derive(Debug, Clone)]
pub struct State {
pub memory: Vec<isize>,
pub instruction_pointer: usize,
pub last_output: Option<isize>,
pub last_input: Option<isize>,
pub halted: bool,
}
impl State {
pub fn new(memory: Vec<isize>) -> Self {
Self {
memory,
instruction_pointer: 0,
last_output: None,
last_input: None,
halted: false,
}
}
pub fn len(&self) -> usize {
self.memory.len()
}
pub fn next_instruction(&self) -> Option<(Instruction, usize)> {
// Get the next instruction that should be executed and it's size. This is basically
// what we had done in the previous days but we've abstracted it so both the normal and tui
// versions can use it.
if self.instruction_pointer >= self.len() || self.halted {
return None;
}
let opcode = self[self.instruction_pointer];
let op = opcode % 100;
macro_rules! param {
($instruction:expr, 3) => {
$instruction(
Parameter::new(opcode, 1, self[self.instruction_pointer + 1]),
Parameter::new(opcode, 2, self[self.instruction_pointer + 2]),
Parameter::new(opcode, 3, self[self.instruction_pointer + 3]),
)
};
($instruction:expr, 2) => {
$instruction(
Parameter::new(opcode, 1, self[self.instruction_pointer + 1]),
Parameter::new(opcode, 2, self[self.instruction_pointer + 2]),
)
};
($instruction:expr, 1) => {
$instruction(Parameter::new(
opcode,
1,
self[self.instruction_pointer + 1],
))
};
($instruction:expr) => {
$instruction
};
}
let instruction = match op {
1 => param!(Instruction::Add, 3),
2 => param!(Instruction::Multiply, 3),
3 => param!(Instruction::Input, 1),
4 => param!(Instruction::Output, 1),
5 => param!(Instruction::JumpIfTrue, 2),
6 => param!(Instruction::JumpIfFalse, 2),
7 => param!(Instruction::LessThan, 3),
8 => param!(Instruction::Equals, 3),
99 => Instruction::Halt,
_ => panic!("invalid opcode"),
};
Some((instruction, instruction.parameter_count() + 1))
}
}
impl std::ops::Index<usize> for State {
type Output = isize;
fn index(&self, index: usize) -> &Self::Output {
&self.memory[index]
}
}
impl std::ops::IndexMut<usize> for State {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.memory[index]
}
}
pub struct Process {
state: State,
block_on_input: bool,
pub input: Receiver<isize>,
output: Sender<isize>,
}
impl Process {
// We'll use this to get the state of the process for the tui.
pub fn state(&self) -> State {
self.state.clone()
}
pub fn new_with_program_and_input(
program: &str,
block_on_input: bool,
input: Receiver<isize>,
output: Sender<isize>,
) -> Self {
Self {
state: State::new(
program
.trim()
.split(',')
.map(|s| s.parse::<isize>().unwrap())
.collect::<Vec<_>>(),
),
block_on_input,
input,
output,
}
}
pub fn run(&mut self) {
// Run the process until it halts.
while !self.state().halted {
self.step();
}
}
pub fn step(&mut self) {
// Run a single step of the process. This will execute the next instruction and update the
// instruction pointer if the instruction was executed successfully.
if let Some((instruction, instruction_size)) = self.state.next_instruction() {
if self.evaluate_instruction(instruction) {
self.state.instruction_pointer += instruction_size;
}
}
}
pub fn evaluate_instruction(&mut self, instruction: Instruction) -> bool {
// This is largely the same as previous days but we now return a bool to indicate if the
// instruction pointer should be updated. We'll return false if we are in non-blocking mode
// or if we jumped and don't want to update the instruction pointer.
if self.state.halted {
return false;
}
macro_rules! eval {
(write $dest:ident) => {
let $dest = match $dest {
Parameter::Position(pos) => pos,
Parameter::Immediate(_) => panic!("invalid write parameter"),
};
};
($param:ident) => {
let $param = match $param {
Parameter::Position(pos) => self.state[pos],
Parameter::Immediate(value) => value,
};
};
($param:ident, $($params:ident),+) => {
eval! { $param }
eval! { $($params),+ }
};
(write $dest:ident, $($params:ident),+) => {
eval! { write $dest }
eval! { $($params),+ }
};
}
match instruction {
Instruction::Add(left, right, dest) => {
eval! { write dest, left, right };
self.state[dest] = left + right;
}
Instruction::Multiply(left, right, dest) => {
eval! { write dest, left, right };
self.state[dest] = left * right;
}
Instruction::LessThan(left, right, dest) => {
eval! { write dest, left, right };
self.state[dest] = match left < right {
true => 1,
false => 0,
}
}
Instruction::Input(dest) => {
eval! { write dest };
if !self.block_on_input {
// We use try_recv here so we can return false if there's no input available.
match self.input.try_recv() {
Ok(value) => self.state[dest] = value,
Err(_) => return false,
}
} else {
self.state[dest] = match self.input.recv() {
Ok(value) => value,
Err(_) => return false,
};
}
self.state.last_input = Some(self.state[dest]);
}
Instruction::Output(value) => {
eval! { value };
self.state.last_output = Some(value);
self.output.send(value).unwrap();
}
Instruction::JumpIfTrue(value, dest) => {
eval! { value, dest };
if value != 0 {
self.state.instruction_pointer = dest as usize;
// We don't want to update the instruction pointer.
return false;
}
}
Instruction::JumpIfFalse(value, dest) => {
eval! { value, dest };
if value == 0 {
self.state.instruction_pointer = dest as usize;
// We don't want to update the instruction pointer.
return false;
}
}
Instruction::Equals(left, right, dest) => {
eval! { write dest, left, right };
self.state[dest] = match left == right {
true => 1,
false => 0,
}
}
Instruction::Halt => {
self.state.halted = true;
}
};
true
}
}
use std::sync::{mpsc::Sender, Arc, Mutex};
use std::{io::stdout, str::FromStr, time::Duration};
use crate::process::Process;
use crate::{instruction::Instruction, ipc::Channel};
use anyhow::Result;
use crossterm::{
event::{
poll, read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent,
KeyEventKind, MouseEvent, MouseEventKind,
},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Style, Stylize},
text::{Span, Text},
widgets::{
block::Title, Block, BorderType, Borders, Cell, List, Paragraph, Row, Scrollbar,
ScrollbarOrientation, ScrollbarState, Table, TableState, Tabs,
},
Frame, Terminal,
};
#[allow(dead_code)]
enum Monokai {
DarkBlack,
LightBlack,
Background,
DarkerGrey,
DarkGrey,
Grey,
LightGrey,
LighterGrey,
White,
Blue,
Green,
Violet,
Orange,
Red,
Yellow,
}
impl From<Monokai> for Color {
fn from(color: Monokai) -> Self {
match color {
Monokai::DarkBlack => Color::from_str("#19181a").unwrap(),
Monokai::LightBlack => Color::from_str("#221f22").unwrap(),
Monokai::Background => Color::from_str("#2d2a2e").unwrap(),
Monokai::DarkerGrey => Color::from_str("#403e41").unwrap(),
Monokai::DarkGrey => Color::from_str("#5b595c").unwrap(),
Monokai::Grey => Color::from_str("#727072").unwrap(),
Monokai::LightGrey => Color::from_str("#939293").unwrap(),
Monokai::LighterGrey => Color::from_str("#c1c0c0").unwrap(),
Monokai::White => Color::from_str("#fcfcfa").unwrap(),
Monokai::Blue => Color::from_str("#78dce8").unwrap(),
Monokai::Green => Color::from_str("#a9dc76").unwrap(),
Monokai::Violet => Color::from_str("#ab9df2").unwrap(),
Monokai::Orange => Color::from_str("#fc9867").unwrap(),
Monokai::Red => Color::from_str("#ff6188").unwrap(),
Monokai::Yellow => Color::from_str("#ffd866").unwrap(),
}
}
}
pub struct App {
processes: Vec<Arc<Mutex<Process>>>,
channels: Vec<Channel>,
notifiers: Vec<Sender<()>>,
process: usize,
table_states: Vec<TableState>,
scroll_states: Vec<ScrollbarState>,
}
impl App {
pub fn new(
processes: Vec<Arc<Mutex<Process>>>,
channels: Vec<Channel>,
notifiers: Vec<Sender<()>>,
) -> Self {
assert!(!processes.is_empty());
let count = processes.len();
let scroll_states = processes
.iter()
.map(|p| ScrollbarState::new(p.lock().unwrap().state().len() / 8))
.collect::<Vec<_>>();
Self {
processes,
channels,
notifiers,
process: 0,
table_states: vec![TableState::default(); count],
scroll_states,
}
}
pub fn run(&mut self) -> Result<()> {
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
enable_raw_mode()?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout))?;
loop {
terminal.draw(|frame| self.draw(frame))?;
if poll(Duration::from_millis(10))? && self.handle_event()? {
break;
}
}
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
Ok(())
}
fn draw(&mut self, frame: &mut Frame) {
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(1),
Constraint::Length(3),
Constraint::Min(1),
Constraint::Length(1),
]
.as_ref(),
)
.split(frame.size());
let cols = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(50), Constraint::Max(30)].as_ref())
.split(rows[2]);
let sidebar = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(3), Constraint::Max(8), Constraint::Max(10)].as_ref())
.split(cols[1]);
self.draw_header(frame, rows[0]);
self.draw_tabs(frame, rows[1]);
self.draw_state(frame, sidebar[0]);
self.draw_channels(frame, sidebar[1]);
self.draw_talking_head(frame, sidebar[2]);
self.draw_memory(frame, cols[0]);
self.draw_help(frame, rows[3]);
}
fn draw_channels(&self, frame: &mut Frame, chunk: Rect) {
let block = Block::default()
.title(Title::from("Channels").alignment(Alignment::Center))
.borders(Borders::ALL)
.border_style(Style::default().fg(Monokai::Violet.into()))
.border_type(BorderType::Rounded)
.style(
Style::default()
.fg(Monokai::White.into())
.bg(Monokai::Background.into()),
);
let channels: Vec<_> = self
.channels
.iter()
.enumerate()
.map(|(i, channel)| {
let mut style = Style::default().fg(Monokai::LightGrey.into());
if i == self.process {
style = style.fg(Monokai::White.into());
}
Span::from(format!("{i}: {:?}", channel.queue())).style(style)
})
.collect();
let list = List::new(channels).block(block);
frame.render_widget(list, chunk);
}
fn draw_tabs(&self, frame: &mut Frame, chunk: Rect) {
let block = Block::default()
.title(Title::from("Processes").alignment(Alignment::Center))
.borders(Borders::ALL)
.border_style(Style::default().fg(Monokai::Yellow.into()))
.border_type(BorderType::Rounded)
.style(
Style::default()
.fg(Monokai::White.into())
.bg(Monokai::Background.into()),
);
let tabs = self
.processes
.iter()
.enumerate()
.map(|(i, process)| {
let mut style = Style::default().bg(Monokai::Grey.into());
if process.lock().unwrap().state().halted {
style = style.fg(Monokai::Red.into());
} else if i == self.process {
style = style.fg(Monokai::White.into());
}
Span::from(format!("< {} >", i)).style(style)
})
.collect();
let tabs = Tabs::new(tabs)
.select(self.process)
.block(block)
.style(Style::default().fg(Monokai::DarkerGrey.into()))
.highlight_style(Style::default().bg(Monokai::Green.into()));
frame.render_widget(tabs, chunk);
}
fn draw_talking_head(&self, frame: &mut Frame, chunk: Rect) {
let block = Block::default()
.title(Title::from("Talking Head").alignment(Alignment::Center))
.borders(Borders::ALL)
.border_style(Style::default().fg(Monokai::Blue.into()))
.border_type(BorderType::Rounded)
.style(
Style::default()
.fg(Monokai::White.into())
.bg(Monokai::Background.into()),
);
frame.render_widget(block, chunk);
}
fn draw_state(&self, frame: &mut Frame, chunk: Rect) {
let state_block = Block::default()
.title(Title::from("State").alignment(Alignment::Center))
.borders(Borders::ALL)
.border_style(Style::default().fg(Monokai::Red.into()))
.border_type(BorderType::Rounded)
.style(
Style::default()
.fg(Monokai::White.into())
.bg(Monokai::Background.into()),
);
let state = self.processes[self.process].lock().unwrap().state();
let instruction = match state.next_instruction() {
Some((instruction, _)) => instruction,
None => Instruction::Halt,
};
let states = vec![
format!("IP: {:?}", state.instruction_pointer),
format!("Halted: {:?}", state.halted),
format!("Last Input: {:?}", state.last_input),
format!("Last Output: {:?}", state.last_output),
format!(""),
format!("{}", instruction),
];
let items = states.iter().map(Text::raw);
let list = List::new(items).block(state_block);
frame.render_widget(list, chunk);
}
fn draw_memory(&mut self, frame: &mut Frame, chunk: Rect) {
let block = Block::default()
.title(Title::from("Memory").alignment(Alignment::Center))
.borders(Borders::ALL)
.border_style(Style::default().fg(Monokai::Orange.into()))
.border_type(BorderType::Rounded)
.style(
Style::default()
.fg(Monokai::White.into())
.bg(Monokai::Background.into()),
);
let state = self.processes[self.process].lock().unwrap().state();
let (instruction, positions) = match state.next_instruction() {
Some((instruction, _)) => (instruction, instruction.position_parameters()),
None => (Instruction::Halt, Vec::new()),
};
let mut params_left = 0;
let chunks: Vec<_> = state
.memory
.chunks(8)
.enumerate()
.map(|(i, chunk)| {
let mut row = vec![Cell::from(format!("{:04}", i * 8))
.style(Style::default().bold().bg(Monokai::DarkerGrey.into()))];
for (j, v) in chunk.iter().enumerate() {
let mut style = Style::default().bg(Monokai::Background.into());
if state.instruction_pointer == i * 8 + j {
style = style.bg(Monokai::Green.into());
params_left = instruction.parameter_count();
} else if params_left > 0 {
style = style.bg(Monokai::Red.into());
params_left -= 1;
} else if positions.contains(&(i * 8 + j)) {
style = style.bg(Monokai::Blue.into());
}
row.push(Cell::from(format!("{}", v)).style(style));
}
Row::new(row)
})
.collect();
let widths = [Constraint::Length(10); 9];
let table = Table::new(chunks, widths)
.block(block)
.header(
Row::new(vec![
Cell::from("Location"),
Cell::from("+0"),
Cell::from("+1"),
Cell::from("+2"),
Cell::from("+3"),
Cell::from("+4"),
Cell::from("+5"),
Cell::from("+6"),
Cell::from("+7"),
])
.style(Style::default().bg(Monokai::DarkerGrey.into())),
)
.column_spacing(0);
frame.render_stateful_widget(table, chunk, &mut self.table_states[self.process]);
frame.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalRight)
.begin_symbol(None)
.end_symbol(None),
chunk.inner(&ratatui::layout::Margin {
horizontal: 1,
vertical: 1,
}),
&mut self.scroll_states[self.process],
);
}
fn draw_help(&self, frame: &mut Frame, chunk: Rect) {
let block = Block::default().style(
Style::default()
.fg(Monokai::Background.into())
.bg(Monokai::Green.into()),
);
let status = Paragraph::new("(q)uit | (s)tep | (0-4) select process")
.block(block)
.alignment(Alignment::Left);
frame.render_widget(status, chunk);
}
fn draw_header(&self, frame: &mut Frame, chunk: Rect) {
let title_block = Block::default().style(
Style::default()
.fg(Monokai::Background.into())
.bg(Monokai::Violet.into()),
);
let title = Paragraph::new("INTCODE COMPUTER")
.block(title_block)
.alignment(Alignment::Center);
frame.render_widget(title, chunk);
}
fn handle_event(&mut self) -> Result<bool> {
match read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => self.handle_key(key),
Event::Mouse(mouse) => self.handle_mouse(mouse),
_ => Ok(false),
}
}
fn handle_mouse(&mut self, mouse: MouseEvent) -> Result<bool> {
let table_state = &mut self.table_states[self.process];
let state = self.processes[self.process].lock().unwrap().state();
match mouse.kind {
MouseEventKind::ScrollUp => {
self.scroll_states[self.process].prev();
if table_state.offset() > 0 {
// The table state expects a value to be selected, so we need to select the
// previous row. They unwrap_or(0), might be a good feature/bug to fix.
table_state.select(Some(table_state.offset() - 1));
*table_state.offset_mut() -= 1;
}
}
MouseEventKind::ScrollDown => {
self.scroll_states[self.process].next();
if table_state.offset() < state.len() - 1 {
table_state.select(Some(table_state.offset() + 1));
*table_state.offset_mut() += 1;
}
}
_ => {}
}
Ok(false)
}
fn handle_key(&mut self, key: KeyEvent) -> Result<bool> {
match key.code {
KeyCode::Char('q') => Ok(true),
KeyCode::Char('s') => {
if !self.processes[self.process].lock().unwrap().state().halted {
self.notifiers[self.process].send(())?;
}
Ok(false)
}
KeyCode::Char(c) => {
if let Some(i) = c.to_digit(10) {
let i = i as usize;
if i < self.processes.len() {
self.process = i;
}
}
Ok(false)
}
_ => Ok(false),
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment