1use crate::erlang::ConnectionStatus;
22use std::collections::{HashMap, VecDeque};
23use std::time::{Duration, Instant};
24use zeroize::Zeroize;
25
26#[derive(Debug, Clone)]
28pub struct SystemMessage {
29 pub text: String,
31 #[allow(dead_code)]
33 pub sys_code: Option<String>,
34 pub timestamp: Instant,
36 pub min_display_duration: Duration,
38}
39
40impl SystemMessage {
41 pub fn new(text: String) -> Self {
43 Self {
44 text,
45 sys_code: None,
46 timestamp: Instant::now(),
47 min_display_duration: Duration::from_secs(3),
48 }
49 }
50
51 pub fn with_code(text: String, sys_code: String) -> Self {
53 Self {
54 text,
55 sys_code: Some(sys_code),
56 timestamp: Instant::now(),
57 min_display_duration: Duration::from_secs(3),
58 }
59 }
60
61 pub fn can_advance(&self) -> bool {
63 self.timestamp.elapsed() >= self.min_display_duration
64 }
65
66 pub fn reset_timestamp(&mut self) {
68 self.timestamp = Instant::now();
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq)]
76pub enum Tab {
77 Chat,
79 Admin,
81 Status,
83 Help,
85}
86
87impl Tab {
88 pub fn next(&self) -> Self {
92 match self {
93 Tab::Chat => Tab::Admin,
94 Tab::Admin => Tab::Status,
95 Tab::Status => Tab::Help,
96 Tab::Help => Tab::Chat,
97 }
98 }
99
100 pub fn previous(&self) -> Self {
104 match self {
105 Tab::Chat => Tab::Help,
106 Tab::Admin => Tab::Chat,
107 Tab::Status => Tab::Admin,
108 Tab::Help => Tab::Status,
109 }
110 }
111}
112
113#[derive(Debug, Clone)]
115pub struct User {
116 pub name: String,
118 pub online: bool,
120}
121
122#[derive(Debug, Clone)]
124pub struct Message {
125 pub from: String,
127 pub content: String,
129 pub is_sent: bool,
131 #[allow(dead_code)]
133 pub timestamp: u64,
134}
135
136impl Message {
137 pub fn received(from: &str, content: &str) -> Self {
138 Self {
139 from: from.to_string(),
140 content: content.to_string(),
141 is_sent: false,
142 timestamp: 0,
143 }
144 }
145
146 pub fn sent(from: &str, content: &str) -> Self {
147 Self {
148 from: from.to_string(),
149 content: content.to_string(),
150 is_sent: true,
151 timestamp: 0,
152 }
153 }
154}
155
156#[derive(Debug, Clone, Default)]
158pub struct EngineStatus {
159 pub username: String,
160 pub active_sessions: u32,
161 pub message_count: u32,
162 pub error_count: u32,
163 pub uptime_ms: u64,
164 pub session_details: Vec<SessionDetail>,
165 pub cert_expires_at: Option<i64>,
166 pub cert_renewal_in_progress: bool,
167 pub cert_time_until_expiry: Option<i64>,
168}
169
170#[derive(Debug, Clone)]
172#[allow(dead_code)]
173pub struct SessionDetail {
174 pub peer_username: String,
175 pub message_count: u32,
176 pub error_count: u32,
177 pub dh_ratchet_step: u32,
178 pub send_msg_number: u32,
179 pub recv_msg_number: u32,
180 pub prev_recv_chain_length: u32,
181 pub skipped_keys_count: u32,
182 pub current_state: String,
183 pub transition_count: u32,
184 pub created_at: u64,
185 pub last_updated: u64,
186 pub receiving_chain_active: bool,
187 pub sending_chain_active: bool,
188 pub has_remote_dh: bool,
189}
190
191#[derive(Debug, Clone)]
193#[allow(dead_code)]
194pub struct DetailedUser {
195 pub username: String,
196 pub gpg_fp: String,
197 pub online: bool,
198 pub status: String,
199 pub registered_at: u64,
200 pub last_seen: u64,
201 pub registered_by: String,
202 pub metadata: Option<UserMetadata>,
203}
204
205#[derive(Debug, Clone)]
208pub struct UserMetadata {
209 pub fields: std::collections::HashMap<String, String>,
210}
211
212#[derive(Debug, Clone)]
214pub struct Certificate {
215 pub serial: String,
216 pub issued_at: i64,
217 pub expires_at: i64,
218 pub status: String,
219 pub revoked_at: Option<i64>,
220 pub revoked_by: Option<String>,
221 pub revoked_reason: Option<String>,
222}
223
224#[derive(Debug, Clone)]
229#[allow(dead_code)]
230pub struct ConversationView {
231 pub peer: String,
233
234 pub messages: VecDeque<Message>,
236
237 pub scroll_offset: usize,
239
240 pub loaded_count: usize,
242
243 pub total_count: Option<usize>,
245
246 pub is_loading: bool,
248
249 pub has_more_history: bool,
251
252 pub oldest_timestamp: Option<u64>,
254
255 pub newest_timestamp: Option<u64>,
257}
258
259impl ConversationView {
260 pub fn new(peer: String) -> Self {
262 Self {
263 peer,
264 messages: VecDeque::with_capacity(100),
265 scroll_offset: 0,
266 loaded_count: 0,
267 total_count: None,
268 is_loading: false,
269 has_more_history: true,
270 oldest_timestamp: None,
271 newest_timestamp: None,
272 }
273 }
274
275 pub fn prepend_messages(&mut self, messages: Vec<Message>) {
279 if let Some(first) = messages.first() {
280 self.oldest_timestamp = Some(first.timestamp);
281 }
282
283 for msg in messages.into_iter().rev() {
284 self.messages.push_front(msg);
285 }
286
287 self.loaded_count = self.messages.len();
288 }
289
290 pub fn append_messages(&mut self, messages: Vec<Message>) {
294 if let Some(last) = messages.last() {
295 self.newest_timestamp = Some(last.timestamp);
296 }
297
298 self.messages.extend(messages);
299 self.loaded_count = self.messages.len();
300 }
301
302 pub fn add_message(&mut self, message: Message) {
304 self.newest_timestamp = Some(message.timestamp);
305 self.messages.push_back(message);
306 self.loaded_count = self.messages.len();
307 }
308
309 pub fn get_oldest_timestamp(&self) -> Option<u64> {
311 self.messages.front().map(|m| m.timestamp)
312 }
313
314 pub fn is_at_top(&self) -> bool {
316 self.scroll_offset >= self.messages.len().saturating_sub(1)
317 }
318
319 #[allow(dead_code)]
321 pub fn is_at_bottom(&self) -> bool {
322 self.scroll_offset == 0
323 }
324}
325
326pub struct App {
343 pub active_tab: Tab,
345 pub users: Vec<User>,
347 pub messages: HashMap<String, Vec<Message>>,
349 pub current_peer: Option<String>,
351 pub input_buffer: String,
353 pub input_cursor: usize,
355 pub selected_user: usize,
357 pub is_mock_mode: bool,
359 pub screen: AppScreen,
361 pub passphrase: Option<Secret>,
363 pub passphrase_input: String,
365 pub splash_error: Option<String>,
367 pub should_exit: bool,
369 pub connection_status: ConnectionStatus,
371 pub erlang_node_name: Option<String>,
373 pub engine_status: Option<EngineStatus>,
375 pub detailed_users: Option<Vec<DetailedUser>>,
377 pub conversations: HashMap<String, ConversationView>,
379 pub should_load_history: bool,
381 pub status_message: Option<String>,
383 pub admin_form: AdminForm,
385 pub admin_mode: AdminMode,
387 pub admin_selected_user: usize,
389 pub certificates: Option<Vec<Certificate>>,
391 pub certificates_for_user: Option<String>,
393 pub admin_scroll_offset: u16,
395 pub status_scroll_offset: u16,
397 pub help_scroll_offset: u16,
399 pub system_message_queue: VecDeque<SystemMessage>,
401 pub current_system_message: Option<SystemMessage>,
403 pub server_connection_up: bool,
405}
406
407#[derive(Debug, Clone, Default)]
409#[allow(dead_code)]
410pub struct AdminForm {
411 pub gpg_fp: String,
413 pub key_path: String,
415 pub metadata: String,
417 pub active_field: AdminField,
419 pub visible: bool,
421}
422
423#[derive(Debug, Clone, Copy, PartialEq, Eq)]
425pub enum AdminMode {
426 Menu,
427 RegisterUser,
428 SuspendUser,
429 ReactivateUser,
430 RevokeUser,
431 ListCertificates,
432}
433
434#[derive(Debug, Clone, Copy, PartialEq, Eq)]
436pub enum AdminField {
437 GpgFingerprint,
438 KeyPath,
439 Metadata,
440}
441
442impl Default for AdminField {
443 fn default() -> Self {
444 AdminField::GpgFingerprint
445 }
446}
447
448impl App {
449 pub fn new() -> Self {
454 Self {
455 active_tab: Tab::Chat,
456 users: Vec::new(), messages: HashMap::new(),
458 current_peer: None,
459 input_buffer: String::new(),
460 input_cursor: 0,
461 selected_user: 0,
462 is_mock_mode: false,
463 screen: AppScreen::Splash,
464 passphrase: None,
465 passphrase_input: String::new(),
466 splash_error: None,
467 should_exit: false,
468 connection_status: ConnectionStatus::Disconnected,
469 erlang_node_name: None,
470 engine_status: None,
471 detailed_users: None,
472 conversations: HashMap::new(),
473 should_load_history: false,
474 status_message: None,
475 admin_form: AdminForm::default(),
476 admin_mode: AdminMode::Menu,
477 admin_selected_user: 0,
478 certificates: None,
479 certificates_for_user: None,
480 admin_scroll_offset: 0,
481 status_scroll_offset: 0,
482 help_scroll_offset: 0,
483 system_message_queue: VecDeque::new(),
484 current_system_message: None,
485 server_connection_up: false,
486 }
487 }
488
489 pub fn update_online_users(&mut self, online_users: Vec<String>) {
495 let current_username = self.get_current_username().map(|s| s.to_string());
497
498 for user in &mut self.users {
500 user.online = false;
501 }
502
503 for username in &online_users {
505 if current_username.as_deref() == Some(username.as_str()) {
507 continue;
508 }
509
510 if let Some(user) = self.users.iter_mut().find(|u| &u.name == username) {
511 user.online = true;
512 } else {
513 self.users.push(User {
515 name: username.clone(),
516 online: true,
517 });
518 }
519 }
520
521 if let Some(current_user) = current_username {
523 self.users.retain(|u| u.name != current_user);
524 }
525
526 self.users.sort_by(|a, b| match (a.online, b.online) {
528 (true, false) => std::cmp::Ordering::Less,
529 (false, true) => std::cmp::Ordering::Greater,
530 _ => a.name.cmp(&b.name),
531 });
532 }
533
534 pub fn get_current_username(&self) -> Option<&str> {
539 self.erlang_node_name
540 .as_ref()
541 .map(|name| name.split('@').next().unwrap_or(name.as_str()))
542 }
543
544 pub fn with_mock_data() -> Self {
549 let users = vec![
550 User {
551 name: "alice".to_string(),
552 online: true,
553 },
554 User {
555 name: "bob".to_string(),
556 online: false,
557 },
558 User {
559 name: "charlie".to_string(),
560 online: true,
561 },
562 User {
563 name: "dave".to_string(),
564 online: false,
565 },
566 User {
567 name: "eve".to_string(),
568 online: true,
569 },
570 ];
571
572 let mut messages = HashMap::new();
573
574 messages.insert(
576 "alice".to_string(),
577 vec![
578 Message::received("alice", "Hey there! How's the TUI coming along?"),
579 Message::sent("me", "Pretty good! Just got the layout working."),
580 Message::received("alice", "That's awesome! Can't wait to try it."),
581 Message::sent("me", "Still need to hook up the Erlang backend though."),
582 Message::received("alice", "One step at a time! 🚀"),
583 ],
584 );
585
586 messages.insert(
588 "charlie".to_string(),
589 vec![
590 Message::received("charlie", "Did you see the new design?"),
591 Message::sent("me", "Yes! Looks great with the tabs."),
592 Message::received("charlie", "Thanks! Let me know if you need help."),
593 ],
594 );
595
596 Self {
597 active_tab: Tab::Chat,
598 users,
599 messages,
600 current_peer: Some("alice".to_string()),
601 input_buffer: String::new(),
602 input_cursor: 0,
603 selected_user: 0,
604 is_mock_mode: true,
605 screen: AppScreen::Splash,
606 passphrase: None,
607 passphrase_input: String::new(),
608 splash_error: None,
609 should_exit: false,
610 connection_status: ConnectionStatus::Disconnected,
611 erlang_node_name: None,
612 engine_status: None,
613 detailed_users: None,
614 conversations: HashMap::new(),
615 should_load_history: false,
616 status_message: None,
617 admin_form: AdminForm::default(),
618 admin_mode: AdminMode::Menu,
619 admin_selected_user: 0,
620 certificates: None,
621 certificates_for_user: None,
622 admin_scroll_offset: 0,
623 status_scroll_offset: 0,
624 help_scroll_offset: 0,
625 system_message_queue: VecDeque::new(),
626 current_system_message: None,
627 server_connection_up: false,
628 }
629 }
630
631 pub fn next_tab(&mut self) {
633 self.active_tab = self.active_tab.next();
634 self.admin_reset_scroll();
636 self.status_reset_scroll();
637 self.help_reset_scroll();
638 }
639
640 pub fn previous_tab(&mut self) {
642 self.active_tab = self.active_tab.previous();
643 self.admin_reset_scroll();
645 self.status_reset_scroll();
646 self.help_reset_scroll();
647 }
648
649 pub fn next_user(&mut self) {
651 if self.users.is_empty() {
652 return;
653 }
654 self.selected_user = (self.selected_user + 1) % self.users.len();
655
656 self.open_selected_chat();
658 }
659
660 pub fn previous_user(&mut self) {
662 if self.users.is_empty() {
663 return;
664 }
665 if self.selected_user == 0 {
666 self.selected_user = self.users.len() - 1;
667 } else {
668 self.selected_user -= 1;
669 }
670
671 self.open_selected_chat();
673 }
674
675 pub fn open_selected_chat(&mut self) {
677 let idx = self.selected_user;
678 if idx < self.users.len() {
679 let peer = self.users[idx].name.clone();
680 self.current_peer = Some(peer.clone());
681
682 let needs_load = {
684 let conv = self.get_conversation(&peer);
685 conv.map(|c| c.messages.is_empty()).unwrap_or(true)
686 };
687
688 if needs_load {
689 self.should_load_history = true;
690 }
691 }
692 }
693
694 pub fn send_message(&mut self) {
696 if self.input_buffer.is_empty() {
697 return;
698 }
699
700 if let Some(peer) = self.current_peer.clone() {
701 let content = self.input_buffer.clone();
702 let timestamp = std::time::SystemTime::now()
703 .duration_since(std::time::UNIX_EPOCH)
704 .unwrap()
705 .as_secs();
706
707 let message = Message {
708 from: "me".to_string(),
709 content: content.clone(),
710 is_sent: true,
711 timestamp,
712 };
713
714 self.messages
716 .entry(peer.clone())
717 .or_insert_with(Vec::new)
718 .push(message.clone());
719
720 let conv = self.get_conversation_mut(&peer);
722 conv.add_message(message);
723 conv.scroll_offset = 0; self.input_buffer.clear();
727 self.input_cursor = 0;
728
729 }
732 }
733
734 pub fn input_char(&mut self, c: char) {
736 let byte_index = self.char_index_to_byte_index(self.input_cursor);
737 self.input_buffer.insert(byte_index, c);
738 self.input_cursor += 1;
739 }
740
741 pub fn input_backspace(&mut self) {
743 if self.input_cursor > 0 {
744 self.input_cursor -= 1;
745 let byte_index = self.char_index_to_byte_index(self.input_cursor);
746 self.input_buffer.remove(byte_index);
747 }
748 }
749
750 pub fn clear_input(&mut self) {
752 self.input_buffer.clear();
753 self.input_cursor = 0;
754 }
755
756 pub fn input_move_home(&mut self) {
758 self.input_cursor = 0;
759 }
760
761 pub fn input_move_end(&mut self) {
763 self.input_cursor = self.input_buffer.chars().count();
764 }
765
766 pub fn input_move_left(&mut self) {
768 if self.input_cursor > 0 {
769 self.input_cursor -= 1;
770 }
771 }
772
773 pub fn input_move_right(&mut self) {
775 let len = self.input_buffer.chars().count();
776 if self.input_cursor < len {
777 self.input_cursor += 1;
778 }
779 }
780
781 pub fn input_delete(&mut self) {
783 let len = self.input_buffer.chars().count();
784 if self.input_cursor < len {
785 let byte_index = self.char_index_to_byte_index(self.input_cursor);
786 self.input_buffer.remove(byte_index);
787 }
788 }
789
790 fn char_index_to_byte_index(&self, char_index: usize) -> usize {
792 self.input_buffer
793 .char_indices()
794 .nth(char_index)
795 .map(|(byte_idx, _)| byte_idx)
796 .unwrap_or(self.input_buffer.len())
797 }
798
799 #[allow(dead_code)]
801 pub fn receive_message(&mut self, from: &str, content: &str) {
802 let timestamp = std::time::SystemTime::now()
803 .duration_since(std::time::UNIX_EPOCH)
804 .unwrap()
805 .as_secs();
806
807 let message = Message {
808 from: from.to_string(),
809 content: content.to_string(),
810 is_sent: false,
811 timestamp,
812 };
813
814 self.messages
816 .entry(from.to_string())
817 .or_insert_with(Vec::new)
818 .push(message.clone());
819
820 self.get_conversation_mut(from).add_message(message);
822 }
823
824 pub fn update_engine_status(&mut self, status: EngineStatus) {
826 self.engine_status = Some(status);
827 }
828
829 pub fn update_detailed_users(&mut self, users: Vec<DetailedUser>) {
831 self.detailed_users = Some(users);
832 }
833
834 pub fn set_status_message(&mut self, message: String) {
840 self.status_message = Some(message);
841 }
842
843 pub fn clear_status_message(&mut self) {
845 self.status_message = None;
846 }
847
848 pub fn admin_focus_next(&mut self) {
850 self.admin_form.active_field = match self.admin_form.active_field {
851 AdminField::GpgFingerprint => AdminField::KeyPath,
852 AdminField::KeyPath => AdminField::Metadata,
853 AdminField::Metadata => AdminField::GpgFingerprint,
854 };
855 }
856
857 pub fn admin_focus_previous(&mut self) {
859 self.admin_form.active_field = match self.admin_form.active_field {
860 AdminField::GpgFingerprint => AdminField::Metadata,
861 AdminField::KeyPath => AdminField::GpgFingerprint,
862 AdminField::Metadata => AdminField::KeyPath,
863 };
864 }
865
866 pub fn admin_input_char(&mut self, ch: char) {
868 if ch.is_control() {
869 return;
870 }
871
872 match self.admin_form.active_field {
873 AdminField::GpgFingerprint => self.admin_form.gpg_fp.push(ch),
874 AdminField::KeyPath => self.admin_form.key_path.push(ch),
875 AdminField::Metadata => self.admin_form.metadata.push(ch),
876 }
877 }
878
879 pub fn admin_backspace(&mut self) {
881 match self.admin_form.active_field {
882 AdminField::GpgFingerprint => {
883 self.admin_form.gpg_fp.pop();
884 }
885 AdminField::KeyPath => {
886 self.admin_form.key_path.pop();
887 }
888 AdminField::Metadata => {
889 self.admin_form.metadata.pop();
890 }
891 }
892 }
893
894 pub fn admin_clear_inputs(&mut self) {
896 self.admin_form.gpg_fp.clear();
897 self.admin_form.key_path.clear();
898 self.admin_form.metadata.clear();
899 self.admin_form.active_field = AdminField::GpgFingerprint;
900 }
901
902 pub fn admin_form_complete(&self) -> bool {
904 !self.admin_form.gpg_fp.trim().is_empty() && !self.admin_form.key_path.trim().is_empty()
905 }
906
907 pub fn admin_next_user(&mut self) {
909 if let Some(ref users) = self.detailed_users {
910 if !users.is_empty() {
911 self.admin_selected_user = (self.admin_selected_user + 1) % users.len();
912 }
913 }
914 }
915
916 pub fn admin_previous_user(&mut self) {
918 if let Some(ref users) = self.detailed_users {
919 if !users.is_empty() {
920 if self.admin_selected_user == 0 {
921 self.admin_selected_user = users.len() - 1;
922 } else {
923 self.admin_selected_user -= 1;
924 }
925 }
926 }
927 }
928
929 pub fn admin_get_selected_user(&self) -> Option<&DetailedUser> {
931 self.detailed_users
932 .as_ref()
933 .and_then(|users| users.get(self.admin_selected_user))
934 }
935
936 pub fn admin_scroll_up(&mut self) {
938 self.admin_scroll_offset = self.admin_scroll_offset.saturating_sub(1);
939 }
940
941 pub fn admin_scroll_down(&mut self) {
943 self.admin_scroll_offset = self.admin_scroll_offset.saturating_add(1);
944 }
945
946 pub fn admin_reset_scroll(&mut self) {
948 self.admin_scroll_offset = 0;
949 }
950
951 pub fn status_scroll_up(&mut self) {
953 self.status_scroll_offset = self.status_scroll_offset.saturating_add(1);
954 }
955
956 pub fn status_scroll_down(&mut self) {
958 self.status_scroll_offset = self.status_scroll_offset.saturating_sub(1);
959 }
960
961 pub fn status_reset_scroll(&mut self) {
963 self.status_scroll_offset = 0;
964 }
965
966 pub fn help_scroll_up(&mut self) {
968 self.help_scroll_offset = self.help_scroll_offset.saturating_add(1);
969 }
970
971 pub fn help_scroll_down(&mut self) {
973 self.help_scroll_offset = self.help_scroll_offset.saturating_sub(1);
974 }
975
976 pub fn help_reset_scroll(&mut self) {
978 self.help_scroll_offset = 0;
979 }
980
981 pub fn get_conversation_mut(&mut self, peer: &str) -> &mut ConversationView {
985 self.conversations
986 .entry(peer.to_string())
987 .or_insert_with(|| ConversationView::new(peer.to_string()))
988 }
989
990 pub fn get_conversation(&self, peer: &str) -> Option<&ConversationView> {
994 self.conversations.get(peer)
995 }
996
997 pub fn add_system_message(&mut self, text: String, sys_code: Option<String>) {
1011 if let Some(ref code) = sys_code {
1013 match code.as_str() {
1014 "server_connection_up" => self.server_connection_up = true,
1015 "server_connection_down" => {
1016 self.server_connection_up = false;
1017 for user in &mut self.users {
1019 user.online = false;
1020 }
1021 }
1022 _ => {}
1023 }
1024 }
1025
1026 let message = if let Some(code) = sys_code {
1027 SystemMessage::with_code(text, code)
1028 } else {
1029 SystemMessage::new(text)
1030 };
1031
1032 if self.current_system_message.is_none() {
1033 self.current_system_message = Some(message);
1035 } else {
1036 self.system_message_queue.push_back(message);
1038 }
1039 }
1040
1041 pub fn update_system_message(&mut self) {
1046 if let Some(ref current) = self.current_system_message {
1047 if current.can_advance() {
1049 if let Some(mut next_message) = self.system_message_queue.pop_front() {
1051 next_message.reset_timestamp();
1053 self.current_system_message = Some(next_message);
1054 } else {
1055 self.current_system_message = None;
1057 }
1058 }
1059 }
1060 }
1061
1062 pub fn get_current_system_message(&self) -> Option<&str> {
1064 self.current_system_message.as_ref().map(|m| m.text.as_str())
1065 }
1066
1067 #[allow(dead_code)]
1069 pub fn clear_system_messages(&mut self) {
1070 self.current_system_message = None;
1071 self.system_message_queue.clear();
1072 }
1073}
1074
1075#[derive(Debug, Clone, Copy, PartialEq)]
1079pub enum AppScreen {
1080 Splash,
1082 Main,
1084}
1085
1086#[derive(Debug, Clone)]
1104pub struct Secret(String);
1105
1106impl Secret {
1107 pub fn new(v: String) -> Self {
1120 Self(v)
1121 }
1122
1123 #[allow(dead_code)]
1135 pub fn as_str(&self) -> &str {
1136 &self.0
1137 }
1138}
1139
1140impl Drop for Secret {
1142 fn drop(&mut self) {
1143 self.0.zeroize();
1144 }
1145}