refactor: improve rust idiomatic usage and error handling

This commit is contained in:
Kristofers Solo 2025-06-30 21:52:37 +03:00
parent b450d3abcf
commit 135a8a0c39
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
16 changed files with 295 additions and 296 deletions

View File

@ -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<bool>) {
pub async fn move_dir(
&mut self,
torrent: &Torrent,
location: &Path,
move_from: Option<bool>,
) -> 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::<Vec<_>>();
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(())
}
}

View File

@ -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<Self> {
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)
}

View File

@ -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<str> 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()
}
}

View File

@ -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<Torrents> {
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)
}
}

View File

@ -11,7 +11,7 @@ pub enum Selected {
impl Into<HashSet<i64>> for Selected {
fn into(self) -> HashSet<i64> {
match self {
Selected::Current(id) => vec![id].into_iter().collect(),
Selected::Current(id) => std::iter::once(id).collect(),
Selected::List(ids) => ids,
}
}

View File

@ -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<i64> for FileSize {
fn from(size: i64) -> Self {
FileSize(size)
}
}

View File

@ -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<T: ToString>(option: Option<T>) -> String {
option.map_or_else(|| "N/A".into(), |val| val.to_string())
}

View File

@ -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<i64> for NetSpeed {
fn from(speed: i64) -> Self {
NetSpeed(speed)
}
}

View File

@ -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")
}

View File

@ -30,7 +30,7 @@ pub fn get_action(key_event: KeyEvent) -> Option<Action> {
}
/// 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(),
}

View File

@ -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(())
}

View File

@ -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) {

View File

@ -38,7 +38,9 @@ impl<B: Backend> Tui<B> {
// 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);
}));

View File

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

View File

@ -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),
)
}

View File

@ -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::<Vec<_>>(),
)