@@ -114,50 +114,58 @@ async fn main() -> Result<()> {
114114 } ) ) ;
115115
116116 if enable_tui {
117- // Run TUI mode with LocalSet to allow non-Send futures
118- // AppState contains cpal::Stream which has RefCell closures (not Send)
119- // LocalSet allows spawning non- Send futures on a single-threaded executor
117+ // Run TUI mode
118+ // The WebSocket server runs in a native thread with its own runtime
119+ // This avoids Send requirements and allows the TUI to use blocking I/O
120120 info ! ( "Starting TUI interface..." ) ;
121121
122122 let tui_rx = tui_rx. unwrap ( ) ;
123123
124124 // Create TUI app
125125 let app = tui:: App :: new ( ) ;
126126
127- // Use LocalSet to run both WebSocket server and TUI on same thread
128- let local = tokio:: task:: LocalSet :: new ( ) ;
129- local. run_until ( async move {
130- // Send initial device info
131- if let Some ( tx) = & tui_tx {
132- let _ = tx. send ( tui:: AppEvent :: DeviceInfo {
133- input_device,
134- output_device,
135- sample_rate,
136- buffer_size,
137- } ) . await ;
138-
139- let _ = tx. send ( tui:: AppEvent :: Log {
140- level : tui:: LogLevel :: Info ,
141- message : format ! ( "OpenStudio Bridge v{} started" , env!( "CARGO_PKG_VERSION" ) ) ,
142- } ) . await ;
143- }
144-
145- // Spawn WebSocket server in background local task (non-Send safe)
146- let server_state = state. clone ( ) ;
147- tokio:: task:: spawn_local ( async move {
148- if let Err ( e) = protocol:: run_server ( "127.0.0.1:9999" , server_state) . await {
149- tracing:: error!( "WebSocket server error: {}" , e) ;
150- }
151- } ) ;
152-
153- // Small delay to ensure server is listening before TUI takes over terminal
154- tokio:: time:: sleep ( tokio:: time:: Duration :: from_millis ( 100 ) ) . await ;
155-
156- // Run TUI on main thread
157- if let Err ( e) = tui:: run ( app, tui_rx) . await {
158- tracing:: error!( "TUI error: {}" , e) ;
159- }
160- } ) . await ;
127+ // Send initial device info
128+ if let Some ( tx) = & tui_tx {
129+ let _ = tx. send ( tui:: AppEvent :: DeviceInfo {
130+ input_device,
131+ output_device,
132+ sample_rate,
133+ buffer_size,
134+ } ) . await ;
135+
136+ let _ = tx. send ( tui:: AppEvent :: Log {
137+ level : tui:: LogLevel :: Info ,
138+ message : format ! ( "OpenStudio Bridge v{} started" , env!( "CARGO_PKG_VERSION" ) ) ,
139+ } ) . await ;
140+ }
141+
142+ // Spawn WebSocket server in a native thread with its own tokio runtime
143+ // This avoids Send requirements (AppState contains non-Send cpal::Stream)
144+ // and allows the TUI's blocking I/O to not starve the server
145+ let server_state = state. clone ( ) ;
146+ std:: thread:: Builder :: new ( )
147+ . name ( "websocket-server" . to_string ( ) )
148+ . spawn ( move || {
149+ let rt = tokio:: runtime:: Builder :: new_current_thread ( )
150+ . enable_all ( )
151+ . build ( )
152+ . expect ( "Failed to create WebSocket server runtime" ) ;
153+
154+ rt. block_on ( async move {
155+ if let Err ( e) = protocol:: run_server ( "127.0.0.1:9999" , server_state) . await {
156+ tracing:: error!( "WebSocket server error: {}" , e) ;
157+ }
158+ } ) ;
159+ } )
160+ . expect ( "Failed to spawn WebSocket server thread" ) ;
161+
162+ // Small delay to ensure server is listening before TUI takes over terminal
163+ tokio:: time:: sleep ( tokio:: time:: Duration :: from_millis ( 100 ) ) . await ;
164+
165+ // Run TUI on main thread (uses blocking terminal I/O)
166+ if let Err ( e) = tui:: run ( app, tui_rx) . await {
167+ tracing:: error!( "TUI error: {}" , e) ;
168+ }
161169 } else {
162170 // Run headless mode
163171 info ! ( "Bridge running on ws://localhost:9999 (headless mode)" ) ;
0 commit comments