1use crate::app::App;
6use crate::app::Screen;
7use crate::app::reload_state::{get_mtime, snapshot_include_dir_mtimes, snapshot_include_mtimes};
8use crate::app::{HostForm, SnippetForm, TunnelForm};
9use crate::snippet::Snippet;
10use crate::ssh_config::model::PatternEntry;
11use crate::tunnel::TunnelRule;
12
13#[derive(Clone)]
15pub struct FormBaseline {
16 pub alias: String,
17 pub hostname: String,
18 pub user: String,
19 pub port: String,
20 pub identity_file: String,
21 pub proxy_jump: String,
22 pub askpass: String,
23 pub vault_ssh: String,
24 pub vault_addr: String,
25 pub tags: String,
26}
27
28#[derive(Clone)]
30pub struct TunnelFormBaseline {
31 pub tunnel_type: crate::tunnel::TunnelType,
32 pub bind_port: String,
33 pub remote_host: String,
34 pub remote_port: String,
35 pub bind_address: String,
36}
37
38#[derive(Clone)]
40pub struct SnippetFormBaseline {
41 pub name: String,
42 pub command: String,
43 pub description: String,
44}
45
46#[derive(Clone)]
48pub struct ProviderFormBaseline {
49 pub url: String,
50 pub token: String,
51 pub profile: String,
52 pub project: String,
53 pub compartment: String,
54 pub regions: String,
55 pub alias_prefix: String,
56 pub user: String,
57 pub identity_file: String,
58 pub verify_tls: bool,
59 pub auto_sync: bool,
60 pub vault_role: String,
61 pub vault_addr: String,
62}
63
64impl App {
65 pub fn clear_form_mtime(&mut self) {
67 self.conflict.form_mtime = None;
68 self.conflict.form_include_mtimes.clear();
69 self.conflict.form_include_dir_mtimes.clear();
70 self.conflict.provider_form_mtime = None;
71 }
72
73 pub fn capture_form_mtime(&mut self) {
75 self.conflict.form_mtime = get_mtime(&self.reload.config_path);
76 self.conflict.form_include_mtimes = snapshot_include_mtimes(&self.hosts_state.ssh_config);
77 self.conflict.form_include_dir_mtimes =
78 snapshot_include_dir_mtimes(&self.hosts_state.ssh_config);
79 }
80
81 pub fn capture_provider_form_mtime(&mut self) {
83 let path = dirs::home_dir().map(|h| h.join(".purple/providers"));
84 self.conflict.provider_form_mtime = path.as_ref().and_then(|p| get_mtime(p));
85 }
86
87 pub fn capture_form_baseline(&mut self) {
89 self.forms.host_baseline = Some(FormBaseline {
90 alias: self.forms.host.alias.clone(),
91 hostname: self.forms.host.hostname.clone(),
92 user: self.forms.host.user.clone(),
93 port: self.forms.host.port.clone(),
94 identity_file: self.forms.host.identity_file.clone(),
95 proxy_jump: self.forms.host.proxy_jump.clone(),
96 askpass: self.forms.host.askpass.clone(),
97 vault_ssh: self.forms.host.vault_ssh.clone(),
98 vault_addr: self.forms.host.vault_addr.clone(),
99 tags: self.forms.host.tags.clone(),
100 });
101 }
102
103 pub fn host_form_is_dirty(&self) -> bool {
105 match &self.forms.host_baseline {
106 Some(b) => {
107 self.forms.host.alias != b.alias
108 || self.forms.host.hostname != b.hostname
109 || self.forms.host.user != b.user
110 || self.forms.host.port != b.port
111 || self.forms.host.identity_file != b.identity_file
112 || self.forms.host.proxy_jump != b.proxy_jump
113 || self.forms.host.askpass != b.askpass
114 || self.forms.host.vault_ssh != b.vault_ssh
115 || self.forms.host.vault_addr != b.vault_addr
116 || self.forms.host.tags != b.tags
117 }
118 None => false,
119 }
120 }
121
122 pub fn close_host_form(&mut self) {
125 self.close_host_form_inner(None);
126 }
127
128 pub fn close_host_form_after_save(&mut self, target_alias: &str) {
131 self.close_host_form_inner(Some(target_alias));
132 }
133
134 fn close_host_form_inner(&mut self, select: Option<&str>) {
135 log::debug!("[purple] close_host_form select={:?}", select);
136 self.clear_form_mtime();
137 self.forms.host_baseline = None;
138 self.set_screen(Screen::HostList);
139 if let Some(alias) = select {
140 self.select_host_by_alias(alias);
141 }
142 self.flush_pending_vault_write();
143 }
144
145 pub fn close_provider_form(&mut self) {
148 log::debug!("[purple] close_provider_form");
149 self.clear_form_mtime();
150 self.providers.form_baseline = None;
151 self.set_screen(Screen::Providers);
152 self.flush_pending_vault_write();
153 }
154
155 pub fn close_tunnel_form(&mut self, return_to: Screen) {
159 log::debug!(
160 "[purple] close_tunnel_form return_to={:?}",
161 std::mem::discriminant(&return_to)
162 );
163 self.clear_form_mtime();
164 self.tunnels.form_baseline = None;
165 self.set_screen(return_to);
166 }
167
168 pub fn close_snippet_form(&mut self, target_aliases: Vec<String>) {
172 log::debug!(
173 "[purple] close_snippet_form aliases={}",
174 target_aliases.len()
175 );
176 self.snippets.form_baseline = None;
177 self.set_screen(Screen::SnippetPicker { target_aliases });
178 }
179
180 pub fn open_host_add_form(&mut self) {
182 log::debug!("[purple] open_host_add_form");
183 self.forms.host = HostForm::new();
184 self.set_screen(Screen::AddHost);
185 self.capture_form_mtime();
186 self.capture_form_baseline();
187 }
188
189 pub fn open_host_pattern_add_form(&mut self) {
192 log::debug!("[purple] open_host_pattern_add_form");
193 self.forms.host = HostForm::new_pattern();
194 self.set_screen(Screen::AddHost);
195 self.capture_form_mtime();
196 self.capture_form_baseline();
197 }
198
199 pub fn open_host_edit_form(
204 &mut self,
205 host: crate::ssh_config::model::HostEntry,
206 stale_hint: Option<String>,
207 ) -> bool {
208 if let Some(ref source) = host.source_file {
209 self.notify_error(crate::messages::included_host_lives_in(
210 &host.alias,
211 &source.display(),
212 ));
213 return false;
214 }
215 let raw = match self.hosts_state.ssh_config.raw_host_entry(&host.alias) {
218 Some(entry) => entry,
219 None => {
220 self.notify_warning(crate::messages::HOST_NOT_FOUND_IN_CONFIG);
221 return false;
222 }
223 };
224 let inherited = self.hosts_state.ssh_config.inherited_hints(&host.alias);
225 log::debug!("[purple] open_host_edit_form alias={}", host.alias);
226 self.forms.host = HostForm::from_entry(&raw, inherited);
227 if let Some(hint) = stale_hint {
228 self.notify_warning(crate::messages::stale_host(&hint));
229 }
230 self.set_screen(Screen::EditHost { alias: host.alias });
231 self.capture_form_mtime();
232 self.capture_form_baseline();
233 true
234 }
235
236 pub fn open_host_pattern_edit_form(&mut self, pattern: &PatternEntry) {
238 log::debug!(
239 "[purple] open_host_pattern_edit_form pattern={}",
240 pattern.pattern
241 );
242 self.forms.host = HostForm::from_pattern_entry(pattern);
243 self.set_screen(Screen::EditHost {
244 alias: pattern.pattern.clone(),
245 });
246 self.capture_form_mtime();
247 self.capture_form_baseline();
248 }
249
250 pub fn open_tunnel_add_form(&mut self, alias: String) {
253 log::debug!("[purple] open_tunnel_add_form alias={}", alias);
254 self.tunnels.form = TunnelForm::new();
255 self.set_screen(Screen::TunnelForm {
256 alias,
257 editing: None,
258 });
259 self.capture_form_mtime();
260 self.capture_tunnel_form_baseline();
261 }
262
263 pub fn open_tunnel_edit_form(&mut self, alias: String, rule: &TunnelRule, editing: usize) {
266 log::debug!(
267 "[purple] open_tunnel_edit_form alias={} editing={}",
268 alias,
269 editing
270 );
271 self.tunnels.form = TunnelForm::from_rule(rule);
272 self.set_screen(Screen::TunnelForm {
273 alias,
274 editing: Some(editing),
275 });
276 self.capture_form_mtime();
277 self.capture_tunnel_form_baseline();
278 }
279
280 pub fn open_snippet_add_form(&mut self, target_aliases: Vec<String>) {
283 log::debug!(
284 "[purple] open_snippet_add_form aliases={}",
285 target_aliases.len()
286 );
287 self.snippets.form = SnippetForm::new();
288 self.set_screen(Screen::SnippetForm {
289 target_aliases,
290 editing: None,
291 });
292 self.capture_snippet_form_baseline();
293 }
294
295 pub fn open_snippet_edit_form(
298 &mut self,
299 snippet: &Snippet,
300 target_aliases: Vec<String>,
301 editing: usize,
302 ) {
303 log::debug!(
304 "[purple] open_snippet_edit_form name={} editing={}",
305 snippet.name,
306 editing
307 );
308 self.snippets.form = SnippetForm::from_snippet(snippet);
309 self.set_screen(Screen::SnippetForm {
310 target_aliases,
311 editing: Some(editing),
312 });
313 self.capture_snippet_form_baseline();
314 }
315
316 pub fn open_provider_form(&mut self, id: crate::providers::config::ProviderConfigId) {
320 let provider_impl = crate::providers::get_provider(id.provider.as_str());
321 let short_label = provider_impl
322 .as_ref()
323 .map(|p| p.short_label().to_string())
324 .unwrap_or_else(|| id.provider.clone());
325 let existing_section = self.providers.config.section_by_id(&id).cloned();
326 let label_entry = existing_section.is_none() && id.label.as_deref() == Some("");
327 let provider_first_field =
328 crate::app::ProviderFormField::fields_for(id.provider.as_str())[0];
329 let first_field = if label_entry {
330 crate::app::ProviderFormField::Label
331 } else {
332 provider_first_field
333 };
334 log::debug!(
335 "[purple] open_provider_form provider={} label_entry={}",
336 id.provider,
337 label_entry
338 );
339
340 self.providers.form = if let Some(section) = existing_section {
341 let cursor_pos = match first_field {
342 crate::app::ProviderFormField::Url => section.url.chars().count(),
343 crate::app::ProviderFormField::Token => section.token.chars().count(),
344 _ => 0,
345 };
346 crate::app::ProviderFormFields {
347 label: String::new(),
348 label_entry: false,
349 url: section.url.clone(),
350 token: section.token.clone(),
351 profile: section.profile.clone(),
352 project: section.project.clone(),
353 compartment: section.compartment.clone(),
354 regions: section.regions.clone(),
355 alias_prefix: section.alias_prefix.clone(),
356 user: section.user.clone(),
357 identity_file: section.identity_file.clone(),
358 verify_tls: section.verify_tls,
359 auto_sync: section.auto_sync,
360 vault_role: section.vault_role.clone(),
361 vault_addr: section.vault_addr.clone(),
362 focused_field: first_field,
363 cursor_pos,
364 expanded: true,
365 }
366 } else {
367 let default_prefix = match id.label.as_deref() {
372 Some("") | None => short_label.clone(),
373 Some(l) => format!("{}-{}", short_label, l),
374 };
375 crate::app::ProviderFormFields {
376 label: String::new(),
377 label_entry,
378 url: String::new(),
379 token: String::new(),
380 profile: String::new(),
381 project: String::new(),
382 compartment: String::new(),
383 regions: String::new(),
384 alias_prefix: default_prefix,
385 user: "root".to_string(),
386 identity_file: String::new(),
387 verify_tls: true,
388 auto_sync: id
389 .kind()
390 .is_none_or(crate::providers::ProviderKind::default_auto_sync),
391 vault_role: String::new(),
392 vault_addr: String::new(),
393 focused_field: first_field,
394 cursor_pos: 0,
395 expanded: false,
396 }
397 };
398 self.set_screen(Screen::ProviderForm { id });
399 self.capture_provider_form_mtime();
400 self.capture_provider_form_baseline();
401 }
402
403 pub fn capture_tunnel_form_baseline(&mut self) {
405 self.tunnels.form_baseline = Some(TunnelFormBaseline {
406 tunnel_type: self.tunnels.form.tunnel_type,
407 bind_port: self.tunnels.form.bind_port.clone(),
408 remote_host: self.tunnels.form.remote_host.clone(),
409 remote_port: self.tunnels.form.remote_port.clone(),
410 bind_address: self.tunnels.form.bind_address.clone(),
411 });
412 }
413
414 pub fn tunnel_form_is_dirty(&self) -> bool {
416 match &self.tunnels.form_baseline {
417 Some(b) => {
418 self.tunnels.form.tunnel_type != b.tunnel_type
419 || self.tunnels.form.bind_port != b.bind_port
420 || self.tunnels.form.remote_host != b.remote_host
421 || self.tunnels.form.remote_port != b.remote_port
422 || self.tunnels.form.bind_address != b.bind_address
423 }
424 None => false,
425 }
426 }
427
428 pub fn capture_snippet_form_baseline(&mut self) {
430 self.snippets.form_baseline = Some(SnippetFormBaseline {
431 name: self.snippets.form.name.clone(),
432 command: self.snippets.form.command.clone(),
433 description: self.snippets.form.description.clone(),
434 });
435 }
436
437 pub fn snippet_form_is_dirty(&self) -> bool {
439 match &self.snippets.form_baseline {
440 Some(b) => {
441 self.snippets.form.name != b.name
442 || self.snippets.form.command != b.command
443 || self.snippets.form.description != b.description
444 }
445 None => false,
446 }
447 }
448
449 pub fn capture_provider_form_baseline(&mut self) {
451 self.providers.form_baseline = Some(ProviderFormBaseline {
452 url: self.providers.form.url.clone(),
453 token: self.providers.form.token.clone(),
454 profile: self.providers.form.profile.clone(),
455 project: self.providers.form.project.clone(),
456 compartment: self.providers.form.compartment.clone(),
457 regions: self.providers.form.regions.clone(),
458 alias_prefix: self.providers.form.alias_prefix.clone(),
459 user: self.providers.form.user.clone(),
460 identity_file: self.providers.form.identity_file.clone(),
461 verify_tls: self.providers.form.verify_tls,
462 auto_sync: self.providers.form.auto_sync,
463 vault_role: self.providers.form.vault_role.clone(),
464 vault_addr: self.providers.form.vault_addr.clone(),
465 });
466 }
467
468 pub fn provider_form_is_dirty(&self) -> bool {
470 match &self.providers.form_baseline {
471 Some(b) => {
472 self.providers.form.url != b.url
473 || self.providers.form.token != b.token
474 || self.providers.form.profile != b.profile
475 || self.providers.form.project != b.project
476 || self.providers.form.compartment != b.compartment
477 || self.providers.form.regions != b.regions
478 || self.providers.form.alias_prefix != b.alias_prefix
479 || self.providers.form.user != b.user
480 || self.providers.form.identity_file != b.identity_file
481 || self.providers.form.verify_tls != b.verify_tls
482 || self.providers.form.auto_sync != b.auto_sync
483 || self.providers.form.vault_role != b.vault_role
484 || self.providers.form.vault_addr != b.vault_addr
485 }
486 None => false,
487 }
488 }
489
490 pub fn config_changed_since_form_open(&self) -> bool {
492 match self.conflict.form_mtime {
493 Some(open_mtime) => {
494 if get_mtime(&self.reload.config_path) != Some(open_mtime) {
495 return true;
496 }
497 self.conflict
498 .form_include_mtimes
499 .iter()
500 .any(|(path, old_mtime)| get_mtime(path) != *old_mtime)
501 || self
502 .conflict
503 .form_include_dir_mtimes
504 .iter()
505 .any(|(path, old_mtime)| get_mtime(path) != *old_mtime)
506 }
507 None => false,
508 }
509 }
510
511 pub fn provider_config_changed_since_form_open(&self) -> bool {
513 let path = dirs::home_dir().map(|h| h.join(".purple/providers"));
514 let current_mtime = path.as_ref().and_then(|p| get_mtime(p));
515 self.conflict.provider_form_mtime != current_mtime
516 }
517}