diff --git a/src/app/command.rs b/src/app/command.rs index 9c3d7ed..9aa7ab1 100644 --- a/src/app/command.rs +++ b/src/app/command.rs @@ -23,7 +23,9 @@ impl Torrents { if !to_start.is_empty() { let ids: Vec<_> = to_start.into_iter().map(|(id, _)| id).collect(); - self.client.torrent_action(TorrentAction::Start, ids).await?; + self.client + .torrent_action(TorrentAction::Start, ids) + .await?; } if !to_stop.is_empty() { let ids: Vec<_> = to_stop.into_iter().map(|(id, _)| id).collect(); @@ -133,7 +135,11 @@ impl Torrents { return Ok(()); }; self.client - .torrent_rename_path(vec![id], old_name.clone(), name.to_string_lossy().into_owned()) + .torrent_rename_path( + vec![id], + old_name.clone(), + name.to_string_lossy().into_owned(), + ) .await?; Ok(()) } diff --git a/src/app/mod.rs b/src/app/mod.rs index a53640c..0b7effd 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -14,6 +14,15 @@ use std::path::PathBuf; use types::Selected; pub use {tab::Tab, torrent::Torrents}; +/// Input mode type for the application. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum InputMode { + #[default] + None, + Move, + Rename, +} + /// Main Application. #[derive(Debug)] pub struct App { @@ -25,7 +34,7 @@ pub struct App { pub show_help: bool, pub config: Config, pub input_handler: InputHandler, - pub input_mode: bool, + pub input_mode: InputMode, } impl App { @@ -41,11 +50,11 @@ impl App { tabs: vec![Tab::All, Tab::Active, Tab::Downloading], index: 0, state: TableState::default(), - torrents: Torrents::new()?, // Handle the Result here + torrents: Torrents::new()?, show_help: false, config, input_handler: InputHandler::new(), - input_mode: false, + input_mode: InputMode::None, }) } @@ -178,18 +187,49 @@ impl App { self.torrents .move_torrents(ids, &self.input_handler.text) .await?; - self.input_handler.clear(); - self.input_mode = false; - self.close_help(); + self.clear_input(); Ok(()) } + /// Prepare move action by pre-filling current download directory. pub fn prepare_move_action(&mut self) { if let Some(download_dir) = self.get_current_download_dir() { self.input_handler .set_text(download_dir.to_string_lossy().into_owned()); } - self.input_mode = true; + self.input_mode = InputMode::Move; + } + + /// Rename the highlighted torrent. + /// + /// # Errors + /// + /// Returns an error if the RPC call fails. + pub async fn rename_torrent(&mut self) -> Result<()> { + let Some(torrent) = self.get_current_torrent() else { + self.clear_input(); + return Ok(()); + }; + self.torrents + .rename(&torrent, std::path::Path::new(&self.input_handler.text)) + .await?; + self.clear_input(); + Ok(()) + } + + /// Prepare rename action by pre-filling current torrent name. + pub fn prepare_rename_action(&mut self) { + if let Some(name) = self.get_current_torrent_name() { + self.input_handler.set_text(name); + } + self.input_mode = InputMode::Rename; + } + + /// Clear input and reset input mode. + fn clear_input(&mut self) { + self.input_handler.clear(); + self.input_mode = InputMode::None; + self.close_help(); } pub fn select(&mut self) { @@ -222,6 +262,16 @@ impl App { } fn get_current_download_dir(&self) -> Option { + self.get_current_torrent() + .and_then(|t| t.download_dir) + .map(PathBuf::from) + } + + fn get_current_torrent_name(&self) -> Option { + self.get_current_torrent().and_then(|t| t.name) + } + + fn get_current_torrent(&self) -> Option { let Selected::Current(current_id) = self.selected(true) else { return None; }; @@ -229,7 +279,6 @@ impl App { .torrents .iter() .find(|t| t.id == Some(current_id)) - .and_then(|t| t.download_dir.as_deref()) - .map(PathBuf::from) + .cloned() } } diff --git a/src/app/torrent.rs b/src/app/torrent.rs index 196ba5f..2494ea9 100644 --- a/src/app/torrent.rs +++ b/src/app/torrent.rs @@ -75,7 +75,6 @@ impl Torrents { .torrents; Ok(self) } - } impl Debug for Torrents { diff --git a/src/config/keybinds.rs b/src/config/keybinds.rs index facb938..cbde39b 100644 --- a/src/config/keybinds.rs +++ b/src/config/keybinds.rs @@ -32,4 +32,6 @@ pub struct KeybindsConfig { pub toggle_help: String, #[from_file(default = "m")] pub move_torrent: String, + #[from_file(default = "r")] + pub rename_torrent: String, } diff --git a/src/handler.rs b/src/handler.rs index 6db9421..ce757df 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,4 +1,4 @@ -use crate::app::{App, action::Action}; +use crate::app::{App, InputMode, action::Action}; use crate::error::Result; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use thiserror::Error; @@ -29,10 +29,10 @@ async fn handle_input(key_event: KeyEvent, app: &mut App) -> Result Result> { - if app.input_mode { + if app.input_mode != InputMode::None { return handle_input(key_event, app).await; } @@ -56,6 +56,7 @@ pub async fn get_action(key_event: KeyEvent, app: &mut App) -> Result Result Result<()> { info!("updating app with action: {}", action); @@ -82,13 +83,17 @@ pub async fn update(app: &mut App, action: Action) -> Result<()> { Action::PauseAll => app.torrents.stop_all().await?, Action::StartAll => app.torrents.start_all().await?, Action::Move => app.prepare_move_action(), + Action::Rename => app.prepare_rename_action(), Action::Delete(x) => app.delete(x).await?, - Action::Rename => unimplemented!(), Action::Select => app.select(), - Action::Submit => app.move_torrent().await?, + Action::Submit => match app.input_mode { + InputMode::Move => app.move_torrent().await?, + InputMode::Rename => app.rename_torrent().await?, + InputMode::None => {} + }, Action::Cancel => { app.input_handler.clear(); - app.input_mode = false; + app.input_mode = InputMode::None; } } Ok(()) @@ -162,11 +167,10 @@ fn parse_keybind(key_str: &str) -> std::result::Result f if f.starts_with('f') => { - key_code = Some(KeyCode::F( - f[1..] - .parse() - .map_err(|_| ParseKeybindError::UnknownPart(part.to_owned()))?, - )); + key_code = + Some(KeyCode::F(f[1..].parse().map_err(|_| { + ParseKeybindError::UnknownPart(part.to_owned()) + })?)); } // single-character fallback diff --git a/src/ui/input.rs b/src/ui/input.rs index 3423149..7e2e558 100644 --- a/src/ui/input.rs +++ b/src/ui/input.rs @@ -1,4 +1,4 @@ -use crate::app::App; +use crate::app::{App, InputMode}; use ratatui::{ prelude::*, widgets::{Block, Borders, Clear, Paragraph}, @@ -9,7 +9,13 @@ pub fn render(f: &mut Frame, app: &App) { let size = f.area(); let input_area = Rect::new(size.width / 4, size.height / 2 - 1, size.width / 2, 3); - let block = Block::default().title("Move to").borders(Borders::ALL); + let title = match app.input_mode { + InputMode::Move => "Move to", + InputMode::Rename => "Rename", + InputMode::None => return, + }; + + let block = Block::default().title(title).borders(Borders::ALL); f.render_widget(Clear, input_area); f.render_widget(block, input_area); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 8dfe67d..16cae0b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -3,7 +3,7 @@ mod input; mod table; use crate::{ - app::{App, Tab}, + app::{App, InputMode, Tab}, config::color::ColorConfig, }; use help::render_help; @@ -58,7 +58,7 @@ pub fn render(app: &mut App, frame: &mut Frame) { render_help(frame, app); } - if app.input_mode { + if app.input_mode != InputMode::None { input::render(frame, app); } } diff --git a/tests/handler.rs b/tests/handler.rs index 77a638e..6c7d37d 100644 --- a/tests/handler.rs +++ b/tests/handler.rs @@ -1,5 +1,5 @@ use crossterm::event::{KeyCode, KeyEvent}; -use traxor::{app::App, app::action::Action, config::Config, handler::get_action}; +use traxor::{app::App, app::InputMode, app::action::Action, config::Config, handler::get_action}; #[tokio::test] async fn test_get_action_quit() { @@ -137,7 +137,7 @@ async fn test_get_action_toggle_help() { async fn test_get_action_input_mode() { let config = Config::load().unwrap(); let mut app = App::new(config).unwrap(); - app.input_mode = true; + app.input_mode = InputMode::Move; assert_eq!( get_action(KeyEvent::from(KeyCode::Enter), &mut app) .await