mirror of
https://github.com/kristoferssolo/traxor.git
synced 2025-12-31 05:32:43 +00:00
This commit is contained in:
parent
3abeb83660
commit
be542551f3
28
Cargo.lock
generated
28
Cargo.lock
generated
@ -199,9 +199,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.7.1"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
|
||||
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
@ -316,22 +316,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.0.1"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
|
||||
checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "2.0.1"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||
checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn",
|
||||
"unicode-xid",
|
||||
]
|
||||
@ -1414,6 +1415,15 @@ version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.44"
|
||||
@ -1493,6 +1503,12 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
|
||||
@ -9,7 +9,7 @@ edition = "2024"
|
||||
filecaster = { version = "0.2", features = ["derive", "merge"] }
|
||||
color-eyre = "0.6"
|
||||
crossterm = "0.29"
|
||||
derive_more = { version = "2.0", features = ["display"] }
|
||||
derive_more = { version = "2.1", features = ["display"] }
|
||||
dirs = "6.0"
|
||||
merge = "0.2"
|
||||
ratatui = { version = "0.29" }
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use super::{Torrents, types::Selected};
|
||||
use color_eyre::{Result, eyre::eyre};
|
||||
use crate::error::Result;
|
||||
use std::{collections::HashSet, path::Path};
|
||||
use transmission_rpc::types::{Torrent, TorrentAction, TorrentStatus};
|
||||
|
||||
@ -9,11 +9,11 @@ impl Torrents {
|
||||
/// TODO: add error types
|
||||
pub async fn toggle(&mut self, ids: Selected) -> Result<()> {
|
||||
let ids: HashSet<_> = ids.into();
|
||||
let torrents_to_toggle: Vec<_> = self
|
||||
let torrents_to_toggle = self
|
||||
.torrents
|
||||
.iter()
|
||||
.filter(|torrent| torrent.id.is_some_and(|id| ids.contains(&id)))
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for torrent in torrents_to_toggle {
|
||||
let action = match torrent.status {
|
||||
@ -21,10 +21,7 @@ impl Torrents {
|
||||
_ => TorrentAction::Stop,
|
||||
};
|
||||
if let Some(id) = torrent.id() {
|
||||
self.client
|
||||
.torrent_action(action, vec![id])
|
||||
.await
|
||||
.map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||
self.client.torrent_action(action, vec![id]).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@ -51,10 +48,7 @@ impl Torrents {
|
||||
.collect();
|
||||
|
||||
for (id, action) in torrents_to_toggle {
|
||||
self.client
|
||||
.torrent_action(action, vec![id])
|
||||
.await
|
||||
.map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||
self.client.torrent_action(action, vec![id]).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -85,8 +79,7 @@ impl Torrents {
|
||||
if let Some(id) = torrent.id() {
|
||||
self.client
|
||||
.torrent_set_location(vec![id], location.to_string_lossy().into(), move_from)
|
||||
.await
|
||||
.map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -97,8 +90,7 @@ impl Torrents {
|
||||
pub async fn delete(&mut self, ids: Selected, delete_local_data: bool) -> Result<()> {
|
||||
self.client
|
||||
.torrent_remove(ids.into(), delete_local_data)
|
||||
.await
|
||||
.map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -109,8 +101,7 @@ impl Torrents {
|
||||
if let (Some(id), Some(old_name)) = (torrent.id(), torrent.name.clone()) {
|
||||
self.client
|
||||
.torrent_rename_path(vec![id], old_name, name.to_string_lossy().into())
|
||||
.await
|
||||
.map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -125,10 +116,7 @@ impl Torrents {
|
||||
.filter_map(Torrent::id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.client
|
||||
.torrent_action(action, ids)
|
||||
.await
|
||||
.map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||
self.client.torrent_action(action, ids).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
7
src/app/constants.rs
Normal file
7
src/app/constants.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub const DEFAULT_TICK_RATE_MS: u64 = 250;
|
||||
pub const TORRENT_UPDATE_INTERVAL_SECS: u64 = 2;
|
||||
pub const DEFAULT_RPC_URL: &str = "http://localhost:9091/transmission/rpc";
|
||||
|
||||
pub const HELP_POPUP_HEIGHT: u16 = 15;
|
||||
pub const INPUT_WIDTH_DIVISOR: u16 = 4;
|
||||
pub const TAB_COUNT: usize = 3;
|
||||
102
src/app/input.rs
Normal file
102
src/app/input.rs
Normal file
@ -0,0 +1,102 @@
|
||||
use crate::error::Result;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InputHandler {
|
||||
pub text: String,
|
||||
pub cursor_position: usize,
|
||||
pub completions: Vec<String>,
|
||||
pub completion_idx: usize,
|
||||
}
|
||||
|
||||
impl InputHandler {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn insert_char(&mut self, ch: char) {
|
||||
self.text.insert(self.cursor_position, ch);
|
||||
self.cursor_position += 1;
|
||||
}
|
||||
|
||||
pub fn delete_char(&mut self) {
|
||||
if self.cursor_position > 0 {
|
||||
self.cursor_position -= 1;
|
||||
self.text.remove(self.cursor_position);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.text.clear();
|
||||
self.cursor_position = 0;
|
||||
self.completions.clear();
|
||||
self.completion_idx = 0;
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: String) {
|
||||
self.cursor_position = text.len();
|
||||
self.text = text;
|
||||
}
|
||||
|
||||
pub async fn complete(&mut self) -> Result<()> {
|
||||
let path = PathBuf::from(&self.text);
|
||||
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_from_completions();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_completions(&mut self, matches: Vec<String>) {
|
||||
if matches.is_empty() {
|
||||
self.completions.clear();
|
||||
self.completion_idx = 0;
|
||||
} else if matches != self.completions {
|
||||
self.completions = matches;
|
||||
self.completion_idx = 0;
|
||||
} else {
|
||||
self.completion_idx = (self.completion_idx + 1) % self.completions.len();
|
||||
}
|
||||
}
|
||||
|
||||
fn update_from_completions(&mut self) {
|
||||
if let Some(completions) = self.completions.get(self.completion_idx) {
|
||||
self.set_text(completions.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
115
src/app/mod.rs
115
src/app/mod.rs
@ -1,36 +1,34 @@
|
||||
pub mod action;
|
||||
mod command;
|
||||
pub mod constants;
|
||||
mod input;
|
||||
mod tab;
|
||||
mod torrent;
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
|
||||
use crate::config::Config;
|
||||
use color_eyre::Result;
|
||||
use crate::error::Result;
|
||||
use crate::{app::input::InputHandler, config::Config};
|
||||
use ratatui::widgets::TableState;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs;
|
||||
use std::path::PathBuf;
|
||||
use types::Selected;
|
||||
pub use {tab::Tab, torrent::Torrents};
|
||||
|
||||
/// Main Application.
|
||||
#[derive(Debug)]
|
||||
pub struct App<'a> {
|
||||
pub struct App {
|
||||
pub running: bool,
|
||||
index: usize,
|
||||
tabs: &'a [Tab],
|
||||
tabs: Vec<Tab>,
|
||||
pub state: TableState,
|
||||
pub torrents: Torrents,
|
||||
pub show_help: bool,
|
||||
pub config: Config,
|
||||
pub input: String,
|
||||
pub cursor_position: usize,
|
||||
pub input_handler: InputHandler,
|
||||
pub input_mode: bool,
|
||||
pub completions: Vec<String>,
|
||||
pub completion_idx: usize,
|
||||
}
|
||||
|
||||
impl App<'_> {
|
||||
impl App {
|
||||
/// Constructs a new instance of [`App`].
|
||||
/// Returns instance of `Self`.
|
||||
///
|
||||
@ -40,17 +38,14 @@ impl App<'_> {
|
||||
pub fn new(config: Config) -> Result<Self> {
|
||||
Ok(Self {
|
||||
running: true,
|
||||
tabs: &[Tab::All, Tab::Active, Tab::Downloading],
|
||||
tabs: vec![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_handler: InputHandler::new(),
|
||||
input_mode: false,
|
||||
completions: Vec::new(),
|
||||
completion_idx: 0,
|
||||
})
|
||||
}
|
||||
|
||||
@ -58,14 +53,7 @@ impl App<'_> {
|
||||
///
|
||||
/// TODO: add error types
|
||||
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(())
|
||||
self.input_handler.complete().await
|
||||
}
|
||||
|
||||
/// Handles the tick event of the terminal.
|
||||
@ -149,8 +137,8 @@ impl App<'_> {
|
||||
/// Returns [`Tab`] slice
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub const fn tabs(&self) -> &[Tab] {
|
||||
self.tabs
|
||||
pub fn tabs(&self) -> &[Tab] {
|
||||
&self.tabs
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@ -192,16 +180,18 @@ impl App<'_> {
|
||||
///
|
||||
/// TODO: add error types
|
||||
pub async fn move_torrent(&mut self) -> Result<()> {
|
||||
self.torrents.move_selection(&self.input).await?;
|
||||
self.input.clear();
|
||||
self.cursor_position = 0;
|
||||
self.torrents
|
||||
.move_selection(&self.input_handler.text)
|
||||
.await?;
|
||||
self.input_handler.clear();
|
||||
self.input_mode = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn prepare_move_action(&mut self) {
|
||||
if let Some(download_dir) = self.get_current_downlaod_dir() {
|
||||
self.update_cursor(&download_dir);
|
||||
self.input_handler
|
||||
.set_text(download_dir.to_string_lossy().to_string());
|
||||
}
|
||||
self.input_mode = true;
|
||||
}
|
||||
@ -235,29 +225,6 @@ impl App<'_> {
|
||||
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
|
||||
@ -270,46 +237,4 @@ impl App<'_> {
|
||||
Selected::List(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_cursor(&mut self, path: &Path) {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ use std::fmt::Display;
|
||||
use transmission_rpc::types::TorrentGetField;
|
||||
|
||||
/// Available tabs.
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub enum Tab {
|
||||
#[default]
|
||||
All,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use color_eyre::{Result, eyre::eyre};
|
||||
use crate::{app::constants::DEFAULT_RPC_URL, error::Result};
|
||||
use std::{collections::HashSet, fmt::Debug};
|
||||
use transmission_rpc::{
|
||||
TransClient,
|
||||
@ -22,7 +22,7 @@ impl Torrents {
|
||||
///
|
||||
/// TODO: add error types
|
||||
pub fn new() -> Result<Self> {
|
||||
let url = Url::parse("http://localhost:9091/transmission/rpc")?;
|
||||
let url = Url::parse(DEFAULT_RPC_URL)?;
|
||||
Ok(Self {
|
||||
client: TransClient::new(url),
|
||||
torrents: Vec::new(),
|
||||
@ -70,8 +70,7 @@ impl Torrents {
|
||||
self.torrents = self
|
||||
.client
|
||||
.torrent_get(self.fields.clone(), None)
|
||||
.await
|
||||
.map_err(|e| eyre!("Transmission RPC error: {}", e.to_string()))?
|
||||
.await?
|
||||
.arguments
|
||||
.torrents;
|
||||
Ok(self)
|
||||
@ -84,8 +83,7 @@ impl Torrents {
|
||||
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()))?;
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
30
src/error.rs
Normal file
30
src/error.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TraxorError {
|
||||
#[error("Transmission RPC error: {0}")]
|
||||
TransmissionRpc(String),
|
||||
|
||||
#[error("Configuration error: {0}")]
|
||||
Config(String),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("URL parse error: {0}")]
|
||||
UrlParse(#[from] url::ParseError),
|
||||
|
||||
#[error("No torrent selected")]
|
||||
NoSelection,
|
||||
|
||||
#[error("Invalid torrent ID: {0}")]
|
||||
InvalidTorrentId(i64),
|
||||
}
|
||||
|
||||
impl From<Box<dyn std::error::Error + Send + Sync>> for TraxorError {
|
||||
fn from(e: Box<dyn std::error::Error + Send + Sync>) -> Self {
|
||||
Self::TransmissionRpc(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, TraxorError>;
|
||||
@ -1,25 +1,23 @@
|
||||
use crate::app::{App, action::Action};
|
||||
use color_eyre::Result;
|
||||
use crate::error::Result;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, info};
|
||||
|
||||
#[tracing::instrument(name = "Handling input", skip(app))]
|
||||
async fn handle_input(key_event: KeyEvent, app: &mut App<'_>) -> Result<Option<Action>> {
|
||||
async fn handle_input(key_event: KeyEvent, app: &mut App) -> Result<Option<Action>> {
|
||||
match key_event.code {
|
||||
KeyCode::Enter => Ok(Some(Action::Submit)),
|
||||
KeyCode::Tab => {
|
||||
app.complete_input().await?;
|
||||
Ok(None)
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
app.input.push(c);
|
||||
app.cursor_position = app.input.len();
|
||||
KeyCode::Char(ch) => {
|
||||
app.input_handler.insert_char(ch);
|
||||
Ok(None)
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.input.pop();
|
||||
app.cursor_position = app.input.len();
|
||||
app.input_handler.delete_char();
|
||||
Ok(None)
|
||||
}
|
||||
KeyCode::Esc => Ok(Some(Action::Cancel)),
|
||||
@ -33,7 +31,7 @@ async fn handle_input(key_event: KeyEvent, app: &mut App<'_>) -> Result<Option<A
|
||||
///
|
||||
/// TODO: add error types
|
||||
#[tracing::instrument(name = "Getting action", skip(app))]
|
||||
pub async fn get_action(key_event: KeyEvent, app: &mut App<'_>) -> Result<Option<Action>> {
|
||||
pub async fn get_action(key_event: KeyEvent, app: &mut App) -> Result<Option<Action>> {
|
||||
if app.input_mode {
|
||||
return handle_input(key_event, app).await;
|
||||
}
|
||||
@ -74,7 +72,7 @@ pub async fn get_action(key_event: KeyEvent, app: &mut App<'_>) -> Result<Option
|
||||
///
|
||||
/// TODO: add error types
|
||||
#[tracing::instrument(name = "Update", skip(app))]
|
||||
pub async fn update(app: &mut App<'_>, action: Action) -> Result<()> {
|
||||
pub async fn update(app: &mut App, action: Action) -> Result<()> {
|
||||
info!("updating app with action: {}", action);
|
||||
match action {
|
||||
Action::Quit => app.quit(),
|
||||
@ -94,7 +92,7 @@ pub async fn update(app: &mut App<'_>, action: Action) -> Result<()> {
|
||||
Action::Select => app.select(),
|
||||
Action::Submit => app.move_torrent().await?,
|
||||
Action::Cancel => {
|
||||
app.input.clear();
|
||||
app.input_handler.clear();
|
||||
app.input_mode = false;
|
||||
}
|
||||
}
|
||||
@ -118,7 +116,7 @@ pub enum ParseKeybingError {
|
||||
UnknownPart(String),
|
||||
}
|
||||
|
||||
fn parse_keybind(key_str: &str) -> Result<KeyEvent, ParseKeybingError> {
|
||||
fn parse_keybind(key_str: &str) -> std::result::Result<KeyEvent, ParseKeybingError> {
|
||||
let mut modifiers = KeyModifiers::NONE;
|
||||
let mut key_code = None;
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
pub mod app;
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod event;
|
||||
pub mod handler;
|
||||
pub mod telemetry;
|
||||
|
||||
50
src/main.rs
50
src/main.rs
@ -3,11 +3,15 @@ use ratatui::{Terminal, backend::CrosstermBackend};
|
||||
use std::{io, sync::Arc};
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
task::JoinHandle,
|
||||
time::{self, Duration},
|
||||
};
|
||||
use tracing::{trace, warn};
|
||||
use tracing::warn;
|
||||
use traxor::{
|
||||
app::App,
|
||||
app::{
|
||||
App,
|
||||
constants::{DEFAULT_TICK_RATE_MS, TORRENT_UPDATE_INTERVAL_SECS},
|
||||
},
|
||||
config::Config,
|
||||
event::{Event, EventHandler},
|
||||
handler::{get_action, update},
|
||||
@ -18,36 +22,19 @@ use traxor::{
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
|
||||
let config = Config::load()?;
|
||||
setup_logger(&config)?;
|
||||
|
||||
// Wrap App in Arc<Mutex<>> so we can share it between UI and updater
|
||||
let app = Arc::new(Mutex::new(App::new(config)?));
|
||||
|
||||
// Clone for updater task
|
||||
let app_clone = app.clone();
|
||||
spawn_torrent_updater(app.clone());
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut interval = time::interval(Duration::from_secs(2));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
let mut app = app_clone.lock().await;
|
||||
if let Err(e) = app.torrents.update().await {
|
||||
warn!("Failed to update torrents: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// TUI setup
|
||||
let backend = CrosstermBackend::new(io::stderr());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
let events = EventHandler::new(250); // Update time in ms
|
||||
let events = EventHandler::new(DEFAULT_TICK_RATE_MS);
|
||||
let mut tui = Tui::new(terminal, events);
|
||||
tui.init()?;
|
||||
|
||||
// Main loop
|
||||
loop {
|
||||
{
|
||||
let app_guard = app.lock().await;
|
||||
@ -62,22 +49,29 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
match tui.events.next()? {
|
||||
Event::Tick => {}
|
||||
Event::Key(key_event) => {
|
||||
let mut app_guard = app.lock().await;
|
||||
if let Some(action) = get_action(key_event, &mut app_guard).await? {
|
||||
update(&mut app_guard, action).await?;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse_event) => {
|
||||
trace!(target: "app", "Event::Mouse: {:?}", mouse_event);
|
||||
}
|
||||
Event::Resize(x, y) => {
|
||||
trace!(target: "app", "Event::Resize: ({}, {})", x, y);
|
||||
}
|
||||
Event::Mouse(_) | Event::Resize(_, _) | Event::Tick => {}
|
||||
}
|
||||
}
|
||||
|
||||
tui.exit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn_torrent_updater(app: Arc<Mutex<App>>) -> JoinHandle<()> {
|
||||
tokio::spawn(async move {
|
||||
let mut interval = time::interval(Duration::from_secs(TORRENT_UPDATE_INTERVAL_SECS));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let mut app = app.lock().await;
|
||||
if let Err(e) = app.torrents.update().await {
|
||||
warn!("Failed to update torrents: {e}");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ use ratatui::{
|
||||
};
|
||||
use tracing::warn;
|
||||
|
||||
pub fn render(f: &mut Frame, app: &mut App) {
|
||||
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);
|
||||
|
||||
@ -13,7 +13,7 @@ pub fn render(f: &mut Frame, app: &mut App) {
|
||||
f.render_widget(Clear, input_area);
|
||||
f.render_widget(block, input_area);
|
||||
|
||||
let input = Paragraph::new(app.input.as_str()).block(Block::default());
|
||||
let input = Paragraph::new(app.input_handler.text.as_str()).block(Block::default());
|
||||
f.render_widget(
|
||||
input,
|
||||
input_area.inner(Margin {
|
||||
@ -22,14 +22,10 @@ pub fn render(f: &mut Frame, app: &mut App) {
|
||||
}),
|
||||
);
|
||||
|
||||
let cursor_offset = u16::try_from(app.cursor_position)
|
||||
.map_err(|_| {
|
||||
warn!(
|
||||
"cursor_position {} out of u16 range. Clamping to 0",
|
||||
app.cursor_position
|
||||
);
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let cursor_offset = u16::try_from(app.input_handler.cursor_position).unwrap_or_else(|_| {
|
||||
warn!("cursor_position out of range, clamping");
|
||||
0
|
||||
});
|
||||
|
||||
f.set_cursor_position(Position::new(
|
||||
input_area.x + cursor_offset + 1,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user