mirror of
https://github.com/kristoferssolo/traxor.git
synced 2025-10-21 20:10:35 +00:00
feat(move): add move functionality
This commit is contained in:
parent
095b102ecf
commit
8870478cc6
@ -13,6 +13,7 @@ delete = "d"
|
|||||||
delete_force = "D"
|
delete_force = "D"
|
||||||
select = " "
|
select = " "
|
||||||
toggle_help = "?"
|
toggle_help = "?"
|
||||||
|
move = "m"
|
||||||
|
|
||||||
[colors]
|
[colors]
|
||||||
highlight_background = "magenta"
|
highlight_background = "magenta"
|
||||||
|
|||||||
@ -6,7 +6,7 @@ pub enum Action {
|
|||||||
NextTorrent,
|
NextTorrent,
|
||||||
PrevTorrent,
|
PrevTorrent,
|
||||||
SwitchTab(u8),
|
SwitchTab(u8),
|
||||||
ToggleHelp, // Add this line
|
ToggleHelp,
|
||||||
ToggleTorrent,
|
ToggleTorrent,
|
||||||
ToggleAll,
|
ToggleAll,
|
||||||
PauseAll,
|
PauseAll,
|
||||||
@ -15,4 +15,6 @@ pub enum Action {
|
|||||||
Delete(bool),
|
Delete(bool),
|
||||||
Rename,
|
Rename,
|
||||||
Select,
|
Select,
|
||||||
|
Submit,
|
||||||
|
Cancel,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
use super::{types::Selected, Torrents};
|
use super::{types::Selected, Torrents};
|
||||||
|
use color_eyre::{eyre::eyre, Result};
|
||||||
use std::{collections::HashSet, path::Path};
|
use std::{collections::HashSet, path::Path};
|
||||||
use transmission_rpc::types::{Torrent, TorrentAction, TorrentStatus};
|
use transmission_rpc::types::{Torrent, TorrentAction, TorrentStatus};
|
||||||
|
|
||||||
impl Torrents {
|
impl Torrents {
|
||||||
pub async fn toggle(&mut self, ids: Selected) -> color_eyre::eyre::Result<()> {
|
pub async fn toggle(&mut self, ids: Selected) -> Result<()> {
|
||||||
let ids: HashSet<_> = ids.into();
|
let ids: HashSet<_> = ids.into();
|
||||||
let torrents_to_toggle: Vec<_> = self
|
let torrents_to_toggle: Vec<_> = self
|
||||||
.torrents
|
.torrents
|
||||||
@ -20,15 +21,13 @@ impl Torrents {
|
|||||||
self.client
|
self.client
|
||||||
.torrent_action(action, vec![id])
|
.torrent_action(action, vec![id])
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||||
color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string())
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn toggle_all(&mut self) -> color_eyre::eyre::Result<()> {
|
pub async fn toggle_all(&mut self) -> Result<()> {
|
||||||
let torrents_to_toggle: Vec<_> = self
|
let torrents_to_toggle: Vec<_> = self
|
||||||
.torrents
|
.torrents
|
||||||
.iter()
|
.iter()
|
||||||
@ -49,18 +48,16 @@ impl Torrents {
|
|||||||
self.client
|
self.client
|
||||||
.torrent_action(action, vec![id])
|
.torrent_action(action, vec![id])
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||||
color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string())
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_all(&mut self) -> color_eyre::eyre::Result<()> {
|
pub async fn start_all(&mut self) -> Result<()> {
|
||||||
self.action_all(TorrentAction::StartNow).await
|
self.action_all(TorrentAction::StartNow).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stop_all(&mut self) -> color_eyre::eyre::Result<()> {
|
pub async fn stop_all(&mut self) -> Result<()> {
|
||||||
self.action_all(TorrentAction::Stop).await
|
self.action_all(TorrentAction::Stop).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,43 +66,35 @@ impl Torrents {
|
|||||||
torrent: &Torrent,
|
torrent: &Torrent,
|
||||||
location: &Path,
|
location: &Path,
|
||||||
move_from: Option<bool>,
|
move_from: Option<bool>,
|
||||||
) -> color_eyre::eyre::Result<()> {
|
) -> Result<()> {
|
||||||
if let Some(id) = torrent.id() {
|
if let Some(id) = torrent.id() {
|
||||||
self.client
|
self.client
|
||||||
.torrent_set_location(vec![id], location.to_string_lossy().into(), move_from)
|
.torrent_set_location(vec![id], location.to_string_lossy().into(), move_from)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||||
color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string())
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete(
|
pub async fn delete(&mut self, ids: Selected, delete_local_data: bool) -> Result<()> {
|
||||||
&mut self,
|
|
||||||
ids: Selected,
|
|
||||||
delete_local_data: bool,
|
|
||||||
) -> color_eyre::eyre::Result<()> {
|
|
||||||
self.client
|
self.client
|
||||||
.torrent_remove(ids.into(), delete_local_data)
|
.torrent_remove(ids.into(), delete_local_data)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?;
|
.map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn rename(&mut self, torrent: &Torrent, name: &Path) -> color_eyre::eyre::Result<()> {
|
pub async fn rename(&mut self, torrent: &Torrent, name: &Path) -> Result<()> {
|
||||||
if let (Some(id), Some(old_name)) = (torrent.id(), torrent.name.clone()) {
|
if let (Some(id), Some(old_name)) = (torrent.id(), torrent.name.clone()) {
|
||||||
self.client
|
self.client
|
||||||
.torrent_rename_path(vec![id], old_name, name.to_string_lossy().into())
|
.torrent_rename_path(vec![id], old_name, name.to_string_lossy().into())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||||
color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string())
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn action_all(&mut self, action: TorrentAction) -> color_eyre::eyre::Result<()> {
|
async fn action_all(&mut self, action: TorrentAction) -> Result<()> {
|
||||||
let ids = self
|
let ids = self
|
||||||
.torrents
|
.torrents
|
||||||
.iter()
|
.iter()
|
||||||
@ -115,7 +104,7 @@ impl Torrents {
|
|||||||
self.client
|
self.client
|
||||||
.torrent_action(action, ids)
|
.torrent_action(action, ids)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?;
|
.map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ pub mod types;
|
|||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use color_eyre::Result;
|
||||||
use ratatui::widgets::TableState;
|
use ratatui::widgets::TableState;
|
||||||
use types::Selected;
|
use types::Selected;
|
||||||
pub use {tab::Tab, torrent::Torrents};
|
pub use {tab::Tab, torrent::Torrents};
|
||||||
@ -20,12 +21,15 @@ pub struct App<'a> {
|
|||||||
pub torrents: Torrents,
|
pub torrents: Torrents,
|
||||||
pub show_help: bool,
|
pub show_help: bool,
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
|
pub input: String,
|
||||||
|
pub cursor_position: usize,
|
||||||
|
pub input_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> App<'a> {
|
impl<'a> App<'a> {
|
||||||
/// Constructs a new instance of [`App`].
|
/// Constructs a new instance of [`App`].
|
||||||
/// Returns instance of `Self`.
|
/// Returns instance of `Self`.
|
||||||
pub fn new(config: Config) -> color_eyre::eyre::Result<Self> {
|
pub fn new(config: Config) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
running: true,
|
running: true,
|
||||||
tabs: &[Tab::All, Tab::Active, Tab::Downloading],
|
tabs: &[Tab::All, Tab::Active, Tab::Downloading],
|
||||||
@ -34,11 +38,14 @@ impl<'a> App<'a> {
|
|||||||
torrents: Torrents::new()?, // Handle the Result here
|
torrents: Torrents::new()?, // Handle the Result here
|
||||||
show_help: false,
|
show_help: false,
|
||||||
config,
|
config,
|
||||||
|
input: String::new(),
|
||||||
|
cursor_position: 0,
|
||||||
|
input_mode: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the tick event of the terminal.
|
/// Handles the tick event of the terminal.
|
||||||
pub async fn tick(&mut self) -> color_eyre::eyre::Result<()> {
|
pub async fn tick(&mut self) -> Result<()> {
|
||||||
self.torrents.update().await?;
|
self.torrents.update().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -122,20 +129,28 @@ impl<'a> App<'a> {
|
|||||||
self.show_help = true;
|
self.show_help = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn toggle_torrents(&mut self) -> color_eyre::eyre::Result<()> {
|
pub async fn toggle_torrents(&mut self) -> Result<()> {
|
||||||
let ids = self.selected(false);
|
let ids = self.selected(false);
|
||||||
self.torrents.toggle(ids).await?;
|
self.torrents.toggle(ids).await?;
|
||||||
self.close_help();
|
self.close_help();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete(&mut self, delete_local_data: bool) -> color_eyre::eyre::Result<()> {
|
pub async fn delete(&mut self, delete_local_data: bool) -> Result<()> {
|
||||||
let ids = self.selected(false);
|
let ids = self.selected(false);
|
||||||
self.torrents.delete(ids, delete_local_data).await?;
|
self.torrents.delete(ids, delete_local_data).await?;
|
||||||
self.close_help();
|
self.close_help();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn move_torrent(&mut self) -> Result<()> {
|
||||||
|
self.torrents.move_selection(&self.input).await?;
|
||||||
|
self.input.clear();
|
||||||
|
self.cursor_position = 0;
|
||||||
|
self.input_mode = false;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn select(&mut self) {
|
pub fn select(&mut self) {
|
||||||
if let Selected::Current(current_id) = self.selected(true) {
|
if let Selected::Current(current_id) = self.selected(true) {
|
||||||
if self.torrents.selected.contains(¤t_id) {
|
if self.torrents.selected.contains(¤t_id) {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use color_eyre::eyre::Result;
|
use color_eyre::{eyre::eyre, Result};
|
||||||
use std::{collections::HashSet, fmt::Debug};
|
use std::{collections::HashSet, fmt::Debug};
|
||||||
use transmission_rpc::{
|
use transmission_rpc::{
|
||||||
types::{Torrent, TorrentGetField},
|
types::{Id, Torrent, TorrentGetField},
|
||||||
TransClient,
|
TransClient,
|
||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
@ -50,11 +50,20 @@ impl Torrents {
|
|||||||
.client
|
.client
|
||||||
.torrent_get(self.fields.clone(), None)
|
.torrent_get(self.fields.clone(), None)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?
|
.map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?
|
||||||
.arguments
|
.arguments
|
||||||
.torrents;
|
.torrents;
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn move_selection(&mut self, location: &str) -> Result<()> {
|
||||||
|
let ids: Vec<Id> = self.selected.iter().map(|id| Id::Id(*id)).collect();
|
||||||
|
self.client
|
||||||
|
.torrent_set_location(ids, location.to_string(), Some(true))
|
||||||
|
.await
|
||||||
|
.map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for Torrents {
|
impl Debug for Torrents {
|
||||||
@ -66,7 +75,9 @@ impl Debug for Torrents {
|
|||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"fields:
|
"fields:
|
||||||
{:?};\n\ntorrents: {:?}",
|
{:?};
|
||||||
|
|
||||||
|
torrents: {:?}",
|
||||||
fields, self.torrents
|
fields, self.torrents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,11 +11,11 @@ use transmission_rpc::types::{
|
|||||||
|
|
||||||
pub trait Wrapper {
|
pub trait Wrapper {
|
||||||
fn title(&self) -> String {
|
fn title(&self) -> String {
|
||||||
"".to_string()
|
String::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn value(&self, torrent: &Torrent) -> String {
|
fn value(&self, torrent: &Torrent) -> String {
|
||||||
format!("{}", torrent.name.as_ref().unwrap_or(&String::from("")))
|
torrent.name.clone().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn width(&self) -> u16 {
|
fn width(&self) -> u16 {
|
||||||
|
|||||||
@ -18,6 +18,7 @@ pub struct KeybindsConfig {
|
|||||||
pub delete_force: Option<String>,
|
pub delete_force: Option<String>,
|
||||||
pub select: Option<String>,
|
pub select: Option<String>,
|
||||||
pub toggle_help: Option<String>,
|
pub toggle_help: Option<String>,
|
||||||
|
pub move_torrent: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for KeybindsConfig {
|
impl Default for KeybindsConfig {
|
||||||
@ -37,6 +38,7 @@ impl Default for KeybindsConfig {
|
|||||||
delete_force: Some("D".to_string()),
|
delete_force: Some("D".to_string()),
|
||||||
select: Some(" ".to_string()),
|
select: Some(" ".to_string()),
|
||||||
toggle_help: Some("?".to_string()),
|
toggle_help: Some("?".to_string()),
|
||||||
|
move_torrent: Some("m".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use color_eyre::eyre::Result;
|
use color_eyre::Result;
|
||||||
use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent};
|
use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent};
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|||||||
213
src/handler.rs
213
src/handler.rs
@ -1,109 +1,68 @@
|
|||||||
use crate::app::{action::Action, App};
|
use crate::app::{action::Action, App};
|
||||||
|
use color_eyre::Result;
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
use tracing::{event, info_span, Level};
|
use tracing::{event, info_span, Level};
|
||||||
|
|
||||||
/// Handles the key events of [`App`].
|
fn handle_input(key_event: KeyEvent, app: &mut App) -> Option<Action> {
|
||||||
#[tracing::instrument]
|
|
||||||
pub fn get_action(key_event: KeyEvent, app: &App) -> Option<Action> {
|
|
||||||
let span = info_span!("get_action");
|
|
||||||
let _enter = span.enter();
|
|
||||||
event!(Level::INFO, "handling key event: {:?}", key_event);
|
|
||||||
|
|
||||||
let config_keybinds = &app.config.keybinds;
|
|
||||||
|
|
||||||
// Helper to check if a KeyEvent matches a configured keybind string
|
|
||||||
let matches_keybind = |event: &KeyEvent, config_key: &Option<String>| {
|
|
||||||
if let Some(key_str) = config_key {
|
|
||||||
let parts: Vec<&str> = key_str.split('+').collect();
|
|
||||||
let mut parsed_modifiers = KeyModifiers::NONE;
|
|
||||||
let mut parsed_key_code = None;
|
|
||||||
|
|
||||||
for part in &parts {
|
|
||||||
match part.to_lowercase().as_str() {
|
|
||||||
"ctrl" => parsed_modifiers.insert(KeyModifiers::CONTROL),
|
|
||||||
"alt" => parsed_modifiers.insert(KeyModifiers::ALT),
|
|
||||||
"shift" => parsed_modifiers.insert(KeyModifiers::SHIFT),
|
|
||||||
"esc" => parsed_key_code = Some(KeyCode::Esc),
|
|
||||||
"enter" => parsed_key_code = Some(KeyCode::Enter),
|
|
||||||
"left" => parsed_key_code = Some(KeyCode::Left),
|
|
||||||
"right" => parsed_key_code = Some(KeyCode::Right),
|
|
||||||
"up" => parsed_key_code = Some(KeyCode::Up),
|
|
||||||
"down" => parsed_key_code = Some(KeyCode::Down),
|
|
||||||
"tab" => parsed_key_code = Some(KeyCode::Tab),
|
|
||||||
"backspace" => parsed_key_code = Some(KeyCode::Backspace),
|
|
||||||
"delete" => parsed_key_code = Some(KeyCode::Delete),
|
|
||||||
"home" => parsed_key_code = Some(KeyCode::Home),
|
|
||||||
"end" => parsed_key_code = Some(KeyCode::End),
|
|
||||||
"pageup" => parsed_key_code = Some(KeyCode::PageUp),
|
|
||||||
"pagedown" => parsed_key_code = Some(KeyCode::PageDown),
|
|
||||||
"null" => parsed_key_code = Some(KeyCode::Null),
|
|
||||||
"insert" => parsed_key_code = Some(KeyCode::Insert),
|
|
||||||
_ => {
|
|
||||||
if part.len() == 1 {
|
|
||||||
if let Some(c) = part.chars().next() {
|
|
||||||
parsed_key_code = Some(KeyCode::Char(c));
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if part.starts_with("f") && part.len() > 1 {
|
|
||||||
if let Ok(f_num) = part[1..].parse::<u8>() {
|
|
||||||
parsed_key_code = Some(KeyCode::F(f_num));
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsed_key_code.is_none() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.code == parsed_key_code.unwrap() && event.modifiers == parsed_modifiers
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match key_event.code {
|
match key_event.code {
|
||||||
_ if matches_keybind(&key_event, &config_keybinds.quit) => Some(Action::Quit),
|
KeyCode::Enter => Some(Action::Submit),
|
||||||
_ if matches_keybind(&key_event, &config_keybinds.next_tab) => Some(Action::NextTab),
|
KeyCode::Char(c) => {
|
||||||
_ if matches_keybind(&key_event, &config_keybinds.prev_tab) => Some(Action::PrevTab),
|
app.input.push(c);
|
||||||
_ if matches_keybind(&key_event, &config_keybinds.next_torrent) => {
|
app.cursor_position = app.input.len();
|
||||||
Some(Action::NextTorrent)
|
None
|
||||||
}
|
}
|
||||||
_ if matches_keybind(&key_event, &config_keybinds.prev_torrent) => {
|
KeyCode::Backspace => {
|
||||||
Some(Action::PrevTorrent)
|
app.input.pop();
|
||||||
|
app.cursor_position = app.input.len();
|
||||||
|
None
|
||||||
}
|
}
|
||||||
_ if matches_keybind(&key_event, &config_keybinds.switch_tab_1) => {
|
KeyCode::Esc => Some(Action::Cancel),
|
||||||
Some(Action::SwitchTab(0))
|
|
||||||
}
|
|
||||||
_ if matches_keybind(&key_event, &config_keybinds.switch_tab_2) => {
|
|
||||||
Some(Action::SwitchTab(1))
|
|
||||||
}
|
|
||||||
_ if matches_keybind(&key_event, &config_keybinds.switch_tab_3) => {
|
|
||||||
Some(Action::SwitchTab(2))
|
|
||||||
}
|
|
||||||
_ if matches_keybind(&key_event, &config_keybinds.toggle_torrent) => {
|
|
||||||
Some(Action::ToggleTorrent)
|
|
||||||
}
|
|
||||||
_ if matches_keybind(&key_event, &config_keybinds.toggle_all) => Some(Action::ToggleAll),
|
|
||||||
_ if matches_keybind(&key_event, &config_keybinds.delete) => Some(Action::Delete(false)),
|
|
||||||
_ if matches_keybind(&key_event, &config_keybinds.delete_force) => {
|
|
||||||
Some(Action::Delete(true))
|
|
||||||
}
|
|
||||||
_ if matches_keybind(&key_event, &config_keybinds.select) => Some(Action::Select),
|
|
||||||
_ if matches_keybind(&key_event, &config_keybinds.toggle_help) => Some(Action::ToggleHelp),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles the key events of [`App`].
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub fn get_action(key_event: KeyEvent, app: &mut App) -> Option<Action> {
|
||||||
|
if app.input_mode {
|
||||||
|
return handle_input(key_event, app);
|
||||||
|
}
|
||||||
|
|
||||||
|
let span = info_span!("get_action");
|
||||||
|
let _enter = span.enter();
|
||||||
|
event!(Level::INFO, "handling key event: {:?}", key_event);
|
||||||
|
|
||||||
|
let keybinds = &app.config.keybinds;
|
||||||
|
|
||||||
|
let actions = [
|
||||||
|
(Action::Quit, &keybinds.quit),
|
||||||
|
(Action::NextTab, &keybinds.next_tab),
|
||||||
|
(Action::PrevTab, &keybinds.prev_tab),
|
||||||
|
(Action::NextTorrent, &keybinds.next_torrent),
|
||||||
|
(Action::PrevTorrent, &keybinds.prev_torrent),
|
||||||
|
(Action::SwitchTab(0), &keybinds.switch_tab_1),
|
||||||
|
(Action::SwitchTab(1), &keybinds.switch_tab_2),
|
||||||
|
(Action::SwitchTab(2), &keybinds.switch_tab_3),
|
||||||
|
(Action::ToggleTorrent, &keybinds.toggle_torrent),
|
||||||
|
(Action::ToggleAll, &keybinds.toggle_all),
|
||||||
|
(Action::Delete(false), &keybinds.delete),
|
||||||
|
(Action::Delete(true), &keybinds.delete_force),
|
||||||
|
(Action::Select, &keybinds.select),
|
||||||
|
(Action::ToggleHelp, &keybinds.toggle_help),
|
||||||
|
(Action::Move, &keybinds.move_torrent),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (action, keybind) in actions {
|
||||||
|
if matches_keybind(&key_event, keybind) {
|
||||||
|
return Some(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Handles the updates of [`App`].
|
/// Handles the updates of [`App`].
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn update(app: &mut App<'_>, action: Action) -> color_eyre::eyre::Result<()> {
|
pub async fn update(app: &mut App<'_>, action: Action) -> Result<()> {
|
||||||
let span = info_span!("update");
|
let span = info_span!("update");
|
||||||
let _enter = span.enter();
|
let _enter = span.enter();
|
||||||
event!(Level::INFO, "updating app with action: {:?}", action);
|
event!(Level::INFO, "updating app with action: {:?}", action);
|
||||||
@ -119,10 +78,76 @@ pub async fn update(app: &mut App<'_>, action: Action) -> color_eyre::eyre::Resu
|
|||||||
Action::ToggleAll => app.torrents.toggle_all().await?,
|
Action::ToggleAll => app.torrents.toggle_all().await?,
|
||||||
Action::PauseAll => app.torrents.stop_all().await?,
|
Action::PauseAll => app.torrents.stop_all().await?,
|
||||||
Action::StartAll => app.torrents.start_all().await?,
|
Action::StartAll => app.torrents.start_all().await?,
|
||||||
Action::Move => unimplemented!(),
|
Action::Move => app.input_mode = true,
|
||||||
Action::Delete(x) => app.delete(x).await?,
|
Action::Delete(x) => app.delete(x).await?,
|
||||||
Action::Rename => unimplemented!(),
|
Action::Rename => unimplemented!(),
|
||||||
Action::Select => app.select(),
|
Action::Select => app.select(),
|
||||||
|
Action::Submit => app.move_torrent().await?,
|
||||||
|
Action::Cancel => {
|
||||||
|
app.input.clear();
|
||||||
|
app.input_mode = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if a KeyEvent matches a configured keybind string
|
||||||
|
fn matches_keybind(event: &KeyEvent, config_key: &Option<String>) -> bool {
|
||||||
|
let Some(key_str) = config_key else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let (modifiers, key_code) = parse_keybind(key_str);
|
||||||
|
let Some(key_code) = key_code else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
event.code == key_code && event.modifiers == modifiers
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_keybind(key_str: &str) -> (KeyModifiers, Option<KeyCode>) {
|
||||||
|
let mut modifiers = KeyModifiers::NONE;
|
||||||
|
let mut key_code = None;
|
||||||
|
|
||||||
|
for part in key_str.split('+') {
|
||||||
|
match part.trim().to_lowercase().as_str() {
|
||||||
|
"ctrl" => modifiers.insert(KeyModifiers::CONTROL),
|
||||||
|
"alt" => modifiers.insert(KeyModifiers::ALT),
|
||||||
|
"shift" => modifiers.insert(KeyModifiers::SHIFT),
|
||||||
|
key @ ("esc" | "enter" | "left" | "right" | "up" | "down" | "tab" | "backspace"
|
||||||
|
| "delete" | "home" | "end" | "pageup" | "pagedown" | "null" | "insert") => {
|
||||||
|
key_code = Some(match key {
|
||||||
|
"esc" => KeyCode::Esc,
|
||||||
|
"enter" => KeyCode::Enter,
|
||||||
|
"left" => KeyCode::Left,
|
||||||
|
"right" => KeyCode::Right,
|
||||||
|
"up" => KeyCode::Up,
|
||||||
|
"down" => KeyCode::Down,
|
||||||
|
"tab" => KeyCode::Tab,
|
||||||
|
"backspace" => KeyCode::Backspace,
|
||||||
|
"delete" => KeyCode::Delete,
|
||||||
|
"home" => KeyCode::Home,
|
||||||
|
"end" => KeyCode::End,
|
||||||
|
"pageup" => KeyCode::PageUp,
|
||||||
|
"pagedown" => KeyCode::PageDown,
|
||||||
|
"null" => KeyCode::Null,
|
||||||
|
"insert" => KeyCode::Insert,
|
||||||
|
_ => unreachable!(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
f_key if f_key.starts_with('f') => {
|
||||||
|
if let Ok(num) = f_key[1..].parse::<u8>() {
|
||||||
|
key_code = Some(KeyCode::F(num));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
single_char if single_char.len() == 1 => {
|
||||||
|
if let Some(c) = single_char.chars().next() {
|
||||||
|
key_code = Some(KeyCode::Char(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return (modifiers, None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(modifiers, key_code)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use color_eyre::eyre::Result;
|
use color_eyre::Result;
|
||||||
use tracing_appender::rolling;
|
use tracing_appender::rolling;
|
||||||
use tracing_subscriber::{self, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
use tracing_subscriber::{self, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
mod log;
|
mod log;
|
||||||
|
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::Result;
|
||||||
use log::setup_logger;
|
use log::setup_logger;
|
||||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||||
use std::io;
|
use std::io;
|
||||||
@ -40,7 +40,7 @@ async fn main() -> Result<()> {
|
|||||||
match tui.events.next()? {
|
match tui.events.next()? {
|
||||||
Event::Tick => app.tick().await?,
|
Event::Tick => app.tick().await?,
|
||||||
Event::Key(key_event) => {
|
Event::Key(key_event) => {
|
||||||
if let Some(action) = get_action(key_event, &app) {
|
if let Some(action) = get_action(key_event, &mut app) {
|
||||||
update(&mut app, action).await?;
|
update(&mut app, action).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::event::EventHandler;
|
use crate::event::EventHandler;
|
||||||
use crate::ui;
|
use crate::ui;
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::Result;
|
||||||
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
||||||
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
|
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
|
||||||
use ratatui::backend::Backend;
|
use ratatui::backend::Backend;
|
||||||
|
|||||||
25
src/ui/input.rs
Normal file
25
src/ui/input.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use crate::app::App;
|
||||||
|
use ratatui::{prelude::*, widgets::*};
|
||||||
|
|
||||||
|
pub fn render(f: &mut Frame, app: &mut 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);
|
||||||
|
f.render_widget(Clear, input_area);
|
||||||
|
f.render_widget(block, input_area);
|
||||||
|
|
||||||
|
let input = Paragraph::new(app.input.as_str()).block(Block::default());
|
||||||
|
f.render_widget(
|
||||||
|
input,
|
||||||
|
input_area.inner(Margin {
|
||||||
|
vertical: 1,
|
||||||
|
horizontal: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
f.set_cursor_position(ratatui::layout::Position::new(
|
||||||
|
input_area.x + app.cursor_position as u16 + 1,
|
||||||
|
input_area.y + 1,
|
||||||
|
));
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
mod help;
|
mod help;
|
||||||
|
mod input;
|
||||||
mod table;
|
mod table;
|
||||||
|
|
||||||
use crate::app::{App, Tab};
|
use crate::app::{App, Tab};
|
||||||
@ -63,4 +64,8 @@ pub fn render(app: &mut App, frame: &mut Frame) {
|
|||||||
if app.show_help {
|
if app.show_help {
|
||||||
render_help(frame, app);
|
render_help(frame, app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if app.input_mode {
|
||||||
|
input::render(frame, app);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user