Skip to content

Commit 5bcacfe

Browse files
committed
Refactor: WriteFailure struct, shared kinds, dialog sub-components, copy threading
Four improvements that close the elegance gaps from the previous round, plus the copy-side provider-enrichment threading. WriteFailure struct (replaces MoveOpFailure tuple): - New `pub(crate) struct WriteFailure { error, volume_ctx }` in volume_copy.rs with two constructors: `from_volume(path, VolumeError)` (does the dual-use clone in one place) and `synthetic(WriteOperationError)` (no volume context). - Shared `write_error_event_from(...)` helper picks `with_friendly` or `new` based on whether the failure carries volume context. Used by both move and copy paths. - Drops the per-call-site `e.clone()` boilerplate. Copy-side threading: - `copy_volumes_with_progress` now returns `Result<(), WriteFailure>` instead of `Result<(), WriteOperationError>`. All `?` propagations updated, the two `resolve_volume_conflict` sites map their WriteOperationError through `WriteFailure::synthetic`, and the `copy_error: Option<WriteFailure>` carries the originating VolumeError + path through to the outer emit. - `copy_between_volumes` calls `write_error_event_from(...)` so cross-volume copy failures get provider-enriched FriendlyError just like move (closes the asymmetry the previous round documented). - Two pre-existing test assertions updated to pattern-match the new WriteFailure shape (sequential cancel, concurrent cancel/IoError tests). Friendly content deduplication: - New `friendly_error/kinds.rs` module holds canonical FriendlyError constructors keyed by conceptual kind: not_found, permission_denied, already_exists, cancelled, device_disconnected, read_only, storage_full, connection_timeout, not_supported, io_serious. - `volume_error.rs` shrank 184 → 49 lines, `write_error.rs` 189 → 139. Both delegate to `kinds::*` for shared variants; variants with kind-specific fields (InsufficientSpace's bytes, InvalidName's message, etc.) stay inline. - `WriteOperationError::DeviceDisconnected` overrides `retry_hint` to true after calling the kind helper (operation context — Retry button makes sense for ops, not for listings). TransferErrorDialog sub-components: - Extracted `FriendlyErrorContent.svelte` (markdown rendering + anchor click delegate to system-settings/external opener) and `FallbackErrorContent.svelte` (variant-derived plain-text path) from the dialog. - Dialog now just chooses between them based on `friendlyError` presence. Renders title + icon + container colors at the dialog level, defers body rendering to the appropriate child. - 6 new tests for FriendlyErrorContent (markdown contract, anchor routing, no-link no-crash safety net) — pins the component independently of the dialog wrapper. Tests: 1727 Svelte tests (+6 new), 1571 Rust unit, 28 integration. Full `./scripts/check.sh` green; `claude-md-reminder` clean.
1 parent 51dff4c commit 5bcacfe

11 files changed

Lines changed: 698 additions & 490 deletions

