refactor(utils): update NetSpeed and FileSize structs

This commit is contained in:
Kristofers Solo 2025-07-02 13:51:48 +03:00
parent 9efeec3890
commit 9baf60c98b
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
12 changed files with 671 additions and 310 deletions

1
Cargo.lock generated
View File

@ -1628,6 +1628,7 @@ dependencies = [
"anyhow", "anyhow",
"crossterm 0.29.0", "crossterm 0.29.0",
"ratatui", "ratatui",
"thiserror",
"tokio", "tokio",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",

View File

@ -14,3 +14,4 @@ anyhow = "1.0"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
thiserror = "2.0"

View File

@ -1,9 +1,6 @@
use std::{collections::HashSet, path::Path};
use transmission_rpc::types::{Torrent, TorrentAction, TorrentStatus};
use super::{types::Selected, Torrents}; use super::{types::Selected, Torrents};
use std::{collections::HashSet, path::Path};
use transmission_rpc::types::{Torrent, TorrentAction, TorrentStatus};
impl Torrents { impl Torrents {
pub async fn toggle(&mut self, ids: Selected) -> anyhow::Result<()> { pub async fn toggle(&mut self, ids: Selected) -> anyhow::Result<()> {

View File

@ -1,17 +1,15 @@
mod tab;
mod torrent;
pub mod utils;
use ratatui::widgets::TableState;
pub mod action; pub mod action;
mod command; mod command;
mod tab;
mod torrent;
pub mod types; pub mod types;
pub mod utils;
pub use {tab::Tab, torrent::Torrents};
use self::types::Selected; use ratatui::widgets::TableState;
pub use self::{tab::Tab, torrent::Torrents}; use types::Selected;
/// Main Application. /// Main Application.
/// TODO: write description
#[derive(Debug)] #[derive(Debug)]
pub struct App<'a> { pub struct App<'a> {
pub running: bool, pub running: bool,
@ -149,7 +147,10 @@ impl<'a> App<'a> {
fn selected(&self, highlighted: bool) -> Selected { fn selected(&self, highlighted: bool) -> Selected {
let torrents = &self.torrents.torrents; let torrents = &self.torrents.torrents;
if self.torrents.selected.is_empty() || highlighted { 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 { if let Some(id) = selected_id {
return Selected::Current(id); return Selected::Current(id);
} }

View File

@ -1,7 +1,6 @@
use transmission_rpc::types::TorrentGetField; use transmission_rpc::types::TorrentGetField;
/// Available tabs. /// Available tabs.
/// TODO: write description
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub enum Tab { pub enum Tab {
#[default] #[default]

View File

@ -1,11 +1,9 @@
use std::{collections::HashSet, fmt::Debug};
use anyhow::Result; use anyhow::Result;
use std::{collections::HashSet, fmt::Debug};
use transmission_rpc::{ use transmission_rpc::{
types::{Torrent, TorrentGetField}, types::{Torrent, TorrentGetField},
TransClient, TransClient,
}; };
use url::Url; use url::Url;
/// List of torrents. /// List of torrents.

View File

@ -1,5 +1,4 @@
use std::collections::HashSet; use std::collections::HashSet;
use transmission_rpc::types::Id; use transmission_rpc::types::Id;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]

View File

@ -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<i64> for FileSize { #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
fn from(bytes: i64) -> Self { pub struct FileSize(u64);
FileSize(bytes)
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 { macro_rules! impl_from_unsigned {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { ($($t:ty),*) => {
let bytes = self.0; $(
if bytes < 1024 { impl From<$t> for FileSize {
write!(f, "{} B", bytes) fn from(value: $t) -> Self {
} else if bytes < 1024 * 1024 { Self(value as u64)
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)) )*
};
}
macro_rules! impl_try_from_signed {
($($t:ty),*) => {
$(
impl TryFrom<$t> for FileSize {
type Error = FileSizeError;
fn try_from(value: $t) -> Result<Self, Self::Error> {
if value < 0 {
Err(FileSizeError::NegativeSize { value: value as i64 })
} else { } else {
write!(f, "{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0)) 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])
}
}
#[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");
}
}

View File

@ -1,10 +1,11 @@
use transmission_rpc::types::{ErrorType, Torrent, TorrentGetField, TorrentStatus};
pub mod filesize; pub mod filesize;
pub mod netspeed; pub mod netspeed;
use crate::app::utils::filesize::FileSize; use filesize::FileSize;
use crate::app::utils::netspeed::NetSpeed; use netspeed::NetSpeed;
use transmission_rpc::types::{
ErrorType, IdleMode, RatioMode, Torrent, TorrentGetField, TorrentStatus,
};
pub trait Wrapper { pub trait Wrapper {
fn title(&self) -> String { fn title(&self) -> String {
@ -23,113 +24,136 @@ pub trait Wrapper {
impl Wrapper for TorrentGetField { impl Wrapper for TorrentGetField {
fn title(&self) -> String { fn title(&self) -> String {
match self { match self {
Self::ActivityDate => "Activity Date".to_string(), Self::ActivityDate => "Activity Date",
Self::AddedDate => "Added Date".to_string(), Self::AddedDate => "Added Date",
Self::Availability => todo!(), Self::Availability => "Availability",
Self::BandwidthPriority => todo!(), Self::BandwidthPriority => "Bandwidth Priority",
Self::Comment => todo!(), Self::Comment => "Comment",
Self::CorruptEver => todo!(), Self::CorruptEver => "Corrupt Ever",
Self::Creator => todo!(), Self::Creator => "Creator",
Self::DateCreated => todo!(), Self::DateCreated => "Date Created",
Self::DesiredAvailable => todo!(), Self::DesiredAvailable => "Desired Available",
Self::DoneDate => "Done Date".to_string(), Self::DoneDate => "Done Date",
Self::DownloadDir => "Path".to_string(), Self::DownloadDir => "Path",
Self::DownloadLimit => todo!(), Self::DownloadLimit => "Download Limit",
Self::DownloadLimited => todo!(), Self::DownloadLimited => "Download Limited",
Self::DownloadedEver => todo!(), Self::DownloadedEver => "Downloaded Ever",
Self::EditDate => "Edit Date".to_string(), Self::EditDate => "Edit Date",
Self::Error => "Error Type".to_string(), Self::Error => "Error Type",
Self::ErrorString => "Error String".to_string(), Self::ErrorString => "Error String",
Self::Eta => "ETA".to_string(), Self::Eta => "ETA",
Self::EtaIdle => todo!(), Self::EtaIdle => "ETA Idle",
Self::FileCount => todo!(), Self::FileCount => "File Count",
Self::FileStats => "File Stats".to_string(), Self::FileStats => "File Stats",
Self::Files => "Files".to_string(), Self::Files => "Files",
Self::Group => todo!(), Self::Group => "Group",
Self::HashString => "Hash String".to_string(), Self::HashString => "Hash String",
Self::HaveUnchecked => todo!(), Self::HaveUnchecked => "Have Unchecked",
Self::HaveValid => todo!(), Self::HaveValid => "Have Valid",
Self::HonorsSessionLimits => todo!(), Self::HonorsSessionLimits => "Honors Session Limits",
Self::Id => "Id".to_string(), Self::Id => "Id",
Self::IsFinished => "Finished".to_string(), Self::IsFinished => "Finished",
Self::IsPrivate => "Private".to_string(), Self::IsPrivate => "Private",
Self::IsStalled => "Stalled".to_string(), Self::IsStalled => "Stalled",
Self::Labels => "Labels".to_string(), Self::Labels => "Labels",
Self::LeftUntilDone => "Left Until Done".to_string(), Self::LeftUntilDone => "Left Until Done",
Self::MagnetLink => todo!(), Self::MagnetLink => "Magnet Link",
Self::ManualAnnounceTime => todo!(), Self::ManualAnnounceTime => "Manual Announce Time",
Self::MaxConnectedPeers => todo!(), Self::MaxConnectedPeers => "Max Connected Peers",
Self::MetadataPercentComplete => "Metadata Percent Complete".to_string(), Self::MetadataPercentComplete => "Metadata Percent Complete",
Self::Name => "Name".to_string(), Self::Name => "Name",
Self::PeerLimit => todo!(), Self::PeerLimit => "Peer Limit",
Self::Peers => todo!(), Self::Peers => "Peers",
Self::PeersConnected => "Connected".to_string(), Self::PeersConnected => "Connected",
Self::PeersFrom => todo!(), Self::PeersFrom => "Peers From",
Self::PeersGettingFromUs => "Peers".to_string(), Self::PeersGettingFromUs => "Peers",
Self::PeersSendingToUs => "Seeds".to_string(), Self::PeersSendingToUs => "Seeds",
Self::PercentComplete => todo!(), Self::PercentComplete => "Percent Complete",
Self::PercentDone => "%".to_string(), Self::PercentDone => "%",
Self::PieceCount => todo!(), Self::PieceCount => "Piece Count",
Self::PieceSize => todo!(), Self::PieceSize => "Piece Size",
Self::Pieces => todo!(), Self::Pieces => "Pieces",
Self::PrimaryMimeType => todo!(), Self::PrimaryMimeType => "Primary Mime Type",
Self::Priorities => "Priorities".to_string(), Self::Priorities => "Priorities",
Self::QueuePosition => "Queue".to_string(), Self::QueuePosition => "Queue",
Self::RateDownload => "Download Speed".to_string(), Self::RateDownload => "Download Speed",
Self::RateUpload => "Upload Speed".to_string(), Self::RateUpload => "Upload Speed",
Self::RecheckProgress => "Progress".to_string(), Self::RecheckProgress => "Progress",
Self::SecondsDownloading => todo!(), Self::SecondsDownloading => "Seconds Downloading",
Self::SecondsSeeding => "Seconds Seeding".to_string(), Self::SecondsSeeding => "Seconds Seeding",
Self::SeedIdleLimit => todo!(), Self::SeedIdleLimit => "Seed Idle Limit",
Self::SeedIdleMode => todo!(), Self::SeedIdleMode => "Seed Idle Mode",
Self::SeedRatioLimit => "Seed Ratio Limit".to_string(), Self::SeedRatioLimit => "Seed Ratio Limit",
Self::SeedRatioMode => "Seed Ratio Mode".to_string(), Self::SeedRatioMode => "Seed Ratio Mode",
Self::SequentialDownload => todo!(), Self::SequentialDownload => "Sequential Download",
Self::SizeWhenDone => "Size".to_string(), Self::SizeWhenDone => "Size",
Self::StartDate => todo!(), Self::StartDate => "Start Date",
Self::Status => "Status".to_string(), Self::Status => "Status",
Self::TorrentFile => "Torrent File".to_string(), Self::TorrentFile => "Torrent File",
Self::TotalSize => "Total Size".to_string(), Self::TotalSize => "Total Size",
Self::TrackerList => todo!(), Self::TrackerList => "Tracker List",
Self::TrackerStats => todo!(), Self::TrackerStats => "Tracker Stats",
Self::Trackers => "Trackers".to_string(), Self::Trackers => "Trackers",
Self::UploadLimit => todo!(), Self::UploadLimit => "Upload Limit",
Self::UploadLimited => todo!(), Self::UploadLimited => "Upload Limited",
Self::UploadRatio => "Ratio".to_string(), Self::UploadRatio => "Ratio",
Self::UploadedEver => "Uploaded".to_string(), Self::UploadedEver => "Uploaded",
Self::Wanted => "Wanted".to_string(), Self::Wanted => "Wanted",
Self::Webseeds => todo!(), Self::Webseeds => "Webseeds",
Self::WebseedsSendingToUs => "Webseeds Sending to Us".to_string(), Self::WebseedsSendingToUs => "Webseeds Sending to Us",
} }
.into()
} }
fn value(&self, torrent: &Torrent) -> String { fn value(&self, torrent: &Torrent) -> String {
match self { match self {
Self::ActivityDate => torrent.activity_date.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), Self::ActivityDate => torrent
Self::AddedDate => torrent.added_date.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), .activity_date
Self::Availability => todo!(), .map(|v| v.to_string())
Self::BandwidthPriority => todo!(), .unwrap_or_default(),
Self::Comment => todo!(), Self::AddedDate => torrent
Self::CorruptEver => todo!(), .added_date
Self::Creator => todo!(), .map(|v| v.to_string())
Self::DateCreated => todo!(), .unwrap_or_default(),
Self::DesiredAvailable => todo!(), Self::Availability => "N/A".to_string(),
Self::DoneDate => torrent.done_date.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), Self::BandwidthPriority => torrent
Self::DownloadDir => torrent.download_dir.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), .bandwidth_priority
Self::DownloadLimit => todo!(), .map(|v| format!("{:?}", v))
Self::DownloadLimited => todo!(), .unwrap_or_default(),
Self::DownloadedEver => todo!(), Self::Comment => torrent.comment.clone().unwrap_or_default(),
Self::EditDate => torrent.edit_date.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), 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 { Self::Error => match torrent.error {
Some(error) => match error { Some(error) => match error {
ErrorType::Ok => "Ok".to_string(), ErrorType::Ok => "Ok",
ErrorType::LocalError => "LocalError".to_string(), ErrorType::LocalError => "LocalError",
ErrorType::TrackerError => "TrackerError".to_string(), ErrorType::TrackerError => "TrackerError",
ErrorType::TrackerWarning => "TrackerWarning".to_string(), ErrorType::TrackerWarning => "TrackerWarning",
}, },
None => "N/A".to_string(), None => "N/A",
}, }
Self::ErrorString => torrent.error_string.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), .to_string(),
Self::ErrorString => torrent.error_string.clone().unwrap_or_default(),
Self::Eta => match torrent.eta { Self::Eta => match torrent.eta {
Some(eta) => match eta { Some(eta) => match eta {
-1 => "".to_string(), -1 => "".to_string(),
@ -138,184 +162,306 @@ impl Wrapper for TorrentGetField {
}, },
None => "".to_string(), None => "".to_string(),
}, },
Self::EtaIdle => todo!(), Self::EtaIdle => torrent.eta_idle.map(|v| v.to_string()).unwrap_or_default(),
Self::FileCount => todo!(), Self::FileCount => torrent
Self::FileStats => match &torrent.file_stats { .file_count
Some(file_stats) => file_stats .map(|v| v.to_string())
.iter() .unwrap_or_default(),
.map(|x| format!("{:?}", x.priority)) Self::FileStats => torrent
.collect(), .file_stats
None => "N/A".to_string(), .as_ref()
}, .map(|v| format!("{}", v.len()))
Self::Files => match &torrent.files { .unwrap_or_default(),
Some(files) => files.iter().map(|x| x.name.to_owned()).collect(), Self::Files => torrent
None => "N/A".to_string(), .files
}, .as_ref()
Self::Group => todo!(), .map(|v| format!("{}", v.len()))
Self::HashString => torrent.hash_string.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), .unwrap_or_default(),
Self::Group => torrent.group.clone().unwrap_or_default(),
Self::HashString => torrent.hash_string.clone().unwrap_or_default(),
Self::HaveUnchecked => todo!(), Self::HaveUnchecked => todo!(),
Self::HaveValid => todo!(), Self::HaveValid => FileSize::from(torrent.have_valid.unwrap_or(0)).to_string(),
Self::HonorsSessionLimits => todo!(), Self::HonorsSessionLimits => torrent
Self::Id => torrent.id.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), .honors_session_limits
Self::IsFinished => torrent.is_finished.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), .map(|v| v.to_string())
Self::IsPrivate => torrent.is_private.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), .unwrap_or_default(),
Self::IsStalled => torrent.is_stalled.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), Self::Id => torrent.id.map(|v| v.to_string()).unwrap_or_default(),
Self::Labels => torrent.labels.as_ref().map_or_else(|| "N/A".to_string(), |v| v.join(" ")), Self::IsFinished => torrent
Self::LeftUntilDone => FileSize::from(torrent.left_until_done.unwrap_or(0)).to_string(), .is_finished
Self::MagnetLink => todo!(), .map(|v| v.to_string())
Self::ManualAnnounceTime => todo!(), .unwrap_or_default(),
Self::MaxConnectedPeers => todo!(), Self::IsPrivate => torrent
Self::MetadataPercentComplete => torrent.metadata_percent_complete.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), .is_private
Self::Name => torrent.name.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), .map(|v| v.to_string())
Self::PeerLimit => todo!(), .unwrap_or_default(),
Self::Peers => todo!(), Self::IsStalled => torrent
Self::PeersConnected => torrent.peers_connected.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), .is_stalled
Self::PeersFrom => todo!(), .map(|v| v.to_string())
Self::PeersGettingFromUs => torrent.peers_getting_from_us.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), .unwrap_or_default(),
Self::PeersSendingToUs => torrent.peers_sending_to_us.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), Self::Labels => torrent.labels.clone().unwrap_or_default().join(", "),
Self::PercentComplete => todo!(), Self::LeftUntilDone => todo!(),
Self::PercentDone => match torrent.percent_done { Self::MagnetLink => torrent.magnet_link.clone().unwrap_or_default(),
Some(percent_done) => format!("{:.0}", percent_done * 100.0), Self::ManualAnnounceTime => torrent
None => "N/A".to_string(), .manual_announce_time
}, .map(|v| v.to_string())
Self::PieceCount => todo!(), .unwrap_or_default(),
Self::PieceSize => todo!(), Self::MaxConnectedPeers => torrent
Self::Pieces => todo!(), .max_connected_peers
Self::PrimaryMimeType => todo!(), .map(|v| v.to_string())
Self::Priorities => match &torrent.priorities { .unwrap_or_default(),
Some(priorities) => priorities.iter().map(|x| format!("{:?}", x)).collect(), Self::MetadataPercentComplete => torrent
None => "N/A".to_string(), .metadata_percent_complete
}, .map(|v| format!("{:.2}", v))
Self::QueuePosition => "N/A".to_string(), .unwrap_or_default(),
Self::RateDownload => NetSpeed::from(torrent.rate_download.unwrap_or(0)).to_string(), Self::Name => torrent.name.clone().unwrap_or_default(),
Self::RateUpload => NetSpeed::from(torrent.rate_upload.unwrap_or(0)).to_string(), Self::PeerLimit => torrent
Self::RecheckProgress => torrent.recheck_progress.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), .peer_limit
Self::SecondsDownloading => todo!(), .map(|v| v.to_string())
Self::SecondsSeeding => torrent.seconds_seeding.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), .unwrap_or_default(),
Self::SeedIdleLimit => todo!(), Self::Peers => torrent
Self::SeedIdleMode => todo!(), .peers
Self::SeedRatioLimit => torrent.seed_ratio_limit.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), .as_ref()
Self::SeedRatioMode => "N/A".to_string(), .map(|v| format!("{}", v.len()))
Self::SequentialDownload => todo!(), .unwrap_or_default(),
Self::SizeWhenDone => FileSize::from(torrent.size_when_done.unwrap_or(0)).to_string(), Self::PeersConnected => torrent
Self::StartDate => todo!(), .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 { Self::Status => match torrent.status {
Some(status) => match status { Some(status) => match status {
TorrentStatus::Stopped => "Stopped".to_string(), TorrentStatus::Stopped => "Stopped",
TorrentStatus::Seeding => "Seeding".to_string(), TorrentStatus::Seeding => "Seeding",
TorrentStatus::Verifying => "Verifying".to_string(), TorrentStatus::Verifying => "Verifying",
TorrentStatus::Downloading => "Downloading".to_string(), TorrentStatus::Downloading => "Downloading",
TorrentStatus::QueuedToSeed => "QueuedToSeed".to_string(), TorrentStatus::QueuedToSeed => "QueuedToSeed",
TorrentStatus::QueuedToVerify => "QueuedToVerify".to_string(), TorrentStatus::QueuedToVerify => "QueuedToVerify",
TorrentStatus::QueuedToDownload => "QueuedToDownload".to_string(), TorrentStatus::QueuedToDownload => "QueuedToDownload",
}, },
None => "N/A".to_string(), None => "N/A",
}, }
Self::TorrentFile => torrent.torrent_file.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), .to_string(),
Self::TotalSize => FileSize::from(torrent.total_size.unwrap_or(0)).to_string(), Self::TorrentFile => torrent.torrent_file.clone().unwrap_or_default(),
Self::TrackerList => todo!(), Self::TotalSize => FileSize::try_from(torrent.total_size.unwrap_or(0))
Self::TrackerStats => todo!(), .unwrap_or_default()
Self::Trackers => match &torrent.trackers { .to_string(),
Some(trackers) => trackers.iter().map(|x| x.announce.to_string()).collect(), Self::TrackerList => torrent.tracker_list.clone().unwrap_or_default(),
None => "N/A".to_string(), Self::TrackerStats => torrent
}, .tracker_stats
Self::UploadLimit => todo!(), .as_ref()
Self::UploadLimited => todo!(), .map(|v| format!("{}", v.len()))
Self::UploadRatio => match torrent.upload_ratio { .unwrap_or_default(),
Some(upload_ratio) => format!("{:.2}", upload_ratio), Self::Trackers => torrent
None => "N/A".to_string(), .trackers
}, .as_ref()
Self::UploadedEver => FileSize::from(torrent.uploaded_ever.unwrap_or(0)).to_string(), .map(|v| format!("{}", v.len()))
Self::Wanted => match &torrent.wanted { .unwrap_or_default(),
Some(wanted) => wanted.iter().map(|x| x.to_string()).collect(), Self::UploadLimit => NetSpeed::try_from(torrent.upload_limit.unwrap_or(0))
None => "N/A".to_string(), .unwrap_or_default()
}, .to_string(),
Self::Webseeds => todo!(), Self::UploadLimited => torrent
Self::WebseedsSendingToUs => "N/A".to_string(), .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 { fn width(&self) -> u16 {
match self { match self {
Self::ActivityDate => 10, Self::ActivityDate => 20,
Self::AddedDate => 10, Self::AddedDate => 20,
Self::Availability => todo!(), Self::Availability => 10,
Self::BandwidthPriority => todo!(), Self::BandwidthPriority => 10,
Self::Comment => todo!(), Self::Comment => 20,
Self::CorruptEver => todo!(), Self::CorruptEver => 15,
Self::Creator => todo!(), Self::Creator => 20,
Self::DateCreated => todo!(), Self::DateCreated => 20,
Self::DesiredAvailable => todo!(), Self::DesiredAvailable => 15,
Self::DoneDate => 10, Self::DoneDate => 20,
Self::DownloadDir => 30, Self::DownloadDir => 30,
Self::DownloadLimit => todo!(), Self::DownloadLimit => 15,
Self::DownloadLimited => todo!(), Self::DownloadLimited => 10,
Self::DownloadedEver => todo!(), Self::DownloadedEver => 15,
Self::EditDate => 10, Self::EditDate => 20,
Self::Error => 10, Self::Error => 15,
Self::ErrorString => 10, Self::ErrorString => 20,
Self::Eta => 10, Self::Eta => 10,
Self::EtaIdle => todo!(), Self::EtaIdle => 10,
Self::FileCount => todo!(), Self::FileCount => 10,
Self::FileStats => 10, Self::FileStats => 10,
Self::Files => 10, Self::Files => 10,
Self::Group => todo!(), Self::Group => 10,
Self::HashString => 10, Self::HashString => 42,
Self::HaveUnchecked => todo!(), Self::HaveUnchecked => 15,
Self::HaveValid => todo!(), Self::HaveValid => 15,
Self::HonorsSessionLimits => todo!(), Self::HonorsSessionLimits => 10,
Self::Id => 10, Self::Id => 5,
Self::IsFinished => 10, Self::IsFinished => 10,
Self::IsPrivate => 10, Self::IsPrivate => 10,
Self::IsStalled => 10, Self::IsStalled => 10,
Self::Labels => 10, Self::Labels => 20,
Self::LeftUntilDone => 10, Self::LeftUntilDone => 15,
Self::MagnetLink => todo!(), Self::MagnetLink => 50,
Self::ManualAnnounceTime => todo!(), Self::ManualAnnounceTime => 20,
Self::MaxConnectedPeers => todo!(), Self::MaxConnectedPeers => 10,
Self::MetadataPercentComplete => 10, Self::MetadataPercentComplete => 10,
Self::Name => 70, Self::Name => 70,
Self::PeerLimit => todo!(), Self::PeerLimit => 10,
Self::Peers => todo!(), Self::Peers => 10,
Self::PeersConnected => 10, Self::PeersConnected => 10,
Self::PeersFrom => todo!(), Self::PeersFrom => 20,
Self::PeersGettingFromUs => 10, Self::PeersGettingFromUs => 10,
Self::PeersSendingToUs => 10, Self::PeersSendingToUs => 10,
Self::PercentComplete => todo!(), Self::PercentComplete => 10,
Self::PercentDone => 10, Self::PercentDone => 10,
Self::PieceCount => todo!(), Self::PieceCount => 10,
Self::PieceSize => todo!(), Self::PieceSize => 15,
Self::Pieces => todo!(), Self::Pieces => 20,
Self::PrimaryMimeType => todo!(), Self::PrimaryMimeType => 20,
Self::Priorities => 10, Self::Priorities => 10,
Self::QueuePosition => 10, Self::QueuePosition => 10,
Self::RateDownload => 10, Self::RateDownload => 15,
Self::RateUpload => 10, Self::RateUpload => 15,
Self::RecheckProgress => 10, Self::RecheckProgress => 10,
Self::SecondsDownloading => todo!(), Self::SecondsDownloading => 15,
Self::SecondsSeeding => 10, Self::SecondsSeeding => 15,
Self::SeedIdleLimit => todo!(), Self::SeedIdleLimit => 10,
Self::SeedIdleMode => todo!(), Self::SeedIdleMode => 15,
Self::SeedRatioLimit => 10, Self::SeedRatioLimit => 10,
Self::SeedRatioMode => 10, Self::SeedRatioMode => 15,
Self::SequentialDownload => todo!(), Self::SequentialDownload => 10,
Self::SizeWhenDone => 10, Self::SizeWhenDone => 15,
Self::StartDate => todo!(), Self::StartDate => 20,
Self::Status => 15, Self::Status => 15,
Self::TorrentFile => 10, Self::TorrentFile => 30,
Self::TotalSize => 10, Self::TotalSize => 15,
Self::TrackerList => todo!(), Self::TrackerList => 30,
Self::TrackerStats => todo!(), Self::TrackerStats => 10,
Self::Trackers => 10, Self::Trackers => 10,
Self::UploadLimit => todo!(), Self::UploadLimit => 15,
Self::UploadLimited => todo!(), Self::UploadLimited => 10,
Self::UploadRatio => 10, Self::UploadRatio => 10,
Self::UploadedEver => 10, Self::UploadedEver => 15,
Self::Wanted => 10, Self::Wanted => 10,
Self::Webseeds => todo!(), Self::Webseeds => 20,
Self::WebseedsSendingToUs => 10, Self::WebseedsSendingToUs => 10,
} }
} }
} }

View File

@ -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<i64> for NetSpeed { #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
fn from(bytes_per_second: i64) -> Self { pub struct NetSpeed(u64);
NetSpeed(bytes_per_second)
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 { macro_rules! impl_from_unsigned {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { ($($t:ty),*) => {
let bytes_per_second = self.0; $(
if bytes_per_second < 1024 { impl From<$t> for NetSpeed {
write!(f, "{} B/s", bytes_per_second) fn from(value: $t) -> Self {
} else if bytes_per_second < 1024 * 1024 { Self(value as u64)
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)) )*
};
}
macro_rules! impl_try_from_signed {
($($t:ty),*) => {
$(
impl TryFrom<$t> for NetSpeed {
type Error = NetSpeedError;
fn try_from(value: $t) -> Result<Self, Self::Error> {
if value < 0 {
Err(NetSpeedError::NegativeSpeed { value: value as i64 })
} else { } else {
write!(f, "{:.2} GB/s", bytes_per_second as f64 / (1024.0 * 1024.0 * 1024.0)) 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])
}
}
#[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 })
);
}
}

View File

@ -20,7 +20,6 @@ pub enum Event {
/// Terminal event handler. /// Terminal event handler.
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
/// TODO: write description
pub struct EventHandler { pub struct EventHandler {
/// Event sender channel. /// Event sender channel.
sender: mpsc::Sender<Event>, sender: mpsc::Sender<Event>,

View File

@ -1,3 +1,8 @@
mod popup;
mod table;
use crate::app::{App, Tab};
use popup::render_popup;
use ratatui::{ use ratatui::{
layout::Alignment, layout::Alignment,
prelude::{Constraint, Direction, Layout}, prelude::{Constraint, Direction, Layout},
@ -6,12 +11,7 @@ use ratatui::{
widgets::{Block, BorderType, Borders, Clear, Tabs}, widgets::{Block, BorderType, Borders, Clear, Tabs},
Frame, Frame,
}; };
mod popup; use table::render_table;
mod table;
use crate::app::{App, Tab};
use self::{popup::render_popup, table::render_table};
/// Renders the user interface widgets. /// Renders the user interface widgets.
pub fn render(app: &mut App, frame: &mut Frame) { pub fn render(app: &mut App, frame: &mut Frame) {