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 pub default_hosts: Vec<String>,
47}
48
49#[derive(Clone)]
51pub struct ProviderFormBaseline {
52 pub url: String,
53 pub token: String,
54 pub profile: String,
55 pub project: String,
56 pub compartment: String,
57 pub regions: String,
58 pub filter: String,
59 pub alias_prefix: String,
60 pub user: String,
61 pub identity_file: String,
62 pub verify_tls: bool,
63 pub auto_sync: bool,
64 pub vault_role: String,
65 pub vault_addr: String,
66 pub ssm: crate::providers::aws_ssm::SsmMode,
67}
68
69impl App {
70 pub fn clear_form_mtime(&mut self) {
72 self.conflict.clear_form_mtimes();
73 }
74
75 pub fn capture_form_mtime(&mut self) {
77 self.conflict.form_mtime = get_mtime(&self.reload.config_path);
78 self.conflict.form_include_mtimes = snapshot_include_mtimes(&self.hosts_state.ssh_config);
79 self.conflict.form_include_dir_mtimes =
80 snapshot_include_dir_mtimes(&self.env, &self.hosts_state.ssh_config);
81 }
82
83 pub fn capture_provider_form_mtime(&mut self) {
85 let path = self
86 .env
87 .paths()
88 .map(crate::runtime::env::Paths::providers_config);
89 self.conflict.provider_form_mtime = path.as_ref().and_then(|p| get_mtime(p));
90 }
91
92 pub fn capture_form_baseline(&mut self) {
94 self.forms.host_baseline = Some(FormBaseline {
95 alias: self.forms.host.alias.clone(),
96 hostname: self.forms.host.hostname.clone(),
97 user: self.forms.host.user.clone(),
98 port: self.forms.host.port.clone(),
99 identity_file: self.forms.host.identity_file.clone(),
100 proxy_jump: self.forms.host.proxy_jump.clone(),
101 askpass: self.forms.host.askpass.clone(),
102 vault_ssh: self.forms.host.vault_ssh.clone(),
103 vault_addr: self.forms.host.vault_addr.clone(),
104 tags: self.forms.host.tags.clone(),
105 });
106 }
107
108 pub fn host_form_is_dirty(&self) -> bool {
110 self.forms.host_form_is_dirty()
111 }
112
113 pub fn close_host_form(&mut self) {
116 self.close_host_form_inner(None);
117 }
118
119 pub fn close_host_form_after_save(&mut self, target_alias: &str) {
122 self.close_host_form_inner(Some(target_alias));
123 }
124
125 fn close_host_form_inner(&mut self, select: Option<&str>) {
126 log::debug!("[purple] close_host_form select={:?}", select);
127 self.clear_form_mtime();
128 self.forms.host_baseline = None;
129 self.set_screen(Screen::HostList);
130 if let Some(alias) = select {
131 self.select_host_by_alias(alias);
132 }
133 self.flush_pending_vault_write();
134 }
135
136 pub fn close_provider_form(&mut self) {
139 log::debug!("[purple] close_provider_form");
140 self.clear_form_mtime();
141 self.providers.form_baseline = None;
142 self.set_screen(Screen::Providers);
143 self.flush_pending_vault_write();
144 }
145
146 pub fn close_tunnel_form(&mut self, return_to: Screen) {
150 log::debug!(
151 "[purple] close_tunnel_form return_to={:?}",
152 std::mem::discriminant(&return_to)
153 );
154 self.clear_form_mtime();
155 self.tunnels.form_baseline = None;
156 self.set_screen(return_to);
157 }
158
159 pub fn close_snippet_form(&mut self, target_aliases: Vec<String>) {
163 log::debug!(
164 "[purple] close_snippet_form aliases={}",
165 target_aliases.len()
166 );
167 self.snippets.form_baseline = None;
168 self.snippets.set_flow_targets(target_aliases);
169 self.snippets.set_form_editing(None);
170 self.set_screen(Screen::SnippetPicker);
171 }
172
173 pub fn open_host_add_form(&mut self) {
175 log::debug!("[purple] open_host_add_form");
176 self.forms.host = HostForm::new();
177 self.set_screen(Screen::AddHost);
178 self.capture_form_mtime();
179 self.capture_form_baseline();
180 }
181
182 pub fn open_host_pattern_add_form(&mut self) {
185 log::debug!("[purple] open_host_pattern_add_form");
186 self.forms.host = HostForm::new_pattern();
187 self.set_screen(Screen::AddHost);
188 self.capture_form_mtime();
189 self.capture_form_baseline();
190 }
191
192 pub fn open_host_edit_form(
197 &mut self,
198 host: crate::ssh_config::model::HostEntry,
199 stale_hint: Option<String>,
200 ) -> bool {
201 if let Some(ref source) = host.source_file {
202 self.notify_error(crate::messages::included_host_lives_in(
203 &host.alias,
204 &source.display(),
205 ));
206 return false;
207 }
208 let raw = match self.hosts_state.ssh_config.raw_host_entry(&host.alias) {
211 Some(entry) => entry,
212 None => {
213 self.notify_warning(crate::messages::HOST_NOT_FOUND_IN_CONFIG);
214 return false;
215 }
216 };
217 let inherited = self.hosts_state.ssh_config.inherited_hints(&host.alias);
218 log::debug!("[purple] open_host_edit_form alias={}", host.alias);
219 self.forms.host = HostForm::from_entry(&raw, inherited);
220 if let Some(hint) = stale_hint {
221 self.notify_warning(crate::messages::stale_host(&hint));
222 }
223 self.set_screen(Screen::EditHost { alias: host.alias });
224 self.capture_form_mtime();
225 self.capture_form_baseline();
226 true
227 }
228
229 pub fn open_host_pattern_edit_form(&mut self, pattern: &PatternEntry) {
231 log::debug!(
232 "[purple] open_host_pattern_edit_form pattern={}",
233 pattern.pattern
234 );
235 self.forms.host = HostForm::from_pattern_entry(pattern);
236 self.set_screen(Screen::EditHost {
237 alias: pattern.pattern.clone(),
238 });
239 self.capture_form_mtime();
240 self.capture_form_baseline();
241 }
242
243 pub fn open_tunnel_add_form(&mut self, alias: String) {
246 log::debug!("[purple] open_tunnel_add_form alias={}", alias);
247 self.tunnels.form = TunnelForm::new();
248 self.set_screen(Screen::TunnelForm {
249 alias,
250 editing: None,
251 });
252 self.capture_form_mtime();
253 self.capture_tunnel_form_baseline();
254 }
255
256 pub fn open_tunnel_edit_form(&mut self, alias: String, rule: &TunnelRule, editing: usize) {
259 log::debug!(
260 "[purple] open_tunnel_edit_form alias={} editing={}",
261 alias,
262 editing
263 );
264 self.tunnels.form = TunnelForm::from_rule(rule);
265 self.set_screen(Screen::TunnelForm {
266 alias,
267 editing: Some(editing),
268 });
269 self.capture_form_mtime();
270 self.capture_tunnel_form_baseline();
271 }
272
273 pub fn open_snippet_add_form(&mut self, target_aliases: Vec<String>) {
276 log::debug!(
277 "[purple] open_snippet_add_form aliases={}",
278 target_aliases.len()
279 );
280 self.snippets.form = SnippetForm::new();
281 self.snippets.set_flow_targets(target_aliases);
282 self.snippets.set_form_editing(None);
283 self.set_screen(Screen::SnippetForm);
284 self.capture_snippet_form_baseline();
285 }
286
287 pub fn open_snippet_edit_form(
290 &mut self,
291 snippet: &Snippet,
292 target_aliases: Vec<String>,
293 editing: usize,
294 ) {
295 log::debug!(
296 "[purple] open_snippet_edit_form name={} editing={}",
297 snippet.name,
298 editing
299 );
300 self.snippets.form = SnippetForm::from_snippet(snippet);
301 self.snippets.set_flow_targets(target_aliases);
302 self.snippets.set_form_editing(Some(editing));
303 self.set_screen(Screen::SnippetForm);
304 self.capture_snippet_form_baseline();
305 }
306
307 pub fn open_provider_form(&mut self, id: crate::providers::config::ProviderConfigId) {
311 let provider_impl = crate::providers::get_provider(id.provider.as_str());
312 let short_label = provider_impl
313 .as_ref()
314 .map(|p| p.short_label().to_string())
315 .unwrap_or_else(|| id.provider.clone());
316 let existing_section = self.providers.config.section_by_id(&id).cloned();
317 let label_entry = existing_section.is_none() && id.label.as_deref() == Some("");
318 let provider_first_field =
319 crate::app::ProviderFormField::fields_for(id.provider.as_str())[0];
320 let first_field = if label_entry {
321 crate::app::ProviderFormField::Label
322 } else {
323 provider_first_field
324 };
325 log::debug!(
326 "[purple] open_provider_form provider={} label_entry={}",
327 id.provider,
328 label_entry
329 );
330
331 self.providers.form = if let Some(section) = existing_section {
332 let cursor_pos = match first_field {
333 crate::app::ProviderFormField::Url => section.url.chars().count(),
334 crate::app::ProviderFormField::Token => section.token.chars().count(),
335 _ => 0,
336 };
337 crate::app::ProviderFormFields {
338 label: String::new(),
339 label_entry: false,
340 url: section.url.clone(),
341 token: section.token.clone(),
342 profile: section.profile.clone(),
343 project: section.project.clone(),
344 compartment: section.compartment.clone(),
345 regions: section.regions.clone(),
346 filter: section.filter.clone(),
347 alias_prefix: section.alias_prefix.clone(),
348 user: section.user.clone(),
349 identity_file: section.identity_file.clone(),
350 verify_tls: section.verify_tls,
351 auto_sync: section.auto_sync,
352 vault_role: section.vault_role.clone(),
353 ssm: section.ssm,
354 vault_addr: section.vault_addr.clone(),
355 focused_field: first_field,
356 cursor_pos,
357 expanded: true,
358 }
359 } else {
360 let default_prefix = match id.label.as_deref() {
365 Some("") | None => short_label.clone(),
366 Some(l) => format!("{}-{}", short_label, l),
367 };
368 crate::app::ProviderFormFields {
369 label: String::new(),
370 label_entry,
371 url: String::new(),
372 token: String::new(),
373 profile: String::new(),
374 project: String::new(),
375 compartment: String::new(),
376 regions: String::new(),
377 filter: String::new(),
378 alias_prefix: default_prefix,
379 user: "root".to_string(),
380 identity_file: String::new(),
381 verify_tls: true,
382 auto_sync: id
383 .kind()
384 .is_none_or(crate::providers::ProviderKind::default_auto_sync),
385 vault_role: String::new(),
386 ssm: crate::providers::aws_ssm::SsmMode::default(),
387 vault_addr: String::new(),
388 focused_field: first_field,
389 cursor_pos: 0,
390 expanded: false,
391 }
392 };
393 self.set_screen(Screen::ProviderForm { id });
394 self.capture_provider_form_mtime();
395 self.capture_provider_form_baseline();
396 }
397
398 pub fn capture_tunnel_form_baseline(&mut self) {
400 self.tunnels.form_baseline = Some(TunnelFormBaseline {
401 tunnel_type: self.tunnels.form.tunnel_type,
402 bind_port: self.tunnels.form.bind_port.clone(),
403 remote_host: self.tunnels.form.remote_host.clone(),
404 remote_port: self.tunnels.form.remote_port.clone(),
405 bind_address: self.tunnels.form.bind_address.clone(),
406 });
407 }
408
409 pub fn tunnel_form_is_dirty(&self) -> bool {
411 self.tunnels.form_is_dirty()
412 }
413
414 pub fn capture_snippet_form_baseline(&mut self) {
416 self.snippets.form_baseline = Some(SnippetFormBaseline {
417 name: self.snippets.form.name.clone(),
418 command: self.snippets.form.command.clone(),
419 description: self.snippets.form.description.clone(),
420 default_hosts: self.snippets.form.default_hosts.clone(),
421 });
422 }
423
424 pub fn snippet_form_is_dirty(&self) -> bool {
426 self.snippets.form_is_dirty()
427 }
428
429 pub fn capture_provider_form_baseline(&mut self) {
431 self.providers.form_baseline = Some(ProviderFormBaseline {
432 url: self.providers.form.url.clone(),
433 token: self.providers.form.token.clone(),
434 profile: self.providers.form.profile.clone(),
435 project: self.providers.form.project.clone(),
436 compartment: self.providers.form.compartment.clone(),
437 regions: self.providers.form.regions.clone(),
438 filter: self.providers.form.filter.clone(),
439 alias_prefix: self.providers.form.alias_prefix.clone(),
440 user: self.providers.form.user.clone(),
441 identity_file: self.providers.form.identity_file.clone(),
442 verify_tls: self.providers.form.verify_tls,
443 auto_sync: self.providers.form.auto_sync,
444 vault_role: self.providers.form.vault_role.clone(),
445 vault_addr: self.providers.form.vault_addr.clone(),
446 ssm: self.providers.form.ssm,
447 });
448 }
449
450 pub fn provider_form_is_dirty(&self) -> bool {
452 self.providers.form_is_dirty()
453 }
454
455 pub fn config_changed_since_form_open(&self) -> bool {
457 config_changed(&self.conflict, &self.reload.config_path)
458 }
459
460 pub fn provider_config_changed_since_form_open(&self) -> bool {
462 let path = self
463 .env
464 .paths()
465 .map(crate::runtime::env::Paths::providers_config);
466 let current_mtime = path.as_ref().and_then(|p| get_mtime(p));
467 self.conflict.provider_form_mtime != current_mtime
468 }
469}