diff --git a/src/main.rs b/src/main.rs index 85c7657..7d52d5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2411,26 +2411,24 @@ fn render_downloads_panel(frame: &mut Frame, app: &App, area: Rect) { DownloadStatus::Failed(_) => "failed", }; + // Always mark the active (front) item with ▶ like before let active_prefix = if i == 0 { "▶ " } else { " " }; - let prog = if matches!(it.status, DownloadStatus::Downloading) { - format!(" {}B", it.bytes_downloaded) + // If downloading, show "NN% (ETA)" BEFORE the title + let prog_prefix = if matches!(it.status, DownloadStatus::Downloading) { + format_download_pct_eta(it.bytes_downloaded, it.bytes_total, it.added_at) } else { String::new() }; rows.push(ListItem::new(format!( - "{active_prefix}{status}: {}{prog}", + "{active_prefix}{status}: {prog_prefix}{}", it.title ))); } - // Optional footer message (only if we still have room) - if let Some(msg) = &st.last_message { - if rows.len() < max_rows { - rows.push(ListItem::new(format!("— {msg}"))); - } - } + // NOTE: intentionally removed the footer "— {msg}" line + // because you said it’s unnecessary. } } Err(_) => rows.push(ListItem::new("(downloads lock poisoned)")), @@ -2451,10 +2449,9 @@ fn render_downloads_panel(frame: &mut Frame, app: &App, area: Rect) { .highlight_symbol(">> ") .highlight_style(Style::default().add_modifier(Modifier::REVERSED)); - // THIS is the missing piece: selection state for the list let mut state = ListState::default(); - // clamp selection so it never goes out of bounds + // Clamp selection so it never goes out of bounds let len = app .downloads .as_ref() @@ -2471,6 +2468,39 @@ fn render_downloads_panel(frame: &mut Frame, app: &App, area: Rect) { frame.render_stateful_widget(list, area, &mut state); } +// Returns e.g. "56% (4:32) " +// If total is unknown, returns "--% (--:--) " +fn format_download_pct_eta(downloaded: u64, total: Option, added_at: Instant) -> String { + // Percent + let pct_str = match total { + Some(t) if t > 0 => { + let pct = (downloaded as f64 / t as f64) * 100.0; + format!("{:0.0}%", pct.clamp(0.0, 100.0)) + } + _ => "--%".to_string(), + }; + + // ETA from average speed since added_at (good enough for UI) + let elapsed = added_at.elapsed().as_secs_f64(); + let speed = if elapsed > 0.0 { + downloaded as f64 / elapsed + } else { + 0.0 + }; + + let eta_str = match (total, speed) { + (Some(t), s) if t > downloaded && s > 1.0 => { + let remaining = (t - downloaded) as f64; + let eta_secs = remaining / s; + fmt_hms(eta_secs) + } + _ => "--:--".to_string(), + }; + + format!("{pct_str} ({eta_str}) ") +} + + fn render_mpv_panel(frame: &mut Frame, app: &App, area: Rect) { // Make the bar exactly 1 row and don't depend on bordered blocks. let [c0, c1, c2, c3, c4, c5] = Layout::vertical([