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)]
47pub enum Focus {
48 Content,
49}
50
51#[derive(Clone, Copy, PartialEq, Eq)]
52pub enum StatusKind {
53 Info,
54 Ok,
55 Warn,
56 Err,
57}
58
59pub struct StatusToast {
60 pub message: String,
61 pub kind: StatusKind,
62 pub at: Instant,
63}
64
65pub struct ConfigField {
67 pub key: String,
68 pub value: String,
69 pub hint: &'static str,
70}
71
72pub struct App {
73 pub tab: Tab,
74 pub focus: Focus,
75 pub overlay: Overlay,
76
77 pub sessions: Vec<SessionInfo>,
78 pub jobs: Vec<Job>,
79 pub tunnel_state: Option<TunnelState>,
80 pub config_fields: Vec<ConfigField>,
81
82 pub selected_session: usize,
83 pub selected_job: usize,
84 pub selected_config: usize,
85
86 pub spinner_frame: usize,
87 pub status: Option<StatusToast>,
88 pub should_quit: bool,
89}
90
91impl App {
92 pub fn new() -> Self {
93 let mut app = Self {
94 tab: Tab::Sessions,
95 focus: Focus::Content,
96 overlay: Overlay::None,
97 sessions: Vec::new(),
98 jobs: Vec::new(),
99 tunnel_state: None,
100 config_fields: Vec::new(),
101 selected_session: 0,
102 selected_job: 0,
103 selected_config: 0,
104 spinner_frame: 0,
105 status: None,
106 should_quit: false,
107 };
108 crate::tui::app::data::initial_load(&mut app);
109 app
110 }
111
112 pub fn set_status(&mut self, message: impl Into<String>, kind: StatusKind) {
113 self.status = Some(StatusToast {
114 message: message.into(),
115 kind,
116 at: Instant::now(),
117 });
118 }
119
120 pub fn clear_expired_status(&mut self) {
121 if let Some(s) = &self.status {
122 if s.at.elapsed().as_secs() >= 3 {
123 self.status = None;
124 }
125 }
126 }
127
128 pub fn selected_session_info(&self) -> Option<&SessionInfo> {
129 self.sessions.get(self.selected_session)
130 }
131
132 pub fn selected_config_field(&self) -> Option<&ConfigField> {
133 self.config_fields.get(self.selected_config)
134 }
135
136 pub fn move_selection(&mut self, delta: i64) {
138 let (cursor, len) = match self.tab {
139 Tab::Sessions => (&mut self.selected_session, self.sessions.len()),
140 Tab::Jobs => (&mut self.selected_job, self.jobs.len()),
141 Tab::Config => (&mut self.selected_config, self.config_fields.len()),
142 };
143 if len == 0 {
144 *cursor = 0;
145 return;
146 }
147 let new = (*cursor as i64 + delta).rem_euclid(len as i64);
148 *cursor = new as usize;
149 }
150}
151
152impl Default for App {
153 fn default() -> Self {
154 Self::new()
155 }
156}