Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 45 additions & 37 deletions native-bridge/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,50 +114,58 @@ 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();

// 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)");
Expand Down
16 changes: 2 additions & 14 deletions native-bridge/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
23 changes: 17 additions & 6 deletions native-bridge/src/tui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
Expand Down Expand Up @@ -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), _) => {
Expand Down
Loading