mirror of
https://github.com/kristoferssolo/traxor.git
synced 2026-01-14 20:46:14 +00:00
297 lines
7.7 KiB
Rust
297 lines
7.7 KiB
Rust
pub mod action;
|
|
mod command;
|
|
pub mod constants;
|
|
mod input;
|
|
mod tab;
|
|
mod torrent;
|
|
pub mod types;
|
|
pub mod utils;
|
|
|
|
use crate::error::Result;
|
|
use crate::{app::input::InputHandler, config::Config};
|
|
use ratatui::widgets::TableState;
|
|
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,
|
|
/// Confirm delete dialog. Bool indicates whether to delete local data.
|
|
ConfirmDelete(bool),
|
|
}
|
|
|
|
/// Main Application.
|
|
#[derive(Debug)]
|
|
pub struct App {
|
|
pub running: bool,
|
|
index: usize,
|
|
tabs: Vec<Tab>,
|
|
pub state: TableState,
|
|
pub torrents: Torrents,
|
|
pub show_help: bool,
|
|
pub config: Config,
|
|
pub input_handler: InputHandler,
|
|
pub input_mode: InputMode,
|
|
}
|
|
|
|
impl App {
|
|
/// Constructs a new instance of [`App`].
|
|
/// Returns instance of `Self`.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// TODO: add error types
|
|
pub fn new(config: Config) -> Result<Self> {
|
|
Ok(Self {
|
|
running: true,
|
|
tabs: vec![Tab::All, Tab::Active, Tab::Downloading],
|
|
index: 0,
|
|
state: TableState::default(),
|
|
torrents: Torrents::new()?,
|
|
show_help: false,
|
|
config,
|
|
input_handler: InputHandler::new(),
|
|
input_mode: InputMode::None,
|
|
})
|
|
}
|
|
|
|
/// # Errors
|
|
///
|
|
/// TODO: add error types
|
|
pub async fn complete_input(&mut self) -> Result<()> {
|
|
self.input_handler.complete().await
|
|
}
|
|
|
|
/// Handles the tick event of the terminal.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// TODO: add error types
|
|
pub async fn tick(&mut self) -> Result<()> {
|
|
self.torrents.update().await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Set running to false to quit the application.
|
|
#[inline]
|
|
pub const fn quit(&mut self) {
|
|
self.running = false;
|
|
}
|
|
|
|
pub fn next(&mut self) {
|
|
let len = self.torrents.len();
|
|
if len == 0 {
|
|
return;
|
|
}
|
|
let i = self
|
|
.state
|
|
.selected()
|
|
.map_or(0, |i| if i >= len - 1 { 0 } else { i + 1 });
|
|
self.close_help();
|
|
self.state.select(Some(i));
|
|
}
|
|
|
|
pub fn previous(&mut self) {
|
|
let len = self.torrents.len();
|
|
if len == 0 {
|
|
return;
|
|
}
|
|
let i = self
|
|
.state
|
|
.selected()
|
|
.map_or(0, |i| if i == 0 { len - 1 } else { i - 1 });
|
|
self.close_help();
|
|
self.state.select(Some(i));
|
|
}
|
|
|
|
/// Switches to the next tab.
|
|
#[inline]
|
|
pub const fn next_tab(&mut self) {
|
|
self.close_help();
|
|
self.index = (self.index + 1) % self.tabs.len();
|
|
}
|
|
|
|
/// Switches to the previous tab.
|
|
#[inline]
|
|
pub fn prev_tab(&mut self) {
|
|
self.close_help();
|
|
self.index = self.index.checked_sub(1).unwrap_or(self.tabs.len() - 1);
|
|
}
|
|
|
|
/// Switches to the tab whose index is `idx`.
|
|
#[inline]
|
|
pub const fn switch_tab(&mut self, idx: usize) {
|
|
self.close_help();
|
|
self.index = idx;
|
|
}
|
|
|
|
/// Returns current active [`Tab`] number
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn index(&self) -> usize {
|
|
self.index
|
|
}
|
|
|
|
/// Returns [`Tab`] slice
|
|
#[inline]
|
|
#[must_use]
|
|
pub fn tabs(&self) -> &[Tab] {
|
|
&self.tabs
|
|
}
|
|
|
|
#[inline]
|
|
pub const fn toggle_help(&mut self) {
|
|
self.show_help = !self.show_help;
|
|
}
|
|
|
|
#[inline]
|
|
pub const fn close_help(&mut self) {
|
|
self.show_help = false;
|
|
}
|
|
|
|
#[inline]
|
|
pub const fn open_help(&mut self) {
|
|
self.show_help = true;
|
|
}
|
|
|
|
/// # Errors
|
|
///
|
|
/// TODO: add error types
|
|
pub async fn toggle_torrents(&mut self) -> Result<()> {
|
|
let ids = self.selected(false);
|
|
self.torrents.toggle(ids).await?;
|
|
self.close_help();
|
|
Ok(())
|
|
}
|
|
|
|
/// Move selected or highlighted torrent(s) to a new location.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if the RPC call fails.
|
|
pub async fn move_torrent(&mut self) -> Result<()> {
|
|
let ids = self.selected(false);
|
|
self.torrents
|
|
.move_torrents(ids, &self.input_handler.text)
|
|
.await?;
|
|
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 = 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();
|
|
}
|
|
|
|
/// Prepare delete confirmation dialog.
|
|
pub const fn prepare_delete(&mut self, delete_local_data: bool) {
|
|
self.input_mode = InputMode::ConfirmDelete(delete_local_data);
|
|
}
|
|
|
|
/// Execute the confirmed delete action.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error if the RPC call fails.
|
|
pub async fn confirm_delete(&mut self) -> Result<()> {
|
|
let InputMode::ConfirmDelete(delete_local_data) = self.input_mode else {
|
|
return Ok(());
|
|
};
|
|
let ids = self.selected(false);
|
|
self.torrents.delete(ids, delete_local_data).await?;
|
|
self.clear_input();
|
|
Ok(())
|
|
}
|
|
|
|
pub fn select(&mut self) {
|
|
if let Selected::Current(current_id) = self.selected(true) {
|
|
if self.torrents.selected.contains(¤t_id) {
|
|
self.torrents.selected.remove(¤t_id);
|
|
} else {
|
|
self.torrents.selected.insert(current_id);
|
|
}
|
|
}
|
|
self.next();
|
|
}
|
|
|
|
fn selected(&self, highlighted: bool) -> Selected {
|
|
let torrents = &self.torrents.torrents;
|
|
if (self.torrents.selected.is_empty() || highlighted)
|
|
&& let Some(id) = self
|
|
.state
|
|
.selected()
|
|
.and_then(|idx| torrents.get(idx).and_then(|t| t.id))
|
|
{
|
|
return Selected::Current(id);
|
|
}
|
|
Selected::List(
|
|
torrents
|
|
.iter()
|
|
.filter_map(|t| t.id.filter(|id| self.torrents.selected.contains(id)))
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
fn get_current_download_dir(&self) -> Option<PathBuf> {
|
|
self.get_current_torrent()
|
|
.and_then(|t| t.download_dir)
|
|
.map(PathBuf::from)
|
|
}
|
|
|
|
fn get_current_torrent_name(&self) -> Option<String> {
|
|
self.get_current_torrent().and_then(|t| t.name)
|
|
}
|
|
|
|
fn get_current_torrent(&self) -> Option<transmission_rpc::types::Torrent> {
|
|
let Selected::Current(current_id) = self.selected(true) else {
|
|
return None;
|
|
};
|
|
self.torrents
|
|
.torrents
|
|
.iter()
|
|
.find(|t| t.id == Some(current_id))
|
|
.cloned()
|
|
}
|
|
}
|