Last active
January 29, 2024 02:22
-
-
Save icub3d/cf42f0cc31247e994f57e6737ec3723d to your computer and use it in GitHub Desktop.
Advent of Code 2019 - Day 07 // Intcode + Ratatui
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"), | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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