1pub const FAILED_TO_SAVE: &str = "Failed to save";
11pub fn failed_to_save(e: &impl std::fmt::Display) -> String {
12 format!("{}: {}", FAILED_TO_SAVE, e)
13}
14
15pub const CONFIG_CHANGED_EXTERNALLY: &str =
16 "Config changed externally. Press Esc and re-open to pick up changes.";
17
18pub const DEMO_CONNECTION_DISABLED: &str = "Demo mode. Connection disabled.";
21pub const DEMO_SYNC_DISABLED: &str = "Demo mode. Sync disabled.";
22pub const DEMO_TUNNELS_DISABLED: &str = "Demo mode. Tunnels disabled.";
23pub const DEMO_VAULT_SIGNING_DISABLED: &str = "Demo mode. Vault SSH signing disabled.";
24pub const DEMO_FILE_BROWSER_DISABLED: &str = "Demo mode. File browser disabled.";
25pub const DEMO_CONTAINER_REFRESH_DISABLED: &str = "Demo mode. Container refresh disabled.";
26pub const DEMO_CONTAINER_ACTIONS_DISABLED: &str = "Demo mode. Container actions disabled.";
27pub const DEMO_EXECUTION_DISABLED: &str = "Demo mode. Execution disabled.";
28pub const DEMO_PROVIDER_CHANGES_DISABLED: &str = "Demo mode. Provider config changes disabled.";
29
30pub fn stale_host(hint: &str) -> String {
37 let trimmed = hint.trim().trim_end_matches('.');
38 if trimmed.is_empty() {
39 "Stale host.".to_string()
40 } else {
41 format!("Stale host. {}.", trimmed)
42 }
43}
44
45pub fn copied_ssh_command(alias: &str) -> String {
48 format!("Copied SSH command for {}.", alias)
49}
50
51pub fn copied_config_block(alias: &str) -> String {
52 format!("Copied config block for {}.", alias)
53}
54
55pub fn showing_unreachable(count: usize) -> String {
56 format!(
57 "Showing {} unreachable host{}.",
58 count,
59 if count == 1 { "" } else { "s" }
60 )
61}
62
63pub fn sorted_by(label: &str) -> String {
64 format!("Sorted by {}.", label)
65}
66
67pub fn sorted_by_save_failed(label: &str, e: &impl std::fmt::Display) -> String {
68 format!("Sorted by {}. (save failed: {})", label, e)
69}
70
71pub fn grouped_by(label: &str) -> String {
72 format!("Grouped by {}.", label)
73}
74
75pub fn grouped_by_save_failed(label: &str, e: &impl std::fmt::Display) -> String {
76 format!("Grouped by {}. (save failed: {})", label, e)
77}
78
79pub const UNGROUPED: &str = "Ungrouped.";
80
81pub fn ungrouped_save_failed(e: &impl std::fmt::Display) -> String {
82 format!("Ungrouped. (save failed: {})", e)
83}
84
85pub const GROUPED_BY_TAG: &str = "Grouped by tag.";
86
87pub fn grouped_by_tag_save_failed(e: &impl std::fmt::Display) -> String {
88 format!("Grouped by tag. (save failed: {})", e)
89}
90
91pub fn host_restored(alias: &str) -> String {
92 format!("{} is back from the dead.", alias)
93}
94
95pub fn restored_tags(count: usize) -> String {
96 format!(
97 "Restored tags on {} host{}.",
98 count,
99 if count == 1 { "" } else { "s" }
100 )
101}
102
103pub const NOTHING_TO_UNDO: &str = "Nothing to undo.";
104pub const NO_IMPORTABLE_HOSTS: &str = "No importable hosts in known_hosts.";
105pub const NO_STALE_HOSTS: &str = "No stale hosts.";
106pub const NO_HOST_SELECTED: &str = "No host selected.";
107pub const NO_HOSTS_TO_RUN: &str = "No hosts to run on.";
108pub const NO_HOSTS_TO_TAG: &str = "No hosts to tag.";
109pub const PING_FIRST: &str = "Ping first (p/P), then filter with !.";
110pub const PINGING_ALL: &str = "Pinging all the things...";
111pub const ESC_QUIT_HINT: &str = "Nothing to cancel. Press q to quit.";
112
113pub fn included_file_edit(name: &str) -> String {
114 format!("{} is in an included file. Edit it there.", name)
115}
116
117pub fn included_file_delete(name: &str) -> String {
118 format!("{} is in an included file. Delete it there.", name)
119}
120
121pub fn included_file_clone(name: &str) -> String {
122 format!("{} is in an included file. Clone it there.", name)
123}
124
125pub fn included_host_lives_in(alias: &str, path: &impl std::fmt::Display) -> String {
126 format!("{} lives in {}. Edit it there.", alias, path)
127}
128
129pub fn included_host_clone_there(alias: &str, path: &impl std::fmt::Display) -> String {
130 format!("{} lives in {}. Clone it there.", alias, path)
131}
132
133pub fn included_host_tag_there(alias: &str, path: &impl std::fmt::Display) -> String {
134 format!("{} is included from {}. Tag it there.", alias, path)
135}
136
137pub const HOST_NOT_FOUND_IN_CONFIG: &str = "Host not found in config.";
138
139pub const SMART_PARSED: &str = "Smart-parsed that for you. Check the fields.";
142pub const LOOKS_LIKE_ADDRESS: &str = "Looks like an address. Suggested as Host.";
143
144pub const HOST_ALIAS_EMPTY: &str = "Alias can't be empty. Every host needs a name!";
152pub const HOST_PATTERN_EMPTY: &str = "Pattern can't be empty.";
153pub const HOST_PATTERN_NEEDS_WILDCARD: &str =
154 "Pattern needs a wildcard (*, ?, [) or multiple hosts.";
155pub const HOST_ALIAS_WHITESPACE: &str = "Alias can't contain whitespace. Keep it simple.";
156pub const HOST_ALIAS_HASH: &str =
157 "Alias can't contain '#'. That's a comment character in SSH config.";
158pub const HOST_ALIAS_PATTERN_CHARS: &str =
159 "Alias can't contain pattern characters. That creates a match pattern, not a host.";
160pub const HOST_HOSTNAME_EMPTY: &str = "Hostname can't be empty. Where should we connect to?";
161pub const HOST_HOSTNAME_WHITESPACE: &str = "Hostname can't contain whitespace.";
162pub const HOST_PORT_INVALID: &str = "That's not a port number. Ports are 1-65535, not poetry.";
163pub const HOST_PORT_ZERO: &str = "Port 0? Bold choice, but no. Try 1-65535.";
164pub const HOST_VAULT_ROLE_INVALID: &str = "Vault SSH role: only letters, digits, /, _ and - \
165 are allowed (e.g. ssh-client-signer/sign/my-role).";
166pub const HOST_VAULT_ADDR_INVALID: &str = "Vault SSH address: must be a non-empty URL \
167 without spaces or control characters (e.g. http://127.0.0.1:8200).";
168
169pub fn field_control_chars(name: &str) -> String {
172 format!(
173 "{} contains control characters. That's not going to work.",
174 name
175 )
176}
177
178pub const TUNNEL_BIND_PORT_INVALID: &str = "Bind port must be 1-65535.";
181pub const TUNNEL_BIND_PORT_ZERO: &str = "Bind port can't be 0.";
182pub const TUNNEL_REMOTE_HOST_EMPTY: &str = "Remote host can't be empty.";
183pub const TUNNEL_REMOTE_HOST_SPACES: &str = "Remote host can't contain spaces.";
184pub const TUNNEL_REMOTE_PORT_INVALID: &str = "Remote port must be 1-65535.";
185pub const TUNNEL_REMOTE_PORT_ZERO: &str = "Remote port can't be 0.";
186
187pub fn field_control_chars_short(name: &str) -> String {
190 format!("{} contains control characters.", name)
191}
192
193pub const SNIPPET_NAME_EMPTY: &str = "Snippet name cannot be empty.";
196pub const SNIPPET_NAME_WHITESPACE: &str =
197 "Snippet name cannot have leading or trailing whitespace.";
198pub const SNIPPET_NAME_INVALID_CHARS: &str = "Snippet name cannot contain #, [ or ].";
199pub const SNIPPET_NAME_CONTROL_CHARS: &str = "Snippet name cannot contain control characters.";
200pub const SNIPPET_COMMAND_EMPTY: &str = "Command cannot be empty.";
201pub const SNIPPET_COMMAND_CONTROL_CHARS: &str = "Command cannot contain control characters.";
202pub const SNIPPET_DESCRIPTION_CONTROL_CHARS: &str = "Description contains control characters.";
203
204pub fn pattern_already_exists(alias: &str) -> String {
207 format!("Pattern '{}' already exists.", alias)
208}
209
210pub fn host_alias_already_exists(alias: &str) -> String {
211 format!("'{}' already exists. Aliases must be unique.", alias)
212}
213
214pub const PATTERN_NO_LONGER_EXISTS: &str = "Pattern no longer exists.";
215pub const HOST_NO_LONGER_EXISTS: &str = "Host no longer exists.";
216
217pub fn cert_path_resolve_failed(e: &impl std::fmt::Display) -> String {
218 format!("Failed to resolve cert path: {}", e)
219}
220
221pub fn welcome_aboard(alias: &str) -> String {
224 format!("Welcome aboard, {}!", alias)
225}
226
227pub const BULK_TAG_NO_HOSTS_SELECTED: &str = "No hosts selected.";
230
231pub fn goodbye_host(alias: &str) -> String {
234 format!("Goodbye, {}. We barely knew ye. (u to undo)", alias)
235}
236
237pub fn host_not_found(alias: &str) -> String {
238 format!("Host '{}' not found.", alias)
239}
240
241pub fn siblings_stripped(alias: &str, sibling_count: usize) -> String {
245 if sibling_count == 1 {
246 format!(
247 "Stripped {}. 1 sibling alias kept its shared config.",
248 alias
249 )
250 } else {
251 format!(
252 "Stripped {}. {} sibling aliases kept their shared config.",
253 alias, sibling_count
254 )
255 }
256}
257
258pub fn confirm_delete_siblings_note(siblings: &[String]) -> String {
262 let shown: Vec<&str> = siblings.iter().take(3).map(String::as_str).collect();
263 let tail = if siblings.len() > shown.len() {
264 format!(" +{} more", siblings.len() - shown.len())
265 } else {
266 String::new()
267 };
268 format!("Siblings kept: {}{}", shown.join(", "), tail)
269}
270
271pub fn cert_cleanup_warning(path: &impl std::fmt::Display, e: &impl std::fmt::Display) -> String {
272 format!("Warning: failed to clean up Vault SSH cert {}: {}", path, e)
273}
274
275pub const CLONED_VAULT_CLEARED: &str = "Cloned. Vault SSH role cleared on copy.";
278
279pub const TUNNEL_REMOVED: &str = "Tunnel removed.";
282pub const TUNNEL_SAVED: &str = "Tunnel saved.";
283pub const TUNNEL_NOT_FOUND: &str = "Tunnel not found in config.";
284pub const TUNNEL_INCLUDED_READ_ONLY: &str = "Included host. Tunnels are read-only.";
285pub const TUNNEL_ORIGINAL_NOT_FOUND: &str = "Original tunnel not found in config.";
286pub const TUNNEL_LIST_CHANGED: &str = "Tunnel list changed externally. Press Esc and re-open.";
287pub const TUNNEL_DUPLICATE: &str = "Duplicate tunnel already configured.";
288pub const TUNNEL_NO_EDITABLE_HOSTS: &str = "No editable hosts. Add a host first.";
289pub const TUNNEL_HOST_PICKER_NO_MATCH: &str = "No matches.";
290
291pub fn tunnel_stopped(alias: &str) -> String {
292 format!("Tunnel for {} stopped.", alias)
293}
294
295pub fn tunnel_started(alias: &str) -> String {
296 format!("Tunnel for {} started.", alias)
297}
298
299pub fn tunnel_start_failed(e: &impl std::fmt::Display) -> String {
300 format!("Failed to start tunnel: {}", e)
301}
302
303pub fn pinging_host(alias: &str, show_hint: bool) -> String {
306 if show_hint {
307 format!("Pinging {}... (Shift+P pings all)", alias)
308 } else {
309 format!("Pinging {}...", alias)
310 }
311}
312
313pub fn bastion_not_found(alias: &str) -> String {
314 format!("Bastion {} not found in config.", alias)
315}
316
317pub fn provider_removed(display_name: &str) -> String {
320 format!(
321 "Removed {} configuration. Synced hosts remain in your SSH config.",
322 display_name
323 )
324}
325
326pub fn label_invalid(reason: &str) -> String {
327 format!("Invalid name: {}", reason)
328}
329
330pub const LABEL_MUST_DIFFER: &str = "The two names must be different.";
331
332pub const LABEL_MIGRATION_FIELD_CURRENT: &str = " Name for your current config ";
333pub const LABEL_MIGRATION_FIELD_NEW: &str = " Name for the new config ";
334
335pub fn confirm_remove_provider(display: &str) -> String {
336 format!(" Remove {}? ", display)
337}
338
339pub fn confirm_remove_labeled_config(display: &str, label: &str) -> String {
340 format!(" Remove {} ({})? ", display, label)
341}
342
343pub const EXPAND_TO_REMOVE_CONFIG: &str =
344 "Expand the provider and pick a specific config to remove.";
345
346pub fn provider_not_configured(display_name: &str) -> String {
347 format!("{} is not configured. Nothing to remove.", display_name)
348}
349
350pub fn provider_configure_first(display_name: &str) -> String {
351 format!("Configure {} first. Press Enter to set up.", display_name)
352}
353
354pub fn provider_saved_syncing(display_name: &str) -> String {
355 format!("Saved {} configuration. Syncing...", display_name)
356}
357
358pub fn provider_saved(display_name: &str) -> String {
359 format!("Saved {} configuration.", display_name)
360}
361
362pub fn no_stale_hosts_for(display_name: &str) -> String {
363 format!("No stale hosts for {}.", display_name)
364}
365
366pub fn contains_control_chars(name: &str) -> String {
367 format!("{} contains control characters.", name)
368}
369
370pub const TOKEN_FORMAT_AWS: &str = "Token format: AccessKeyId:SecretAccessKey";
371pub const URL_REQUIRED_PROXMOX: &str = "URL is required for Proxmox VE.";
372pub const PROJECT_REQUIRED_GCP: &str = "Project ID can't be empty. Set your GCP project ID.";
373pub const COMPARTMENT_REQUIRED_OCI: &str =
374 "Compartment can't be empty. Set your OCI compartment OCID.";
375pub const REGIONS_REQUIRED_AWS: &str = "Select at least one AWS region.";
376pub const ZONES_REQUIRED_SCALEWAY: &str = "Select at least one Scaleway zone.";
377pub const SUBSCRIPTIONS_REQUIRED_AZURE: &str = "Enter at least one Azure subscription ID.";
378pub const ALIAS_PREFIX_INVALID: &str =
379 "Alias prefix can't contain spaces or pattern characters (*, ?, [, !).";
380pub const USER_NO_WHITESPACE: &str = "User can't contain whitespace.";
381pub const VAULT_ROLE_FORMAT: &str = "Vault SSH role must be in the form <mount>/sign/<role>.";
382
383pub const PROVIDER_CONFIG_CHANGED_EXTERNALLY: &str =
384 "Provider config changed externally. Press Esc and re-open to pick up changes.";
385pub const PROVIDER_URL_REQUIRES_HTTPS: &str =
386 "URL must start with https://. Toggle Verify TLS off for self-signed certificates.";
387pub const PROVIDER_TOKEN_REQUIRED_GCP: &str =
388 "Token can't be empty. Provide a service account JSON key file path or access token.";
389pub const PROVIDER_TOKEN_REQUIRED_ORACLE: &str =
390 "Token can't be empty. Provide the path to your OCI config file (e.g. ~/.oci/config).";
391
392pub fn provider_token_required(display_name: &str) -> String {
393 format!(
394 "Token can't be empty. Grab one from your {} dashboard.",
395 display_name
396 )
397}
398
399pub fn azure_subscription_id_invalid(sub: &str) -> String {
400 format!(
401 "Invalid subscription ID '{}'. Expected UUID format \
402 (e.g. 12345678-1234-1234-1234-123456789012).",
403 sub
404 )
405}
406
407pub const VAULT_SIGNING_CANCELLED: &str = "Vault SSH signing cancelled.";
410
411pub fn vault_signing_aborted(failed: u32, last_error: Option<&str>) -> String {
416 format!(
417 "Vault SSH signing aborted after {} consecutive failures. Press V to retry. Last error: {}",
418 failed,
419 last_error.unwrap_or("unknown")
420 )
421}
422
423pub fn bulk_tag_apply_status(
431 changed_hosts: usize,
432 added: usize,
433 removed: usize,
434 skipped_included: usize,
435) -> String {
436 let mut parts: Vec<String> = Vec::new();
437 if changed_hosts > 0 {
438 let host_word = if changed_hosts == 1 { "" } else { "s" };
439 let mut head = format!("Updated {} host{}", changed_hosts, host_word);
440 let mut delta = Vec::new();
441 if added > 0 {
442 delta.push(format!("+{}", added));
443 }
444 if removed > 0 {
445 delta.push(format!("-{}", removed));
446 }
447 if !delta.is_empty() {
448 head = format!("{} ({})", head, delta.join(" "));
449 }
450 parts.push(head);
451 }
452 if skipped_included > 0 {
453 let file_word = if skipped_included == 1 { "" } else { "s" };
454 parts.push(format!(
455 "skipped {} in include file{}",
456 skipped_included, file_word
457 ));
458 }
459 parts.join(". ")
460}
461
462pub fn vault_sign_summary(
463 signed: u32,
464 failed: u32,
465 skipped: u32,
466 first_error: Option<&str>,
467) -> String {
468 let total = signed + failed + skipped;
469 let cert_word = if total == 1 {
470 "certificate"
471 } else {
472 "certificates"
473 };
474 if failed > 0 {
475 if let Some(err) = first_error {
476 if total == 1 {
477 return err.to_string();
478 }
479 format!(
480 "Signed {} of {} {}. {} failed: {}",
481 signed, total, cert_word, failed, err
482 )
483 } else {
484 format!(
485 "Signed {} of {} {}. {} failed",
486 signed, total, cert_word, failed
487 )
488 }
489 } else if skipped > 0 && signed == 0 {
490 format!(
491 "All {} {} already valid. Nothing to sign.",
492 total, cert_word
493 )
494 } else if skipped > 0 {
495 format!(
496 "Signed {} of {} {}. {} already valid.",
497 signed, total, cert_word, skipped
498 )
499 } else {
500 format!("Signed {} of {} {}.", signed, total, cert_word)
501 }
502}
503pub const VAULT_NO_ROLE_CONFIGURED: &str = "No Vault SSH role configured. Set one in the host form \
504 (Vault SSH role field) or on a provider for shared defaults.";
505pub const VAULT_NO_HOSTS_WITH_ROLE: &str = "No hosts with a Vault SSH role configured.";
506pub const VAULT_ALL_CERTS_VALID: &str = "All Vault SSH certificates are still valid.";
507pub const VAULT_NO_ADDRESS: &str = "No Vault address set. Edit the host (e) or provider \
508 and fill in the Vault SSH Address field.";
509
510pub fn vault_error(msg: &str) -> String {
511 format!("Vault SSH: {}", msg)
512}
513
514pub fn vault_signed(alias: &str) -> String {
515 format!("Signed Vault SSH cert for {}", alias)
516}
517
518pub fn vault_sign_failed(alias: &str, message: &str) -> String {
519 format!("Vault SSH: failed to sign {}: {}", alias, message)
520}
521
522pub fn vault_signing_progress(spinner: &str, done: usize, total: usize, alias: &str) -> String {
523 format!(
524 "{} Signing {}/{}: {} (V to cancel)",
525 spinner, done, total, alias
526 )
527}
528
529pub fn vault_cert_saved_host_gone(alias: &str) -> String {
530 format!(
531 "Vault SSH cert saved for {} but host no longer in config \
532 (renamed or deleted). CertificateFile NOT written.",
533 alias
534 )
535}
536
537pub fn vault_spawn_failed(e: &impl std::fmt::Display) -> String {
538 format!("Vault SSH: failed to spawn signing thread: {}", e)
539}
540
541pub fn vault_cert_check_failed(alias: &str, message: &str) -> String {
542 format!("Cert check failed for {}: {}", alias, message)
543}
544
545pub fn vault_role_set(role: &str) -> String {
546 format!("Vault SSH role set to {}.", role)
547}
548
549pub fn vault_signed_pre_connect(alias: &str) -> String {
553 format!("Signed Vault SSH cert for {}.", alias)
554}
555
556pub fn vault_signed_pre_connect_chain(target: &str, count: usize) -> String {
561 if count <= 1 {
562 format!("Signed Vault SSH cert for {}.", target)
563 } else {
564 format!("Signed Vault SSH certs for {} ({} hosts).", target, count)
565 }
566}
567
568pub fn vault_sign_failed_pre_connect(alias: &str, message: &str) -> String {
573 format!("Vault SSH signing failed for {}: {}", alias, message)
574}
575
576pub fn vault_cert_pubkey_resolve_failed(e: &impl std::fmt::Display) -> String {
580 format!("Vault SSH cert failed: {}", e)
581}
582
583pub fn vault_cert_host_block_missing(alias: &str, cert_path: &std::path::Path) -> String {
588 format!(
589 "Warning: signed cert for {} but host block is no longer in ssh config; \
590 CertificateFile not written (cert saved to {})",
591 alias,
592 cert_path.display()
593 )
594}
595
596pub fn vault_cert_config_write_failed(alias: &str, e: &impl std::fmt::Display) -> String {
599 format!(
600 "Warning: signed cert for {} but failed to update SSH config CertificateFile: {}",
601 alias, e
602 )
603}
604
605pub fn snippet_removed(name: &str) -> String {
608 format!("Removed snippet '{}'.", name)
609}
610
611pub fn snippet_added(name: &str) -> String {
612 format!("Added snippet '{}'.", name)
613}
614
615pub fn snippet_updated(name: &str) -> String {
616 format!("Updated snippet '{}'.", name)
617}
618
619pub fn snippet_exists(name: &str) -> String {
620 format!("'{}' already exists.", name)
621}
622
623pub const OUTPUT_COPIED: &str = "Output copied.";
624
625pub fn copy_failed(e: &impl std::fmt::Display) -> String {
626 format!("Copy failed: {}", e)
627}
628
629pub fn clipboard_run_failed(cmd: &str) -> String {
635 format!("Failed to run {}.", cmd)
636}
637
638pub fn clipboard_write_failed(cmd: &str) -> String {
639 format!("Failed to write to {}.", cmd)
640}
641
642pub fn clipboard_wait_failed(cmd: &str) -> String {
643 format!("Failed to wait for {}.", cmd)
644}
645
646pub fn clipboard_exited_error(cmd: &str) -> String {
647 format!("{} exited with error.", cmd)
648}
649
650pub fn import_open_failed(path: &impl std::fmt::Display, e: &impl std::fmt::Display) -> String {
656 format!("Can't open {}: {}", path, e)
657}
658
659pub fn import_known_hosts_open_failed(e: &impl std::fmt::Display) -> String {
660 format!("Can't open known_hosts: {}", e)
661}
662
663pub const IMPORT_HOME_DIR_UNKNOWN: &str = "Could not determine home directory.";
664pub const IMPORT_KNOWN_HOSTS_MISSING: &str = "~/.ssh/known_hosts not found.";
665
666pub fn snippet_ssh_launch_failed(e: &impl std::fmt::Display) -> String {
669 format!("Failed to launch ssh: {}", e)
670}
671
672pub fn vault_create_dir_failed(path: &impl std::fmt::Display) -> String {
679 format!("Failed to create {}", path)
680}
681
682pub fn vault_write_cert_failed(path: &impl std::fmt::Display) -> String {
683 format!("Failed to write certificate to {}", path)
684}
685
686pub fn vault_ssh_keygen_run_failed(e: &impl std::fmt::Display) -> String {
687 format!("Failed to run ssh-keygen: {}", e)
688}
689
690pub const CONTAINER_ID_EMPTY: &str = "Container ID must not be empty.";
697pub const CONTAINER_RUNTIME_MISSING: &str = "No container runtime found. Install Docker or Podman.";
698
699pub fn container_id_invalid_char(c: char) -> String {
700 format!("Container ID contains invalid character: '{c}'")
701}
702
703pub fn container_unknown_sentinel(s: &str) -> String {
704 format!("Unknown sentinel: {s}")
705}
706
707pub fn container_invalid_id(reason: &str) -> String {
708 format!("Container exec blocked: {reason}")
709}
710
711pub fn scp_copying_one(source: &str) -> String {
714 format!("Copying {}...", source)
715}
716
717pub fn scp_copying_many(count: usize) -> String {
720 format!("Copying {} files...", count)
721}
722
723pub fn scp_failed_exit_code(code: i32) -> String {
726 format!("Copy failed (exit code {}).", code)
727}
728
729pub fn scp_spawn_failed(e: &impl std::fmt::Display) -> String {
733 format!("scp failed: {}", e)
734}
735
736pub const GLOBAL_DEFAULT_CLEARED: &str = "Global default cleared.";
739pub const PASSWORD_SOURCE_CLEARED: &str = "Password source cleared.";
740pub const ASKPASS_CUSTOM_COMMAND_HINT: &str =
741 "Type your command. Use %a (alias) and %h (hostname) as placeholders.";
742
743pub fn global_default_set(label: &str) -> String {
744 format!("Global default set to {}.", label)
745}
746
747pub fn password_source_set(label: &str) -> String {
748 format!("Password source set to {}.", label)
749}
750
751pub fn complete_path(label: &str) -> String {
752 format!("Complete the {} path.", label)
753}
754
755pub fn key_selected(name: &str) -> String {
756 format!("Locked and loaded with {}.", name)
757}
758
759pub fn keys_copy_success(name: &str) -> String {
763 format!("Copied {}.pub to clipboard.", name)
764}
765
766pub fn keys_copy_read_failed(name: &str) -> String {
768 format!("Could not read {}.pub from disk.", name)
769}
770
771pub const KEYS_EMPTY_HINT: &str = "No SSH keys in ~/.ssh/. Run ssh-keygen.";
774
775pub const KEY_PUSH_NO_HOSTS: &str =
778 "No hosts in ~/.ssh/config. Add a host first, then come back here.";
779
780pub const VAULT_STRIP_EMPTY: &str =
783 " No active certs. Press V to sign all Vault SSH hosts at once.";
784
785pub const KEY_PUSH_VAULT_TAG: &str = " (vault)";
788
789pub fn key_push_picker_title_eligible(key_label: &str, eligible: usize, total: usize) -> String {
791 format!(
792 "Push {} \u{203A} Select Hosts ({} eligible of {})",
793 key_label, eligible, total
794 )
795}
796
797pub fn key_push_picker_title_selected(
798 key_label: &str,
799 selected: usize,
800 total: usize,
801 eligible: usize,
802) -> String {
803 format!(
804 "Push {} \u{203A} {} selected of {} ({} eligible)",
805 key_label, selected, total, eligible
806 )
807}
808
809pub fn key_push_no_pubkey(name: &str) -> String {
811 format!(
812 "Cannot read {}.pub. The file is missing or unreadable.",
813 name
814 )
815}
816
817pub const KEY_PUSH_NONE_SELECTED: &str = "Select at least one host with Space.";
819
820pub const KEY_PUSH_VAULT_SKIP: &str =
824 "Vault SSH host. Use V on the host list to sign a cert instead.";
825
826pub fn key_push_in_progress(key_name: &str, host_count: usize) -> String {
828 format!("Pushing {} to {} host(s)...", key_name, host_count)
829}
830
831pub fn key_push_thread_spawn_failed() -> String {
833 "Could not spawn push worker thread. Check resource limits.".to_string()
834}
835
836pub const KEY_PUSH_ALREADY_IN_PROGRESS: &str =
839 "A push is already running. Press Esc to cancel first.";
840
841pub fn key_push_pubkey_not_regular(name: &str) -> String {
845 format!("{}.pub is not a regular file. Symlinks are rejected.", name)
846}
847
848pub fn key_push_pubkey_too_large(name: &str, bytes: u64) -> String {
852 format!(
853 "{}.pub is {} bytes, larger than the 16 KiB push limit.",
854 name, bytes
855 )
856}
857
858pub fn key_push_invalid_pubkey(name: &str, detail: &str) -> String {
863 format!("{}.pub failed validation: {}. Push aborted.", name, detail)
864}
865
866pub const KEY_PUSH_NO_HOSTS_SELECTED: &str =
870 "Picker committed with no eligible hosts. Push aborted.";
871
872pub const KEY_PUSH_CERT_NOT_PUSHABLE: &str =
876 "Certificates cannot be pushed as static keys. Sign with V instead.";
877
878pub fn key_push_cancelled(done: usize, total: usize) -> String {
882 format!(
883 "Push cancelled after {} of {} host(s). Re-run to finish the rest.",
884 done, total,
885 )
886}
887
888pub fn key_push_confirm_body(key_name: &str, host_count: usize) -> String {
890 if host_count == 1 {
891 format!("Push {} to 1 host?", key_name)
892 } else {
893 format!("Push {} to {} hosts?", key_name, host_count)
894 }
895}
896
897pub fn key_push_success(appended: usize, already: usize) -> String {
899 if appended == 0 && already > 0 {
900 format!("Key already present on {} host(s). Nothing to do.", already)
901 } else if already == 0 {
902 format!("Pushed to {} host(s).", appended)
903 } else {
904 format!(
905 "Pushed to {} host(s). Already present on {}.",
906 appended, already
907 )
908 }
909}
910
911pub fn key_push_partial_failure(succeeded: usize, failed: usize) -> String {
914 format!("Pushed to {} host(s). {} failed.", succeeded, failed)
915}
916
917pub fn key_push_all_failed(count: usize) -> String {
919 format!(
920 "Push failed for all {} host(s). Check the host log for details.",
921 count
922 )
923}
924
925pub fn proxy_jump_set(alias: &str) -> String {
926 format!("Jumping through {}.", alias)
927}
928
929pub fn save_default_failed(e: &impl std::fmt::Display) -> String {
930 format!("Failed to save default: {}", e)
931}
932
933pub fn container_action_complete(action: &str) -> String {
936 format!("Container {} complete.", action)
937}
938
939pub const HOST_KEY_UNKNOWN: &str = "Host key unknown. Connect first (Enter) to trust the host.";
940pub const HOST_KEY_CHANGED: &str =
941 "Host key changed. Possible tampering or server re-install. Clear with ssh-keygen -R.";
942
943pub const CONTAINER_RUNTIME_NOT_FOUND: &str = "Docker or Podman not found on remote host.";
947pub const CONTAINER_PERMISSION_DENIED: &str =
948 "Permission denied. Is your user in the docker group?";
949pub const CONTAINER_DAEMON_NOT_RUNNING: &str = "Container daemon is not running.";
950pub const CONTAINER_CONNECTION_REFUSED: &str = "Connection refused.";
951pub const CONTAINER_HOST_UNREACHABLE: &str = "Host unreachable.";
952
953pub fn container_command_failed(code: i32) -> String {
957 format!("Command failed with code {}.", code)
958}
959
960pub const CONTAINER_INSPECT_EMPTY: &str = "Inspect returned no data.";
962
963pub fn container_inspect_parse_failed(reason: &str) -> String {
965 format!("Inspect parse failed: {}", reason)
966}
967
968pub fn container_not_running(name: &str) -> String {
972 format!("{} is not running. Cannot exec.", name)
973}
974
975pub const DEMO_CONTAINER_EXEC_DISABLED: &str = "Demo mode: container exec disabled.";
977
978pub fn container_exec_opened_in_tmux(name: &str, alias: &str) -> String {
980 format!("Opened {} on {} in tmux window.", name, alias)
981}
982
983pub fn container_exec_ended(name: &str) -> String {
985 format!("Container shell ended: {}.", name)
986}
987
988pub fn container_exec_failed_with_reason(name: &str, reason: &str) -> String {
990 format!("Container exec failed for {}: {}", name, reason)
991}
992
993pub fn container_exec_exited_with_code(name: &str, code: i32) -> String {
995 format!("Container exec for {} exited with code {}.", name, code)
996}
997
998pub fn container_exec_spawn_failed(name: &str) -> String {
1000 format!("Failed to launch ssh for container {}.", name)
1001}
1002
1003pub const CONTAINER_EXEC_INVALID_COMMAND: &str =
1005 "Command rejected: control characters not allowed.";
1006
1007pub const CONTAINER_LOGS_LOADING: &str = "fetching logs…";
1011
1012pub fn container_logs_fetched(secs_ago: u64) -> String {
1015 format!(
1016 "fetched {} ago",
1017 crate::containers::format_uptime_short(secs_ago)
1018 )
1019}
1020
1021pub fn container_logs_failed(reason: &str) -> String {
1023 format!("logs fetch failed: {}", reason)
1024}
1025
1026pub fn container_logs_search_position(current: usize, total: usize) -> String {
1029 format!("{} of {}", current, total)
1030}
1031
1032pub const CONTAINER_LOGS_SEARCH_NO_MATCHES: &str = "no matches";
1034
1035pub const CONTAINER_RESTART_BODY: &str =
1039 "Sends SIGTERM, waits 10s, then SIGKILL. Live connections will drop.";
1040pub const CONTAINER_STOP_BODY: &str = "Sends SIGTERM, waits 10s, then SIGKILL. Container will not restart unless its policy reschedules it.";
1041
1042pub fn container_stack_unknown(name: &str) -> String {
1045 format!("Stack unknown for {}: open the detail panel first.", name)
1046}
1047
1048pub fn container_stack_no_running(project: &str) -> String {
1049 format!("Stack {} has no running members to restart.", project)
1050}
1051
1052pub const CONTAINER_STACK_RESTART_BODY: &str = "Restart cycles every running member one by one. Exited members are not touched. Live connections will drop.";
1053
1054pub const CONTAINER_HOST_RESTART_ALL_BODY: &str = "Restart cycles every running container on the host one by one. Exited containers are not touched. Live connections will drop.";
1060
1061pub const CONTAINER_HOST_STOP_ALL_BODY: &str = "Stops every running container on the host one by one. Exited containers are not touched. Restart policies may reschedule them.";
1063
1064pub fn container_action_needs_single(action: &str) -> String {
1070 format!(
1071 "{} need a single container. Place the cursor on a container row.",
1072 action.to_lowercase()
1073 )
1074}
1075
1076pub fn container_host_no_running(alias: &str) -> String {
1078 format!("No running containers on {}.", alias)
1079}
1080
1081pub fn container_refreshing(alias: &str) -> String {
1085 format!("Refreshing {}…", alias)
1086}
1087
1088pub const REFRESH_BATCH_ALREADY_RUNNING: &str = "Refresh already in progress.";
1090
1091pub const REFRESH_NOTHING_TO_REFRESH: &str = "No cached hosts to refresh. Press 'a' to add a host.";
1093
1094pub fn container_refresh_progress(done: usize, total: usize) -> String {
1096 format!("Refreshing {}/{} hosts…", done, total)
1097}
1098
1099pub fn container_refresh_complete(total: usize) -> String {
1101 format!(
1102 "Refreshed {} host{}.",
1103 total,
1104 if total == 1 { "" } else { "s" }
1105 )
1106}
1107
1108pub const CONTAINER_HOST_PICKER_NO_MATCH: &str = "No hosts match.";
1110
1111pub const CONTAINER_HOST_PICKER_NOTHING_TO_ADD: &str =
1113 "All hosts already cached. Use 'r' or 'R' to refresh.";
1114
1115pub fn imported_hosts(imported: usize, skipped: usize) -> String {
1118 format!(
1119 "Imported {} host{}, skipped {} duplicate{}.",
1120 imported,
1121 if imported == 1 { "" } else { "s" },
1122 skipped,
1123 if skipped == 1 { "" } else { "s" }
1124 )
1125}
1126
1127pub fn all_hosts_exist(skipped: usize) -> String {
1128 if skipped == 1 {
1129 "Host already exists.".to_string()
1130 } else {
1131 format!("All {} hosts already exist.", skipped)
1132 }
1133}
1134
1135pub fn config_repaired(groups: usize, orphaned: usize) -> String {
1138 format!(
1139 "Repaired SSH config ({} absorbed, {} orphaned group headers).",
1140 groups, orphaned
1141 )
1142}
1143
1144pub fn no_exact_match(alias: &str) -> String {
1145 format!("No exact match for '{}'. Here's what we found.", alias)
1146}
1147
1148pub fn group_pref_reset_failed(e: &impl std::fmt::Display) -> String {
1149 format!("Group preference reset. (save failed: {})", e)
1150}
1151
1152pub fn opened_in_tmux(alias: &str) -> String {
1155 format!("Opened {} in new tmux window.", alias)
1156}
1157
1158pub fn tmux_error(e: &impl std::fmt::Display) -> String {
1159 format!("tmux: {}", e)
1160}
1161
1162pub fn connection_failed(alias: &str) -> String {
1163 format!("Connection to {} failed.", alias)
1164}
1165
1166pub fn connection_spawn_failed(e: &impl std::fmt::Display) -> String {
1170 format!("Connection failed: {}", e)
1171}
1172
1173pub fn ssh_failed_with_reason(alias: &str, reason: &str) -> String {
1176 format!("SSH to {} failed. {}", alias, reason)
1177}
1178
1179pub fn ssh_exited_with_code(alias: &str, code: i32) -> String {
1182 format!("SSH to {} exited with code {}.", alias, code)
1183}
1184
1185pub fn host_key_remove_failed(stderr: &str) -> String {
1188 format!("Failed to remove host key: {}", stderr)
1189}
1190
1191pub fn ssh_keygen_failed(e: &impl std::fmt::Display) -> String {
1192 format!("Failed to run ssh-keygen: {}", e)
1193}
1194
1195pub const TRANSFER_COMPLETE: &str = "Transfer complete.";
1198
1199pub fn provider_progress(spinner: &str, name: &str, message: &str) -> String {
1208 format!("{} {}: {}", spinner, name, message)
1209}
1210
1211pub const AGE_JUST_NOW: &str = "just now";
1214
1215pub fn relative_age(elapsed: std::time::Duration) -> String {
1219 let secs = elapsed.as_secs();
1220 if secs < 5 {
1221 AGE_JUST_NOW.to_string()
1222 } else if secs < 60 {
1223 format!("{}s ago", secs)
1224 } else if secs < 3600 {
1225 format!("{}m ago", secs / 60)
1226 } else if secs < 86400 {
1227 format!("{}h ago", secs / 3600)
1228 } else {
1229 format!("{}d ago", secs / 86400)
1230 }
1231}
1232
1233pub fn vault_config_reapply_failed(signed: usize, e: &impl std::fmt::Display) -> String {
1236 format!(
1237 "External edits detected; signed {} certs but failed to re-apply CertificateFile: {}",
1238 signed, e
1239 )
1240}
1241
1242pub fn vault_external_edits_merged(summary: &str, reapplied: usize) -> String {
1243 format!(
1244 "{} External ssh config edits detected, merged {} CertificateFile directives.",
1245 summary, reapplied
1246 )
1247}
1248
1249pub fn vault_external_edits_no_write(summary: &str) -> String {
1250 format!(
1251 "{} External ssh config edits detected; certs on disk, no CertificateFile written.",
1252 summary
1253 )
1254}
1255
1256pub fn vault_reparse_failed(signed: usize, e: &impl std::fmt::Display) -> String {
1257 format!(
1258 "Signed {} certs but cannot re-parse ssh config after external edit: {}. \
1259 Certs are on disk under ~/.purple/certs/.",
1260 signed, e
1261 )
1262}
1263
1264pub fn vault_config_update_failed(signed: usize, e: &impl std::fmt::Display) -> String {
1265 format!(
1266 "Signed {} certs but failed to update SSH config: {}",
1267 signed, e
1268 )
1269}
1270
1271pub fn vault_config_write_after_sign(e: &impl std::fmt::Display) -> String {
1272 format!("Failed to update config after vault signing: {}", e)
1273}
1274
1275pub fn removed_host_key(hostname: &str) -> String {
1280 format!("Removed host key for {}. Reconnecting...", hostname)
1281}
1282
1283pub fn tagged_host(alias: &str, count: usize) -> String {
1286 format!(
1287 "Tagged {} with {} label{}.",
1288 alias,
1289 count,
1290 if count == 1 { "" } else { "s" }
1291 )
1292}
1293
1294pub fn config_reloaded(count: usize) -> String {
1297 format!("Config reloaded. {} hosts.", count)
1298}
1299
1300pub fn synced_progress(
1316 spinner: &str,
1317 active_names: &str,
1318 done: usize,
1319 total: usize,
1320 added: usize,
1321 updated: usize,
1322 stale: usize,
1323) -> String {
1324 debug_assert!(
1325 !active_names.is_empty(),
1326 "synced_progress must only be called while a provider is still in flight"
1327 );
1328 let diff = sync_diff_suffix(added, updated, stale);
1329 format!(
1330 "{} Syncing {} \u{00B7} {}/{}{}",
1331 spinner, active_names, done, total, diff
1332 )
1333}
1334
1335pub fn synced_done(
1340 done: usize,
1341 total: usize,
1342 names: &str,
1343 added: usize,
1344 updated: usize,
1345 stale: usize,
1346) -> String {
1347 let diff = sync_diff_suffix(added, updated, stale);
1348 format!("Synced {}/{} \u{00B7} {}{}", done, total, names, diff)
1349}
1350
1351fn sync_diff_suffix(added: usize, updated: usize, stale: usize) -> String {
1352 let parts: Vec<String> = [(added, '+'), (updated, '~'), (stale, '-')]
1353 .iter()
1354 .filter(|(n, _)| *n > 0)
1355 .map(|(n, sign)| format!("{}{}", sign, n))
1356 .collect();
1357 if parts.is_empty() {
1358 String::new()
1359 } else {
1360 format!(" ({})", parts.join(" "))
1361 }
1362}
1363
1364pub const SYNC_THREAD_SPAWN_FAILED: &str = "Failed to start sync thread.";
1365
1366pub const SYNC_UNKNOWN_PROVIDER: &str = "Unknown provider.";
1367
1368pub fn vault_signing_cancelled_summary(
1371 signed: u32,
1372 failed: u32,
1373 first_error: Option<&str>,
1374) -> String {
1375 let mut msg = format!(
1376 "Vault SSH signing cancelled ({} signed, {} failed)",
1377 signed, failed
1378 );
1379 if let Some(err) = first_error {
1380 msg.push_str(": ");
1381 msg.push_str(err);
1382 }
1383 msg
1384}
1385
1386pub fn regions_selected_count(count: usize, label: &str) -> String {
1389 let s = if count == 1 { "" } else { "s" };
1390 format!("{} {}{} selected.", count, label, s)
1391}
1392
1393pub const NO_CLIPBOARD_TOOL: &str =
1398 "No clipboard tool found. Install pbcopy (macOS), wl-copy (Wayland), or xclip/xsel (X11).";
1399
1400pub const MCP_TOOL_DENIED_READ_ONLY: &str = "Tool denied. Server started with --read-only. Restart without --read-only to enable state-changing tools.";
1403
1404pub fn mcp_audit_init_failed(path: &impl std::fmt::Display, e: &impl std::fmt::Display) -> String {
1408 format!(
1409 "Failed to initialise MCP audit log at {}: {}. Continuing without audit logging.",
1410 path, e
1411 )
1412}
1413
1414pub fn mcp_audit_write_failed(e: &impl std::fmt::Display) -> String {
1416 format!("Failed to write MCP audit entry: {}", e)
1417}
1418
1419pub fn mcp_config_file_not_found(path: &impl std::fmt::Display) -> String {
1423 format!("SSH config file not found: {}", path)
1424}
1425
1426pub const MCP_AUDIT_HOME_DIR_UNAVAILABLE: &str = "Could not determine home directory; MCP audit log disabled. Set --audit-log <PATH> explicitly to enable auditing.";
1430
1431pub const PALETTE_PLACEHOLDER: &str = "Find anything";
1435pub const PALETTE_NO_RESULTS: &str = "No matches.";
1437pub const PALETTE_SNIPPET_NEEDS_HOST: &str =
1440 "Pick a host first, then run a snippet from the jump bar.";
1441pub fn jump_more_rows(n: usize) -> String {
1444 format!("+{n} more (scroll down)")
1445}
1446
1447#[path = "messages/cli.rs"]
1450pub mod cli;
1451pub mod footer;
1452
1453pub mod update {
1456 pub const WHATS_NEW_HINT: &str = "Press n inside purple to see what's new.";
1457 pub const DONE: &str = "done.";
1458 pub const CHECKSUM_OK: &str = "ok.";
1459 pub const SUDO_WARNING: &str =
1460 "Running via sudo. Consider fixing directory permissions instead.";
1461
1462 pub const STEP_CHECKING: &str = " Checking for updates... ";
1467 pub const STEP_VERIFYING_CHECKSUM: &str = " Verifying checksum... ";
1468 pub const STEP_INSTALLING: &str = " Installing... ";
1469
1470 pub fn already_on(current: &str) -> String {
1471 format!("already on v{} (latest).", current)
1472 }
1473
1474 pub fn available(latest: &str, current: &str) -> String {
1475 format!("v{} available (current: v{}).", latest, current)
1476 }
1477
1478 pub fn step_downloading(version: &str) -> String {
1482 format!(" Downloading v{}... ", version)
1483 }
1484
1485 pub fn sudo_warning_line(bold_bang: &str) -> String {
1489 format!(" {} {}", bold_bang, SUDO_WARNING)
1490 }
1491
1492 pub fn header(bold_name: &str) -> String {
1493 format!("\n {} updater\n", bold_name)
1494 }
1495
1496 pub fn binary_path(path: &std::path::Path) -> String {
1497 format!(" Binary: {}", path.display())
1498 }
1499
1500 pub fn installed_at(bold_version: &str, path: &std::path::Path) -> String {
1501 format!("\n {} installed at {}.", bold_version, path.display())
1502 }
1503
1504 pub fn whats_new_hint_indented() -> String {
1505 format!("\n {}", WHATS_NEW_HINT)
1506 }
1507}
1508
1509pub mod askpass {
1512 pub const BW_NOT_FOUND: &str = "Bitwarden CLI (bw) not found. SSH will prompt for password.";
1513 pub const BW_NOT_LOGGED_IN: &str = "Bitwarden vault not logged in. Run 'bw login' first.";
1514 pub const EMPTY_PASSWORD: &str = "Empty password. SSH will prompt for password.";
1515 pub const PASSWORD_IN_KEYCHAIN: &str = "Password stored in keychain.";
1516
1517 pub fn read_failed(e: &impl std::fmt::Display) -> String {
1518 format!("Failed to read password: {}", e)
1519 }
1520
1521 pub fn unlock_failed_retry(e: &impl std::fmt::Display) -> String {
1522 format!("Unlock failed: {}. Try again.", e)
1523 }
1524
1525 pub fn unlock_failed_prompt(e: &impl std::fmt::Display) -> String {
1526 format!("Unlock failed: {}. SSH will prompt for password.", e)
1527 }
1528
1529 pub fn password_prompt(alias: &str) -> String {
1533 format!("Password for {}: ", alias)
1534 }
1535
1536 pub fn keychain_password_prompt(alias: &str) -> String {
1539 format!("Password for {} (stored in keychain): ", alias)
1540 }
1541
1542 pub fn keychain_store_failed(e: &impl std::fmt::Display) -> String {
1545 format!(
1546 "Failed to store in keychain: {}. SSH will prompt for password.",
1547 e
1548 )
1549 }
1550
1551 pub const PROTON_NOT_FOUND: &str =
1552 "Proton Pass CLI (pass-cli) not found. SSH will prompt for password.";
1553
1554 pub const PROTON_LOGIN_PROMPT: &str = "Proton Pass PAT: ";
1555
1556 pub const PROTON_LOGIN_SUCCESS: &str = "Logged in to Proton Pass.";
1557
1558 pub fn proton_login_failed_retry(e: &impl std::fmt::Display) -> String {
1559 format!("Proton Pass login failed: {}. Try again.", e)
1560 }
1561
1562 pub fn proton_login_failed_prompt(e: &impl std::fmt::Display) -> String {
1563 format!(
1564 "Proton Pass login failed: {}. SSH will prompt for password.",
1565 e
1566 )
1567 }
1568}
1569
1570pub mod logging {
1573 pub fn init_failed(e: &impl std::fmt::Display) -> String {
1574 format!("[purple] Failed to initialize logger: {}", e)
1575 }
1576
1577 pub const SSH_VERSION_FAILED: &str = "[purple] Failed to detect SSH version. Is ssh installed?";
1578}
1579
1580pub mod hints {
1586 pub const IDENTITY_FILE_PICK: &str = "Space to pick a key";
1591 pub const DEFAULT_SSH_USER: &str = "root";
1592
1593 pub const HOST_ALIAS: &str = "e.g. prod or db-01";
1595 pub const HOST_ALIAS_PATTERN: &str = "10.0.0.* or *.example.com";
1596 pub const HOST_HOSTNAME: &str = "192.168.1.1 or example.com";
1597 pub const HOST_PORT: &str = "22";
1598 pub const HOST_PROXY_JUMP: &str = "Space to pick a host";
1599 pub const HOST_VAULT_SSH: &str = "e.g. ssh-client-signer/sign/my-role (auth via vault login)";
1600 pub const HOST_VAULT_SSH_PICKER: &str = "Space to pick a role or type one";
1601 pub const HOST_VAULT_ADDR: &str =
1602 "e.g. http://127.0.0.1:8200 (inherits from provider or env when empty)";
1603 pub const HOST_TAGS: &str = "e.g. prod, staging, us-east (comma-separated)";
1604 pub const HOST_ASKPASS_PICK: &str = "Space to pick a source";
1605
1606 pub fn askpass_default(default: &str) -> String {
1607 format!("default: {}", default)
1608 }
1609
1610 pub fn inherits_from(value: &str, provider: &str) -> String {
1611 format!("inherits {} from {}", value, provider)
1612 }
1613
1614 pub const TUNNEL_BIND_PORT: &str = "8080";
1616 pub const TUNNEL_REMOTE_HOST: &str = "localhost";
1617 pub const TUNNEL_REMOTE_PORT: &str = "80";
1618
1619 pub const SNIPPET_NAME: &str = "check-disk";
1621 pub const SNIPPET_COMMAND: &str = "df -h";
1622 pub const SNIPPET_OPTIONAL: &str = "(optional)";
1623
1624 pub const PROVIDER_URL: &str = "https://pve.example.com:8006";
1626 pub const PROVIDER_TOKEN_DEFAULT: &str = "your-api-token";
1627 pub const PROVIDER_TOKEN_PROXMOX: &str = "user@pam!token=secret";
1628 pub const PROVIDER_TOKEN_AWS: &str = "AccessKeyId:Secret (or use Profile)";
1629 pub const PROVIDER_TOKEN_GCP: &str = "/path/to/service-account.json (or access token)";
1630 pub const PROVIDER_TOKEN_AZURE: &str = "/path/to/service-principal.json (or access token)";
1631 pub const PROVIDER_TOKEN_TAILSCALE: &str = "API key (leave empty for local CLI)";
1632 pub const PROVIDER_TOKEN_ORACLE: &str = "~/.oci/config";
1633 pub const PROVIDER_TOKEN_OVH: &str = "app_key:app_secret:consumer_key";
1634 pub const PROVIDER_PROFILE: &str = "Name from ~/.aws/credentials (or use Token)";
1635 pub const PROVIDER_PROJECT_DEFAULT: &str = "my-gcp-project-id";
1636 pub const PROVIDER_PROJECT_OVH: &str = "Public Cloud project ID";
1637 pub const PROVIDER_COMPARTMENT: &str = "ocid1.compartment.oc1..aaaa...";
1638 pub const PROVIDER_REGIONS_DEFAULT: &str = "Space to select regions";
1639 pub const PROVIDER_REGIONS_GCP: &str = "Space to select zones (empty = all)";
1640 pub const PROVIDER_REGIONS_SCALEWAY: &str = "Space to select zones";
1641 pub const PROVIDER_REGIONS_AZURE: &str = "comma-separated subscription IDs";
1643 pub const PROVIDER_REGIONS_OVH: &str = "Space to select endpoint (default: EU)";
1644 pub const PROVIDER_USER_AWS: &str = "ec2-user";
1645 pub const PROVIDER_USER_GCP: &str = "ubuntu";
1646 pub const PROVIDER_USER_AZURE: &str = "azureuser";
1647 pub const PROVIDER_USER_ORACLE: &str = "opc";
1648 pub const PROVIDER_USER_OVH: &str = "ubuntu";
1649 pub const PROVIDER_VAULT_ROLE: &str =
1650 "e.g. ssh-client-signer/sign/my-role (vault login; inherited)";
1651 pub const PROVIDER_VAULT_ADDR: &str = "e.g. http://127.0.0.1:8200 (inherited by all hosts)";
1652 pub const PROVIDER_ALIAS_PREFIX_DEFAULT: &str = "prefix";
1653}
1654
1655#[cfg(test)]
1656mod hints_tests {
1657 use super::hints;
1658
1659 #[test]
1660 fn askpass_default_formats() {
1661 assert_eq!(hints::askpass_default("keychain"), "default: keychain");
1662 }
1663
1664 #[test]
1665 fn askpass_default_formats_empty() {
1666 assert_eq!(hints::askpass_default(""), "default: ");
1667 }
1668
1669 #[test]
1670 fn inherits_from_formats() {
1671 assert_eq!(
1672 hints::inherits_from("role/x", "aws"),
1673 "inherits role/x from aws"
1674 );
1675 }
1676
1677 #[test]
1678 fn picker_hints_mention_space_not_enter() {
1679 for s in [
1682 hints::IDENTITY_FILE_PICK,
1683 hints::HOST_PROXY_JUMP,
1684 hints::HOST_VAULT_SSH_PICKER,
1685 hints::HOST_ASKPASS_PICK,
1686 hints::PROVIDER_REGIONS_DEFAULT,
1687 hints::PROVIDER_REGIONS_GCP,
1688 hints::PROVIDER_REGIONS_SCALEWAY,
1689 hints::PROVIDER_REGIONS_OVH,
1690 ] {
1691 assert!(
1692 s.starts_with("Space "),
1693 "picker hint must mention Space: {s}"
1694 );
1695 assert!(!s.contains("Enter "), "picker hint must not say Enter: {s}");
1696 }
1697 }
1698}
1699
1700#[path = "messages/whats_new.rs"]
1701pub mod whats_new;
1702
1703#[path = "messages/whats_new_toast.rs"]
1704pub mod whats_new_toast;
1705
1706#[cfg(test)]
1707mod stale_host_tests {
1708 use super::stale_host;
1709
1710 #[test]
1711 fn empty_hint_returns_bare_sentence() {
1712 assert_eq!(stale_host(""), "Stale host.");
1713 }
1714
1715 #[test]
1716 fn empty_after_trim_returns_bare_sentence() {
1717 assert_eq!(stale_host(" "), "Stale host.");
1718 }
1719
1720 #[test]
1721 fn provider_hint_is_appended_with_space_and_period() {
1722 assert_eq!(
1723 stale_host("Gone from DigitalOcean"),
1724 "Stale host. Gone from DigitalOcean."
1725 );
1726 }
1727
1728 #[test]
1729 fn trailing_period_in_hint_is_not_doubled() {
1730 assert_eq!(
1731 stale_host("Gone from DigitalOcean."),
1732 "Stale host. Gone from DigitalOcean."
1733 );
1734 }
1735
1736 #[test]
1737 fn leading_space_in_hint_is_trimmed() {
1738 assert_eq!(stale_host(" Gone from AWS"), "Stale host. Gone from AWS.");
1739 }
1740}
1741
1742#[cfg(test)]
1743mod relative_age_tests {
1744 use super::relative_age;
1745 use std::time::Duration;
1746
1747 #[test]
1748 fn relative_age_boundaries() {
1749 assert_eq!(relative_age(Duration::from_secs(0)), "just now");
1750 assert_eq!(relative_age(Duration::from_secs(4)), "just now");
1751 assert_eq!(relative_age(Duration::from_secs(5)), "5s ago");
1752 assert_eq!(relative_age(Duration::from_secs(59)), "59s ago");
1753 assert_eq!(relative_age(Duration::from_secs(60)), "1m ago");
1754 assert_eq!(relative_age(Duration::from_secs(3599)), "59m ago");
1755 assert_eq!(relative_age(Duration::from_secs(3600)), "1h ago");
1756 assert_eq!(relative_age(Duration::from_secs(86399)), "23h ago");
1757 assert_eq!(relative_age(Duration::from_secs(86400)), "1d ago");
1758 assert_eq!(relative_age(Duration::from_secs(86400 * 7)), "7d ago");
1759 }
1760}