From a6b16d1e5891d7d38f5a6d4d782715ce0a9273b0 Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Thu, 4 Jul 2024 15:15:01 +0200 Subject: [PATCH 01/12] POC custom headers --- Cargo.lock | 1 + rm-config/Cargo.toml | 1 + rm-config/defaults/config.toml | 12 +- rm-config/src/lib.rs | 2 + rm-config/src/main_config.rs | 55 +++-- rm-config/src/utils.rs | 4 +- rm-main/src/ui/tabs/torrents/mod.rs | 2 +- .../ui/tabs/torrents/rustmission_torrent.rs | 82 ++++++-- rm-main/src/ui/tabs/torrents/table_manager.rs | 198 ++++++++++++------ 9 files changed, 261 insertions(+), 96 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc26bd3..7f06dbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1444,6 +1444,7 @@ dependencies = [ "serde", "thiserror", "toml", + "transmission-rpc", "url", "xdg", ] diff --git a/rm-config/Cargo.toml b/rm-config/Cargo.toml index e1ab5f9..f903d13 100644 --- a/rm-config/Cargo.toml +++ b/rm-config/Cargo.toml @@ -20,3 +20,4 @@ url.workspace = true ratatui.workspace = true crossterm.workspace = true thiserror.workspace = true +transmission-rpc.workspace = true diff --git a/rm-config/defaults/config.toml b/rm-config/defaults/config.toml index 5cce726..af432f5 100644 --- a/rm-config/defaults/config.toml +++ b/rm-config/defaults/config.toml @@ -17,11 +17,15 @@ headers_hide = false [connection] url = "http://CHANGE_ME:9091/transmission/rpc" # REQUIRED! -# If you need username and password to authenticate: -# username = "CHANGE_ME" -# password = "CHANGE_ME" - # Refresh timings (in seconds) torrents_refresh = 5 stats_refresh = 10 free_space_refresh = 10 + +# If you need username and password to authenticate: +# username = "CHANGE_ME" +# password = "CHANGE_ME" + + +[torrents_tab] +headers = [ "Name", "DownloadDir" ] diff --git a/rm-config/src/lib.rs b/rm-config/src/lib.rs index a2e3a8b..4a80584 100644 --- a/rm-config/src/lib.rs +++ b/rm-config/src/lib.rs @@ -11,6 +11,7 @@ use main_config::MainConfig; pub struct Config { pub general: main_config::General, pub connection: main_config::Connection, + pub torrents_tab: main_config::TorrentsTab, pub keybindings: KeymapConfig, pub directories: Directories, } @@ -33,6 +34,7 @@ impl Config { Ok(Self { general: main_config.general, connection: main_config.connection, + torrents_tab: main_config.torrents_tab, keybindings: keybindings.clone(), directories, }) diff --git a/rm-config/src/main_config.rs b/rm-config/src/main_config.rs index 2a45893..b51bfc7 100644 --- a/rm-config/src/main_config.rs +++ b/rm-config/src/main_config.rs @@ -1,19 +1,21 @@ -use std::{path::PathBuf, sync::OnceLock}; +use std::{io::ErrorKind, path::PathBuf, sync::OnceLock}; use anyhow::Result; use ratatui::style::Color; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer}; +use transmission_rpc::types::TorrentGetField; use url::Url; -use crate::utils::{self, put_config}; +use crate::utils::{self}; -#[derive(Serialize, Deserialize)] +#[derive(Deserialize)] pub struct MainConfig { pub general: General, pub connection: Connection, + pub torrents_tab: TorrentsTab, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Deserialize)] pub struct General { #[serde(default)] pub auto_hide: bool, @@ -33,7 +35,7 @@ fn default_beginner_mode() -> bool { true } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Deserialize)] pub struct Connection { pub username: Option, pub password: Option, @@ -50,19 +52,46 @@ fn default_refresh() -> u64 { 5 } +#[derive(Deserialize)] +pub struct TorrentsTab { + #[serde(deserialize_with = "get_field_deserializer")] + pub headers: Vec, +} + +fn get_field_deserializer<'de, D: Deserializer<'de>>( + deserializer: D, +) -> Result, D::Error> { + let vals: Vec = Vec::deserialize(deserializer)?; + + let mut headers = vec![]; + + for val in vals { + match val.to_lowercase().as_str() { + "name" => headers.push(TorrentGetField::Name), + "downloaddir" => headers.push(TorrentGetField::DownloadDir), + _ => todo!(), + } + } + + Ok(headers) +} + impl MainConfig { pub(crate) const FILENAME: &'static str = "config.toml"; const DEFAULT_CONFIG: &'static str = include_str!("../defaults/config.toml"); pub(crate) fn init() -> Result { - let Ok(config) = utils::fetch_config(Self::FILENAME) else { - put_config(Self::DEFAULT_CONFIG, Self::FILENAME)?; - // TODO: check if the user really changed the config. - println!("Update {:?} and start rustmission again", Self::path()); - std::process::exit(0); + match utils::fetch_config::(Self::FILENAME) { + Ok(config) => return Ok(config), + Err(e) => match e { + utils::ConfigFetchingError::Io(e) if e.kind() == ErrorKind::NotFound => { + utils::put_config::(Self::DEFAULT_CONFIG, Self::FILENAME)?; + println!("Update {:?} and start rustmission again", Self::path()); + std::process::exit(0); + } + _ => anyhow::bail!(e), + }, }; - - Ok(config) } pub(crate) fn path() -> &'static PathBuf { diff --git a/rm-config/src/utils.rs b/rm-config/src/utils.rs index 102afde..3654cdb 100644 --- a/rm-config/src/utils.rs +++ b/rm-config/src/utils.rs @@ -42,9 +42,9 @@ pub fn fetch_config(config_name: &str) -> Result( content: &'static str, filename: &str, -) -> Result { +) -> Result { let config_path = get_config_path(filename); let mut config_file = File::create(config_path)?; config_file.write_all(content.as_bytes())?; - Ok(toml::from_str(content).expect("default configs are correct")) + Ok(toml::from_str(content)?) } diff --git a/rm-main/src/ui/tabs/torrents/mod.rs b/rm-main/src/ui/tabs/torrents/mod.rs index 167cb75..4cda236 100644 --- a/rm-main/src/ui/tabs/torrents/mod.rs +++ b/rm-main/src/ui/tabs/torrents/mod.rs @@ -121,7 +121,7 @@ impl TorrentsTab { .accent_color); let table_widget = { - let table = Table::new(torrent_rows, table_manager_lock.widths) + let table = Table::new(torrent_rows, &table_manager_lock.widths) .highlight_style(highlight_table_style); if !self.ctx.config.general.headers_hide { table.header(Row::new( diff --git a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs index d5b5e6a..4ecbfe3 100644 --- a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs +++ b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs @@ -3,7 +3,7 @@ use ratatui::{ text::{Line, Span}, widgets::Row, }; -use transmission_rpc::types::{Id, Torrent, TorrentStatus}; +use transmission_rpc::types::{Id, Torrent, TorrentGetField, TorrentStatus}; use crate::utils::{ bytes_to_human_format, download_speed_format, seconds_to_human_format, upload_speed_format, @@ -24,18 +24,69 @@ pub struct RustmissionTorrent { } impl RustmissionTorrent { - pub fn to_row(&self) -> ratatui::widgets::Row { - Row::new([ - Line::from(self.torrent_name.as_str()), - Line::from(""), - Line::from(self.size_when_done.as_str()), - Line::from(self.progress.as_str()), - Line::from(self.eta_secs.as_str()), - Line::from(download_speed_format(&self.download_speed)), - Line::from(upload_speed_format(&self.upload_speed)), - Line::from(self.download_dir.as_str()), - ]) - .style(self.style) + pub fn to_row(&self, headers: &Vec) -> ratatui::widgets::Row { + let mut cells = vec![]; + for header in headers { + match header { + TorrentGetField::ActivityDate => todo!(), + TorrentGetField::AddedDate => todo!(), + TorrentGetField::DoneDate => todo!(), + TorrentGetField::DownloadDir => cells.push(Line::from(self.download_dir.as_str())), + TorrentGetField::EditDate => todo!(), + TorrentGetField::Error => todo!(), + TorrentGetField::ErrorString => todo!(), + TorrentGetField::Eta => cells.push(Line::from(self.size_when_done.as_str())), + TorrentGetField::FileCount => todo!(), + TorrentGetField::FileStats => todo!(), + TorrentGetField::Files => todo!(), + TorrentGetField::HashString => todo!(), + TorrentGetField::Id => todo!(), + TorrentGetField::IsFinished => todo!(), + TorrentGetField::IsPrivate => todo!(), + TorrentGetField::IsStalled => todo!(), + TorrentGetField::Labels => todo!(), + TorrentGetField::LeftUntilDone => todo!(), + TorrentGetField::MetadataPercentComplete => todo!(), + TorrentGetField::Name => cells.push(Line::from(self.torrent_name.as_str())), + TorrentGetField::PeersConnected => todo!(), + TorrentGetField::PeersGettingFromUs => todo!(), + TorrentGetField::PeersSendingToUs => todo!(), + TorrentGetField::PercentDone => todo!(), + TorrentGetField::Priorities => todo!(), + TorrentGetField::QueuePosition => todo!(), + TorrentGetField::RateDownload => todo!(), + TorrentGetField::RateUpload => todo!(), + TorrentGetField::RecheckProgress => todo!(), + TorrentGetField::SecondsSeeding => todo!(), + TorrentGetField::SeedRatioLimit => todo!(), + TorrentGetField::SeedRatioMode => todo!(), + TorrentGetField::SizeWhenDone => { + cells.push(Line::from(self.size_when_done.as_str())) + } + TorrentGetField::Status => todo!(), + TorrentGetField::TorrentFile => todo!(), + TorrentGetField::TotalSize => todo!(), + TorrentGetField::Trackers => todo!(), + TorrentGetField::UploadRatio => todo!(), + TorrentGetField::UploadedEver => todo!(), + TorrentGetField::Wanted => todo!(), + TorrentGetField::WebseedsSendingToUs => todo!(), + } + } + + Row::new(cells).style(self.style) + + // Row::new([ + // Line::from(self.torrent_name.as_str()), + // Line::from(""), + // Line::from(self.size_when_done.as_str()), + // Line::from(self.progress.as_str()), + // Line::from(self.eta_secs.as_str()), + // Line::from(download_speed_format(&self.download_speed)), + // Line::from(upload_speed_format(&self.upload_speed)), + // Line::from(self.download_dir.as_str()), + // ]) + // .style(self.style) } pub fn to_row_with_higlighted_indices( @@ -116,10 +167,7 @@ impl From<&Torrent> for RustmissionTorrent { _ => Style::default(), }; - let download_dir = t - .download_dir - .clone() - .expect("torrent download directory requested"); + let download_dir = t.download_dir.clone().expect("field requested"); Self { torrent_name, diff --git a/rm-main/src/ui/tabs/torrents/table_manager.rs b/rm-main/src/ui/tabs/torrents/table_manager.rs index f340efa..fa44678 100644 --- a/rm-main/src/ui/tabs/torrents/table_manager.rs +++ b/rm-main/src/ui/tabs/torrents/table_manager.rs @@ -1,6 +1,7 @@ use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use ratatui::{prelude::*, widgets::Row}; use std::sync::{Arc, Mutex}; +use transmission_rpc::types::TorrentGetField; use crate::{app, ui::components::table::GenericTable}; @@ -9,31 +10,69 @@ use super::rustmission_torrent::RustmissionTorrent; pub struct TableManager { ctx: app::Ctx, pub table: GenericTable, - pub widths: [Constraint; 8], + pub widths: Vec, pub filter: Arc>>, pub torrents_displaying_no: u16, - header: Vec, + headers: Vec, } impl TableManager { pub fn new(ctx: app::Ctx, table: GenericTable) -> Self { - let widths = Self::default_widths(); + let widths = Self::default_widths(&ctx.config.torrents_tab.headers); + let mut headers = vec![]; + for header in &ctx.config.torrents_tab.headers { + match header { + TorrentGetField::ActivityDate => todo!(), + TorrentGetField::AddedDate => todo!(), + TorrentGetField::DoneDate => todo!(), + TorrentGetField::DownloadDir => headers.push("Directory".to_owned()), + TorrentGetField::EditDate => todo!(), + TorrentGetField::Error => todo!(), + TorrentGetField::ErrorString => todo!(), + TorrentGetField::Eta => todo!(), + TorrentGetField::FileCount => todo!(), + TorrentGetField::FileStats => todo!(), + TorrentGetField::Files => todo!(), + TorrentGetField::HashString => todo!(), + TorrentGetField::Id => todo!(), + TorrentGetField::IsFinished => todo!(), + TorrentGetField::IsPrivate => todo!(), + TorrentGetField::IsStalled => todo!(), + TorrentGetField::Labels => todo!(), + TorrentGetField::LeftUntilDone => todo!(), + TorrentGetField::MetadataPercentComplete => todo!(), + TorrentGetField::Name => headers.push("Name".to_owned()), + TorrentGetField::PeersConnected => todo!(), + TorrentGetField::PeersGettingFromUs => todo!(), + TorrentGetField::PeersSendingToUs => todo!(), + TorrentGetField::PercentDone => todo!(), + TorrentGetField::Priorities => todo!(), + TorrentGetField::QueuePosition => todo!(), + TorrentGetField::RateDownload => todo!(), + TorrentGetField::RateUpload => todo!(), + TorrentGetField::RecheckProgress => todo!(), + TorrentGetField::SecondsSeeding => todo!(), + TorrentGetField::SeedRatioLimit => todo!(), + TorrentGetField::SeedRatioMode => todo!(), + TorrentGetField::SizeWhenDone => todo!(), + TorrentGetField::Status => todo!(), + TorrentGetField::TorrentFile => todo!(), + TorrentGetField::TotalSize => todo!(), + TorrentGetField::Trackers => todo!(), + TorrentGetField::UploadRatio => todo!(), + TorrentGetField::UploadedEver => todo!(), + TorrentGetField::Wanted => todo!(), + TorrentGetField::WebseedsSendingToUs => todo!(), + } + } + Self { ctx, table, widths, filter: Arc::new(Mutex::new(None)), torrents_displaying_no: 0, - header: vec![ - "Name".to_owned(), - "".to_owned(), - "Size".to_owned(), - "Progress".to_owned(), - "ETA".to_owned(), - "Download".to_owned(), - "Upload".to_owned(), - "Directory".to_owned(), - ], + headers, } } @@ -46,13 +85,13 @@ impl TableManager { self.table .items .iter() - .map(RustmissionTorrent::to_row) + .map(|t| t.to_row(&self.ctx.config.torrents_tab.headers)) .collect() } } pub const fn header(&self) -> &Vec { - &self.header + &self.headers } pub fn current_torrent(&mut self) -> Option<&mut RustmissionTorrent> { @@ -101,54 +140,95 @@ impl TableManager { rows } - const fn default_widths() -> [Constraint; 8] { - [ - Constraint::Max(70), // Name - Constraint::Length(5), // - Constraint::Length(12), // Size - Constraint::Length(12), // Progress - Constraint::Length(12), // ETA - Constraint::Length(12), // Download - Constraint::Length(12), // Upload - Constraint::Max(70), // Download directory - ] + fn default_widths(headers: &Vec) -> Vec { + let mut constraints = vec![]; + + for header in headers { + match header { + TorrentGetField::ActivityDate => todo!(), + TorrentGetField::AddedDate => todo!(), + TorrentGetField::DoneDate => todo!(), + TorrentGetField::DownloadDir => constraints.push(Constraint::Max(70)), + TorrentGetField::EditDate => todo!(), + TorrentGetField::Error => todo!(), + TorrentGetField::ErrorString => todo!(), + TorrentGetField::Eta => todo!(), + TorrentGetField::FileCount => todo!(), + TorrentGetField::FileStats => todo!(), + TorrentGetField::Files => todo!(), + TorrentGetField::HashString => todo!(), + TorrentGetField::Id => todo!(), + TorrentGetField::IsFinished => todo!(), + TorrentGetField::IsPrivate => todo!(), + TorrentGetField::IsStalled => todo!(), + TorrentGetField::Labels => todo!(), + TorrentGetField::LeftUntilDone => todo!(), + TorrentGetField::MetadataPercentComplete => todo!(), + TorrentGetField::Name => constraints.push(Constraint::Max(70)), + TorrentGetField::PeersConnected => todo!(), + TorrentGetField::PeersGettingFromUs => todo!(), + TorrentGetField::PeersSendingToUs => todo!(), + TorrentGetField::PercentDone => todo!(), + TorrentGetField::Priorities => todo!(), + TorrentGetField::QueuePosition => todo!(), + TorrentGetField::RateDownload => todo!(), + TorrentGetField::RateUpload => todo!(), + TorrentGetField::RecheckProgress => todo!(), + TorrentGetField::SecondsSeeding => todo!(), + TorrentGetField::SeedRatioLimit => todo!(), + TorrentGetField::SeedRatioMode => todo!(), + TorrentGetField::SizeWhenDone => todo!(), + TorrentGetField::Status => todo!(), + TorrentGetField::TorrentFile => todo!(), + TorrentGetField::TotalSize => todo!(), + TorrentGetField::Trackers => todo!(), + TorrentGetField::UploadRatio => todo!(), + TorrentGetField::UploadedEver => todo!(), + TorrentGetField::Wanted => todo!(), + TorrentGetField::WebseedsSendingToUs => todo!(), + } + } + constraints } - fn header_widths(&self, rows: &[RustmissionTorrent]) -> [Constraint; 8] { + fn header_widths(&self, rows: &[RustmissionTorrent]) -> Vec { if !self.ctx.config.general.auto_hide { - return Self::default_widths(); - } - - 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 = 11; - } - if !row.upload_speed.is_empty() { - upload_width = 11; - } - if !row.progress.is_empty() { - progress_width = 11; - } - - if !row.eta_secs.is_empty() { - eta_width = 11; - } + return Self::default_widths(&self.ctx.config.torrents_tab.headers); } - [ - Constraint::Max(70), // Name - Constraint::Length(5), // - Constraint::Length(11), // Size - Constraint::Length(progress_width), // Progress - Constraint::Length(eta_width), // ETA - Constraint::Length(download_width), // Download - Constraint::Length(upload_width), // Upload - Constraint::Max(70), // Download directory - ] + let mut constraints = Self::default_widths(&self.ctx.config.torrents_tab.headers); + constraints + + // 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 = 11; + // } + // if !row.upload_speed.is_empty() { + // upload_width = 11; + // } + // if !row.progress.is_empty() { + // progress_width = 11; + // } + + // if !row.eta_secs.is_empty() { + // eta_width = 11; + // } + // } + + // [ + // Constraint::Max(70), // Name + // Constraint::Length(5), // + // Constraint::Length(11), // Size + // Constraint::Length(progress_width), // Progress + // Constraint::Length(eta_width), // ETA + // Constraint::Length(download_width), // Download + // Constraint::Length(upload_width), // Upload + // Constraint::Max(70), // Download directory + // ] } } From 526252bb1af351b179dee1fa35f165d0c196ed32 Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Thu, 4 Jul 2024 16:18:28 +0200 Subject: [PATCH 02/12] customizable headers --- rm-config/src/lib.rs | 2 +- rm-config/src/main_config.rs | 55 ++++-- rm-main/src/ui/tabs/torrents/mod.rs | 4 +- .../ui/tabs/torrents/rustmission_torrent.rs | 71 ++----- rm-main/src/ui/tabs/torrents/table_manager.rs | 176 +++++------------- 5 files changed, 103 insertions(+), 205 deletions(-) diff --git a/rm-config/src/lib.rs b/rm-config/src/lib.rs index 4a80584..59347e3 100644 --- a/rm-config/src/lib.rs +++ b/rm-config/src/lib.rs @@ -1,5 +1,5 @@ pub mod keymap; -mod main_config; +pub mod main_config; mod utils; use std::path::PathBuf; diff --git a/rm-config/src/main_config.rs b/rm-config/src/main_config.rs index b51bfc7..0e1c4ea 100644 --- a/rm-config/src/main_config.rs +++ b/rm-config/src/main_config.rs @@ -1,9 +1,8 @@ use std::{io::ErrorKind, path::PathBuf, sync::OnceLock}; use anyhow::Result; -use ratatui::style::Color; -use serde::{Deserialize, Deserializer}; -use transmission_rpc::types::TorrentGetField; +use ratatui::{layout::Constraint, style::Color}; +use serde::{Deserialize, Serialize}; use url::Url; use crate::utils::{self}; @@ -52,28 +51,46 @@ fn default_refresh() -> u64 { 5 } -#[derive(Deserialize)] -pub struct TorrentsTab { - #[serde(deserialize_with = "get_field_deserializer")] - pub headers: Vec, +#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum Header { + Name, + SizeWhenDone, + Progress, + Eta, + DownloadRate, + UploadRate, + DownloadDir, } -fn get_field_deserializer<'de, D: Deserializer<'de>>( - deserializer: D, -) -> Result, D::Error> { - let vals: Vec = Vec::deserialize(deserializer)?; - - let mut headers = vec![]; +impl Header { + pub fn default_constraint(&self) -> Constraint { + match self { + Header::Name => Constraint::Max(70), + Header::SizeWhenDone => Constraint::Length(12), + Header::Progress => Constraint::Length(12), + Header::Eta => Constraint::Length(12), + Header::DownloadRate => Constraint::Length(12), + Header::UploadRate => Constraint::Length(12), + Header::DownloadDir => Constraint::Max(70), + } + } - for val in vals { - match val.to_lowercase().as_str() { - "name" => headers.push(TorrentGetField::Name), - "downloaddir" => headers.push(TorrentGetField::DownloadDir), - _ => todo!(), + pub fn header_name(&self) -> &'static str { + match self { + Header::Name => "Name", + Header::SizeWhenDone => "Size", + Header::Progress => "Progress", + Header::Eta => "ETA", + Header::DownloadRate => "Download", + Header::UploadRate => "Upload", + Header::DownloadDir => "Directory", } } +} - Ok(headers) +#[derive(Deserialize)] +pub struct TorrentsTab { + pub headers: Vec
, } impl MainConfig { diff --git a/rm-main/src/ui/tabs/torrents/mod.rs b/rm-main/src/ui/tabs/torrents/mod.rs index 4cda236..b5d69fb 100644 --- a/rm-main/src/ui/tabs/torrents/mod.rs +++ b/rm-main/src/ui/tabs/torrents/mod.rs @@ -124,9 +124,7 @@ impl TorrentsTab { let table = Table::new(torrent_rows, &table_manager_lock.widths) .highlight_style(highlight_table_style); if !self.ctx.config.general.headers_hide { - table.header(Row::new( - table_manager_lock.header().iter().map(|s| s.as_str()), - )) + table.header(Row::new(table_manager_lock.header().iter().cloned())) } else { table } diff --git a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs index 4ecbfe3..0393891 100644 --- a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs +++ b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs @@ -3,7 +3,8 @@ use ratatui::{ text::{Line, Span}, widgets::Row, }; -use transmission_rpc::types::{Id, Torrent, TorrentGetField, TorrentStatus}; +use rm_config::main_config::Header; +use transmission_rpc::types::{Id, Torrent, TorrentStatus}; use crate::utils::{ bytes_to_human_format, download_speed_format, seconds_to_human_format, upload_speed_format, @@ -24,69 +25,25 @@ pub struct RustmissionTorrent { } impl RustmissionTorrent { - pub fn to_row(&self, headers: &Vec) -> ratatui::widgets::Row { + pub fn to_row(&self, headers: &Vec
) -> ratatui::widgets::Row { let mut cells = vec![]; for header in headers { match header { - TorrentGetField::ActivityDate => todo!(), - TorrentGetField::AddedDate => todo!(), - TorrentGetField::DoneDate => todo!(), - TorrentGetField::DownloadDir => cells.push(Line::from(self.download_dir.as_str())), - TorrentGetField::EditDate => todo!(), - TorrentGetField::Error => todo!(), - TorrentGetField::ErrorString => todo!(), - TorrentGetField::Eta => cells.push(Line::from(self.size_when_done.as_str())), - TorrentGetField::FileCount => todo!(), - TorrentGetField::FileStats => todo!(), - TorrentGetField::Files => todo!(), - TorrentGetField::HashString => todo!(), - TorrentGetField::Id => todo!(), - TorrentGetField::IsFinished => todo!(), - TorrentGetField::IsPrivate => todo!(), - TorrentGetField::IsStalled => todo!(), - TorrentGetField::Labels => todo!(), - TorrentGetField::LeftUntilDone => todo!(), - TorrentGetField::MetadataPercentComplete => todo!(), - TorrentGetField::Name => cells.push(Line::from(self.torrent_name.as_str())), - TorrentGetField::PeersConnected => todo!(), - TorrentGetField::PeersGettingFromUs => todo!(), - TorrentGetField::PeersSendingToUs => todo!(), - TorrentGetField::PercentDone => todo!(), - TorrentGetField::Priorities => todo!(), - TorrentGetField::QueuePosition => todo!(), - TorrentGetField::RateDownload => todo!(), - TorrentGetField::RateUpload => todo!(), - TorrentGetField::RecheckProgress => todo!(), - TorrentGetField::SecondsSeeding => todo!(), - TorrentGetField::SeedRatioLimit => todo!(), - TorrentGetField::SeedRatioMode => todo!(), - TorrentGetField::SizeWhenDone => { - cells.push(Line::from(self.size_when_done.as_str())) + Header::Name => cells.push(Line::from(self.torrent_name.as_str())), + Header::SizeWhenDone => cells.push(Line::from(self.size_when_done.as_str())), + Header::Progress => cells.push(Line::from(self.progress.as_str())), + Header::Eta => cells.push(Line::from(self.eta_secs.as_str())), + Header::DownloadRate => { + cells.push(Line::from(download_speed_format(&self.download_speed))) } - TorrentGetField::Status => todo!(), - TorrentGetField::TorrentFile => todo!(), - TorrentGetField::TotalSize => todo!(), - TorrentGetField::Trackers => todo!(), - TorrentGetField::UploadRatio => todo!(), - TorrentGetField::UploadedEver => todo!(), - TorrentGetField::Wanted => todo!(), - TorrentGetField::WebseedsSendingToUs => todo!(), - } + Header::UploadRate => { + cells.push(Line::from(upload_speed_format(&self.upload_speed))) + } + Header::DownloadDir => cells.push(Line::from(self.download_dir.as_str())), + }; } Row::new(cells).style(self.style) - - // Row::new([ - // Line::from(self.torrent_name.as_str()), - // Line::from(""), - // Line::from(self.size_when_done.as_str()), - // Line::from(self.progress.as_str()), - // Line::from(self.eta_secs.as_str()), - // Line::from(download_speed_format(&self.download_speed)), - // Line::from(upload_speed_format(&self.upload_speed)), - // Line::from(self.download_dir.as_str()), - // ]) - // .style(self.style) } pub fn to_row_with_higlighted_indices( diff --git a/rm-main/src/ui/tabs/torrents/table_manager.rs b/rm-main/src/ui/tabs/torrents/table_manager.rs index fa44678..07b2a73 100644 --- a/rm-main/src/ui/tabs/torrents/table_manager.rs +++ b/rm-main/src/ui/tabs/torrents/table_manager.rs @@ -1,7 +1,10 @@ use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use ratatui::{prelude::*, widgets::Row}; -use std::sync::{Arc, Mutex}; -use transmission_rpc::types::TorrentGetField; +use rm_config::main_config::Header; +use std::{ + collections::BTreeMap, + sync::{Arc, Mutex}, +}; use crate::{app, ui::components::table::GenericTable}; @@ -13,7 +16,7 @@ pub struct TableManager { pub widths: Vec, pub filter: Arc>>, pub torrents_displaying_no: u16, - headers: Vec, + headers: Vec<&'static str>, } impl TableManager { @@ -21,49 +24,7 @@ impl TableManager { let widths = Self::default_widths(&ctx.config.torrents_tab.headers); let mut headers = vec![]; for header in &ctx.config.torrents_tab.headers { - match header { - TorrentGetField::ActivityDate => todo!(), - TorrentGetField::AddedDate => todo!(), - TorrentGetField::DoneDate => todo!(), - TorrentGetField::DownloadDir => headers.push("Directory".to_owned()), - TorrentGetField::EditDate => todo!(), - TorrentGetField::Error => todo!(), - TorrentGetField::ErrorString => todo!(), - TorrentGetField::Eta => todo!(), - TorrentGetField::FileCount => todo!(), - TorrentGetField::FileStats => todo!(), - TorrentGetField::Files => todo!(), - TorrentGetField::HashString => todo!(), - TorrentGetField::Id => todo!(), - TorrentGetField::IsFinished => todo!(), - TorrentGetField::IsPrivate => todo!(), - TorrentGetField::IsStalled => todo!(), - TorrentGetField::Labels => todo!(), - TorrentGetField::LeftUntilDone => todo!(), - TorrentGetField::MetadataPercentComplete => todo!(), - TorrentGetField::Name => headers.push("Name".to_owned()), - TorrentGetField::PeersConnected => todo!(), - TorrentGetField::PeersGettingFromUs => todo!(), - TorrentGetField::PeersSendingToUs => todo!(), - TorrentGetField::PercentDone => todo!(), - TorrentGetField::Priorities => todo!(), - TorrentGetField::QueuePosition => todo!(), - TorrentGetField::RateDownload => todo!(), - TorrentGetField::RateUpload => todo!(), - TorrentGetField::RecheckProgress => todo!(), - TorrentGetField::SecondsSeeding => todo!(), - TorrentGetField::SeedRatioLimit => todo!(), - TorrentGetField::SeedRatioMode => todo!(), - TorrentGetField::SizeWhenDone => todo!(), - TorrentGetField::Status => todo!(), - TorrentGetField::TorrentFile => todo!(), - TorrentGetField::TotalSize => todo!(), - TorrentGetField::Trackers => todo!(), - TorrentGetField::UploadRatio => todo!(), - TorrentGetField::UploadedEver => todo!(), - TorrentGetField::Wanted => todo!(), - TorrentGetField::WebseedsSendingToUs => todo!(), - } + headers.push(header.header_name()); } Self { @@ -90,7 +51,7 @@ impl TableManager { } } - pub const fn header(&self) -> &Vec { + pub const fn header(&self) -> &Vec<&'static str> { &self.headers } @@ -140,95 +101,60 @@ impl TableManager { rows } - fn default_widths(headers: &Vec) -> Vec { + fn default_widths(headers: &Vec
) -> Vec { let mut constraints = vec![]; for header in headers { - match header { - TorrentGetField::ActivityDate => todo!(), - TorrentGetField::AddedDate => todo!(), - TorrentGetField::DoneDate => todo!(), - TorrentGetField::DownloadDir => constraints.push(Constraint::Max(70)), - TorrentGetField::EditDate => todo!(), - TorrentGetField::Error => todo!(), - TorrentGetField::ErrorString => todo!(), - TorrentGetField::Eta => todo!(), - TorrentGetField::FileCount => todo!(), - TorrentGetField::FileStats => todo!(), - TorrentGetField::Files => todo!(), - TorrentGetField::HashString => todo!(), - TorrentGetField::Id => todo!(), - TorrentGetField::IsFinished => todo!(), - TorrentGetField::IsPrivate => todo!(), - TorrentGetField::IsStalled => todo!(), - TorrentGetField::Labels => todo!(), - TorrentGetField::LeftUntilDone => todo!(), - TorrentGetField::MetadataPercentComplete => todo!(), - TorrentGetField::Name => constraints.push(Constraint::Max(70)), - TorrentGetField::PeersConnected => todo!(), - TorrentGetField::PeersGettingFromUs => todo!(), - TorrentGetField::PeersSendingToUs => todo!(), - TorrentGetField::PercentDone => todo!(), - TorrentGetField::Priorities => todo!(), - TorrentGetField::QueuePosition => todo!(), - TorrentGetField::RateDownload => todo!(), - TorrentGetField::RateUpload => todo!(), - TorrentGetField::RecheckProgress => todo!(), - TorrentGetField::SecondsSeeding => todo!(), - TorrentGetField::SeedRatioLimit => todo!(), - TorrentGetField::SeedRatioMode => todo!(), - TorrentGetField::SizeWhenDone => todo!(), - TorrentGetField::Status => todo!(), - TorrentGetField::TorrentFile => todo!(), - TorrentGetField::TotalSize => todo!(), - TorrentGetField::Trackers => todo!(), - TorrentGetField::UploadRatio => todo!(), - TorrentGetField::UploadedEver => todo!(), - TorrentGetField::Wanted => todo!(), - TorrentGetField::WebseedsSendingToUs => todo!(), - } + constraints.push(header.default_constraint()) } constraints } fn header_widths(&self, rows: &[RustmissionTorrent]) -> Vec { + let headers = &self.ctx.config.torrents_tab.headers; + if !self.ctx.config.general.auto_hide { - return Self::default_widths(&self.ctx.config.torrents_tab.headers); + return Self::default_widths(&headers); } - let mut constraints = Self::default_widths(&self.ctx.config.torrents_tab.headers); - constraints + let mut map = BTreeMap::new(); + + for header in headers { + map.insert(header, header.default_constraint()); + } + + let hidable_headers = [ + Header::Progress, + Header::UploadRate, + Header::DownloadRate, + Header::Eta, + ]; + + for hidable_header in &hidable_headers { + map.entry(hidable_header) + .and_modify(|c| *c = Constraint::Length(0)); + } + + for row in rows { + if !row.download_speed.is_empty() { + map.entry(&Header::DownloadRate) + .and_modify(|c| *c = Header::DownloadRate.default_constraint()); + } + if !row.upload_speed.is_empty() { + map.entry(&Header::UploadRate) + .and_modify(|c| *c = Header::UploadRate.default_constraint()); + } + if !row.progress.is_empty() { + map.entry(&Header::Progress) + .and_modify(|c| *c = Header::Progress.default_constraint()); + } + + if !row.eta_secs.is_empty() { + map.entry(&Header::Eta) + .and_modify(|c| *c = Header::Eta.default_constraint()); + } + } - // 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 = 11; - // } - // if !row.upload_speed.is_empty() { - // upload_width = 11; - // } - // if !row.progress.is_empty() { - // progress_width = 11; - // } - - // if !row.eta_secs.is_empty() { - // eta_width = 11; - // } - // } - - // [ - // Constraint::Max(70), // Name - // Constraint::Length(5), // - // Constraint::Length(11), // Size - // Constraint::Length(progress_width), // Progress - // Constraint::Length(eta_width), // ETA - // Constraint::Length(download_width), // Download - // Constraint::Length(upload_width), // Upload - // Constraint::Max(70), // Download directory - // ] + map.values().cloned().collect() } } From 68818fecbe54e1c41f4c65c5ac38b9ff4506657f Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Thu, 4 Jul 2024 16:28:57 +0200 Subject: [PATCH 03/12] set default headers --- rm-config/defaults/config.toml | 8 +++++--- rm-config/src/main_config.rs | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/rm-config/defaults/config.toml b/rm-config/defaults/config.toml index af432f5..20f9da6 100644 --- a/rm-config/defaults/config.toml +++ b/rm-config/defaults/config.toml @@ -1,4 +1,4 @@ -[general] +[General] # Whether to hide empty columns or not auto_hide = false @@ -19,7 +19,7 @@ url = "http://CHANGE_ME:9091/transmission/rpc" # REQUIRED! # Refresh timings (in seconds) torrents_refresh = 5 -stats_refresh = 10 +stats_refresh = 5 free_space_refresh = 10 # If you need username and password to authenticate: @@ -28,4 +28,6 @@ free_space_refresh = 10 [torrents_tab] -headers = [ "Name", "DownloadDir" ] +# Available fields: +# Name, SizeWhenDone, Progress, DownloadRate, UploadRate, DownloadDir +headers = ["Name", "SizeWhenDone", "Progress", "DownloadRate", "UploadRate"] diff --git a/rm-config/src/main_config.rs b/rm-config/src/main_config.rs index 0e1c4ea..c6c2475 100644 --- a/rm-config/src/main_config.rs +++ b/rm-config/src/main_config.rs @@ -11,6 +11,7 @@ use crate::utils::{self}; pub struct MainConfig { pub general: General, pub connection: Connection, + #[serde(default)] pub torrents_tab: TorrentsTab, } @@ -93,6 +94,21 @@ pub struct TorrentsTab { pub headers: Vec
, } +impl Default for TorrentsTab { + fn default() -> Self { + Self { + headers: vec![ + Header::Name, + Header::SizeWhenDone, + Header::Progress, + Header::Eta, + Header::DownloadRate, + Header::UploadRate, + ], + } + } +} + impl MainConfig { pub(crate) const FILENAME: &'static str = "config.toml"; const DEFAULT_CONFIG: &'static str = include_str!("../defaults/config.toml"); From 1e6b8b7e32307db004d02825dbdc9a33396b80bb Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Fri, 5 Jul 2024 10:21:43 +0200 Subject: [PATCH 04/12] fix minor things --- rm-config/defaults/config.toml | 2 +- .../ui/tabs/torrents/rustmission_torrent.rs | 21 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/rm-config/defaults/config.toml b/rm-config/defaults/config.toml index 20f9da6..7a991dd 100644 --- a/rm-config/defaults/config.toml +++ b/rm-config/defaults/config.toml @@ -1,4 +1,4 @@ -[General] +[general] # Whether to hide empty columns or not auto_hide = false diff --git a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs index 0393891..ec47572 100644 --- a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs +++ b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs @@ -28,19 +28,18 @@ impl RustmissionTorrent { pub fn to_row(&self, headers: &Vec
) -> ratatui::widgets::Row { let mut cells = vec![]; for header in headers { - match header { - Header::Name => cells.push(Line::from(self.torrent_name.as_str())), - Header::SizeWhenDone => cells.push(Line::from(self.size_when_done.as_str())), - Header::Progress => cells.push(Line::from(self.progress.as_str())), - Header::Eta => cells.push(Line::from(self.eta_secs.as_str())), - Header::DownloadRate => { - cells.push(Line::from(download_speed_format(&self.download_speed))) + let cell = { + match header { + Header::Name => Line::from(self.torrent_name.as_str()), + Header::SizeWhenDone => Line::from(self.size_when_done.as_str()), + Header::Progress => Line::from(self.progress.as_str()), + Header::Eta => Line::from(self.eta_secs.as_str()), + Header::DownloadRate => Line::from(download_speed_format(&self.download_speed)), + Header::UploadRate => Line::from(upload_speed_format(&self.upload_speed)), + Header::DownloadDir => Line::from(self.download_dir.as_str()), } - Header::UploadRate => { - cells.push(Line::from(upload_speed_format(&self.upload_speed))) - } - Header::DownloadDir => cells.push(Line::from(self.download_dir.as_str())), }; + cells.push(cell); } Row::new(cells).style(self.style) From 17cafecd211155b91755e6d9a18f8a46f4cd0fd8 Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Fri, 5 Jul 2024 11:13:42 +0200 Subject: [PATCH 05/12] add padding and preserve order --- rm-config/src/main_config.rs | 5 ++++- rm-main/src/ui/tabs/torrents/rustmission_torrent.rs | 1 + rm-main/src/ui/tabs/torrents/table_manager.rs | 12 +++++++++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/rm-config/src/main_config.rs b/rm-config/src/main_config.rs index c6c2475..b2e46a8 100644 --- a/rm-config/src/main_config.rs +++ b/rm-config/src/main_config.rs @@ -52,7 +52,7 @@ fn default_refresh() -> u64 { 5 } -#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Serialize, Deserialize, Hash, PartialEq, Eq)] pub enum Header { Name, SizeWhenDone, @@ -61,6 +61,7 @@ pub enum Header { DownloadRate, UploadRate, DownloadDir, + Padding, } impl Header { @@ -73,6 +74,7 @@ impl Header { Header::DownloadRate => Constraint::Length(12), Header::UploadRate => Constraint::Length(12), Header::DownloadDir => Constraint::Max(70), + Header::Padding => Constraint::Length(2), } } @@ -85,6 +87,7 @@ impl Header { Header::DownloadRate => "Download", Header::UploadRate => "Upload", Header::DownloadDir => "Directory", + Header::Padding => "", } } } diff --git a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs index ec47572..3c163cc 100644 --- a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs +++ b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs @@ -37,6 +37,7 @@ impl RustmissionTorrent { Header::DownloadRate => Line::from(download_speed_format(&self.download_speed)), Header::UploadRate => Line::from(upload_speed_format(&self.upload_speed)), Header::DownloadDir => Line::from(self.download_dir.as_str()), + Header::Padding => Line::raw(""), } }; cells.push(cell); diff --git a/rm-main/src/ui/tabs/torrents/table_manager.rs b/rm-main/src/ui/tabs/torrents/table_manager.rs index 07b2a73..eca3aee 100644 --- a/rm-main/src/ui/tabs/torrents/table_manager.rs +++ b/rm-main/src/ui/tabs/torrents/table_manager.rs @@ -2,7 +2,7 @@ use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use ratatui::{prelude::*, widgets::Row}; use rm_config::main_config::Header; use std::{ - collections::BTreeMap, + collections::HashMap, sync::{Arc, Mutex}, }; @@ -117,7 +117,7 @@ impl TableManager { return Self::default_widths(&headers); } - let mut map = BTreeMap::new(); + let mut map = HashMap::new(); for header in headers { map.insert(header, header.default_constraint()); @@ -155,6 +155,12 @@ impl TableManager { } } - map.values().cloned().collect() + let mut constraints = vec![]; + + for header in headers { + constraints.push(map.remove(header).expect("this header exists")) + } + + constraints } } From 8a31b2fd8b04ae6ce7e512681d71ce090f71bbe2 Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Fri, 5 Jul 2024 11:14:56 +0200 Subject: [PATCH 06/12] document padding --- rm-config/defaults/config.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rm-config/defaults/config.toml b/rm-config/defaults/config.toml index 7a991dd..4d5f73a 100644 --- a/rm-config/defaults/config.toml +++ b/rm-config/defaults/config.toml @@ -29,5 +29,6 @@ free_space_refresh = 10 [torrents_tab] # Available fields: -# Name, SizeWhenDone, Progress, DownloadRate, UploadRate, DownloadDir +# Name, SizeWhenDone, Progress, DownloadRate, UploadRate, DownloadDir, +# Padding headers = ["Name", "SizeWhenDone", "Progress", "DownloadRate", "UploadRate"] From cc095a16ca8057a5d5b68dc94e9468b7e5ec6bff Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Fri, 5 Jul 2024 11:42:49 +0200 Subject: [PATCH 07/12] fix headers when searching --- rm-config/src/main_config.rs | 2 +- .../ui/tabs/torrents/rustmission_torrent.rs | 56 ++++++++++--------- rm-main/src/ui/tabs/torrents/table_manager.rs | 6 +- 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/rm-config/src/main_config.rs b/rm-config/src/main_config.rs index b2e46a8..5cf5c48 100644 --- a/rm-config/src/main_config.rs +++ b/rm-config/src/main_config.rs @@ -52,7 +52,7 @@ fn default_refresh() -> u64 { 5 } -#[derive(Serialize, Deserialize, Hash, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, Copy)] pub enum Header { Name, SizeWhenDone, diff --git a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs index 3c163cc..6922481 100644 --- a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs +++ b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs @@ -25,31 +25,32 @@ pub struct RustmissionTorrent { } impl RustmissionTorrent { - pub fn to_row(&self, headers: &Vec
) -> ratatui::widgets::Row { - let mut cells = vec![]; - for header in headers { - let cell = { - match header { - Header::Name => Line::from(self.torrent_name.as_str()), - Header::SizeWhenDone => Line::from(self.size_when_done.as_str()), - Header::Progress => Line::from(self.progress.as_str()), - Header::Eta => Line::from(self.eta_secs.as_str()), - Header::DownloadRate => Line::from(download_speed_format(&self.download_speed)), - Header::UploadRate => Line::from(upload_speed_format(&self.upload_speed)), - Header::DownloadDir => Line::from(self.download_dir.as_str()), - Header::Padding => Line::raw(""), - } - }; - cells.push(cell); + fn header_to_line(&self, header: Header) -> Line { + match header { + Header::Name => Line::from(self.torrent_name.as_str()), + Header::SizeWhenDone => Line::from(self.size_when_done.as_str()), + Header::Progress => Line::from(self.progress.as_str()), + Header::Eta => Line::from(self.eta_secs.as_str()), + Header::DownloadRate => Line::from(download_speed_format(&self.download_speed)), + Header::UploadRate => Line::from(upload_speed_format(&self.upload_speed)), + Header::DownloadDir => Line::from(self.download_dir.as_str()), + Header::Padding => Line::raw(""), } + } - Row::new(cells).style(self.style) + pub fn to_row(&self, headers: &Vec
) -> ratatui::widgets::Row { + headers + .iter() + .map(|header| self.header_to_line(*header)) + .collect::() + .style(self.style) } pub fn to_row_with_higlighted_indices( &self, highlighted_indices: Vec, highlight_style: Style, + headers: &Vec
, ) -> ratatui::widgets::Row { let mut torrent_name_line = Line::default(); @@ -61,16 +62,17 @@ impl RustmissionTorrent { } } - Row::new([ - Line::from(torrent_name_line), - Line::from(""), - Line::from(self.size_when_done.as_str()), - Line::from(self.progress.as_str()), - Line::from(self.eta_secs.as_str()), - Line::from(download_speed_format(&self.download_speed)), - Line::from(upload_speed_format(&self.upload_speed)), - Line::from(self.download_dir.as_str()), - ]) + let mut cells = vec![]; + + for header in headers { + if *header == Header::Name { + cells.push(Line::from(torrent_name_line.clone())) + } else { + cells.push(self.header_to_line(*header)) + } + } + + Row::new(cells) } pub const fn status(&self) -> TorrentStatus { diff --git a/rm-main/src/ui/tabs/torrents/table_manager.rs b/rm-main/src/ui/tabs/torrents/table_manager.rs index eca3aee..48df59f 100644 --- a/rm-main/src/ui/tabs/torrents/table_manager.rs +++ b/rm-main/src/ui/tabs/torrents/table_manager.rs @@ -94,7 +94,11 @@ impl TableManager { for torrent in torrents { if let Some((_, indices)) = matcher.fuzzy_indices(&torrent.torrent_name, filter) { - rows.push(torrent.to_row_with_higlighted_indices(indices, highlight_style)) + rows.push(torrent.to_row_with_higlighted_indices( + indices, + highlight_style, + &self.ctx.config.torrents_tab.headers, + )) } } From 14770105d96c62ddb94fabd0fcf3a6e1a7e543ad Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Fri, 5 Jul 2024 12:13:44 +0200 Subject: [PATCH 08/12] add more headers --- rm-config/defaults/config.toml | 4 +- rm-config/src/main_config.rs | 9 ++++ rm-main/src/transmission/fetchers.rs | 1 + .../ui/tabs/torrents/rustmission_torrent.rs | 43 +++++++++++++------ 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/rm-config/defaults/config.toml b/rm-config/defaults/config.toml index 4d5f73a..8193274 100644 --- a/rm-config/defaults/config.toml +++ b/rm-config/defaults/config.toml @@ -29,6 +29,6 @@ free_space_refresh = 10 [torrents_tab] # Available fields: -# Name, SizeWhenDone, Progress, DownloadRate, UploadRate, DownloadDir, -# Padding +# Id, Name, SizeWhenDone, Progress, DownloadRate, UploadRate, DownloadDir, +# Padding, UploadRatio, UploadedEver headers = ["Name", "SizeWhenDone", "Progress", "DownloadRate", "UploadRate"] diff --git a/rm-config/src/main_config.rs b/rm-config/src/main_config.rs index 5cf5c48..a23728d 100644 --- a/rm-config/src/main_config.rs +++ b/rm-config/src/main_config.rs @@ -62,6 +62,9 @@ pub enum Header { UploadRate, DownloadDir, Padding, + UploadRatio, + UploadedEver, + Id, } impl Header { @@ -75,6 +78,9 @@ impl Header { Header::UploadRate => Constraint::Length(12), Header::DownloadDir => Constraint::Max(70), Header::Padding => Constraint::Length(2), + Header::UploadRatio => Constraint::Length(6), + Header::UploadedEver => Constraint::Length(12), + Header::Id => Constraint::Length(4), } } @@ -88,6 +94,9 @@ impl Header { Header::UploadRate => "Upload", Header::DownloadDir => "Directory", Header::Padding => "", + Header::UploadRatio => "Ratio", + Header::UploadedEver => "Up Ever", + Header::Id => "Id", } } } diff --git a/rm-main/src/transmission/fetchers.rs b/rm-main/src/transmission/fetchers.rs index 45fc025..6848b0d 100644 --- a/rm-main/src/transmission/fetchers.rs +++ b/rm-main/src/transmission/fetchers.rs @@ -72,6 +72,7 @@ pub async fn torrents(ctx: app::Ctx, table_manager: Arc>) { TorrentGetField::RateDownload, TorrentGetField::Status, TorrentGetField::DownloadDir, + TorrentGetField::UploadedEver, ]; let rpc_response = ctx .client diff --git a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs index 6922481..1d3e452 100644 --- a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs +++ b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs @@ -18,6 +18,8 @@ pub struct RustmissionTorrent { pub eta_secs: String, pub download_speed: String, pub upload_speed: String, + pub uploaded_ever: String, + pub upload_ratio: String, status: TorrentStatus, pub style: Style, pub id: Id, @@ -25,19 +27,6 @@ pub struct RustmissionTorrent { } impl RustmissionTorrent { - fn header_to_line(&self, header: Header) -> Line { - match header { - Header::Name => Line::from(self.torrent_name.as_str()), - Header::SizeWhenDone => Line::from(self.size_when_done.as_str()), - Header::Progress => Line::from(self.progress.as_str()), - Header::Eta => Line::from(self.eta_secs.as_str()), - Header::DownloadRate => Line::from(download_speed_format(&self.download_speed)), - Header::UploadRate => Line::from(upload_speed_format(&self.upload_speed)), - Header::DownloadDir => Line::from(self.download_dir.as_str()), - Header::Padding => Line::raw(""), - } - } - pub fn to_row(&self, headers: &Vec
) -> ratatui::widgets::Row { headers .iter() @@ -75,6 +64,25 @@ impl RustmissionTorrent { Row::new(cells) } + fn header_to_line(&self, header: Header) -> Line { + match header { + Header::Name => Line::from(self.torrent_name.as_str()), + Header::SizeWhenDone => Line::from(self.size_when_done.as_str()), + Header::Progress => Line::from(self.progress.as_str()), + Header::Eta => Line::from(self.eta_secs.as_str()), + Header::DownloadRate => Line::from(download_speed_format(&self.download_speed)), + Header::UploadRate => Line::from(upload_speed_format(&self.upload_speed)), + Header::DownloadDir => Line::from(self.download_dir.as_str()), + Header::Padding => Line::raw(""), + Header::Id => match &self.id { + Id::Id(id) => Line::from(id.to_string()), + Id::Hash(hash) => Line::from(hash.as_str()), + }, + Header::UploadRatio => Line::from(self.upload_ratio.as_str()), + Header::UploadedEver => Line::from(self.uploaded_ever.as_str()), + } + } + pub const fn status(&self) -> TorrentStatus { self.status } @@ -128,6 +136,13 @@ impl From<&Torrent> for RustmissionTorrent { let download_dir = t.download_dir.clone().expect("field requested"); + let uploaded_ever = bytes_to_human_format(t.uploaded_ever.expect("field requested")); + + let upload_ratio = { + let ratio = t.upload_ratio.expect("field requested"); + format!("{:.1}", ratio) + }; + Self { torrent_name, size_when_done, @@ -139,6 +154,8 @@ impl From<&Torrent> for RustmissionTorrent { style, id, download_dir, + uploaded_ever, + upload_ratio, } } } From b2a9269882ba382e134c3404c7a0cf3e02cc0af0 Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Fri, 5 Jul 2024 13:46:52 +0200 Subject: [PATCH 09/12] even more headers! --- Cargo.lock | 53 +++++++++++++++++++ Cargo.toml | 1 + rm-config/defaults/config.toml | 2 +- rm-config/src/main_config.rs | 9 ++++ rm-main/Cargo.toml | 1 + rm-main/src/transmission/fetchers.rs | 3 ++ .../ui/tabs/torrents/rustmission_torrent.rs | 39 +++++++++++++- 7 files changed, 105 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f06dbc..1dd394f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.14" @@ -222,7 +237,12 @@ version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", ] [[package]] @@ -839,6 +859,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1571,6 +1614,7 @@ version = "0.3.3" dependencies = [ "anyhow", "base64 0.22.1", + "chrono", "clap", "crossterm", "futures", @@ -2345,6 +2389,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 9aef960..260402c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ rss = "2" reqwest = "0.12" regex = "1" thiserror = "1" +chrono = "0.4" # Async tokio = { version = "1", features = ["macros", "sync"] } diff --git a/rm-config/defaults/config.toml b/rm-config/defaults/config.toml index 8193274..ed79737 100644 --- a/rm-config/defaults/config.toml +++ b/rm-config/defaults/config.toml @@ -30,5 +30,5 @@ free_space_refresh = 10 [torrents_tab] # Available fields: # Id, Name, SizeWhenDone, Progress, DownloadRate, UploadRate, DownloadDir, -# Padding, UploadRatio, UploadedEver +# Padding, UploadRatio, UploadedEver, AddedDate, ActivityDate, PeersConnected headers = ["Name", "SizeWhenDone", "Progress", "DownloadRate", "UploadRate"] diff --git a/rm-config/src/main_config.rs b/rm-config/src/main_config.rs index a23728d..b47edaf 100644 --- a/rm-config/src/main_config.rs +++ b/rm-config/src/main_config.rs @@ -65,6 +65,9 @@ pub enum Header { UploadRatio, UploadedEver, Id, + ActivityDate, + AddedDate, + PeersConnected, } impl Header { @@ -81,6 +84,9 @@ impl Header { Header::UploadRatio => Constraint::Length(6), Header::UploadedEver => Constraint::Length(12), Header::Id => Constraint::Length(4), + Header::ActivityDate => Constraint::Length(14), + Header::AddedDate => Constraint::Length(12), + Header::PeersConnected => Constraint::Length(6), } } @@ -97,6 +103,9 @@ impl Header { Header::UploadRatio => "Ratio", Header::UploadedEver => "Up Ever", Header::Id => "Id", + Header::ActivityDate => "Last active", + Header::AddedDate => "Added", + Header::PeersConnected => "Peers", } } } diff --git a/rm-main/Cargo.toml b/rm-main/Cargo.toml index d518e11..05d531f 100644 --- a/rm-main/Cargo.toml +++ b/rm-main/Cargo.toml @@ -34,3 +34,4 @@ rss.workspace = true reqwest.workspace = true regex.workspace = true throbber-widgets-tui.workspace = true +chrono.workspace = true diff --git a/rm-main/src/transmission/fetchers.rs b/rm-main/src/transmission/fetchers.rs index 6848b0d..4181c78 100644 --- a/rm-main/src/transmission/fetchers.rs +++ b/rm-main/src/transmission/fetchers.rs @@ -73,6 +73,9 @@ pub async fn torrents(ctx: app::Ctx, table_manager: Arc>) { TorrentGetField::Status, TorrentGetField::DownloadDir, TorrentGetField::UploadedEver, + TorrentGetField::ActivityDate, + TorrentGetField::AddedDate, + TorrentGetField::PeersConnected, ]; let rpc_response = ctx .client diff --git a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs index 1d3e452..668491a 100644 --- a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs +++ b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs @@ -1,3 +1,4 @@ +use chrono::{Datelike, NaiveDateTime}; use ratatui::{ style::{Style, Stylize}, text::{Line, Span}, @@ -24,6 +25,9 @@ pub struct RustmissionTorrent { pub style: Style, pub id: Id, pub download_dir: String, + pub activity_date: NaiveDateTime, + pub added_date: NaiveDateTime, + pub peers_connected: i64, } impl RustmissionTorrent { @@ -80,6 +84,9 @@ impl RustmissionTorrent { }, Header::UploadRatio => Line::from(self.upload_ratio.as_str()), Header::UploadedEver => Line::from(self.uploaded_ever.as_str()), + Header::ActivityDate => time_to_line(self.activity_date), + Header::AddedDate => time_to_line(self.added_date), + Header::PeersConnected => Line::from(self.peers_connected.to_string()), } } @@ -139,10 +146,26 @@ impl From<&Torrent> for RustmissionTorrent { let uploaded_ever = bytes_to_human_format(t.uploaded_ever.expect("field requested")); let upload_ratio = { - let ratio = t.upload_ratio.expect("field requested"); - format!("{:.1}", ratio) + let raw = t.upload_ratio.expect("field requested"); + format!("{:.1}", raw) }; + let activity_date = { + let raw = t.activity_date.expect("field requested"); + chrono::DateTime::from_timestamp(raw, 0) + .unwrap() + .naive_local() + }; + + let added_date = { + let raw = t.added_date.expect("field requested"); + chrono::DateTime::from_timestamp(raw, 0) + .unwrap() + .naive_local() + }; + + let peers_connected = t.peers_connected.expect("field requested"); + Self { torrent_name, size_when_done, @@ -156,6 +179,18 @@ impl From<&Torrent> for RustmissionTorrent { download_dir, uploaded_ever, upload_ratio, + activity_date, + added_date, + peers_connected, } } } + +fn time_to_line<'a>(time: NaiveDateTime) -> Line<'a> { + let today = chrono::Local::now(); + if time.year() == today.year() && time.month() == today.month() && time.day() == today.day() { + Line::from(time.format("Today %H:%M").to_string()) + } else { + Line::from(time.format("%y|%m|%d %H:%M").to_string()) + } +} From 13e2084ee5ca865ee89d43a22805fe604f217ae3 Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Sat, 6 Jul 2024 14:57:57 +0200 Subject: [PATCH 10/12] add torrents_tab table to readme --- README.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1bff434..9d80ccf 100644 --- a/README.md +++ b/README.md @@ -78,14 +78,21 @@ headers_hide = false [connection] url = "http://CHANGE_ME:9091/transmission/rpc" # REQUIRED! +# Refresh timings (in seconds) +torrents_refresh = 5 +stats_refresh = 5 +free_space_refresh = 10 + # If you need username and password to authenticate: # username = "CHANGE_ME" # password = "CHANGE_ME" -# Refresh timings (in seconds) -torrents_refresh = 5 -stats_refresh = 10 -free_space_refresh = 10 +[torrents_tab] +# Available fields: +# Id, Name, SizeWhenDone, Progress, DownloadRate, UploadRate, DownloadDir, +# Padding, UploadRatio, UploadedEver, AddedDate, ActivityDate, PeersConnected +headers = ["Name", "SizeWhenDone", "Progress", "DownloadRate", "UploadRate"] + ``` There's also a self-documenting keymap config located at `~/.config/rustmission/keymap.toml` with sane defaults. From d52f98681bb66a2de9eec2a92ff13aa4fa246a3c Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Sat, 6 Jul 2024 19:19:12 +0200 Subject: [PATCH 11/12] add small status header --- README.md | 1 + rm-config/defaults/config.toml | 1 + rm-config/src/main_config.rs | 61 ++++++++++--------- .../ui/tabs/torrents/rustmission_torrent.rs | 15 +++++ 4 files changed, 49 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 9d80ccf..b58c21e 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ free_space_refresh = 10 # Available fields: # Id, Name, SizeWhenDone, Progress, DownloadRate, UploadRate, DownloadDir, # Padding, UploadRatio, UploadedEver, AddedDate, ActivityDate, PeersConnected +# SmallStatus headers = ["Name", "SizeWhenDone", "Progress", "DownloadRate", "UploadRate"] ``` diff --git a/rm-config/defaults/config.toml b/rm-config/defaults/config.toml index ed79737..7d87ef1 100644 --- a/rm-config/defaults/config.toml +++ b/rm-config/defaults/config.toml @@ -31,4 +31,5 @@ free_space_refresh = 10 # Available fields: # Id, Name, SizeWhenDone, Progress, DownloadRate, UploadRate, DownloadDir, # Padding, UploadRatio, UploadedEver, AddedDate, ActivityDate, PeersConnected +# SmallStatus headers = ["Name", "SizeWhenDone", "Progress", "DownloadRate", "UploadRate"] diff --git a/rm-config/src/main_config.rs b/rm-config/src/main_config.rs index b47edaf..50f85fe 100644 --- a/rm-config/src/main_config.rs +++ b/rm-config/src/main_config.rs @@ -68,44 +68,47 @@ pub enum Header { ActivityDate, AddedDate, PeersConnected, + SmallStatus, } impl Header { pub fn default_constraint(&self) -> Constraint { match self { - Header::Name => Constraint::Max(70), - Header::SizeWhenDone => Constraint::Length(12), - Header::Progress => Constraint::Length(12), - Header::Eta => Constraint::Length(12), - Header::DownloadRate => Constraint::Length(12), - Header::UploadRate => Constraint::Length(12), - Header::DownloadDir => Constraint::Max(70), - Header::Padding => Constraint::Length(2), - Header::UploadRatio => Constraint::Length(6), - Header::UploadedEver => Constraint::Length(12), - Header::Id => Constraint::Length(4), - Header::ActivityDate => Constraint::Length(14), - Header::AddedDate => Constraint::Length(12), - Header::PeersConnected => Constraint::Length(6), + Self::Name => Constraint::Max(70), + Self::SizeWhenDone => Constraint::Length(12), + Self::Progress => Constraint::Length(12), + Self::Eta => Constraint::Length(12), + Self::DownloadRate => Constraint::Length(12), + Self::UploadRate => Constraint::Length(12), + Self::DownloadDir => Constraint::Max(70), + Self::Padding => Constraint::Length(2), + Self::UploadRatio => Constraint::Length(6), + Self::UploadedEver => Constraint::Length(12), + Self::Id => Constraint::Length(4), + Self::ActivityDate => Constraint::Length(14), + Self::AddedDate => Constraint::Length(12), + Self::PeersConnected => Constraint::Length(6), + Self::SmallStatus => Constraint::Length(1), } } pub fn header_name(&self) -> &'static str { - match self { - Header::Name => "Name", - Header::SizeWhenDone => "Size", - Header::Progress => "Progress", - Header::Eta => "ETA", - Header::DownloadRate => "Download", - Header::UploadRate => "Upload", - Header::DownloadDir => "Directory", - Header::Padding => "", - Header::UploadRatio => "Ratio", - Header::UploadedEver => "Up Ever", - Header::Id => "Id", - Header::ActivityDate => "Last active", - Header::AddedDate => "Added", - Header::PeersConnected => "Peers", + match *self { + Self::Name => "Name", + Self::SizeWhenDone => "Size", + Self::Progress => "Progress", + Self::Eta => "ETA", + Self::DownloadRate => "Download", + Self::UploadRate => "Upload", + Self::DownloadDir => "Directory", + Self::Padding => "", + Self::UploadRatio => "Ratio", + Self::UploadedEver => "Up Ever", + Self::Id => "Id", + Self::ActivityDate => "Last active", + Self::AddedDate => "Added", + Self::PeersConnected => "Peers", + Self::SmallStatus => "", } } } diff --git a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs index 668491a..7dd0137 100644 --- a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs +++ b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs @@ -87,6 +87,21 @@ impl RustmissionTorrent { Header::ActivityDate => time_to_line(self.activity_date), Header::AddedDate => time_to_line(self.added_date), Header::PeersConnected => Line::from(self.peers_connected.to_string()), + Header::SmallStatus => match self.status() { + TorrentStatus::Stopped => Line::from("󰏤"), + TorrentStatus::QueuedToVerify => Line::from("󱥸"), + TorrentStatus::Verifying => Line::from("󰑓"), + TorrentStatus::QueuedToDownload => Line::from("󱥸"), + TorrentStatus::QueuedToSeed => Line::from("󱥸"), + TorrentStatus::Seeding => { + if !self.upload_speed.is_empty() { + Line::from("") + } else { + Line::from("󰄬") + } + } + TorrentStatus::Downloading => Line::from(""), + }, } } From 6b681c28c48aa94eaee9553d59e65e91ff6b32f1 Mon Sep 17 00:00:00 2001 From: Remigiusz Micielski Date: Mon, 8 Jul 2024 17:08:36 +0200 Subject: [PATCH 12/12] use the same up/down symbols --- rm-main/src/ui/tabs/torrents/bottom_stats.rs | 2 +- rm-main/src/utils.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rm-main/src/ui/tabs/torrents/bottom_stats.rs b/rm-main/src/ui/tabs/torrents/bottom_stats.rs index 717c06c..eec8ab7 100644 --- a/rm-main/src/ui/tabs/torrents/bottom_stats.rs +++ b/rm-main/src/ui/tabs/torrents/bottom_stats.rs @@ -40,7 +40,7 @@ impl Component for BottomStats { let download = bytes_to_human_format(stats.download_speed); let upload = bytes_to_human_format(stats.upload_speed); - let mut text = format!("▼ {download} | ▲ {upload}"); + let mut text = format!(" {download} |  {upload}"); if let Some(free_space) = &*self.free_space.lock().unwrap() { let free_space = bytes_to_human_format(free_space.size_bytes); diff --git a/rm-main/src/utils.rs b/rm-main/src/utils.rs index 992c984..f9c2677 100644 --- a/rm-main/src/utils.rs +++ b/rm-main/src/utils.rs @@ -64,14 +64,14 @@ pub fn seconds_to_human_format(seconds: i64) -> String { pub fn download_speed_format(download_speed: &str) -> String { if download_speed.len() > 0 { - return format!("▼ {}", download_speed); + return format!(" {}", download_speed); } download_speed.to_string() } pub fn upload_speed_format(upload_speed: &str) -> String { if upload_speed.len() > 0 { - return format!("▲ {}", upload_speed); + return format!(" {}", upload_speed); } upload_speed.to_string() }