Skip to main content

purple_ssh/messages/
container.rs

1//! Container-related user-facing strings: per-runtime listing errors,
2//! container library validation, exec/logs/restart/stop confirms,
3//! stack restart, host-wide bulk actions, refresh batch progress and
4//! SCP copy labels (the SCP transfer originates from the file browser
5//! overlay so it shares the file-transfer subprocess context).
6
7// ── Container library errors ────────────────────────────────────────
8//
9// Validation (`validate_container_id`) errors propagate via the
10// `ContainerActionComplete` event and become toasts. The "no runtime"
11// and "unknown sentinel" lines surface in the same path.
12
13pub const CONTAINER_ID_EMPTY: &str = "Container ID must not be empty.";
14pub const CONTAINER_RUNTIME_MISSING: &str = "No container runtime found. Install Docker or Podman.";
15
16pub fn container_id_invalid_char(c: char) -> String {
17    format!("Container ID contains invalid character: '{c}'")
18}
19
20pub fn container_unknown_sentinel(s: &str) -> String {
21    format!("Unknown sentinel: {s}")
22}
23
24pub fn container_invalid_id(reason: &str) -> String {
25    format!("Container exec blocked: {reason}")
26}
27
28/// Transient label shown on the file browser overlay while an scp transfer
29/// is running. Singular form for a single source.
30pub fn scp_copying_one(source: &str) -> String {
31    format!("Copying {}...", source)
32}
33
34/// Transient label shown on the file browser overlay while an scp transfer
35/// is running. Plural form when multiple files were selected at once.
36pub fn scp_copying_many(count: usize) -> String {
37    format!("Copying {} files...", count)
38}
39
40/// Toast shown when scp exited non-zero with no captured stderr to relay.
41/// The exit code is the only signal we have left.
42pub fn scp_failed_exit_code(code: i32) -> String {
43    format!("Copy failed (exit code {}).", code)
44}
45
46/// Toast shown when the scp subprocess itself failed to spawn or wait
47/// (e.g. binary missing, signal interrupted), distinct from a non-zero
48/// exit which uses `scp_failed_exit_code`.
49pub fn scp_spawn_failed(e: &impl std::fmt::Display) -> String {
50    format!("scp failed: {}", e)
51}
52
53// ── Containers ──────────────────────────────────────────────────────
54
55pub fn container_action_complete(action: &str) -> String {
56    format!("Container {} complete.", action)
57}
58
59pub const HOST_KEY_UNKNOWN: &str = "Host key unknown. Connect first (Enter) to trust the host.";
60pub const HOST_KEY_CHANGED: &str =
61    "Host key changed. Possible tampering or server re-install. Clear with ssh-keygen -R.";
62
63// User-friendly classifications of stderr from a remote `docker ps` /
64// `podman ps`. The raw stderr is too technical and varies across
65// distros; these phrasings give the user the actionable next step.
66pub const CONTAINER_RUNTIME_NOT_FOUND: &str = "Docker or Podman not found on remote host.";
67pub const CONTAINER_PERMISSION_DENIED: &str =
68    "Permission denied. Is your user in the docker group?";
69pub const CONTAINER_DAEMON_NOT_RUNNING: &str = "Container daemon is not running.";
70pub const CONTAINER_CONNECTION_REFUSED: &str = "Connection refused.";
71pub const CONTAINER_HOST_UNREACHABLE: &str = "Host unreachable.";
72
73/// Generic fallback when none of the container error classifiers
74/// matched. The exit code is the only signal we can show without
75/// leaking unfiltered remote stderr.
76pub fn container_command_failed(code: i32) -> String {
77    format!("Command failed with code {}.", code)
78}
79
80/// `docker inspect` returned no JSON (empty array or empty stdout).
81pub const CONTAINER_INSPECT_EMPTY: &str = "Inspect returned no data.";
82
83/// `docker inspect` stdout was not valid JSON.
84pub fn container_inspect_parse_failed(reason: &str) -> String {
85    format!("Inspect parse failed: {}", reason)
86}
87
88// ── Container exec (Enter on containers overview) ──────────────────
89
90/// User pressed Enter on a non-running container.
91pub fn container_not_running(name: &str) -> String {
92    format!("{} is not running. Cannot exec.", name)
93}
94
95/// Demo mode interactive guard.
96pub const DEMO_CONTAINER_EXEC_DISABLED: &str = "Demo mode: container exec disabled.";
97
98/// Tmux mode opened a new window for the exec session.
99pub fn container_exec_opened_in_tmux(name: &str, alias: &str) -> String {
100    format!("Opened {} on {} in tmux window.", name, alias)
101}
102
103/// Interactive shell exited cleanly.
104pub fn container_exec_ended(name: &str) -> String {
105    format!("Container shell ended: {}.", name)
106}
107
108/// Interactive shell failed with a parsed stderr reason.
109pub fn container_exec_failed_with_reason(name: &str, reason: &str) -> String {
110    format!("Container exec failed for {}: {}", name, reason)
111}
112
113/// Interactive shell exited non-zero with no stderr reason.
114pub fn container_exec_exited_with_code(name: &str, code: i32) -> String {
115    format!("Container exec for {} exited with code {}.", name, code)
116}
117
118/// `Command::new("ssh").spawn()` failed.
119pub fn container_exec_spawn_failed(name: &str) -> String {
120    format!("Failed to launch ssh for container {}.", name)
121}
122
123/// Exec prompt rejected the typed command (control chars, newline).
124pub const CONTAINER_EXEC_INVALID_COMMAND: &str =
125    "Command rejected: control characters not allowed.";
126
127// ── Container logs (l) ─────────────────────────────────────────────
128
129/// Title shown in the logs overlay border for "logs are loading".
130pub const CONTAINER_LOGS_LOADING: &str = "fetching logs…";
131
132/// Title for "logs are ready". Uses the short relative-time format
133/// (12s, 5m, 2h) so the badge stays compact regardless of staleness.
134pub fn container_logs_fetched(secs_ago: u64) -> String {
135    format!(
136        "fetched {} ago",
137        crate::containers::format_uptime_short(secs_ago)
138    )
139}
140
141/// Title for "logs fetch failed".
142pub fn container_logs_failed(reason: &str) -> String {
143    format!("logs fetch failed: {}", reason)
144}
145
146/// Search position badge for the logs overlay: `3 of 12` while the
147/// user navigates `/foo` matches with n/N.
148pub fn container_logs_search_position(current: usize, total: usize) -> String {
149    format!("{} of {}", current, total)
150}
151
152/// Search badge when the query has no hits in the current body.
153pub const CONTAINER_LOGS_SEARCH_NO_MATCHES: &str = "no matches";
154
155// ── Container restart/stop (K / S) ─────────────────────────────────
156
157/// Confirm body line that summarises a destructive action's mechanics.
158pub const CONTAINER_RESTART_BODY: &str =
159    "Sends SIGTERM, waits 10s, then SIGKILL. Live connections will drop.";
160pub const CONTAINER_STOP_BODY: &str = "Sends SIGTERM, waits 10s, then SIGKILL. Container will not restart unless its policy reschedules it.";
161
162// ── Container stack restart (Ctrl-K) ───────────────────────────────
163
164pub fn container_stack_unknown(name: &str) -> String {
165    format!("Stack unknown for {}: open the detail panel first.", name)
166}
167
168pub fn container_stack_no_running(project: &str) -> String {
169    format!("Stack {} has no running members to restart.", project)
170}
171
172pub const CONTAINER_STACK_RESTART_BODY: &str = "Restart cycles every running member one by one. Exited members are not touched. Live connections will drop.";
173
174// ── Container host-wide bulk actions (K / S on a divider) ──────────
175
176/// Body line on the bulk-restart-host confirm dialog. Same mechanics
177/// as a single restart but spelled out so the user knows it walks the
178/// host one container at a time.
179pub 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.";
180
181/// Body line on the bulk-stop-host confirm dialog.
182pub 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.";
183
184/// Footer toast when the user presses a single-target action key (l, e)
185/// while the cursor is parked on a host-divider row. Steers the user
186/// back to a container row instead of silently no-op'ing. `action` is
187/// lowercased for sentence-case readability ("logs needs..." reads
188/// better than "Logs applies...").
189pub fn container_action_needs_single(action: &str) -> String {
190    format!(
191        "{} need a single container. Place the cursor on a container row.",
192        action.to_lowercase()
193    )
194}
195
196/// Toast when bulk K/S on a divider finds no running containers.
197pub fn container_host_no_running(alias: &str) -> String {
198    format!("No running containers on {}.", alias)
199}
200
201// ── Container refresh (r / R / a) ──────────────────────────────────
202
203/// `r` keypress: single-host refresh started.
204pub fn container_refreshing(alias: &str) -> String {
205    format!("Refreshing {}…", alias)
206}
207
208/// `R` keypress while a previous batch is still in flight.
209pub const REFRESH_BATCH_ALREADY_RUNNING: &str = "Refresh already in progress.";
210
211/// `R` keypress on an empty container cache.
212pub const REFRESH_NOTHING_TO_REFRESH: &str = "No cached hosts to refresh. Press 'a' to add a host.";
213
214/// Batch progress readout shown in the status footer.
215pub fn container_refresh_progress(done: usize, total: usize) -> String {
216    format!("Refreshing {}/{} hosts…", done, total)
217}
218
219/// Batch completed.
220pub fn container_refresh_complete(total: usize) -> String {
221    format!(
222        "Refreshed {} host{}.",
223        total,
224        if total == 1 { "" } else { "s" }
225    )
226}
227
228/// Host picker: no hosts match the live query.
229pub const CONTAINER_HOST_PICKER_NO_MATCH: &str = "No hosts match.";
230
231/// Host picker: every host already has a cache entry.
232pub const CONTAINER_HOST_PICKER_NOTHING_TO_ADD: &str =
233    "All hosts already cached. Use 'r' or 'R' to refresh.";