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