From 135a8a0c39f5e9fabecce79d22270dab8858682b Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Mon, 30 Jun 2025 21:52:37 +0300 Subject: [PATCH] refactor: improve rust idiomatic usage and error handling --- src/app/command.rs | 93 ++++++++-------- src/app/mod.rs | 39 +++---- src/app/tab.rs | 18 ++-- src/app/torrent.rs | 14 +-- src/app/types.rs | 2 +- src/app/utils/filesize.rs | 57 +++++----- src/app/utils/mod.rs | 220 +++++++++++++++++++------------------- src/app/utils/netspeed.rs | 56 +++++----- src/event.rs | 14 ++- src/handler.rs | 12 +-- src/log.rs | 12 ++- src/main.rs | 6 +- src/tui.rs | 4 +- src/ui/mod.rs | 14 ++- src/ui/popup.rs | 22 ++-- src/ui/table.rs | 8 +- 16 files changed, 295 insertions(+), 296 deletions(-) diff --git a/src/app/command.rs b/src/app/command.rs index e755683..9a408cd 100644 --- a/src/app/command.rs +++ b/src/app/command.rs @@ -1,35 +1,36 @@ use std::{collections::HashSet, path::Path}; -use tracing::error; + use transmission_rpc::types::{Torrent, TorrentAction, TorrentStatus}; use super::{types::Selected, Torrents}; impl Torrents { - pub async fn toggle(&mut self, ids: Selected) { + pub async fn toggle(&mut self, ids: Selected) -> anyhow::Result<()> { let ids: HashSet<_> = ids.into(); - let torrents = self.torrents.iter().filter(|torrent| { - if let Some(id) = torrent.id { - return ids.contains(&id); - } - false - }); + let torrents_to_toggle: Vec<_> = self + .torrents + .iter() + .filter(|torrent| torrent.id.map_or(false, |id| ids.contains(&id))) + .collect(); - for torrent in torrents { + for torrent in torrents_to_toggle { let action = match torrent.status { Some(TorrentStatus::Stopped) => TorrentAction::Start, _ => TorrentAction::Stop, }; if let Some(id) = torrent.id() { - if let Err(e) = self.client.torrent_action(action, vec![id]).await { - error!("{:?}", e); - } + self.client + .torrent_action(action, vec![id]) + .await + .map_err(|e| anyhow::anyhow!("Transmission RPC error: {}", e.to_string()))?; } } + Ok(()) } - pub async fn toggle_all(&mut self) { - let torrents: Vec<_> = self + pub async fn toggle_all(&mut self) -> anyhow::Result<()> { + let torrents_to_toggle: Vec<_> = self .torrents .iter() .filter_map(|torrent| { @@ -45,67 +46,67 @@ impl Torrents { }) .collect(); - for (id, action) in torrents { - if let Err(e) = self.client.torrent_action(action, vec![id]).await { - error!("{:?}", e); - } + for (id, action) in torrents_to_toggle { + self.client + .torrent_action(action, vec![id]) + .await + .map_err(|e| anyhow::anyhow!("Transmission RPC error: {}", e.to_string()))?; } + Ok(()) } - pub async fn start_all(&mut self) { - if let Err(e) = self.action_all(TorrentAction::StartNow).await { - error!("{:?}", e); - } + pub async fn start_all(&mut self) -> anyhow::Result<()> { + self.action_all(TorrentAction::StartNow).await } - pub async fn stop_all(&mut self) { - if let Err(e) = self.action_all(TorrentAction::Stop).await { - error!("{:?}", e); - } + pub async fn stop_all(&mut self) -> anyhow::Result<()> { + self.action_all(TorrentAction::Stop).await } - pub async fn move_dir(&mut self, torrent: &Torrent, location: &Path, move_from: Option) { + pub async fn move_dir( + &mut self, + torrent: &Torrent, + location: &Path, + move_from: Option, + ) -> anyhow::Result<()> { if let Some(id) = torrent.id() { - if let Err(e) = self - .client + self.client .torrent_set_location(vec![id], location.to_string_lossy().into(), move_from) .await - { - error!("{:?}", e); - } + .map_err(|e| anyhow::anyhow!("Transmission RPC error: {}", e.to_string()))?; } + Ok(()) } - pub async fn delete(&mut self, ids: Selected, delete_local_data: bool) { - if let Err(e) = self - .client + pub async fn delete(&mut self, ids: Selected, delete_local_data: bool) -> anyhow::Result<()> { + self.client .torrent_remove(ids.into(), delete_local_data) .await - { - error!("{:?}", e); - } + .map_err(|e| anyhow::anyhow!("Transmission RPC error: {}", e.to_string()))?; + Ok(()) } - pub async fn rename(&mut self, torrent: &Torrent, name: &Path) { + pub async fn rename(&mut self, torrent: &Torrent, name: &Path) -> anyhow::Result<()> { if let (Some(id), Some(old_name)) = (torrent.id(), torrent.name.clone()) { - if let Err(e) = self - .client + self.client .torrent_rename_path(vec![id], old_name, name.to_string_lossy().into()) .await - { - error!("{:?}", e); - } + .map_err(|e| anyhow::anyhow!("Transmission RPC error: {}", e.to_string()))?; } + Ok(()) } - async fn action_all(&mut self, action: TorrentAction) -> transmission_rpc::types::Result<()> { + async fn action_all(&mut self, action: TorrentAction) -> anyhow::Result<()> { let ids = self .torrents .iter() .filter_map(|torrent| torrent.id()) .collect::>(); - self.client.torrent_action(action, ids).await?; + self.client + .torrent_action(action, ids) + .await + .map_err(|e| anyhow::anyhow!("Transmission RPC error: {}", e.to_string()))?; Ok(()) } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 6fb8d57..7ba8cfa 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -25,20 +25,21 @@ pub struct App<'a> { impl<'a> App<'a> { /// Constructs a new instance of [`App`]. /// Returns instance of `Self`. - pub fn new() -> Self { - Self { + pub fn new() -> anyhow::Result { + Ok(Self { running: true, tabs: &[Tab::All, Tab::Active, Tab::Downloading], index: 0, state: TableState::default(), - torrents: Torrents::new(), + torrents: Torrents::new()?, // Handle the Result here show_popup: false, - } + }) } /// Handles the tick event of the terminal. - pub async fn tick(&mut self) { - self.torrents.update().await; + pub async fn tick(&mut self) -> anyhow::Result<()> { + self.torrents.update().await?; + Ok(()) } /// Set running to false to quit the application. @@ -120,16 +121,18 @@ impl<'a> App<'a> { self.show_popup = true; } - pub async fn toggle_torrents(&mut self) { + pub async fn toggle_torrents(&mut self) -> anyhow::Result<()> { let ids = self.selected(false); - self.torrents.toggle(ids).await; + self.torrents.toggle(ids).await?; self.close_popup(); + Ok(()) } - pub async fn delete(&mut self, delete_local_data: bool) { + pub async fn delete(&mut self, delete_local_data: bool) -> anyhow::Result<()> { let ids = self.selected(false); - self.torrents.delete(ids, delete_local_data).await; + self.torrents.delete(ids, delete_local_data).await?; self.close_popup(); + Ok(()) } pub fn select(&mut self) { @@ -146,24 +149,14 @@ impl<'a> App<'a> { fn selected(&self, highlighted: bool) -> Selected { let torrents = &self.torrents.torrents; if self.torrents.selected.is_empty() || highlighted { - let torrent_id = || { - let idx = self.state.selected()?; - let torrent = torrents.get(idx)?; - torrent.id - }; - if let Some(id) = 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); } } let selected_torrents = torrents .iter() - .filter_map(|torrent| { - let id = torrent.id; - if self.torrents.selected.contains(&id?) { - return id; - } - None - }) + .filter_map(|torrent| torrent.id.filter(|id| self.torrents.selected.contains(id))) .collect(); Selected::List(selected_torrents) } diff --git a/src/app/tab.rs b/src/app/tab.rs index f8cc21b..835f1d7 100644 --- a/src/app/tab.rs +++ b/src/app/tab.rs @@ -49,12 +49,18 @@ impl Tab { } } -impl ToString for Tab { - fn to_string(&self) -> String { - match *self { - Tab::All => String::from("All"), - Tab::Active => String::from("Active"), - Tab::Downloading => String::from("Downloading"), +impl AsRef for Tab { + fn as_ref(&self) -> &str { + match self { + Tab::All => "All", + Tab::Active => "Active", + Tab::Downloading => "Downloading", } } } + +impl ToString for Tab { + fn to_string(&self) -> String { + self.as_ref().into() + } +} diff --git a/src/app/torrent.rs b/src/app/torrent.rs index d2a2f93..aa928eb 100644 --- a/src/app/torrent.rs +++ b/src/app/torrent.rs @@ -19,14 +19,14 @@ pub struct Torrents { impl Torrents { /// Constructs a new instance of [`Torrents`]. - pub fn new() -> Torrents { - let url = Url::parse("http://localhost:9091/transmission/rpc").unwrap(); - Self { + pub fn new() -> Result { + let url = Url::parse("http://localhost:9091/transmission/rpc")?; + Ok(Self { client: TransClient::new(url), torrents: Vec::new(), selected: HashSet::new(), fields: None, - } + }) } /// Returns the number of [`Torrent`]s in [`Torrents`] @@ -47,15 +47,15 @@ impl Torrents { } /// Updates [`Torrent`] values. - pub async fn update(&mut self) -> &mut Self { + pub async fn update(&mut self) -> Result<&mut Self> { self.torrents = self .client .torrent_get(self.fields.clone(), None) .await - .unwrap() + .map_err(|e| anyhow::anyhow!("Transmission RPC error: {}", e.to_string()))? .arguments .torrents; - self + Ok(self) } } diff --git a/src/app/types.rs b/src/app/types.rs index ef7f659..ec6038a 100644 --- a/src/app/types.rs +++ b/src/app/types.rs @@ -11,7 +11,7 @@ pub enum Selected { impl Into> for Selected { fn into(self) -> HashSet { match self { - Selected::Current(id) => vec![id].into_iter().collect(), + Selected::Current(id) => std::iter::once(id).collect(), Selected::List(ids) => ids, } } diff --git a/src/app/utils/filesize.rs b/src/app/utils/filesize.rs index 94ce8e5..c197d3d 100644 --- a/src/app/utils/filesize.rs +++ b/src/app/utils/filesize.rs @@ -1,38 +1,31 @@ +use std::fmt; + +const KB: f64 = 1e3; +const MB: f64 = 1e6; +const GB: f64 = 1e9; +const TB: f64 = 1e12; + pub struct FileSize(pub i64); -impl FileSize { - pub fn to_b(&self) -> String { - format!("{} b", self.0) - } - - pub fn to_kb(&self) -> String { - format!("{:.2} KB", self.0 as f64 / 1e3) - } - - pub fn to_mb(&self) -> String { - format!("{:.2} MB", self.0 as f64 / 1e6) - } - - pub fn to_gb(&self) -> String { - format!("{:.2} GB", self.0 as f64 / 1e9) - } - - pub fn to_tb(&self) -> String { - format!("{:.2} TB", self.0 as f64 / 1e12) - } -} - -impl ToString for FileSize { - fn to_string(&self) -> String { +impl fmt::Display for FileSize { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.0 == 0 { - return "0".to_string(); - } - match self.0 as f64 { - b if b >= 1e12 => self.to_tb(), - b if b >= 1e9 => self.to_gb(), - b if b >= 1e6 => self.to_mb(), - b if b >= 1e3 => self.to_kb(), - _ => self.to_b(), + return write!(f, "0"); } + let size = self.0 as f64; + let (value, unit) = match size { + s if s >= TB => (s / TB, "TB"), + s if s >= GB => (s / GB, "GB"), + s if s >= MB => (s / MB, "MB"), + s if s >= KB => (s / KB, "KB"), + _ => (size, "B"), + }; + write!(f, "{:.2} {}", value, unit) + } +} + +impl From for FileSize { + fn from(size: i64) -> Self { + FileSize(size) } } diff --git a/src/app/utils/mod.rs b/src/app/utils/mod.rs index 8be2d06..f9a847f 100644 --- a/src/app/utils/mod.rs +++ b/src/app/utils/mod.rs @@ -1,17 +1,18 @@ +use transmission_rpc::types::{ErrorType, Torrent, TorrentGetField, TorrentStatus}; + +mod filesize; mod netspeed; -use transmission_rpc::types::{ErrorType, Torrent, TorrentGetField, TorrentStatus}; -mod filesize; -use filesize::FileSize; -use netspeed::NetSpeed; +use crate::app::utils::filesize::FileSize; +use crate::app::utils::netspeed::NetSpeed; pub trait Wrapper { fn title(&self) -> String { - String::from("") + "".to_string() } - fn value(&self, torrent: Torrent) -> String { - format!("{}", torrent.name.unwrap_or(String::from(""))) + fn value(&self, torrent: &Torrent) -> String { + format!("{}", torrent.name.as_ref().unwrap_or(&String::from(""))) } fn width(&self) -> u16 { @@ -22,8 +23,8 @@ pub trait Wrapper { impl Wrapper for TorrentGetField { fn title(&self) -> String { match self { - Self::ActivityDate => String::from("Activity Date"), - Self::AddedDate => String::from("Added Date"), + Self::ActivityDate => "Activity Date".to_string(), + Self::AddedDate => "Added Date".to_string(), Self::Availability => todo!(), Self::BandwidthPriority => todo!(), Self::Comment => todo!(), @@ -31,81 +32,81 @@ impl Wrapper for TorrentGetField { Self::Creator => todo!(), Self::DateCreated => todo!(), Self::DesiredAvailable => todo!(), - Self::DoneDate => String::from("Done Date"), - Self::DownloadDir => String::from("Path"), + Self::DoneDate => "Done Date".to_string(), + Self::DownloadDir => "Path".to_string(), Self::DownloadLimit => todo!(), Self::DownloadLimited => todo!(), Self::DownloadedEver => todo!(), - Self::EditDate => String::from("Edit Date"), - Self::Error => String::from("Error Type"), - Self::ErrorString => String::from("Error String"), - Self::Eta => String::from("ETA"), + 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 => String::from("File Stats"), - Self::Files => String::from("Files"), + Self::FileStats => "File Stats".to_string(), + Self::Files => "Files".to_string(), Self::Group => todo!(), - Self::HashString => String::from("Hash String"), + Self::HashString => "Hash String".to_string(), Self::HaveUnchecked => todo!(), Self::HaveValid => todo!(), Self::HonorsSessionLimits => todo!(), - Self::Id => String::from("Id"), - Self::IsFinished => String::from("Finished"), - Self::IsPrivate => String::from("Private"), - Self::IsStalled => String::from("Stalled"), - Self::Labels => String::from("Labels"), - Self::LeftUntilDone => String::from("Left Until Done"), + 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 => String::from("Metadata Percent Complete"), - Self::Name => String::from("Name"), + Self::MetadataPercentComplete => "Metadata Percent Complete".to_string(), + Self::Name => "Name".to_string(), Self::PeerLimit => todo!(), Self::Peers => todo!(), - Self::PeersConnected => String::from("Connected"), + Self::PeersConnected => "Connected".to_string(), Self::PeersFrom => todo!(), - Self::PeersGettingFromUs => String::from("Peers"), - Self::PeersSendingToUs => String::from("Seeds"), + Self::PeersGettingFromUs => "Peers".to_string(), + Self::PeersSendingToUs => "Seeds".to_string(), Self::PercentComplete => todo!(), - Self::PercentDone => String::from("%"), + Self::PercentDone => "%".to_string(), Self::PieceCount => todo!(), Self::PieceSize => todo!(), Self::Pieces => todo!(), Self::PrimaryMimeType => todo!(), - Self::Priorities => String::from("Priorities"), - Self::QueuePosition => String::from("Queue"), - Self::RateDownload => String::from("Download Speed"), - Self::RateUpload => String::from("Upload Speed"), - Self::RecheckProgress => String::from("Progress"), + 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 => String::from("Seconds Seeding"), + Self::SecondsSeeding => "Seconds Seeding".to_string(), Self::SeedIdleLimit => todo!(), Self::SeedIdleMode => todo!(), - Self::SeedRatioLimit => String::from("Seed Ratio Limit"), - Self::SeedRatioMode => String::from("Seed Ratio Mode"), + Self::SeedRatioLimit => "Seed Ratio Limit".to_string(), + Self::SeedRatioMode => "Seed Ratio Mode".to_string(), Self::SequentialDownload => todo!(), - Self::SizeWhenDone => String::from("Size"), + Self::SizeWhenDone => "Size".to_string(), Self::StartDate => todo!(), - Self::Status => String::from("Status"), - Self::TorrentFile => String::from("Torrent File"), - Self::TotalSize => String::from("Total Size"), + 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 => String::from("Trackers"), + Self::Trackers => "Trackers".to_string(), Self::UploadLimit => todo!(), Self::UploadLimited => todo!(), - Self::UploadRatio => String::from("Ratio"), - Self::UploadedEver => String::from("Uploaded"), - Self::Wanted => String::from("Wanted"), + Self::UploadRatio => "Ratio".to_string(), + Self::UploadedEver => "Uploaded".to_string(), + Self::Wanted => "Wanted".to_string(), Self::Webseeds => todo!(), - Self::WebseedsSendingToUs => String::from("Webseeds Sending to Us"), + Self::WebseedsSendingToUs => "Webseeds Sending to Us".to_string(), } } - fn value(&self, torrent: Torrent) -> String { + fn value(&self, torrent: &Torrent) -> String { match self { - Self::ActivityDate => optional_to_string(torrent.activity_date), - Self::AddedDate => optional_to_string(torrent.added_date), + 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!(), @@ -113,127 +114,124 @@ impl Wrapper for TorrentGetField { Self::Creator => todo!(), Self::DateCreated => todo!(), Self::DesiredAvailable => todo!(), - Self::DoneDate => optional_to_string(torrent.done_date), - Self::DownloadDir => optional_to_string(torrent.download_dir), + 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 => optional_to_string(torrent.edit_date), + Self::EditDate => torrent.edit_date.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), Self::Error => match torrent.error { Some(error) => match error { - ErrorType::Ok => String::from("Ok"), - ErrorType::LocalError => String::from("LocalError"), - ErrorType::TrackerError => String::from("TrackerError"), - ErrorType::TrackerWarning => String::from("TrackerWarning"), + ErrorType::Ok => "Ok".to_string(), + ErrorType::LocalError => "LocalError".to_string(), + ErrorType::TrackerError => "TrackerError".to_string(), + ErrorType::TrackerWarning => "TrackerWarning".to_string(), }, - None => String::from("N/A"), + None => "N/A".to_string(), }, - Self::ErrorString => optional_to_string(torrent.error_string), + Self::ErrorString => torrent.error_string.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), Self::Eta => match torrent.eta { Some(eta) => match eta { - -1 => "".into(), - -2 => "?".into(), + -1 => "".to_string(), + -2 => "?".to_string(), _ => format!("{} s", eta), }, - None => String::from(""), + None => "".to_string(), }, Self::EtaIdle => todo!(), Self::FileCount => todo!(), - Self::FileStats => match torrent.file_stats { + Self::FileStats => match &torrent.file_stats { Some(file_stats) => file_stats .iter() .map(|x| format!("{:?}", x.priority)) .collect(), - None => String::from("N/A"), + None => "N/A".to_string(), }, - Self::Files => match torrent.files { + Self::Files => match &torrent.files { Some(files) => files.iter().map(|x| x.name.to_owned()).collect(), - None => String::from("N/A"), + None => "N/A".to_string(), }, Self::Group => todo!(), - Self::HashString => optional_to_string(torrent.hash_string), + Self::HashString => torrent.hash_string.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), Self::HaveUnchecked => todo!(), Self::HaveValid => todo!(), Self::HonorsSessionLimits => todo!(), - Self::Id => optional_to_string(torrent.id), - Self::IsFinished => optional_to_string(torrent.is_finished), - Self::IsPrivate => optional_to_string(torrent.is_private), - Self::IsStalled => optional_to_string(torrent.is_stalled), - Self::Labels => match torrent.labels { - Some(labels) => labels.join(" "), - None => String::from("N/A"), - }, - Self::LeftUntilDone => FileSize(torrent.left_until_done.unwrap_or(0)).to_string(), + 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 => optional_to_string(torrent.metadata_percent_complete), - Self::Name => optional_to_string(torrent.name), + 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 => optional_to_string(torrent.peers_connected), + Self::PeersConnected => torrent.peers_connected.as_ref().map_or_else(|| "N/A".to_string(), |v| v.to_string()), Self::PeersFrom => todo!(), - Self::PeersGettingFromUs => optional_to_string(torrent.peers_getting_from_us), - Self::PeersSendingToUs => optional_to_string(torrent.peers_sending_to_us), + 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 => String::from("N/A"), + None => "N/A".to_string(), }, Self::PieceCount => todo!(), Self::PieceSize => todo!(), Self::Pieces => todo!(), Self::PrimaryMimeType => todo!(), - Self::Priorities => match torrent.priorities { + Self::Priorities => match &torrent.priorities { Some(priorities) => priorities.iter().map(|x| format!("{:?}", x)).collect(), - None => String::from("N/A"), + None => "N/A".to_string(), }, - Self::QueuePosition => String::from("N/A"), - Self::RateDownload => NetSpeed(torrent.rate_download.unwrap_or(0)).to_string(), - Self::RateUpload => NetSpeed(torrent.rate_upload.unwrap_or(0)).to_string(), - Self::RecheckProgress => optional_to_string(torrent.recheck_progress), + 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 => optional_to_string(torrent.seconds_seeding), + 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 => optional_to_string(torrent.seed_ratio_limit), - Self::SeedRatioMode => String::from("N/A"), + 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(torrent.size_when_done.unwrap_or(0)).to_string(), + Self::SizeWhenDone => FileSize::from(torrent.size_when_done.unwrap_or(0)).to_string(), Self::StartDate => todo!(), Self::Status => match torrent.status { Some(status) => match status { - TorrentStatus::Stopped => String::from("Stopped"), - TorrentStatus::Seeding => String::from("Seeding"), - TorrentStatus::Verifying => String::from("Verifying"), - TorrentStatus::Downloading => String::from("Downloading"), - TorrentStatus::QueuedToSeed => String::from("QueuedToSeed"), - TorrentStatus::QueuedToVerify => String::from("QueuedToVerify"), - TorrentStatus::QueuedToDownload => String::from("QueuedToDownload"), + 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(), }, - None => String::from("N/A"), + None => "N/A".to_string(), }, - Self::TorrentFile => optional_to_string(torrent.torrent_file), - Self::TotalSize => FileSize(torrent.total_size.unwrap_or(0)).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 { + Self::Trackers => match &torrent.trackers { Some(trackers) => trackers.iter().map(|x| x.announce.to_string()).collect(), - None => String::from("N/A"), + 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 => String::from("N/A"), + None => "N/A".to_string(), }, - Self::UploadedEver => FileSize(torrent.uploaded_ever.unwrap_or(0)).to_string(), - Self::Wanted => match torrent.wanted { + 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 => String::from("N/A"), + None => "N/A".to_string(), }, Self::Webseeds => todo!(), - Self::WebseedsSendingToUs => String::from("N/A"), + Self::WebseedsSendingToUs => "N/A".to_string(), } } @@ -320,6 +318,4 @@ impl Wrapper for TorrentGetField { } } -fn optional_to_string(option: Option) -> String { - option.map_or_else(|| "N/A".into(), |val| val.to_string()) -} + diff --git a/src/app/utils/netspeed.rs b/src/app/utils/netspeed.rs index 6156f10..980761e 100644 --- a/src/app/utils/netspeed.rs +++ b/src/app/utils/netspeed.rs @@ -1,33 +1,35 @@ +use std::fmt; + +const KBPS: f64 = 1_000.0; +const MBPS: f64 = 1_000_000.0; +const GBPS: f64 = 1_000_000_000.0; + pub struct NetSpeed(pub i64); -impl NetSpeed { - pub fn to_bps(&self) -> String { - format!("{} bps", self.0) - } - - pub fn to_kbps(&self) -> String { - format!("{:.2} kbps", self.0 as f64 / 1e3) - } - - pub fn to_mbps(&self) -> String { - format!("{:.2} mbps", self.0 as f64 / 1e6) - } - - pub fn to_gbps(&self) -> String { - format!("{:.2} gbps", self.0 as f64 / 1e9) - } -} - -impl ToString for NetSpeed { - fn to_string(&self) -> String { - if self.0 == 0 { - return "0".to_string(); +impl fmt::Display for NetSpeed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let speed = self.0 as f64; + if speed == 0.0 { + return write!(f, "0 bps"); } - match self.0 as f64 { - b if b >= 1e9 => self.to_gbps(), - b if b >= 1e6 => self.to_mbps(), - b if b >= 1e3 => self.to_kbps(), - _ => self.to_bps(), + + let (value, unit) = match speed { + s if s >= GBPS => (s / GBPS, "Gbps"), + s if s >= MBPS => (s / MBPS, "Mbps"), + s if s >= KBPS => (s / KBPS, "kbps"), + _ => (speed, "bps"), + }; + + if unit == "bps" { + write!(f, "{:.0} {}", value, unit) + } else { + write!(f, "{:.2} {}", value, unit) } } } + +impl From for NetSpeed { + fn from(speed: i64) -> Self { + NetSpeed(speed) + } +} diff --git a/src/event.rs b/src/event.rs index d63a67a..39d6cc6 100644 --- a/src/event.rs +++ b/src/event.rs @@ -45,11 +45,15 @@ impl EventHandler { .unwrap_or(tick_rate); if event::poll(timeout).expect("no events available") { - match event::read().expect("unable to read event") { - CrosstermEvent::Key(e) => sender.send(Event::Key(e)), - CrosstermEvent::Mouse(e) => sender.send(Event::Mouse(e)), - CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)), - _ => unimplemented!(), + match event::read() { + Ok(CrosstermEvent::Key(e)) => sender.send(Event::Key(e)), + Ok(CrosstermEvent::Mouse(e)) => sender.send(Event::Mouse(e)), + Ok(CrosstermEvent::Resize(w, h)) => sender.send(Event::Resize(w, h)), + Err(e) => { + eprintln!("Error reading event: {:?}", e); + break; + } + _ => Ok(()), // Ignore other events } .expect("failed to send terminal event") } diff --git a/src/handler.rs b/src/handler.rs index 476bfe7..3944ff0 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -30,7 +30,7 @@ pub fn get_action(key_event: KeyEvent) -> Option { } /// Handles the updates of [`App`]. -pub async fn update(app: &mut App<'_>, action: Action) -> transmission_rpc::types::Result<()> { +pub async fn update(app: &mut App<'_>, action: Action) -> anyhow::Result<()> { match action { Action::Quit => app.quit(), Action::NextTab => app.next_tab(), @@ -39,12 +39,12 @@ pub async fn update(app: &mut App<'_>, action: Action) -> transmission_rpc::type Action::PrevTorrent => app.previous(), Action::SwitchTab(x) => app.switch_tab(x as usize), Action::TogglePopup => app.toggle_popup(), - Action::ToggleTorrent => app.toggle_torrents().await, - Action::ToggleAll => app.torrents.toggle_all().await, - Action::PauseAll => app.torrents.stop_all().await, - Action::StartAll => app.torrents.start_all().await, + Action::ToggleTorrent => app.toggle_torrents().await?, + Action::ToggleAll => app.torrents.toggle_all().await?, + Action::PauseAll => app.torrents.stop_all().await?, + Action::StartAll => app.torrents.start_all().await?, Action::Move => unimplemented!(), - Action::Delete(x) => app.delete(x).await, + Action::Delete(x) => app.delete(x).await?, Action::Rename => unimplemented!(), Action::Select => app.select(), } diff --git a/src/log.rs b/src/log.rs index a54f446..478d8ca 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,15 +1,17 @@ use std::{fs::File, path::PathBuf, str::FromStr}; +use anyhow::Result; use tracing::Level; use tracing_subscriber::fmt; -pub fn setup_logger() { - std::fs::create_dir_all(".logs").unwrap(); - let path = PathBuf::from_str(".logs/traxor.log").unwrap(); - let log_file = File::create(path).expect("Failed to create log file"); +pub fn setup_logger() -> Result<()> { + std::fs::create_dir_all(".logs")?; + let path = PathBuf::from_str(".logs/traxor.log")?; + let log_file = File::create(path)?; let subscriber = fmt::Subscriber::builder() .with_max_level(Level::TRACE) .with_writer(log_file) .finish(); - tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); + tracing::subscriber::set_global_default(subscriber)?; + Ok(()) } diff --git a/src/main.rs b/src/main.rs index 9f3ee00..e39d5ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,10 +12,10 @@ mod log; #[tokio::main] async fn main() -> Result<()> { // Setup the logger. - setup_logger(); + setup_logger()?; // Create an application. - let mut app = App::new(); + let mut app = App::new()?; // Initialize the terminal user interface. let backend = CrosstermBackend::new(io::stderr()); @@ -30,7 +30,7 @@ async fn main() -> Result<()> { tui.draw(&mut app)?; // Handle events. match tui.events.next()? { - Event::Tick => app.tick().await, + Event::Tick => app.tick().await?, // Event::Key(key_event) => handle_key_events(key_event, &mut app).await?, Event::Key(key_event) => { if let Some(action) = get_action(key_event) { diff --git a/src/tui.rs b/src/tui.rs index 293de6f..1dd0681 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -38,7 +38,9 @@ impl Tui { // This way, you won't have your terminal messed up if an unexpected error happens. let panic_hook = panic::take_hook(); panic::set_hook(Box::new(move |panic| { - Self::reset().expect("failed to reset the terminal"); + if let Err(e) = Self::reset() { + eprintln!("Error resetting terminal: {:?}", e); + } panic_hook(panic); })); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index c7d3b6e..739d2e6 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -45,11 +45,15 @@ pub fn render(app: &mut App, frame: &mut Frame) { frame.render_widget(tabs, chunks[0]); // renders tab - let table = match app.index() { - 0 => render_table(app, Tab::All), - 1 => render_table(app, Tab::Active), - 2 => render_table(app, Tab::Downloading), - _ => unreachable!(), + let table = if app.index() == 0 { + render_table(app, Tab::All) + } else if app.index() == 1 { + render_table(app, Tab::Active) + } else if app.index() == 2 { + render_table(app, Tab::Downloading) + } else { + // Fallback or handle error, though unreachable!() implies this won't happen + render_table(app, Tab::All) // Default to Tab::All if index is unexpected }; frame.render_stateful_widget(table, chunks[1], &mut app.state); // renders table diff --git a/src/ui/popup.rs b/src/ui/popup.rs index 386bf67..aa74ad2 100644 --- a/src/ui/popup.rs +++ b/src/ui/popup.rs @@ -1,17 +1,13 @@ -use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::layout::Rect; pub fn render_popup(r: Rect) -> Rect { - let percent_y = 20; - let popup_layput = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage(100 - percent_y), - Constraint::Percentage(percent_y), - ]) - .split(r); + let vertical_margin = r.height / 5; + let horizontal_margin = r.width / 5; - Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(0), Constraint::Percentage(100)]) - .split(popup_layput[1])[1] + Rect::new( + r.x + horizontal_margin, + r.y + vertical_margin, + r.width - (2 * horizontal_margin), + r.height - (2 * vertical_margin), + ) } diff --git a/src/ui/table.rs b/src/ui/table.rs index 6f04584..0627e15 100644 --- a/src/ui/table.rs +++ b/src/ui/table.rs @@ -19,12 +19,12 @@ pub fn render_table<'a>(app: &mut App, tab: Tab) -> Table<'a> { fields .iter() .map(|&field| { - if let Some(id) = &torrent.clone().id { - if selected.contains(id) { - return field.value(torrent.clone()).set_style(highlight_style); + if let Some(id) = torrent.id { + if selected.contains(&id) { + return field.value(torrent).set_style(highlight_style); } } - field.value(torrent.clone()).into() + field.value(torrent).into() }) .collect::>(), )