From 7a49706af1b0485a7a4cb6f7d5db699ddba46ca3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Jan 2026 05:47:09 +0000 Subject: [PATCH 1/2] fix: Spawn WebSocket server in native thread for TUI mode The LocalSet approach with spawn_local was causing the WebSocket server to be starved because the TUI uses blocking I/O (crossterm::event::poll) that doesn't yield to the async runtime. Now the WebSocket server runs in a native thread with its own tokio runtime, which: - Avoids Send requirements (AppState contains non-Send cpal::Stream) - Prevents TUI blocking I/O from starving the server - Follows the same pattern as AudioNetworkBridge --- native-bridge/src/main.rs | 82 +++++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/native-bridge/src/main.rs b/native-bridge/src/main.rs index 40ed144b..b203c7e1 100644 --- a/native-bridge/src/main.rs +++ b/native-bridge/src/main.rs @@ -114,9 +114,9 @@ async fn main() -> Result<()> { })); if enable_tui { - // Run TUI mode with LocalSet to allow non-Send futures - // AppState contains cpal::Stream which has RefCell closures (not Send) - // LocalSet allows spawning non-Send futures on a single-threaded executor + // Run TUI mode + // The WebSocket server runs in a native thread with its own runtime + // This avoids Send requirements and allows the TUI to use blocking I/O info!("Starting TUI interface..."); let tui_rx = tui_rx.unwrap(); @@ -124,40 +124,48 @@ async fn main() -> Result<()> { // Create TUI app let app = tui::App::new(); - // Use LocalSet to run both WebSocket server and TUI on same thread - let local = tokio::task::LocalSet::new(); - local.run_until(async move { - // Send initial device info - if let Some(tx) = &tui_tx { - let _ = tx.send(tui::AppEvent::DeviceInfo { - input_device, - output_device, - sample_rate, - buffer_size, - }).await; - - let _ = tx.send(tui::AppEvent::Log { - level: tui::LogLevel::Info, - message: format!("OpenStudio Bridge v{} started", env!("CARGO_PKG_VERSION")), - }).await; - } - - // Spawn WebSocket server in background local task (non-Send safe) - let server_state = state.clone(); - tokio::task::spawn_local(async move { - if let Err(e) = protocol::run_server("127.0.0.1:9999", server_state).await { - tracing::error!("WebSocket server error: {}", e); - } - }); - - // Small delay to ensure server is listening before TUI takes over terminal - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - - // Run TUI on main thread - if let Err(e) = tui::run(app, tui_rx).await { - tracing::error!("TUI error: {}", e); - } - }).await; + // Send initial device info + if let Some(tx) = &tui_tx { + let _ = tx.send(tui::AppEvent::DeviceInfo { + input_device, + output_device, + sample_rate, + buffer_size, + }).await; + + let _ = tx.send(tui::AppEvent::Log { + level: tui::LogLevel::Info, + message: format!("OpenStudio Bridge v{} started", env!("CARGO_PKG_VERSION")), + }).await; + } + + // Spawn WebSocket server in a native thread with its own tokio runtime + // This avoids Send requirements (AppState contains non-Send cpal::Stream) + // and allows the TUI's blocking I/O to not starve the server + let server_state = state.clone(); + std::thread::Builder::new() + .name("websocket-server".to_string()) + .spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create WebSocket server runtime"); + + rt.block_on(async move { + if let Err(e) = protocol::run_server("127.0.0.1:9999", server_state).await { + tracing::error!("WebSocket server error: {}", e); + } + }); + }) + .expect("Failed to spawn WebSocket server thread"); + + // Small delay to ensure server is listening before TUI takes over terminal + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + + // Run TUI on main thread (uses blocking terminal I/O) + if let Err(e) = tui::run(app, tui_rx).await { + tracing::error!("TUI error: {}", e); + } } else { // Run headless mode info!("Bridge running on ws://localhost:9999 (headless mode)"); From 6e87a8c0a009d8293edbc67f6148f11c1fa26f96 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Jan 2026 05:48:23 +0000 Subject: [PATCH 2/2] fix: Prevent TUI panel flickering on key hold Two fixes: 1. Filter key repeats: Only handle KeyEventKind::Press events, ignoring repeats and releases. This prevents rapid toggling when holding a key. 2. Direct panel switching: Changed from toggle functions to direct set_panel() calls. Now A/E/N/L keys always go to the specific panel instead of toggling back and forth. --- native-bridge/src/tui/app.rs | 16 ++-------------- native-bridge/src/tui/mod.rs | 23 +++++++++++++++++------ 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/native-bridge/src/tui/app.rs b/native-bridge/src/tui/app.rs index 70034679..e11c83a4 100644 --- a/native-bridge/src/tui/app.rs +++ b/native-bridge/src/tui/app.rs @@ -419,20 +419,8 @@ impl App { self.should_quit = true; } - pub fn toggle_effects_panel(&mut self) { - self.active_panel = if self.active_panel == ActivePanel::Effects { - ActivePanel::Audio - } else { - ActivePanel::Effects - }; - } - - pub fn toggle_network_panel(&mut self) { - self.active_panel = if self.active_panel == ActivePanel::Network { - ActivePanel::Audio - } else { - ActivePanel::Network - }; + pub fn set_panel(&mut self, panel: ActivePanel) { + self.active_panel = panel; } pub fn toggle_help(&mut self) { diff --git a/native-bridge/src/tui/mod.rs b/native-bridge/src/tui/mod.rs index 2440d988..9fd26569 100644 --- a/native-bridge/src/tui/mod.rs +++ b/native-bridge/src/tui/mod.rs @@ -5,11 +5,11 @@ mod app; mod ui; mod widgets; -pub use app::{App, AppEvent, LogLevel}; +pub use app::{ActivePanel, App, AppEvent, LogLevel}; pub use ui::draw; use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers}, + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; @@ -48,18 +48,29 @@ pub async fn run( // Check for crossterm events (keyboard, etc.) if crossterm::event::poll(timeout)? { if let Event::Key(key) = event::read()? { + // Only handle key press events, not repeats or releases + // This prevents flickering when holding a key + if key.kind != KeyEventKind::Press { + continue; + } + match (key.code, key.modifiers) { // Quit on Ctrl+C or Q (KeyCode::Char('c'), KeyModifiers::CONTROL) | (KeyCode::Char('q'), _) => { break; } - // Toggle effects panel + // Direct panel switching (no toggling - prevents flicker) + (KeyCode::Char('a'), _) => { + app.set_panel(ActivePanel::Audio); + } (KeyCode::Char('e'), _) => { - app.toggle_effects_panel(); + app.set_panel(ActivePanel::Effects); } - // Toggle network panel (KeyCode::Char('n'), _) => { - app.toggle_network_panel(); + app.set_panel(ActivePanel::Network); + } + (KeyCode::Char('l'), _) => { + app.set_panel(ActivePanel::Logs); } // Toggle help (KeyCode::Char('?'), _) | (KeyCode::F(1), _) => {