1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
//! Screen enum: tags the currently-displayed overlay or view.
use std::path::PathBuf;
/// Top-level page selected via the top navigation bar.
///
/// Orthogonal to [`Screen`]. `Screen` tracks overlays and modal forms,
/// `TopPage` tracks which base view (hosts vs tunnels) renders behind them.
/// Tab/Shift+Tab cycles through the variants when no overlay is active.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum TopPage {
#[default]
Hosts,
Tunnels,
}
impl TopPage {
/// Cycle to the next page (Hosts -> Tunnels -> Hosts).
pub fn next(self) -> Self {
match self {
TopPage::Hosts => TopPage::Tunnels,
TopPage::Tunnels => TopPage::Hosts,
}
}
/// Cycle to the previous page. With two variants this is the same as `next`.
pub fn prev(self) -> Self {
self.next()
}
}
/// State for the What's New overlay.
#[derive(Debug, Default, Clone, PartialEq)]
pub struct WhatsNewState {
pub scroll: u16,
}
/// Which screen is currently displayed.
#[derive(Debug, Clone, PartialEq)]
pub enum Screen {
HostList,
AddHost,
EditHost {
alias: String,
},
ConfirmDelete {
alias: String,
},
Help {
return_screen: Box<Screen>,
},
KeyList,
KeyDetail {
index: usize,
},
HostDetail {
index: usize,
},
TagPicker,
ThemePicker,
Providers,
ProviderForm {
provider: String,
},
TunnelList {
alias: String,
},
TunnelForm {
alias: String,
editing: Option<usize>,
},
/// Host picker reached from the Tunnels overview when adding a new
/// tunnel: the user must choose a host before the tunnel form opens.
/// On confirm, transitions to `TunnelForm { alias, editing: None }`.
TunnelHostPicker,
SnippetPicker {
target_aliases: Vec<String>,
},
SnippetForm {
target_aliases: Vec<String>,
editing: Option<usize>,
},
SnippetOutput {
snippet_name: String,
target_aliases: Vec<String>,
},
SnippetParamForm {
snippet: crate::snippet::Snippet,
target_aliases: Vec<String>,
},
ConfirmHostKeyReset {
alias: String,
hostname: String,
known_hosts_path: String,
askpass: Option<String>,
},
FileBrowser {
alias: String,
},
Containers {
alias: String,
},
ConfirmImport {
count: usize,
},
ConfirmPurgeStale {
aliases: Vec<String>,
provider: Option<String>,
},
ConfirmVaultSign {
/// Precomputed list of (alias, role, certificate_file, pubkey_path) for
/// hosts that resolve to a vault SSH role. Computed when the user
/// presses `V`. `certificate_file` is the host's existing
/// `CertificateFile` directive (empty when unset) and is needed so the
/// background worker checks renewal status against the actually
/// configured cert path rather than purple's default.
signable: Vec<(String, String, String, PathBuf, Option<String>)>,
},
Welcome {
has_backup: bool,
host_count: usize,
known_hosts_count: usize,
},
/// Bulk tag editor: tri-state checkbox picker that edits tags across
/// all hosts in `multi_select` in one go. Opened via `t` when a
/// multi-host selection is active.
BulkTagEditor,
/// What's New overlay: shows recent changelog sections to the user
/// after an upgrade. Opened via the upgrade toast or `n` key.
WhatsNew(WhatsNewState),
}
impl Screen {
/// Stable short variant name used in state-transition logs.
/// Omits inner fields so log lines never leak host aliases, paths or
/// tokens.
pub fn variant_name(&self) -> &'static str {
match self {
Screen::HostList => "HostList",
Screen::AddHost => "AddHost",
Screen::EditHost { .. } => "EditHost",
Screen::ConfirmDelete { .. } => "ConfirmDelete",
Screen::Help { .. } => "Help",
Screen::KeyList => "KeyList",
Screen::KeyDetail { .. } => "KeyDetail",
Screen::HostDetail { .. } => "HostDetail",
Screen::TagPicker => "TagPicker",
Screen::ThemePicker => "ThemePicker",
Screen::Providers => "Providers",
Screen::ProviderForm { .. } => "ProviderForm",
Screen::TunnelList { .. } => "TunnelList",
Screen::TunnelForm { .. } => "TunnelForm",
Screen::TunnelHostPicker => "TunnelHostPicker",
Screen::SnippetPicker { .. } => "SnippetPicker",
Screen::SnippetForm { .. } => "SnippetForm",
Screen::SnippetOutput { .. } => "SnippetOutput",
Screen::SnippetParamForm { .. } => "SnippetParamForm",
Screen::ConfirmHostKeyReset { .. } => "ConfirmHostKeyReset",
Screen::FileBrowser { .. } => "FileBrowser",
Screen::Containers { .. } => "Containers",
Screen::ConfirmImport { .. } => "ConfirmImport",
Screen::ConfirmPurgeStale { .. } => "ConfirmPurgeStale",
Screen::ConfirmVaultSign { .. } => "ConfirmVaultSign",
Screen::Welcome { .. } => "Welcome",
Screen::BulkTagEditor => "BulkTagEditor",
Screen::WhatsNew(_) => "WhatsNew",
}
}
}