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