cryptic_tui/
main.rs

1#![doc = include_str!("../README.md")]
2//!
3//! ## Module Organization
4//!
5//! The application is structured into several modules:
6//! - [`app`] - Application state and data structures
7//! - [`cli`] - Command-line argument parsing
8//! - [`erlang`] - Erlang node connectivity, RPC, and message history loading
9//! - [`dist_node`] - Distributed Erlang message receiver
10//! - [`formatting`] - Emoji and markdown text formatting
11//! - [`ui`] - Terminal UI rendering
12
13// Note: The main README content above is included via include_str!
14// This section adds module-specific navigation for rustdoc
15
16//! ```bash
17//! tail -f ~/.cryptic/$CRYPTIC_USERNAME/${CRYPTIC_SERVER_HOST}_${CRYPTIC_SERVER_PORT}/logs/cryptic-tui.log.$(date +%Y-%m-%d)
18//! ```
19
20mod app;
21mod cli;
22mod dist_node;
23mod erlang;
24mod formatting;
25mod ui;
26
27use anyhow::Result;
28use app::{App, AppScreen, Secret, Tab, User};
29use cli::CliArgs;
30use crossterm::{
31    event::{self, Event, KeyCode, KeyEventKind, KeyModifiers},
32    execute,
33    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
34};
35use erlang::{ConnectionStatus, ErlangConnection};
36use ratatui::{backend::CrosstermBackend, Terminal};
37use std::{
38    io,
39    sync::Arc,
40    time::{Duration, Instant},
41};
42use tracing::{error, info, warn};
43
44/// Handle events received from Erlang event bus
45fn handle_erlang_event(
46    app: &mut App,
47    json_str: &str,
48    _refresh_online_users: &mut bool,
49    refresh_detailed_users: &mut bool,
50) {
51    match serde_json::from_str::<serde_json::Value>(json_str) {
52        Ok(event) => {
53            // Check event type
54            if let Some(event_type) = event.get("type").and_then(|t| t.as_str()) {
55                info!("Handling event type: {}", event_type);
56
57                match event_type {
58                    "websocket_message" => {
59                        // Extract nested message
60                        if let Some(message) = event.get("message") {
61                            if let Some(msg_type) = message.get("type").and_then(|t| t.as_str()) {
62                                match msg_type {
63                                    "online_users" => {
64                                        // Extract users array and update online status
65                                        if let Some(users) =
66                                            message.get("users").and_then(|u| u.as_array())
67                                        {
68                                            let online_users: Vec<String> = users
69                                                .iter()
70                                                .filter_map(|u| u.as_str().map(|s| s.to_string()))
71                                                .collect();
72                                            info!("Updating online users: {:?}", online_users);
73                                            app.update_online_users(online_users);
74                                            // Receiving online_users means server is up
75                                            app.server_connection_up = true;
76                                        }
77                                    }
78                                    "user_registered" => {
79                                        // Handle successful user registration notification
80                                        let gpg_fp = message
81                                            .get("gpg_fp")
82                                            .and_then(|v| v.as_str())
83                                            .unwrap_or("unknown");
84                                        let registered_by = message
85                                            .get("registered_by")
86                                            .and_then(|v| v.as_str())
87                                            .unwrap_or("unknown");
88                                        
89                                        info!("User registered: {} by {}", gpg_fp, registered_by);
90                                        app.set_status_message(format!(
91                                            "✓ User registered successfully: {}",
92                                            &gpg_fp[gpg_fp.len().saturating_sub(16)..]
93                                        ));
94                                        
95                                        // Clear the form and refresh user list
96                                        app.admin_clear_inputs();
97                                        *refresh_detailed_users = true;
98                                    }
99                                    "error" => {
100                                        // Extract and display error message
101                                        if let Some(error_msg) =
102                                            message.get("message").and_then(|m| m.as_str())
103                                        {
104                                            error!("Server error: {}", error_msg);
105                                            app.set_status_message(format!("Error: {}", error_msg));
106                                        }
107                                    }
108                                    _ => {
109                                        info!("Unhandled websocket_message type: {}", msg_type);
110                                    }
111                                }
112                            }
113                        }
114                    }
115                    "ca_response" => {
116                        // Handle Cryptic Admin responses
117                        // Receiving any ca_response means server is up
118                        app.server_connection_up = true;
119                        
120                        if let Some(response) = event.get("response") {
121                            if let Some(resp_type) = response.get("type").and_then(|t| t.as_str()) {
122                                match resp_type {
123                                    "list_users_response" => {
124                                        info!("Received list_users_response in ca_response");
125
126                                        // Convert the response to JSON string for parsing
127                                        if let Ok(json_str) = serde_json::to_string(response) {
128                                            info!("User list response JSON: {}", json_str);
129                                            match parse_list_users(&json_str) {
130                                                Ok(users) => {
131                                                    let user_count = users.len();
132                                                    app.update_detailed_users(users);
133                                                    info!("User list updated successfully with {} users", user_count);
134                                                }
135                                                Err(e) => {
136                                                    error!(
137                                                        "Failed to parse user list response: {}",
138                                                        e
139                                                    );
140                                                }
141                                            }
142                                        } else {
143                                            error!("Failed to serialize response to JSON");
144                                        }
145                                    }
146                                    "register_user_response" => {
147                                        if let Some(status) =
148                                            response.get("status").and_then(|s| s.as_str())
149                                        {
150                                            let gpg_fp = response
151                                                .get("gpg_fp")
152                                                .and_then(|v| v.as_str())
153                                                .unwrap_or("unknown");
154                                            if status == "success" {
155                                                info!("User registration succeeded for {}", gpg_fp);
156                                                app.set_status_message(format!(
157                                                    "Registration succeeded for {}",
158                                                    gpg_fp
159                                                ));
160                                                app.admin_clear_inputs();
161                                                *refresh_detailed_users = true;
162                                            } else {
163                                                let message = response
164                                                    .get("message")
165                                                    .and_then(|v| v.as_str())
166                                                    .unwrap_or("unknown error");
167                                                error!(
168                                                    "Registration failed for {}: {}",
169                                                    gpg_fp, message
170                                                );
171                                                app.set_status_message(format!(
172                                                    "Registration failed for {}: {}",
173                                                    gpg_fp, message
174                                                ));
175                                            }
176                                        }
177                                    }
178                                    "list_certificates_response" => {
179                                        info!("Received list_certificates_response");
180                                        if let Ok(json_str) = serde_json::to_string(response) {
181                                            info!("Certificate list response JSON: {}", json_str);
182                                            match parse_certificates(&json_str) {
183                                                Ok(certs) => {
184                                                    let cert_count = certs.len();
185                                                    app.certificates = Some(certs);
186                                                    info!("Certificate list updated with {} certificates", cert_count);
187                                                    app.set_status_message(format!("Loaded {} certificate(s)", cert_count));
188                                                }
189                                                Err(e) => {
190                                                    error!("Failed to parse certificate list: {}", e);
191                                                    app.set_status_message(format!("Failed to parse certificates: {}", e));
192                                                }
193                                            }
194                                        } else {
195                                            error!("Failed to serialize certificate response to JSON");
196                                        }
197                                    }
198                                    _ => {
199                                        info!("Unhandled ca_response type: {}", resp_type);
200                                    }
201                                }
202                            }
203                        }
204                    }
205                    "deliver_message" => {
206                        // Extract message details
207                        if let (Some(from), Some(message), Some(timestamp)) = (
208                            event.get("from").and_then(|f| f.as_str()),
209                            event.get("message").and_then(|m| m.as_str()),
210                            event.get("timestamp").and_then(|t| t.as_str()),
211                        ) {
212                            // Receiving messages means server is up
213                            app.server_connection_up = true;
214                            
215                            info!(
216                                "Received message from {}: {} (timestamp: {})",
217                                from, message, timestamp
218                            );
219
220                            // Check current message count before adding
221                            let count_before = app.messages.get(from).map(|v| v.len()).unwrap_or(0);
222
223                            // Add message to chat history
224                            app.receive_message(from, message);
225
226                            let count_after = app.messages.get(from).map(|v| v.len()).unwrap_or(0);
227                            info!(
228                                "Message count for {}: {} -> {}",
229                                from, count_before, count_after
230                            );
231
232                            // If this is the current peer, reset scroll to show new message
233                            if app.current_peer.as_deref() == Some(from) {
234                                if let Some(conv) = app.conversations.get_mut(from) {
235                                    conv.scroll_offset = 0; // Jump to bottom to show new message
236                                }
237                            }
238
239                            // Mark sender as online immediately (but not ourselves)
240                            let current_username = app.get_current_username();
241                            if Some(from) != current_username {
242                                if let Some(user) = app.users.iter_mut().find(|u| u.name == from) {
243                                    user.online = true;
244                                    info!("Marked {} as online", from);
245                                } else {
246                                    // Add new user if not in list
247                                    app.users.push(User {
248                                        name: from.to_string(),
249                                        online: true,
250                                    });
251                                    info!("Added new user {} as online", from);
252                                }
253                            }
254
255                            // If no current peer, or message is from a different peer,
256                            // switch to the sender to show the new message
257                            if app.current_peer.is_none()
258                                || app.current_peer.as_deref() != Some(from)
259                            {
260                                info!("Switching to peer: {}", from);
261                                app.current_peer = Some(from.to_string());
262
263                                // Update selected_user to match the sender
264                                if let Some(index) = app.users.iter().position(|u| u.name == from) {
265                                    app.selected_user = index;
266                                }
267                            }
268                        } else {
269                            error!("deliver_message event missing required fields");
270                        }
271                    }
272                    "system_message" => {
273                        info!("system_message event: {:?}", event);
274                        
275                        // Extract sys_code if present (as string or byte array)
276                        let sys_code = if let Some(code) = event.get("sys_code").and_then(|c| c.as_str()) {
277                            Some(code.to_string())
278                        } else if let Some(bytes) = event.get("sys_code").and_then(|c| c.as_array()) {
279                            let byte_vec: Vec<u8> = bytes
280                                .iter()
281                                .filter_map(|v| v.as_u64().map(|n| n as u8))
282                                .collect();
283                            String::from_utf8(byte_vec).ok()
284                        } else {
285                            None
286                        };
287                        
288                        // Try to extract message as string first, then as byte array
289                        let msg_text = if let Some(msg) = event.get("message").and_then(|m| m.as_str()) {
290                            Some(msg.to_string())
291                        } else if let Some(bytes) = event.get("message").and_then(|m| m.as_array()) {
292                            // Message came as array of byte values (from Erlang binary)
293                            let byte_vec: Vec<u8> = bytes
294                                .iter()
295                                .filter_map(|v| v.as_u64().map(|n| n as u8))
296                                .collect();
297                            
298                            String::from_utf8(byte_vec).ok()
299                        } else {
300                            None
301                        };
302                        
303                        if let Some(msg) = msg_text {
304                            info!("System message extracted: {} (sys_code: {:?})", msg, sys_code);
305                            app.add_system_message(msg, sys_code);
306                        } else {
307                            error!("Failed to extract message field from system_message event");
308                        }
309                    }
310                    _ => {
311                        info!("Unhandled event type: {}", event_type);
312                    }
313                }
314            }
315        }
316        Err(e) => {
317            error!("Failed to parse event JSON: {}", e);
318        }
319    }
320}
321
322/// Parse engine status from JSON response
323fn parse_engine_status(json_str: &str) -> Result<app::EngineStatus> {
324    use app::{EngineStatus, SessionDetail};
325
326    let value: serde_json::Value = serde_json::from_str(json_str)?;
327
328    let username = value
329        .get("username")
330        .and_then(|v| v.as_str())
331        .unwrap_or("unknown")
332        .to_string();
333
334    let active_sessions = value
335        .get("active_sessions")
336        .and_then(|v| v.as_u64())
337        .unwrap_or(0) as u32;
338
339    let message_count = value
340        .get("message_count")
341        .and_then(|v| v.as_u64())
342        .unwrap_or(0) as u32;
343
344    let error_count = value
345        .get("error_count")
346        .and_then(|v| v.as_u64())
347        .unwrap_or(0) as u32;
348
349    let uptime_ms = value.get("uptime").and_then(|v| v.as_u64()).unwrap_or(0);
350
351    let mut session_details = Vec::new();
352
353    if let Some(sessions) = value.get("session_details").and_then(|v| v.as_array()) {
354        for session in sessions {
355            let detail = SessionDetail {
356                peer_username: session
357                    .get("peer_username")
358                    .and_then(|v| v.as_str())
359                    .unwrap_or("unknown")
360                    .to_string(),
361                message_count: session
362                    .get("message_count")
363                    .and_then(|v| v.as_u64())
364                    .unwrap_or(0) as u32,
365                error_count: session
366                    .get("error_count")
367                    .and_then(|v| v.as_u64())
368                    .unwrap_or(0) as u32,
369                dh_ratchet_step: session
370                    .get("dh_ratchet_step")
371                    .and_then(|v| v.as_u64())
372                    .unwrap_or(0) as u32,
373                send_msg_number: session
374                    .get("send_msg_number")
375                    .and_then(|v| v.as_u64())
376                    .unwrap_or(0) as u32,
377                recv_msg_number: session
378                    .get("recv_msg_number")
379                    .and_then(|v| v.as_u64())
380                    .unwrap_or(0) as u32,
381                prev_recv_chain_length: session
382                    .get("prev_recv_chain_length")
383                    .and_then(|v| v.as_u64())
384                    .unwrap_or(0) as u32,
385                skipped_keys_count: session
386                    .get("skipped_keys_count")
387                    .and_then(|v| v.as_u64())
388                    .unwrap_or(0) as u32,
389                current_state: session
390                    .get("current_state")
391                    .and_then(|v| v.as_str())
392                    .unwrap_or("unknown")
393                    .to_string(),
394                transition_count: session
395                    .get("transition_count")
396                    .and_then(|v| v.as_u64())
397                    .unwrap_or(0) as u32,
398                created_at: session
399                    .get("created_at")
400                    .and_then(|v| v.as_u64())
401                    .unwrap_or(0),
402                last_updated: session
403                    .get("last_updated")
404                    .and_then(|v| v.as_u64())
405                    .unwrap_or(0),
406                receiving_chain_active: session
407                    .get("receiving_chain_active")
408                    .and_then(|v| v.as_bool())
409                    .unwrap_or(false),
410                sending_chain_active: session
411                    .get("sending_chain_active")
412                    .and_then(|v| v.as_bool())
413                    .unwrap_or(false),
414                has_remote_dh: session
415                    .get("has_remote_dh")
416                    .and_then(|v| v.as_bool())
417                    .unwrap_or(false),
418            };
419            session_details.push(detail);
420        }
421    }
422
423    // Parse certificate renewal info - it's at the top level, not nested
424    let cert_expires_at = value
425        .get("cert_expires_at")
426        .and_then(|v| v.as_i64());
427    
428    let cert_renewal_in_progress = value
429        .get("renewal_in_progress")
430        .and_then(|v| v.as_bool())
431        .unwrap_or(false);
432    
433    let cert_time_until_expiry = value
434        .get("time_until_expiry")
435        .and_then(|v| v.as_i64());
436
437    Ok(EngineStatus {
438        username,
439        active_sessions,
440        message_count,
441        error_count,
442        uptime_ms,
443        session_details,
444        cert_expires_at,
445        cert_renewal_in_progress,
446        cert_time_until_expiry,
447    })
448}
449
450/// Parse list_users response from JSON
451fn parse_list_users(json_str: &str) -> Result<Vec<app::DetailedUser>> {
452    use app::{DetailedUser, UserMetadata};
453
454    let value: serde_json::Value = serde_json::from_str(json_str)?;
455
456    let mut users = Vec::new();
457
458    if let Some(user_array) = value.get("users").and_then(|v| v.as_array()) {
459        for user in user_array {
460            let username = user
461                .get("username")
462                .and_then(|v| v.as_str())
463                .unwrap_or("unknown")
464                .to_string();
465
466            let gpg_fp = user
467                .get("gpg_fp")
468                .and_then(|v| v.as_str())
469                .unwrap_or("")
470                .to_string();
471
472            let online = user
473                .get("online")
474                .and_then(|v| v.as_bool())
475                .unwrap_or(false);
476
477            let status = user
478                .get("status")
479                .and_then(|v| v.as_str())
480                .unwrap_or("unknown")
481                .to_string();
482
483            let registered_at = user
484                .get("registered_at")
485                .and_then(|v| v.as_u64())
486                .unwrap_or(0);
487
488            let last_seen = user.get("last_seen").and_then(|v| v.as_u64()).unwrap_or(0);
489
490            let registered_by = user
491                .get("registered_by")
492                .and_then(|v| v.as_str())
493                .unwrap_or("undefined")
494                .to_string();
495
496            // Parse optional metadata - extract all key-value pairs
497            let metadata = user.get("metadata").and_then(|m| {
498                if let Some(obj) = m.as_object() {
499                    let mut fields = std::collections::HashMap::new();
500                    for (key, value) in obj {
501                        if let Some(val_str) = value.as_str() {
502                            fields.insert(key.clone(), val_str.to_string());
503                        } else {
504                            // Handle non-string values by converting to string
505                            fields.insert(key.clone(), value.to_string());
506                        }
507                    }
508                    if !fields.is_empty() {
509                        Some(UserMetadata { fields })
510                    } else {
511                        None
512                    }
513                } else {
514                    None
515                }
516            });
517
518            users.push(DetailedUser {
519                username,
520                gpg_fp,
521                online,
522                status,
523                registered_at,
524                last_seen,
525                registered_by,
526                metadata,
527            });
528        }
529    }
530
531    Ok(users)
532}
533
534/// Parse certificate list from JSON response
535fn parse_certificates(json_str: &str) -> Result<Vec<app::Certificate>> {
536    use app::Certificate;
537
538    let value: serde_json::Value = serde_json::from_str(json_str)?;
539
540    let mut certificates = Vec::new();
541
542    if let Some(cert_array) = value.get("certificates").and_then(|v| v.as_array()) {
543        for cert in cert_array {
544            let serial = cert
545                .get("serial")
546                .and_then(|v| v.as_str())
547                .unwrap_or("")
548                .to_string();
549
550            let issued_at = cert
551                .get("issued_at")
552                .and_then(|v| v.as_i64())
553                .unwrap_or(0);
554
555            let expires_at = cert
556                .get("expires_at")
557                .and_then(|v| v.as_i64())
558                .unwrap_or(0);
559
560            let status = cert
561                .get("status")
562                .and_then(|v| v.as_str())
563                .unwrap_or("unknown")
564                .to_string();
565
566            let revoked_at = cert
567                .get("revoked_at")
568                .and_then(|v| v.as_i64());
569
570            let revoked_by = cert
571                .get("revoked_by")
572                .and_then(|v| v.as_str())
573                .map(|s| s.to_string());
574
575            let revoked_reason = cert
576                .get("revoked_reason")
577                .and_then(|v| v.as_str())
578                .map(|s| s.to_string());
579
580            certificates.push(Certificate {
581                serial,
582                issued_at,
583                expires_at,
584                status,
585                revoked_at,
586                revoked_by,
587                revoked_reason,
588            });
589        }
590    }
591
592    Ok(certificates)
593}
594
595/// Initialize logging to a file in `~/.cryptic/<username>/<server>_<port>/logs/`
596fn init_logging() -> Result<()> {
597    let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
598    
599    // Get connection info from environment variables set by bin/cryptic script
600    let username = std::env::var("CRYPTIC_USERNAME").unwrap_or_else(|_| "default".to_string());
601    let server_host = std::env::var("CRYPTIC_SERVER_HOST").unwrap_or_else(|_| "localhost".to_string());
602    let server_port = std::env::var("CRYPTIC_SERVER_PORT").unwrap_or_else(|_| "8443".to_string());
603    
604    let log_dir = format!("{}/.cryptic/{}/{}_{}/logs", home, username, server_host, server_port);
605    std::fs::create_dir_all(&log_dir)?;
606
607    let file_appender = tracing_appender::rolling::daily(&log_dir, "cryptic-tui.log");
608
609    tracing_subscriber::fmt()
610        .with_writer(file_appender)
611        .with_ansi(false)
612        .with_target(true)
613        .with_thread_ids(true)
614        .init();
615
616    info!("Cryptic TUI started - logging to {}", log_dir);
617    Ok(())
618}
619
620fn main() -> Result<()> {
621    // Initialize logging first
622    init_logging()?;
623
624    // Run the async main in smol runtime
625    smol::block_on(async_main())
626}
627
628async fn async_main() -> Result<()> {
629    // Parse CLI arguments
630    let args = CliArgs::parse_args();
631    info!(
632        "CLI arguments parsed: node={:?}, cookie_provided={}",
633        args.get_node_name(),
634        args.get_cookie().is_some()
635    );
636
637    // Initialize terminal UI
638    let mut terminal = init_terminal()?;
639
640    // Create app - use empty app if connecting to Erlang, mock data otherwise
641    let mut app = if args.get_node_name().is_some() {
642        App::new() // Empty app, users will come from Erlang
643    } else {
644        App::with_mock_data() // Mock data for testing without Erlang
645    };
646
647    // If node specified, store it for later connection
648    if let Some(node_name) = args.get_node_name() {
649        app.erlang_node_name = Some(node_name.to_string());
650        app.is_mock_mode = false;
651    }
652
653    // Create Erlang connection if node specified
654    let erlang_conn: Option<Arc<ErlangConnection>> = if let Some(node_name) = args.get_node_name() {
655        let cookie = args
656            .get_cookie()
657            .map(String::from)
658            .or_else(|| read_erlang_cookie().ok())
659            .unwrap_or_else(|| "nocookie".to_string());
660
661        info!("Creating Erlang connection to node: {}", node_name);
662        if args.get_cookie().is_some() {
663            info!("Using cookie from command line");
664        } else {
665            info!("Using cookie from ~/.erlang.cookie");
666        }
667
668        Some(Arc::new(ErlangConnection::new(
669            node_name.to_string(),
670            cookie,
671        )))
672    } else {
673        info!("No Erlang node specified, running in mock mode");
674        None
675    };
676
677    let res = run(&mut terminal, &mut app, erlang_conn).await;
678
679    // Always restore terminal state
680    restore_terminal()?;
681
682    info!("Cryptic TUI shutting down");
683
684    // Propagate any run errors
685    res
686}
687
688fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
689    enable_raw_mode()?;
690    let mut stdout = io::stdout();
691    execute!(stdout, EnterAlternateScreen)?;
692    let backend = CrosstermBackend::new(stdout);
693    Ok(Terminal::new(backend)?)
694}
695
696fn restore_terminal() -> Result<()> {
697    disable_raw_mode()?;
698    execute!(io::stdout(), LeaveAlternateScreen)?;
699    Ok(())
700}
701
702async fn run(
703    terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
704    app: &mut App,
705    erlang_conn: Option<Arc<ErlangConnection>>,
706) -> Result<()> {
707    let mut connected = false;
708    let mut dist_node: Option<dist_node::DistNode> = None;
709    let mut message_to_send: Option<(String, String)> = None; // (peer, message)
710    let mut admin_register_to_send: Option<(String, String, String)> = None;
711    let mut admin_suspend_to_send: Option<String> = None;
712    let mut admin_reactivate_to_send: Option<String> = None;
713    let mut admin_revoke_to_send: Option<String> = None;
714    let mut refresh_certificates = false; // Flag to trigger certificate list refresh
715    let mut certificates_fingerprint: Option<String> = None; // Fingerprint for certificate request
716    let mut refresh_engine_status = false; // Flag to trigger engine status refresh
717    let mut refresh_detailed_users = false; // Flag to trigger detailed user list refresh
718    let mut refresh_online_users = false; // Flag to trigger online users refresh
719    let mut last_online_check = Instant::now(); // Timer for periodic online users check
720    let online_check_interval = Duration::from_secs(10); // Check online users every 10 seconds
721
722    while !app.should_exit {
723        // Update system message display (check if we can advance to next message)
724        app.update_system_message();
725        
726        terminal.draw(|f| ui::draw(f, app))?;
727
728        // If just transitioned to Main screen and have Erlang connection, connect
729        if app.screen == AppScreen::Main && !connected && erlang_conn.is_some() {
730            if let Some(ref conn) = erlang_conn {
731                info!(
732                    "Attempting to connect to Erlang node: {}",
733                    conn.get_remote_node_name()
734                );
735                app.connection_status = ConnectionStatus::Connecting;
736                terminal.draw(|f| ui::draw(f, app))?; // Redraw to show "Connecting..."
737
738                match conn.connect().await {
739                    Ok(_) => {
740                        info!(
741                            "Successfully connected to Erlang node: {}",
742                            conn.get_remote_node_name()
743                        );
744                        app.connection_status = ConnectionStatus::Connected;
745                        connected = true;
746
747                        // Subscribe to event bus
748                        info!("Subscribing to event bus");
749                        match conn.subscribe_to_event_bus().await {
750                            Ok(_event_rx) => {
751                                info!("Successfully subscribed to event bus");
752                                // TODO Phase 3: Spawn task to handle events from event_rx
753                                // For Phase 2, events visible in logs as RegSend
754                            }
755                            Err(e) => {
756                                error!("Failed to subscribe to event bus: {}", e);
757                            }
758                        }
759
760                        // Start dist_node to receive RegSend messages
761                        info!("Starting distributed Erlang node for receiving messages");
762                        match dist_node::DistNode::connect(
763                            format!("cryptic_tui@{}", "127.0.0.1"),
764                            conn.get_remote_node_name().to_string(),
765                            conn.get_cookie().to_string(),
766                        )
767                        .await
768                        {
769                            Ok(node) => {
770                                info!("DistNode connected successfully: {}", node.node_name());
771                                let rust_node_name = node.node_name();
772                                dist_node = Some(node);
773
774                                // Start the bridge on the Erlang side to forward events to us
775                                info!("Starting cryptic_tui_bridge on Erlang node");
776
777                                // Get passphrase from app state
778                                let passphrase =
779                                    app.passphrase.as_ref().map(|s| s.as_str()).unwrap_or("");
780
781                                match conn.start_tui_bridge(&rust_node_name, passphrase).await {
782                                    Ok(bridge_result) => {
783                                        info!("Bridge started successfully: {:?}", bridge_result);
784
785                                        // Request the list of online users to populate the UI
786                                        info!("Requesting online users list");
787                                        match conn.request_online_users().await {
788                                            Ok(_) => {
789                                                info!("Online users request sent successfully");
790                                            }
791                                            Err(e) => {
792                                                error!("Failed to request online users: {}", e);
793                                            }
794                                        }
795                                    }
796                                    Err(e) => {
797                                        error!("Failed to start bridge: {}", e);
798                                        // Not fatal - continue anyway
799                                    }
800                                }
801                            }
802                            Err(e) => {
803                                error!("Failed to start DistNode: {}", e);
804                            }
805                        }
806                    }
807                    Err(e) => {
808                        error!("Failed to connect to Erlang node: {}", e);
809                        app.connection_status = ConnectionStatus::Error;
810                        app.splash_error = Some(format!("Connection failed: {}", e));
811                        // Optionally: fall back to mock mode or retry
812                    }
813                }
814            }
815        }
816
817        // Check for messages from dist_node
818        if let Some(ref mut node) = dist_node {
819            while let Ok(msg) = node.try_recv() {
820                use erl_dist::term::Term; // Use erl_dist's Term type
821
822                // Skip Tick messages (they're handled automatically)
823                if matches!(msg, dist_node::Message::Tick) {
824                    continue;
825                }
826
827                info!("Received message from Erlang: {:?}", msg);
828
829                // Parse RegSend messages to extract events
830                if let dist_node::Message::RegSend(regsend) = msg {
831                    // Message format: {tui_event, JsonBinary}
832                    if let Term::Tuple(tuple) = &regsend.message {
833                        if tuple.elements.len() == 2 {
834                            if let Term::Atom(atom) = &tuple.elements[0] {
835                                if atom.name == "tui_event" {
836                                    if let Term::Binary(binary) = &tuple.elements[1] {
837                                        // Convert bytes to string
838                                        match String::from_utf8(binary.bytes.clone()) {
839                                            Ok(json_str) => {
840                                                info!("Received TUI event JSON: {}", json_str);
841                                                handle_erlang_event(
842                                                    app,
843                                                    &json_str,
844                                                    &mut refresh_online_users,
845                                                    &mut refresh_detailed_users,
846                                                );
847                                            }
848                                            Err(e) => {
849                                                error!("Failed to decode UTF-8 from binary: {}", e)
850                                            }
851                                        }
852                                    }
853                                }
854                            }
855                        }
856                    }
857                }
858            }
859        }
860
861        // Poll for input with timeout to allow periodic redraws / future background tasks
862        if event::poll(Duration::from_millis(120))? {
863            if let Event::Key(key) = event::read()? {
864                // Ignore key repeats on some terminals
865                if key.kind == KeyEventKind::Repeat {
866                    continue;
867                }
868                if app.screen == AppScreen::Splash {
869                    handle_splash_key(app, key)?;
870                } else {
871                    // Check if user is sending a message
872                    if key.code == KeyCode::Enter
873                        && matches!(app.active_tab, Tab::Chat)
874                        && !app.input_buffer.is_empty()
875                        && app.current_peer.is_some()
876                    {
877                        // Capture message for sending
878                        if let Some(peer) = &app.current_peer {
879                            message_to_send = Some((peer.clone(), app.input_buffer.clone()));
880                        }
881                    }
882                    let (result, should_refresh_status, should_refresh_users, admin_action) =
883                        handle_main_key(app, key);
884                    result?;
885                    if should_refresh_status {
886                        refresh_engine_status = true;
887                    }
888                    if should_refresh_users {
889                        refresh_detailed_users = true;
890                    }
891                    match admin_action {
892                        Some(AdminAction::RegisterUser { gpg_fp, key_path, metadata }) => {
893                            admin_register_to_send = Some((gpg_fp, key_path, metadata));
894                        }
895                        Some(AdminAction::SuspendUser { gpg_fp }) => {
896                            admin_suspend_to_send = Some(gpg_fp);
897                        }
898                        Some(AdminAction::ReactivateUser { gpg_fp }) => {
899                            admin_reactivate_to_send = Some(gpg_fp);
900                        }
901                        Some(AdminAction::RevokeUser { gpg_fp }) => {
902                            admin_revoke_to_send = Some(gpg_fp);
903                        }
904                        Some(AdminAction::ListCertificates { gpg_fp }) => {
905                            // Store the fingerprint and request certificates
906                            if let Some(user) = app.admin_get_selected_user() {
907                                app.certificates_for_user = Some(user.username.clone());
908                            }
909                            refresh_certificates = true;
910                            certificates_fingerprint = Some(gpg_fp);
911                        }
912                        Some(AdminAction::RenewCertificate) => {
913                            // Trigger certificate renewal
914                            if let Some(ref conn) = erlang_conn {
915                                match conn.renew_certificate().await {
916                                    Ok(()) => {
917                                        app.set_status_message("Certificate renewal initiated - check system messages for progress".to_string());
918                                        // Refresh status to show renewal progress
919                                        refresh_engine_status = true;
920                                    }
921                                    Err(e) => {
922                                        app.set_status_message(format!("Failed to renew certificate: {}", e));
923                                    }
924                                }
925                            } else {
926                                app.set_status_message("Not connected to Erlang node".to_string());
927                            }
928                        }
929                        None => {}
930                    }
931                }
932            }
933        }
934
935        // Send message via Erlang if queued
936        if let Some((peer, message)) = message_to_send.take() {
937            if let Some(ref conn) = erlang_conn {
938                info!("Sending message to {}: {}", peer, message);
939                match send_message_via_erlang(conn, &peer, &message).await {
940                    Ok(_) => {
941                        info!("Message sent successfully to {}", peer);
942                    }
943                    Err(e) => {
944                        error!("Failed to send message to {}: {}", peer, e);
945                        // TODO: Show error in UI
946                    }
947                }
948            } else {
949                warn!("Cannot send message - no Erlang connection");
950            }
951        }
952
953        // Submit admin registration if requested
954        if let Some((gpg_fp, key_path, metadata)) = admin_register_to_send.take() {
955            if let Some(ref conn) = erlang_conn {
956                info!(
957                    "Submitting admin_register for fingerprint {} with key path {} and metadata '{}'",
958                    gpg_fp, key_path, metadata
959                );
960                app.set_status_message(format!("Registering {} (waiting for response)...", gpg_fp));
961                match conn.admin_register_user(&gpg_fp, &key_path, &metadata).await {
962                    Ok(_) => {
963                        info!("admin_register RPC accepted for {}", gpg_fp);
964                    }
965                    Err(e) => {
966                        error!("admin_register RPC failed: {}", e);
967                        app.set_status_message(format!("Registration failed: {}", e));
968                    }
969                }
970            } else {
971                warn!("Cannot register user - no Erlang connection");
972                app.set_status_message("Cannot register user: not connected".to_string());
973            }
974        }
975
976        // Submit admin suspend if requested
977        if let Some(gpg_fp) = admin_suspend_to_send.take() {
978            if let Some(ref conn) = erlang_conn {
979                info!("Submitting admin_suspend for fingerprint {}", gpg_fp);
980                match conn.admin_suspend_user(&gpg_fp).await {
981                    Ok(_) => {
982                        info!("admin_suspend RPC accepted for {}", gpg_fp);
983                        app.set_status_message(format!("User suspended successfully"));
984                        refresh_detailed_users = true;
985                    }
986                    Err(e) => {
987                        error!("admin_suspend RPC failed: {}", e);
988                        app.set_status_message(format!("Suspend failed: {}", e));
989                    }
990                }
991            } else {
992                warn!("Cannot suspend user - no Erlang connection");
993                app.set_status_message("Cannot suspend user: not connected".to_string());
994            }
995        }
996
997        // Submit admin reactivate if requested
998        if let Some(gpg_fp) = admin_reactivate_to_send.take() {
999            if let Some(ref conn) = erlang_conn {
1000                info!("Submitting admin_reactivate for fingerprint {}", gpg_fp);
1001                match conn.admin_reactivate_user(&gpg_fp).await {
1002                    Ok(_) => {
1003                        info!("admin_reactivate RPC accepted for {}", gpg_fp);
1004                        app.set_status_message(format!("User reactivated successfully"));
1005                        refresh_detailed_users = true;
1006                    }
1007                    Err(e) => {
1008                        error!("admin_reactivate RPC failed: {}", e);
1009                        app.set_status_message(format!("Reactivate failed: {}", e));
1010                    }
1011                }
1012            } else {
1013                warn!("Cannot reactivate user - no Erlang connection");
1014                app.set_status_message("Cannot reactivate user: not connected".to_string());
1015            }
1016        }
1017
1018        // Submit admin revoke if requested
1019        if let Some(gpg_fp) = admin_revoke_to_send.take() {
1020            if let Some(ref conn) = erlang_conn {
1021                info!("Submitting admin_revoke for fingerprint {}", gpg_fp);
1022                match conn.admin_revoke_user(&gpg_fp).await {
1023                    Ok(_) => {
1024                        info!("admin_revoke RPC accepted for {}", gpg_fp);
1025                        app.set_status_message(format!("User revoked successfully"));
1026                        refresh_detailed_users = true;
1027                    }
1028                    Err(e) => {
1029                        error!("admin_revoke RPC failed: {}", e);
1030                        app.set_status_message(format!("Revoke failed: {}", e));
1031                    }
1032                }
1033            } else {
1034                warn!("Cannot revoke user - no Erlang connection");
1035                app.set_status_message("Cannot revoke user: not connected".to_string());
1036            }
1037        }
1038
1039        // Periodic online users check (every 10 seconds)
1040        if connected
1041            && erlang_conn.is_some()
1042            && last_online_check.elapsed() >= online_check_interval
1043        {
1044            last_online_check = Instant::now();
1045            refresh_online_users = true;
1046            info!("Triggering periodic online users check");
1047        }
1048
1049        // Refresh engine status if requested
1050        if refresh_engine_status {
1051            refresh_engine_status = false;
1052            if let Some(ref conn) = erlang_conn {
1053                info!("Refreshing engine status");
1054                match conn.request_engine_status().await {
1055                    Ok(term) => {
1056                        // The response format is {ok, JsonBinary} from Erlang
1057                        match term {
1058                            eetf::Term::Tuple(tuple) if tuple.elements.len() == 2 => {
1059                                // Check if first element is 'ok' atom
1060                                if let eetf::Term::Atom(atom) = &tuple.elements[0] {
1061                                    if atom.name == "ok" {
1062                                        // Extract the binary from the second element
1063                                        if let eetf::Term::Binary(binary) = &tuple.elements[1] {
1064                                            match String::from_utf8(binary.bytes.clone()) {
1065                                                Ok(json_str) => {
1066                                                    info!(
1067                                                        "Received engine status JSON: {}",
1068                                                        json_str
1069                                                    );
1070                                                    match parse_engine_status(&json_str) {
1071                                                        Ok(status) => {
1072                                                            app.update_engine_status(status);
1073                                                            info!("Engine status updated successfully");
1074                                                        }
1075                                                        Err(e) => {
1076                                                            error!(
1077                                                                "Failed to parse engine status: {}",
1078                                                                e
1079                                                            );
1080                                                        }
1081                                                    }
1082                                                }
1083                                                Err(e) => {
1084                                                    error!(
1085                                                        "Failed to decode engine status UTF-8: {}",
1086                                                        e
1087                                                    );
1088                                                }
1089                                            }
1090                                        } else {
1091                                            error!(
1092                                                "Second element of tuple is not a binary: {:?}",
1093                                                tuple.elements[1]
1094                                            );
1095                                        }
1096                                    } else {
1097                                        error!("First element is not 'ok' atom: {:?}", atom.name);
1098                                    }
1099                                } else {
1100                                    error!(
1101                                        "First element of tuple is not an atom: {:?}",
1102                                        tuple.elements[0]
1103                                    );
1104                                }
1105                            }
1106                            eetf::Term::Binary(binary) => {
1107                                // Fallback: handle direct binary response (for backward compatibility)
1108                                match String::from_utf8(binary.bytes) {
1109                                    Ok(json_str) => {
1110                                        info!(
1111                                            "Received engine status JSON (direct binary): {}",
1112                                            json_str
1113                                        );
1114                                        match parse_engine_status(&json_str) {
1115                                            Ok(status) => {
1116                                                app.update_engine_status(status);
1117                                                info!("Engine status updated successfully");
1118                                            }
1119                                            Err(e) => {
1120                                                error!("Failed to parse engine status: {}", e);
1121                                            }
1122                                        }
1123                                    }
1124                                    Err(e) => {
1125                                        error!("Failed to decode engine status UTF-8: {}", e);
1126                                    }
1127                                }
1128                            }
1129                            _ => {
1130                                error!("Unexpected engine status response format: {:?}", term);
1131                            }
1132                        }
1133                    }
1134                    Err(e) => {
1135                        error!("Failed to request engine status: {}", e);
1136                    }
1137                }
1138            } else {
1139                warn!("Cannot refresh engine status - no Erlang connection");
1140            }
1141        }
1142
1143        // Refresh detailed user list if requested
1144        if refresh_detailed_users {
1145            refresh_detailed_users = false;
1146            if let Some(ref conn) = erlang_conn {
1147                info!("Requesting detailed user list");
1148                match conn.request_list_users().await {
1149                    Ok(_) => {
1150                        info!("User list request sent successfully");
1151                    }
1152                    Err(e) => {
1153                        error!("Failed to request user list: {}", e);
1154                    }
1155                }
1156            } else {
1157                warn!("Cannot refresh user list - no Erlang connection");
1158            }
1159        }
1160
1161        // Refresh certificates list if requested
1162        if refresh_certificates {
1163            refresh_certificates = false;
1164            if let (Some(ref conn), Some(ref fingerprint)) = (&erlang_conn, &certificates_fingerprint) {
1165                info!("Requesting certificate list for fingerprint {}", fingerprint);
1166                match conn.admin_list_certificates(fingerprint).await {
1167                    Ok(_) => {
1168                        info!("Certificate list request sent successfully");
1169                    }
1170                    Err(e) => {
1171                        error!("Failed to request certificate list: {}", e);
1172                        app.set_status_message(format!("Failed to load certificates: {}", e));
1173                    }
1174                }
1175            } else {
1176                warn!("Cannot refresh certificate list - no Erlang connection or fingerprint");
1177            }
1178        }
1179
1180        // Refresh online users list if requested
1181        if refresh_online_users {
1182            refresh_online_users = false;
1183            if let Some(ref conn) = erlang_conn {
1184                info!("Requesting online users list");
1185                match conn.request_online_users().await {
1186                    Ok(_) => {
1187                        info!("Online users request sent successfully");
1188                    }
1189                    Err(e) => {
1190                        error!("Failed to request online users: {}", e);
1191                    }
1192                }
1193            } else {
1194                warn!("Cannot refresh online users - no Erlang connection");
1195            }
1196        }
1197
1198        // Load message history if requested
1199        if app.should_load_history {
1200            app.should_load_history = false;
1201
1202            if let (Some(peer), Some(ref conn)) = (&app.current_peer, &erlang_conn) {
1203                let peer_clone = peer.clone();
1204
1205                // Check if this is initial load or scrolling up
1206                let is_initial = {
1207                    let conv = app.get_conversation(&peer_clone);
1208                    conv.map(|c| c.messages.is_empty()).unwrap_or(true)
1209                };
1210
1211                if is_initial {
1212                    // Initial load: get recent messages
1213                    info!("Loading initial history for peer: {}", peer_clone);
1214
1215                    match conn.load_recent_messages(&peer_clone, 50).await {
1216                        Ok(messages) => {
1217                            info!("Loaded {} recent messages", messages.len());
1218                            for (idx, msg) in messages.iter().enumerate() {
1219                                info!(
1220                                    "Recent history [{}] peer={} from={} sent={} ts={} content={}",
1221                                    idx,
1222                                    peer_clone,
1223                                    msg.from,
1224                                    msg.is_sent,
1225                                    msg.timestamp,
1226                                    msg.content
1227                                );
1228                            }
1229                            let conv = app.get_conversation_mut(&peer_clone);
1230                            conv.append_messages(messages);
1231                            conv.is_loading = false;
1232                        }
1233                        Err(e) => {
1234                            error!("Failed to load recent messages: {}", e);
1235                            if let Some(conv) = app.conversations.get_mut(&peer_clone) {
1236                                conv.is_loading = false;
1237                            }
1238                        }
1239                    }
1240                } else {
1241                    // Scrolling up: load older messages
1242                    let oldest_ts = {
1243                        let conv = app.get_conversation(&peer_clone);
1244                        conv.and_then(|c| c.get_oldest_timestamp())
1245                    };
1246
1247                    if let Some(ts) = oldest_ts {
1248                        info!(
1249                            "Loading messages before timestamp {} for peer: {}",
1250                            ts, peer_clone
1251                        );
1252
1253                        // Mark as loading
1254                        if let Some(conv) = app.conversations.get_mut(&peer_clone) {
1255                            conv.is_loading = true;
1256                        }
1257
1258                        match conn.load_messages_before(&peer_clone, ts, 50).await {
1259                            Ok(messages) => {
1260                                if messages.is_empty() {
1261                                    info!("No more history available for {}", peer_clone);
1262                                    if let Some(conv) = app.conversations.get_mut(&peer_clone) {
1263                                        conv.has_more_history = false;
1264                                        conv.is_loading = false;
1265                                    }
1266                                } else {
1267                                    info!(
1268                                        "Loaded {} older messages for {}",
1269                                        messages.len(),
1270                                        peer_clone
1271                                    );
1272                                    for (idx, msg) in messages.iter().enumerate() {
1273                                        info!(
1274                                            "Older history [{}] peer={} from={} sent={} ts={} content={}",
1275                                            idx,
1276                                            peer_clone,
1277                                            msg.from,
1278                                            msg.is_sent,
1279                                            msg.timestamp,
1280                                            msg.content
1281                                        );
1282                                    }
1283                                    if let Some(conv) = app.conversations.get_mut(&peer_clone) {
1284                                        conv.prepend_messages(messages);
1285                                        conv.is_loading = false;
1286                                    }
1287                                }
1288                            }
1289                            Err(e) => {
1290                                error!("Failed to load older messages: {}", e);
1291                                if let Some(conv) = app.conversations.get_mut(&peer_clone) {
1292                                    conv.is_loading = false;
1293                                }
1294                            }
1295                        }
1296                    }
1297                }
1298            } else if app.current_peer.is_none() {
1299                warn!("Cannot load history - no peer selected");
1300            } else {
1301                warn!("Cannot load history - no Erlang connection");
1302            }
1303        }
1304    }
1305    Ok(())
1306}
1307
1308async fn send_message_via_erlang(
1309    conn: &ErlangConnection,
1310    to_user: &str,
1311    message: &str,
1312) -> Result<()> {
1313    // Build JSON message for cryptic_rpc:send_message/1
1314    // The function expects a single JSON binary argument
1315    let json_msg = serde_json::json!({
1316        "type": "send_message",
1317        "to_user": to_user,
1318        "plaintext": message
1319    });
1320
1321    let json_string = serde_json::to_string(&json_msg)?;
1322    let json_bytes = json_string.as_bytes().to_vec();
1323
1324    // Call cryptic_rpc:send_message with a single JSON binary argument
1325    let args = eetf::List::from(vec![eetf::Term::Binary(eetf::Binary::from(json_bytes))]);
1326
1327    conn.rpc_call("cryptic_rpc", "send_message", args).await?;
1328    Ok(())
1329}
1330
1331fn handle_splash_key(app: &mut App, key: crossterm::event::KeyEvent) -> Result<()> {
1332    match key.code {
1333        KeyCode::Char('q') if ctrl(&key) => {
1334            info!("User quit from splash screen");
1335            app.should_exit = true;
1336        }
1337        KeyCode::Esc => {
1338            app.passphrase_input.clear();
1339            app.splash_error = None;
1340        }
1341        KeyCode::Backspace => {
1342            app.passphrase_input.pop();
1343        }
1344        KeyCode::Char(c) if no_mods(&key) || shift_only(&key) => {
1345            // Basic printable filtering
1346            if !c.is_control() {
1347                app.passphrase_input.push(c);
1348            }
1349        }
1350        KeyCode::Enter => {
1351            if app.passphrase_input.trim().is_empty() {
1352                app.splash_error = Some("Passphrase cannot be empty".into());
1353            } else if app.passphrase_input.len() < 4 {
1354                app.splash_error = Some("Passphrase too short (min 4 chars)".into());
1355            } else {
1356                info!("Passphrase accepted, transitioning to main screen");
1357                // Store secret and transition
1358                let secret = Secret::new(app.passphrase_input.clone());
1359                app.passphrase = Some(secret);
1360                app.passphrase_input.clear();
1361                app.splash_error = None;
1362                app.screen = AppScreen::Main;
1363            }
1364        }
1365        _ => {}
1366    }
1367    Ok(())
1368}
1369
1370enum AdminAction {
1371    RegisterUser { gpg_fp: String, key_path: String, metadata: String },
1372    SuspendUser { gpg_fp: String },
1373    ReactivateUser { gpg_fp: String },
1374    RevokeUser { gpg_fp: String },
1375    ListCertificates { gpg_fp: String },
1376    RenewCertificate,
1377}
1378
1379fn handle_main_key(
1380    app: &mut App,
1381    key: crossterm::event::KeyEvent,
1382) -> (Result<()>, bool, bool, Option<AdminAction>) {
1383    let mut refresh_status = false;
1384    let mut refresh_users = false;
1385    let mut admin_action = None;
1386
1387    match key.code {
1388        KeyCode::Char('q') if ctrl(&key) => {
1389            app.should_exit = true;
1390        }
1391        KeyCode::Tab if no_mods(&key) => {
1392            // In Admin register mode, Tab cycles between form fields
1393            if matches!(app.active_tab, Tab::Admin) && matches!(app.admin_mode, app::AdminMode::RegisterUser) {
1394                app.admin_focus_next();
1395            } else {
1396                app.next_tab();
1397            }
1398        }
1399        KeyCode::BackTab => {
1400            // In Admin register mode, Shift+Tab cycles backward between fields
1401            if matches!(app.active_tab, Tab::Admin) && matches!(app.admin_mode, app::AdminMode::RegisterUser) {
1402                app.admin_focus_previous();
1403            } else {
1404                app.previous_tab();
1405            }
1406        }
1407        KeyCode::Char('h') if ctrl(&key) && !key.modifiers.contains(KeyModifiers::SHIFT) => {
1408            app.previous_tab()
1409        }
1410        KeyCode::Char('l') if ctrl(&key) && !key.modifiers.contains(KeyModifiers::SHIFT) => {
1411            app.next_tab()
1412        }
1413        KeyCode::Char('r') if no_mods(&key) && matches!(app.active_tab, Tab::Status) => {
1414            // Refresh engine status when on Status tab
1415            refresh_status = true;
1416            app.status_reset_scroll();
1417        }
1418        KeyCode::Char('n') if no_mods(&key) && matches!(app.active_tab, Tab::Status) => {
1419            // Trigger certificate renewal
1420            admin_action = Some(AdminAction::RenewCertificate);
1421        }
1422        // Admin menu shortcuts
1423        KeyCode::Char('l') if no_mods(&key) && matches!(app.active_tab, Tab::Admin) && matches!(app.admin_mode, app::AdminMode::Menu) => {
1424            // List users
1425            refresh_users = true;
1426            app.admin_reset_scroll();
1427        }
1428        KeyCode::Char('r') if no_mods(&key) && matches!(app.active_tab, Tab::Admin) && matches!(app.admin_mode, app::AdminMode::Menu) => {
1429            // Show register form
1430            app.admin_mode = app::AdminMode::RegisterUser;
1431            app.admin_clear_inputs();
1432            app.admin_reset_scroll();
1433        }
1434        KeyCode::Char('s') if no_mods(&key) && matches!(app.active_tab, Tab::Admin) && matches!(app.admin_mode, app::AdminMode::Menu) => {
1435            // Show suspend confirmation
1436            app.admin_mode = app::AdminMode::SuspendUser;
1437            app.clear_status_message();
1438            app.admin_reset_scroll();
1439        }
1440        KeyCode::Char('a') if no_mods(&key) && matches!(app.active_tab, Tab::Admin) && matches!(app.admin_mode, app::AdminMode::Menu) => {
1441            // Show reactivate confirmation
1442            app.admin_mode = app::AdminMode::ReactivateUser;
1443            app.clear_status_message();
1444            app.admin_reset_scroll();
1445        }
1446        KeyCode::Char('v') if no_mods(&key) && matches!(app.active_tab, Tab::Admin) && matches!(app.admin_mode, app::AdminMode::Menu) => {
1447            // Show revoke confirmation
1448            app.admin_mode = app::AdminMode::RevokeUser;
1449            app.clear_status_message();
1450            app.admin_reset_scroll();
1451        }
1452        KeyCode::Char('c') if no_mods(&key) && matches!(app.active_tab, Tab::Admin) && matches!(app.admin_mode, app::AdminMode::Menu) => {
1453            // Show certificate list
1454            app.admin_mode = app::AdminMode::ListCertificates;
1455            app.clear_status_message();
1456            app.admin_reset_scroll();
1457        }
1458
1459        // Scroll handling for Chat tab and Admin tab
1460        // PageUp/PageDown (or Fn+Up/Down on MacBook)
1461        KeyCode::PageUp => match app.active_tab {
1462            Tab::Chat if app.current_peer.is_some() => {
1463                if let Some(peer) = &app.current_peer.clone() {
1464                    let conv = app.get_conversation_mut(peer);
1465
1466                    // Scroll up by 10 lines
1467                    conv.scroll_offset = conv.scroll_offset.saturating_add(10);
1468
1469                    // Trigger history load if at top and more history available
1470                    if conv.is_at_top() && !conv.is_loading && conv.has_more_history {
1471                        app.should_load_history = true;
1472                    }
1473                }
1474            }
1475            Tab::Admin => {
1476                // Scroll up by 5 lines in admin views
1477                for _ in 0..5 {
1478                    app.admin_scroll_up();
1479                }
1480            }
1481            Tab::Status => {
1482                // Scroll up by 5 lines in status view
1483                for _ in 0..5 {
1484                    app.status_scroll_up();
1485                }
1486            }
1487            Tab::Help => {
1488                // Scroll up by 5 lines in help view
1489                for _ in 0..5 {
1490                    app.help_scroll_up();
1491                }
1492            }
1493            _ => {}
1494        },
1495        KeyCode::PageDown => match app.active_tab {
1496            Tab::Chat if app.current_peer.is_some() => {
1497                if let Some(peer) = &app.current_peer.clone() {
1498                    let conv = app.get_conversation_mut(peer);
1499                    conv.scroll_offset = conv.scroll_offset.saturating_sub(10);
1500                }
1501            }
1502            Tab::Admin => {
1503                // Scroll down by 5 lines in admin views
1504                for _ in 0..5 {
1505                    app.admin_scroll_down();
1506                }
1507            }
1508            Tab::Status => {
1509                // Scroll down by 5 lines in status view
1510                for _ in 0..5 {
1511                    app.status_scroll_down();
1512                }
1513            }
1514            Tab::Help => {
1515                // Scroll down by 5 lines in help view
1516                for _ in 0..5 {
1517                    app.help_scroll_down();
1518                }
1519            }
1520            _ => {}
1521        },
1522        // Ctrl+U/D for scrolling (vim-style, works great on MacBooks)
1523        KeyCode::Char('u') if ctrl(&key) => match app.active_tab {
1524            Tab::Chat if app.current_peer.is_some() => {
1525                if let Some(peer) = &app.current_peer.clone() {
1526                    let conv = app.get_conversation_mut(peer);
1527
1528                    // Scroll up by 10 lines
1529                    conv.scroll_offset = conv.scroll_offset.saturating_add(10);
1530
1531                    // Trigger history load if at top and more history available
1532                    if conv.is_at_top() && !conv.is_loading && conv.has_more_history {
1533                        app.should_load_history = true;
1534                    }
1535                }
1536            }
1537            Tab::Admin => {
1538                // Scroll up by 5 lines in admin views
1539                for _ in 0..5 {
1540                    app.admin_scroll_up();
1541                }
1542            }
1543            Tab::Status => {
1544                // Scroll up by 5 lines in status view
1545                for _ in 0..5 {
1546                    app.status_scroll_up();
1547                }
1548            }
1549            Tab::Help => {
1550                // Scroll up by 5 lines in help view
1551                for _ in 0..5 {
1552                    app.help_scroll_up();
1553                }
1554            }
1555            _ => {}
1556        },
1557        KeyCode::Char('d') if ctrl(&key) => match app.active_tab {
1558            Tab::Chat if app.current_peer.is_some() => {
1559                if let Some(peer) = &app.current_peer.clone() {
1560                    let conv = app.get_conversation_mut(peer);
1561                    conv.scroll_offset = conv.scroll_offset.saturating_sub(10);
1562                }
1563            }
1564            Tab::Admin => {
1565                // Scroll down by 5 lines in admin views
1566                for _ in 0..5 {
1567                    app.admin_scroll_down();
1568                }
1569            }
1570            Tab::Status => {
1571                // Scroll down by 5 lines in status view
1572                for _ in 0..5 {
1573                    app.status_scroll_down();
1574                }
1575            }
1576            Tab::Help => {
1577                // Scroll down by 5 lines in help view
1578                for _ in 0..5 {
1579                    app.help_scroll_down();
1580                }
1581            }
1582            _ => {}
1583        },
1584        KeyCode::Home if matches!(app.active_tab, Tab::Chat) && app.current_peer.is_some() => {
1585            if let Some(peer) = &app.current_peer.clone() {
1586                let conv = app.get_conversation_mut(peer);
1587                conv.scroll_offset = conv.messages.len().saturating_sub(1);
1588
1589                // Trigger history load when jumping to top
1590                if !conv.is_loading && conv.has_more_history {
1591                    app.should_load_history = true;
1592                }
1593            }
1594        }
1595        KeyCode::End if matches!(app.active_tab, Tab::Chat) && app.current_peer.is_some() => {
1596            if let Some(peer) = &app.current_peer.clone() {
1597                let conv = app.get_conversation_mut(peer);
1598                conv.scroll_offset = 0; // Jump to bottom (newest messages)
1599            }
1600        }
1601
1602        KeyCode::Up => match app.active_tab {
1603            Tab::Chat => app.previous_user(),
1604            Tab::Admin if matches!(app.admin_mode, app::AdminMode::Menu) => app.admin_previous_user(),
1605            _ => {}
1606        },
1607        KeyCode::Down => match app.active_tab {
1608            Tab::Chat => app.next_user(),
1609            Tab::Admin if matches!(app.admin_mode, app::AdminMode::Menu) => app.admin_next_user(),
1610            _ => {}
1611        },
1612        KeyCode::Enter => match app.active_tab {
1613            Tab::Chat => {
1614                if !app.input_buffer.is_empty() && app.current_peer.is_some() {
1615                    app.send_message();
1616                } else {
1617                    app.open_selected_chat();
1618                }
1619            }
1620            Tab::Admin => {
1621                if matches!(app.admin_mode, app::AdminMode::RegisterUser) && app.admin_form_complete() {
1622                    let gpg_fp = app.admin_form.gpg_fp.trim().to_string();
1623                    let key_path = app.admin_form.key_path.trim().to_string();
1624                    let metadata = app.admin_form.metadata.trim().to_string();
1625                    app.set_status_message(format!("Registering {}...", gpg_fp));
1626                    admin_action = Some(AdminAction::RegisterUser { gpg_fp, key_path, metadata });
1627                    // Return to menu after submission
1628                    app.admin_mode = app::AdminMode::Menu;
1629                    app.admin_reset_scroll();
1630                } else if matches!(app.admin_mode, app::AdminMode::SuspendUser) {
1631                    if let Some(user) = app.admin_get_selected_user() {
1632                        if user.status == "active" {
1633                            let username = user.username.clone();
1634                            let gpg_fp = user.gpg_fp.clone();
1635                            app.set_status_message(format!("Suspending {}...", username));
1636                            admin_action = Some(AdminAction::SuspendUser { gpg_fp });
1637                            app.admin_mode = app::AdminMode::Menu;
1638                            app.admin_reset_scroll();
1639                        } else {
1640                            app.set_status_message(format!("Cannot suspend user with status: {}", user.status));
1641                        }
1642                    }
1643                } else if matches!(app.admin_mode, app::AdminMode::ReactivateUser) {
1644                    if let Some(user) = app.admin_get_selected_user() {
1645                        if user.status == "suspended" {
1646                            let username = user.username.clone();
1647                            let gpg_fp = user.gpg_fp.clone();
1648                            app.set_status_message(format!("Reactivating {}...", username));
1649                            admin_action = Some(AdminAction::ReactivateUser { gpg_fp });
1650                            app.admin_mode = app::AdminMode::Menu;
1651                            app.admin_reset_scroll();
1652                        } else {
1653                            app.set_status_message(format!("Cannot reactivate user with status: {}", user.status));
1654                        }
1655                    }
1656                } else if matches!(app.admin_mode, app::AdminMode::RevokeUser) {
1657                    if let Some(user) = app.admin_get_selected_user() {
1658                        if user.status != "revoked" {
1659                            let username = user.username.clone();
1660                            let gpg_fp = user.gpg_fp.clone();
1661                            app.set_status_message(format!("Revoking {}...", username));
1662                            admin_action = Some(AdminAction::RevokeUser { gpg_fp });
1663                            app.admin_mode = app::AdminMode::Menu;
1664                            app.admin_reset_scroll();
1665                        } else {
1666                            app.set_status_message(format!("User is already revoked"));
1667                        }
1668                    }
1669                } else if matches!(app.admin_mode, app::AdminMode::ListCertificates) {
1670                    if let Some(user) = app.admin_get_selected_user() {
1671                        let username = user.username.clone();
1672                        let gpg_fp = user.gpg_fp.clone();
1673                        app.set_status_message(format!("Loading certificates for {}...", username));
1674                        admin_action = Some(AdminAction::ListCertificates { gpg_fp });
1675                        // Stay in ListCertificates mode to show results
1676                    }
1677                }
1678            }
1679            _ => {}
1680        },
1681        KeyCode::Esc => match app.active_tab {
1682            Tab::Chat => app.clear_input(),
1683            Tab::Admin => {
1684                // In any admin sub-mode, Esc returns to menu
1685                if !matches!(app.admin_mode, app::AdminMode::Menu) {
1686                    app.admin_mode = app::AdminMode::Menu;
1687                    app.admin_reset_scroll();
1688                }
1689                app.admin_clear_inputs();
1690                app.clear_status_message();
1691            }
1692            _ => {}
1693        },
1694
1695        // Text editing - Emacs-style navigation
1696        KeyCode::Char('a') if ctrl(&key) && matches!(app.active_tab, Tab::Chat) => {
1697            app.input_move_home()
1698        }
1699        KeyCode::Char('e') if ctrl(&key) && matches!(app.active_tab, Tab::Chat) => {
1700            app.input_move_end()
1701        }
1702        KeyCode::Char('b') if ctrl(&key) && matches!(app.active_tab, Tab::Chat) => {
1703            app.input_move_left()
1704        }
1705        KeyCode::Char('f') if ctrl(&key) && matches!(app.active_tab, Tab::Chat) => {
1706            app.input_move_right()
1707        }
1708        KeyCode::Char('d') if ctrl(&key) && !matches!(app.active_tab, Tab::Chat) => {
1709            match app.active_tab {
1710                Tab::Admin => app.admin_backspace(),
1711                _ => app.input_delete(),
1712            }
1713        }
1714
1715        // Arrow key navigation in input
1716        KeyCode::Left if !ctrl(&key) && matches!(app.active_tab, Tab::Chat) => {
1717            app.input_move_left()
1718        }
1719        KeyCode::Right if !ctrl(&key) && matches!(app.active_tab, Tab::Chat) => {
1720            app.input_move_right()
1721        }
1722
1723        // Text editing
1724        KeyCode::Delete => match app.active_tab {
1725            Tab::Chat => app.input_delete(),
1726            Tab::Admin if matches!(app.admin_mode, app::AdminMode::RegisterUser) => app.admin_backspace(),
1727            _ => {}
1728        },
1729        KeyCode::Char(c) if no_mods(&key) || shift_only(&key) => match app.active_tab {
1730            Tab::Chat => app.input_char(c),
1731            Tab::Admin if matches!(app.admin_mode, app::AdminMode::RegisterUser) => app.admin_input_char(c),
1732            _ => {}
1733        },
1734        KeyCode::Backspace => match app.active_tab {
1735            Tab::Chat => app.input_backspace(),
1736            Tab::Admin if matches!(app.admin_mode, app::AdminMode::RegisterUser) => app.admin_backspace(),
1737            _ => {}
1738        },
1739        _ => {}
1740    }
1741    (Ok(()), refresh_status, refresh_users, admin_action)
1742}
1743
1744#[inline]
1745fn no_mods(key: &crossterm::event::KeyEvent) -> bool {
1746    key.modifiers.is_empty()
1747}
1748
1749#[inline]
1750fn shift_only(key: &crossterm::event::KeyEvent) -> bool {
1751    key.modifiers == KeyModifiers::SHIFT
1752}
1753
1754#[inline]
1755fn ctrl(key: &crossterm::event::KeyEvent) -> bool {
1756    key.modifiers.contains(KeyModifiers::CONTROL)
1757}
1758
1759/// Read Erlang cookie from ~/.erlang.cookie
1760fn read_erlang_cookie() -> Result<String> {
1761    let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
1762    let cookie_path = format!("{}/.erlang.cookie", home);
1763    match std::fs::read_to_string(&cookie_path) {
1764        Ok(cookie) => {
1765            info!("Successfully read Erlang cookie from {}", cookie_path);
1766            Ok(cookie.trim().to_string())
1767        }
1768        Err(e) => {
1769            warn!("Failed to read Erlang cookie from {}: {}", cookie_path, e);
1770            Err(anyhow::anyhow!("Failed to read .erlang.cookie: {}", e))
1771        }
1772    }
1773}