diff --git a/src/terminal/view.rs b/src/terminal/view.rs index ec72d4d3..6436f535 100644 --- a/src/terminal/view.rs +++ b/src/terminal/view.rs @@ -2943,7 +2943,10 @@ impl TerminalView { ) { log::warn!("failed to open file link {}: {reason}", path.display()); let path = path.display().to_string(); - let reason = reason.to_string(); + // The log above keeps the exact error for whoever is debugging; the + // notification gets the sentence, because "os error 13" is not an + // answer to the question the reader actually has. + let reason = crate::ui::host_ops::explain_io(reason); window.push_notification( crate::ui::i18n::t_fmt( crate::ui::i18n::L10nKey::LinkFileOpenFailed, diff --git a/src/ui/code_editor.rs b/src/ui/code_editor.rs index 086f45d8..ca5872de 100644 --- a/src/ui/code_editor.rs +++ b/src/ui/code_editor.rs @@ -494,7 +494,10 @@ impl Tty7App { Err(e) => { return Err(t_fmt( L10nKey::EditorCantOpen, - &[("path", &path.display().to_string()), ("e", &e.to_string())], + &[ + ("path", &path.display().to_string()), + ("e", &crate::ui::host_ops::explain_io(&e)), + ], )); } }; @@ -512,7 +515,10 @@ impl Tty7App { Err(e) => { return Err(t_fmt( L10nKey::EditorCantRead, - &[("path", &path.display().to_string()), ("e", &e.to_string())], + &[ + ("path", &path.display().to_string()), + ("e", &crate::ui::host_ops::explain_io(&e)), + ], )); } }; diff --git a/src/ui/file_tree.rs b/src/ui/file_tree.rs index 07dcf014..9c48315e 100644 --- a/src/ui/file_tree.rs +++ b/src/ui/file_tree.rs @@ -1051,7 +1051,7 @@ impl Tty7App { L10nKey::LinkFileOpenFailed, &[ ("path", &path.display().to_string()), - ("error", &e.to_string()), + ("error", &crate::ui::host_ops::explain_io(&e)), ], ), cx, diff --git a/src/ui/host_ops.rs b/src/ui/host_ops.rs index 4e7e9082..f106315d 100644 --- a/src/ui/host_ops.rs +++ b/src/ui/host_ops.rs @@ -190,6 +190,15 @@ impl HostOps { /// rest. `Display` on an `io::Error` answers "what happened" for a developer /// reading a log; it does not answer "what now" for the person who just lost /// a save, and "Permission denied (os error 13)" is the shape of that gap. +/// +/// This is the only way an `io::Error` should reach a person. Everything that +/// goes through [`HostOps::notify_err`] gets it for free; the handful of +/// places that build their own string have to ask. Opening a file used to +/// print `os error 13` while saving the same file explained itself, because +/// the save went through `notify_err` and the open did not. +/// +/// The raw error still belongs in the log next to the call — the two readers +/// want different things, so both are written rather than one chosen. pub fn explain_io(err: &std::io::Error) -> String { use std::io::ErrorKind; let key = match err.kind() {