@@ -756,6 +756,8 @@ async fn handle_tui(matches: &ArgMatches) -> Result<()> {
756756 use ratatui:: { backend:: CrosstermBackend , Terminal } ;
757757 use std:: io;
758758 use std:: time:: Duration ;
759+ use tokio:: sync:: mpsc;
760+ use crate :: utils:: bbs:: meshtastic:: { MeshtasticClient , MeshtasticPacket } ;
759761
760762 let initial_board = matches. get_one :: < String > ( "board" ) . unwrap ( ) ;
761763 let mesh_address = matches. get_one :: < String > ( "mesh" ) ;
@@ -769,9 +771,29 @@ async fn handle_tui(matches: &ArgMatches) -> Result<()> {
769771 }
770772
771773 // Try to connect to Meshtastic if --mesh flag provided
774+ let mut mesh_rx: Option < mpsc:: UnboundedReceiver < MeshtasticPacket > > = None ;
772775 if let Some ( addr) = mesh_address {
773776 println ! ( "📻 Connecting to Meshtastic @ {}..." , addr) ;
774- bbs_state. try_connect_meshtastic ( Some ( addr) ) ;
777+
778+ // Try async connection
779+ if let Some ( mut client) = MeshtasticClient :: from_address ( addr) {
780+ match client. connect_and_run ( ) . await {
781+ Ok ( rx) => {
782+ bbs_state. agent_status . meshtastic_connected = true ;
783+ bbs_state. agent_status . meshtastic_node_id = Some ( format ! ( "!{:08x}" , client. our_node_id( ) ) ) ;
784+ bbs_state. status_message = format ! ( "📻 Connected to mesh @ {}" , addr) ;
785+ mesh_rx = Some ( rx) ;
786+ println ! ( "✅ Meshtastic connected!" ) ;
787+ }
788+ Err ( e) => {
789+ println ! ( "⚠️ Meshtastic connection failed: {}" , e) ;
790+ // Fall back to sync wrapper for status display only
791+ bbs_state. try_connect_meshtastic ( Some ( addr) ) ;
792+ }
793+ }
794+ } else {
795+ bbs_state. try_connect_meshtastic ( Some ( addr) ) ;
796+ }
775797 }
776798
777799 // Find and select the initial board
@@ -789,8 +811,8 @@ async fn handle_tui(matches: &ArgMatches) -> Result<()> {
789811 let backend = CrosstermBackend :: new ( stdout) ;
790812 let mut terminal = Terminal :: new ( backend) ?;
791813
792- // Main event loop
793- let result = run_bbs_tui ( & mut terminal, & mut bbs_state) ;
814+ // Main event loop with async mesh support
815+ let result = run_bbs_tui_async ( & mut terminal, & mut bbs_state, mesh_rx ) . await ;
794816
795817 // Restore terminal
796818 disable_raw_mode ( ) ?;
@@ -894,6 +916,137 @@ fn run_bbs_tui<B: ratatui::backend::Backend>(
894916 }
895917}
896918
919+ /// Async BBS TUI event loop with Meshtastic support
920+ async fn run_bbs_tui_async < B : ratatui:: backend:: Backend > (
921+ terminal : & mut ratatui:: Terminal < B > ,
922+ state : & mut crate :: utils:: bbs:: tui_widgets:: BBSTuiState ,
923+ mut mesh_rx : Option < tokio:: sync:: mpsc:: UnboundedReceiver < crate :: utils:: bbs:: meshtastic:: MeshtasticPacket > > ,
924+ ) -> Result < ( ) > {
925+ use crossterm:: event:: { self , Event , KeyCode , KeyModifiers } ;
926+ use std:: time:: Duration ;
927+ use crate :: utils:: bbs:: meshtastic:: { MeshtasticPacket , BBSCommandRouter } ;
928+
929+ loop {
930+ // Draw UI
931+ terminal. draw ( |f| {
932+ crate :: utils:: bbs:: tui_widgets:: render_bbs_tab ( f, f. area ( ) , state) ;
933+ } ) ?;
934+
935+ // Check for mesh messages (non-blocking)
936+ if let Some ( ref mut rx) = mesh_rx {
937+ // Try to receive without blocking
938+ while let Ok ( packet) = rx. try_recv ( ) {
939+ match packet {
940+ MeshtasticPacket :: TextMessage { from, message, .. } => {
941+ // Check if it's a BBS command
942+ let is_command = message. trim ( ) . starts_with ( '/' ) ;
943+
944+ // Add to mesh messages
945+ state. add_mesh_message ( from, message. clone ( ) , is_command) ;
946+
947+ // If it's a command, process it
948+ if is_command {
949+ if let Some ( cmd) = BBSCommandRouter :: parse_command ( & message) {
950+ state. status_message = format ! ( "📻 Cmd from !{:08x}: {:?}" , from, cmd) ;
951+ }
952+ } else {
953+ state. status_message = format ! ( "📻 Msg from !{:08x}: {}" , from,
954+ if message. len( ) > 30 { format!( "{}..." , & message[ ..30 ] ) } else { message } ) ;
955+ }
956+ }
957+ MeshtasticPacket :: NodeInfo { node_id, short_name, long_name } => {
958+ state. update_mesh_node ( node_id, short_name. clone ( ) ) ;
959+ state. status_message = format ! ( "📻 Node: !{:08x} = {}" , node_id, short_name) ;
960+ }
961+ _ => { }
962+ }
963+ }
964+ }
965+
966+ // Handle keyboard input (with timeout for responsiveness)
967+ if event:: poll ( Duration :: from_millis ( 50 ) ) ? {
968+ if let Event :: Key ( key) = event:: read ( ) ? {
969+ // Handle input mode first (when input is active, capture all chars)
970+ if state. input_active {
971+ match key. code {
972+ KeyCode :: Enter => {
973+ if !state. input_buffer . trim ( ) . is_empty ( ) {
974+ if let Err ( e) = state. post_message ( & state. input_buffer . clone ( ) ) {
975+ state. status_message = format ! ( "Error: {}" , e) ;
976+ } else {
977+ state. status_message = "Message posted!" . to_string ( ) ;
978+ let _ = state. load_posts ( ) ;
979+ }
980+ state. input_buffer . clear ( ) ;
981+ }
982+ state. input_active = false ;
983+ }
984+ KeyCode :: Esc => {
985+ state. input_buffer . clear ( ) ;
986+ state. input_active = false ;
987+ }
988+ KeyCode :: Backspace => {
989+ state. input_buffer . pop ( ) ;
990+ }
991+ KeyCode :: Char ( c) if !key. modifiers . contains ( KeyModifiers :: CONTROL ) => {
992+ state. input_buffer . push ( c) ;
993+ }
994+ _ => { }
995+ }
996+ continue ;
997+ }
998+
999+ // Not in input mode - handle navigation
1000+ match key. code {
1001+ KeyCode :: Char ( 'q' ) | KeyCode :: Esc => {
1002+ return Ok ( ( ) ) ;
1003+ }
1004+ KeyCode :: Char ( 'i' ) => {
1005+ state. input_active = true ;
1006+ }
1007+ KeyCode :: Char ( 'j' ) | KeyCode :: Down => {
1008+ state. scroll_offset = state. scroll_offset . saturating_add ( 1 ) ;
1009+ }
1010+ KeyCode :: Char ( 'k' ) | KeyCode :: Up => {
1011+ state. scroll_offset = state. scroll_offset . saturating_sub ( 1 ) ;
1012+ }
1013+ KeyCode :: Char ( 'r' ) => {
1014+ let _ = state. refresh_boards ( ) ;
1015+ let _ = state. refresh_agents ( ) ;
1016+ if state. current_board . is_some ( ) {
1017+ let _ = state. load_posts ( ) ;
1018+ }
1019+ state. status_message = "Refreshed" . to_string ( ) ;
1020+ }
1021+ KeyCode :: Char ( 'm' ) => {
1022+ // Toggle showing mesh messages in posts view (future feature)
1023+ state. status_message = format ! ( "📻 {} mesh messages" , state. mesh_messages. len( ) ) ;
1024+ }
1025+ KeyCode :: Char ( c @ '1' ..='9' ) => {
1026+ let board_idx = c. to_digit ( 10 ) . unwrap ( ) as usize - 1 ;
1027+ if board_idx < state. boards . len ( ) {
1028+ let board_id = state. boards [ board_idx] . id ;
1029+ let board_name = state. boards [ board_idx] . name . clone ( ) ;
1030+ state. current_board = Some ( board_id) ;
1031+ state. selected_board_index = Some ( board_idx) ;
1032+ let _ = state. load_posts ( ) ;
1033+ state. scroll_offset = 0 ;
1034+ state. status_message = format ! ( "Switched to: {}" , board_name) ;
1035+ }
1036+ }
1037+ KeyCode :: Char ( 'c' ) if key. modifiers . contains ( KeyModifiers :: CONTROL ) => {
1038+ return Ok ( ( ) ) ;
1039+ }
1040+ _ => { }
1041+ }
1042+ }
1043+ }
1044+
1045+ // Small yield to allow mesh receiver to process
1046+ tokio:: task:: yield_now ( ) . await ;
1047+ }
1048+ }
1049+
8971050/// Handle stats command
8981051async fn handle_stats ( matches : & ArgMatches ) -> Result < ( ) > {
8991052 let mut conn = db_err ( db:: establish_connection ( ) ) ?;
0 commit comments