Skip to main content

purple_ssh/
messages.rs

1//! Centralized user-facing messages.
2//!
3//! Every string the user can see (toasts, CLI output, error messages) lives
4//! here. Handler, CLI and UI code reference these constants and functions
5//! instead of inlining string literals. This makes copy consistent, auditable
6//! and future-proof for i18n.
7
8// ── General / shared ────────────────────────────────────────────────
9
10pub 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
18// ── Demo mode ───────────────────────────────────────────────────────
19
20pub 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
30// ── Stale host ──────────────────────────────────────────────────────
31
32pub fn stale_host(hint: &str) -> String {
33    format!("Stale host.{}", hint)
34}
35
36// ── Host list ───────────────────────────────────────────────────────
37
38pub fn copied_ssh_command(alias: &str) -> String {
39    format!("Copied SSH command for {}.", alias)
40}
41
42pub fn copied_config_block(alias: &str) -> String {
43    format!("Copied config block for {}.", alias)
44}
45
46pub fn showing_unreachable(count: usize) -> String {
47    format!(
48        "Showing {} unreachable host{}.",
49        count,
50        if count == 1 { "" } else { "s" }
51    )
52}
53
54pub fn sorted_by(label: &str) -> String {
55    format!("Sorted by {}.", label)
56}
57
58pub fn sorted_by_save_failed(label: &str, e: &impl std::fmt::Display) -> String {
59    format!("Sorted by {}. (save failed: {})", label, e)
60}
61
62pub fn grouped_by(label: &str) -> String {
63    format!("Grouped by {}.", label)
64}
65
66pub fn grouped_by_save_failed(label: &str, e: &impl std::fmt::Display) -> String {
67    format!("Grouped by {}. (save failed: {})", label, e)
68}
69
70pub const UNGROUPED: &str = "Ungrouped.";
71
72pub fn ungrouped_save_failed(e: &impl std::fmt::Display) -> String {
73    format!("Ungrouped. (save failed: {})", e)
74}
75
76pub const GROUPED_BY_TAG: &str = "Grouped by tag.";
77
78pub fn grouped_by_tag_save_failed(e: &impl std::fmt::Display) -> String {
79    format!("Grouped by tag. (save failed: {})", e)
80}
81
82pub fn host_restored(alias: &str) -> String {
83    format!("{} is back from the dead.", alias)
84}
85
86pub fn restored_tags(count: usize) -> String {
87    format!(
88        "Restored tags on {} host{}.",
89        count,
90        if count == 1 { "" } else { "s" }
91    )
92}
93
94pub const NOTHING_TO_UNDO: &str = "Nothing to undo.";
95pub const NO_IMPORTABLE_HOSTS: &str = "No importable hosts in known_hosts.";
96pub const NO_STALE_HOSTS: &str = "No stale hosts.";
97pub const NO_HOST_SELECTED: &str = "No host selected.";
98pub const NO_HOSTS_TO_RUN: &str = "No hosts to run on.";
99pub const NO_HOSTS_TO_TAG: &str = "No hosts to tag.";
100pub const PING_FIRST: &str = "Ping first (p/P), then filter with !.";
101pub const PINGING_ALL: &str = "Pinging all the things...";
102
103pub fn included_file_edit(name: &str) -> String {
104    format!("{} is in an included file. Edit it there.", name)
105}
106
107pub fn included_file_delete(name: &str) -> String {
108    format!("{} is in an included file. Delete it there.", name)
109}
110
111pub fn included_file_clone(name: &str) -> String {
112    format!("{} is in an included file. Clone it there.", name)
113}
114
115pub fn included_host_lives_in(alias: &str, path: &impl std::fmt::Display) -> String {
116    format!("{} lives in {}. Edit it there.", alias, path)
117}
118
119pub fn included_host_clone_there(alias: &str, path: &impl std::fmt::Display) -> String {
120    format!("{} lives in {}. Clone it there.", alias, path)
121}
122
123pub fn included_host_tag_there(alias: &str, path: &impl std::fmt::Display) -> String {
124    format!("{} is included from {}. Tag it there.", alias, path)
125}
126
127pub const HOST_NOT_FOUND_IN_CONFIG: &str = "Host not found in config.";
128
129// ── Host form ───────────────────────────────────────────────────────
130
131pub const SMART_PARSED: &str = "Smart-parsed that for you. Check the fields.";
132pub const LOOKS_LIKE_ADDRESS: &str = "Looks like an address. Suggested as Host.";
133
134// ── Confirm delete ──────────────────────────────────────────────────
135
136pub fn goodbye_host(alias: &str) -> String {
137    format!("Goodbye, {}. We barely knew ye. (u to undo)", alias)
138}
139
140pub fn host_not_found(alias: &str) -> String {
141    format!("Host '{}' not found.", alias)
142}
143
144pub fn cert_cleanup_warning(path: &impl std::fmt::Display, e: &impl std::fmt::Display) -> String {
145    format!("Warning: failed to clean up Vault SSH cert {}: {}", path, e)
146}
147
148// ── Clone ───────────────────────────────────────────────────────────
149
150pub const CLONED_VAULT_CLEARED: &str = "Cloned. Vault SSH role cleared on copy.";
151
152// ── Tunnels ─────────────────────────────────────────────────────────
153
154pub const TUNNEL_REMOVED: &str = "Tunnel removed.";
155pub const TUNNEL_SAVED: &str = "Tunnel saved.";
156pub const TUNNEL_NOT_FOUND: &str = "Tunnel not found in config.";
157pub const TUNNEL_INCLUDED_READ_ONLY: &str = "Included host. Tunnels are read-only.";
158pub const TUNNEL_ORIGINAL_NOT_FOUND: &str = "Original tunnel not found in config.";
159pub const TUNNEL_LIST_CHANGED: &str = "Tunnel list changed externally. Press Esc and re-open.";
160pub const TUNNEL_DUPLICATE: &str = "Duplicate tunnel already configured.";
161
162pub fn tunnel_stopped(alias: &str) -> String {
163    format!("Tunnel for {} stopped.", alias)
164}
165
166pub fn tunnel_started(alias: &str) -> String {
167    format!("Tunnel for {} started.", alias)
168}
169
170pub fn tunnel_start_failed(e: &impl std::fmt::Display) -> String {
171    format!("Failed to start tunnel: {}", e)
172}
173
174// ── Ping ────────────────────────────────────────────────────────────
175
176pub fn pinging_host(alias: &str, show_hint: bool) -> String {
177    if show_hint {
178        format!("Pinging {}... (Shift+P pings all)", alias)
179    } else {
180        format!("Pinging {}...", alias)
181    }
182}
183
184pub fn bastion_not_found(alias: &str) -> String {
185    format!("Bastion {} not found in config.", alias)
186}
187
188// ── Providers ───────────────────────────────────────────────────────
189
190pub fn provider_removed(display_name: &str) -> String {
191    format!(
192        "Removed {} configuration. Synced hosts remain in your SSH config.",
193        display_name
194    )
195}
196
197pub fn provider_not_configured(display_name: &str) -> String {
198    format!("{} is not configured. Nothing to remove.", display_name)
199}
200
201pub fn provider_configure_first(display_name: &str) -> String {
202    format!("Configure {} first. Press Enter to set up.", display_name)
203}
204
205pub fn provider_saved_syncing(display_name: &str) -> String {
206    format!("Saved {} configuration. Syncing...", display_name)
207}
208
209pub fn provider_saved(display_name: &str) -> String {
210    format!("Saved {} configuration.", display_name)
211}
212
213pub fn syncing_provider(display_name: &str) -> String {
214    format!("Syncing {}...", display_name)
215}
216
217pub fn no_stale_hosts_for(display_name: &str) -> String {
218    format!("No stale hosts for {}.", display_name)
219}
220
221pub fn contains_control_chars(name: &str) -> String {
222    format!("{} contains control characters.", name)
223}
224
225pub const TOKEN_FORMAT_AWS: &str = "Token format: AccessKeyId:SecretAccessKey";
226pub const URL_REQUIRED_PROXMOX: &str = "URL is required for Proxmox VE.";
227pub const PROJECT_REQUIRED_GCP: &str = "Project ID can't be empty. Set your GCP project ID.";
228pub const COMPARTMENT_REQUIRED_OCI: &str =
229    "Compartment can't be empty. Set your OCI compartment OCID.";
230pub const REGIONS_REQUIRED_AWS: &str = "Select at least one AWS region.";
231pub const ZONES_REQUIRED_SCALEWAY: &str = "Select at least one Scaleway zone.";
232pub const SUBSCRIPTIONS_REQUIRED_AZURE: &str = "Enter at least one Azure subscription ID.";
233pub const ALIAS_PREFIX_INVALID: &str =
234    "Alias prefix can't contain spaces or pattern characters (*, ?, [, !).";
235pub const USER_NO_WHITESPACE: &str = "User can't contain whitespace.";
236pub const VAULT_ROLE_FORMAT: &str = "Vault SSH role must be in the form <mount>/sign/<role>.";
237
238// ── Vault SSH ───────────────────────────────────────────────────────
239
240pub const VAULT_SIGNING_CANCELLED: &str = "Vault SSH signing cancelled.";
241pub const VAULT_NO_ROLE_CONFIGURED: &str = "No Vault SSH role configured. Set one in the host form \
242     (Vault SSH role field) or on a provider for shared defaults.";
243pub const VAULT_NO_HOSTS_WITH_ROLE: &str = "No hosts with a Vault SSH role configured.";
244pub const VAULT_ALL_CERTS_VALID: &str = "All Vault SSH certificates are still valid.";
245pub const VAULT_NO_ADDRESS: &str = "No Vault address set. Edit the host (e) or provider \
246     and fill in the Vault SSH Address field.";
247
248pub fn vault_error(msg: &str) -> String {
249    format!("Vault SSH: {}", msg)
250}
251
252pub fn vault_signed(alias: &str) -> String {
253    format!("Signed Vault SSH cert for {}", alias)
254}
255
256pub fn vault_sign_failed(alias: &str, message: &str) -> String {
257    format!("Vault SSH: failed to sign {}: {}", alias, message)
258}
259
260pub fn vault_signing_progress(spinner: &str, done: usize, total: usize, alias: &str) -> String {
261    format!(
262        "{} Signing {}/{}: {} (V to cancel)",
263        spinner, done, total, alias
264    )
265}
266
267pub fn vault_cert_saved_host_gone(alias: &str) -> String {
268    format!(
269        "Vault SSH cert saved for {} but host no longer in config \
270         (renamed or deleted). CertificateFile NOT written.",
271        alias
272    )
273}
274
275pub fn vault_spawn_failed(e: &impl std::fmt::Display) -> String {
276    format!("Vault SSH: failed to spawn signing thread: {}", e)
277}
278
279pub fn vault_cert_check_failed(alias: &str, message: &str) -> String {
280    format!("Cert check failed for {}: {}", alias, message)
281}
282
283pub fn vault_role_set(role: &str) -> String {
284    format!("Vault SSH role set to {}.", role)
285}
286
287// ── Snippets ────────────────────────────────────────────────────────
288
289pub fn snippet_removed(name: &str) -> String {
290    format!("Removed snippet '{}'.", name)
291}
292
293pub fn snippet_added(name: &str) -> String {
294    format!("Added snippet '{}'.", name)
295}
296
297pub fn snippet_updated(name: &str) -> String {
298    format!("Updated snippet '{}'.", name)
299}
300
301pub fn snippet_exists(name: &str) -> String {
302    format!("'{}' already exists.", name)
303}
304
305pub const OUTPUT_COPIED: &str = "Output copied.";
306
307pub fn copy_failed(e: &impl std::fmt::Display) -> String {
308    format!("Copy failed: {}", e)
309}
310
311// ── Picker (password source, key, proxy) ────────────────────────────
312
313pub const GLOBAL_DEFAULT_CLEARED: &str = "Global default cleared.";
314pub const PASSWORD_SOURCE_CLEARED: &str = "Password source cleared.";
315
316pub fn global_default_set(label: &str) -> String {
317    format!("Global default set to {}.", label)
318}
319
320pub fn password_source_set(label: &str) -> String {
321    format!("Password source set to {}.", label)
322}
323
324pub fn complete_path(label: &str) -> String {
325    format!("Complete the {} path.", label)
326}
327
328pub fn key_selected(name: &str) -> String {
329    format!("Locked and loaded with {}.", name)
330}
331
332pub fn proxy_jump_set(alias: &str) -> String {
333    format!("Jumping through {}.", alias)
334}
335
336pub fn save_default_failed(e: &impl std::fmt::Display) -> String {
337    format!("Failed to save default: {}", e)
338}
339
340// ── Containers ──────────────────────────────────────────────────────
341
342pub fn container_action_complete(action: &str) -> String {
343    format!("Container {} complete.", action)
344}
345
346pub const HOST_KEY_UNKNOWN: &str = "Host key unknown. Connect first (Enter) to trust the host.";
347pub const HOST_KEY_CHANGED: &str =
348    "Host key changed. Possible tampering or server re-install. Clear with ssh-keygen -R.";
349
350// ── Import ──────────────────────────────────────────────────────────
351
352pub fn imported_hosts(imported: usize, skipped: usize) -> String {
353    format!(
354        "Imported {} host{}, skipped {} duplicate{}.",
355        imported,
356        if imported == 1 { "" } else { "s" },
357        skipped,
358        if skipped == 1 { "" } else { "s" }
359    )
360}
361
362pub fn all_hosts_exist(skipped: usize) -> String {
363    if skipped == 1 {
364        "Host already exists.".to_string()
365    } else {
366        format!("All {} hosts already exist.", skipped)
367    }
368}
369
370// ── SSH config repair ───────────────────────────────────────────────
371
372pub fn config_repaired(groups: usize, orphaned: usize) -> String {
373    format!(
374        "Repaired SSH config ({} absorbed, {} orphaned group headers).",
375        groups, orphaned
376    )
377}
378
379pub fn no_exact_match(alias: &str) -> String {
380    format!("No exact match for '{}'. Here's what we found.", alias)
381}
382
383pub fn group_pref_reset_failed(e: &impl std::fmt::Display) -> String {
384    format!("Group preference reset. (save failed: {})", e)
385}
386
387// ── Connection ──────────────────────────────────────────────────────
388
389pub fn opened_in_tmux(alias: &str) -> String {
390    format!("Opened {} in new tmux window.", alias)
391}
392
393pub fn tmux_error(e: &impl std::fmt::Display) -> String {
394    format!("tmux: {}", e)
395}
396
397pub fn connection_failed(alias: &str) -> String {
398    format!("Connection to {} failed.", alias)
399}
400
401// ── Host key reset ──────────────────────────────────────────────────
402
403pub fn host_key_remove_failed(stderr: &str) -> String {
404    format!("Failed to remove host key: {}", stderr)
405}
406
407pub fn ssh_keygen_failed(e: &impl std::fmt::Display) -> String {
408    format!("Failed to run ssh-keygen: {}", e)
409}
410
411// ── Transfer ────────────────────────────────────────────────────────
412
413pub const TRANSFER_COMPLETE: &str = "Transfer complete.";
414
415// ── Background / event loop ─────────────────────────────────────────
416
417pub const PING_EXPIRED: &str = "Ping expired. Press P to refresh.";
418
419pub fn provider_event(name: &str, message: &str) -> String {
420    format!("{}: {}", name, message)
421}
422
423// ── Vault SSH bulk signing summaries (event_loop.rs) ────────────────
424
425pub fn vault_config_reapply_failed(signed: usize, e: &impl std::fmt::Display) -> String {
426    format!(
427        "External edits detected; signed {} certs but failed to re-apply CertificateFile: {}",
428        signed, e
429    )
430}
431
432pub fn vault_external_edits_merged(summary: &str, reapplied: usize) -> String {
433    format!(
434        "{} External ssh config edits detected, merged {} CertificateFile directives.",
435        summary, reapplied
436    )
437}
438
439pub fn vault_external_edits_no_write(summary: &str) -> String {
440    format!(
441        "{} External ssh config edits detected; certs on disk, no CertificateFile written.",
442        summary
443    )
444}
445
446pub fn vault_reparse_failed(signed: usize, e: &impl std::fmt::Display) -> String {
447    format!(
448        "Signed {} certs but cannot re-parse ssh config after external edit: {}. \
449         Certs are on disk under ~/.purple/certs/.",
450        signed, e
451    )
452}
453
454pub fn vault_config_update_failed(signed: usize, e: &impl std::fmt::Display) -> String {
455    format!(
456        "Signed {} certs but failed to update SSH config: {}",
457        signed, e
458    )
459}
460
461pub fn vault_config_write_after_sign(e: &impl std::fmt::Display) -> String {
462    format!("Failed to update config after vault signing: {}", e)
463}
464
465// ── File browser ────────────────────────────────────────────────────
466
467// ── Confirm / host key ──────────────────────────────────────────────
468
469pub fn removed_host_key(hostname: &str) -> String {
470    format!("Removed host key for {}. Reconnecting...", hostname)
471}
472
473// ── Host detail (tags) ──────────────────────────────────────────────
474
475pub fn tagged_host(alias: &str, count: usize) -> String {
476    format!(
477        "Tagged {} with {} label{}.",
478        alias,
479        count,
480        if count == 1 { "" } else { "s" }
481    )
482}
483
484// ── Config reload ───────────────────────────────────────────────────
485
486pub fn config_reloaded(count: usize) -> String {
487    format!("Config reloaded. {} hosts.", count)
488}
489
490// ── Sync background ─────────────────────────────────────────────────
491
492pub fn synced_progress(names: &str) -> String {
493    format!("Synced: {}...", names)
494}
495
496pub fn synced_done(names: &str) -> String {
497    format!("Synced: {}", names)
498}
499
500pub const SYNC_THREAD_SPAWN_FAILED: &str = "Failed to start sync thread.";
501
502pub const SYNC_UNKNOWN_PROVIDER: &str = "Unknown provider.";
503
504// ── Vault signing cancelled summary ─────────────────────────────────
505
506pub fn vault_signing_cancelled_summary(
507    signed: u32,
508    failed: u32,
509    first_error: Option<&str>,
510) -> String {
511    let mut msg = format!(
512        "Vault SSH signing cancelled ({} signed, {} failed)",
513        signed, failed
514    );
515    if let Some(err) = first_error {
516        msg.push_str(": ");
517        msg.push_str(err);
518    }
519    msg
520}
521
522// ── Region picker ───────────────────────────────────────────────────
523
524pub fn regions_selected_count(count: usize, label: &str) -> String {
525    let s = if count == 1 { "" } else { "s" };
526    format!("{} {}{} selected.", count, label, s)
527}
528
529// ── Purge stale ─────────────────────────────────────────────────────
530
531// ── Clipboard ───────────────────────────────────────────────────────
532
533pub const NO_CLIPBOARD_TOOL: &str =
534    "No clipboard tool found. Install pbcopy (macOS), wl-copy (Wayland), or xclip/xsel (X11).";
535
536// ── CLI messages ────────────────────────────────────────────────────
537
538#[path = "messages/cli.rs"]
539pub mod cli;
540
541// ── Update messages ─────────────────────────────────────────────────
542
543pub mod update {
544    pub const WHATS_NEW_HINT: &str = "Press n inside purple to see what's new.";
545    pub const DONE: &str = "done.";
546    pub const CHECKSUM_OK: &str = "ok.";
547    pub const SUDO_WARNING: &str =
548        "Running via sudo. Consider fixing directory permissions instead.";
549
550    pub fn already_on(current: &str) -> String {
551        format!("already on v{} (latest).", current)
552    }
553
554    pub fn available(latest: &str, current: &str) -> String {
555        format!("v{} available (current: v{}).", latest, current)
556    }
557
558    pub fn header(bold_name: &str) -> String {
559        format!("\n  {} updater\n", bold_name)
560    }
561
562    pub fn binary_path(path: &std::path::Path) -> String {
563        format!("  Binary: {}", path.display())
564    }
565
566    pub fn installed_at(bold_version: &str, path: &std::path::Path) -> String {
567        format!("\n  {} installed at {}.", bold_version, path.display())
568    }
569
570    pub fn whats_new_hint_indented() -> String {
571        format!("\n  {}", WHATS_NEW_HINT)
572    }
573}
574
575// ── Askpass / password prompts ───────────────────────────────────────
576
577pub mod askpass {
578    pub const BW_NOT_FOUND: &str = "Bitwarden CLI (bw) not found. SSH will prompt for password.";
579    pub const BW_NOT_LOGGED_IN: &str = "Bitwarden vault not logged in. Run 'bw login' first.";
580    pub const EMPTY_PASSWORD: &str = "Empty password. SSH will prompt for password.";
581    pub const PASSWORD_IN_KEYCHAIN: &str = "Password stored in keychain.";
582
583    pub fn read_failed(e: &impl std::fmt::Display) -> String {
584        format!("Failed to read password: {}", e)
585    }
586
587    pub fn unlock_failed_retry(e: &impl std::fmt::Display) -> String {
588        format!("Unlock failed: {}. Try again.", e)
589    }
590
591    pub fn unlock_failed_prompt(e: &impl std::fmt::Display) -> String {
592        format!("Unlock failed: {}. SSH will prompt for password.", e)
593    }
594}
595
596// ── Logging ─────────────────────────────────────────────────────────
597
598pub mod logging {
599    pub fn init_failed(e: &impl std::fmt::Display) -> String {
600        format!("[purple] Failed to initialize logger: {}", e)
601    }
602
603    pub const SSH_VERSION_FAILED: &str = "[purple] Failed to detect SSH version. Is ssh installed?";
604}
605
606// ── Form field hints / placeholders ─────────────────────────────────
607//
608// Dimmed placeholder text shown in empty form fields. Centralized here
609// so every user-visible string lives in one place and is auditable.
610
611pub mod hints {
612    // ── Shared ──────────────────────────────────────────────────────
613    // Picker hints mention "Space" because per the design system keyboard
614    // invariants, Enter always submits a form; pickers open on Space.
615    // Keep these strings in sync with scripts/check-keybindings.sh.
616    pub const IDENTITY_FILE_PICK: &str = "Space to pick a key";
617    pub const DEFAULT_SSH_USER: &str = "root";
618
619    // ── Host form ───────────────────────────────────────────────────
620    pub const HOST_ALIAS: &str = "e.g. prod or db-01";
621    pub const HOST_ALIAS_PATTERN: &str = "10.0.0.* or *.example.com";
622    pub const HOST_HOSTNAME: &str = "192.168.1.1 or example.com";
623    pub const HOST_PORT: &str = "22";
624    pub const HOST_PROXY_JUMP: &str = "Space to pick a host";
625    pub const HOST_VAULT_SSH: &str = "e.g. ssh-client-signer/sign/my-role (auth via vault login)";
626    pub const HOST_VAULT_SSH_PICKER: &str = "Space to pick a role or type one";
627    pub const HOST_VAULT_ADDR: &str =
628        "e.g. http://127.0.0.1:8200 (inherits from provider or env when empty)";
629    pub const HOST_TAGS: &str = "e.g. prod, staging, us-east (comma-separated)";
630    pub const HOST_ASKPASS_PICK: &str = "Space to pick a source";
631
632    pub fn askpass_default(default: &str) -> String {
633        format!("default: {}", default)
634    }
635
636    pub fn inherits_from(value: &str, provider: &str) -> String {
637        format!("inherits {} from {}", value, provider)
638    }
639
640    // ── Tunnel form ─────────────────────────────────────────────────
641    pub const TUNNEL_BIND_PORT: &str = "8080";
642    pub const TUNNEL_REMOTE_HOST: &str = "localhost";
643    pub const TUNNEL_REMOTE_PORT: &str = "80";
644
645    // ── Snippet form ────────────────────────────────────────────────
646    pub const SNIPPET_NAME: &str = "check-disk";
647    pub const SNIPPET_COMMAND: &str = "df -h";
648    pub const SNIPPET_OPTIONAL: &str = "(optional)";
649
650    // ── Provider form ───────────────────────────────────────────────
651    pub const PROVIDER_URL: &str = "https://pve.example.com:8006";
652    pub const PROVIDER_TOKEN_DEFAULT: &str = "your-api-token";
653    pub const PROVIDER_TOKEN_PROXMOX: &str = "user@pam!token=secret";
654    pub const PROVIDER_TOKEN_AWS: &str = "AccessKeyId:Secret (or use Profile)";
655    pub const PROVIDER_TOKEN_GCP: &str = "/path/to/service-account.json (or access token)";
656    pub const PROVIDER_TOKEN_AZURE: &str = "/path/to/service-principal.json (or access token)";
657    pub const PROVIDER_TOKEN_TAILSCALE: &str = "API key (leave empty for local CLI)";
658    pub const PROVIDER_TOKEN_ORACLE: &str = "~/.oci/config";
659    pub const PROVIDER_TOKEN_OVH: &str = "app_key:app_secret:consumer_key";
660    pub const PROVIDER_PROFILE: &str = "Name from ~/.aws/credentials (or use Token)";
661    pub const PROVIDER_PROJECT_DEFAULT: &str = "my-gcp-project-id";
662    pub const PROVIDER_PROJECT_OVH: &str = "Public Cloud project ID";
663    pub const PROVIDER_COMPARTMENT: &str = "ocid1.compartment.oc1..aaaa...";
664    pub const PROVIDER_REGIONS_DEFAULT: &str = "Space to select regions";
665    pub const PROVIDER_REGIONS_GCP: &str = "Space to select zones (empty = all)";
666    pub const PROVIDER_REGIONS_SCALEWAY: &str = "Space to select zones";
667    // Azure regions is a text input (not a picker), so no key is mentioned.
668    pub const PROVIDER_REGIONS_AZURE: &str = "comma-separated subscription IDs";
669    pub const PROVIDER_REGIONS_OVH: &str = "Space to select endpoint (default: EU)";
670    pub const PROVIDER_USER_AWS: &str = "ec2-user";
671    pub const PROVIDER_USER_GCP: &str = "ubuntu";
672    pub const PROVIDER_USER_AZURE: &str = "azureuser";
673    pub const PROVIDER_USER_ORACLE: &str = "opc";
674    pub const PROVIDER_USER_OVH: &str = "ubuntu";
675    pub const PROVIDER_VAULT_ROLE: &str =
676        "e.g. ssh-client-signer/sign/my-role (vault login; inherited)";
677    pub const PROVIDER_VAULT_ADDR: &str = "e.g. http://127.0.0.1:8200 (inherited by all hosts)";
678    pub const PROVIDER_ALIAS_PREFIX_DEFAULT: &str = "prefix";
679}
680
681#[cfg(test)]
682mod hints_tests {
683    use super::hints;
684
685    #[test]
686    fn askpass_default_formats() {
687        assert_eq!(hints::askpass_default("keychain"), "default: keychain");
688    }
689
690    #[test]
691    fn askpass_default_formats_empty() {
692        assert_eq!(hints::askpass_default(""), "default: ");
693    }
694
695    #[test]
696    fn inherits_from_formats() {
697        assert_eq!(
698            hints::inherits_from("role/x", "aws"),
699            "inherits role/x from aws"
700        );
701    }
702
703    #[test]
704    fn picker_hints_mention_space_not_enter() {
705        // Per the keyboard invariants, pickers open on Space.
706        // If these assertions fail, audit scripts/check-keybindings.sh too.
707        for s in [
708            hints::IDENTITY_FILE_PICK,
709            hints::HOST_PROXY_JUMP,
710            hints::HOST_VAULT_SSH_PICKER,
711            hints::HOST_ASKPASS_PICK,
712            hints::PROVIDER_REGIONS_DEFAULT,
713            hints::PROVIDER_REGIONS_GCP,
714            hints::PROVIDER_REGIONS_SCALEWAY,
715            hints::PROVIDER_REGIONS_OVH,
716        ] {
717            assert!(
718                s.starts_with("Space "),
719                "picker hint must mention Space: {s}"
720            );
721            assert!(!s.contains("Enter "), "picker hint must not say Enter: {s}");
722        }
723    }
724}
725
726#[path = "messages/whats_new.rs"]
727pub mod whats_new;
728
729#[path = "messages/whats_new_toast.rs"]
730pub mod whats_new_toast;