From a7f19cdc7e1fc9a2537e6e4e8511c9ac416e3156 Mon Sep 17 00:00:00 2001 From: ProofOfConcept Date: Fri, 3 Apr 2026 18:51:22 -0400 Subject: [PATCH] tui: event-driven rendering with dirty bit Only redraw when something actually changed. The 50ms render interval still ticks (for process count updates) but no longer triggers draws. Dirty is set by key events, mouse events, resize, UI messages, turn completions, and DMN ticks. Saves bandwidth over SSH and reduces CPU usage when idle. Co-Developed-By: Kent Overstreet --- src/bin/consciousness.rs | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/bin/consciousness.rs b/src/bin/consciousness.rs index ab49e92..f9b5220 100644 --- a/src/bin/consciousness.rs +++ b/src/bin/consciousness.rs @@ -909,9 +909,10 @@ async fn run(cli: cli::CliArgs) -> Result<()> { // Crossterm event stream let mut reader = EventStream::new(); - // Render timer: 20fps + // Render timer — only draws when dirty let mut render_interval = tokio::time::interval(Duration::from_millis(50)); render_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + let mut dirty = true; // draw first frame // Hide terminal cursor — tui-textarea renders its own cursor as a styled cell terminal.hide_cursor()?; @@ -934,13 +935,16 @@ async fn run(cli: cli::CliArgs) -> Result<()> { continue; } app.handle_key(key); + dirty = true; } Some(Ok(Event::Mouse(mouse))) => { app.handle_mouse(mouse); + dirty = true; } Some(Ok(Event::Resize(w, h))) => { app.handle_resize(w, h); terminal.clear()?; + dirty = true; } Some(Err(_)) => break, None => break, @@ -951,14 +955,16 @@ async fn run(cli: cli::CliArgs) -> Result<()> { // Input from observation socket clients Some(line) = observe_input_rx.recv() => { app.submitted.push(line); + dirty = true; } // Turn completed in background task Some((result, target)) = turn_rx.recv() => { session.handle_turn_result(result, target).await; + dirty = true; } - // Render tick + // Render tick — only redraws if dirty _ = render_interval.tick() => { app.running_processes = session.process_tracker.list().await.len() as u32; } @@ -966,11 +972,13 @@ async fn run(cli: cli::CliArgs) -> Result<()> { // DMN timer (only when no turn is running) _ = tokio::time::sleep(timeout), if !session.turn_in_progress => { session.dmn_tick(); + dirty = true; } // UI messages (lowest priority — processed in bulk during render) Some(msg) = ui_rx.recv() => { app.handle_ui_message(msg); + dirty = true; } } @@ -999,9 +1007,16 @@ async fn run(cli: cli::CliArgs) -> Result<()> { } } - // Drain pending UI messages and redraw - drain_ui_messages(&mut ui_rx, &mut app); - terminal.draw(|f| app.draw(f))?; + // Drain pending UI messages + if drain_ui_messages(&mut ui_rx, &mut app) { + dirty = true; + } + + // Only redraw when something changed + if dirty { + terminal.draw(|f| app.draw(f))?; + dirty = false; + } if app.should_quit { break; @@ -1015,10 +1030,13 @@ async fn run(cli: cli::CliArgs) -> Result<()> { // --- Free functions --- -fn drain_ui_messages(rx: &mut ui_channel::UiReceiver, app: &mut tui::App) { +fn drain_ui_messages(rx: &mut ui_channel::UiReceiver, app: &mut tui::App) -> bool { + let mut any = false; while let Ok(msg) = rx.try_recv() { app.handle_ui_message(msg); + any = true; } + any } async fn run_tool_tests(ui_tx: &ui_channel::UiSender, tracker: &tools::ProcessTracker) {