p2p_chat/ui/runner/
mod.rs

1//! This module contains the main logic for running the terminal user interface (TUI).
2use super::{TerminalUI, UIAction, UIEvent};
3use crate::cli::commands::{Node, UiNotification};
4use crate::logging::{LogBuffer, TUILogCollector};
5use anyhow::Result;
6use crossterm::event::{self, Event};
7use std::sync::Arc;
8use std::time::Duration;
9use tokio::sync::mpsc;
10use tracing::{debug, error, info};
11
12mod actions;
13use actions::handle_ui_action;
14
15/// Initializes and runs the terminal user interface.
16///
17/// This function sets up the logging, UI communication channels,
18/// initializes the `TerminalUI`, and spawns various background tasks
19/// for handling events, actions, and network notifications.
20///
21/// # Arguments
22///
23/// * `node` - A shared reference to the application's core `Node`.
24/// * `ui_notify_rx` - Receiver for UI notifications from other parts of the application.
25/// * `web_port` - The port on which the web UI is running.
26///
27/// # Errors
28///
29/// Returns an error if the terminal UI fails to run.
30pub async fn run_tui(
31    node: Arc<Node>,
32    mut ui_notify_rx: tokio::sync::mpsc::UnboundedReceiver<UiNotification>,
33    web_port: u16,
34) -> Result<()> {
35    info!("🚀 Starting P2P Messenger TUI");
36
37    // Initialize log buffer and collector.
38    let log_buffer = Arc::new(LogBuffer::new(10000));
39
40    // Set up TUI log collector.
41    if let Err(e) = TUILogCollector::init_subscriber(log_buffer.clone()) {
42        debug!("Failed to initialize TUI log collector: {}", e);
43    }
44
45    // Create channels for UI communication.
46    let (ui_event_tx, ui_event_rx) = mpsc::unbounded_channel::<UIEvent>();
47    let (ui_action_tx, mut ui_action_rx) = mpsc::unbounded_channel::<UIAction>();
48
49    // Connect log buffer to UI events.
50    log_buffer.set_ui_sender(ui_event_tx.clone());
51
52    // Send web UI notification to chat.
53    let _ = ui_event_tx.send(UIEvent::ChatMessage(format!(
54        "🌐 Web UI available at: http://127.0.0.1:{}",
55        web_port
56    )));
57
58    // Initialize terminal UI.
59    let mut terminal_ui = TerminalUI::new(ui_event_rx, ui_action_tx.clone());
60    terminal_ui.set_node(node.clone());
61    terminal_ui.set_log_buffer(log_buffer.clone());
62
63    const INITIAL_HISTORY_LIMIT: usize = 10;
64    if let Ok(initial_messages) = node
65        .history
66        .get_recent_messages(&node.identity.peer_id, INITIAL_HISTORY_LIMIT)
67        .await
68    {
69        terminal_ui.preload_messages(initial_messages);
70    }
71
72    // Load friends for autocompletion.
73    let friends = match node.friends.list_friends().await {
74        Ok(friends_list) => friends_list
75            .into_iter()
76            .filter_map(|f| f.nickname.or_else(|| Some(f.peer_id.to_string())))
77            .collect(),
78        Err(e) => {
79            debug!("Failed to load friends for autocompletion: {}", e);
80            Vec::new()
81        }
82    };
83
84    terminal_ui.update_friends(friends);
85
86    // Spawn terminal event handler.
87    let ui_event_tx_clone = ui_event_tx.clone();
88    tokio::spawn(async move {
89        loop {
90            if event::poll(Duration::from_millis(100)).unwrap_or(false) {
91                match event::read() {
92                    Ok(Event::Key(key_event)) => {
93                        if let Err(e) = ui_event_tx_clone.send(UIEvent::KeyPress(key_event)) {
94                            debug!("Failed to send key event: {}", e);
95                            break;
96                        }
97                    }
98                    Ok(Event::Resize(width, height)) => {
99                        if let Err(e) = ui_event_tx_clone.send(UIEvent::Resize(width, height)) {
100                            debug!("Failed to send resize event: {}", e);
101                            break;
102                        }
103                    }
104                    _ => {}
105                }
106            }
107        }
108    });
109
110    // Spawn UI action handler.
111    let node_clone = node.clone();
112    let ui_event_tx_actions = ui_event_tx.clone();
113    tokio::spawn(async move {
114        while let Some(action) = ui_action_rx.recv().await {
115            if let Err(e) = handle_ui_action(action, &node_clone, ui_event_tx_actions.clone()).await
116            {
117                error!("Failed to dispatch UI action: {}", e);
118            }
119        }
120    });
121
122    // Spawn UI notification handler (for incoming messages and peer events).
123    let ui_event_tx_notifications = ui_event_tx.clone();
124    let node_for_notifications = node.clone();
125    tokio::spawn(async move {
126        while let Some(notification) = ui_notify_rx.recv().await {
127            match notification {
128                UiNotification::NewMessage(message) => {
129                    if let Err(e) = ui_event_tx_notifications.send(UIEvent::NewMessage(message)) {
130                        debug!("Failed to send new message event: {}", e);
131                        break;
132                    }
133                }
134                UiNotification::PeerConnected(_) | UiNotification::PeerDisconnected(_) => {
135                    // Update peers count immediately.
136                    if let Ok(peers) = node_for_notifications.network.get_connected_peers().await {
137                        let _ = ui_event_tx_notifications.send(UIEvent::UpdatePeersCount(peers.len()));
138                        let peer_strings: Vec<String> =
139                            peers.iter().map(|p| p.to_string()).collect();
140                        let _ =
141                            ui_event_tx_notifications.send(UIEvent::UpdateDiscoveredPeers(peer_strings));
142                    }
143                }
144                UiNotification::DeliveryStatusUpdate { .. } => {
145                    // Web UI only notification, CLI doesn't need this.
146                }
147            }
148        }
149    });
150
151    // Spawn periodic peers updater (for autocomplete).
152    let ui_event_tx_peers = ui_event_tx.clone();
153    let node_peers = node.clone();
154    tokio::spawn(async move {
155        let mut interval = tokio::time::interval(std::time::Duration::from_secs(30));
156        loop {
157            interval.tick().await;
158            match node_peers.network.get_connected_peers().await {
159                Ok(peers) => {
160                    let _ = ui_event_tx_peers.send(UIEvent::UpdatePeersCount(peers.len()));
161                    let peer_strings: Vec<String> = peers.iter().map(|p| p.to_string()).collect();
162                    let _ = ui_event_tx_peers.send(UIEvent::UpdateDiscoveredPeers(peer_strings));
163                }
164                Err(_) => {
165                    // Ignore errors, will try again next interval.
166                }
167            }
168        }
169    });
170
171    // Load initial friends for autocompletion.
172    let friends = match node.friends.list_friends().await {
173        Ok(friends_list) => friends_list
174            .into_iter()
175            .filter_map(|f| f.nickname.or_else(|| Some(f.peer_id.to_string())))
176            .collect(),
177        Err(e) => {
178            debug!("Failed to load friends for autocompletion: {}", e);
179            Vec::new()
180        }
181    };
182
183    info!(
184        "TUI initialized with {} friends for autocompletion",
185        friends.len()
186    );
187
188    // Update initial peers count and discovered peers.
189    match node.network.get_connected_peers().await {
190        Ok(peers) => {
191            let _ = ui_event_tx.send(UIEvent::UpdatePeersCount(peers.len()));
192            let peer_strings: Vec<String> = peers.iter().map(|p| p.to_string()).collect();
193            let _ = ui_event_tx.send(UIEvent::UpdateDiscoveredPeers(peer_strings));
194        }
195        Err(_) => {
196            // If we can't get peers count, default to 0 which is already set.
197        }
198    }
199
200    // Run the terminal UI.
201    terminal_ui.run().await
202}