File tree

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
//! Shared `FriendlyError` constructors keyed by conceptual error kind.
2+
//!
3+
//! Both `volume_error::friendly_error_from_volume_error` (listing path) and
4+
//! `write_error::friendly_from_write_error` (copy/move/delete/trash error path)
5+
//! map several variants to the same conceptual outcome — "not found",
6+
//! "permission denied", "device disconnected", and so on. Without a single
7+
//! source of truth the user could see different titles or suggestions for the
8+
//! same situation depending on which layer the error originated in.
9+
//!
10+
//! Each function here returns the canonical `FriendlyError` for one kind. The
11+
//! caller passes the raw-detail string (formatted differently per source —
12+
//! `VolumeError::to_string()` vs `format!("{err:?}")`) and any kind-specific
13+
//! data (the path, error message, etc.).
14+
//!
15+
//! Variants that don't share semantics across the two sources (e.g.
16+
//! `WriteOperationError::SymlinkLoop`, `VolumeError::FriendlyGit`) stay inline
17+
//! in their respective mapper.
18+
19+
use super::{ErrorActionKind, ErrorCategory, FriendlyError};
20+
21+
pub(super) fn not_found(path_display: &str, raw_detail: String) -> FriendlyError {
22+
FriendlyError {
23+
category: ErrorCategory::NeedsAction,
24+
title: "Path not found".into(),
25+
explanation: format!(
26+
"Cmdr couldn't find `{}`. It may have been moved, renamed, or deleted \
27+
while Cmdr was trying to access it.",
28+
path_display
29+
),
30+
suggestion: "Here's what to try:\n\
31+
- Check that the path is spelled correctly\n\
32+
- If this is on a network drive, make sure it's connected and the share is accessible\n\
33+
- Navigate to the parent folder and look for the item there\n\
34+
- In Terminal, run `ls -la` on the parent folder to see what's there"
35+
.into(),
36+
raw_detail,
37+
retry_hint: false,
38+
action_kind: None,
39+
}
40+
}
41+
42+
pub(super) fn permission_denied(path_display: &str, raw_detail: String) -> FriendlyError {
43+
FriendlyError {
44+
category: ErrorCategory::NeedsAction,
45+
title: "No permission".into(),
46+
explanation: format!(
47+
"Cmdr doesn't have permission to access `{}`. macOS controls which apps \
48+
can access which folders, and Cmdr hasn't been granted access to this one yet.",
49+
path_display
50+
),
51+
suggestion: "Here's what to try:\n\
52+
- Open **System Settings > Privacy & Security > Files and Folders** and grant Cmdr access\n\
53+
- Check the folder's permissions in Finder: right-click the folder, choose Get Info, \
54+
and look under Sharing & Permissions\n\
55+
- If this is a shared folder, ask the owner to update permissions\n\
56+
- In Terminal, run `ls -la` on the path to see the current permissions"
57+
.into(),
58+
raw_detail,
59+
retry_hint: false,
60+
action_kind: Some(ErrorActionKind::OpenPrivacySettings),
61+
}
62+
}
63+
64+
pub(super) fn already_exists(path_display: &str, raw_detail: String) -> FriendlyError {
65+
FriendlyError {
66+
category: ErrorCategory::NeedsAction,
67+
title: "Already exists".into(),
68+
explanation: format!(
69+
"A file or folder already exists at `{}`, so Cmdr can't create a new one there.",
70+
path_display
71+
),
72+
suggestion: "Rename the existing item or choose a different name for the new one.".into(),
73+
raw_detail,
74+
retry_hint: false,
75+
action_kind: None,
76+
}
77+
}
78+
79+
pub(super) fn cancelled(raw_detail: String) -> FriendlyError {
80+
FriendlyError {
81+
category: ErrorCategory::Transient,
82+
title: "Cancelled".into(),
83+
explanation: "The operation was cancelled before it could finish.".into(),
84+
suggestion: "Navigate here again whenever you're ready to retry.".into(),
85+
raw_detail,
86+
retry_hint: true,
87+
action_kind: None,
88+
}
89+
}
90+
91+
pub(super) fn device_disconnected(path_display: &str, raw_detail: String) -> FriendlyError {
92+
FriendlyError {
93+
category: ErrorCategory::NeedsAction,
94+
title: "Device disconnected".into(),
95+
explanation: format!(
96+
"The device holding `{}` was disconnected during the operation. \
97+
This can happen if a USB cable comes loose, a phone goes to sleep, \
98+
or a network drive drops its connection.",
99+
path_display
100+
),
101+
suggestion: "Here's what to try:\n\
102+
- Reconnect the device and make sure the cable is secure\n\
103+
- If it's a phone, unlock it and make sure file transfer mode is active\n\
104+
- Navigate here again once the device is back"
105+
.into(),
106+
raw_detail,
107+
// listing path doesn't show a Retry button — the user navigates back.
108+
// Operations (write_error) override to true so the dialog gets a Retry.
109+
retry_hint: false,
110+
action_kind: None,
111+
}
112+
}
113+
114+
pub(super) fn read_only(raw_detail: String) -> FriendlyError {
115+
FriendlyError {
116+
category: ErrorCategory::NeedsAction,
117+
title: "Read-only".into(),
118+
explanation: "This volume is read-only, so Cmdr can't make changes to it. This could \
119+
be because the device has a physical write-protection switch, the disk image was \
120+
mounted as read-only, or the file system doesn't support writing."
121+
.into(),
122+
suggestion: "Here's what to try:\n\
123+
- If the device has a physical write-protection switch (common on SD cards), flip it off\n\
124+
- If this is a disk image, remount it with write access\n\
125+
- Otherwise, copy the files to a writable location first"
126+
.into(),
127+
raw_detail,
128+
retry_hint: false,
129+
action_kind: None,
130+
}
131+
}
132+
133+
pub(super) fn storage_full(raw_detail: String) -> FriendlyError {
134+
FriendlyError {
135+
category: ErrorCategory::NeedsAction,
136+
title: "Disk is full".into(),
137+
explanation: "There isn't enough free space on this volume to complete the operation.".into(),
138+
suggestion: "Here's what to try:\n\
139+
- Free up space by moving or deleting files you no longer need\n\
140+
- Empty the Trash (right-click the Trash icon in the Dock)\n\
141+
- In Terminal, run `df -h` to see how much space is left on each volume"
142+
.into(),
143+
raw_detail,
144+
retry_hint: false,
145+
action_kind: None,
146+
}
147+
}
148+
149+
pub(super) fn connection_timeout(raw_detail: String) -> FriendlyError {
150+
FriendlyError {
151+
category: ErrorCategory::Transient,
152+
title: "Connection timed out".into(),
153+
explanation: "Cmdr tried to access this resource but the connection didn't respond in time. \
154+
This usually means the server or device is slow to respond, or the network \
155+
connection is unstable."
156+
.into(),
157+
suggestion: "Here's what to try:\n\
158+
- Check that the device or server is powered on and reachable\n\
159+
- Check your Wi-Fi or Ethernet connection\n\
160+
- In Terminal, try `ping <hostname>` to test if the server is reachable\n\
161+
- Try again"
162+
.into(),
163+
raw_detail,
164+
retry_hint: true,
165+
action_kind: None,
166+
}
167+
}
168+
169+
pub(super) fn not_supported(raw_detail: String) -> FriendlyError {
170+
FriendlyError {
171+
category: ErrorCategory::NeedsAction,
172+
title: "Not supported".into(),
173+
explanation: "This operation isn't supported on this type of volume. Some volumes \
174+
(like phone storage or certain network drives) don't support all operations."
175+
.into(),
176+
suggestion: "Try a different approach, or use Finder for this operation.".into(),
177+
raw_detail,
178+
retry_hint: false,
179+
action_kind: None,
180+
}
181+
}
182+
183+
pub(super) fn io_serious(path_display: &str, message: &str, raw_detail: String) -> FriendlyError {
184+
FriendlyError {
185+
category: ErrorCategory::Serious,
186+
title: "Couldn't read this folder".into(),
187+
explanation: format!(
188+
"Cmdr ran into a problem with `{}`: {}. This could be a temporary glitch \
189+
or a sign that the disk or device needs attention.",
190+
path_display, message
191+
),
192+
suggestion: "Here's what to try:\n\
193+
- Check that the disk or device is still connected\n\
194+
- Try again\n\
195+
- If this keeps happening, try running **Disk Utility > First Aid** on this volume"
196+
.into(),
197+
raw_detail,
198+
retry_hint: true,
199+
action_kind: None,
200+
}
201+
}

apps/desktop/src-tauri/src/file_system/volume/friendly_error/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
1616
mod empty_root;
1717
mod errno;
18+
mod kinds;
1819
mod volume_error;
1920
mod write_error;
2021

0 commit comments

Comments
 (0)