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",
"crossterm 0.29.0",
"ratatui",
"thiserror",
"tokio",
"tracing",
"tracing-subscriber",

View File

@ -14,3 +14,4 @@ anyhow = "1.0"
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
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 std::{collections::HashSet, path::Path};
use transmission_rpc::types::{Torrent, TorrentAction, TorrentStatus};
impl Torrents {
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;
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);
}

View File

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

View File

@ -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.

View File

@ -1,5 +1,4 @@
use std::collections::HashSet;
use transmission_rpc::types::Id;
#[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 {
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<Self, Self::Error> {
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])
}
}
}
#[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 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,
}
}
}

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 {
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<Self, Self::Error> {
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])
}
}
}
#[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.
#[allow(dead_code)]
#[derive(Debug)]
/// TODO: write description
pub struct EventHandler {
/// Event sender channel.
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::{
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) {