mirror of
https://github.com/kristoferssolo/traxor.git
synced 2025-10-21 20:10:35 +00:00
refactor: improve rust idiomatic usage and error handling
This commit is contained in:
parent
b450d3abcf
commit
135a8a0c39
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
14
src/event.rs
14
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")
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
|
||||
12
src/log.rs
12
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(())
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}));
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
@ -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<_>>(),
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user