diff --git a/Cargo.lock b/Cargo.lock index d552ad0..f790724 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] [[package]] -name = "adler2" -version = "2.0.0" +name = "adler" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" @@ -109,12 +109,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "anyhow" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" - [[package]] name = "async-trait" version = "0.1.83" @@ -153,17 +147,17 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", + "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", - "windows-targets", ] [[package]] @@ -290,6 +284,34 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "color-eyre" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", + "url", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -552,6 +574,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -735,9 +767,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.1" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "h2" @@ -1070,6 +1102,12 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "2.7.0" @@ -1111,11 +1149,11 @@ dependencies = [ [[package]] name = "intuitils" -version = "0.0.5" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c6f633be0731795bacebacce3a2214fe8bdb875dd040130ac7fa946c3fbf00" +checksum = "ed6c1f2b56aed8f4031285cb3f3dad6d8a532522e84d35780a5c74491ad25dd2" dependencies = [ - "anyhow", + "color-eyre", "crossterm", "futures", "ratatui", @@ -1183,6 +1221,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.169" @@ -1213,9 +1257,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lru" @@ -1275,11 +1319,11 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ - "adler2", + "adler", ] [[package]] @@ -1334,9 +1378,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -1402,6 +1446,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "parking_lot" version = "0.12.3" @@ -1814,7 +1864,7 @@ dependencies = [ name = "rm-config" version = "0.5.1" dependencies = [ - "anyhow", + "color-eyre", "crossterm", "intuitils", "magnetease", @@ -1833,6 +1883,7 @@ name = "rm-shared" version = "0.5.1" dependencies = [ "chrono", + "color-eyre", "crossterm", "magnetease", "ratatui", @@ -1924,10 +1975,10 @@ dependencies = [ name = "rustmission" version = "0.5.1" dependencies = [ - "anyhow", "base64", "chrono", "clap", + "color-eyre", "crossterm", "futures", "fuzzy-matcher", @@ -2108,6 +2159,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2552,19 +2612,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", ] [[package]] name = "transmission-rpc" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "975758b6dd51b367e4454e9d4ae9b61a5419c6454822ffe1ab92c29bd6196a58" +checksum = "05204060bd751037cbbfe488e22d4a47991aa583052e85adbf4e96ab834a41bf" dependencies = [ + "base64", "chrono", "enum-iterator", "log", "reqwest", "serde", + "serde_json", "serde_repr", ] @@ -2671,6 +2755,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 13752fa..299b0c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,12 +17,11 @@ license = "GPL-3.0-or-later" rm-config = { version = "0.5", path = "rm-config" } rm-shared = { version = "0.5", path = "rm-shared" } -intuitils = "0.0.5" +intuitils = "0.0.6" magnetease = "0.3.1" -anyhow = "1" serde = { version = "1", features = ["derive"] } -transmission-rpc = "0.4" +transmission-rpc = "0.5" fuzzy-matcher = "0.3.7" clap = { version = "4", features = ["derive"] } base64 = "0.22" @@ -35,6 +34,7 @@ regex = "1" thiserror = "1" chrono = "0.4" open = "5.3.0" +color-eyre = { version = "0.6", features = ["issue-url"] } # Async tokio = { version = "1", features = ["macros", "sync", "rt-multi-thread"] } diff --git a/rm-config/Cargo.toml b/rm-config/Cargo.toml index c13a1f0..12b3cf9 100644 --- a/rm-config/Cargo.toml +++ b/rm-config/Cargo.toml @@ -15,7 +15,7 @@ rm-shared.workspace = true xdg.workspace = true toml.workspace = true serde.workspace = true -anyhow.workspace = true +color-eyre.workspace = true url.workspace = true ratatui.workspace = true crossterm.workspace = true diff --git a/rm-config/defaults/keymap.toml b/rm-config/defaults/keymap.toml index b5750c6..00e29cb 100644 --- a/rm-config/defaults/keymap.toml +++ b/rm-config/defaults/keymap.toml @@ -48,10 +48,14 @@ keybindings = [ { on = "p", action = "Pause" }, { on = "f", action = "ShowFiles" }, { on = "s", action = "ShowStats" }, - { on = "d", action = "Delete" }, ] +[torrents_tab_file_viewer] +keybindings = [ + { on = "p", action = "ChangeFilePriority" }, +] + [search_tab] keybindings = [ { on = "p", action = "ShowProvidersInfo" } diff --git a/rm-config/src/keymap/actions/mod.rs b/rm-config/src/keymap/actions/mod.rs index 74a2ef1..f9ba16e 100644 --- a/rm-config/src/keymap/actions/mod.rs +++ b/rm-config/src/keymap/actions/mod.rs @@ -1,3 +1,4 @@ pub mod general; pub mod search_tab; pub mod torrents_tab; +pub mod torrents_tab_file_viewer; diff --git a/rm-config/src/keymap/actions/torrents_tab_file_viewer.rs b/rm-config/src/keymap/actions/torrents_tab_file_viewer.rs new file mode 100644 index 0000000..6339709 --- /dev/null +++ b/rm-config/src/keymap/actions/torrents_tab_file_viewer.rs @@ -0,0 +1,24 @@ +use intuitils::user_action::UserAction; +use rm_shared::action::Action; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum TorrentsFileViewerAction { + ChangeFilePriority, +} + +impl UserAction for TorrentsFileViewerAction { + fn desc(&self) -> &'static str { + match self { + TorrentsFileViewerAction::ChangeFilePriority => "change file priority", + } + } +} + +impl From for Action { + fn from(value: TorrentsFileViewerAction) -> Self { + match value { + TorrentsFileViewerAction::ChangeFilePriority => Action::ChangeFilePriority, + } + } +} diff --git a/rm-config/src/keymap/mod.rs b/rm-config/src/keymap/mod.rs index deff645..ef61dd4 100644 --- a/rm-config/src/keymap/mod.rs +++ b/rm-config/src/keymap/mod.rs @@ -1,6 +1,13 @@ pub mod actions; -use intuitils::config::{keybindings::KeybindsHolder, IntuiConfig}; +use std::collections::HashMap; + +use actions::torrents_tab_file_viewer::TorrentsFileViewerAction; +use crossterm::event::KeyModifiers; +use intuitils::config::{ + keybindings::{KeyModifier, Keybinding, KeybindsHolder}, + IntuiConfig, +}; use serde::Deserialize; use rm_shared::action::Action; @@ -13,9 +20,30 @@ pub use self::actions::{ pub struct KeymapConfig { pub general: KeybindsHolder, pub torrents_tab: KeybindsHolder, + #[serde(default = "default_torrents_tab_file_viewer")] + pub torrents_tab_file_viewer: KeybindsHolder, pub search_tab: KeybindsHolder, } +fn default_torrents_tab_file_viewer() -> KeybindsHolder { + // Set default bind for changing file priority + let keycode = crossterm::event::KeyCode::Char('p'); + let action = TorrentsFileViewerAction::ChangeFilePriority; + + let mut map = HashMap::new(); + map.insert((keycode, KeyModifiers::NONE), Action::ChangeFilePriority); + + KeybindsHolder { + keybindings: vec![Keybinding { + on: keycode, + modifier: KeyModifier::None, + action, + show_in_help: true, + }], + map, + } +} + impl IntuiConfig for KeymapConfig { fn app_name() -> &'static str { "rustmission" diff --git a/rm-config/src/lib.rs b/rm-config/src/lib.rs index d6c716f..ef5a31c 100644 --- a/rm-config/src/lib.rs +++ b/rm-config/src/lib.rs @@ -4,8 +4,8 @@ pub mod main_config; use std::{path::PathBuf, sync::LazyLock}; -use anyhow::Result; use categories::CategoriesConfig; +use color_eyre::Result; use intuitils::config::IntuiConfig; use keymap::KeymapConfig; use main_config::MainConfig; diff --git a/rm-main/Cargo.toml b/rm-main/Cargo.toml index aa0f21c..c8d03d9 100644 --- a/rm-main/Cargo.toml +++ b/rm-main/Cargo.toml @@ -17,7 +17,7 @@ path = "src/main.rs" rm-config.workspace = true rm-shared.workspace = true magnetease.workspace = true -anyhow.workspace = true +color-eyre.workspace = true serde.workspace = true transmission-rpc.workspace = true fuzzy-matcher.workspace = true diff --git a/rm-main/src/cli/add_torrent.rs b/rm-main/src/cli/add_torrent.rs index a6eb416..9497576 100644 --- a/rm-main/src/cli/add_torrent.rs +++ b/rm-main/src/cli/add_torrent.rs @@ -1,7 +1,7 @@ use std::{fs::File, io::Read}; -use anyhow::Result; use base64::Engine; +use color_eyre::Result; use transmission_rpc::types::TorrentAddArgs; use crate::transmission; diff --git a/rm-main/src/cli/fetch_rss.rs b/rm-main/src/cli/fetch_rss.rs index 4122035..4f6c716 100644 --- a/rm-main/src/cli/fetch_rss.rs +++ b/rm-main/src/cli/fetch_rss.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Result}; +use color_eyre::{eyre::bail, Result}; use regex::Regex; use transmission_rpc::types::TorrentAddArgs; diff --git a/rm-main/src/cli/mod.rs b/rm-main/src/cli/mod.rs index 1e55f43..cbd27fd 100644 --- a/rm-main/src/cli/mod.rs +++ b/rm-main/src/cli/mod.rs @@ -1,8 +1,8 @@ mod add_torrent; mod fetch_rss; -use anyhow::Result; use clap::{Parser, Subcommand}; +use color_eyre::Result; use add_torrent::add_torrent; use fetch_rss::fetch_rss; diff --git a/rm-main/src/main.rs b/rm-main/src/main.rs index e4a92c7..256996b 100644 --- a/rm-main/src/main.rs +++ b/rm-main/src/main.rs @@ -2,12 +2,29 @@ mod cli; pub mod transmission; mod tui; -use anyhow::Result; +use std::io::stdout; + use clap::Parser; +use color_eyre::Result; +use crossterm::{ + cursor::Show, + event::DisableMouseCapture, + execute, + terminal::{disable_raw_mode, LeaveAlternateScreen}, +}; use tui::app::App; #[tokio::main()] async fn main() -> Result<()> { + color_eyre::config::HookBuilder::default() + .issue_url(concat!(env!("CARGO_PKG_REPOSITORY"), "/issues/new")) + .add_issue_metadata("version", env!("CARGO_PKG_VERSION")) + .issue_filter(|kind| match kind { + color_eyre::ErrorKind::NonRecoverable(_) => true, + color_eyre::ErrorKind::Recoverable(_) => false, + }) + .install()?; + let args = cli::Args::parse(); if let Some(command) = args.command { @@ -21,6 +38,10 @@ async fn main() -> Result<()> { async fn run_tui() -> Result<()> { let app = App::new().await?; - app.run().await?; + if let Err(e) = app.run().await { + let _ = disable_raw_mode(); + let _ = execute!(stdout(), LeaveAlternateScreen, Show, DisableMouseCapture); + return Err(e); + }; Ok(()) } diff --git a/rm-main/src/transmission/action.rs b/rm-main/src/transmission/action.rs index db7a253..8cf50b3 100644 --- a/rm-main/src/transmission/action.rs +++ b/rm-main/src/transmission/action.rs @@ -158,9 +158,7 @@ pub async fn action_handler( Err(err) => { let msg = "Failed to get session data"; let err_message = ErrorMessage::new(FAILED_TO_COMMUNICATE, msg, err); - update_tx - .send(UpdateAction::Error(Box::new(err_message))) - .unwrap(); + sender.send(Err(Box::new(err_message))).unwrap(); } }, TorrentAction::Move(ids, new_directory) => { @@ -203,7 +201,10 @@ pub async fn action_handler( } TorrentAction::GetTorrentsById(ids, sender) => { match client.torrent_get(None, Some(ids.clone())).await { - Ok(torrents) => sender.send(Ok(torrents.arguments.torrents)).unwrap(), + Ok(torrents) => { + // TODO: log using tracing in case of an error. + let _ = sender.send(Ok(torrents.arguments.torrents)); + } Err(err) => { let msg = format!("Failed to fetch torrents with these IDs: {:?}", ids); let err_message = ErrorMessage::new(FAILED_TO_COMMUNICATE, msg, err); @@ -217,10 +218,7 @@ pub async fn action_handler( } else { vec![category] }; - let args = TorrentSetArgs { - labels: Some(labels), - ..Default::default() - }; + let args = TorrentSetArgs::default().labels(labels); match client.torrent_set(args, Some(ids)).await { Ok(_) => update_tx.send(UpdateAction::StatusTaskSuccess).unwrap(), Err(err) => { diff --git a/rm-main/src/transmission/fetchers.rs b/rm-main/src/transmission/fetchers.rs index a503f8c..1ae2eb7 100644 --- a/rm-main/src/transmission/fetchers.rs +++ b/rm-main/src/transmission/fetchers.rs @@ -6,7 +6,7 @@ use transmission_rpc::types::TorrentGetField; use rm_shared::action::UpdateAction; -use crate::tui::app::CTX; +use crate::tui::ctx::CTX; use super::TorrentAction; @@ -85,6 +85,7 @@ pub async fn torrents() { TorrentGetField::Error, TorrentGetField::ErrorString, TorrentGetField::Labels, + TorrentGetField::FileStats, ]; let (torrents_tx, torrents_rx) = oneshot::channel(); CTX.send_torrent_action(TorrentAction::GetTorrents(fields, torrents_tx)); diff --git a/rm-main/src/tui/app.rs b/rm-main/src/tui/app.rs index 869bd86..254ab0d 100644 --- a/rm-main/src/tui/app.rs +++ b/rm-main/src/tui/app.rs @@ -1,4 +1,7 @@ -use std::sync::{LazyLock, Mutex}; +use std::{ + io::stdout, + panic::{set_hook, take_hook}, +}; use crate::{ transmission::{self, TorrentAction}, @@ -7,76 +10,78 @@ use crate::{ use intuitils::Terminal; use rm_config::CONFIG; -use rm_shared::action::{Action, UpdateAction}; +use rm_shared::{ + action::{Action, UpdateAction}, + current_window::{TorrentWindow, Window}, +}; -use anyhow::Result; -use crossterm::event::{Event, KeyCode, KeyModifiers}; -use tokio::sync::{ - mpsc::{self, unbounded_channel, UnboundedReceiver, UnboundedSender}, - oneshot, +use color_eyre::{ + eyre::{self}, + Result, Section, +}; +use crossterm::{ + cursor::Show, + event::{DisableMouseCapture, KeyCode, KeyModifiers}, + execute, + terminal::{disable_raw_mode, LeaveAlternateScreen}, }; +use tokio::sync::{mpsc::UnboundedReceiver, oneshot}; use super::{ - main_window::{CurrentTab, MainWindow}, + ctx::{CTX, CTX_RAW}, + main_window::MainWindow, tabs::torrents::SESSION_GET, }; -pub static CTX: LazyLock = LazyLock::new(|| CTX_RAW.0.clone()); - -static CTX_RAW: LazyLock<( - Ctx, - Mutex< - Option<( - UnboundedReceiver, - UnboundedReceiver, - UnboundedReceiver, - )>, - >, -)> = LazyLock::new(|| { - let (ctx, act_rx, upd_rx, tor_rx) = Ctx::new(); - (ctx, Mutex::new(Some((act_rx, upd_rx, tor_rx)))) -}); - -#[derive(Clone)] -pub struct Ctx { - action_tx: UnboundedSender, - update_tx: UnboundedSender, - trans_tx: UnboundedSender, -} +pub struct AppKeyEvent(crossterm::event::KeyEvent); -impl Ctx { - fn new() -> ( - Self, - UnboundedReceiver, - UnboundedReceiver, - UnboundedReceiver, - ) { - let (action_tx, action_rx) = unbounded_channel(); - let (update_tx, update_rx) = unbounded_channel(); - let (trans_tx, trans_rx) = unbounded_channel(); - - ( - Self { - action_tx, - update_tx, - trans_tx, - }, - action_rx, - update_rx, - trans_rx, - ) +impl From for AppKeyEvent { + fn from(value: crossterm::event::KeyEvent) -> Self { + Self(value) } +} - pub(crate) fn send_action(&self, action: Action) { - self.action_tx.send(action).unwrap(); +impl AppKeyEvent { + pub fn is_ctrl_c(&self) -> bool { + if self.0.modifiers == KeyModifiers::CONTROL + && (self.0.code == KeyCode::Char('c') || self.0.code == KeyCode::Char('C')) + { + return true; + } + false } - pub(crate) fn send_torrent_action(&self, action: TorrentAction) { - self.trans_tx.send(action).unwrap(); + fn keybinding(&self) -> (KeyCode, KeyModifiers) { + match self.0.code { + KeyCode::Char(e) => { + let modifier = if e.is_uppercase() { + KeyModifiers::NONE + } else { + self.0.modifiers + }; + (self.0.code, modifier) + } + _ => (self.0.code, self.0.modifiers), + } } - pub(crate) fn send_update_action(&self, action: UpdateAction) { - self.update_tx.send(action).unwrap(); + fn to_action(&self, current_window: Window) -> Option { + let keymap = match current_window { + Window::Torrents(torrents_tab_current_window) => match torrents_tab_current_window { + TorrentWindow::General => &CONFIG.keybindings.torrents_tab.map, + TorrentWindow::FileViewer => &CONFIG.keybindings.torrents_tab_file_viewer.map, + }, + Window::Search(_) => &CONFIG.keybindings.search_tab.map, + }; + + let keybinding = self.keybinding(); + + for keymap in [&CONFIG.keybindings.general.map, keymap] { + if let Some(action) = keymap.get(&keybinding).cloned() { + return Some(action); + } + } + None } } @@ -109,7 +114,13 @@ impl App { let (sess_tx, sess_rx) = oneshot::channel(); CTX.send_torrent_action(TorrentAction::GetSessionGet(sess_tx)); - SESSION_GET.set(sess_rx.await.unwrap().unwrap()).unwrap(); + match sess_rx.await.unwrap() { + Ok(sess_get) => SESSION_GET.set(sess_get).unwrap(), + Err(e) => CTX.send_update_action(UpdateAction::UnrecoverableError(Box::new( + eyre::eyre!(e.source).wrap_err("error connecting to transmission daemon") + .suggestion("Check if the transmission daemon IP address is correct and ensure you have an internet connection."), + ))), + } }); Ok(Self { @@ -124,6 +135,14 @@ impl App { pub async fn run(mut self) -> Result<()> { let mut terminal = Terminal::new()?; + let original_hook = take_hook(); + + set_hook(Box::new(move |panic_info| { + let _ = disable_raw_mode(); + let _ = execute!(stdout(), LeaveAlternateScreen, Show, DisableMouseCapture); + original_hook(panic_info); + })); + terminal.init()?; self.render(&mut terminal)?; @@ -142,23 +161,44 @@ impl App { let update_action = self.update_rx.recv(); let tick_action = interval.tick(); - let current_tab = self.main_window.tabs.current(); + let current_window = self.main_window.current_window(); tokio::select! { _ = tick_action => self.tick(), event = tui_event => { - event_to_action(self.mode, current_tab, event.unwrap()); + let event = event.unwrap(); + + use crossterm::event::{Event, MouseEventKind}; + match event { + Event::Key(key_event) => { + let app_key_event = AppKeyEvent::from(key_event); + if app_key_event.is_ctrl_c() { + self.should_quit = true; + } else if self.mode == Mode::Input { + self.handle_user_action(Action::Input(key_event)); + } else if let Some(action) = app_key_event.to_action(current_window) { + self.handle_user_action(action); + } + }, + Event::Mouse(mouse_event) => match mouse_event.kind { + MouseEventKind::ScrollDown => self.handle_user_action(Action::ScrollDownBy(3)), + MouseEventKind::ScrollUp => self.handle_user_action(Action::ScrollUpBy(3)), + _ => (), + }, + Event::Resize(_, _) => self.render(terminal).unwrap(), + _ => (), + } }, - update_action = update_action => self.handle_update_action(update_action.unwrap()).await, + update_action = update_action => self.handle_update_action(update_action.unwrap()).await?, action = action => { if let Some(action) = action { if action.is_render() { - tokio::task::block_in_place(|| self.render(terminal).unwrap() ); + self.render(terminal)?; } else { - self.handle_user_action(action).await + self.handle_user_action(action); } } } @@ -171,14 +211,17 @@ impl App { } fn render(&mut self, terminal: &mut Terminal) -> Result<()> { - terminal.draw(|f| { - self.main_window.render(f, f.area()); - })?; + tokio::task::block_in_place(|| { + terminal + .draw(|f| { + self.main_window.render(f, f.area()); + }) + .unwrap(); + }); Ok(()) } - #[must_use] - async fn handle_user_action(&mut self, action: Action) { + fn handle_user_action(&mut self, action: Action) { use Action as A; match &action { A::HardQuit => { @@ -191,8 +234,9 @@ impl App { } } - async fn handle_update_action(&mut self, action: UpdateAction) { + async fn handle_update_action(&mut self, action: UpdateAction) -> Result<()> { match action { + UpdateAction::UnrecoverableError(report) => return Err(*report), UpdateAction::SwitchToInputMode => { self.mode = Mode::Input; } @@ -203,6 +247,7 @@ impl App { _ => self.main_window.handle_update_action(action), }; CTX.send_action(Action::Render); + Ok(()) } fn tick(&mut self) { @@ -215,58 +260,3 @@ pub enum Mode { Input, Normal, } - -pub fn event_to_action(mode: Mode, current_tab: CurrentTab, event: Event) { - // Handle CTRL+C first - if let Event::Key(key_event) = event { - if key_event.modifiers == KeyModifiers::CONTROL - && (key_event.code == KeyCode::Char('c') || key_event.code == KeyCode::Char('C')) - { - CTX.send_action(Action::HardQuit); - } - } - - match event { - Event::Key(key) if mode == Mode::Input => CTX.send_action(Action::Input(key)), - Event::Mouse(mouse_event) => match mouse_event.kind { - crossterm::event::MouseEventKind::ScrollDown => { - CTX.send_action(Action::ScrollDownBy(3)) - } - crossterm::event::MouseEventKind::ScrollUp => CTX.send_action(Action::ScrollUpBy(3)), - _ => (), - }, - Event::Key(key) => { - let keymaps = match current_tab { - CurrentTab::Torrents => [ - &CONFIG.keybindings.general.map, - &CONFIG.keybindings.torrents_tab.map, - ], - CurrentTab::Search => [ - &CONFIG.keybindings.general.map, - &CONFIG.keybindings.search_tab.map, - ], - }; - - let keybinding = match key.code { - KeyCode::Char(e) => { - let modifier = if e.is_uppercase() { - KeyModifiers::NONE - } else { - key.modifiers - }; - (key.code, modifier) - } - _ => (key.code, key.modifiers), - }; - - for keymap in keymaps { - if let Some(action) = keymap.get(&keybinding).cloned() { - CTX.send_action(action); - return; - } - } - } - Event::Resize(_, _) => CTX.send_action(Action::Render), - _ => (), - } -} diff --git a/rm-main/src/tui/components/misc.rs b/rm-main/src/tui/components/misc.rs index bda4f54..704d5a0 100644 --- a/rm-main/src/tui/components/misc.rs +++ b/rm-main/src/tui/components/misc.rs @@ -1,25 +1,19 @@ use ratatui::{ - layout::{Alignment, Margin, Rect}, + layout::{Margin, Rect}, style::{Style, Styled, Stylize}, - widgets::{ - block::{Position, Title}, - Block, BorderType, - }, + text::Line, + widgets::{block::Title, Block, BorderType}, }; use rm_config::CONFIG; use crate::tui::main_window::centered_rect; -pub fn popup_close_button_highlight() -> Title<'static> { - Title::from(" [ CLOSE ] ".fg(CONFIG.general.accent_color).bold()) - .alignment(Alignment::Right) - .position(Position::Bottom) +pub fn popup_close_button_highlight() -> Line<'static> { + Line::from(" [ CLOSE ] ".fg(CONFIG.general.accent_color).bold()).right_aligned() } -pub fn popup_close_button() -> Title<'static> { - Title::from(" [CLOSE] ".bold()) - .alignment(Alignment::Right) - .position(Position::Bottom) +pub fn popup_close_button() -> Line<'static> { + Line::from(" [CLOSE] ".bold()).right_aligned() } pub fn popup_block(title: &str) -> Block { diff --git a/rm-main/src/tui/ctx.rs b/rm-main/src/tui/ctx.rs new file mode 100644 index 0000000..aa47172 --- /dev/null +++ b/rm-main/src/tui/ctx.rs @@ -0,0 +1,65 @@ +use std::sync::{LazyLock, Mutex}; + +use rm_shared::action::{Action, UpdateAction}; +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; + +use crate::transmission::TorrentAction; + +pub static CTX: LazyLock = LazyLock::new(|| CTX_RAW.0.clone()); + +pub(super) static CTX_RAW: LazyLock<( + Ctx, + Mutex< + Option<( + UnboundedReceiver, + UnboundedReceiver, + UnboundedReceiver, + )>, + >, +)> = LazyLock::new(|| { + let (ctx, act_rx, upd_rx, tor_rx) = Ctx::new(); + (ctx, Mutex::new(Some((act_rx, upd_rx, tor_rx)))) +}); + +#[derive(Clone)] +pub struct Ctx { + pub(super) action_tx: UnboundedSender, + pub(super) update_tx: UnboundedSender, + pub(super) trans_tx: UnboundedSender, +} + +impl Ctx { + fn new() -> ( + Self, + UnboundedReceiver, + UnboundedReceiver, + UnboundedReceiver, + ) { + let (action_tx, action_rx) = unbounded_channel(); + let (update_tx, update_rx) = unbounded_channel(); + let (trans_tx, trans_rx) = unbounded_channel(); + + ( + Self { + action_tx, + update_tx, + trans_tx, + }, + action_rx, + update_rx, + trans_rx, + ) + } + + pub(crate) fn send_action(&self, action: Action) { + self.action_tx.send(action).unwrap(); + } + + pub(crate) fn send_torrent_action(&self, action: TorrentAction) { + self.trans_tx.send(action).unwrap(); + } + + pub(crate) fn send_update_action(&self, action: UpdateAction) { + self.update_tx.send(action).unwrap(); + } +} diff --git a/rm-main/src/tui/global_popups/help.rs b/rm-main/src/tui/global_popups/help.rs index d3a147f..489332a 100644 --- a/rm-main/src/tui/global_popups/help.rs +++ b/rm-main/src/tui/global_popups/help.rs @@ -10,8 +10,8 @@ use rm_config::CONFIG; use rm_shared::action::Action; use crate::tui::{ - app::CTX, components::{popup_block_with_close_highlight, popup_rects, Component, ComponentAction}, + ctx::CTX, }; macro_rules! add_line { @@ -28,6 +28,7 @@ pub struct HelpPopup { scroll: Option, global_keys: Vec<(String, &'static str)>, torrent_keys: Vec<(String, &'static str)>, + torrent_file_viewer_keys: Vec<(String, &'static str)>, search_keys: Vec<(String, &'static str)>, max_key_len: usize, max_line_len: usize, @@ -71,6 +72,10 @@ impl HelpPopup { .keybindings .torrents_tab .get_help_repr_with_override(override_keycode); + let torrent_file_viewer_keys = CONFIG + .keybindings + .torrents_tab_file_viewer + .get_help_repr_with_override(override_keycode); let search_keys = CONFIG .keybindings .search_tab @@ -101,6 +106,7 @@ impl HelpPopup { scroll: None, global_keys, torrent_keys, + torrent_file_viewer_keys, search_keys, max_key_len, max_line_len, @@ -190,6 +196,7 @@ impl Component for HelpPopup { let global_keys = padded_keys(&mut self.global_keys); let torrent_keys = padded_keys(&mut self.torrent_keys); + let torrent_file_viewer_keys = padded_keys(&mut self.torrent_file_viewer_keys); let search_keys = padded_keys(&mut self.search_keys); let mut lines = vec![]; @@ -222,6 +229,16 @@ impl Component for HelpPopup { insert_keys(&mut lines, torrent_keys); + lines.push( + Line::from(vec![Span::styled( + "Torrents File Viewer", + Style::default().bold().underlined(), + )]) + .centered(), + ); + + insert_keys(&mut lines, torrent_file_viewer_keys); + lines.push( Line::from(vec![Span::styled( "Search Tab", diff --git a/rm-main/src/tui/global_popups/mod.rs b/rm-main/src/tui/global_popups/mod.rs index 0fa94de..bd505d4 100644 --- a/rm-main/src/tui/global_popups/mod.rs +++ b/rm-main/src/tui/global_popups/mod.rs @@ -9,8 +9,8 @@ pub use help::HelpPopup; use rm_shared::action::Action; use super::{ - app::CTX, components::{Component, ComponentAction}, + ctx::CTX, }; pub(super) struct GlobalPopupManager { @@ -30,7 +30,7 @@ impl GlobalPopupManager { self.error_popup.is_some() || self.help_popup.is_some() } - fn toggle_help(&mut self) { + pub fn toggle_help(&mut self) { if self.help_popup.is_some() { self.help_popup = None; } else { diff --git a/rm-main/src/tui/main_window.rs b/rm-main/src/tui/main_window.rs index 74b2ca7..3224c12 100644 --- a/rm-main/src/tui/main_window.rs +++ b/rm-main/src/tui/main_window.rs @@ -4,12 +4,14 @@ use intui_tabs::{Tabs, TabsState}; use ratatui::prelude::*; use rm_config::CONFIG; -use rm_shared::action::{Action, UpdateAction}; +use rm_shared::{ + action::{Action, UpdateAction}, + current_window::Window, +}; -use crate::tui::app::CTX; +use crate::tui::ctx::CTX; use super::{ - app, components::{Component, ComponentAction}, global_popups::{ErrorPopup, GlobalPopupManager}, tabs::{search::SearchTab, torrents::TorrentsTab}, @@ -17,7 +19,7 @@ use super::{ #[derive(Clone, Copy, PartialEq, Eq)] pub enum CurrentTab { - Torrents = 0, + Torrents, Search, } @@ -38,8 +40,8 @@ impl Display for CurrentTab { pub struct MainWindow { pub tabs: intui_tabs::TabsState, - torrents_tab: TorrentsTab, - search_tab: SearchTab, + pub torrents_tab: TorrentsTab, + pub search_tab: SearchTab, global_popup_manager: GlobalPopupManager, } @@ -52,6 +54,13 @@ impl MainWindow { global_popup_manager: GlobalPopupManager::new(), } } + + pub fn current_window(&self) -> Window { + match self.tabs.current() { + CurrentTab::Torrents => Window::Torrents(self.torrents_tab.current_window), + CurrentTab::Search => Window::Search(self.search_tab.current_window), + } + } } impl Component for MainWindow { @@ -92,7 +101,8 @@ impl Component for MainWindow { fn handle_update_action(&mut self, action: UpdateAction) { match action { UpdateAction::Error(err) => { - let error_popup = ErrorPopup::new(err.title, err.description, err.source); + let error_popup = + ErrorPopup::new(err.title, err.description, err.source.to_string()); self.global_popup_manager.error_popup = Some(error_popup); } action if self.tabs.current() == CurrentTab::Torrents => { diff --git a/rm-main/src/tui/mod.rs b/rm-main/src/tui/mod.rs index a855911..0f6e9b1 100644 --- a/rm-main/src/tui/mod.rs +++ b/rm-main/src/tui/mod.rs @@ -1,5 +1,6 @@ pub mod app; mod components; +pub mod ctx; mod global_popups; pub mod main_window; pub mod tabs; diff --git a/rm-main/src/tui/tabs/search/bottom_bar.rs b/rm-main/src/tui/tabs/search/bottom_bar.rs index b9b383e..419100b 100644 --- a/rm-main/src/tui/tabs/search/bottom_bar.rs +++ b/rm-main/src/tui/tabs/search/bottom_bar.rs @@ -10,8 +10,8 @@ use rm_shared::action::{Action, UpdateAction}; use throbber_widgets_tui::ThrobberState; use crate::tui::{ - app::CTX, components::{keybinding_style, Component, ComponentAction}, + ctx::CTX, tabs::torrents::tasks, }; diff --git a/rm-main/src/tui/tabs/search/mod.rs b/rm-main/src/tui/tabs/search/mod.rs index 057c0f5..0b782d6 100644 --- a/rm-main/src/tui/tabs/search/mod.rs +++ b/rm-main/src/tui/tabs/search/mod.rs @@ -19,11 +19,12 @@ use tokio::sync::mpsc::{self, UnboundedSender}; use tui_input::{backend::crossterm::to_input_request, Input}; use crate::tui::{ - app::CTX, components::{Component, ComponentAction, GenericTable}, + ctx::CTX, }; use rm_shared::{ action::{Action, UpdateAction}, + current_window::SearchWindow, utils::bytes_to_human_format, }; @@ -34,6 +35,7 @@ enum SearchTabFocus { } pub(crate) struct SearchTab { + pub current_window: SearchWindow, focus: SearchTabFocus, input: Input, search_query_rx: UnboundedSender, @@ -107,6 +109,7 @@ impl SearchTab { }); Self { + current_window: SearchWindow::General, focus: SearchTabFocus::List, input: Input::default(), table, diff --git a/rm-main/src/tui/tabs/search/popups/mod.rs b/rm-main/src/tui/tabs/search/popups/mod.rs index 499b7ad..07a0cf8 100644 --- a/rm-main/src/tui/tabs/search/popups/mod.rs +++ b/rm-main/src/tui/tabs/search/popups/mod.rs @@ -1,8 +1,7 @@ mod providers; -use crate::tui::app::CTX; -use crate::tui::components::Component; -use crate::tui::components::ComponentAction; +use crate::tui::components::{Component, ComponentAction}; +use crate::tui::ctx::CTX; use providers::ProvidersPopup; use ratatui::prelude::*; use ratatui::Frame; diff --git a/rm-main/src/tui/tabs/search/popups/providers.rs b/rm-main/src/tui/tabs/search/popups/providers.rs index e2b4a8f..93b2c02 100644 --- a/rm-main/src/tui/tabs/search/popups/providers.rs +++ b/rm-main/src/tui/tabs/search/popups/providers.rs @@ -97,7 +97,7 @@ impl Component for ProvidersPopup { let block = Block::bordered() .border_type(BorderType::Rounded) .title(Title::from(" Providers ".set_style(title_style))) - .title(popup_close_button_highlight()); + .title_bottom(popup_close_button_highlight()); let widths = [ Constraint::Length(10), // Provider name (and icon status prefix) diff --git a/rm-main/src/tui/tabs/torrents/mod.rs b/rm-main/src/tui/tabs/torrents/mod.rs index da5a842..6ad360b 100644 --- a/rm-main/src/tui/tabs/torrents/mod.rs +++ b/rm-main/src/tui/tabs/torrents/mod.rs @@ -8,21 +8,27 @@ pub mod tasks; use std::sync::OnceLock; use crate::transmission::TorrentAction; -use crate::tui::app::CTX; use crate::tui::components::{Component, ComponentAction}; +use crate::tui::ctx::CTX; use popups::details::DetailsPopup; use popups::stats::StatisticsPopup; -use ratatui::prelude::*; -use ratatui::widgets::{Cell, Row, Table}; +use ratatui::{ + prelude::*, + widgets::{Cell, Row, Table}, +}; + use rm_config::CONFIG; -use rm_shared::status_task::StatusTask; +use rm_shared::{ + action::{Action, ErrorMessage, UpdateAction}, + current_window::TorrentWindow, + status_task::StatusTask, +}; use rustmission_torrent::RustmissionTorrent; use tasks::TorrentSelection; use transmission_rpc::types::{Id, SessionGet, TorrentStatus}; use crate::transmission; -use rm_shared::action::{Action, ErrorMessage, UpdateAction}; use self::bottom_stats::BottomStats; use self::popups::files::FilesPopup; @@ -33,6 +39,7 @@ use self::task_manager::TaskManager; pub static SESSION_GET: OnceLock = OnceLock::new(); pub struct TorrentsTab { + pub current_window: TorrentWindow, table_manager: TableManager, popup_manager: PopupManager, task_manager: TaskManager, @@ -53,6 +60,7 @@ impl TorrentsTab { task_manager: TaskManager::new(), table_manager, popup_manager: PopupManager::new(), + current_window: TorrentWindow::General, } } } @@ -63,11 +71,8 @@ impl Component for TorrentsTab { Layout::vertical([Constraint::Min(10), Constraint::Length(1)]).areas(rect); self.render_table(f, torrents_list_rect); - self.bottom_stats.render(f, stats_rect); - self.task_manager.render(f, stats_rect); - self.popup_manager.render(f, f.area()); } @@ -196,6 +201,9 @@ impl Component for TorrentsTab { fn handle_update_action(&mut self, action: UpdateAction) { match action { + UpdateAction::ChangeTorrentWindow(window) => { + self.current_window = window; + } UpdateAction::SessionStats(stats) => { if let Some(CurrentPopup::Stats(popup)) = &mut self.popup_manager.current_popup { popup.update_stats(&stats) @@ -358,7 +366,9 @@ impl TorrentsTab { if let Some(highlighted_torrent) = self.table_manager.current_torrent() { let popup = FilesPopup::new(highlighted_torrent.id.clone()); self.popup_manager.show_popup(CurrentPopup::Files(popup)); - CTX.send_action(Action::Render); + + let update_action = UpdateAction::ChangeTorrentWindow(TorrentWindow::FileViewer); + CTX.send_update_action(update_action); } } diff --git a/rm-main/src/tui/tabs/torrents/popups/details.rs b/rm-main/src/tui/tabs/torrents/popups/details.rs index 4abccfe..5cea183 100644 --- a/rm-main/src/tui/tabs/torrents/popups/details.rs +++ b/rm-main/src/tui/tabs/torrents/popups/details.rs @@ -7,8 +7,8 @@ use rm_shared::{action::Action, utils::bytes_to_human_format}; use style::Styled; use crate::tui::{ - app::CTX, components::{keybinding_style, popup_close_button_highlight, Component, ComponentAction}, + ctx::CTX, main_window::centered_rect, tabs::torrents::rustmission_torrent::{CategoryType, RustmissionTorrent}, }; @@ -61,7 +61,7 @@ impl Component for DetailsPopup { let block = Block::bordered() .border_type(BorderType::Rounded) .title(Title::from(" Details ".set_style(title_style))) - .title(popup_close_button_highlight()); + .title_bottom(popup_close_button_highlight()); let mut lines = vec![]; diff --git a/rm-main/src/tui/tabs/torrents/popups/files.rs b/rm-main/src/tui/tabs/torrents/popups/files.rs index 613e94d..a552263 100644 --- a/rm-main/src/tui/tabs/torrents/popups/files.rs +++ b/rm-main/src/tui/tabs/torrents/popups/files.rs @@ -3,24 +3,24 @@ use std::{collections::BTreeMap, time::Duration}; use ratatui::{ prelude::*, style::Styled, - widgets::{ - block::{Position, Title}, - Clear, Paragraph, - }, + widgets::{Clear, List, ListState, Paragraph}, +}; +use rm_config::{ + keymap::{actions::torrents_tab_file_viewer::TorrentsFileViewerAction, GeneralAction}, + CONFIG, }; -use rm_config::{keymap::GeneralAction, CONFIG}; use tokio::{sync::oneshot, task::JoinHandle}; -use transmission_rpc::types::{Id, Torrent, TorrentSetArgs}; +use transmission_rpc::types::{Id, Priority, Torrent, TorrentSetArgs}; use tui_tree_widget::{Tree, TreeItem, TreeState}; use crate::{ transmission::TorrentAction, tui::{ - app::CTX, components::{ keybinding_style, popup_block, popup_close_button, popup_close_button_highlight, popup_rects, Component, ComponentAction, }, + ctx::CTX, }, }; use rm_shared::{ @@ -29,9 +29,115 @@ use rm_shared::{ utils::{bytes_to_human_format, bytes_to_short_human_format}, }; +struct PriorityPopup { + torrent_id: Id, + files: Vec, + list_state: ListState, +} + +impl PriorityPopup { + fn new(torrent_id: Id, files: Vec) -> Self { + Self { + torrent_id, + files, + list_state: ListState::default().with_selected(Some(1)), + } + } +} + +impl Component for PriorityPopup { + fn handle_actions(&mut self, action: Action) -> ComponentAction { + if action.is_soft_quit() { + return ComponentAction::Quit; + } + + match action { + Action::Up => { + self.list_state.select_previous(); + CTX.send_action(Action::Render); + return ComponentAction::Nothing; + } + Action::Down => { + self.list_state.select_next(); + CTX.send_action(Action::Render); + return ComponentAction::Nothing; + } + Action::Confirm => { + let torrent_id = match self.torrent_id { + Id::Id(id) => id, + Id::Hash(_) => unreachable!(), + }; + + let args = match self.list_state.selected().unwrap() { + 0 => { + let args = TorrentSetArgs::new().priority_low(self.files.clone()); + CTX.send_torrent_action(TorrentAction::SetArgs( + Box::new(args), + Some(vec![self.torrent_id.clone()]), + )); + } + 1 => { + let args = TorrentSetArgs::new().priority_normal(self.files.clone()); + CTX.send_torrent_action(TorrentAction::SetArgs( + Box::new(args), + Some(vec![self.torrent_id.clone()]), + )); + } + 2 => { + let args = TorrentSetArgs::new().priority_high(self.files.clone()); + CTX.send_torrent_action(TorrentAction::SetArgs( + Box::new(args), + Some(vec![self.torrent_id.clone()]), + )); + } + _ => unreachable!(), + }; + CTX.send_action(Action::Render); + return ComponentAction::Quit; + } + _ => return ComponentAction::Nothing, + } + } + + fn handle_update_action(&mut self, action: UpdateAction) { + let _action = action; + } + + fn render(&mut self, f: &mut Frame, rect: Rect) { + let [block_rect] = Layout::horizontal([Constraint::Length(20)]) + .flex(layout::Flex::Center) + .areas(rect); + let [block_rect] = Layout::vertical([Constraint::Length(5)]) + .flex(layout::Flex::Center) + .areas(block_rect); + + let block = popup_block(" Priority "); + + let list_rect = block_rect.inner(Margin { + horizontal: 1, + vertical: 1, + }); + let list = List::new([ + Text::raw("Low").centered(), + Text::raw("Normal").centered(), + Text::raw("High").centered(), + ]) + .highlight_style( + Style::default() + .fg(CONFIG.general.accent_color) + .bg(Color::Black) + .bold(), + ); + + f.render_widget(block, block_rect); + f.render_stateful_widget(list, list_rect, &mut self.list_state); + } +} + pub struct FilesPopup { torrent: Option, torrent_id: Id, + priority_popup: Option, tree_state: TreeState, tree: Node, current_focus: CurrentFocus, @@ -84,6 +190,7 @@ impl FilesPopup { switched_after_fetched_data: false, torrent_id, torrent_info_task_handle, + priority_popup: None, } } @@ -94,11 +201,11 @@ impl FilesPopup { } } - fn selected_ids(&self) -> Vec { + fn selected_ids(&self) -> Vec { self.tree_state .selected() .iter() - .filter_map(|str_id| str_id.parse::().ok()) + .filter_map(|str_id| str_id.parse::().ok()) .collect() } } @@ -107,20 +214,35 @@ impl Component for FilesPopup { #[must_use] fn handle_actions(&mut self, action: Action) -> ComponentAction { use Action as A; - match (action, self.current_focus) { - (action, _) if action.is_soft_quit() => { + + match (&mut self.priority_popup, action, self.current_focus) { + (Some(priority_popup), action, _) => { + if priority_popup.handle_actions(action).is_quit() { + self.priority_popup = None; + CTX.send_action(A::Render); + return ComponentAction::Nothing; + } + } + (_, action, _) if action.is_soft_quit() => { self.torrent_info_task_handle.abort(); return ComponentAction::Quit; } - (A::ChangeFocus, _) => { + (None, A::ChangeFilePriority, CurrentFocus::Files) => { + self.priority_popup = Some(PriorityPopup::new( + self.torrent_id.clone(), + self.selected_ids(), + )); + CTX.send_action(A::Render); + } + (None, A::ChangeFocus, _) => { self.switch_focus(); CTX.send_action(A::Render); } - (A::Confirm, CurrentFocus::CloseButton) => { + (None, A::Confirm, CurrentFocus::CloseButton) => { self.torrent_info_task_handle.abort(); return ComponentAction::Quit; } - (A::Select | A::Confirm, CurrentFocus::Files) => { + (None, A::Select | A::Confirm, CurrentFocus::Files) => { if self.torrent.is_some() { let mut wanted_ids = self .torrent @@ -141,7 +263,7 @@ impl Component for FilesPopup { let mut wanted_in_selection_no = 0; for selected_id in &selected_ids { - if wanted_ids[*selected_id as usize] == 1 { + if wanted_ids[*selected_id as usize] { wanted_in_selection_no += 1; } else { wanted_in_selection_no -= 1; @@ -150,11 +272,11 @@ impl Component for FilesPopup { if wanted_in_selection_no > 0 { for selected_id in &selected_ids { - wanted_ids[*selected_id as usize] = 0; + wanted_ids[*selected_id as usize] = false; } } else { for selected_id in &selected_ids { - wanted_ids[*selected_id as usize] = 1; + wanted_ids[*selected_id as usize] = true; } } @@ -163,18 +285,12 @@ impl Component for FilesPopup { for transmission_file in self.tree.get_by_ids(&selected_ids) { transmission_file.set_wanted(false); } - TorrentSetArgs { - files_unwanted: Some(selected_ids), - ..Default::default() - } + TorrentSetArgs::default().files_unwanted(selected_ids) } else { for transmission_file in self.tree.get_by_ids(&selected_ids) { transmission_file.set_wanted(true); } - TorrentSetArgs { - files_wanted: Some(selected_ids), - ..Default::default() - } + TorrentSetArgs::default().files_wanted(selected_ids) } }; @@ -187,15 +303,15 @@ impl Component for FilesPopup { } } - (A::Up | A::ScrollUpBy(_), CurrentFocus::Files) => { + (None, A::Up | A::ScrollUpBy(_), CurrentFocus::Files) => { self.tree_state.key_up(); CTX.send_action(Action::Render); } - (A::Down | A::ScrollDownBy(_), CurrentFocus::Files) => { + (None, A::Down | A::ScrollDownBy(_), CurrentFocus::Files) => { self.tree_state.key_down(); CTX.send_action(Action::Render); } - (A::XdgOpen, CurrentFocus::Files) => { + (None, A::XdgOpen, CurrentFocus::Files) => { if let Some(torrent) = &self.torrent { let mut identifier = self.tree_state.selected().to_vec(); @@ -203,7 +319,7 @@ impl Component for FilesPopup { return ComponentAction::Nothing; } - if let Ok(file_id) = identifier.last().unwrap().parse::() { + if let Ok(file_id) = identifier.last().unwrap().parse::() { identifier.pop(); identifier .push(self.tree.get_by_ids(&[file_id]).pop().unwrap().name.clone()) @@ -296,7 +412,16 @@ impl Component for FilesPopup { .get_keys_for_action_joined(GeneralAction::XdgOpen) { keys.push(Span::styled(key, keybinding_style())); - keys.push(Span::raw(" - xdg_open ")); + keys.push(Span::raw(" - xdg_open | ")); + } + + if let Some(key) = CONFIG + .keybindings + .torrents_tab_file_viewer + .get_keys_for_action_joined(TorrentsFileViewerAction::ChangeFilePriority) + { + keys.push(Span::styled(key, keybinding_style())); + keys.push(Span::raw(" - change file priority ")); } Line::from(keys) @@ -311,12 +436,8 @@ impl Component for FilesPopup { .set_style(highlight_style) .into_right_aligned_line(), ) - .title(close_button) - .title( - Title::from(keybinding_tip) - .alignment(Alignment::Left) - .position(Position::Bottom), - ); + .title_bottom(close_button) + .title_bottom(Line::from(keybinding_tip).left_aligned()); let tree_items = self.tree.make_tree(); @@ -327,6 +448,10 @@ impl Component for FilesPopup { f.render_widget(Clear, popup_rect); f.render_stateful_widget(tree_widget, block_rect, &mut self.tree_state); + + if let Some(popup) = &mut self.priority_popup { + popup.render(f, rect); + } } else { let paragraph = Paragraph::new("Loading..."); let block = block.title(popup_close_button_highlight()); @@ -341,6 +466,7 @@ struct TransmissionFile { name: String, id: usize, wanted: bool, + priority: Priority, length: i64, bytes_completed: i64, } @@ -349,6 +475,14 @@ impl TransmissionFile { fn set_wanted(&mut self, new_wanted: bool) { self.wanted = new_wanted; } + + fn priority_str(&self) -> &'static str { + match self.priority { + Priority::Low => "Low", + Priority::Normal => "Normal", + Priority::High => "High", + } + } } struct Node { @@ -371,7 +505,9 @@ impl Node { for (id, file) in files.iter().enumerate() { let path: Vec = file.name.split('/').map(str::to_string).collect(); - let wanted = torrent.wanted.as_ref().unwrap()[id] != 0; + let wanted = torrent.wanted.as_ref().unwrap()[id] != false; + + let priority = torrent.priorities.as_ref().unwrap()[id].clone(); let file = TransmissionFile { id, @@ -379,6 +515,7 @@ impl Node { wanted, length: file.length, bytes_completed: file.bytes_completed, + priority, }; root.add_transmission_file(file, &path); @@ -402,10 +539,10 @@ impl Node { } } - fn get_by_ids(&mut self, ids: &[i32]) -> Vec<&mut TransmissionFile> { + fn get_by_ids(&mut self, ids: &[usize]) -> Vec<&mut TransmissionFile> { let mut transmission_files = vec![]; for file in &mut self.items { - if ids.contains(&(file.id as i32)) { + if ids.contains(&(file.id as usize)) { transmission_files.push(file); } } @@ -438,6 +575,8 @@ impl Node { name.push_span(Span::raw("| ")); + name.push_span(format!("[{}] ", transmission_file.priority_str())); + if progress != 1.0 { name.push_span(Span::styled( progress_percent, diff --git a/rm-main/src/tui/tabs/torrents/popups/mod.rs b/rm-main/src/tui/tabs/torrents/popups/mod.rs index 20dfa26..a7cbd0d 100644 --- a/rm-main/src/tui/tabs/torrents/popups/mod.rs +++ b/rm-main/src/tui/tabs/torrents/popups/mod.rs @@ -1,11 +1,14 @@ use crate::tui::{ - app::CTX, components::{Component, ComponentAction}, + ctx::CTX, }; use self::{files::FilesPopup, stats::StatisticsPopup}; use details::DetailsPopup; -use rm_shared::action::{Action, UpdateAction}; +use rm_shared::{ + action::{Action, UpdateAction}, + current_window::TorrentWindow, +}; use ratatui::prelude::*; @@ -54,7 +57,7 @@ impl Component for PopupManager { if should_close { self.close_popup(); - CTX.send_action(Action::Render); + CTX.send_update_action(UpdateAction::ChangeTorrentWindow(TorrentWindow::General)); } } ComponentAction::Nothing diff --git a/rm-main/src/tui/tabs/torrents/rustmission_torrent.rs b/rm-main/src/tui/tabs/torrents/rustmission_torrent.rs index b72b659..4a7477b 100644 --- a/rm-main/src/tui/tabs/torrents/rustmission_torrent.rs +++ b/rm-main/src/tui/tabs/torrents/rustmission_torrent.rs @@ -1,4 +1,4 @@ -use chrono::{Datelike, NaiveDateTime}; +use chrono::{DateTime, Datelike, Utc}; use ratatui::{ style::{Style, Stylize}, text::{Line, Span}, @@ -9,7 +9,7 @@ use rm_shared::{ header::Header, utils::{bytes_to_human_format, seconds_to_human_format}, }; -use transmission_rpc::types::{ErrorType, Id, Torrent, TorrentStatus}; +use transmission_rpc::types::{ErrorType, FileStat, Id, Torrent, TorrentStatus}; #[derive(Clone)] pub struct RustmissionTorrent { @@ -25,8 +25,9 @@ pub struct RustmissionTorrent { style: Style, pub id: Id, pub download_dir: String, - pub activity_date: NaiveDateTime, - pub added_date: NaiveDateTime, + pub file_stats: Vec, + pub activity_date: DateTime, + pub added_date: DateTime, pub peers_connected: i64, pub category: Option, pub error: Option, @@ -328,6 +329,8 @@ impl From for RustmissionTorrent { let download_dir = t.download_dir.clone().expect("field requested"); + let file_stats = t.file_stats.expect("field requested"); + let uploaded_ever = bytes_to_human_format(t.uploaded_ever.expect("field requested")); let upload_ratio = { @@ -335,19 +338,9 @@ impl From for RustmissionTorrent { format!("{:.1}", raw) }; - let activity_date = { - let raw = t.activity_date.expect("field requested"); - chrono::DateTime::from_timestamp(raw, 0) - .unwrap() - .naive_local() - }; + let activity_date = t.activity_date.expect("field requested"); - let added_date = { - let raw = t.added_date.expect("field requested"); - chrono::DateTime::from_timestamp(raw, 0) - .unwrap() - .naive_local() - }; + let added_date = t.added_date.expect("field requested"); let peers_connected = t.peers_connected.expect("field requested"); @@ -390,6 +383,7 @@ impl From for RustmissionTorrent { style, id, download_dir, + file_stats, uploaded_ever, upload_ratio, activity_date, @@ -402,7 +396,7 @@ impl From for RustmissionTorrent { } } -fn time_to_line<'a>(time: NaiveDateTime) -> Line<'a> { +fn time_to_line<'a>(time: DateTime) -> 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()) diff --git a/rm-main/src/tui/tabs/torrents/task_manager.rs b/rm-main/src/tui/tabs/torrents/task_manager.rs index 84c3973..9aed683 100644 --- a/rm-main/src/tui/tabs/torrents/task_manager.rs +++ b/rm-main/src/tui/tabs/torrents/task_manager.rs @@ -9,8 +9,8 @@ use rm_shared::{ use transmission_rpc::types::Id; use crate::tui::{ - app::CTX, components::{Component, ComponentAction}, + ctx::CTX, }; use super::tasks::{self, CurrentTaskState, TorrentSelection}; diff --git a/rm-main/src/tui/tabs/torrents/tasks/add_magnet.rs b/rm-main/src/tui/tabs/torrents/tasks/add_magnet.rs index 3a447fc..719d57e 100644 --- a/rm-main/src/tui/tabs/torrents/tasks/add_magnet.rs +++ b/rm-main/src/tui/tabs/torrents/tasks/add_magnet.rs @@ -5,8 +5,8 @@ use rm_config::CONFIG; use crate::{ transmission::TorrentAction, tui::{ - app::CTX, components::{Component, ComponentAction, InputManager}, + ctx::CTX, tabs::torrents::SESSION_GET, }, }; diff --git a/rm-main/src/tui/tabs/torrents/tasks/change_category.rs b/rm-main/src/tui/tabs/torrents/tasks/change_category.rs index 6a7a4dc..ce2515d 100644 --- a/rm-main/src/tui/tabs/torrents/tasks/change_category.rs +++ b/rm-main/src/tui/tabs/torrents/tasks/change_category.rs @@ -9,8 +9,8 @@ use rm_shared::{ use crate::{ transmission::TorrentAction, tui::{ - app::CTX, components::{Component, ComponentAction, InputManager}, + ctx::CTX, tabs::torrents::SESSION_GET, }, }; diff --git a/rm-main/src/tui/tabs/torrents/tasks/delete_torrent.rs b/rm-main/src/tui/tabs/torrents/tasks/delete_torrent.rs index 9e0b39f..9656275 100644 --- a/rm-main/src/tui/tabs/torrents/tasks/delete_torrent.rs +++ b/rm-main/src/tui/tabs/torrents/tasks/delete_torrent.rs @@ -2,8 +2,8 @@ use crossterm::event::KeyCode; use ratatui::prelude::*; use crate::transmission::TorrentAction; -use crate::tui::app::CTX; use crate::tui::components::{Component, ComponentAction, InputManager}; +use crate::tui::ctx::CTX; use rm_shared::action::{Action, UpdateAction}; use rm_shared::status_task::StatusTask; diff --git a/rm-main/src/tui/tabs/torrents/tasks/filter.rs b/rm-main/src/tui/tabs/torrents/tasks/filter.rs index 754db4e..c53e6ae 100644 --- a/rm-main/src/tui/tabs/torrents/tasks/filter.rs +++ b/rm-main/src/tui/tabs/torrents/tasks/filter.rs @@ -4,8 +4,8 @@ use ratatui::prelude::*; use rm_shared::action::{Action, UpdateAction}; use crate::tui::{ - app::CTX, components::{Component, ComponentAction, InputManager}, + ctx::CTX, }; pub struct Filter { diff --git a/rm-main/src/tui/tabs/torrents/tasks/move_torrent.rs b/rm-main/src/tui/tabs/torrents/tasks/move_torrent.rs index 08fe180..43da81e 100644 --- a/rm-main/src/tui/tabs/torrents/tasks/move_torrent.rs +++ b/rm-main/src/tui/tabs/torrents/tasks/move_torrent.rs @@ -8,8 +8,8 @@ use rm_shared::{ use crate::{ transmission::TorrentAction, tui::{ - app::{self, CTX}, components::{Component, ComponentAction, InputManager}, + ctx::CTX, }, }; diff --git a/rm-main/src/tui/tabs/torrents/tasks/rename.rs b/rm-main/src/tui/tabs/torrents/tasks/rename.rs index 1929c69..c6547af 100644 --- a/rm-main/src/tui/tabs/torrents/tasks/rename.rs +++ b/rm-main/src/tui/tabs/torrents/tasks/rename.rs @@ -9,8 +9,8 @@ use transmission_rpc::types::Id; use crate::{ transmission::TorrentAction, tui::{ - app::CTX, components::{Component, ComponentAction, InputManager}, + ctx::CTX, }, }; diff --git a/rm-main/src/tui/tabs/torrents/tasks/status.rs b/rm-main/src/tui/tabs/torrents/tasks/status.rs index 4b72e27..9fa102f 100644 --- a/rm-main/src/tui/tabs/torrents/tasks/status.rs +++ b/rm-main/src/tui/tabs/torrents/tasks/status.rs @@ -6,7 +6,7 @@ use rm_shared::{ use throbber_widgets_tui::ThrobberState; use tokio::time::{self, Instant}; -use crate::tui::{app::CTX, components::Component}; +use crate::tui::{components::Component, ctx::CTX}; pub struct Status { task: StatusTask, diff --git a/rm-shared/Cargo.toml b/rm-shared/Cargo.toml index edd3ac4..8d16867 100644 --- a/rm-shared/Cargo.toml +++ b/rm-shared/Cargo.toml @@ -14,6 +14,7 @@ license.workspace = true crossterm.workspace = true transmission-rpc.workspace = true magnetease.workspace = true +color-eyre.workspace = true ratatui.workspace = true chrono.workspace = true serde.workspace = true diff --git a/rm-shared/src/action.rs b/rm-shared/src/action.rs index c029077..a194330 100644 --- a/rm-shared/src/action.rs +++ b/rm-shared/src/action.rs @@ -4,7 +4,7 @@ use crossterm::event::KeyEvent; use magnetease::{MagneteaseError, MagneteaseResult}; use transmission_rpc::types::{FreeSpace, SessionGet, SessionStats, Torrent}; -use crate::status_task::StatusTask; +use crate::{current_window::TorrentWindow, status_task::StatusTask}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Action { @@ -41,6 +41,7 @@ pub enum Action { AddMagnet, MoveTorrent, ChangeCategory, + ChangeFilePriority, Rename, // Search Tab ShowProvidersInfo, @@ -51,7 +52,9 @@ pub enum UpdateAction { SwitchToInputMode, SwitchToNormalMode, Error(Box), + UnrecoverableError(Box), // Torrents Tab + ChangeTorrentWindow(TorrentWindow), SessionStats(Arc), SessionGet(Arc), FreeSpace(Arc), @@ -73,23 +76,23 @@ pub enum UpdateAction { StatusTaskSetSuccess(StatusTask), } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug)] pub struct ErrorMessage { pub title: String, pub description: String, - pub source: String, + pub source: Box, } impl ErrorMessage { pub fn new( title: impl Into, message: impl Into, - error: Box, + error: Box, ) -> Self { Self { title: title.into(), description: message.into(), - source: error.to_string(), + source: error, } } } diff --git a/rm-shared/src/current_window.rs b/rm-shared/src/current_window.rs new file mode 100644 index 0000000..692f3f1 --- /dev/null +++ b/rm-shared/src/current_window.rs @@ -0,0 +1,16 @@ +#[derive(Debug, Clone, Copy)] +pub enum Window { + Torrents(TorrentWindow), + Search(SearchWindow), +} + +#[derive(Debug, Clone, Copy)] +pub enum TorrentWindow { + General, + FileViewer, +} + +#[derive(Debug, Clone, Copy)] +pub enum SearchWindow { + General, +} diff --git a/rm-shared/src/lib.rs b/rm-shared/src/lib.rs index b3e2933..07f3dc3 100644 --- a/rm-shared/src/lib.rs +++ b/rm-shared/src/lib.rs @@ -1,4 +1,5 @@ pub mod action; +pub mod current_window; pub mod header; pub mod status_task; pub mod utils;