use std::collections::BTreeMap;
use std::io::{self, Stdout};
use std::time::{Duration, Instant};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use libhaystack::val::Value;
use ratatui::{
Terminal,
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block as TuiBlock, Borders, Cell, Paragraph, Row, Table},
};
use tokio::sync::mpsc;
use tokio::task::yield_now;
use uuid::Uuid;
use logic_mesh::base::engine::Engine;
use logic_mesh::base::engine::messages::{ChangeSource, EngineMessage, WatchMessage};
use logic_mesh::base::program::{
Program, ProgramBlock,
data::{LinkData, PinValue, Position},
};
use logic_mesh::multi_threaded::MultiThreadedEngine;
type Backend = CrosstermBackend<Stdout>;
#[derive(Default, Clone)]
struct BlockRow {
name: String,
state: String,
fault_reason: Option<String>,
inputs: BTreeMap<String, Value>,
outputs: BTreeMap<String, Value>,
last_update: Option<Instant>,
}
#[tokio::main(flavor = "multi_thread")]
async fn main() -> anyhow::Result<()> {
let program = sample_program();
let mut eng = MultiThreadedEngine::new();
let (reply_tx, mut reply_rx) = mpsc::channel(32);
let channel_id = Uuid::new_v4();
let engine_sender = eng.create_message_channel(channel_id, reply_tx);
let engine_handle = tokio::spawn(async move {
eng.run().await;
});
yield_now().await;
let (watch_tx, mut watch_rx) = mpsc::unbounded_channel::<WatchMessage>();
engine_sender
.send(EngineMessage::WatchBlockSubReq(channel_id, watch_tx))
.await?;
expect_reply(&mut reply_rx, "WatchBlockSubRes").await;
engine_sender
.send(EngineMessage::LoadProgramReq(channel_id, program.clone()))
.await?;
expect_reply(&mut reply_rx, "LoadProgramRes").await;
let mut rows: BTreeMap<Uuid, BlockRow> = BTreeMap::new();
for (id_str, pb) in &program.blocks {
if let Ok(id) = Uuid::parse_str(id_str) {
rows.insert(
id,
BlockRow {
name: pb.label.clone().unwrap_or_else(|| pb.name.clone()),
state: "running".to_string(),
..Default::default()
},
);
}
}
let mut terminal = init_terminal()?;
let result = run_ui(&mut terminal, &mut rows, &mut watch_rx).await;
restore_terminal(&mut terminal)?;
let _ = engine_sender.send(EngineMessage::Shutdown).await;
let _ = engine_handle.await;
result
}
async fn run_ui(
terminal: &mut Terminal<Backend>,
rows: &mut BTreeMap<Uuid, BlockRow>,
watch_rx: &mut mpsc::UnboundedReceiver<WatchMessage>,
) -> anyhow::Result<()> {
let mut tick = tokio::time::interval(Duration::from_millis(33));
loop {
tokio::select! {
Some(msg) = watch_rx.recv() => {
apply_watch_message(rows, msg);
draw(terminal, rows)?;
}
_ = tick.tick() => {
if event::poll(Duration::from_millis(0))?
&& let Event::Key(k) = event::read()?
&& (k.code == KeyCode::Char('q') || k.code == KeyCode::Esc)
{
return Ok(());
}
draw(terminal, rows)?;
}
}
}
}
fn apply_watch_message(rows: &mut BTreeMap<Uuid, BlockRow>, msg: WatchMessage) {
let entry = rows.entry(msg.block_id).or_default();
entry.state = msg.state.label().to_string();
entry.fault_reason = msg.state.fault_reason().map(|s| s.to_string());
entry.last_update = Some(Instant::now());
for (pin, change) in msg.changes {
match change {
ChangeSource::Input(name, value) => {
entry.inputs.insert(name, value);
}
ChangeSource::Output(name, value) => {
entry.outputs.insert(name, value);
}
}
let _ = pin; }
}
fn draw(terminal: &mut Terminal<Backend>, rows: &BTreeMap<Uuid, BlockRow>) -> anyhow::Result<()> {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(0)])
.split(f.area());
let header = Paragraph::new(Line::from(vec![
Span::styled("Logic Mesh", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" — press "),
Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to quit"),
]))
.block(TuiBlock::default().borders(Borders::ALL));
f.render_widget(header, chunks[0]);
let header_row = Row::new(vec!["block", "state", "inputs", "outputs"])
.style(Style::default().add_modifier(Modifier::BOLD));
let body: Vec<Row> = rows
.values()
.map(|r| {
let state_style = if r.state == "fault" {
Style::default().fg(Color::Red)
} else {
Style::default()
};
Row::new(vec![
Cell::from(r.name.clone()),
Cell::from(if let Some(reason) = &r.fault_reason {
format!("{}: {}", r.state, reason)
} else {
r.state.clone()
})
.style(state_style),
Cell::from(fmt_pins(&r.inputs)),
Cell::from(fmt_pins(&r.outputs)),
])
})
.collect();
let table = Table::new(
body,
[
Constraint::Length(20),
Constraint::Length(30),
Constraint::Percentage(40),
Constraint::Percentage(40),
],
)
.header(header_row)
.block(TuiBlock::default().borders(Borders::ALL).title("Blocks"));
f.render_widget(table, chunks[1]);
})?;
Ok(())
}
fn fmt_pins(pins: &BTreeMap<String, Value>) -> String {
if pins.is_empty() {
return String::new();
}
pins.iter()
.map(|(name, v)| format!("{name}={}", fmt_value(v)))
.collect::<Vec<_>>()
.join(" ")
}
fn fmt_value(v: &Value) -> String {
if let Ok(n) = TryInto::<f64>::try_into(v) {
format!("{n:.2}")
} else {
format!("{v:?}")
}
}
async fn expect_reply<R: std::fmt::Debug>(rx: &mut mpsc::Receiver<R>, what: &str) {
if let Some(msg) = rx.recv().await {
let _ = msg;
} else {
eprintln!("Engine reply channel closed waiting for {what}");
}
}
fn init_terminal() -> io::Result<Terminal<Backend>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
Terminal::new(CrosstermBackend::new(stdout))
}
fn restore_terminal(terminal: &mut Terminal<Backend>) -> io::Result<()> {
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
Ok(())
}
fn sample_program() -> Program {
let sine1 = Uuid::new_v4();
let sine2 = Uuid::new_v4();
let adder = Uuid::new_v4();
let mut blocks = BTreeMap::new();
blocks.insert(
sine1.to_string(),
ProgramBlock {
name: "SineWave".to_string(),
lib: "core".to_string(),
label: Some("fast sine".to_string()),
positions: Some(Position { x: 0.0, y: 0.0 }),
inputs: pin_map(&[("freq", 50.into()), ("amplitude", 3.into())]),
outputs: Default::default(),
},
);
blocks.insert(
sine2.to_string(),
ProgramBlock {
name: "SineWave".to_string(),
lib: "core".to_string(),
label: Some("slow sine".to_string()),
positions: Some(Position { x: 0.0, y: 100.0 }),
inputs: pin_map(&[("freq", 200.into()), ("amplitude", 7.into())]),
outputs: Default::default(),
},
);
blocks.insert(
adder.to_string(),
ProgramBlock {
name: "Add".to_string(),
lib: "core".to_string(),
label: Some("sum".to_string()),
positions: Some(Position { x: 200.0, y: 50.0 }),
inputs: Default::default(),
outputs: Default::default(),
},
);
let mut links = BTreeMap::new();
add_link(&mut links, &sine1, "out", &adder, "in0");
add_link(&mut links, &sine2, "out", &adder, "in1");
Program {
name: Some("sine + sum".to_string()),
description: Some("Two sine sources feeding an adder.".to_string()),
blocks,
links,
}
}
fn pin_map(entries: &[(&str, Value)]) -> BTreeMap<String, PinValue> {
entries
.iter()
.map(|(name, v)| {
(
(*name).to_string(),
PinValue {
value: v.clone(),
is_connected: false,
},
)
})
.collect()
}
fn add_link(
links: &mut BTreeMap<String, LinkData>,
src: &Uuid,
src_pin: &str,
dst: &Uuid,
dst_pin: &str,
) {
let id = Uuid::new_v4().to_string();
links.insert(
id.clone(),
LinkData {
id: Some(id),
source_block_uuid: src.to_string(),
target_block_uuid: dst.to_string(),
source_block_pin_name: src_pin.to_string(),
target_block_pin_name: dst_pin.to_string(),
},
);
}