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.env, &self.hosts_state.ssh_config);
78 }
79
80 pub fn capture_provider_form_mtime(&mut self) {
82 let path = self
83 .env
84 .paths()
85 .map(crate::runtime::env::Paths::providers_config);
86 self.conflict.provider_form_mtime = path.as_ref().and_then(|p| get_mtime(p));
87 }
88
89 pub fn capture_form_baseline(&mut self) {
91 self.forms.host_baseline = Some(FormBaseline {
92 alias: self.forms.host.alias.clone(),
93 hostname: self.forms.host.hostname.clone(),
94 user: self.forms.host.user.clone(),
95 port: self.forms.host.port.clone(),
96 identity_file: self.forms.host.identity_file.clone(),
97 proxy_jump: self.forms.host.proxy_jump.clone(),
98 askpass: self.forms.host.askpass.clone(),
99 vault_ssh: self.forms.host.vault_ssh.clone(),
100 vault_addr: self.forms.host.vault_addr.clone(),
101 tags: self.forms.host.tags.clone(),
102 });
103 }
104
105 pub fn host_form_is_dirty(&self) -> bool {
107 self.forms.host_form_is_dirty()
108 }
109
110 pub fn close_host_form(&mut self) {
113 self.close_host_form_inner(None);
114 }
115
116 pub fn close_host_form_after_save(&mut self, target_alias: &str) {
119 self.close_host_form_inner(Some(target_alias));
120 }
121
122 fn close_host_form_inner(&mut self, select: Option<&str>) {
123 log::debug!("[purple] close_host_form select={:?}", select);
124 self.clear_form_mtime();
125 self.forms.host_baseline = None;
126 self.set_screen(Screen::HostList);
127 if let Some(alias) = select {
128 self.select_host_by_alias(alias);
129 }
130 self.flush_pending_vault_write();
131 }
132
133 pub fn close_provider_form(&mut self) {
136 log::debug!("[purple] close_provider_form");
137 self.clear_form_mtime();
138 self.providers.form_baseline = None;
139 self.set_screen(Screen::Providers);
140 self.flush_pending_vault_write();
141 }
142
143 pub fn close_tunnel_form(&mut self, return_to: Screen) {
147 log::debug!(
148 "[purple] close_tunnel_form return_to={:?}",
149 std::mem::discriminant(&return_to)
150 );
151 self.clear_form_mtime();
152 self.tunnels.form_baseline = None;
153 self.set_screen(return_to);
154 }
155
156 pub fn close_snippet_form(&mut self, target_aliases: Vec<String>) {
160 log::debug!(
161 "[purple] close_snippet_form aliases={}",
162 target_aliases.len()
163 );
164 self.snippets.form_baseline = None;
165 self.snippets.set_flow_targets(target_aliases);
166 self.snippets.set_form_editing(None);
167 self.set_screen(Screen::SnippetPicker);
168 }
169
170 pub fn open_host_add_form(&mut self) {
172 log::debug!("[purple] open_host_add_form");
173 self.forms.host = HostForm::new();
174 self.set_screen(Screen::AddHost);
175 self.capture_form_mtime();
176 self.capture_form_baseline();
177 }
178
179 pub fn open_host_pattern_add_form(&mut self) {
182 log::debug!("[purple] open_host_pattern_add_form");
183 self.forms.host = HostForm::new_pattern();
184 self.set_screen(Screen::AddHost);
185 self.capture_form_mtime();
186 self.capture_form_baseline();
187 }
188
189 pub fn open_host_edit_form(
194 &mut self,
195 host: crate::ssh_config::model::HostEntry,
196 stale_hint: Option<String>,
197 ) -> bool {
198 if let Some(ref source) = host.source_file {
199 self.notify_error(crate::messages::included_host_lives_in(
200 &host.alias,
201 &source.display(),
202 ));
203 return false;
204 }
205 let raw = match self.hosts_state.ssh_config.raw_host_entry(&host.alias) {
208 Some(entry) => entry,
209 None => {
210 self.notify_warning(crate::messages::HOST_NOT_FOUND_IN_CONFIG);
211 return false;
212 }
213 };
214 let inherited = self.hosts_state.ssh_config.inherited_hints(&host.alias);
215 log::debug!("[purple] open_host_edit_form alias={}", host.alias);
216 self.forms.host = HostForm::from_entry(&raw, inherited);
217 if let Some(hint) = stale_hint {
218 self.notify_warning(crate::messages::stale_host(&hint));
219 }
220 self.set_screen(Screen::EditHost { alias: host.alias });
221 self.capture_form_mtime();
222 self.capture_form_baseline();
223 true
224 }
225
226 pub fn open_host_pattern_edit_form(&mut self, pattern: &PatternEntry) {
228 log::debug!(
229 "[purple] open_host_pattern_edit_form pattern={}",
230 pattern.pattern
231 );
232 self.forms.host = HostForm::from_pattern_entry(pattern);
233 self.set_screen(Screen::EditHost {
234 alias: pattern.pattern.clone(),
235 });
236 self.capture_form_mtime();
237 self.capture_form_baseline();
238 }
239
240 pub fn open_tunnel_add_form(&mut self, alias: String) {
243 log::debug!("[purple] open_tunnel_add_form alias={}", alias);
244 self.tunnels.form = TunnelForm::new();
245 self.set_screen(Screen::TunnelForm {
246 alias,
247 editing: None,
248 });
249 self.capture_form_mtime();
250 self.capture_tunnel_form_baseline();
251 }
252
253 pub fn open_tunnel_edit_form(&mut self, alias: String, rule: &TunnelRule, editing: usize) {
256 log::debug!(
257 "[purple] open_tunnel_edit_form alias={} editing={}",
258 alias,
259 editing
260 );
261 self.tunnels.form = TunnelForm::from_rule(rule);
262 self.set_screen(Screen::TunnelForm {
263 alias,
264 editing: Some(editing),
265 });
266 self.capture_form_mtime();
267 self.capture_tunnel_form_baseline();
268 }
269
270 pub fn open_snippet_add_form(&mut self, target_aliases: Vec<String>) {
273 log::debug!(
274 "[purple] open_snippet_add_form aliases={}",
275 target_aliases.len()
276 );
277 self.snippets.form = SnippetForm::new();
278 self.snippets.set_flow_targets(target_aliases);
279 self.snippets.set_form_editing(None);
280 self.set_screen(Screen::SnippetForm);
281 self.capture_snippet_form_baseline();
282 }
283
284 pub fn open_snippet_edit_form(
287 &mut self,
288 snippet: &Snippet,
289 target_aliases: Vec<String>,
290 editing: usize,
291 ) {
292 log::debug!(
293 "[purple] open_snippet_edit_form name={} editing={}",
294 snippet.name,
295 editing
296 );
297 self.snippets.form = SnippetForm::from_snippet(snippet);
298 self.snippets.set_flow_targets(target_aliases);
299 self.snippets.set_form_editing(Some(editing));
300 self.set_screen(Screen::SnippetForm);
301 self.capture_snippet_form_baseline();
302 }
303
304 pub fn open_provider_form(&mut self, id: crate::providers::config::ProviderConfigId) {
308 let provider_impl = crate::providers::get_provider(id.provider.as_str());
309 let short_label = provider_impl
310 .as_ref()
311 .map(|p| p.short_label().to_string())
312 .unwrap_or_else(|| id.provider.clone());
313 let existing_section = self.providers.config.section_by_id(&id).cloned();
314 let label_entry = existing_section.is_none() && id.label.as_deref() == Some("");
315 let provider_first_field =
316 crate::app::ProviderFormField::fields_for(id.provider.as_str())[0];
317 let first_field = if label_entry {
318 crate::app::ProviderFormField::Label
319 } else {
320 provider_first_field
321 };
322 log::debug!(
323 "[purple] open_provider_form provider={} label_entry={}",
324 id.provider,
325 label_entry
326 );
327
328 self.providers.form = if let Some(section) = existing_section {
329 let cursor_pos = match first_field {
330 crate::app::ProviderFormField::Url => section.url.chars().count(),
331 crate::app::ProviderFormField::Token => section.token.chars().count(),
332 _ => 0,
333 };
334 crate::app::ProviderFormFields {
335 label: String::new(),
336 label_entry: false,
337 url: section.url.clone(),
338 token: section.token.clone(),
339 profile: section.profile.clone(),
340 project: section.project.clone(),
341 compartment: section.compartment.clone(),
342 regions: section.regions.clone(),
343 alias_prefix: section.alias_prefix.clone(),
344 user: section.user.clone(),
345 identity_file: section.identity_file.clone(),
346 verify_tls: section.verify_tls,
347 auto_sync: section.auto_sync,
348 vault_role: section.vault_role.clone(),
349 vault_addr: section.vault_addr.clone(),
350 focused_field: first_field,
351 cursor_pos,
352 expanded: true,
353 }
354 } else {
355 let default_prefix = match id.label.as_deref() {
360 Some("") | None => short_label.clone(),
361 Some(l) => format!("{}-{}", short_label, l),
362 };
363 crate::app::ProviderFormFields {
364 label: String::new(),
365 label_entry,
366 url: String::new(),
367 token: String::new(),
368 profile: String::new(),
369 project: String::new(),
370 compartment: String::new(),
371 regions: String::new(),
372 alias_prefix: default_prefix,
373 user: "root".to_string(),
374 identity_file: String::new(),
375 verify_tls: true,
376 auto_sync: id
377 .kind()
378 .is_none_or(crate::providers::ProviderKind::default_auto_sync),
379 vault_role: String::new(),
380 vault_addr: String::new(),
381 focused_field: first_field,
382 cursor_pos: 0,
383 expanded: false,
384 }
385 };
386 self.set_screen(Screen::ProviderForm { id });
387 self.capture_provider_form_mtime();
388 self.capture_provider_form_baseline();
389 }
390
391 pub fn capture_tunnel_form_baseline(&mut self) {
393 self.tunnels.form_baseline = Some(TunnelFormBaseline {
394 tunnel_type: self.tunnels.form.tunnel_type,
395 bind_port: self.tunnels.form.bind_port.clone(),
396 remote_host: self.tunnels.form.remote_host.clone(),
397 remote_port: self.tunnels.form.remote_port.clone(),
398 bind_address: self.tunnels.form.bind_address.clone(),
399 });
400 }
401
402 pub fn tunnel_form_is_dirty(&self) -> bool {
404 self.tunnels.form_is_dirty()
405 }
406
407 pub fn capture_snippet_form_baseline(&mut self) {
409 self.snippets.form_baseline = Some(SnippetFormBaseline {
410 name: self.snippets.form.name.clone(),
411 command: self.snippets.form.command.clone(),
412 description: self.snippets.form.description.clone(),
413 });
414 }
415
416 pub fn snippet_form_is_dirty(&self) -> bool {
418 self.snippets.form_is_dirty()
419 }
420
421 pub fn capture_provider_form_baseline(&mut self) {
423 self.providers.form_baseline = Some(ProviderFormBaseline {
424 url: self.providers.form.url.clone(),
425 token: self.providers.form.token.clone(),
426 profile: self.providers.form.profile.clone(),
427 project: self.providers.form.project.clone(),
428 compartment: self.providers.form.compartment.clone(),
429 regions: self.providers.form.regions.clone(),
430 alias_prefix: self.providers.form.alias_prefix.clone(),
431 user: self.providers.form.user.clone(),
432 identity_file: self.providers.form.identity_file.clone(),
433 verify_tls: self.providers.form.verify_tls,
434 auto_sync: self.providers.form.auto_sync,
435 vault_role: self.providers.form.vault_role.clone(),
436 vault_addr: self.providers.form.vault_addr.clone(),
437 });
438 }
439
440 pub fn provider_form_is_dirty(&self) -> bool {
442 self.providers.form_is_dirty()
443 }
444
445 pub fn config_changed_since_form_open(&self) -> bool {
447 config_changed(&self.conflict, &self.reload.config_path)
448 }
449
450 pub fn provider_config_changed_since_form_open(&self) -> bool {
452 let path = self
453 .env
454 .paths()
455 .map(crate::runtime::env::Paths::providers_config);
456 let current_mtime = path.as_ref().and_then(|p| get_mtime(p));
457 self.conflict.provider_form_mtime != current_mtime
458 }
459}