1#![doc = include_str!("../README.md")]
2mod 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
44fn 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 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 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 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 app.server_connection_up = true;
76 }
77 }
78 "user_registered" => {
79 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 app.admin_clear_inputs();
97 *refresh_detailed_users = true;
98 }
99 "error" => {
100 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 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 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 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 app.server_connection_up = true;
214
215 info!(
216 "Received message from {}: {} (timestamp: {})",
217 from, message, timestamp
218 );
219
220 let count_before = app.messages.get(from).map(|v| v.len()).unwrap_or(0);
222
223 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 app.current_peer.as_deref() == Some(from) {
234 if let Some(conv) = app.conversations.get_mut(from) {
235 conv.scroll_offset = 0; }
237 }
238
239 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 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 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 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 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 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 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
322fn 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 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
450fn 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 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 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
534fn 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
595fn init_logging() -> Result<()> {
597 let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
598
599 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 init_logging()?;
623
624 smol::block_on(async_main())
626}
627
628async fn async_main() -> Result<()> {
629 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 let mut terminal = init_terminal()?;
639
640 let mut app = if args.get_node_name().is_some() {
642 App::new() } else {
644 App::with_mock_data() };
646
647 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 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 restore_terminal()?;
681
682 info!("Cryptic TUI shutting down");
683
684 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; 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; let mut certificates_fingerprint: Option<String> = None; let mut refresh_engine_status = false; let mut refresh_detailed_users = false; let mut refresh_online_users = false; let mut last_online_check = Instant::now(); let online_check_interval = Duration::from_secs(10); while !app.should_exit {
723 app.update_system_message();
725
726 terminal.draw(|f| ui::draw(f, app))?;
727
728 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))?; 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 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 }
755 Err(e) => {
756 error!("Failed to subscribe to event bus: {}", e);
757 }
758 }
759
760 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 info!("Starting cryptic_tui_bridge on Erlang node");
776
777 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 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 }
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 }
813 }
814 }
815 }
816
817 if let Some(ref mut node) = dist_node {
819 while let Ok(msg) = node.try_recv() {
820 use erl_dist::term::Term; if matches!(msg, dist_node::Message::Tick) {
824 continue;
825 }
826
827 info!("Received message from Erlang: {:?}", msg);
828
829 if let dist_node::Message::RegSend(regsend) = msg {
831 if let Term::Tuple(tuple) = ®send.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 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 if event::poll(Duration::from_millis(120))? {
863 if let Event::Key(key) = event::read()? {
864 if key.kind == KeyEventKind::Repeat {
866 continue;
867 }
868 if app.screen == AppScreen::Splash {
869 handle_splash_key(app, key)?;
870 } else {
871 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 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 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 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_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 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 }
947 }
948 } else {
949 warn!("Cannot send message - no Erlang connection");
950 }
951 }
952
953 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 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 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 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 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 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 match term {
1058 eetf::Term::Tuple(tuple) if tuple.elements.len() == 2 => {
1059 if let eetf::Term::Atom(atom) = &tuple.elements[0] {
1061 if atom.name == "ok" {
1062 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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_status = true;
1416 app.status_reset_scroll();
1417 }
1418 KeyCode::Char('n') if no_mods(&key) && matches!(app.active_tab, Tab::Status) => {
1419 admin_action = Some(AdminAction::RenewCertificate);
1421 }
1422 KeyCode::Char('l') if no_mods(&key) && matches!(app.active_tab, Tab::Admin) && matches!(app.admin_mode, app::AdminMode::Menu) => {
1424 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 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 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 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 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 app.admin_mode = app::AdminMode::ListCertificates;
1455 app.clear_status_message();
1456 app.admin_reset_scroll();
1457 }
1458
1459 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 conv.scroll_offset = conv.scroll_offset.saturating_add(10);
1468
1469 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 for _ in 0..5 {
1478 app.admin_scroll_up();
1479 }
1480 }
1481 Tab::Status => {
1482 for _ in 0..5 {
1484 app.status_scroll_up();
1485 }
1486 }
1487 Tab::Help => {
1488 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 for _ in 0..5 {
1505 app.admin_scroll_down();
1506 }
1507 }
1508 Tab::Status => {
1509 for _ in 0..5 {
1511 app.status_scroll_down();
1512 }
1513 }
1514 Tab::Help => {
1515 for _ in 0..5 {
1517 app.help_scroll_down();
1518 }
1519 }
1520 _ => {}
1521 },
1522 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 conv.scroll_offset = conv.scroll_offset.saturating_add(10);
1530
1531 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 for _ in 0..5 {
1540 app.admin_scroll_up();
1541 }
1542 }
1543 Tab::Status => {
1544 for _ in 0..5 {
1546 app.status_scroll_up();
1547 }
1548 }
1549 Tab::Help => {
1550 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 for _ in 0..5 {
1567 app.admin_scroll_down();
1568 }
1569 }
1570 Tab::Status => {
1571 for _ in 0..5 {
1573 app.status_scroll_down();
1574 }
1575 }
1576 Tab::Help => {
1577 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 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; }
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 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 }
1677 }
1678 }
1679 _ => {}
1680 },
1681 KeyCode::Esc => match app.active_tab {
1682 Tab::Chat => app.clear_input(),
1683 Tab::Admin => {
1684 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 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 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 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
1759fn 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}