virtuoso_cli/tui/app/
state.rs1use crate::models::{SessionInfo, TunnelState};
2use crate::spectre::jobs::Job;
3use crate::tui::app::overlay::Overlay;
4use std::time::Instant;
5
6#[derive(Clone, Copy, PartialEq, Eq)]
7pub enum Tab {
8 Sessions,
9 Jobs,
10 Config,
11}
12
13impl Tab {
14 pub fn label(self) -> &'static str {
15 match self {
16 Tab::Sessions => "Sessions",
17 Tab::Jobs => "Jobs",
18 Tab::Config => "Config",
19 }
20 }
21
22 pub fn next(self) -> Self {
23 match self {
24 Tab::Sessions => Tab::Jobs,
25 Tab::Jobs => Tab::Config,
26 Tab::Config => Tab::Sessions,
27 }
28 }
29
30 pub fn prev(self) -> Self {
31 match self {
32 Tab::Sessions => Tab::Config,
33 Tab::Jobs => Tab::Sessions,
34 Tab::Config => Tab::Jobs,
35 }
36 }
37
38 pub fn all() -> [Tab; 3] {
39 [Tab::Sessions, Tab::Jobs, Tab::Config]
40 }
41}
42
43#[derive(Clone, Copy, PartialEq, Eq)]
44pub enum StatusKind {
45 Info,
46 Ok,
47 Warn,
48 Err,
49}
50
51pub struct StatusToast {
52 pub message: String,
53 pub kind: StatusKind,
54 pub at: Instant,
55}
56
57pub struct ConfigField {
59 pub key: String,
60 pub value: String,
61 pub hint: &'static str,
62}
63
64pub struct App {
65 pub tab: Tab,
66 pub overlay: Overlay,
67
68 pub sessions: Vec<SessionInfo>,
69 pub jobs: Vec<Job>,
70 pub tunnel_state: Option<TunnelState>,
71 pub config_fields: Vec<ConfigField>,
72
73 pub selected_session: usize,
74 pub selected_job: usize,
75 pub selected_config: usize,
76
77 pub spinner_frame: usize,
78 pub status: Option<StatusToast>,
79 pub should_quit: bool,
80}
81
82impl App {
83 pub fn new() -> Self {
84 let mut app = Self {
85 tab: Tab::Sessions,
86 overlay: Overlay::None,
87 sessions: Vec::new(),
88 jobs: Vec::new(),
89 tunnel_state: None,
90 config_fields: Vec::new(),
91 selected_session: 0,
92 selected_job: 0,
93 selected_config: 0,
94 spinner_frame: 0,
95 status: None,
96 should_quit: false,
97 };
98 crate::tui::app::data::initial_load(&mut app);
99 app
100 }
101
102 pub fn set_status(&mut self, message: impl Into<String>, kind: StatusKind) {
103 self.status = Some(StatusToast {
104 message: message.into(),
105 kind,
106 at: Instant::now(),
107 });
108 }
109
110 pub fn clear_expired_status(&mut self) {
111 if let Some(s) = &self.status {
112 if s.at.elapsed().as_secs() >= 3 {
113 self.status = None;
114 }
115 }
116 }
117
118 pub fn selected_session_info(&self) -> Option<&SessionInfo> {
119 self.sessions.get(self.selected_session)
120 }
121
122 pub fn selected_config_field(&self) -> Option<&ConfigField> {
123 self.config_fields.get(self.selected_config)
124 }
125
126 pub fn move_selection(&mut self, delta: i64) {
128 let (cursor, len) = match self.tab {
129 Tab::Sessions => (&mut self.selected_session, self.sessions.len()),
130 Tab::Jobs => (&mut self.selected_job, self.jobs.len()),
131 Tab::Config => (&mut self.selected_config, self.config_fields.len()),
132 };
133 if len == 0 {
134 *cursor = 0;
135 return;
136 }
137 let new = (*cursor as i64 + delta).rem_euclid(len as i64);
138 *cursor = new as usize;
139 }
140}
141
142impl Default for App {
143 fn default() -> Self {
144 Self::new()
145 }
146}