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