From 8cff913f43281bb09ffbea0ccd70935a52d4b21f Mon Sep 17 00:00:00 2001 From: micielski <73398428+micielski@users.noreply.github.com> Date: Fri, 5 Jul 2024 17:12:23 +0000 Subject: [PATCH 01/10] fix: resize not triggering render (#57) --- rm-shared/src/action.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rm-shared/src/action.rs b/rm-shared/src/action.rs index 56cfa1a..af86c06 100644 --- a/rm-shared/src/action.rs +++ b/rm-shared/src/action.rs @@ -92,6 +92,7 @@ pub fn event_to_action( } keymap.get(&(key.code, key.modifiers)).cloned() } + Event::Resize(_, _) => Some(A::Render), _ => None, } } From f5c14ade8d09eed6295cd88e163b5360ce85c3a1 Mon Sep 17 00:00:00 2001 From: micielski <73398428+micielski@users.noreply.github.com> Date: Sat, 6 Jul 2024 11:55:24 +0000 Subject: [PATCH 02/10] docs: revamp README (#56) --- README.md | 63 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ee920eb..1bff434 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,62 @@ + + +**Rustmission** + +Performant TUI client for Transmission daemon capable of managing hundreds of torrents. +It boasts a rich feature set that surpasses many other clients, offering a seamless torrenting experience :3 + + +#
-

Rustmission

+

- TUI for the Transmission daemon -

- -

- ⚠️ I DO NOT PIRATE MOVIES. THE TORRENTS YOU SEE IN THIS IMAGE ARE SAMPLE DATA FABRICATED BY ARTIFICIAL INTELLIGENCE. I DO NOT CONDONE PIRACY, AND I WOULD NEVER, EVER DO SUCH THING + ⚠️ Torrents you see are just samples fabricated by AI. Piracy is not something we tolerate here at Intuis.

## Features -- **Built-in Search**: Seamlessly search for magnets directly. This is a killer feature of Rustmission. -- **Async Rust**: Utilizes Rust's async/await syntax for efficient, non-blocking IO operations. -- **Basic Operations**: Allows to add, pause, remove, fuzzy filter your torrents. +- **Basic operations**: Allows to add, pause, remove, fuzzy filter your torrents. +- **Built-in magnet search**: Search for magnets without leaving your terminal. +- **Asynchronous**: UI is always responsive. +- **RSS**: Fetch torrents automatically with a cronjob using `--fetch-rss` + +## Requirements + +- Running [Transmission](https://github.com/transmission/transmission) daemon and its IP address +- [Nerd Fonts](https://www.nerdfonts.com/) ## Installation + + Packaging status + + To install Rustmission, ensure you have Rust and Cargo installed on your system, and then run: ```bash cargo install rustmission ``` -or with Nix: +or with Nix ( :heart: [@0x61nas](https://github.com/0x61nas) ): ```bash -nix run . +nix-shell -p rustmission ``` -or with Brew: +or with Brew ( :heart: [@aidanaden](https://github.com/aidanaden) ): ```bash brew install intuis/tap/rustmission ``` ## Usage -Launch Rustmission in your terminal to initialize the configuration and make adjustments as needed. Subsequently, run Rustmission again. For a list of keybindings, press '?'. +Run `rustmission` in your terminal to initialize the config and make adjustments as needed. Subsequently, run `rustmission` again. For a list of keybindings, press `?` or `F1`. ## Configuration -Rustmission stores its configuration in a TOML file located at ~/.config/rustmission/config.toml by default. You can modify this file to -set the daemon's IP address. +Rustmission stores its configuration in a TOML file located at `~/.config/rustmission/config.toml` by default. You can modify this file to +set the daemon's address. ```toml [general] @@ -53,18 +68,28 @@ auto_hide = false # It can also be a hex, e.g. "#3cb371" accent_color = "LightMagenta" -# If enabled, shows various keybindings throughout the program at the cost of a -# little bit cluttered interface. +# If enabled, shows various keybindings throughout the program at the cost of +# a little bit cluttered interface. beginner_mode = true +# If enabled, hides header row of torrents tab +headers_hide = false + [connection] url = "http://CHANGE_ME:9091/transmission/rpc" # REQUIRED! # If you need username and password to authenticate: # username = "CHANGE_ME" # password = "CHANGE_ME" + +# Refresh timings (in seconds) +torrents_refresh = 5 +stats_refresh = 10 +free_space_refresh = 10 ``` +There's also a self-documenting keymap config located at `~/.config/rustmission/keymap.toml` with sane defaults. + ## Alternatives - [Transgression](https://github.com/PanAeon/transg-tui) - [tremc](https://github.com/tremc/tremc) @@ -72,5 +97,5 @@ url = "http://CHANGE_ME:9091/transmission/rpc" # REQUIRED! - [stig](https://github.com/rndusr/stig) ## Contributing - -Contributions are welcome! If you'd like to contribute to Rustmission, please fork the repository, make your changes, and submit a pull request! +If you'd like to contribute make sure you fork [this repo](https://github.com/intuis/rustmission) and submit a PR! +If you want to implement something major, create an issue first so it can be discussed. From 5709159bdf423dfb7ed232dd25a34003a025bf6a Mon Sep 17 00:00:00 2001 From: micielski <73398428+micielski@users.noreply.github.com> Date: Mon, 8 Jul 2024 17:41:45 +0000 Subject: [PATCH 03/10] feat: configurable headers (#55) --- Cargo.lock | 54 ++++++++ Cargo.toml | 1 + README.md | 16 ++- rm-config/Cargo.toml | 1 + rm-config/defaults/config.toml | 16 ++- rm-config/src/lib.rs | 4 +- rm-config/src/main_config.rs | 112 ++++++++++++++-- rm-config/src/utils.rs | 4 +- rm-main/Cargo.toml | 1 + rm-main/src/transmission/fetchers.rs | 4 + rm-main/src/ui/tabs/torrents/bottom_stats.rs | 2 +- rm-main/src/ui/tabs/torrents/mod.rs | 6 +- .../ui/tabs/torrents/rustmission_torrent.rs | 126 ++++++++++++++---- rm-main/src/ui/tabs/torrents/table_manager.rs | 114 +++++++++------- rm-main/src/utils.rs | 4 +- 15 files changed, 359 insertions(+), 106 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc26bd3..1dd394f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.14" @@ -222,7 +237,12 @@ version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", ] [[package]] @@ -839,6 +859,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1444,6 +1487,7 @@ dependencies = [ "serde", "thiserror", "toml", + "transmission-rpc", "url", "xdg", ] @@ -1570,6 +1614,7 @@ version = "0.3.3" dependencies = [ "anyhow", "base64 0.22.1", + "chrono", "clap", "crossterm", "futures", @@ -2344,6 +2389,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 9aef960..260402c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ rss = "2" reqwest = "0.12" regex = "1" thiserror = "1" +chrono = "0.4" # Async tokio = { version = "1", features = ["macros", "sync"] } diff --git a/README.md b/README.md index 1bff434..b58c21e 100644 --- a/README.md +++ b/README.md @@ -78,14 +78,22 @@ headers_hide = false [connection] url = "http://CHANGE_ME:9091/transmission/rpc" # REQUIRED! +# Refresh timings (in seconds) +torrents_refresh = 5 +stats_refresh = 5 +free_space_refresh = 10 + # If you need username and password to authenticate: # username = "CHANGE_ME" # password = "CHANGE_ME" -# Refresh timings (in seconds) -torrents_refresh = 5 -stats_refresh = 10 -free_space_refresh = 10 +[torrents_tab] +# Available fields: +# Id, Name, SizeWhenDone, Progress, DownloadRate, UploadRate, DownloadDir, +# Padding, UploadRatio, UploadedEver, AddedDate, ActivityDate, PeersConnected +# SmallStatus +headers = ["Name", "SizeWhenDone", "Progress", "DownloadRate", "UploadRate"] + ``` There's also a self-documenting keymap config located at `~/.config/rustmission/keymap.toml` with sane defaults. diff --git a/rm-config/Cargo.toml b/rm-config/Cargo.toml index e1ab5f9..f903d13 100644 --- a/rm-config/Cargo.toml +++ b/rm-config/Cargo.toml @@ -20,3 +20,4 @@ url.workspace = true ratatui.workspace = true crossterm.workspace = true thiserror.workspace = true +transmission-rpc.workspace = true diff --git a/rm-config/defaults/config.toml b/rm-config/defaults/config.toml index 5cce726..7d87ef1 100644 --- a/rm-config/defaults/config.toml +++ b/rm-config/defaults/config.toml @@ -17,11 +17,19 @@ headers_hide = false [connection] url = "http://CHANGE_ME:9091/transmission/rpc" # REQUIRED! +# Refresh timings (in seconds) +torrents_refresh = 5 +stats_refresh = 5 +free_space_refresh = 10 + # If you need username and password to authenticate: # username = "CHANGE_ME" # password = "CHANGE_ME" -# Refresh timings (in seconds) -torrents_refresh = 5 -stats_refresh = 10 -free_space_refresh = 10 + +[torrents_tab] +# Available fields: +# Id, Name, SizeWhenDone, Progress, DownloadRate, UploadRate, DownloadDir, +# Padding, UploadRatio, UploadedEver, AddedDate, ActivityDate, PeersConnected +# SmallStatus +headers = ["Name", "SizeWhenDone", "Progress", "DownloadRate", "UploadRate"] diff --git a/rm-config/src/lib.rs b/rm-config/src/lib.rs index a2e3a8b..59347e3 100644 --- a/rm-config/src/lib.rs +++ b/rm-config/src/lib.rs @@ -1,5 +1,5 @@ pub mod keymap; -mod main_config; +pub mod main_config; mod utils; use std::path::PathBuf; @@ -11,6 +11,7 @@ use main_config::MainConfig; pub struct Config { pub general: main_config::General, pub connection: main_config::Connection, + pub torrents_tab: main_config::TorrentsTab, pub keybindings: KeymapConfig, pub directories: Directories, } @@ -33,6 +34,7 @@ impl Config { Ok(Self { general: main_config.general, connection: main_config.connection, + torrents_tab: main_config.torrents_tab, keybindings: keybindings.clone(), directories, }) diff --git a/rm-config/src/main_config.rs b/rm-config/src/main_config.rs index 2a45893..50f85fe 100644 --- a/rm-config/src/main_config.rs +++ b/rm-config/src/main_config.rs @@ -1,19 +1,21 @@ -use std::{path::PathBuf, sync::OnceLock}; +use std::{io::ErrorKind, path::PathBuf, sync::OnceLock}; use anyhow::Result; -use ratatui::style::Color; +use ratatui::{layout::Constraint, style::Color}; use serde::{Deserialize, Serialize}; use url::Url; -use crate::utils::{self, put_config}; +use crate::utils::{self}; -#[derive(Serialize, Deserialize)] +#[derive(Deserialize)] pub struct MainConfig { pub general: General, pub connection: Connection, + #[serde(default)] + pub torrents_tab: TorrentsTab, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Deserialize)] pub struct General { #[serde(default)] pub auto_hide: bool, @@ -33,7 +35,7 @@ fn default_beginner_mode() -> bool { true } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Deserialize)] pub struct Connection { pub username: Option, pub password: Option, @@ -50,19 +52,103 @@ fn default_refresh() -> u64 { 5 } +#[derive(Serialize, Deserialize, Hash, PartialEq, Eq, Clone, Copy)] +pub enum Header { + Name, + SizeWhenDone, + Progress, + Eta, + DownloadRate, + UploadRate, + DownloadDir, + Padding, + UploadRatio, + UploadedEver, + Id, + ActivityDate, + AddedDate, + PeersConnected, + SmallStatus, +} + +impl Header { + pub fn default_constraint(&self) -> Constraint { + match self { + Self::Name => Constraint::Max(70), + Self::SizeWhenDone => Constraint::Length(12), + Self::Progress => Constraint::Length(12), + Self::Eta => Constraint::Length(12), + Self::DownloadRate => Constraint::Length(12), + Self::UploadRate => Constraint::Length(12), + Self::DownloadDir => Constraint::Max(70), + Self::Padding => Constraint::Length(2), + Self::UploadRatio => Constraint::Length(6), + Self::UploadedEver => Constraint::Length(12), + Self::Id => Constraint::Length(4), + Self::ActivityDate => Constraint::Length(14), + Self::AddedDate => Constraint::Length(12), + Self::PeersConnected => Constraint::Length(6), + Self::SmallStatus => Constraint::Length(1), + } + } + + pub fn header_name(&self) -> &'static str { + match *self { + Self::Name => "Name", + Self::SizeWhenDone => "Size", + Self::Progress => "Progress", + Self::Eta => "ETA", + Self::DownloadRate => "Download", + Self::UploadRate => "Upload", + Self::DownloadDir => "Directory", + Self::Padding => "", + Self::UploadRatio => "Ratio", + Self::UploadedEver => "Up Ever", + Self::Id => "Id", + Self::ActivityDate => "Last active", + Self::AddedDate => "Added", + Self::PeersConnected => "Peers", + Self::SmallStatus => "", + } + } +} + +#[derive(Deserialize)] +pub struct TorrentsTab { + pub headers: Vec
, +} + +impl Default for TorrentsTab { + fn default() -> Self { + Self { + headers: vec![ + Header::Name, + Header::SizeWhenDone, + Header::Progress, + Header::Eta, + Header::DownloadRate, + Header::UploadRate, + ], + } + } +} + impl MainConfig { pub(crate) const FILENAME: &'static str = "config.toml"; const DEFAULT_CONFIG: &'static str = include_str!("../defaults/config.toml"); pub(crate) fn init() -> Result { - let Ok(config) = utils::fetch_config(Self::FILENAME) else { - put_config(Self::DEFAULT_CONFIG, Self::FILENAME)?; - // TODO: check if the user really changed the config. - println!("Update {:?} and start rustmission again", Self::path()); - std::process::exit(0); + match utils::fetch_config::(Self::FILENAME) { + Ok(config) => return Ok(config), + Err(e) => match e { + utils::ConfigFetchingError::Io(e) if e.kind() == ErrorKind::NotFound => { + utils::put_config::(Self::DEFAULT_CONFIG, Self::FILENAME)?; + println!("Update {:?} and start rustmission again", Self::path()); + std::process::exit(0); + } + _ => anyhow::bail!(e), + }, }; - - Ok(config) } pub(crate) fn path() -> &'static PathBuf { diff --git a/rm-config/src/utils.rs b/rm-config/src/utils.rs index 102afde..3654cdb 100644 --- a/rm-config/src/utils.rs +++ b/rm-config/src/utils.rs @@ -42,9 +42,9 @@ pub fn fetch_config(config_name: &str) -> Result( content: &'static str, filename: &str, -) -> Result { +) -> Result { let config_path = get_config_path(filename); let mut config_file = File::create(config_path)?; config_file.write_all(content.as_bytes())?; - Ok(toml::from_str(content).expect("default configs are correct")) + Ok(toml::from_str(content)?) } diff --git a/rm-main/Cargo.toml b/rm-main/Cargo.toml index d518e11..05d531f 100644 --- a/rm-main/Cargo.toml +++ b/rm-main/Cargo.toml @@ -34,3 +34,4 @@ rss.workspace = true reqwest.workspace = true regex.workspace = true throbber-widgets-tui.workspace = true +chrono.workspace = true diff --git a/rm-main/src/transmission/fetchers.rs b/rm-main/src/transmission/fetchers.rs index 45fc025..4181c78 100644 --- a/rm-main/src/transmission/fetchers.rs +++ b/rm-main/src/transmission/fetchers.rs @@ -72,6 +72,10 @@ pub async fn torrents(ctx: app::Ctx, table_manager: Arc>) { TorrentGetField::RateDownload, TorrentGetField::Status, TorrentGetField::DownloadDir, + TorrentGetField::UploadedEver, + TorrentGetField::ActivityDate, + TorrentGetField::AddedDate, + TorrentGetField::PeersConnected, ]; let rpc_response = ctx .client diff --git a/rm-main/src/ui/tabs/torrents/bottom_stats.rs b/rm-main/src/ui/tabs/torrents/bottom_stats.rs index 717c06c..eec8ab7 100644 --- a/rm-main/src/ui/tabs/torrents/bottom_stats.rs +++ b/rm-main/src/ui/tabs/torrents/bottom_stats.rs @@ -40,7 +40,7 @@ impl Component for BottomStats { let download = bytes_to_human_format(stats.download_speed); let upload = bytes_to_human_format(stats.upload_speed); - let mut text = format!("▼ {download} | ▲ {upload}"); + let mut text = format!(" {download} |  {upload}"); if let Some(free_space) = &*self.free_space.lock().unwrap() { let free_space = bytes_to_human_format(free_space.size_bytes); diff --git a/rm-main/src/ui/tabs/torrents/mod.rs b/rm-main/src/ui/tabs/torrents/mod.rs index 167cb75..b5d69fb 100644 --- a/rm-main/src/ui/tabs/torrents/mod.rs +++ b/rm-main/src/ui/tabs/torrents/mod.rs @@ -121,12 +121,10 @@ impl TorrentsTab { .accent_color); let table_widget = { - let table = Table::new(torrent_rows, table_manager_lock.widths) + let table = Table::new(torrent_rows, &table_manager_lock.widths) .highlight_style(highlight_table_style); if !self.ctx.config.general.headers_hide { - table.header(Row::new( - table_manager_lock.header().iter().map(|s| s.as_str()), - )) + table.header(Row::new(table_manager_lock.header().iter().cloned())) } else { table } diff --git a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs index d5b5e6a..7dd0137 100644 --- a/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs +++ b/rm-main/src/ui/tabs/torrents/rustmission_torrent.rs @@ -1,8 +1,10 @@ +use chrono::{Datelike, NaiveDateTime}; use ratatui::{ style::{Style, Stylize}, text::{Line, Span}, widgets::Row, }; +use rm_config::main_config::Header; use transmission_rpc::types::{Id, Torrent, TorrentStatus}; use crate::utils::{ @@ -17,31 +19,31 @@ pub struct RustmissionTorrent { pub eta_secs: String, pub download_speed: String, pub upload_speed: String, + pub uploaded_ever: String, + pub upload_ratio: String, status: TorrentStatus, pub style: Style, pub id: Id, pub download_dir: String, + pub activity_date: NaiveDateTime, + pub added_date: NaiveDateTime, + pub peers_connected: i64, } impl RustmissionTorrent { - pub fn to_row(&self) -> ratatui::widgets::Row { - Row::new([ - Line::from(self.torrent_name.as_str()), - Line::from(""), - Line::from(self.size_when_done.as_str()), - Line::from(self.progress.as_str()), - Line::from(self.eta_secs.as_str()), - Line::from(download_speed_format(&self.download_speed)), - Line::from(upload_speed_format(&self.upload_speed)), - Line::from(self.download_dir.as_str()), - ]) - .style(self.style) + pub fn to_row(&self, headers: &Vec
) -> ratatui::widgets::Row { + headers + .iter() + .map(|header| self.header_to_line(*header)) + .collect::() + .style(self.style) } pub fn to_row_with_higlighted_indices( &self, highlighted_indices: Vec, highlight_style: Style, + headers: &Vec
, ) -> ratatui::widgets::Row { let mut torrent_name_line = Line::default(); @@ -53,16 +55,54 @@ impl RustmissionTorrent { } } - Row::new([ - Line::from(torrent_name_line), - Line::from(""), - Line::from(self.size_when_done.as_str()), - Line::from(self.progress.as_str()), - Line::from(self.eta_secs.as_str()), - Line::from(download_speed_format(&self.download_speed)), - Line::from(upload_speed_format(&self.upload_speed)), - Line::from(self.download_dir.as_str()), - ]) + let mut cells = vec![]; + + for header in headers { + if *header == Header::Name { + cells.push(Line::from(torrent_name_line.clone())) + } else { + cells.push(self.header_to_line(*header)) + } + } + + Row::new(cells) + } + + fn header_to_line(&self, header: Header) -> Line { + match header { + Header::Name => Line::from(self.torrent_name.as_str()), + Header::SizeWhenDone => Line::from(self.size_when_done.as_str()), + Header::Progress => Line::from(self.progress.as_str()), + Header::Eta => Line::from(self.eta_secs.as_str()), + Header::DownloadRate => Line::from(download_speed_format(&self.download_speed)), + Header::UploadRate => Line::from(upload_speed_format(&self.upload_speed)), + Header::DownloadDir => Line::from(self.download_dir.as_str()), + Header::Padding => Line::raw(""), + Header::Id => match &self.id { + Id::Id(id) => Line::from(id.to_string()), + Id::Hash(hash) => Line::from(hash.as_str()), + }, + Header::UploadRatio => Line::from(self.upload_ratio.as_str()), + Header::UploadedEver => Line::from(self.uploaded_ever.as_str()), + Header::ActivityDate => time_to_line(self.activity_date), + Header::AddedDate => time_to_line(self.added_date), + Header::PeersConnected => Line::from(self.peers_connected.to_string()), + Header::SmallStatus => match self.status() { + TorrentStatus::Stopped => Line::from("󰏤"), + TorrentStatus::QueuedToVerify => Line::from("󱥸"), + TorrentStatus::Verifying => Line::from("󰑓"), + TorrentStatus::QueuedToDownload => Line::from("󱥸"), + TorrentStatus::QueuedToSeed => Line::from("󱥸"), + TorrentStatus::Seeding => { + if !self.upload_speed.is_empty() { + Line::from("") + } else { + Line::from("󰄬") + } + } + TorrentStatus::Downloading => Line::from(""), + }, + } } pub const fn status(&self) -> TorrentStatus { @@ -116,10 +156,30 @@ impl From<&Torrent> for RustmissionTorrent { _ => Style::default(), }; - let download_dir = t - .download_dir - .clone() - .expect("torrent download directory requested"); + let download_dir = t.download_dir.clone().expect("field requested"); + + let uploaded_ever = bytes_to_human_format(t.uploaded_ever.expect("field requested")); + + let upload_ratio = { + let raw = t.upload_ratio.expect("field requested"); + format!("{:.1}", raw) + }; + + let activity_date = { + let raw = t.activity_date.expect("field requested"); + chrono::DateTime::from_timestamp(raw, 0) + .unwrap() + .naive_local() + }; + + let added_date = { + let raw = t.added_date.expect("field requested"); + chrono::DateTime::from_timestamp(raw, 0) + .unwrap() + .naive_local() + }; + + let peers_connected = t.peers_connected.expect("field requested"); Self { torrent_name, @@ -132,6 +192,20 @@ impl From<&Torrent> for RustmissionTorrent { style, id, download_dir, + uploaded_ever, + upload_ratio, + activity_date, + added_date, + peers_connected, } } } + +fn time_to_line<'a>(time: NaiveDateTime) -> Line<'a> { + let today = chrono::Local::now(); + if time.year() == today.year() && time.month() == today.month() && time.day() == today.day() { + Line::from(time.format("Today %H:%M").to_string()) + } else { + Line::from(time.format("%y|%m|%d %H:%M").to_string()) + } +} diff --git a/rm-main/src/ui/tabs/torrents/table_manager.rs b/rm-main/src/ui/tabs/torrents/table_manager.rs index f340efa..48df59f 100644 --- a/rm-main/src/ui/tabs/torrents/table_manager.rs +++ b/rm-main/src/ui/tabs/torrents/table_manager.rs @@ -1,6 +1,10 @@ use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use ratatui::{prelude::*, widgets::Row}; -use std::sync::{Arc, Mutex}; +use rm_config::main_config::Header; +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; use crate::{app, ui::components::table::GenericTable}; @@ -9,31 +13,27 @@ use super::rustmission_torrent::RustmissionTorrent; pub struct TableManager { ctx: app::Ctx, pub table: GenericTable, - pub widths: [Constraint; 8], + pub widths: Vec, pub filter: Arc>>, pub torrents_displaying_no: u16, - header: Vec, + headers: Vec<&'static str>, } impl TableManager { pub fn new(ctx: app::Ctx, table: GenericTable) -> Self { - let widths = Self::default_widths(); + let widths = Self::default_widths(&ctx.config.torrents_tab.headers); + let mut headers = vec![]; + for header in &ctx.config.torrents_tab.headers { + headers.push(header.header_name()); + } + Self { ctx, table, widths, filter: Arc::new(Mutex::new(None)), torrents_displaying_no: 0, - header: vec![ - "Name".to_owned(), - "".to_owned(), - "Size".to_owned(), - "Progress".to_owned(), - "ETA".to_owned(), - "Download".to_owned(), - "Upload".to_owned(), - "Directory".to_owned(), - ], + headers, } } @@ -46,13 +46,13 @@ impl TableManager { self.table .items .iter() - .map(RustmissionTorrent::to_row) + .map(|t| t.to_row(&self.ctx.config.torrents_tab.headers)) .collect() } } - pub const fn header(&self) -> &Vec { - &self.header + pub const fn header(&self) -> &Vec<&'static str> { + &self.headers } pub fn current_torrent(&mut self) -> Option<&mut RustmissionTorrent> { @@ -94,61 +94,77 @@ impl TableManager { for torrent in torrents { if let Some((_, indices)) = matcher.fuzzy_indices(&torrent.torrent_name, filter) { - rows.push(torrent.to_row_with_higlighted_indices(indices, highlight_style)) + rows.push(torrent.to_row_with_higlighted_indices( + indices, + highlight_style, + &self.ctx.config.torrents_tab.headers, + )) } } rows } - const fn default_widths() -> [Constraint; 8] { - [ - Constraint::Max(70), // Name - Constraint::Length(5), // - Constraint::Length(12), // Size - Constraint::Length(12), // Progress - Constraint::Length(12), // ETA - Constraint::Length(12), // Download - Constraint::Length(12), // Upload - Constraint::Max(70), // Download directory - ] + fn default_widths(headers: &Vec
) -> Vec { + let mut constraints = vec![]; + + for header in headers { + constraints.push(header.default_constraint()) + } + constraints } - fn header_widths(&self, rows: &[RustmissionTorrent]) -> [Constraint; 8] { + fn header_widths(&self, rows: &[RustmissionTorrent]) -> Vec { + let headers = &self.ctx.config.torrents_tab.headers; + if !self.ctx.config.general.auto_hide { - return Self::default_widths(); + return Self::default_widths(&headers); } - let mut download_width = 0; - let mut upload_width = 0; - let mut progress_width = 0; - let mut eta_width = 0; + let mut map = HashMap::new(); + + for header in headers { + map.insert(header, header.default_constraint()); + } + + let hidable_headers = [ + Header::Progress, + Header::UploadRate, + Header::DownloadRate, + Header::Eta, + ]; + + for hidable_header in &hidable_headers { + map.entry(hidable_header) + .and_modify(|c| *c = Constraint::Length(0)); + } for row in rows { if !row.download_speed.is_empty() { - download_width = 11; + map.entry(&Header::DownloadRate) + .and_modify(|c| *c = Header::DownloadRate.default_constraint()); } if !row.upload_speed.is_empty() { - upload_width = 11; + map.entry(&Header::UploadRate) + .and_modify(|c| *c = Header::UploadRate.default_constraint()); } if !row.progress.is_empty() { - progress_width = 11; + map.entry(&Header::Progress) + .and_modify(|c| *c = Header::Progress.default_constraint()); } if !row.eta_secs.is_empty() { - eta_width = 11; + map.entry(&Header::Eta) + .and_modify(|c| *c = Header::Eta.default_constraint()); } } - [ - Constraint::Max(70), // Name - Constraint::Length(5), // - Constraint::Length(11), // Size - Constraint::Length(progress_width), // Progress - Constraint::Length(eta_width), // ETA - Constraint::Length(download_width), // Download - Constraint::Length(upload_width), // Upload - Constraint::Max(70), // Download directory - ] + let mut constraints = vec![]; + + for header in headers { + constraints.push(map.remove(header).expect("this header exists")) + } + + constraints } } diff --git a/rm-main/src/utils.rs b/rm-main/src/utils.rs index 992c984..f9c2677 100644 --- a/rm-main/src/utils.rs +++ b/rm-main/src/utils.rs @@ -64,14 +64,14 @@ pub fn seconds_to_human_format(seconds: i64) -> String { pub fn download_speed_format(download_speed: &str) -> String { if download_speed.len() > 0 { - return format!("▼ {}", download_speed); + return format!(" {}", download_speed); } download_speed.to_string() } pub fn upload_speed_format(upload_speed: &str) -> String { if upload_speed.len() > 0 { - return format!("▲ {}", upload_speed); + return format!(" {}", upload_speed); } upload_speed.to_string() } From 4cb3c0ece502d820ddcb7a597f164bd50ec1c8c6 Mon Sep 17 00:00:00 2001 From: micielski <73398428+micielski@users.noreply.github.com> Date: Mon, 8 Jul 2024 18:18:11 +0000 Subject: [PATCH 04/10] fix: allow not specifying headers in torrents tab config (#60) --- rm-config/src/main_config.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/rm-config/src/main_config.rs b/rm-config/src/main_config.rs index 50f85fe..ba9f63b 100644 --- a/rm-config/src/main_config.rs +++ b/rm-config/src/main_config.rs @@ -115,20 +115,25 @@ impl Header { #[derive(Deserialize)] pub struct TorrentsTab { + #[serde(default = "default_headers")] pub headers: Vec
, } +fn default_headers() -> Vec
{ + vec![ + Header::Name, + Header::SizeWhenDone, + Header::Progress, + Header::Eta, + Header::DownloadRate, + Header::UploadRate, + ] +} + impl Default for TorrentsTab { fn default() -> Self { Self { - headers: vec![ - Header::Name, - Header::SizeWhenDone, - Header::Progress, - Header::Eta, - Header::DownloadRate, - Header::UploadRate, - ], + headers: default_headers(), } } } From 1064c1a05cff038c09b6188283954c7134743187 Mon Sep 17 00:00:00 2001 From: aidan Date: Tue, 9 Jul 2024 02:18:47 +0800 Subject: [PATCH 05/10] release: 0.4.0 (#54) --- Cargo.lock | 6 +++--- Cargo.toml | 6 +++--- rm-main/Cargo.toml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1dd394f..de34d9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1478,7 +1478,7 @@ dependencies = [ [[package]] name = "rm-config" -version = "0.3.3" +version = "0.4.0" dependencies = [ "anyhow", "crossterm", @@ -1494,7 +1494,7 @@ dependencies = [ [[package]] name = "rm-shared" -version = "0.3.3" +version = "0.4.0" dependencies = [ "crossterm", ] @@ -1610,7 +1610,7 @@ dependencies = [ [[package]] name = "rustmission" -version = "0.3.3" +version = "0.4.0" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index 260402c..e0a4702 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ ] [workspace.package] -version = "0.3.3" +version = "0.4.0" edition = "2021" authors = ["Remigiusz Micielski "] repository = "https://github.com/intuis/rustmission" @@ -14,8 +14,8 @@ homepage = "https://github.com/intuis/rustmission" license = "GPL-3.0-or-later" [workspace.dependencies] -rm-config = { version = "0.3", path = "rm-config" } -rm-shared = { version = "0.3", path = "rm-shared" } +rm-config = { version = "0.4", path = "rm-config" } +rm-shared = { version = "0.4", path = "rm-shared" } magnetease = "0.1" anyhow = "1" diff --git a/rm-main/Cargo.toml b/rm-main/Cargo.toml index 05d531f..877187e 100644 --- a/rm-main/Cargo.toml +++ b/rm-main/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustmission" -version = "0.3.3" +version = "0.4.0" edition = "2021" description = "TUI for Transmission daemon" repository = "https://github.com/intuis/rustmission" From 4009d8473bd7d3edef6bcadb2ddb1c9bc24da2f3 Mon Sep 17 00:00:00 2001 From: micielski <73398428+micielski@users.noreply.github.com> Date: Mon, 8 Jul 2024 18:32:18 +0000 Subject: [PATCH 06/10] chore: add desc to rm-shared (#61) --- rm-shared/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/rm-shared/Cargo.toml b/rm-shared/Cargo.toml index 66fa6de..15c7740 100644 --- a/rm-shared/Cargo.toml +++ b/rm-shared/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "rm-shared" +description = "shared things for rustmission" version.workspace = true edition.workspace = true authors.workspace = true From b3ecc27288b06c88151b50792456da0589440ed6 Mon Sep 17 00:00:00 2001 From: micielski <73398428+micielski@users.noreply.github.com> Date: Mon, 8 Jul 2024 19:03:00 +0000 Subject: [PATCH 07/10] ci: remove musl from targets (#62) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e0a4702..94565a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ installers = ["shell", "homebrew"] # A GitHub repo to push Homebrew formulas to tap = "intuis/homebrew-tap" # Target platforms to build apps for (Rust target-triple syntax) -targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl"] +targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu"] # Publish jobs to run in CI publish-jobs = ["homebrew"] # Publish jobs to run in CI From 27953f80c9e7d5784fd3a6fbbfdc796714f82734 Mon Sep 17 00:00:00 2001 From: micielski <73398428+micielski@users.noreply.github.com> Date: Mon, 8 Jul 2024 19:12:49 +0000 Subject: [PATCH 08/10] release: 0.4.1 (#63) --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- rm-main/Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de34d9b..0094619 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1478,7 +1478,7 @@ dependencies = [ [[package]] name = "rm-config" -version = "0.4.0" +version = "0.4.1" dependencies = [ "anyhow", "crossterm", @@ -1494,7 +1494,7 @@ dependencies = [ [[package]] name = "rm-shared" -version = "0.4.0" +version = "0.4.1" dependencies = [ "crossterm", ] @@ -1610,7 +1610,7 @@ dependencies = [ [[package]] name = "rustmission" -version = "0.4.0" +version = "0.4.1" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index 94565a2..809b68b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ ] [workspace.package] -version = "0.4.0" +version = "0.4.1" edition = "2021" authors = ["Remigiusz Micielski "] repository = "https://github.com/intuis/rustmission" diff --git a/rm-main/Cargo.toml b/rm-main/Cargo.toml index 877187e..dda7e44 100644 --- a/rm-main/Cargo.toml +++ b/rm-main/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustmission" -version = "0.4.0" +version = "0.4.1" edition = "2021" description = "TUI for Transmission daemon" repository = "https://github.com/intuis/rustmission" From ccc14f162d28bf969b7a0a50cf637df7c25c4ca0 Mon Sep 17 00:00:00 2001 From: micielski <73398428+micielski@users.noreply.github.com> Date: Mon, 8 Jul 2024 19:42:00 +0000 Subject: [PATCH 09/10] release: 0.4.3 (#64) --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- rm-main/Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0094619..86e577d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1478,7 +1478,7 @@ dependencies = [ [[package]] name = "rm-config" -version = "0.4.1" +version = "0.4.3" dependencies = [ "anyhow", "crossterm", @@ -1494,7 +1494,7 @@ dependencies = [ [[package]] name = "rm-shared" -version = "0.4.1" +version = "0.4.3" dependencies = [ "crossterm", ] @@ -1610,7 +1610,7 @@ dependencies = [ [[package]] name = "rustmission" -version = "0.4.1" +version = "0.4.3" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index 809b68b..e1fe58a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ ] [workspace.package] -version = "0.4.1" +version = "0.4.3" edition = "2021" authors = ["Remigiusz Micielski "] repository = "https://github.com/intuis/rustmission" diff --git a/rm-main/Cargo.toml b/rm-main/Cargo.toml index dda7e44..9c13280 100644 --- a/rm-main/Cargo.toml +++ b/rm-main/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustmission" -version = "0.4.1" +version = "0.4.3" edition = "2021" description = "TUI for Transmission daemon" repository = "https://github.com/intuis/rustmission" From c234a37f5687b872483e1581a36b781c7a73a467 Mon Sep 17 00:00:00 2001 From: aidan Date: Sun, 14 Jul 2024 15:06:49 +0800 Subject: [PATCH 10/10] feat: add progress status for moving torrents (#66) --- rm-main/src/transmission/action.rs | 25 +++++++++++-------- rm-main/src/ui/tabs/torrents/task_manager.rs | 1 + .../ui/tabs/torrents/tasks/move_torrent.rs | 6 ++--- rm-main/src/ui/tabs/torrents/tasks/status.rs | 12 +++++++++ rm-shared/src/status_task.rs | 1 + 5 files changed, 31 insertions(+), 14 deletions(-) diff --git a/rm-main/src/transmission/action.rs b/rm-main/src/transmission/action.rs index 4e8b956..895c072 100644 --- a/rm-main/src/transmission/action.rs +++ b/rm-main/src/transmission/action.rs @@ -127,23 +127,26 @@ pub async fn action_handler(ctx: app::Ctx, mut trans_rx: UnboundedReceiver { - if let Err(e) = ctx + match ctx .client .lock() .await .torrent_set_location(ids, new_directory.clone(), Option::from(true)) .await { - let error_title = "Failed to move torrent"; - let msg = "Failed to move torrent to new directory:\n\"".to_owned() - + new_directory.as_str() - + "\"\n" - + &e.to_string(); - let error_message = ErrorMessage { - title: error_title.to_string(), - message: msg, - }; - ctx.send_action(Action::Error(Box::new(error_message))); + Ok(_) => ctx.send_action(Action::TaskSuccess), + Err(e) => { + let error_title = "Failed to move torrent"; + let msg = "Failed to move torrent to new directory:\n\"".to_owned() + + new_directory.as_str() + + "\"\n" + + &e.to_string(); + let error_message = ErrorMessage { + title: error_title.to_string(), + message: msg, + }; + ctx.send_action(Action::Error(Box::new(error_message))); + } } } } diff --git a/rm-main/src/ui/tabs/torrents/task_manager.rs b/rm-main/src/ui/tabs/torrents/task_manager.rs index 09648f6..7f60c13 100644 --- a/rm-main/src/ui/tabs/torrents/task_manager.rs +++ b/rm-main/src/ui/tabs/torrents/task_manager.rs @@ -89,6 +89,7 @@ impl Component for TaskManager { _ => None, }, CurrentTask::MoveBar(move_bar) => match move_bar.handle_actions(action) { + Some(A::TaskPending(task)) => self.pending_task(task), Some(A::Quit) => self.cancel_task(), Some(A::Render) => Some(A::Render), _ => None, diff --git a/rm-main/src/ui/tabs/torrents/tasks/move_torrent.rs b/rm-main/src/ui/tabs/torrents/tasks/move_torrent.rs index a947ffc..f0cb237 100644 --- a/rm-main/src/ui/tabs/torrents/tasks/move_torrent.rs +++ b/rm-main/src/ui/tabs/torrents/tasks/move_torrent.rs @@ -1,6 +1,6 @@ use crossterm::event::{KeyCode, KeyEvent}; use ratatui::prelude::*; -use rm_shared::action::Action; +use rm_shared::{action::Action, status_task::StatusTask}; use transmission_rpc::types::Id; use crate::{ @@ -31,8 +31,8 @@ impl MoveBar { let new_location = self.input_mgr.text(); let torrents_to_move = self.torrents_to_move.clone(); self.ctx - .send_torrent_action(TorrentAction::Move(torrents_to_move, new_location)); - return Some(Action::Quit); + .send_torrent_action(TorrentAction::Move(torrents_to_move, new_location.clone())); + return Some(Action::TaskPending(StatusTask::Move(new_location))); } if input.code == KeyCode::Esc { diff --git a/rm-main/src/ui/tabs/torrents/tasks/status.rs b/rm-main/src/ui/tabs/torrents/tasks/status.rs index efc9da3..d07a830 100644 --- a/rm-main/src/ui/tabs/torrents/tasks/status.rs +++ b/rm-main/src/ui/tabs/torrents/tasks/status.rs @@ -40,6 +40,10 @@ impl Component for StatusBar { let display_name = format_display_name(&name); format!("Deleting {display_name}") } + StatusTask::Move(name) => { + let display_name = format_display_name(&name); + format!("Moving to {display_name}") + } }; let default_throbber = throbber_widgets_tui::Throbber::default() .label(status_text) @@ -61,6 +65,10 @@ impl Component for StatusBar { let display_name = format_display_name(&name); format!(" Error deleting {display_name}") } + StatusTask::Move(name) => { + let display_name = format_display_name(&name); + format!(" Error moving to {display_name}") + } }, CurrentTaskState::Success(_) => match &self.task { StatusTask::Add(name) => { @@ -71,6 +79,10 @@ impl Component for StatusBar { let display_name = format_display_name(&name); format!(" Deleted {display_name}") } + StatusTask::Move(name) => { + let display_name = format_display_name(&name); + format!(" Location moved to {display_name}") + } }, _ => return, }; diff --git a/rm-shared/src/status_task.rs b/rm-shared/src/status_task.rs index c550e11..79dae08 100644 --- a/rm-shared/src/status_task.rs +++ b/rm-shared/src/status_task.rs @@ -2,4 +2,5 @@ pub enum StatusTask { Add(String), Delete(String), + Move(String), }