feat(tui): surface the failure reason on failed downloads

Carry a human-readable reason on DownloadStatus::Failed so it flows
through the existing status channel to the TUI. The download Info pane
now shows an Error line for a failed download (connect failure, peer
disconnect, truncated transfer, peer-reported upload failure, or the
underlying transfer error). Failures without a known reason render as
before.
This commit is contained in:
michel
2026-07-19 19:36:15 +02:00
parent 4809dd7059
commit ae28f4236a
9 changed files with 114 additions and 34 deletions
+3 -3
View File
@@ -231,7 +231,7 @@ impl MultiDownloadProgress {
DownloadStatus::InProgress { .. } => "⧗",
DownloadStatus::Paused { .. } => "⏸",
DownloadStatus::Completed => "✓",
DownloadStatus::Failed => "✗",
DownloadStatus::Failed(_) => "✗",
DownloadStatus::TimedOut => "⏱",
};
@@ -272,7 +272,7 @@ impl MultiDownloadProgress {
DownloadStatus::InProgress { .. } => warning_style(),
DownloadStatus::Paused { .. } => info_style(),
DownloadStatus::Completed => primary_style(),
DownloadStatus::Failed | DownloadStatus::TimedOut => {
DownloadStatus::Failed(_) | DownloadStatus::TimedOut => {
error_style()
}
};
@@ -346,7 +346,7 @@ pub fn render_download_stats(
.filter(|d| {
matches!(
d.download.status,
DownloadStatus::Failed | DownloadStatus::TimedOut
DownloadStatus::Failed(_) | DownloadStatus::TimedOut
)
})
.count();
+2 -2
View File
@@ -958,7 +958,7 @@ impl MainTui {
};
if !matches!(
entry.download.status,
DownloadStatus::Failed | DownloadStatus::TimedOut
DownloadStatus::Failed(_) | DownloadStatus::TimedOut
) {
return;
}
@@ -995,7 +995,7 @@ impl MainTui {
!matches!(
entry.download.status,
DownloadStatus::Completed
| DownloadStatus::Failed
| DownloadStatus::Failed(_)
| DownloadStatus::TimedOut
)
});
+63 -4
View File
@@ -72,7 +72,7 @@ fn build_info_lines(
}
DownloadStatus::Paused { .. } => ("Paused".to_string(), info_style()),
DownloadStatus::Completed => ("Completed".to_string(), success_style()),
DownloadStatus::Failed => ("Failed".to_string(), error_style()),
DownloadStatus::Failed(_) => ("Failed".to_string(), error_style()),
DownloadStatus::TimedOut => ("Timed out".to_string(), error_style()),
};
lines.push(label_value_styled("Status", status_text, status_style));
@@ -139,9 +139,20 @@ fn build_info_lines(
lines.push(Line::from(""));
push_progress_lines(&mut lines, *bytes_downloaded, *total_bytes);
}
DownloadStatus::Completed
| DownloadStatus::Failed
| DownloadStatus::TimedOut => {}
DownloadStatus::Failed(reason) => {
if let Some(reason) = reason {
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
format!("{:<LABEL_WIDTH$}", "Error"),
dimmed_style(),
)));
lines.push(Line::from(Span::styled(
reason.clone(),
error_style(),
)));
}
}
DownloadStatus::Completed | DownloadStatus::TimedOut => {}
}
lines
@@ -256,4 +267,52 @@ mod tests {
assert_eq!(basename, "track.mp3");
assert_eq!(parent, "");
}
fn download_with_status(
status: DownloadStatus,
) -> soulseek_rs::types::Download {
let (sender, _rx) = std::sync::mpsc::channel();
soulseek_rs::types::Download {
username: "peer".to_string(),
filename: "song.mp3".to_string(),
token: 1,
size: 100,
download_directory: "~/dl".to_string(),
status,
sender,
queue_position: None,
metadata: soulseek_rs::types::DownloadMetadata::default(),
}
}
fn lines_to_text(lines: &[Line<'static>]) -> String {
lines
.iter()
.map(|l| {
l.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>()
})
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn failed_download_renders_its_error_reason() {
let download = download_with_status(DownloadStatus::Failed(Some(
"Peer disconnected".to_string(),
)));
let text = lines_to_text(&build_info_lines(&download));
assert!(text.contains("Error"), "missing Error label: {text}");
assert!(text.contains("Peer disconnected"), "missing reason: {text}");
}
#[test]
fn failed_download_without_reason_shows_no_error_line() {
let download = download_with_status(DownloadStatus::Failed(None));
let text = lines_to_text(&build_info_lines(&download));
assert!(text.contains("Failed"), "status should still show Failed");
assert!(!text.contains("Error"), "unexpected Error line: {text}");
}
}
+2 -2
View File
@@ -54,7 +54,7 @@ pub fn render_downloads_pane(
DownloadStatus::InProgress { .. } => ("⧗", warning_style()),
DownloadStatus::Paused { .. } => ("⏸", info_style()),
DownloadStatus::Completed => ("✓", success_style()),
DownloadStatus::Failed => ("✗", error_style()),
DownloadStatus::Failed(_) => ("✗", error_style()),
DownloadStatus::TimedOut => ("⏱", error_style()),
};
@@ -91,7 +91,7 @@ pub fn render_downloads_pane(
)
}
DownloadStatus::Completed => "Completed".to_string(),
DownloadStatus::Failed => "Failed".to_string(),
DownloadStatus::Failed(_) => "Failed".to_string(),
DownloadStatus::TimedOut => "Timed out".to_string(),
};