- added run_sync to TraceableProcess

refactored assembler to use the run_sync method defined by the TraceableProcess trait for standard running.
- refactored assembler logging to use the ProcessAgent
This commit is contained in:
2026-07-29 22:03:24 +01:00
parent 5167c465aa
commit 17b80c1246
12 changed files with 178 additions and 337 deletions
+4 -8
View File
@@ -1,22 +1,18 @@
use common::prelude::*; use common::prelude::*;
use common::process::ProcessAgent;
use crate::assembler::Token; use crate::assembler::Token;
use crate::assembler::model::{Node, Opcode}; use crate::assembler::model::{Node, Opcode};
use crate::{assembler::AssembleError, expect_token}; use crate::{assembler::AssembleError, expect_token};
fn log(message: &str) { pub fn codegen(nodes: Vec<Node>, ctx: &ProcessAgent) -> Result<Vec<Instruction>, AssembleError> {
println!("\x1b[32mINFO:\x1b[0m {message}");
}
pub fn codegen(nodes: Vec<Node>) -> Result<Vec<Instruction>, AssembleError> {
let mut instructions = vec![]; let mut instructions = vec![];
for node in nodes { for node in nodes {
println!("{:?}", node); ctx.log(format!("{}", node));
instructions.push(build_instruction(&node)?); instructions.push(build_instruction(&node)?);
} }
log("Assembly Successful ✅"); ctx.log("Assembly Successful ✅");
Ok(instructions) Ok(instructions)
} }
-2
View File
@@ -65,8 +65,6 @@ pub fn lexer(mut program: String, module: u64) -> Result<Vec<Token>, AssembleErr
} }
} }
// println!("{:#?}", tokens);
Ok(tokens) Ok(tokens)
} }
pub fn parse_register(token: &str) -> Result<Option<Token>, AssembleError> { pub fn parse_register(token: &str) -> Result<Option<Token>, AssembleError> {
+26 -138
View File
@@ -50,15 +50,6 @@ pub struct Assembler {
is_running: bool, is_running: bool,
} }
impl TraceableProcess for Assembler {
type Output = DseExecutable;
type Error = AssembleError;
fn run(mut self: Box<Self>, ctx: ProcessAgent) -> Result<Self::Output, Self::Error> {
assemble(&self.src_path)
}
}
impl Assembler { impl Assembler {
#[must_use] #[must_use]
pub fn new(src_path: impl Into<PathBuf>) -> Self { pub fn new(src_path: impl Into<PathBuf>) -> Self {
@@ -70,137 +61,37 @@ impl Assembler {
is_running: false, is_running: false,
} }
} }
}
/// Start the compilation process in a separate thread impl TraceableProcess for Assembler {
pub fn start(&mut self) { type Output = DseExecutable;
if self.is_running { type Error = AssembleError;
return;
}
let src = self.src_path.clone(); fn run(mut self: Box<Self>, ctx: ProcessAgent) -> Result<Self::Output, Self::Error> {
let tx = self.result_tx.clone(); assemble(&self.src_path, &ctx)
thread::spawn(move || match assemble(&src) {
Ok(exe) => {
let mut buffer = Cursor::new(Vec::new());
match exe.write(&mut buffer) {
Ok(()) => {
tx.send(Ok(buffer.into_inner()))
.expect("Failed to send compilation result from worker thread");
}
Err(e) => {
tx.send(Err(AssembleError::Generic))
.expect("...");
}
}
}
Err(err) => { /* unchanged */ }
});
self.is_running = true;
}
/// Polls the result channel to check if a compilation result is ready.
///
/// This method attempts a non-blocking read (`try_recv`) from the result receiver.
/// It returns `Some(Ok(Vec<u8>))` if the compilation successfully finished and
/// the binary output is available, or `None` if no result was received yet.
///
/// If it receives an error via `mpsc::TryRecvError::Disconnected`, it indicates that
/// the worker thread terminated unexpectedly, returning a specific error string.
pub fn poll(&mut self) -> Option<Result<Vec<u8>, String>> {
if !self.is_running {
return None;
}
match self
.result_rx
.as_ref()
.expect("result_rx should be Some while compilation is running")
.try_recv()
{
Ok(result) => {
self.is_running = false;
Some(result.map_err(|e| e.to_string()))
}
Err(mpsc::TryRecvError::Empty) => None,
Err(mpsc::TryRecvError::Disconnected) => {
self.is_running = false;
Some(Err(String::from(
"Compilation terminated before a result was returned",
)))
}
}
}
/// Block until compilation is complete and return the result
pub fn output(&mut self) -> Result<Vec<u8>, String> {
if let Ok(result) = self
.result_rx
.take()
.expect("result_rx should be Some while waiting for compilation result")
.recv()
{
self.is_running = false;
result.map_err(|e| e.to_string())
} else {
self.is_running = false;
Err(String::from(
"Compilation terminated before a result was returned",
))
}
} }
} }
// fn assemble(src: &Path) -> Result<Vec<Instruction>, AssembleError> { fn assemble(src: &Path, ctx: &ProcessAgent) -> Result<DseExecutable, AssembleError> {
// let mut modules = HashSet::new();
// let mut program = Program::new();
//
// let hash = quick_hash(src);
//
// if modules.contains(&hash) {
// return Ok(vec![]);
// }
//
// prepare_dependency(src, &mut modules, &mut program)?;
//
// let mut nodes = program.nodes.clone();
//
// println!("PRE LINK");
// for node in &nodes {
// println!("{:?}", node);
// }
//
// create_sections(&mut nodes)?;
// resolve_symbols(&mut nodes)?;
//
// println!("Generating assembly output...");
//
// let instructions = codegen(nodes)?;
//
// println!("Compilation Successful");
// Ok(instructions)
// }
fn assemble(src: &Path) -> Result<DseExecutable, AssembleError> {
let mut modules = HashSet::new(); let mut modules = HashSet::new();
let mut program = Program::new(); let mut program = Program::new();
prepare_dependency(src, &mut modules, &mut program)?; prepare_dependency(src, &mut modules, &mut program, &ctx)?;
let nodes = program.nodes.clone(); let nodes = program.nodes.clone();
println!("PRE LINK"); // for node in &nodes {
for node in &nodes { // ctx.log(format!("{:?}", node));
println!("{:?}", node); // }
}
let (data_nodes, mut text_nodes) = split_sections(nodes); let (data_nodes, mut text_nodes) = split_sections(nodes);
let symbol_table = resolve_symbols(&data_nodes, &mut text_nodes)?; let symbol_table = resolve_symbols(&data_nodes, &mut text_nodes)?;
let text_len = text_nodes.len() as u32; let text_len = text_nodes.len() as u32;
println!("Generating assembly output..."); ctx.log("Generating assembly output...");
let instructions = codegen(text_nodes)?;
// println!("Generating assembly output...");
let instructions = codegen(text_nodes, ctx)?;
let text: Vec<u32> = instructions let text: Vec<u32> = instructions
.iter() .iter()
.map(|i| i.0).collect(); .map(|i| i.0).collect();
@@ -237,7 +128,7 @@ fn assemble(src: &Path) -> Result<DseExecutable, AssembleError> {
module: Module::Resolved(0), module: Module::Resolved(0),
}))?; }))?;
println!("Compilation Successful"); ctx.log("Compilation Successful");
Ok(DseExecutable::new( Ok(DseExecutable::new(
symbols.as_slice(), symbols.as_slice(),
@@ -264,6 +155,7 @@ fn prepare_dependency(
path: &Path, path: &Path,
modules: &mut HashSet<u64>, modules: &mut HashSet<u64>,
program: &mut Program, program: &mut Program,
ctx: &ProcessAgent,
) -> Result<(), AssembleError> { ) -> Result<(), AssembleError> {
let filename = path let filename = path
.file_name() .file_name()
@@ -271,21 +163,21 @@ fn prepare_dependency(
.expect("Failed to get file name from path"); .expect("Failed to get file name from path");
if let Ok(path) = path.canonicalize() { if let Ok(path) = path.canonicalize() {
println!("{:20} {:20} [{}]", "Building", filename, path.display()); ctx.log(format!("{:20} {:20} [{}]", "Building", filename, path.display()));
} }
let src = let src =
fs::read_to_string(path).map_err(|_| AssembleError::InvalidFile(path.to_path_buf()))?; fs::read_to_string(path).map_err(|_| AssembleError::InvalidFile(path.to_path_buf()))?;
let file_hash = quick_hash(path); let file_hash = quick_hash(path);
println!("{:20} {:20}", "Tokenising", filename); ctx.log(format!("{:20} {:20}", "Tokenising", filename));
let tokens = lexer::lexer(src, file_hash)?; let tokens = lexer::lexer(src, file_hash)?;
// let tokens = Lexer::new(src, file_hash).run()?; // let tokens = Lexer::new(src, file_hash).run()?;
println!("{:20} {:20}", "Parsing", filename); ctx.log(format!("{:20} {:20}", "Parsing", filename));
let parsed = Parser::parse_nodes(tokens)?; let parsed = Parser::parse_nodes(tokens)?;
println!("{:20} {:20}", "Resolving Deps", filename); ctx.log(format!("{:20} {:20}", "Resolving Deps", filename));
// Get the parent directory of the source file to use as the base directory // Get the parent directory of the source file to use as the base directory
let base_dir = path let base_dir = path
.parent() .parent()
@@ -295,32 +187,28 @@ fn prepare_dependency(
let deps = Parser::get_dependencies(&nodes, path)?; let deps = Parser::get_dependencies(&nodes, path)?;
println!("{:20} {:20}", "Expanding Pseudo-ops", filename); ctx.log(format!("{:20} {:20}", "Expanding Pseudo-ops", filename));
// add a section instruction // add a section instruction
nodes.insert( nodes.insert(
0, 0,
node!(None, Opcode::Segment, Token::Immediate(file_hash as u32)), node!(None, Opcode::Segment, Token::Immediate(file_hash as u32)),
); );
// for n in &nodes {
// println!("{n}");
// }
program.add_module(nodes); program.add_module(nodes);
for dep in deps { for dep in deps {
println!( ctx.log(format!(
"{:20} {:20}", "{:20} {:20}",
"Including", "Including",
dep.file_name() dep.file_name()
.and_then(|f| f.to_str()) .and_then(|f| f.to_str())
.expect("Dependency path has no file name or is not valid UTF-8") .expect("Dependency path has no file name or is not valid UTF-8")
); ));
let dep_hash = quick_hash(&dep); let dep_hash = quick_hash(&dep);
if modules.insert(dep_hash) { if modules.insert(dep_hash) {
prepare_dependency(dep.as_path(), modules, program)?; prepare_dependency(dep.as_path(), modules, program, &ctx)?;
} }
} }
-157
View File
@@ -1,157 +0,0 @@
use std::{
collections::HashMap,
fs::canonicalize,
path::{Path, PathBuf},
};
use common::prelude::Register;
use crate::assembler::model::{Module, Node, Opcode, Symbol, Token};
use crate::assembler::quick_hash;
use crate::{assembler::AssembleError, node};
pub fn resolve_symbols(nodes: &mut [Node]) -> Result<(), AssembleError> {
let symbol_table = generate_symbol_table(nodes);
for node in nodes.iter_mut() {
match node.opcode() {
Opcode::Jmp | Opcode::Call | Opcode::Jic | Opcode::Jnc | Opcode::Lli | Opcode::Lui => {
if let Token::Symbol(symbol) = node
.arg(0)
.expect("Expected argument 0 for jump-like opcode")
{
if let Some(address) = symbol_table.get(&symbol) {
node.tokens[0] = Token::Immediate(*address);
} else {
return Err(AssembleError::UndefinedSymbol(symbol));
}
}
}
Opcode::Jez | Opcode::Jnz => {
if let Token::Symbol(symbol) = node
.arg(1)
.expect("Expected argument 0 for jump-like opcode")
{
if let Some(address) = symbol_table.get(&symbol) {
node.tokens[1] = Token::Immediate(*address);
} else {
return Err(AssembleError::UndefinedSymbol(symbol));
}
}
}
_ => (),
}
}
Ok(())
}
fn generate_symbol_table(nodes: &[Node]) -> HashMap<Symbol, u32> {
let mut table = HashMap::new();
for (i, node) in nodes.iter().enumerate() {
if let Some(symbol) = node.label() {
table.insert(symbol, 4 * i as u32);
}
}
table
}
pub fn resolve_dependencies(
mut nodes: Vec<Node>,
base_dir: &Path,
) -> Result<Vec<Node>, AssembleError> {
// First we get a list of imports.
let mut dependencies = Vec::new();
for node in &nodes {
if node.opcode() == Opcode::Include {
// we want the path, and the name
let name = if let Token::Symbol(name) = node
.arg(0)
.expect("Expected argument #0 for Include directive.")
{
name.name.clone()
} else {
unreachable!()
}; //node.2.get(0).unwrap()
let Ok(Token::StringLit(path)) = node.arg(1) else {
unreachable!()
};
let full_path = base_dir.join(path);
let canonical_path = full_path
.canonicalize()
.map_err(|_| AssembleError::InvalidFile(full_path.clone()))?;
let hash = quick_hash(&canonical_path);
dependencies.push((name, hash));
}
}
let mut changes = Vec::<(u32, u32, Symbol)>::new();
// now we resolve the symbols on all the nodes
// we need to check all operands for unresolved signals
for (i, node) in nodes.clone().iter().enumerate() {
let Node {
tokens: operands, ..
} = node;
for (j, token) in operands.iter().enumerate() {
if let Token::Symbol(symbol) = token {
for d in &dependencies {
if let Module::Unresolved(name) = symbol.module.clone() {
if name != d.0 {
continue;
}
let symbol = Symbol {
name: symbol.name.clone(),
module: Module::Resolved(d.1),
};
changes.push((i as u32, j as u32, symbol));
}
}
}
}
}
for (i, j, symbol) in changes {
nodes[i as usize].tokens[j as usize] = Token::Symbol(symbol);
}
Ok(nodes)
}
pub fn create_sections(nodes: &mut Vec<Node>) -> Result<(), AssembleError> {
let mut res = Vec::<Node>::with_capacity(nodes.len());
res.push(node!(None, Opcode::Segment, Token::Immediate(0)));
for n in nodes.iter() {
if n.opcode() == Opcode::Data {
res.push(n.clone());
}
}
let start = res.len() + 2;
res.insert(
0,
node!(
None,
Opcode::Jmp,
Token::Immediate(start as u32 * 4),
Token::Register(Register::Zero)
),
);
for n in nodes.iter() {
if !matches!(n.opcode(), Opcode::Data | Opcode::Include) {
res.push(n.clone());
}
}
*nodes = res;
Ok(())
}
+8 -4
View File
@@ -1,6 +1,9 @@
use assembler::prelude::*; use assembler::prelude::*;
use clap::{Parser, arg}; use clap::{Parser, arg};
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf};
use std::io::Cursor;
use assembler::assembler::AssembleError;
use common::process::TraceableProcess;
#[derive(Parser, Debug, Clone)] #[derive(Parser, Debug, Clone)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
@@ -15,11 +18,12 @@ fn main() {
// Parse command line arguments // Parse command line arguments
let args = Args::parse(); let args = Args::parse();
let mut engine = Assembler::new(PathBuf::from(args.input_path)); let assembler = Assembler::new(PathBuf::from(args.input_path));
engine.start(); let result = assembler.run_sync().expect("assembler failed.");
let result = engine.output().expect("assembler failed."); let mut buffer = Cursor::new(Vec::new());
result.write(&mut buffer).unwrap();
if let Err(e) = fs::write(args.output_path, result) { if let Err(e) = fs::write(args.output_path, buffer.into_inner()) {
eprintln!("Failed to write to output file: {e}"); eprintln!("Failed to write to output file: {e}");
std::process::exit(1); std::process::exit(1);
} }
+35 -5
View File
@@ -1,23 +1,53 @@
use std::fmt::Debug; use std::fmt::Debug;
use std::sync::mpsc; use std::sync::{mpsc, Arc};
use crate::process::AgentSink::{Callback, Channel};
#[derive(Debug, Clone)]
pub struct ProcessAgent { pub struct ProcessAgent {
tx: mpsc::Sender<String>, sink: Arc<AgentSink>,
} }
impl ProcessAgent { impl ProcessAgent {
pub fn new(tx: mpsc::Sender<String>) -> Self { pub fn new(tx: mpsc::Sender<String>) -> Self {
Self { tx } Self { sink: Arc::new(Channel(tx)) }
}
fn new_sync(f: impl Fn(&str) + Send + Sync + 'static) -> Self {
Self { sink: Arc::new(Callback(Box::new(f))) }
} }
pub fn log(&self, msg: impl Into<String>) { pub fn log(&self, msg: impl Into<String>) {
let _ = self.tx.send(msg.into()); let msg = msg.into();
match &*self.sink {
Channel(tx) => { let _ = tx.send(msg); }
Callback(f) => f(&msg),
}
} }
} }
enum AgentSink {
Channel(mpsc::Sender<String>),
Callback(Box<dyn Fn(&str) + Send + Sync>)
}
pub trait TraceableProcess: Debug + Send + 'static { pub trait TraceableProcess: Debug + Send + 'static {
type Output: Send + 'static; type Output: Send + 'static;
type Error: std::error::Error + Send + 'static; type Error: std::error::Error + Send + 'static;
fn label(&self) -> String {
format!("{self:?}")
}
fn run(self: Box<Self>, ctx: ProcessAgent) -> Result<Self::Output, Self::Error>; fn run(self: Box<Self>, ctx: ProcessAgent) -> Result<Self::Output, Self::Error>;
///
fn run_sync(self) -> Result<Self::Output, Self::Error>
where
Self: Sized
{
Self::run(
Box::new(self),
ProcessAgent::new_sync(|line| println!("{line}") )
)
}
} }
+5 -2
View File
@@ -30,6 +30,8 @@ pub trait DsaImplementation: Send {
/// integration testing. /// integration testing.
fn boot(&mut self) -> Result<(), DsaError>; fn boot(&mut self) -> Result<(), DsaError>;
fn reset(&mut self);
/// main run method. the main-loop of the program execution /// main run method. the main-loop of the program execution
fn run(&mut self) -> Result<(), DsaError>; fn run(&mut self) -> Result<(), DsaError>;
@@ -99,9 +101,10 @@ impl EmulatorHandle {
} }
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.state.reseting.store(true, Ordering::Relaxed); self.state.resetting.store(true, Ordering::Relaxed);
self.memory.clear(); self.memory.clear();
self.state.reseting.store(false, Ordering::Relaxed); self.state.proc.store(Arc::new(Default::default()));
self.state.resetting.store(false, Ordering::Relaxed);
} }
pub fn get_register(&self, rg: Register) -> u32 { pub fn get_register(&self, rg: Register) -> u32 {
@@ -28,6 +28,18 @@ pub struct DsaProcessor {
} }
impl DsaProcessor { impl DsaProcessor {
pub fn reset(&mut self) {
self.internal_state = ProcessorSnapshot::default();
self.mmu = MMU::new();
// deallocates the old memory
self.mainstore.clear();
self.mmio_region = None;
self.memory_map = None;
self.__mmap_configured = false;
}
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
internal_state: ProcessorSnapshot::default(), internal_state: ProcessorSnapshot::default(),
@@ -32,6 +32,10 @@ impl DsaImplementation for Emulator {
self.core.boot() self.core.boot()
} }
fn reset(&mut self) {
self.core.reset();
}
fn run(&mut self) -> Result<(), DsaError> { fn run(&mut self) -> Result<(), DsaError> {
// wait for UI to initialise. // wait for UI to initialise.
if self.shared_state.paused.load(Ordering::Relaxed) { if self.shared_state.paused.load(Ordering::Relaxed) {
@@ -88,9 +92,8 @@ impl DsaImplementation for Emulator {
} }
// Check if the previous instruction caused a fault. // Check if the previous instruction caused a fault.
if let Some(fault) = self.pending_fault { if let Some(fault) = self.pending_fault.take() {
println!("WARN fault: {:?}", fault); println!("WARN fault: {:?}", fault);
self.pending_fault = None;
self.core.interrupt(fault); self.core.interrupt(fault);
} }
@@ -216,11 +219,10 @@ impl Emulator {
} }
// Responsibility: reset when requested by the UI // Responsibility: reset when requested by the UI
if self.shared_state.reseting.load(Ordering::Relaxed) == true { if self.shared_state.resetting.load(Ordering::Relaxed) == true {
self.core.internal_state.registers = [0; Register::COUNT as usize]; self.reset();
self.core.internal_state.clock = 0;
while self.shared_state.reseting.load(Ordering::Relaxed) == true { while self.shared_state.resetting.load(Ordering::Relaxed) == true {
thread::sleep(Duration::from_millis(100)); thread::sleep(Duration::from_millis(100));
} }
} }
@@ -8,7 +8,7 @@ use super::interrupts::Interrupt;
pub struct SharedState { pub struct SharedState {
pub proc: ArcSwap<ProcessorSnapshot>, pub proc: ArcSwap<ProcessorSnapshot>,
pub reseting: AtomicBool, pub resetting: AtomicBool,
pub paused: AtomicBool, pub paused: AtomicBool,
pub update_req: AtomicBool, pub update_req: AtomicBool,
pub interrupt_queue: mpsc::Sender<Interrupt>, pub interrupt_queue: mpsc::Sender<Interrupt>,
@@ -17,7 +17,7 @@ pub struct SharedState {
impl SharedState { impl SharedState {
pub fn new(sender: mpsc::Sender<Interrupt>) -> Self { pub fn new(sender: mpsc::Sender<Interrupt>) -> Self {
Self { Self {
reseting: AtomicBool::new(false), resetting: AtomicBool::new(false),
proc: ArcSwap::new(Arc::new(ProcessorSnapshot::default())), proc: ArcSwap::new(Arc::new(ProcessorSnapshot::default())),
paused: AtomicBool::new(true), paused: AtomicBool::new(true),
@@ -32,13 +32,13 @@ impl Controller {
let padding = Vec2::new(14.0, 8.0); let padding = Vec2::new(14.0, 8.0);
match status { match status {
RunningState::Paused | RunningState::Halted | RunningState::Breakpoint(_) => { RunningState::Paused | RunningState::Breakpoint(_) => {
if PaddedWidget::button("▶ Run").color(Color32::GREEN).ui(ui).clicked() { if PaddedWidget::button("▶ Run").color(Color32::GREEN).ui(ui).clicked() {
self.emulator.resume() self.emulator.resume()
} }
}, },
RunningState::Running => { RunningState::Running | RunningState::Halted => {
if PaddedWidget::button("⏸ Pause").color(Color32::YELLOW).ui(ui).clicked() { if PaddedWidget::button("⏸ Pause").color(Color32::YELLOW).ui(ui).clicked() {
self.emulator.pause() self.emulator.pause()
} }
@@ -51,6 +51,8 @@ impl Controller {
} }
} }
status.clone().ui(ui, padding);
if PaddedWidget::button("⟳ Reset All").color(Color32::YELLOW).ui(ui).clicked() { if PaddedWidget::button("⟳ Reset All").color(Color32::YELLOW).ui(ui).clicked() {
self.emulator.reset(); self.emulator.reset();
} }
@@ -79,9 +81,6 @@ impl Controller {
// } // }
// } // }
status.clone().ui(ui, padding);
ui.separator(); ui.separator();
let pcx = self.emulator.get_register(Register::Pcx); let pcx = self.emulator.get_register(Register::Pcx);
@@ -44,6 +44,7 @@ pub struct ProcessManager {
handles: Vec<ProcessHandle>, handles: Vec<ProcessHandle>,
terminated_actions: VecDeque<Action>, terminated_actions: VecDeque<Action>,
active_tab: Option<usize>,
} }
impl ProcessManager { impl ProcessManager {
@@ -66,26 +67,91 @@ impl ProcessManager {
self.handles.push(handle); self.handles.push(handle);
} }
pub fn ui(&mut self, ui: &mut Ui) { fn drain_logs(&mut self) {
for handle in &mut self.handles { for handle in &mut self.handles {
let is_finished = handle.join_handle while let Ok(line) = handle.rx.try_recv() {
.as_ref() handle.logs.push(line);
.is_some_and(|jh| jh.is_finished()); }
}
}
/// Checks for finished processes and queues their resulting Actions.
fn poll_finished(&mut self) {
for handle in &mut self.handles {
let is_finished = handle.join_handle.as_ref().is_some_and(|jh| jh.is_finished());
if is_finished { if is_finished {
if let Some(jh) = handle.join_handle.take() { if let Some(jh) = handle.join_handle.take() {
println!("finished! {}", handle.name);
if let Ok(action) = jh.join() { if let Ok(action) = jh.join() {
self.terminated_actions.push_back(action); self.terminated_actions.push_back(action);
} }
} }
} }
} }
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
self.drain_logs();
self.poll_finished();
if self.handles.is_empty() {
ui.centered_and_justified(|ui| {
ui.weak("No processes running.");
});
return;
}
// keep active_tab pointing at something valid
if !self.active_tab.map_or(false, |id| self.handles.iter().any(|h| h.id == id)) {
self.active_tab = self.handles.first().map(|h| h.id);
}
// --- tab bar ---
ui.horizontal(|ui| { ui.horizontal(|ui| {
for h in &self.handles { for handle in &self.handles {
ui.label(&h.name); let selected = self.active_tab == Some(handle.id);
}; let running = handle.join_handle.is_some();
let label = if running {
format!("● {}", handle.name)
} else {
format!("○ {}", handle.name)
};
if ui.selectable_label(selected, label).clicked() {
self.active_tab = Some(handle.id);
}
}
}); });
ui.separator();
// --- close button for the active tab ---
if let Some(active_id) = self.active_tab {
ui.horizontal(|ui| {
if let Some(handle) = self.handles.iter().find(|h| h.id == active_id) {
ui.label(egui::RichText::new(&handle.name).strong());
}
if ui.small_button("✕ Close").clicked() {
self.handles.retain(|h| h.id != active_id);
self.active_tab = self.handles.first().map(|h| h.id);
}
});
ui.separator();
// --- log view for the active tab ---
if let Some(handle) = self.handles.iter().find(|h| h.id == active_id) {
egui::ScrollArea::vertical()
.id_salt(("process_log", active_id))
.stick_to_bottom(true)
.auto_shrink([false, false])
.show(ui, |ui| {
for line in &handle.logs {
ui.monospace(line);
}
});
}
}
} }
} }