From 6f2c119c4d1d65d7a9d2cb47c10f36d650e70b6e Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Thu, 11 Apr 2024 19:23:16 +0200 Subject: [PATCH] auto hide empty columns --- Cargo.lock | 6 +- rm-main/Cargo.toml | 2 +- rm-main/src/transmission.rs | 42 +++-- rm-main/src/ui/mod.rs | 2 +- rm-main/src/ui/tabs/torrents.rs | 220 ----------------------- rm-main/src/ui/tabs/torrents/torrents.rs | 166 ++++++++++++----- 6 files changed, 150 insertions(+), 288 deletions(-) delete mode 100644 rm-main/src/ui/tabs/torrents.rs diff --git a/Cargo.lock b/Cargo.lock index 62b8291..acddc1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -820,9 +820,9 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.26.1" +version = "0.27.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcb12f8fbf6c62614b0d56eb352af54f6a22410c3b079eb53ee93c7b97dd31d8" +checksum = "35bffb29ab0d1aada530abae601d2a1ce13a74d301d495570dd55328c132b99b" dependencies = [ "bitflags 2.5.0", "cassowary", @@ -1050,7 +1050,7 @@ dependencies = [ "crossterm", "futures", "magnetease", - "ratatui 0.26.1", + "ratatui 0.27.0-alpha.4", "ratatui-macros", "rm-config", "serde", diff --git a/rm-main/Cargo.toml b/rm-main/Cargo.toml index cbf1f31..0c9a369 100644 --- a/rm-main/Cargo.toml +++ b/rm-main/Cargo.toml @@ -22,6 +22,6 @@ futures = "0.3" # TUI crossterm = { version = "0.27", features = ["event-stream"] } -ratatui = "0.26" +ratatui = "0.27.0-alpha.4" ratatui-macros = "0.2" tui-input = "0.8" diff --git a/rm-main/src/transmission.rs b/rm-main/src/transmission.rs index 58432c5..69ff5bc 100644 --- a/rm-main/src/transmission.rs +++ b/rm-main/src/transmission.rs @@ -14,7 +14,7 @@ use transmission_rpc::types::TorrentAction as RPCAction; use crate::{ action::{Action, TorrentAction}, app, - ui::{bytes_to_human_format, popup::ErrorPopup}, + ui::{bytes_to_human_format, popup::ErrorPopup, tabs::torrents::torrents::TableManager}, }; pub async fn stats_fetch(ctx: app::Ctx, stats: Arc>>) { @@ -36,7 +36,7 @@ pub async fn stats_fetch(ctx: app::Ctx, stats: Arc>>, - rows: Arc>>, + table_manager: Arc>, ) { loop { let fields = vec![ @@ -61,10 +61,12 @@ pub async fn torrent_fetch( .unwrap(); let new_torrents = rpc_response.arguments.torrents; - *rows.lock().unwrap() = new_torrents - .iter() - .map(|torrent| torrent_to_row(torrent)) - .collect(); + table_manager.lock().unwrap().set_new_rows( + new_torrents + .iter() + .map(|torrent| torrent_to_row(torrent)) + .collect(), + ); *torrents.lock().unwrap() = new_torrents; ctx.send_action(Action::Render); @@ -73,22 +75,18 @@ pub async fn torrent_fetch( } pub struct RustmissionTorrent { - torrent_name: String, - size_when_done: String, - progress: String, - eta_secs: String, - download_speed: String, - upload_speed: String, - status: TorrentStatus, + pub torrent_name: String, + pub size_when_done: String, + pub progress: String, + pub eta_secs: String, + pub download_speed: String, + pub upload_speed: String, + pub status: TorrentStatus, + pub style: Style, } impl RustmissionTorrent { pub fn to_row(&self) -> ratatui::widgets::Row { - let style = match self.status { - TorrentStatus::Stopped => Style::default().dark_gray().italic(), - _ => Style::default(), - }; - Row::new([ self.torrent_name.as_str(), self.size_when_done.as_str(), @@ -97,7 +95,7 @@ impl RustmissionTorrent { self.download_speed.as_str(), self.upload_speed.as_str(), ]) - .style(style) + .style(self.style) } } @@ -129,6 +127,11 @@ fn torrent_to_row(t: &Torrent) -> RustmissionTorrent { let status = t.status.expect("field requested"); + let style = match status { + TorrentStatus::Stopped => Style::default().dark_gray().italic(), + _ => Style::default(), + }; + RustmissionTorrent { torrent_name, size_when_done, @@ -137,6 +140,7 @@ fn torrent_to_row(t: &Torrent) -> RustmissionTorrent { download_speed, upload_speed, status, + style, } } diff --git a/rm-main/src/ui/mod.rs b/rm-main/src/ui/mod.rs index 6e43f5c..56a5368 100644 --- a/rm-main/src/ui/mod.rs +++ b/rm-main/src/ui/mod.rs @@ -2,7 +2,7 @@ pub mod components; pub mod popup; pub mod tabs; -use tabs::torrents::TorrentsTab; +use crate::ui::tabs::torrents::torrents::TorrentsTab; use crossterm::event::KeyCode; use ratatui::prelude::*; diff --git a/rm-main/src/ui/tabs/torrents.rs b/rm-main/src/ui/tabs/torrents.rs deleted file mode 100644 index 186169b..0000000 --- a/rm-main/src/ui/tabs/torrents.rs +++ /dev/null @@ -1,220 +0,0 @@ -mod task_manager; -mod tasks; - -use std::sync::{Arc, Mutex}; - -use ratatui::prelude::*; -use ratatui::widgets::{Block, BorderType, Clear, Paragraph, Row, Table}; -use ratatui_macros::constraints; -use transmission_rpc::types::{SessionStats, Torrent, TorrentStatus}; - -use crate::action::{Action, TorrentAction}; -use crate::transmission::RustmissionTorrent; -use crate::ui::components::table::GenericTable; -use crate::ui::components::Component; -use crate::ui::{bytes_to_human_format, centered_rect}; -use crate::{app, transmission}; - -use self::task_manager::TaskManager; - -#[derive(Default)] -struct StatsComponent { - // TODO: get rid of the Option - stats: Arc>>, -} - -impl Component for StatsComponent { - fn render(&mut self, f: &mut Frame, rect: Rect) { - if let Some(stats) = &*self.stats.lock().unwrap() { - let upload = bytes_to_human_format(stats.upload_speed); - let download = bytes_to_human_format(stats.download_speed); - let all = stats.torrent_count; - let text = format!("All: {all} | ▲ {download} | ⯆ {upload}"); - let paragraph = Paragraph::new(text).alignment(Alignment::Right); - f.render_widget(paragraph, rect); - } - } -} - -struct StatisticsPopup { - stats: SessionStats, -} - -impl StatisticsPopup { - fn new(stats: SessionStats) -> Self { - Self { stats } - } -} - -impl Component for StatisticsPopup { - fn handle_actions(&mut self, action: Action) -> Option { - if let Action::Confirm = action { - return Some(Action::Quit); - } - None - } - - fn render(&mut self, f: &mut Frame, rect: Rect) { - let popup_rect = centered_rect(rect, 50, 50); - let block_rect = popup_rect.inner(&Margin::new(1, 1)); - let text_rect = block_rect.inner(&Margin::new(3, 2)); - let button_rect = { Layout::vertical(constraints![==100%, ==1]).split(text_rect)[1] }; - - let block = Block::bordered() - .border_type(BorderType::Rounded) - .title(" Statistics ") - .title_style(Style::default().light_magenta()); - - let button = Paragraph::new("[ OK ]").bold().right_aligned(); - - let uploaded_bytes = self.stats.cumulative_stats.uploaded_bytes; - let downloaded_bytes = self.stats.cumulative_stats.downloaded_bytes; - let uploaded = bytes_to_human_format(uploaded_bytes); - let downloaded = bytes_to_human_format(downloaded_bytes); - let ratio = uploaded_bytes / downloaded_bytes; - let text = format!("Uploaded: {uploaded}\nDownloaded: {downloaded}\nRatio: {ratio}"); - let paragraph = Paragraph::new(text); - - f.render_widget(Clear, popup_rect); - f.render_widget(block, block_rect); - f.render_widget(paragraph, text_rect); - f.render_widget(button, button_rect); - } -} - -pub struct TorrentsTab { - table: Arc>>, - rows: Arc>>, - stats: StatsComponent, - task: TaskManager, - statistics_popup: Option, - ctx: app::Ctx, -} - -impl TorrentsTab { - pub fn new(ctx: app::Ctx) -> Self { - let stats = StatsComponent::default(); - let table = Arc::new(Mutex::new(GenericTable::new(vec![]))); - let rows = Arc::new(Mutex::new(vec![])); - - tokio::spawn(transmission::stats_fetch( - ctx.clone(), - Arc::clone(&stats.stats), - )); - - tokio::spawn(transmission::torrent_fetch( - ctx.clone(), - Arc::clone(&table.lock().unwrap().items), - Arc::clone(&rows), - )); - - Self { - table: Arc::clone(&table), - rows, - stats, - task: TaskManager::new(Arc::clone(&table), ctx.clone()), - statistics_popup: None, - ctx, - } - } -} - -impl Component for TorrentsTab { - fn render(&mut self, f: &mut Frame, rect: Rect) { - let [torrents_list_rect, stats_rect] = - Layout::vertical(constraints![>=10, ==1]).areas(rect); - - let header = Row::new(vec![ - "Name", "Size", "Progress", "ETA", "Download", "Upload", - ]); - - let header_widths = [ - Constraint::Length(60), // Name - Constraint::Length(10), // Size - Constraint::Length(10), // Progress - Constraint::Length(10), // ETA - Constraint::Length(10), // Download - Constraint::Length(10), // Upload - ]; - - let rows = self.rows.lock().unwrap(); - - let torrent_rows = rows - .iter() - .map(crate::transmission::RustmissionTorrent::to_row); - - let torrents_table = Table::new(torrent_rows, header_widths) - .header(header) - .highlight_style(Style::default().light_magenta().on_black().bold()); - - f.render_stateful_widget( - torrents_table, - torrents_list_rect, - &mut self.table.lock().unwrap().state.borrow_mut(), - ); - - self.stats.render(f, stats_rect); - - self.task.render(f, stats_rect); - - if let Some(popup) = &mut self.statistics_popup { - popup.render(f, f.size()); - } - } - - #[must_use] - fn handle_actions(&mut self, action: Action) -> Option { - use Action as A; - if let Some(popup) = &mut self.statistics_popup { - if let Some(Action::Quit) = popup.handle_actions(action) { - self.statistics_popup = None; - return Some(Action::Render); - }; - return None; - } - - match action { - A::Up => { - self.table.lock().unwrap().previous(); - Some(Action::Render) - } - A::Down => { - self.table.lock().unwrap().next(); - Some(Action::Render) - } - A::ShowStats => { - if let Some(stats) = &*self.stats.stats.lock().unwrap() { - self.statistics_popup = Some(StatisticsPopup::new(stats.clone())); - Some(Action::Render) - } else { - None - } - } - A::Pause => { - if let Some(torrent) = self.table.lock().unwrap().current_item() { - let torrent_id = torrent.id().unwrap(); - let torrent_status = torrent.status.unwrap(); - - match torrent_status { - TorrentStatus::Stopped => { - self.ctx - .send_torrent_action(TorrentAction::TorrentStart(Box::new(vec![ - torrent_id, - ]))); - } - _ => { - self.ctx - .send_torrent_action(TorrentAction::TorrentStop(Box::new(vec![ - torrent_id, - ]))); - } - } - return None; - } - None - } - - other => self.task.handle_actions(other), - } - } -} diff --git a/rm-main/src/ui/tabs/torrents/torrents.rs b/rm-main/src/ui/tabs/torrents/torrents.rs index 797c0f3..89fe6fe 100644 --- a/rm-main/src/ui/tabs/torrents/torrents.rs +++ b/rm-main/src/ui/tabs/torrents/torrents.rs @@ -1,3 +1,4 @@ +use std::cell::Cell; use std::sync::{Arc, Mutex}; use crate::ui::tabs::torrents::popups::stats::StatisticsPopup; @@ -16,39 +17,77 @@ use crate::{app, transmission}; use super::task_manager::TaskManager; -#[derive(Default)] -struct StatsComponent { - // TODO: get rid of the Option - stats: Arc>>, -} - -impl Component for StatsComponent { - fn render(&mut self, f: &mut Frame, rect: Rect) { - if let Some(stats) = &*self.stats.lock().unwrap() { - let upload = bytes_to_human_format(stats.upload_speed); - let download = bytes_to_human_format(stats.download_speed); - let all = stats.torrent_count; - let text = format!("All: {all} | ▲ {download} | ⯆ {upload}"); - let paragraph = Paragraph::new(text).alignment(Alignment::Right); - f.render_widget(paragraph, rect); - } - } -} - pub struct TorrentsTab { - table: Arc>>, - rows: Arc>>, + table_manager: Arc>, stats: StatsComponent, task: TaskManager, statistics_popup: Option, ctx: app::Ctx, + header: Vec, +} + +pub struct TableManager { + table: Arc>>, + rows: Vec, + widths: [Constraint; 6], +} + +impl TableManager { + fn new(table: Arc>>, rows: Vec) -> Self { + let widths = Self::header_widths(&rows); + TableManager { + rows, + table, + widths, + } + } + + pub fn set_new_rows(&mut self, rows: Vec) { + self.rows = rows; + self.widths = Self::header_widths(&self.rows); + } + + // TODO: benchmark this! + fn header_widths(rows: &[RustmissionTorrent]) -> [Constraint; 6] { + let mut download_width = 0; + let mut upload_width = 0; + let mut progress_width = 0; + let mut eta_width = 0; + + for row in rows { + if !row.download_speed.is_empty() { + download_width = 10; + } + if !row.upload_speed.is_empty() { + upload_width = 10; + } + if !row.progress.is_empty() { + progress_width = 10; + } + + if !row.eta_secs.is_empty() { + eta_width = 10; + } + } + + [ + Constraint::Max(65), // Name + Constraint::Length(10), // Size + Constraint::Length(progress_width), // Progress + Constraint::Length(eta_width), // ETA + Constraint::Length(download_width), // Download + Constraint::Length(upload_width), // Upload + ] + } } impl TorrentsTab { pub fn new(ctx: app::Ctx) -> Self { let stats = StatsComponent::default(); let table = Arc::new(Mutex::new(GenericTable::new(vec![]))); - let rows = Arc::new(Mutex::new(vec![])); + let rows = vec![]; + + let table_manager = Arc::new(Mutex::new(TableManager::new(Arc::clone(&table), rows))); tokio::spawn(transmission::stats_fetch( ctx.clone(), @@ -58,18 +97,29 @@ impl TorrentsTab { tokio::spawn(transmission::torrent_fetch( ctx.clone(), Arc::clone(&table.lock().unwrap().items), - Arc::clone(&rows), + Arc::clone(&table_manager), )); Self { - table: Arc::clone(&table), - rows, + table_manager, stats, task: TaskManager::new(Arc::clone(&table), ctx.clone()), statistics_popup: None, ctx, + header: vec![ + "Name".to_owned(), + "Size".to_owned(), + "Progress".to_owned(), + "ETA".to_owned(), + "Download".to_owned(), + "Upload".to_owned(), + ], } } + + fn header(&self) -> &Vec { + &self.header + } } impl Component for TorrentsTab { @@ -77,33 +127,22 @@ impl Component for TorrentsTab { let [torrents_list_rect, stats_rect] = Layout::vertical(constraints![>=10, ==1]).areas(rect); - let header = Row::new(vec![ - "Name", "Size", "Progress", "ETA", "Download", "Upload", - ]); + let table_manager = &self.table_manager.lock().unwrap(); - let header_widths = [ - Constraint::Length(60), // Name - Constraint::Length(10), // Size - Constraint::Length(10), // Progress - Constraint::Length(10), // ETA - Constraint::Length(10), // Download - Constraint::Length(10), // Upload - ]; - - let rows = self.rows.lock().unwrap(); + let rows = &table_manager.rows; let torrent_rows = rows .iter() .map(crate::transmission::RustmissionTorrent::to_row); - let torrents_table = Table::new(torrent_rows, header_widths) - .header(header) + let torrents_table = Table::new(torrent_rows, table_manager.widths) + .header(Row::new(self.header().iter().map(|s| s.as_str()))) .highlight_style(Style::default().light_magenta().on_black().bold()); f.render_stateful_widget( torrents_table, torrents_list_rect, - &mut self.table.lock().unwrap().state.borrow_mut(), + &mut table_manager.table.lock().unwrap().state.borrow_mut(), ); self.stats.render(f, stats_rect); @@ -128,11 +167,23 @@ impl Component for TorrentsTab { match action { A::Up => { - self.table.lock().unwrap().previous(); + self.table_manager + .lock() + .unwrap() + .table + .lock() + .unwrap() + .previous(); Some(Action::Render) } A::Down => { - self.table.lock().unwrap().next(); + self.table_manager + .lock() + .unwrap() + .table + .lock() + .unwrap() + .next(); Some(Action::Render) } A::ShowStats => { @@ -144,7 +195,15 @@ impl Component for TorrentsTab { } } A::Pause => { - if let Some(torrent) = self.table.lock().unwrap().current_item() { + if let Some(torrent) = self + .table_manager + .lock() + .unwrap() + .table + .lock() + .unwrap() + .current_item() + { let torrent_id = torrent.id().unwrap(); let torrent_status = torrent.status.unwrap(); @@ -171,3 +230,22 @@ impl Component for TorrentsTab { } } } + +#[derive(Default)] +struct StatsComponent { + // TODO: get rid of the Option + stats: Arc>>, +} + +impl Component for StatsComponent { + fn render(&mut self, f: &mut Frame, rect: Rect) { + if let Some(stats) = &*self.stats.lock().unwrap() { + let upload = bytes_to_human_format(stats.upload_speed); + let download = bytes_to_human_format(stats.download_speed); + let all = stats.torrent_count; + let text = format!("All: {all} | ▲ {download} | ⯆ {upload}"); + let paragraph = Paragraph::new(text).alignment(Alignment::Right); + f.render_widget(paragraph, rect); + } + } +}