1use crate::app::App;
6use crate::app::Screen;
7use crate::app::reload_state::{
8 config_changed, get_mtime, snapshot_include_dir_mtimes, snapshot_include_mtimes,
9};
10use crate::app::{HostForm, SnippetForm, TunnelForm};
11use crate::snippet::Snippet;
12use crate::ssh_config::model::PatternEntry;
13use crate::tunnel::TunnelRule;
14
15#[derive(Clone)]
17pub struct FormBaseline {
18 pub alias: String,
19 pub hostname: String,
20 pub user: String,
21 pub port: String,
22 pub identity_file: String,
23 pub proxy_jump: String,
24 pub askpass: String,
25 pub vault_ssh: String,
26 pub vault_addr: String,
27 pub tags: String,
28}
29
30#[derive(Clone)]
32pub struct TunnelFormBaseline {
33 pub tunnel_type: crate::tunnel::TunnelType,
34 pub bind_port: String,
35 pub remote_host: String,
36 pub remote_port: String,
37 pub bind_address: String,
38}
39
40#[derive(Clone)]
42pub struct SnippetFormBaseline {
43 pub name: String,
44 pub command: String,
45 pub description: String,
46}
47
48#[derive(Clone)]
50pub struct ProviderFormBaseline {
51 pub url: String,
52 pub token: String,
53 pub profile: String,
54 pub project: String,
55 pub compartment: String,
56 pub regions: String,
57 pub alias_prefix: String,
58 pub user: String,
59 pub identity_file: String,
60 pub verify_tls: bool,
61 pub auto_sync: bool,
62 pub vault_role: String,
63 pub vault_addr: String,
64}
65
66impl App {
67 pub fn clear_form_mtime(&mut self) {
69 self.conflict.clear_form_mtimes();
70 }
71
72 pub fn capture_form_mtime(&mut self) {
74 self.conflict.form_mtime = get_mtime(&self.reload.config_path);
75 self.conflict.form_include_mtimes = snapshot_include_mtimes(&self.hosts_state.ssh_config);
76 self.conflict.form_include_dir_mtimes =
77 snapshot_include_dir_mtimes(&self.hosts_state.ssh_config);
78 }
79
80 pub fn capture_provider_form_mtime(&mut self) {
82 let path = dirs::home_dir().map(|h| h.join(".purple/providers"));
83 self.conflict.provider_form_mtime = path.as_ref().and_then(|p| get_mtime(p));
84 }
85
86 pub fn capture_form_baseline(&mut self) {
88 self.forms.host_baseline = Some(FormBaseline {
89 alias: self.forms.host.alias.clone(),
90 hostname: self.forms.host.hostname.clone(),
91 user: self.forms.host.user.clone(),
92 port: self.forms.host.port.clone(),
93 identity_file: self.forms.host.identity_file.clone(),
94 proxy_jump: self.forms.host.proxy_jump.clone(),
95 askpass: self.forms.host.askpass.clone(),
96 vault_ssh: self.forms.host.vault_ssh.clone(),
97 vault_addr: self.forms.host.vault_addr.clone(),
98 tags: self.forms.host.tags.clone(),
99 });
100 }
101
102 pub fn host_form_is_dirty(&self) -> bool {
104 self.forms.host_form_is_dirty()
105 }
106
107 pub fn close_host_form(&mut self) {
110 self.close_host_form_inner(None);
111 }
112
113 pub fn close_host_form_after_save(&mut self, target_alias: &str) {
116 self.close_host_form_inner(Some(target_alias));
117 }
118
119 fn close_host_form_inner(&mut self, select: Option<&str>) {
120 log::debug!("[purple] close_host_form select={:?}", select);
121 self.clear_form_mtime();
122 self.forms.host_baseline = None;
123 self.set_screen(Screen::HostList);
124 if let Some(alias) = select {
125 self.select_host_by_alias(alias);
126 }
127 self.flush_pending_vault_write();
128 }
129
130 pub fn close_provider_form(&mut self) {
133 log::debug!("[purple] close_provider_form");
134 self.clear_form_mtime();
135 self.providers.form_baseline = None;
136 self.set_screen(Screen::Providers);
137 self.flush_pending_vault_write();
138 }
139
140 pub fn close_tunnel_form(&mut self, return_to: Screen) {
144 log::debug!(
145 "[purple] close_tunnel_form return_to={:?}",
146 std::mem::discriminant(&return_to)
147 );
148 self.clear_form_mtime();
149 self.tunnels.form_baseline = None;
150 self.set_screen(return_to);
151 }
152
153 pub fn close_snippet_form(&mut self, target_aliases: Vec<String>) {
157 log::debug!(
158 "[purple] close_snippet_form aliases={}",
159 target_aliases.len()
160 );
161 self.snippets.form_baseline = None;
162 self.set_screen(Screen::SnippetPicker { target_aliases });
163 }
164
165 pub fn open_host_add_form(&mut self) {
167 log::debug!("[purple] open_host_add_form");
168 self.forms.host = HostForm::new();
169 self.set_screen(Screen::AddHost);
170 self.capture_form_mtime();
171 self.capture_form_baseline();
172 }
173
174 pub fn open_host_pattern_add_form(&mut self) {
177 log::debug!("[purple] open_host_pattern_add_form");
178 self.forms.host = HostForm::new_pattern();
179 self.set_screen(Screen::AddHost);
180 self.capture_form_mtime();
181 self.capture_form_baseline();
182 }
183
184 pub fn open_host_edit_form(
189 &mut self,
190 host: crate::ssh_config::model::HostEntry,
191 stale_hint: Option<String>,
192 ) -> bool {
193 if let Some(ref source) = host.source_file {
194 self.notify_error(crate::messages::included_host_lives_in(
195 &host.alias,
196 &source.display(),
197 ));
198 return false;
199 }
200 let raw = match self.hosts_state.ssh_config.raw_host_entry(&host.alias) {
203 Some(entry) => entry,
204 None => {
205 self.notify_warning(crate::messages::HOST_NOT_FOUND_IN_CONFIG);
206 return false;
207 }
208 };
209 let inherited = self.hosts_state.ssh_config.inherited_hints(&host.alias);
210 log::debug!("[purple] open_host_edit_form alias={}", host.alias);
211 self.forms.host = HostForm::from_entry(&raw, inherited);
212 if let Some(hint) = stale_hint {
213 self.notify_warning(crate::messages::stale_host(&hint));
214 }
215 self.set_screen(Screen::EditHost { alias: host.alias });
216 self.capture_form_mtime();
217 self.capture_form_baseline();
218 true
219 }
220
221 pub fn open_host_pattern_edit_form(&mut self, pattern: &PatternEntry) {
223 log::debug!(
224 "[purple] open_host_pattern_edit_form pattern={}",
225 pattern.pattern
226 );
227 self.forms.host = HostForm::from_pattern_entry(pattern);
228 self.set_screen(Screen::EditHost {
229 alias: pattern.pattern.clone(),
230 });
231 self.capture_form_mtime();
232 self.capture_form_baseline();
233 }
234
235 pub fn open_tunnel_add_form(&mut self, alias: String) {
238 log::debug!("[purple] open_tunnel_add_form alias={}", alias);
239 self.tunnels.form = TunnelForm::new();
240 self.set_screen(Screen::TunnelForm {
241 alias,
242 editing: None,
243 });
244 self.capture_form_mtime();
245 self.capture_tunnel_form_baseline();
246 }
247
248 pub fn open_tunnel_edit_form(&mut self, alias: String, rule: &TunnelRule, editing: usize) {
251 log::debug!(
252 "[purple] open_tunnel_edit_form alias={} editing={}",
253 alias,
254 editing
255 );
256 self.tunnels.form = TunnelForm::from_rule(rule);
257 self.set_screen(Screen::TunnelForm {
258 alias,
259 editing: Some(editing),
260 });
261 self.capture_form_mtime();
262 self.capture_tunnel_form_baseline();
263 }
264
265 pub fn open_snippet_add_form(&mut self, target_aliases: Vec<String>) {
268 log::debug!(
269 "[purple] open_snippet_add_form aliases={}",
270 target_aliases.len()
271 );
272 self.snippets.form = SnippetForm::new();
273 self.set_screen(Screen::SnippetForm {
274 target_aliases,
275 editing: None,
276 });
277 self.capture_snippet_form_baseline();
278 }
279
280 pub fn open_snippet_edit_form(
283 &mut self,
284 snippet: &Snippet,
285 target_aliases: Vec<String>,
286 editing: usize,
287 ) {
288 log::debug!(
289 "[purple] open_snippet_edit_form name={} editing={}",
290 snippet.name,
291 editing
292 );
293 self.snippets.form = SnippetForm::from_snippet(snippet);
294 self.set_screen(Screen::SnippetForm {
295 target_aliases,
296 editing: Some(editing),
297 });
298 self.capture_snippet_form_baseline();
299 }
300
301 pub fn open_provider_form(&mut self, id: crate::providers::config::ProviderConfigId) {
305 let provider_impl = crate::providers::get_provider(id.provider.as_str());
306 let short_label = provider_impl
307 .as_ref()
308 .map(|p| p.short_label().to_string())
309 .unwrap_or_else(|| id.provider.clone());
310 let existing_section = self.providers.config.section_by_id(&id).cloned();
311 let label_entry = existing_section.is_none() && id.label.as_deref() == Some("");
312 let provider_first_field =
313 crate::app::ProviderFormField::fields_for(id.provider.as_str())[0];
314 let first_field = if label_entry {
315 crate::app::ProviderFormField::Label
316 } else {
317 provider_first_field
318 };
319 log::debug!(
320 "[purple] open_provider_form provider={} label_entry={}",
321 id.provider,
322 label_entry
323 );
324
325 self.providers.form = if let Some(section) = existing_section {
326 let cursor_pos = match first_field {
327 crate::app::ProviderFormField::Url => section.url.chars().count(),
328 crate::app::ProviderFormField::Token => section.token.chars().count(),
329 _ => 0,
330 };
331 crate::app::ProviderFormFields {
332 label: String::new(),
333 label_entry: false,
334 url: section.url.clone(),
335 token: section.token.clone(),
336 profile: section.profile.clone(),
337 project: section.project.clone(),
338 compartment: section.compartment.clone(),
339 regions: section.regions.clone(),
340 alias_prefix: section.alias_prefix.clone(),
341 user: section.user.clone(),
342 identity_file: section.identity_file.clone(),
343 verify_tls: section.verify_tls,
344 auto_sync: section.auto_sync,
345 vault_role: section.vault_role.clone(),
346 vault_addr: section.vault_addr.clone(),
347 focused_field: first_field,
348 cursor_pos,
349 expanded: true,
350 }
351 } else {
352 let default_prefix = match id.label.as_deref() {
357 Some("") | None => short_label.clone(),
358 Some(l) => format!("{}-{}", short_label, l),
359 };
360 crate::app::ProviderFormFields {
361 label: String::new(),
362 label_entry,
363 url: String::new(),
364 token: String::new(),
365 profile: String::new(),
366 project: String::new(),
367 compartment: String::new(),
368 regions: String::new(),
369 alias_prefix: default_prefix,
370 user: "root".to_string(),
371 identity_file: String::new(),
372 verify_tls: true,
373 auto_sync: id
374 .kind()
375 .is_none_or(crate::providers::ProviderKind::default_auto_sync),
376 vault_role: String::new(),
377 vault_addr: String::new(),
378 focused_field: first_field,
379 cursor_pos: 0,
380 expanded: false,
381 }
382 };
383 self.set_screen(Screen::ProviderForm { id });
384 self.capture_provider_form_mtime();
385 self.capture_provider_form_baseline();
386 }
387
388 pub fn capture_tunnel_form_baseline(&mut self) {
390 self.tunnels.form_baseline = Some(TunnelFormBaseline {
391 tunnel_type: self.tunnels.form.tunnel_type,
392 bind_port: self.tunnels.form.bind_port.clone(),
393 remote_host: self.tunnels.form.remote_host.clone(),
394 remote_port: self.tunnels.form.remote_port.clone(),
395 bind_address: self.tunnels.form.bind_address.clone(),
396 });
397 }
398
399 pub fn tunnel_form_is_dirty(&self) -> bool {
401 self.tunnels.form_is_dirty()
402 }
403
404 pub fn capture_snippet_form_baseline(&mut self) {
406 self.snippets.form_baseline = Some(SnippetFormBaseline {
407 name: self.snippets.form.name.clone(),
408 command: self.snippets.form.command.clone(),
409 description: self.snippets.form.description.clone(),
410 });
411 }
412
413 pub fn snippet_form_is_dirty(&self) -> bool {
415 self.snippets.form_is_dirty()
416 }
417
418 pub fn capture_provider_form_baseline(&mut self) {
420 self.providers.form_baseline = Some(ProviderFormBaseline {
421 url: self.providers.form.url.clone(),
422 token: self.providers.form.token.clone(),
423 profile: self.providers.form.profile.clone(),
424 project: self.providers.form.project.clone(),
425 compartment: self.providers.form.compartment.clone(),
426 regions: self.providers.form.regions.clone(),
427 alias_prefix: self.providers.form.alias_prefix.clone(),
428 user: self.providers.form.user.clone(),
429 identity_file: self.providers.form.identity_file.clone(),
430 verify_tls: self.providers.form.verify_tls,
431 auto_sync: self.providers.form.auto_sync,
432 vault_role: self.providers.form.vault_role.clone(),
433 vault_addr: self.providers.form.vault_addr.clone(),
434 });
435 }
436
437 pub fn provider_form_is_dirty(&self) -> bool {
439 self.providers.form_is_dirty()
440 }
441
442 pub fn config_changed_since_form_open(&self) -> bool {
444 config_changed(&self.conflict, &self.reload.config_path)
445 }
446
447 pub fn provider_config_changed_since_form_open(&self) -> bool {
449 let path = dirs::home_dir().map(|h| h.join(".purple/providers"));
450 let current_mtime = path.as_ref().and_then(|p| get_mtime(p));
451 self.conflict.provider_form_mtime != current_mtime
452 }
453}