diff --git a/Cargo.lock b/Cargo.lock index 5822eb7..b4665b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1628,6 +1628,7 @@ dependencies = [ "anyhow", "crossterm 0.29.0", "ratatui", + "thiserror", "tokio", "tracing", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index 720aeaa..6b610d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,4 @@ anyhow = "1.0" tokio = { version = "1", features = ["full"] } tracing = "0.1" tracing-subscriber = "0.3" +thiserror = "2.0" diff --git a/src/app/command.rs b/src/app/command.rs index 9a408cd..a7573dc 100644 --- a/src/app/command.rs +++ b/src/app/command.rs @@ -1,9 +1,6 @@ -use std::{collections::HashSet, path::Path}; - - -use transmission_rpc::types::{Torrent, TorrentAction, TorrentStatus}; - use super::{types::Selected, Torrents}; +use std::{collections::HashSet, path::Path}; +use transmission_rpc::types::{Torrent, TorrentAction, TorrentStatus}; impl Torrents { pub async fn toggle(&mut self, ids: Selected) -> anyhow::Result<()> { diff --git a/src/app/mod.rs b/src/app/mod.rs index 7ba8cfa..8126882 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,17 +1,15 @@ -mod tab; -mod torrent; -pub mod utils; - -use ratatui::widgets::TableState; pub mod action; mod command; +mod tab; +mod torrent; pub mod types; +pub mod utils; +pub use {tab::Tab, torrent::Torrents}; -use self::types::Selected; -pub use self::{tab::Tab, torrent::Torrents}; +use ratatui::widgets::TableState; +use types::Selected; /// Main Application. -/// TODO: write description #[derive(Debug)] pub struct App<'a> { pub running: bool, @@ -149,7 +147,10 @@ impl<'a> App<'a> { fn selected(&self, highlighted: bool) -> Selected { let torrents = &self.torrents.torrents; if self.torrents.selected.is_empty() || highlighted { - let selected_id = self.state.selected().and_then(|idx| torrents.get(idx).and_then(|torrent| torrent.id)); + let selected_id = self + .state + .selected() + .and_then(|idx| torrents.get(idx).and_then(|torrent| torrent.id)); if let Some(id) = selected_id { return Selected::Current(id); } diff --git a/src/app/tab.rs b/src/app/tab.rs index 835f1d7..b16d1e6 100644 --- a/src/app/tab.rs +++ b/src/app/tab.rs @@ -1,7 +1,6 @@ use transmission_rpc::types::TorrentGetField; /// Available tabs. -/// TODO: write description #[derive(Debug, Default)] pub enum Tab { #[default] diff --git a/src/app/torrent.rs b/src/app/torrent.rs index aa928eb..1f1a910 100644 --- a/src/app/torrent.rs +++ b/src/app/torrent.rs @@ -1,11 +1,9 @@ -use std::{collections::HashSet, fmt::Debug}; - use anyhow::Result; +use std::{collections::HashSet, fmt::Debug}; use transmission_rpc::{ types::{Torrent, TorrentGetField}, TransClient, }; - use url::Url; /// List of torrents. diff --git a/src/app/types.rs b/src/app/types.rs index ec6038a..082f07c 100644 --- a/src/app/types.rs +++ b/src/app/types.rs @@ -1,5 +1,4 @@ use std::collections::HashSet; - use transmission_rpc::types::Id; #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/src/app/utils/filesize.rs b/src/app/utils/filesize.rs index 98fb7ce..3ff11d1 100644 --- a/src/app/utils/filesize.rs +++ b/src/app/utils/filesize.rs @@ -1,24 +1,109 @@ -use std::fmt; +use std::fmt::Display; +use thiserror::Error; -pub struct FileSize(i64); +#[derive(Error, Debug, PartialEq)] +pub enum FileSizeError { + #[error("File size cannot be negative: {value}")] + NegativeSize { value: i64 }, + #[error("File size value is too large: {value}")] + ValueTooLarge { value: f64 }, + #[error("File size value is invalid: {reason}")] + InvalidValue { reason: String }, +} -impl From for FileSize { - fn from(bytes: i64) -> Self { - FileSize(bytes) +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default)] +pub struct FileSize(u64); + +impl FileSize { + pub const fn new(bytes: u64) -> Self { + Self(bytes) + } + + pub const fn bytes(&self) -> u64 { + self.0 + } + + pub const fn kilobytes(kb: u64) -> Self { + Self(kb * 1024) + } + + pub const fn megabytes(mb: u64) -> Self { + Self(mb * 1024 * 1024) + } + + pub const fn gigabytes(gb: u64) -> Self { + Self(gb * 1024 * 1024 * 1024) } } -impl fmt::Display for FileSize { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let bytes = self.0; - if bytes < 1024 { - write!(f, "{} B", bytes) - } else if bytes < 1024 * 1024 { - write!(f, "{:.2} KB", bytes as f64 / 1024.0) - } else if bytes < 1024 * 1024 * 1024 { - write!(f, "{:.2} MB", bytes as f64 / (1024.0 * 1024.0)) - } else { - write!(f, "{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0)) +macro_rules! impl_from_unsigned { + ($($t:ty),*) => { + $( + impl From<$t> for FileSize { + fn from(value: $t) -> Self { + Self(value as u64) + } + } + )* + }; +} + +macro_rules! impl_try_from_signed { + ($($t:ty),*) => { + $( + impl TryFrom<$t> for FileSize { + type Error = FileSizeError; + + fn try_from(value: $t) -> Result { + if value < 0 { + Err(FileSizeError::NegativeSize { value: value as i64 }) + } else { + Ok(Self(value as u64)) + } + } + } + )* + }; +} + +impl_from_unsigned!(u8, u16, u32, u64, usize); +impl_try_from_signed!(i8, i16, i32, i64, isize); + +impl Display for FileSize { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB"]; + const THREASHOLD: f64 = 1024.0; + + let bytes = self.0 as f64; + + if bytes < THREASHOLD { + return write!(f, "{} {}", self.0, UNITS[0]); } + + let mut size = bytes; + let mut unit_index = 0; + + while size >= THREASHOLD && unit_index < UNITS.len() - 1 { + size /= THREASHOLD; + unit_index += 1; + } + if unit_index == 0 { + return write!(f, "{} {}", size as u64, UNITS[unit_index]); + } + write!(f, "{:.2} {}", size, UNITS[unit_index]) } -} \ No newline at end of file +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_file_size_display() { + assert_eq!(FileSize::new(512).to_string(), "512 B"); + assert_eq!(FileSize::new(1536).to_string(), "1.50 KB"); + assert_eq!(FileSize::new(1048576).to_string(), "1.00 MB"); + assert_eq!(FileSize::new(1073741824).to_string(), "1.00 GB"); + assert_eq!(FileSize::new(1099511627776).to_string(), "1.00 TB"); + } +} diff --git a/src/app/utils/mod.rs b/src/app/utils/mod.rs index fa4f3f6..45bf974 100644 --- a/src/app/utils/mod.rs +++ b/src/app/utils/mod.rs @@ -1,10 +1,11 @@ -use transmission_rpc::types::{ErrorType, Torrent, TorrentGetField, TorrentStatus}; - pub mod filesize; pub mod netspeed; -use crate::app::utils::filesize::FileSize; -use crate::app::utils::netspeed::NetSpeed; +use filesize::FileSize; +use netspeed::NetSpeed; +use transmission_rpc::types::{ + ErrorType, IdleMode, RatioMode, Torrent, TorrentGetField, TorrentStatus, +}; pub trait Wrapper { fn title(&self) -> String { @@ -23,113 +24,136 @@ pub trait Wrapper { impl Wrapper for TorrentGetField { fn title(&self) -> String { match self { - Self::ActivityDate => "Activity Date".to_string(), - Self::AddedDate => "Added Date".to_string(), - Self::Availability => todo!(), - Self::BandwidthPriority => todo!(), - Self::Comment => todo!(), - Self::CorruptEver => todo!(), - Self::Creator => todo!(), - Self::DateCreated => todo!(), - Self::DesiredAvailable => todo!(), - Self::DoneDate => "Done Date".to_string(), - Self::DownloadDir => "Path".to_string(), - Self::DownloadLimit => todo!(), - Self::DownloadLimited => todo!(), - Self::DownloadedEver => todo!(), - Self::EditDate => "Edit Date".to_string(), - Self::Error => "Error Type".to_string(), - Self::ErrorString => "Error String".to_string(), - Self::Eta => "ETA".to_string(), - Self::EtaIdle => todo!(), - Self::FileCount => todo!(), - Self::FileStats => "File Stats".to_string(), - Self::Files => "Files".to_string(), - Self::Group => todo!(), - Self::HashString => "Hash String".to_string(), - Self::HaveUnchecked => todo!(), - Self::HaveValid => todo!(), - Self::HonorsSessionLimits => todo!(), - Self::Id => "Id".to_string(), - Self::IsFinished => "Finished".to_string(), - Self::IsPrivate => "Private".to_string(), - Self::IsStalled => "Stalled".to_string(), - Self::Labels => "Labels".to_string(), - Self::LeftUntilDone => "Left Until Done".to_string(), - Self::MagnetLink => todo!(), - Self::ManualAnnounceTime => todo!(), - Self::MaxConnectedPeers => todo!(), - Self::MetadataPercentComplete => "Metadata Percent Complete".to_string(), - Self::Name => "Name".to_string(), - Self::PeerLimit => todo!(), - Self::Peers => todo!(), - Self::PeersConnected => "Connected".to_string(), - Self::PeersFrom => todo!(), - Self::PeersGettingFromUs => "Peers".to_string(), - Self::PeersSendingToUs => "Seeds".to_string(), - Self::PercentComplete => todo!(), - Self::PercentDone => "%".to_string(), - Self::PieceCount => todo!(), - Self::PieceSize => todo!(), - Self::Pieces => todo!(), - Self::PrimaryMimeType => todo!(), - Self::Priorities => "Priorities".to_string(), - Self::QueuePosition => "Queue".to_string(), - Self::RateDownload => "Download Speed".to_string(), - Self::RateUpload => "Upload Speed".to_string(), - Self::RecheckProgress => "Progress".to_string(), - Self::SecondsDownloading => todo!(), - Self::SecondsSeeding => "Seconds Seeding".to_string(), - Self::SeedIdleLimit => todo!(), - Self::SeedIdleMode => todo!(), - Self::SeedRatioLimit => "Seed Ratio Limit".to_string(), - Self::SeedRatioMode => "Seed Ratio Mode".to_string(), - Self::SequentialDownload => todo!(), - Self::SizeWhenDone => "Size".to_string(), - Self::StartDate => todo!(), - Self::Status => "Status".to_string(), - Self::TorrentFile => "Torrent File".to_string(), - Self::TotalSize => "Total Size".to_string(), - Self::TrackerList => todo!(), - Self::TrackerStats => todo!(), - Self::Trackers => "Trackers".to_string(), - Self::UploadLimit => todo!(), - Self::UploadLimited => todo!(), - Self::UploadRatio => "Ratio".to_string(), - Self::UploadedEver => "Uploaded".to_string(), - Self::Wanted => "Wanted".to_string(), - Self::Webseeds => todo!(), - Self::WebseedsSendingToUs => "Webseeds Sending to Us".to_string(), + Self::ActivityDate => "Activity Date", + Self::AddedDate => "Added Date", + Self::Availability => "Availability", + Self::BandwidthPriority => "Bandwidth Priority", + Self::Comment => "Comment", + Self::CorruptEver => "Corrupt Ever", + Self::Creator => "Creator", + Self::DateCreated => "Date Created", + Self::DesiredAvailable => "Desired Available", + Self::DoneDate => "Done Date", + Self::DownloadDir => "Path", + Self::DownloadLimit => "Download Limit", + Self::DownloadLimited => "Download Limited", + Self::DownloadedEver => "Downloaded Ever", + Self::EditDate => "Edit Date", + Self::Error => "Error Type", + Self::ErrorString => "Error String", + Self::Eta => "ETA", + Self::EtaIdle => "ETA Idle", + Self::FileCount => "File Count", + Self::FileStats => "File Stats", + Self::Files => "Files", + Self::Group => "Group", + Self::HashString => "Hash String", + Self::HaveUnchecked => "Have Unchecked", + Self::HaveValid => "Have Valid", + Self::HonorsSessionLimits => "Honors Session Limits", + Self::Id => "Id", + Self::IsFinished => "Finished", + Self::IsPrivate => "Private", + Self::IsStalled => "Stalled", + Self::Labels => "Labels", + Self::LeftUntilDone => "Left Until Done", + Self::MagnetLink => "Magnet Link", + Self::ManualAnnounceTime => "Manual Announce Time", + Self::MaxConnectedPeers => "Max Connected Peers", + Self::MetadataPercentComplete => "Metadata Percent Complete", + Self::Name => "Name", + Self::PeerLimit => "Peer Limit", + Self::Peers => "Peers", + Self::PeersConnected => "Connected", + Self::PeersFrom => "Peers From", + Self::PeersGettingFromUs => "Peers", + Self::PeersSendingToUs => "Seeds", + Self::PercentComplete => "Percent Complete", + Self::PercentDone => "%", + Self::PieceCount => "Piece Count", + Self::PieceSize => "Piece Size", + Self::Pieces => "Pieces", + Self::PrimaryMimeType => "Primary Mime Type", + Self::Priorities => "Priorities", + Self::QueuePosition => "Queue", + Self::RateDownload => "Download Speed", + Self::RateUpload => "Upload Speed", + Self::RecheckProgress => "Progress", + Self::SecondsDownloading => "Seconds Downloading", + Self::SecondsSeeding => "Seconds Seeding", + Self::SeedIdleLimit => "Seed Idle Limit", + Self::SeedIdleMode => "Seed Idle Mode", + Self::SeedRatioLimit => "Seed Ratio Limit", + Self::SeedRatioMode => "Seed Ratio Mode", + Self::SequentialDownload => "Sequential Download", + Self::SizeWhenDone => "Size", + Self::StartDate => "Start Date", + Self::Status => "Status", + Self::TorrentFile => "Torrent File", + Self::TotalSize => "Total Size", + Self::TrackerList => "Tracker List", + Self::TrackerStats => "Tracker Stats", + Self::Trackers => "Trackers", + Self::UploadLimit => "Upload Limit", + Self::UploadLimited => "Upload Limited", + Self::UploadRatio => "Ratio", + Self::UploadedEver => "Uploaded", + Self::Wanted => "Wanted", + Self::Webseeds => "Webseeds", + Self::WebseedsSendingToUs => "Webseeds Sending to Us", } + .into() } fn value(&self, torrent: &Torrent) -> String { match self { - Self::ActivityDate => torrent.activity_date.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), - Self::AddedDate => torrent.added_date.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), - Self::Availability => todo!(), - Self::BandwidthPriority => todo!(), - Self::Comment => todo!(), - Self::CorruptEver => todo!(), - Self::Creator => todo!(), - Self::DateCreated => todo!(), - Self::DesiredAvailable => todo!(), - Self::DoneDate => torrent.done_date.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), - Self::DownloadDir => torrent.download_dir.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), - Self::DownloadLimit => todo!(), - Self::DownloadLimited => todo!(), - Self::DownloadedEver => todo!(), - Self::EditDate => torrent.edit_date.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), + Self::ActivityDate => torrent + .activity_date + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::AddedDate => torrent + .added_date + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::Availability => "N/A".to_string(), + Self::BandwidthPriority => torrent + .bandwidth_priority + .map(|v| format!("{:?}", v)) + .unwrap_or_default(), + Self::Comment => torrent.comment.clone().unwrap_or_default(), + Self::CorruptEver => FileSize::from(torrent.corrupt_ever.unwrap_or(0)).to_string(), + Self::Creator => torrent.creator.clone().unwrap_or_default(), + Self::DateCreated => torrent + .date_created + .map(|v| v.to_string()) + .unwrap_or_default() + .to_string(), + Self::DesiredAvailable => { + FileSize::from(torrent.desired_available.unwrap_or(0)).to_string() + } + Self::DoneDate => torrent.done_date.map(|v| v.to_string()).unwrap_or_default(), + Self::DownloadDir => torrent.download_dir.clone().unwrap_or_default().to_string(), + Self::DownloadLimit => NetSpeed::from(torrent.download_limit.unwrap_or(0)).to_string(), + Self::DownloadLimited => torrent + .download_limited + .map(|v| v.to_string()) + .unwrap_or_default() + .to_string(), + Self::DownloadedEver => { + FileSize::from(torrent.downloaded_ever.unwrap_or(0)).to_string() + } + Self::EditDate => torrent.edit_date.map(|v| v.to_string()).unwrap_or_default(), Self::Error => match torrent.error { Some(error) => match error { - ErrorType::Ok => "Ok".to_string(), - ErrorType::LocalError => "LocalError".to_string(), - ErrorType::TrackerError => "TrackerError".to_string(), - ErrorType::TrackerWarning => "TrackerWarning".to_string(), + ErrorType::Ok => "Ok", + ErrorType::LocalError => "LocalError", + ErrorType::TrackerError => "TrackerError", + ErrorType::TrackerWarning => "TrackerWarning", }, - None => "N/A".to_string(), - }, - Self::ErrorString => torrent.error_string.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), + None => "N/A", + } + .to_string(), + Self::ErrorString => torrent.error_string.clone().unwrap_or_default(), Self::Eta => match torrent.eta { Some(eta) => match eta { -1 => "".to_string(), @@ -138,184 +162,306 @@ impl Wrapper for TorrentGetField { }, None => "".to_string(), }, - Self::EtaIdle => todo!(), - Self::FileCount => todo!(), - Self::FileStats => match &torrent.file_stats { - Some(file_stats) => file_stats - .iter() - .map(|x| format!("{:?}", x.priority)) - .collect(), - None => "N/A".to_string(), - }, - Self::Files => match &torrent.files { - Some(files) => files.iter().map(|x| x.name.to_owned()).collect(), - None => "N/A".to_string(), - }, - Self::Group => todo!(), - Self::HashString => torrent.hash_string.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), + Self::EtaIdle => torrent.eta_idle.map(|v| v.to_string()).unwrap_or_default(), + Self::FileCount => torrent + .file_count + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::FileStats => torrent + .file_stats + .as_ref() + .map(|v| format!("{}", v.len())) + .unwrap_or_default(), + Self::Files => torrent + .files + .as_ref() + .map(|v| format!("{}", v.len())) + .unwrap_or_default(), + Self::Group => torrent.group.clone().unwrap_or_default(), + Self::HashString => torrent.hash_string.clone().unwrap_or_default(), Self::HaveUnchecked => todo!(), - Self::HaveValid => todo!(), - Self::HonorsSessionLimits => todo!(), - Self::Id => torrent.id.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), - Self::IsFinished => torrent.is_finished.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), - Self::IsPrivate => torrent.is_private.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), - Self::IsStalled => torrent.is_stalled.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), - Self::Labels => torrent.labels.as_ref().map_or_else(|| "N/A".to_string(), |v| v.join(" ")), - Self::LeftUntilDone => FileSize::from(torrent.left_until_done.unwrap_or(0)).to_string(), - Self::MagnetLink => todo!(), - Self::ManualAnnounceTime => todo!(), - Self::MaxConnectedPeers => todo!(), - Self::MetadataPercentComplete => torrent.metadata_percent_complete.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), - Self::Name => torrent.name.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), - Self::PeerLimit => todo!(), - Self::Peers => todo!(), - Self::PeersConnected => torrent.peers_connected.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), - Self::PeersFrom => todo!(), - Self::PeersGettingFromUs => torrent.peers_getting_from_us.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), - Self::PeersSendingToUs => torrent.peers_sending_to_us.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), - Self::PercentComplete => todo!(), - Self::PercentDone => match torrent.percent_done { - Some(percent_done) => format!("{:.0}", percent_done * 100.0), - None => "N/A".to_string(), - }, - Self::PieceCount => todo!(), - Self::PieceSize => todo!(), - Self::Pieces => todo!(), - Self::PrimaryMimeType => todo!(), - Self::Priorities => match &torrent.priorities { - Some(priorities) => priorities.iter().map(|x| format!("{:?}", x)).collect(), - None => "N/A".to_string(), - }, - Self::QueuePosition => "N/A".to_string(), - Self::RateDownload => NetSpeed::from(torrent.rate_download.unwrap_or(0)).to_string(), - Self::RateUpload => NetSpeed::from(torrent.rate_upload.unwrap_or(0)).to_string(), - Self::RecheckProgress => torrent.recheck_progress.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), - Self::SecondsDownloading => todo!(), - Self::SecondsSeeding => torrent.seconds_seeding.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), - Self::SeedIdleLimit => todo!(), - Self::SeedIdleMode => todo!(), - Self::SeedRatioLimit => torrent.seed_ratio_limit.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), - Self::SeedRatioMode => "N/A".to_string(), - Self::SequentialDownload => todo!(), - Self::SizeWhenDone => FileSize::from(torrent.size_when_done.unwrap_or(0)).to_string(), - Self::StartDate => todo!(), + Self::HaveValid => FileSize::from(torrent.have_valid.unwrap_or(0)).to_string(), + Self::HonorsSessionLimits => torrent + .honors_session_limits + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::Id => torrent.id.map(|v| v.to_string()).unwrap_or_default(), + Self::IsFinished => torrent + .is_finished + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::IsPrivate => torrent + .is_private + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::IsStalled => torrent + .is_stalled + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::Labels => torrent.labels.clone().unwrap_or_default().join(", "), + Self::LeftUntilDone => todo!(), + Self::MagnetLink => torrent.magnet_link.clone().unwrap_or_default(), + Self::ManualAnnounceTime => torrent + .manual_announce_time + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::MaxConnectedPeers => torrent + .max_connected_peers + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::MetadataPercentComplete => torrent + .metadata_percent_complete + .map(|v| format!("{:.2}", v)) + .unwrap_or_default(), + Self::Name => torrent.name.clone().unwrap_or_default(), + Self::PeerLimit => torrent + .peer_limit + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::Peers => torrent + .peers + .as_ref() + .map(|v| format!("{}", v.len())) + .unwrap_or_default(), + Self::PeersConnected => torrent + .peers_connected + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::PeersFrom => torrent + .peers_from + .as_ref() + .map(|p| { + format!( + "d:{} u:{} i:{} t:{}", + p.from_dht, p.from_incoming, p.from_lpd, p.from_tracker + ) + }) + .unwrap_or_default(), + Self::PeersGettingFromUs => torrent + .peers_getting_from_us + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::PeersSendingToUs => torrent + .peers_sending_to_us + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::PercentComplete => torrent + .percent_complete + .map(|v| format!("{:.2}", v)) + .unwrap_or_default(), + Self::PercentDone => torrent + .percent_done + .map(|v| format!("{:.2}", v)) + .unwrap_or_default(), + Self::PieceCount => torrent + .piece_count + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::PieceSize => FileSize::from(torrent.piece_size.unwrap_or(0)).to_string(), + Self::Pieces => torrent + .pieces + .as_ref() + .map(|p| format!("{} bytes", p.len())) + .unwrap_or_default(), + Self::PrimaryMimeType => torrent.primary_mime_type.clone().unwrap_or_default(), + Self::Priorities => torrent + .priorities + .as_ref() + .map(|v| format!("{}", v.len())) + .unwrap_or_default(), + Self::QueuePosition => torrent + .queue_position + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::RateDownload => NetSpeed::try_from(torrent.rate_download.unwrap_or(0)) + .unwrap_or_default() + .to_string(), + Self::RateUpload => NetSpeed::try_from(torrent.rate_upload.unwrap_or(0)) + .unwrap_or_default() + .to_string(), + Self::RecheckProgress => torrent + .recheck_progress + .map(|v| format!("{:.2}", v)) + .unwrap_or_default(), + Self::SecondsDownloading => torrent + .seconds_downloading + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::SecondsSeeding => torrent + .seconds_seeding + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::SeedIdleLimit => torrent + .seed_idle_limit + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::SeedIdleMode => torrent + .seed_idle_mode + .map(|v| match v { + IdleMode::Global => "Global", + IdleMode::Single => "Single", + IdleMode::Unlimited => "Unlimited", + }) + .unwrap_or("N/A") + .to_string(), + Self::SeedRatioLimit => torrent + .seed_ratio_limit + .map(|v| format!("{:.2}", v)) + .unwrap_or_default(), + Self::SeedRatioMode => torrent + .seed_ratio_mode + .map(|v| match v { + RatioMode::Global => "Global", + RatioMode::Single => "Single", + RatioMode::Unlimited => "Unlimited", + }) + .unwrap_or_default() + .to_string(), + Self::SequentialDownload => torrent + .sequential_download + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::SizeWhenDone => FileSize::try_from(torrent.size_when_done.unwrap_or(0)) + .unwrap_or_default() + .to_string(), + Self::StartDate => torrent + .start_date + .map(|v| v.to_string()) + .unwrap_or_default(), Self::Status => match torrent.status { Some(status) => match status { - TorrentStatus::Stopped => "Stopped".to_string(), - TorrentStatus::Seeding => "Seeding".to_string(), - TorrentStatus::Verifying => "Verifying".to_string(), - TorrentStatus::Downloading => "Downloading".to_string(), - TorrentStatus::QueuedToSeed => "QueuedToSeed".to_string(), - TorrentStatus::QueuedToVerify => "QueuedToVerify".to_string(), - TorrentStatus::QueuedToDownload => "QueuedToDownload".to_string(), + TorrentStatus::Stopped => "Stopped", + TorrentStatus::Seeding => "Seeding", + TorrentStatus::Verifying => "Verifying", + TorrentStatus::Downloading => "Downloading", + TorrentStatus::QueuedToSeed => "QueuedToSeed", + TorrentStatus::QueuedToVerify => "QueuedToVerify", + TorrentStatus::QueuedToDownload => "QueuedToDownload", }, - None => "N/A".to_string(), - }, - Self::TorrentFile => torrent.torrent_file.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), - Self::TotalSize => FileSize::from(torrent.total_size.unwrap_or(0)).to_string(), - Self::TrackerList => todo!(), - Self::TrackerStats => todo!(), - Self::Trackers => match &torrent.trackers { - Some(trackers) => trackers.iter().map(|x| x.announce.to_string()).collect(), - None => "N/A".to_string(), - }, - Self::UploadLimit => todo!(), - Self::UploadLimited => todo!(), - Self::UploadRatio => match torrent.upload_ratio { - Some(upload_ratio) => format!("{:.2}", upload_ratio), - None => "N/A".to_string(), - }, - Self::UploadedEver => FileSize::from(torrent.uploaded_ever.unwrap_or(0)).to_string(), - Self::Wanted => match &torrent.wanted { - Some(wanted) => wanted.iter().map(|x| x.to_string()).collect(), - None => "N/A".to_string(), - }, - Self::Webseeds => todo!(), - Self::WebseedsSendingToUs => "N/A".to_string(), + None => "N/A", + } + .to_string(), + Self::TorrentFile => torrent.torrent_file.clone().unwrap_or_default(), + Self::TotalSize => FileSize::try_from(torrent.total_size.unwrap_or(0)) + .unwrap_or_default() + .to_string(), + Self::TrackerList => torrent.tracker_list.clone().unwrap_or_default(), + Self::TrackerStats => torrent + .tracker_stats + .as_ref() + .map(|v| format!("{}", v.len())) + .unwrap_or_default(), + Self::Trackers => torrent + .trackers + .as_ref() + .map(|v| format!("{}", v.len())) + .unwrap_or_default(), + Self::UploadLimit => NetSpeed::try_from(torrent.upload_limit.unwrap_or(0)) + .unwrap_or_default() + .to_string(), + Self::UploadLimited => torrent + .upload_limited + .map(|v| v.to_string()) + .unwrap_or_default(), + Self::UploadRatio => torrent + .upload_ratio + .map(|v| format!("{:.2}", v)) + .unwrap_or_default(), + Self::UploadedEver => FileSize::try_from(torrent.uploaded_ever.unwrap_or(0)) + .unwrap_or_default() + .to_string(), + Self::Wanted => torrent + .wanted + .as_ref() + .map(|v| format!("{}", v.len())) + .unwrap_or_default(), + Self::Webseeds => torrent.webseeds.clone().unwrap_or_default().join(", "), + Self::WebseedsSendingToUs => torrent + .webseeds_sending_to_us + .map(|v| v.to_string()) + .unwrap_or_default(), } } fn width(&self) -> u16 { match self { - Self::ActivityDate => 10, - Self::AddedDate => 10, - Self::Availability => todo!(), - Self::BandwidthPriority => todo!(), - Self::Comment => todo!(), - Self::CorruptEver => todo!(), - Self::Creator => todo!(), - Self::DateCreated => todo!(), - Self::DesiredAvailable => todo!(), - Self::DoneDate => 10, + Self::ActivityDate => 20, + Self::AddedDate => 20, + Self::Availability => 10, + Self::BandwidthPriority => 10, + Self::Comment => 20, + Self::CorruptEver => 15, + Self::Creator => 20, + Self::DateCreated => 20, + Self::DesiredAvailable => 15, + Self::DoneDate => 20, Self::DownloadDir => 30, - Self::DownloadLimit => todo!(), - Self::DownloadLimited => todo!(), - Self::DownloadedEver => todo!(), - Self::EditDate => 10, - Self::Error => 10, - Self::ErrorString => 10, + Self::DownloadLimit => 15, + Self::DownloadLimited => 10, + Self::DownloadedEver => 15, + Self::EditDate => 20, + Self::Error => 15, + Self::ErrorString => 20, Self::Eta => 10, - Self::EtaIdle => todo!(), - Self::FileCount => todo!(), + Self::EtaIdle => 10, + Self::FileCount => 10, Self::FileStats => 10, Self::Files => 10, - Self::Group => todo!(), - Self::HashString => 10, - Self::HaveUnchecked => todo!(), - Self::HaveValid => todo!(), - Self::HonorsSessionLimits => todo!(), - Self::Id => 10, + Self::Group => 10, + Self::HashString => 42, + Self::HaveUnchecked => 15, + Self::HaveValid => 15, + Self::HonorsSessionLimits => 10, + Self::Id => 5, Self::IsFinished => 10, Self::IsPrivate => 10, Self::IsStalled => 10, - Self::Labels => 10, - Self::LeftUntilDone => 10, - Self::MagnetLink => todo!(), - Self::ManualAnnounceTime => todo!(), - Self::MaxConnectedPeers => todo!(), + Self::Labels => 20, + Self::LeftUntilDone => 15, + Self::MagnetLink => 50, + Self::ManualAnnounceTime => 20, + Self::MaxConnectedPeers => 10, Self::MetadataPercentComplete => 10, Self::Name => 70, - Self::PeerLimit => todo!(), - Self::Peers => todo!(), + Self::PeerLimit => 10, + Self::Peers => 10, Self::PeersConnected => 10, - Self::PeersFrom => todo!(), + Self::PeersFrom => 20, Self::PeersGettingFromUs => 10, Self::PeersSendingToUs => 10, - Self::PercentComplete => todo!(), + Self::PercentComplete => 10, Self::PercentDone => 10, - Self::PieceCount => todo!(), - Self::PieceSize => todo!(), - Self::Pieces => todo!(), - Self::PrimaryMimeType => todo!(), + Self::PieceCount => 10, + Self::PieceSize => 15, + Self::Pieces => 20, + Self::PrimaryMimeType => 20, Self::Priorities => 10, Self::QueuePosition => 10, - Self::RateDownload => 10, - Self::RateUpload => 10, + Self::RateDownload => 15, + Self::RateUpload => 15, Self::RecheckProgress => 10, - Self::SecondsDownloading => todo!(), - Self::SecondsSeeding => 10, - Self::SeedIdleLimit => todo!(), - Self::SeedIdleMode => todo!(), + Self::SecondsDownloading => 15, + Self::SecondsSeeding => 15, + Self::SeedIdleLimit => 10, + Self::SeedIdleMode => 15, Self::SeedRatioLimit => 10, - Self::SeedRatioMode => 10, - Self::SequentialDownload => todo!(), - Self::SizeWhenDone => 10, - Self::StartDate => todo!(), + Self::SeedRatioMode => 15, + Self::SequentialDownload => 10, + Self::SizeWhenDone => 15, + Self::StartDate => 20, Self::Status => 15, - Self::TorrentFile => 10, - Self::TotalSize => 10, - Self::TrackerList => todo!(), - Self::TrackerStats => todo!(), + Self::TorrentFile => 30, + Self::TotalSize => 15, + Self::TrackerList => 30, + Self::TrackerStats => 10, Self::Trackers => 10, - Self::UploadLimit => todo!(), - Self::UploadLimited => todo!(), + Self::UploadLimit => 15, + Self::UploadLimited => 10, Self::UploadRatio => 10, - Self::UploadedEver => 10, + Self::UploadedEver => 15, Self::Wanted => 10, - Self::Webseeds => todo!(), + Self::Webseeds => 20, Self::WebseedsSendingToUs => 10, } } } - - diff --git a/src/app/utils/netspeed.rs b/src/app/utils/netspeed.rs index 215b303..0d938af 100644 --- a/src/app/utils/netspeed.rs +++ b/src/app/utils/netspeed.rs @@ -1,24 +1,159 @@ -use std::fmt; +use std::fmt::Display; +use thiserror::Error; -pub struct NetSpeed(i64); +#[derive(Error, Debug, PartialEq)] +pub enum NetSpeedError { + #[error("Network speed cannot be negative: {value}")] + NegativeSpeed { value: i64 }, + #[error("Network speed value is too large: {value}")] + ValueTooLarge { value: f64 }, + #[error("Network speed value is invalid: {reason}")] + InvalidValue { reason: String }, +} -impl From for NetSpeed { - fn from(bytes_per_second: i64) -> Self { - NetSpeed(bytes_per_second) +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default)] +pub struct NetSpeed(u64); + +impl NetSpeed { + pub const fn new(bytes_per_second: u64) -> Self { + Self(bytes_per_second) + } + + pub const fn bytes_per_second(&self) -> u64 { + self.0 + } + + pub const fn kilobytes_per_second(kb: u64) -> Self { + Self(kb * 1024) + } + + pub const fn megabytes_per_second(mb: u64) -> Self { + Self(mb * 1024 * 1024) + } + + pub const fn gigabytes_per_second(gb: u64) -> Self { + Self(gb * 1024 * 1024 * 1024) + } + + pub const fn from_bits_per_second(bps: u64) -> Self { + NetSpeed(bps / 8) + } + + pub const fn to_bits_per_second(&self) -> u64 { + self.0 * 8 + } + + pub const fn from_kilobits_per_second(kbps: u64) -> Self { + NetSpeed(kbps * 1000 / 8) + } + + pub const fn from_megabits_per_second(mbps: u64) -> Self { + NetSpeed(mbps * 1_000_000 / 8) + } + + pub const fn from_gigabits_per_second(gbps: u64) -> Self { + NetSpeed(gbps * 1_000_000_000 / 8) } } -impl fmt::Display for NetSpeed { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let bytes_per_second = self.0; - if bytes_per_second < 1024 { - write!(f, "{} B/s", bytes_per_second) - } else if bytes_per_second < 1024 * 1024 { - write!(f, "{:.2} KB/s", bytes_per_second as f64 / 1024.0) - } else if bytes_per_second < 1024 * 1024 * 1024 { - write!(f, "{:.2} MB/s", bytes_per_second as f64 / (1024.0 * 1024.0)) - } else { - write!(f, "{:.2} GB/s", bytes_per_second as f64 / (1024.0 * 1024.0 * 1024.0)) +macro_rules! impl_from_unsigned { + ($($t:ty),*) => { + $( + impl From<$t> for NetSpeed { + fn from(value: $t) -> Self { + Self(value as u64) + } + } + )* + }; +} + +macro_rules! impl_try_from_signed { + ($($t:ty),*) => { + $( + impl TryFrom<$t> for NetSpeed { + type Error = NetSpeedError; + + fn try_from(value: $t) -> Result { + if value < 0 { + Err(NetSpeedError::NegativeSpeed { value: value as i64 }) + } else { + Ok(Self(value as u64)) + } + } + } + )* + }; +} + +impl_from_unsigned!(u8, u16, u32, u64, usize); +impl_try_from_signed!(i8, i16, i32, i64, isize); + +impl Display for NetSpeed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + const UNITS: &[&str] = &["B/s", "KB/s", "MB/s", "GB/s", "TB/s", "PB/s"]; + const THREASHOLD: f64 = 1024.0; + + let bytes_per_second = self.0 as f64; + + if bytes_per_second < THREASHOLD { + return write!(f, "{} {}", self.0, UNITS[0]); } + + let mut size = bytes_per_second; + let mut unit_index = 0; + + while size >= THREASHOLD && unit_index < UNITS.len() - 1 { + size /= THREASHOLD; + unit_index += 1; + } + if unit_index == 0 { + return write!(f, "{} {}", size as u64, UNITS[unit_index]); + } + write!(f, "{:.2} {}", size, UNITS[unit_index]) } -} \ No newline at end of file +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_net_speed_display() { + assert_eq!(NetSpeed::new(512).to_string(), "512 B/s"); + assert_eq!(NetSpeed::new(1536).to_string(), "1.50 KB/s"); + assert_eq!(NetSpeed::new(1048576).to_string(), "1.00 MB/s"); + assert_eq!(NetSpeed::new(1073741824).to_string(), "1.00 GB/s"); + assert_eq!(NetSpeed::new(1099511627776).to_string(), "1.00 TB/s"); + } + + #[test] + fn test_bits_conversion() { + let speed = NetSpeed::from_bits_per_second(8000); + assert_eq!(speed.bytes_per_second(), 1000); + assert_eq!(speed.to_bits_per_second(), 8000); + } + + #[test] + fn test_network_units() { + // 1 Mbps = 125,000 bytes per second + let speed = NetSpeed::from_megabits_per_second(1); + assert_eq!(speed.bytes_per_second(), 125_000); + + // 1 Gbps = 125,000,000 bytes per second + let speed = NetSpeed::from_gigabits_per_second(1); + assert_eq!(speed.bytes_per_second(), 125_000_000); + } + + #[test] + fn test_try_from_i64() { + assert_eq!( + NetSpeed::try_from(1000i64).unwrap().bytes_per_second(), + 1000 + ); + assert_eq!( + NetSpeed::try_from(-100i64), + Err(NetSpeedError::NegativeSpeed { value: -100 }) + ); + } +} diff --git a/src/event.rs b/src/event.rs index 908a71a..51d3699 100644 --- a/src/event.rs +++ b/src/event.rs @@ -20,7 +20,6 @@ pub enum Event { /// Terminal event handler. #[allow(dead_code)] #[derive(Debug)] -/// TODO: write description pub struct EventHandler { /// Event sender channel. sender: mpsc::Sender, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 739d2e6..0cc38f3 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,3 +1,8 @@ +mod popup; +mod table; + +use crate::app::{App, Tab}; +use popup::render_popup; use ratatui::{ layout::Alignment, prelude::{Constraint, Direction, Layout}, @@ -6,12 +11,7 @@ use ratatui::{ widgets::{Block, BorderType, Borders, Clear, Tabs}, Frame, }; -mod popup; -mod table; - -use crate::app::{App, Tab}; - -use self::{popup::render_popup, table::render_table}; +use table::render_table; /// Renders the user interface widgets. pub fn render(app: &mut App, frame: &mut Frame) {