mirror of
https://github.com/kristoferssolo/traxor.git
synced 2025-10-21 20:10:35 +00:00
286 lines
7.5 KiB
Rust
286 lines
7.5 KiB
Rust
pub mod action;
|
|
mod command;
|
|
mod tab;
|
|
mod torrent;
|
|
pub mod types;
|
|
pub mod utils;
|
|
|
|
use crate::config::Config;
|
|
use color_eyre::Result;
|
|
use ratatui::widgets::TableState;
|
|
use std::path::{Path, PathBuf};
|
|
use tokio::fs;
|
|
use types::Selected;
|
|
pub use {tab::Tab, torrent::Torrents};
|
|
|
|
/// Main Application.
|
|
#[derive(Debug)]
|
|
pub struct App<'a> {
|
|
pub running: bool,
|
|
index: usize,
|
|
tabs: &'a [Tab],
|
|
pub state: TableState,
|
|
pub torrents: Torrents,
|
|
pub show_help: bool,
|
|
pub config: Config,
|
|
pub input: String,
|
|
pub cursor_position: usize,
|
|
pub input_mode: bool,
|
|
pub completions: Vec<String>,
|
|
pub completion_idx: usize,
|
|
}
|
|
|
|
impl<'a> App<'a> {
|
|
/// Constructs a new instance of [`App`].
|
|
/// Returns instance of `Self`.
|
|
pub fn new(config: Config) -> Result<Self> {
|
|
Ok(Self {
|
|
running: true,
|
|
tabs: &[Tab::All, Tab::Active, Tab::Downloading],
|
|
index: 0,
|
|
state: TableState::default(),
|
|
torrents: Torrents::new()?, // Handle the Result here
|
|
show_help: false,
|
|
config,
|
|
input: String::new(),
|
|
cursor_position: 0,
|
|
input_mode: false,
|
|
completions: Vec::new(),
|
|
completion_idx: 0,
|
|
})
|
|
}
|
|
|
|
pub async fn complete_input(&mut self) -> Result<()> {
|
|
let path = PathBuf::from(&self.input);
|
|
let (base_path, partial_name) = split_path_components(path);
|
|
let matches = find_matching_entries(&base_path, &partial_name).await?;
|
|
|
|
self.update_completions(matches);
|
|
self.update_input_with_matches();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Handles the tick event of the terminal.
|
|
pub async fn tick(&mut self) -> Result<()> {
|
|
self.torrents.update().await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Set running to false to quit the application.
|
|
pub fn quit(&mut self) {
|
|
self.running = false;
|
|
}
|
|
|
|
pub fn next(&mut self) {
|
|
let i = match self.state.selected() {
|
|
Some(i) => {
|
|
if i >= self.torrents.len() - 1 {
|
|
0
|
|
} else {
|
|
i + 1
|
|
}
|
|
}
|
|
None => 0,
|
|
};
|
|
self.close_help();
|
|
self.state.select(Some(i));
|
|
}
|
|
|
|
pub fn previous(&mut self) {
|
|
let i = match self.state.selected() {
|
|
Some(i) => {
|
|
if i == 0 {
|
|
self.torrents.len() - 1
|
|
} else {
|
|
i - 1
|
|
}
|
|
}
|
|
None => 0,
|
|
};
|
|
self.close_help();
|
|
self.state.select(Some(i));
|
|
}
|
|
|
|
/// Switches to the next tab.
|
|
pub fn next_tab(&mut self) {
|
|
self.close_help();
|
|
self.index = (self.index + 1) % self.tabs.len();
|
|
}
|
|
|
|
/// Switches to the previous tab.
|
|
pub fn prev_tab(&mut self) {
|
|
self.close_help();
|
|
if self.index > 0 {
|
|
self.index -= 1;
|
|
} else {
|
|
self.index = self.tabs.len() - 1;
|
|
}
|
|
}
|
|
|
|
/// Switches to the tab whose index is `idx`.
|
|
pub fn switch_tab(&mut self, idx: usize) {
|
|
self.close_help();
|
|
self.index = idx
|
|
}
|
|
|
|
/// Returns current active [`Tab`] number
|
|
pub fn index(&self) -> usize {
|
|
self.index
|
|
}
|
|
|
|
/// Returns [`Tab`] slice
|
|
pub fn tabs(&self) -> &[Tab] {
|
|
self.tabs
|
|
}
|
|
|
|
pub fn toggle_help(&mut self) {
|
|
self.show_help = !self.show_help;
|
|
}
|
|
|
|
pub fn close_help(&mut self) {
|
|
self.show_help = false;
|
|
}
|
|
|
|
pub fn open_help(&mut self) {
|
|
self.show_help = true;
|
|
}
|
|
|
|
pub async fn toggle_torrents(&mut self) -> Result<()> {
|
|
let ids = self.selected(false);
|
|
self.torrents.toggle(ids).await?;
|
|
self.close_help();
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn delete(&mut self, delete_local_data: bool) -> Result<()> {
|
|
let ids = self.selected(false);
|
|
self.torrents.delete(ids, delete_local_data).await?;
|
|
self.close_help();
|
|
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 prepare_move_action(&mut self) {
|
|
if let Some(download_dir) = self.get_current_downlaod_dir() {
|
|
let path_buf = PathBuf::from(download_dir);
|
|
self.update_cursor(path_buf);
|
|
}
|
|
self.input_mode = true;
|
|
}
|
|
|
|
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 selected_id = self
|
|
.state
|
|
.selected()
|
|
.and_then(|idx| torrents.get(idx).and_then(|t| t.id));
|
|
if let Some(id) = selected_id {
|
|
return Selected::Current(id);
|
|
}
|
|
}
|
|
let selected_torrents = torrents
|
|
.iter()
|
|
.filter_map(|t| t.id.filter(|id| self.torrents.selected.contains(id)))
|
|
.collect();
|
|
Selected::List(selected_torrents)
|
|
}
|
|
|
|
fn update_completions(&mut self, matches: Vec<String>) {
|
|
if matches.is_empty() {
|
|
self.completions.clear();
|
|
self.completion_idx = 0;
|
|
return;
|
|
}
|
|
|
|
if matches != self.completions {
|
|
self.completions = matches;
|
|
self.completion_idx = 0;
|
|
return;
|
|
}
|
|
|
|
self.completion_idx = (self.completion_idx + 1) % self.completions.len();
|
|
}
|
|
|
|
fn update_input_with_matches(&mut self) {
|
|
if let Some(completion) = self.completions.get(self.completion_idx) {
|
|
self.input = completion.clone();
|
|
self.cursor_position = self.input.len();
|
|
}
|
|
}
|
|
|
|
fn get_current_downlaod_dir(&self) -> Option<PathBuf> {
|
|
match self.selected(true) {
|
|
Selected::Current(current_id) => self
|
|
.torrents
|
|
.torrents
|
|
.iter()
|
|
.find(|&t| t.id == Some(current_id))
|
|
.and_then(|t| t.download_dir.as_ref())
|
|
.map(PathBuf::from),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn update_cursor(&mut self, path: PathBuf) {
|
|
self.input = path.to_string_lossy().to_string();
|
|
self.cursor_position = self.input.len();
|
|
}
|
|
}
|
|
|
|
fn split_path_components(path: PathBuf) -> (PathBuf, String) {
|
|
if path.is_dir() {
|
|
return (path, String::new());
|
|
}
|
|
|
|
let partial = path
|
|
.file_name()
|
|
.unwrap_or_default()
|
|
.to_string_lossy()
|
|
.to_string();
|
|
|
|
let base = path
|
|
.parent()
|
|
.unwrap_or_else(|| Path::new("/"))
|
|
.to_path_buf();
|
|
|
|
(base, partial)
|
|
}
|
|
|
|
async fn find_matching_entries(base_path: &Path, partial_name: &str) -> Result<Vec<String>> {
|
|
let mut entries = fs::read_dir(&base_path).await?;
|
|
let mut matches = Vec::new();
|
|
|
|
while let Some(entry) = entries.next_entry().await? {
|
|
let file_name = entry.file_name().to_string_lossy().to_string();
|
|
|
|
if file_name
|
|
.to_lowercase()
|
|
.starts_with(&partial_name.to_lowercase())
|
|
{
|
|
matches.push(format!("{}/{}", base_path.to_string_lossy(), file_name));
|
|
}
|
|
}
|
|
|
|
Ok(matches)
|
|
}
|