mirror of
https://github.com/kristoferssolo/traxor.git
synced 2025-10-21 20:10:35 +00:00
feat(config): add config file
This commit is contained in:
parent
f393ae8a70
commit
06fa7c003d
120
Cargo.lock
generated
120
Cargo.lock
generated
@ -322,6 +322,27 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@ -741,6 +762,16 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indoc"
|
name = "indoc"
|
||||||
version = "2.0.6"
|
version = "2.0.6"
|
||||||
@ -813,6 +844,16 @@ version = "0.2.174"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.15"
|
version = "0.4.15"
|
||||||
@ -944,6 +985,12 @@ version = "1.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "overload"
|
name = "overload"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@ -1165,6 +1212,17 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.16",
|
||||||
|
"libredox",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@ -1395,6 +1453,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@ -1693,6 +1760,47 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_write",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_write"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@ -1843,9 +1951,12 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"crossterm 0.29.0",
|
"crossterm 0.29.0",
|
||||||
|
"dirs",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
"serde",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-appender",
|
"tracing-appender",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@ -2210,6 +2321,15 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen-rt"
|
name = "wit-bindgen-rt"
|
||||||
version = "0.39.0"
|
version = "0.39.0"
|
||||||
|
|||||||
@ -9,10 +9,13 @@ edition = "2021"
|
|||||||
color-eyre = "0.6"
|
color-eyre = "0.6"
|
||||||
crossterm = "0.29"
|
crossterm = "0.29"
|
||||||
ratatui = { version = "0.29" }
|
ratatui = { version = "0.29" }
|
||||||
thiserror = "2.0"
|
|
||||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-appender = "0.2"
|
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
thiserror = "2.0"
|
||||||
|
tracing-appender = "0.2"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
toml = "0.8"
|
||||||
|
dirs = "6.0"
|
||||||
transmission-rpc = "0.5"
|
transmission-rpc = "0.5"
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
|
|||||||
22
config/default.toml
Normal file
22
config/default.toml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
[keybinds]
|
||||||
|
quit = "q"
|
||||||
|
next_tab = "l"
|
||||||
|
prev_tab = "h"
|
||||||
|
next_torrent = "j"
|
||||||
|
prev_torrent = "k"
|
||||||
|
switch_tab_1 = "1"
|
||||||
|
switch_tab_2 = "2"
|
||||||
|
switch_tab_3 = "3"
|
||||||
|
toggle_torrent = "enter"
|
||||||
|
toggle_all = "a"
|
||||||
|
delete = "d"
|
||||||
|
delete_force = "D"
|
||||||
|
select = " "
|
||||||
|
toggle_help = "?"
|
||||||
|
|
||||||
|
[colors]
|
||||||
|
highlight_background = "magenta"
|
||||||
|
highlight_foreground = "black"
|
||||||
|
warning_foreground = "yellow"
|
||||||
|
info_foreground = "blue"
|
||||||
|
error_foreground = "red"
|
||||||
@ -3,7 +3,7 @@ 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) -> anyhow::Result<()> {
|
pub async fn toggle(&mut self, ids: Selected) -> color_eyre::eyre::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,13 +20,14 @@ impl Torrents {
|
|||||||
self.client
|
self.client
|
||||||
.torrent_action(action, vec![id])
|
.torrent_action(action, vec![id])
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!("Transmission RPC error: {}", e.to_string()))?;
|
.map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn toggle_all(&mut self) -> anyhow::Result<()> {
|
pub async fn toggle_all(&mut self) -> color_eyre::eyre::Result<()> {
|
||||||
let torrents_to_toggle: Vec<_> = self
|
let torrents_to_toggle: Vec<_> = self
|
||||||
.torrents
|
.torrents
|
||||||
.iter()
|
.iter()
|
||||||
@ -47,16 +48,16 @@ impl Torrents {
|
|||||||
self.client
|
self.client
|
||||||
.torrent_action(action, vec![id])
|
.torrent_action(action, vec![id])
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!("Transmission RPC error: {}", e.to_string()))?;
|
.map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_all(&mut self) -> anyhow::Result<()> {
|
pub async fn start_all(&mut self) -> color_eyre::eyre::Result<()> {
|
||||||
self.action_all(TorrentAction::StartNow).await
|
self.action_all(TorrentAction::StartNow).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stop_all(&mut self) -> anyhow::Result<()> {
|
pub async fn stop_all(&mut self) -> color_eyre::eyre::Result<()> {
|
||||||
self.action_all(TorrentAction::Stop).await
|
self.action_all(TorrentAction::Stop).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,35 +66,35 @@ impl Torrents {
|
|||||||
torrent: &Torrent,
|
torrent: &Torrent,
|
||||||
location: &Path,
|
location: &Path,
|
||||||
move_from: Option<bool>,
|
move_from: Option<bool>,
|
||||||
) -> anyhow::Result<()> {
|
) -> color_eyre::eyre::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| anyhow::anyhow!("Transmission RPC error: {}", e.to_string()))?;
|
.map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete(&mut self, ids: Selected, delete_local_data: bool) -> anyhow::Result<()> {
|
pub async fn delete(&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| anyhow::anyhow!("Transmission RPC error: {}", e.to_string()))?;
|
.map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn rename(&mut self, torrent: &Torrent, name: &Path) -> anyhow::Result<()> {
|
pub async fn rename(&mut self, torrent: &Torrent, name: &Path) -> color_eyre::eyre::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| anyhow::anyhow!("Transmission RPC error: {}", e.to_string()))?;
|
.map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn action_all(&mut self, action: TorrentAction) -> anyhow::Result<()> {
|
async fn action_all(&mut self, action: TorrentAction) -> color_eyre::eyre::Result<()> {
|
||||||
let ids = self
|
let ids = self
|
||||||
.torrents
|
.torrents
|
||||||
.iter()
|
.iter()
|
||||||
@ -103,7 +104,7 @@ impl Torrents {
|
|||||||
self.client
|
self.client
|
||||||
.torrent_action(action, ids)
|
.torrent_action(action, ids)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!("Transmission RPC error: {}", e.to_string()))?;
|
.map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,11 @@ mod tab;
|
|||||||
mod torrent;
|
mod torrent;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
pub use {tab::Tab, torrent::Torrents};
|
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
use ratatui::widgets::TableState;
|
use ratatui::widgets::TableState;
|
||||||
use types::Selected;
|
use types::Selected;
|
||||||
|
pub use {tab::Tab, torrent::Torrents};
|
||||||
|
|
||||||
/// Main Application.
|
/// Main Application.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -18,12 +19,13 @@ pub struct App<'a> {
|
|||||||
pub state: TableState,
|
pub state: TableState,
|
||||||
pub torrents: Torrents,
|
pub torrents: Torrents,
|
||||||
pub show_help: bool,
|
pub show_help: bool,
|
||||||
|
pub config: Config,
|
||||||
}
|
}
|
||||||
|
|
||||||
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() -> anyhow::Result<Self> {
|
pub fn new(config: Config) -> color_eyre::eyre::Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
running: true,
|
running: true,
|
||||||
tabs: &[Tab::All, Tab::Active, Tab::Downloading],
|
tabs: &[Tab::All, Tab::Active, Tab::Downloading],
|
||||||
@ -31,11 +33,12 @@ impl<'a> App<'a> {
|
|||||||
state: TableState::default(),
|
state: TableState::default(),
|
||||||
torrents: Torrents::new()?, // Handle the Result here
|
torrents: Torrents::new()?, // Handle the Result here
|
||||||
show_help: false,
|
show_help: false,
|
||||||
|
config,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the tick event of the terminal.
|
/// Handles the tick event of the terminal.
|
||||||
pub async fn tick(&mut self) -> anyhow::Result<()> {
|
pub async fn tick(&mut self) -> color_eyre::eyre::Result<()> {
|
||||||
self.torrents.update().await?;
|
self.torrents.update().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -119,14 +122,14 @@ impl<'a> App<'a> {
|
|||||||
self.show_help = true;
|
self.show_help = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn toggle_torrents(&mut self) -> anyhow::Result<()> {
|
pub async fn toggle_torrents(&mut self) -> color_eyre::eyre::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) -> anyhow::Result<()> {
|
pub async fn delete(&mut self, delete_local_data: bool) -> color_eyre::eyre::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();
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use anyhow::Result;
|
use color_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::{Torrent, TorrentGetField},
|
||||||
@ -50,7 +50,7 @@ impl Torrents {
|
|||||||
.client
|
.client
|
||||||
.torrent_get(self.fields.clone(), None)
|
.torrent_get(self.fields.clone(), None)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!("Transmission RPC error: {}", e.to_string()))?
|
.map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?
|
||||||
.arguments
|
.arguments
|
||||||
.torrents;
|
.torrents;
|
||||||
Ok(self)
|
Ok(self)
|
||||||
|
|||||||
64
src/config/colors.rs
Normal file
64
src/config/colors.rs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
use ratatui::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct ColorsConfig {
|
||||||
|
pub highlight_background: Option<String>,
|
||||||
|
pub highlight_foreground: Option<String>,
|
||||||
|
pub warning_foreground: Option<String>,
|
||||||
|
pub info_foreground: Option<String>,
|
||||||
|
pub error_foreground: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorsConfig {
|
||||||
|
pub fn get_color(&self, color_name: &Option<String>) -> Color {
|
||||||
|
match color_name {
|
||||||
|
Some(name) => match name.to_lowercase().as_str() {
|
||||||
|
"black" => Color::Black,
|
||||||
|
"blue" => Color::Blue,
|
||||||
|
"cyan" => Color::Cyan,
|
||||||
|
"darkgray" => Color::DarkGray,
|
||||||
|
"gray" => Color::Gray,
|
||||||
|
"green" => Color::Green,
|
||||||
|
"lightgreen" => Color::LightGreen,
|
||||||
|
"lightred" => Color::LightRed,
|
||||||
|
"magenta" => Color::Magenta,
|
||||||
|
"red" => Color::Red,
|
||||||
|
"white" => Color::White,
|
||||||
|
"yellow" => Color::Yellow,
|
||||||
|
_ => Color::Reset, // Default to reset, if color name is not recognized
|
||||||
|
},
|
||||||
|
None => Color::Reset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge(&mut self, other: Self) {
|
||||||
|
if let Some(highlight_background) = other.highlight_background {
|
||||||
|
self.highlight_background = Some(highlight_background);
|
||||||
|
}
|
||||||
|
if let Some(highlight_foreground) = other.highlight_foreground {
|
||||||
|
self.highlight_foreground = Some(highlight_foreground);
|
||||||
|
}
|
||||||
|
if let Some(warning_foreground) = other.warning_foreground {
|
||||||
|
self.warning_foreground = Some(warning_foreground);
|
||||||
|
}
|
||||||
|
if let Some(info_foreground) = other.info_foreground {
|
||||||
|
self.info_foreground = Some(info_foreground);
|
||||||
|
}
|
||||||
|
if let Some(error_foreground) = other.error_foreground {
|
||||||
|
self.error_foreground = Some(error_foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ColorsConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
highlight_background: Some("magenta".to_string()),
|
||||||
|
highlight_foreground: Some("black".to_string()),
|
||||||
|
warning_foreground: Some("yellow".to_string()),
|
||||||
|
info_foreground: Some("blue".to_string()),
|
||||||
|
error_foreground: Some("red".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/config/keybinds.rs
Normal file
87
src/config/keybinds.rs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct KeybindsConfig {
|
||||||
|
pub quit: Option<String>,
|
||||||
|
pub next_tab: Option<String>,
|
||||||
|
pub prev_tab: Option<String>,
|
||||||
|
pub next_torrent: Option<String>,
|
||||||
|
pub prev_torrent: Option<String>,
|
||||||
|
pub switch_tab_1: Option<String>,
|
||||||
|
pub switch_tab_2: Option<String>,
|
||||||
|
pub switch_tab_3: Option<String>,
|
||||||
|
pub toggle_torrent: Option<String>,
|
||||||
|
pub toggle_all: Option<String>,
|
||||||
|
pub delete: Option<String>,
|
||||||
|
pub delete_force: Option<String>,
|
||||||
|
pub select: Option<String>,
|
||||||
|
pub toggle_help: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeybindsConfig {
|
||||||
|
pub fn merge(&mut self, other: Self) {
|
||||||
|
if let Some(quit) = other.quit {
|
||||||
|
self.quit = Some(quit);
|
||||||
|
}
|
||||||
|
if let Some(next_tab) = other.next_tab {
|
||||||
|
self.next_tab = Some(next_tab);
|
||||||
|
}
|
||||||
|
if let Some(prev_tab) = other.prev_tab {
|
||||||
|
self.prev_tab = Some(prev_tab);
|
||||||
|
}
|
||||||
|
if let Some(next_torrent) = other.next_torrent {
|
||||||
|
self.next_torrent = Some(next_torrent);
|
||||||
|
}
|
||||||
|
if let Some(prev_torrent) = other.prev_torrent {
|
||||||
|
self.prev_torrent = Some(prev_torrent);
|
||||||
|
}
|
||||||
|
if let Some(switch_tab_1) = other.switch_tab_1 {
|
||||||
|
self.switch_tab_1 = Some(switch_tab_1);
|
||||||
|
}
|
||||||
|
if let Some(switch_tab_2) = other.switch_tab_2 {
|
||||||
|
self.switch_tab_2 = Some(switch_tab_2);
|
||||||
|
}
|
||||||
|
if let Some(switch_tab_3) = other.switch_tab_3 {
|
||||||
|
self.switch_tab_3 = Some(switch_tab_3);
|
||||||
|
}
|
||||||
|
if let Some(toggle_torrent) = other.toggle_torrent {
|
||||||
|
self.toggle_torrent = Some(toggle_torrent);
|
||||||
|
}
|
||||||
|
if let Some(toggle_all) = other.toggle_all {
|
||||||
|
self.toggle_all = Some(toggle_all);
|
||||||
|
}
|
||||||
|
if let Some(delete) = other.delete {
|
||||||
|
self.delete = Some(delete);
|
||||||
|
}
|
||||||
|
if let Some(delete_force) = other.delete_force {
|
||||||
|
self.delete_force = Some(delete_force);
|
||||||
|
}
|
||||||
|
if let Some(select) = other.select {
|
||||||
|
self.select = Some(select);
|
||||||
|
}
|
||||||
|
if let Some(toggle_help) = other.toggle_help {
|
||||||
|
self.toggle_help = Some(toggle_help);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for KeybindsConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
quit: Some("q".to_string()),
|
||||||
|
next_tab: Some("l".to_string()),
|
||||||
|
prev_tab: Some("h".to_string()),
|
||||||
|
next_torrent: Some("j".to_string()),
|
||||||
|
prev_torrent: Some("k".to_string()),
|
||||||
|
switch_tab_1: Some("1".to_string()),
|
||||||
|
switch_tab_2: Some("2".to_string()),
|
||||||
|
switch_tab_3: Some("3".to_string()),
|
||||||
|
toggle_torrent: Some("enter".to_string()),
|
||||||
|
toggle_all: Some("a".to_string()),
|
||||||
|
delete: Some("d".to_string()),
|
||||||
|
delete_force: Some("D".to_string()),
|
||||||
|
select: Some(" ".to_string()),
|
||||||
|
toggle_help: Some("?".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/config/mod.rs
Normal file
61
src/config/mod.rs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
mod colors;
|
||||||
|
mod keybinds;
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
|
use colors::ColorsConfig;
|
||||||
|
use keybinds::KeybindsConfig;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub keybinds: KeybindsConfig,
|
||||||
|
pub colors: ColorsConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn load() -> Result<Self> {
|
||||||
|
let mut config = Self::default();
|
||||||
|
|
||||||
|
// Load system-wide config
|
||||||
|
let system_config_path = PathBuf::from("/etc/xdg/traxor/config.toml");
|
||||||
|
if system_config_path.exists() {
|
||||||
|
let config_str = std::fs::read_to_string(&system_config_path)?;
|
||||||
|
let system_config: Config = toml::from_str(&config_str)?;
|
||||||
|
config.merge(system_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load user-specific config
|
||||||
|
let user_config_path = Self::get_config_path()?;
|
||||||
|
if user_config_path.exists() {
|
||||||
|
let config_str = std::fs::read_to_string(&user_config_path)?;
|
||||||
|
let user_config: Config = toml::from_str(&config_str)?;
|
||||||
|
config.merge(user_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self) -> Result<()> {
|
||||||
|
let config_path = Self::get_config_path()?;
|
||||||
|
let config_str = toml::to_string_pretty(self)?;
|
||||||
|
std::fs::write(&config_path, config_str)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_config_path() -> Result<PathBuf> {
|
||||||
|
let config_dir = std::env::var("XDG_CONFIG_HOME")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
dirs::home_dir()
|
||||||
|
.unwrap_or_else(|| panic!("Could not find home directory"))
|
||||||
|
.join(".config")
|
||||||
|
});
|
||||||
|
Ok(config_dir.join("traxor").join("config.toml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge(&mut self, other: Self) {
|
||||||
|
self.keybinds.merge(other.keybinds);
|
||||||
|
self.colors.merge(other.colors);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use anyhow::Result;
|
use color_eyre::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;
|
||||||
|
|||||||
109
src/handler.rs
109
src/handler.rs
@ -4,39 +4,102 @@ use tracing::{event, info_span, Level};
|
|||||||
|
|
||||||
/// Handles the key events of [`App`].
|
/// Handles the key events of [`App`].
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub fn get_action(key_event: KeyEvent) -> Option<Action> {
|
pub fn get_action(key_event: KeyEvent, app: &App) -> Option<Action> {
|
||||||
let span = info_span!("get_action");
|
let span = info_span!("get_action");
|
||||||
let _enter = span.enter();
|
let _enter = span.enter();
|
||||||
event!(Level::INFO, "handling key event: {:?}", key_event);
|
event!(Level::INFO, "handling key event: {:?}", key_event);
|
||||||
match key_event.code {
|
|
||||||
// Exit application on `ESC` or `q`
|
|
||||||
KeyCode::Esc | KeyCode::Char('q') => Some(Action::Quit),
|
|
||||||
|
|
||||||
// Exit application on `Ctrl-C`
|
let config_keybinds = &app.config.keybinds;
|
||||||
KeyCode::Char('c') | KeyCode::Char('C') => match key_event.modifiers {
|
|
||||||
KeyModifiers::CONTROL => Some(Action::Quit),
|
// Helper to check if a KeyEvent matches a configured keybind string
|
||||||
_ => None,
|
let matches_keybind = |event: &KeyEvent, config_key: &Option<String>| {
|
||||||
},
|
if let Some(key_str) = config_key {
|
||||||
KeyCode::Char('l') | KeyCode::Right => Some(Action::NextTab),
|
let parts: Vec<&str> = key_str.split('+').collect();
|
||||||
KeyCode::Char('h') | KeyCode::Left => Some(Action::PrevTab),
|
let mut parsed_modifiers = KeyModifiers::NONE;
|
||||||
KeyCode::Char('j') | KeyCode::Down => Some(Action::NextTorrent),
|
let mut parsed_key_code = None;
|
||||||
KeyCode::Char('k') | KeyCode::Up => Some(Action::PrevTorrent),
|
|
||||||
KeyCode::Char('1') => Some(Action::SwitchTab(0)),
|
for part in &parts {
|
||||||
KeyCode::Char('2') => Some(Action::SwitchTab(1)),
|
match part.to_lowercase().as_str() {
|
||||||
KeyCode::Char('3') => Some(Action::SwitchTab(2)),
|
"ctrl" => parsed_modifiers.insert(KeyModifiers::CONTROL),
|
||||||
KeyCode::Char('t') | KeyCode::Enter | KeyCode::Menu => Some(Action::ToggleTorrent),
|
"alt" => parsed_modifiers.insert(KeyModifiers::ALT),
|
||||||
KeyCode::Char('a') => Some(Action::ToggleAll),
|
"shift" => parsed_modifiers.insert(KeyModifiers::SHIFT),
|
||||||
KeyCode::Char('d') => Some(Action::Delete(false)),
|
"esc" => parsed_key_code = Some(KeyCode::Esc),
|
||||||
KeyCode::Char('D') => Some(Action::Delete(true)),
|
"enter" => parsed_key_code = Some(KeyCode::Enter),
|
||||||
KeyCode::Char(' ') => Some(Action::Select),
|
"left" => parsed_key_code = Some(KeyCode::Left),
|
||||||
KeyCode::Char('?') => Some(Action::ToggleHelp),
|
"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 {
|
||||||
|
_ if matches_keybind(&key_event, &config_keybinds.quit) => Some(Action::Quit),
|
||||||
|
_ if matches_keybind(&key_event, &config_keybinds.next_tab) => Some(Action::NextTab),
|
||||||
|
_ if matches_keybind(&key_event, &config_keybinds.prev_tab) => Some(Action::PrevTab),
|
||||||
|
_ if matches_keybind(&key_event, &config_keybinds.next_torrent) => Some(Action::NextTorrent),
|
||||||
|
_ if matches_keybind(&key_event, &config_keybinds.prev_torrent) => Some(Action::PrevTorrent),
|
||||||
|
_ if matches_keybind(&key_event, &config_keybinds.switch_tab_1) => {
|
||||||
|
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 updates of [`App`].
|
/// Handles the updates of [`App`].
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn update(app: &mut App<'_>, action: Action) -> anyhow::Result<()> {
|
pub async fn update(app: &mut App<'_>, action: Action) -> color_eyre::eyre::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);
|
||||||
|
|||||||
15
src/lib.rs
15
src/lib.rs
@ -1,14 +1,7 @@
|
|||||||
/// Application.
|
pub mod config;
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
|
||||||
/// Terminal events handler.
|
|
||||||
pub mod event;
|
pub mod event;
|
||||||
|
|
||||||
/// Widget renderer.
|
|
||||||
pub mod ui;
|
|
||||||
|
|
||||||
/// Terminal user interface.
|
|
||||||
pub mod tui;
|
|
||||||
|
|
||||||
/// Event handler.
|
|
||||||
pub mod handler;
|
pub mod handler;
|
||||||
|
pub mod log;
|
||||||
|
pub mod tui;
|
||||||
|
pub mod ui;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
use anyhow::Result;
|
use color_eyre::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};
|
||||||
|
|
||||||
|
|||||||
16
src/main.rs
16
src/main.rs
@ -1,11 +1,12 @@
|
|||||||
mod log;
|
mod log;
|
||||||
|
|
||||||
use color_eyre::Result;
|
use color_eyre::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;
|
||||||
use traxor::{
|
use traxor::{
|
||||||
app::App,
|
app::App,
|
||||||
|
config::Config,
|
||||||
event::{Event, EventHandler},
|
event::{Event, EventHandler},
|
||||||
handler::{get_action, update},
|
handler::{get_action, update},
|
||||||
tui::Tui,
|
tui::Tui,
|
||||||
@ -13,11 +14,16 @@ use traxor::{
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
|
||||||
// Setup the logger.
|
// Setup the logger.
|
||||||
setup_logger()?;
|
setup_logger()?;
|
||||||
|
|
||||||
|
// Load configuration.
|
||||||
|
let config = Config::load()?;
|
||||||
|
|
||||||
// Create an application.
|
// Create an application.
|
||||||
let mut app = App::new()?;
|
let mut app = App::new(config)?;
|
||||||
|
|
||||||
// Initialize the terminal user interface.
|
// Initialize the terminal user interface.
|
||||||
let backend = CrosstermBackend::new(io::stderr());
|
let backend = CrosstermBackend::new(io::stderr());
|
||||||
@ -33,10 +39,9 @@ async fn main() -> Result<()> {
|
|||||||
// Handle events.
|
// Handle events.
|
||||||
match tui.events.next()? {
|
match tui.events.next()? {
|
||||||
Event::Tick => app.tick().await?,
|
Event::Tick => app.tick().await?,
|
||||||
// Event::Key(key_event) => handle_key_events(key_event, &mut app).await?,
|
|
||||||
Event::Key(key_event) => {
|
Event::Key(key_event) => {
|
||||||
if let Some(action) = get_action(key_event) {
|
if let Some(action) = get_action(key_event, &app) {
|
||||||
update(&mut app, action).await.unwrap();
|
update(&mut app, action).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Mouse(_) => {}
|
Event::Mouse(_) => {}
|
||||||
@ -48,3 +53,4 @@ async fn main() -> Result<()> {
|
|||||||
tui.exit()?;
|
tui.exit()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 anyhow::Result;
|
use color_eyre::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;
|
||||||
|
|||||||
@ -1,30 +1,33 @@
|
|||||||
use ratatui::{prelude::*, widgets::*};
|
use ratatui::{prelude::*, widgets::*};
|
||||||
|
use crate::app::App;
|
||||||
|
|
||||||
pub fn render_help(frame: &mut Frame) {
|
pub fn render_help(frame: &mut Frame, app: &App) {
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title("Help")
|
.title("Help")
|
||||||
.title_alignment(Alignment::Center)
|
.title_alignment(Alignment::Center)
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded);
|
.border_type(BorderType::Rounded);
|
||||||
|
|
||||||
|
let keybinds = &app.config.keybinds;
|
||||||
|
|
||||||
let rows = vec![
|
let rows = vec![
|
||||||
Row::new(vec![Cell::from("?"), Cell::from("Show help")]),
|
Row::new(vec![Cell::from(keybinds.toggle_help.as_deref().unwrap_or("?")), Cell::from("Show help")]),
|
||||||
Row::new(vec![Cell::from("q"), Cell::from("Quit")]),
|
Row::new(vec![Cell::from(keybinds.quit.as_deref().unwrap_or("q")), Cell::from("Quit")]),
|
||||||
Row::new(vec![Cell::from("h"), Cell::from("Left")]),
|
Row::new(vec![Cell::from(keybinds.prev_tab.as_deref().unwrap_or("h")), Cell::from("Left")]),
|
||||||
Row::new(vec![Cell::from("l"), Cell::from("Right")]),
|
Row::new(vec![Cell::from(keybinds.next_tab.as_deref().unwrap_or("l")), Cell::from("Right")]),
|
||||||
Row::new(vec![Cell::from("j"), Cell::from("Down")]),
|
Row::new(vec![Cell::from(keybinds.next_torrent.as_deref().unwrap_or("j")), Cell::from("Down")]),
|
||||||
Row::new(vec![Cell::from("k"), Cell::from("Up")]),
|
Row::new(vec![Cell::from(keybinds.prev_torrent.as_deref().unwrap_or("k")), Cell::from("Up")]),
|
||||||
Row::new(vec![Cell::from("1"), Cell::from("Switch to All tab")]),
|
Row::new(vec![Cell::from(keybinds.switch_tab_1.as_deref().unwrap_or("1")), Cell::from("Switch to All tab")]),
|
||||||
Row::new(vec![Cell::from("2"), Cell::from("Switch to Active tab")]),
|
Row::new(vec![Cell::from(keybinds.switch_tab_2.as_deref().unwrap_or("2")), Cell::from("Switch to Active tab")]),
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
Cell::from("3"),
|
Cell::from(keybinds.switch_tab_3.as_deref().unwrap_or("3")),
|
||||||
Cell::from("Switch to Downloading tab"),
|
Cell::from("Switch to Downloading tab"),
|
||||||
]),
|
]),
|
||||||
Row::new(vec![Cell::from("t"), Cell::from("Toggle torrent")]),
|
Row::new(vec![Cell::from(keybinds.toggle_torrent.as_deref().unwrap_or("t")), Cell::from("Toggle torrent")]),
|
||||||
Row::new(vec![Cell::from("a"), Cell::from("Toggle all torrents")]),
|
Row::new(vec![Cell::from(keybinds.toggle_all.as_deref().unwrap_or("a")), Cell::from("Toggle all torrents")]),
|
||||||
Row::new(vec![Cell::from("d"), Cell::from("Delete torrent")]),
|
Row::new(vec![Cell::from(keybinds.delete.as_deref().unwrap_or("d")), Cell::from("Delete torrent")]),
|
||||||
Row::new(vec![Cell::from("D"), Cell::from("Delete torrent and data")]),
|
Row::new(vec![Cell::from(keybinds.delete_force.as_deref().unwrap_or("D")), Cell::from("Delete torrent and data")]),
|
||||||
Row::new(vec![Cell::from(" "), Cell::from("Select torrent")]),
|
Row::new(vec![Cell::from(keybinds.select.as_deref().unwrap_or(" ")), Cell::from("Select torrent")]),
|
||||||
];
|
];
|
||||||
|
|
||||||
let table = Table::new(
|
let table = Table::new(
|
||||||
@ -32,7 +35,7 @@ pub fn render_help(frame: &mut Frame) {
|
|||||||
&[Constraint::Percentage(20), Constraint::Percentage(80)],
|
&[Constraint::Percentage(20), Constraint::Percentage(80)],
|
||||||
)
|
)
|
||||||
.block(block)
|
.block(block)
|
||||||
.style(Style::default().fg(Color::Green));
|
.style(Style::default().fg(app.config.colors.get_color(&app.config.colors.info_foreground)));
|
||||||
|
|
||||||
let area = frame.area();
|
let area = frame.area();
|
||||||
let height = 15; // Desired height for the help menu
|
let height = 15; // Desired height for the help menu
|
||||||
|
|||||||
@ -32,8 +32,8 @@ pub fn render(app: &mut App, frame: &mut Frame) {
|
|||||||
.border_type(BorderType::Rounded),
|
.border_type(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.select(app.index())
|
.select(app.index())
|
||||||
.style(Style::default().fg(Color::Blue))
|
.style(Style::default().fg(app.config.colors.get_color(&app.config.colors.info_foreground)))
|
||||||
.highlight_style(Style::default().fg(Color::Green))
|
.highlight_style(Style::default().fg(app.config.colors.get_color(&app.config.colors.warning_foreground)))
|
||||||
.divider("|");
|
.divider("|");
|
||||||
|
|
||||||
frame.render_widget(tabs, chunks[0]); // renders tab
|
frame.render_widget(tabs, chunks[0]); // renders tab
|
||||||
@ -51,6 +51,6 @@ pub fn render(app: &mut App, frame: &mut Frame) {
|
|||||||
frame.render_stateful_widget(table, chunks[1], &mut app.state); // renders table
|
frame.render_stateful_widget(table, chunks[1], &mut app.state); // renders table
|
||||||
|
|
||||||
if app.show_help {
|
if app.show_help {
|
||||||
render_help(frame);
|
render_help(frame, app);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use crate::app::{utils::Wrapper, App, Tab};
|
use crate::app::{utils::Wrapper, App, Tab};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::Constraint,
|
layout::Constraint,
|
||||||
style::{Color, Style, Styled},
|
style::{Style, Styled},
|
||||||
widgets::{Block, BorderType, Borders, Row, Table},
|
widgets::{Block, BorderType, Borders, Row, Table},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -9,7 +9,10 @@ pub fn render_table<'a>(app: &mut App, tab: Tab) -> Table<'a> {
|
|||||||
let fields = tab.fields();
|
let fields = tab.fields();
|
||||||
let selected = &app.torrents.selected.clone();
|
let selected = &app.torrents.selected.clone();
|
||||||
let torrents = &app.torrents.set_fields(None).torrents;
|
let torrents = &app.torrents.set_fields(None).torrents;
|
||||||
let highlight_style = Style::default().bg(Color::Magenta).fg(Color::Black);
|
|
||||||
|
let highlight_bg = app.config.colors.get_color(&app.config.colors.highlight_background);
|
||||||
|
let highlight_fg = app.config.colors.get_color(&app.config.colors.highlight_foreground);
|
||||||
|
let highlight_style = Style::default().bg(highlight_bg).fg(highlight_fg);
|
||||||
|
|
||||||
let rows: Vec<Row<'_>> = torrents
|
let rows: Vec<Row<'_>> = torrents
|
||||||
.iter()
|
.iter()
|
||||||
@ -35,15 +38,18 @@ pub fn render_table<'a>(app: &mut App, tab: Tab) -> Table<'a> {
|
|||||||
.map(|&field| Constraint::Length(field.width()))
|
.map(|&field| Constraint::Length(field.width()))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let header_fg = app.config.colors.get_color(&app.config.colors.warning_foreground);
|
||||||
let header = Row::new(
|
let header = Row::new(
|
||||||
fields
|
fields
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&field| field.title())
|
.map(|&field| field.title())
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
)
|
)
|
||||||
.style(Style::default().fg(Color::Yellow));
|
.style(Style::default().fg(header_fg));
|
||||||
|
|
||||||
let highlight_style = Style::default().bg(Color::Blue).fg(Color::Black);
|
let row_highlight_bg = app.config.colors.get_color(&app.config.colors.info_foreground);
|
||||||
|
let row_highlight_fg = app.config.colors.get_color(&app.config.colors.highlight_foreground);
|
||||||
|
let row_highlight_style = Style::default().bg(row_highlight_bg).fg(row_highlight_fg);
|
||||||
|
|
||||||
Table::new(rows, widths)
|
Table::new(rows, widths)
|
||||||
.block(
|
.block(
|
||||||
@ -52,6 +58,6 @@ pub fn render_table<'a>(app: &mut App, tab: Tab) -> Table<'a> {
|
|||||||
.border_type(BorderType::Rounded),
|
.border_type(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.header(header)
|
.header(header)
|
||||||
.row_highlight_style(highlight_style)
|
.row_highlight_style(row_highlight_style)
|
||||||
.column_spacing(1)
|
.column_spacing(1)
|
||||||
}
|
}
|
||||||
|
|||||||
23
tests/app.rs
23
tests/app.rs
@ -1,21 +1,24 @@
|
|||||||
use traxor::app::App;
|
use traxor::{app::App, config::Config};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_app_creation() {
|
fn test_app_creation() {
|
||||||
let app = App::new().unwrap();
|
let config = Config::load().unwrap();
|
||||||
|
let app = App::new(config).unwrap();
|
||||||
assert_eq!(app.tabs().len(), 3);
|
assert_eq!(app.tabs().len(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_app_quit() {
|
fn test_app_quit() {
|
||||||
let mut app = App::new().unwrap();
|
let config = Config::load().unwrap();
|
||||||
|
let mut app = App::new(config).unwrap();
|
||||||
app.quit();
|
app.quit();
|
||||||
assert!(!app.running);
|
assert!(!app.running);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_app_next_tab() {
|
fn test_app_next_tab() {
|
||||||
let mut app = App::new().unwrap();
|
let config = Config::load().unwrap();
|
||||||
|
let mut app = App::new(config).unwrap();
|
||||||
assert_eq!(app.index(), 0);
|
assert_eq!(app.index(), 0);
|
||||||
app.next_tab();
|
app.next_tab();
|
||||||
assert_eq!(app.index(), 1);
|
assert_eq!(app.index(), 1);
|
||||||
@ -27,7 +30,8 @@ fn test_app_next_tab() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_app_prev_tab() {
|
fn test_app_prev_tab() {
|
||||||
let mut app = App::new().unwrap();
|
let config = Config::load().unwrap();
|
||||||
|
let mut app = App::new(config).unwrap();
|
||||||
assert_eq!(app.index(), 0);
|
assert_eq!(app.index(), 0);
|
||||||
app.prev_tab();
|
app.prev_tab();
|
||||||
assert_eq!(app.index(), 2); // Wraps around
|
assert_eq!(app.index(), 2); // Wraps around
|
||||||
@ -37,7 +41,8 @@ fn test_app_prev_tab() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_app_switch_tab() {
|
fn test_app_switch_tab() {
|
||||||
let mut app = App::new().unwrap();
|
let config = Config::load().unwrap();
|
||||||
|
let mut app = App::new(config).unwrap();
|
||||||
assert_eq!(app.index(), 0);
|
assert_eq!(app.index(), 0);
|
||||||
app.switch_tab(2);
|
app.switch_tab(2);
|
||||||
assert_eq!(app.index(), 2);
|
assert_eq!(app.index(), 2);
|
||||||
@ -47,7 +52,8 @@ fn test_app_switch_tab() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_app_toggle_popup() {
|
fn test_app_toggle_popup() {
|
||||||
let mut app = App::new().unwrap();
|
let config = Config::load().unwrap();
|
||||||
|
let mut app = App::new(config).unwrap();
|
||||||
assert!(!app.show_help);
|
assert!(!app.show_help);
|
||||||
app.toggle_help();
|
app.toggle_help();
|
||||||
assert!(app.show_help);
|
assert!(app.show_help);
|
||||||
@ -57,7 +63,8 @@ fn test_app_toggle_popup() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_app_open_close_popup() {
|
fn test_app_open_close_popup() {
|
||||||
let mut app = App::new().unwrap();
|
let config = Config::load().unwrap();
|
||||||
|
let mut app = App::new(config).unwrap();
|
||||||
assert!(!app.show_help);
|
assert!(!app.show_help);
|
||||||
app.open_help();
|
app.open_help();
|
||||||
assert!(app.show_help);
|
assert!(app.show_help);
|
||||||
|
|||||||
@ -1,109 +1,86 @@
|
|||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
use traxor::{app::action::Action, handler::get_action};
|
use traxor::{app::action::Action, handler::get_action, app::App, config::Config};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_action_quit() {
|
fn test_get_action_quit() {
|
||||||
assert_eq!(get_action(KeyEvent::from(KeyCode::Esc)), Some(Action::Quit));
|
let config = Config::load().unwrap();
|
||||||
|
let app = App::new(config).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_action(KeyEvent::from(KeyCode::Char('q'))),
|
get_action(KeyEvent::from(KeyCode::Char('q')), &app),
|
||||||
Some(Action::Quit)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
get_action(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)),
|
|
||||||
Some(Action::Quit)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
get_action(KeyEvent::new(KeyCode::Char('C'), KeyModifiers::CONTROL)),
|
|
||||||
Some(Action::Quit)
|
Some(Action::Quit)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_action_navigation() {
|
fn test_get_action_navigation() {
|
||||||
|
let config = Config::load().unwrap();
|
||||||
|
let app = App::new(config).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_action(KeyEvent::from(KeyCode::Char('l'))),
|
get_action(KeyEvent::from(KeyCode::Char('l')), &app),
|
||||||
Some(Action::NextTab)
|
Some(Action::NextTab)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_action(KeyEvent::from(KeyCode::Right)),
|
get_action(KeyEvent::from(KeyCode::Char('h')), &app),
|
||||||
Some(Action::NextTab)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
get_action(KeyEvent::from(KeyCode::Char('h'))),
|
|
||||||
Some(Action::PrevTab)
|
Some(Action::PrevTab)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_action(KeyEvent::from(KeyCode::Left)),
|
get_action(KeyEvent::from(KeyCode::Char('j')), &app),
|
||||||
Some(Action::PrevTab)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
get_action(KeyEvent::from(KeyCode::Char('j'))),
|
|
||||||
Some(Action::NextTorrent)
|
Some(Action::NextTorrent)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_action(KeyEvent::from(KeyCode::Down)),
|
get_action(KeyEvent::from(KeyCode::Char('k')), &app),
|
||||||
Some(Action::NextTorrent)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
get_action(KeyEvent::from(KeyCode::Char('k'))),
|
|
||||||
Some(Action::PrevTorrent)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
get_action(KeyEvent::from(KeyCode::Up)),
|
|
||||||
Some(Action::PrevTorrent)
|
Some(Action::PrevTorrent)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_action_switch_tab() {
|
fn test_get_action_switch_tab() {
|
||||||
|
let config = Config::load().unwrap();
|
||||||
|
let app = App::new(config).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_action(KeyEvent::from(KeyCode::Char('1'))),
|
get_action(KeyEvent::from(KeyCode::Char('1')), &app),
|
||||||
Some(Action::SwitchTab(0))
|
Some(Action::SwitchTab(0))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_action(KeyEvent::from(KeyCode::Char('2'))),
|
get_action(KeyEvent::from(KeyCode::Char('2')), &app),
|
||||||
Some(Action::SwitchTab(1))
|
Some(Action::SwitchTab(1))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_action(KeyEvent::from(KeyCode::Char('3'))),
|
get_action(KeyEvent::from(KeyCode::Char('3')), &app),
|
||||||
Some(Action::SwitchTab(2))
|
Some(Action::SwitchTab(2))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_action_torrent_actions() {
|
fn test_get_action_torrent_actions() {
|
||||||
|
let config = Config::load().unwrap();
|
||||||
|
let app = App::new(config).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_action(KeyEvent::from(KeyCode::Char('t'))),
|
get_action(KeyEvent::from(KeyCode::Enter), &app),
|
||||||
Some(Action::ToggleTorrent)
|
Some(Action::ToggleTorrent)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_action(KeyEvent::from(KeyCode::Enter)),
|
get_action(KeyEvent::from(KeyCode::Char('a')), &app),
|
||||||
Some(Action::ToggleTorrent)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
get_action(KeyEvent::from(KeyCode::Menu)),
|
|
||||||
Some(Action::ToggleTorrent)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
get_action(KeyEvent::from(KeyCode::Char('a'))),
|
|
||||||
Some(Action::ToggleAll)
|
Some(Action::ToggleAll)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_action(KeyEvent::from(KeyCode::Char('d'))),
|
get_action(KeyEvent::from(KeyCode::Char('d')), &app),
|
||||||
Some(Action::Delete(false))
|
Some(Action::Delete(false))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_action(KeyEvent::from(KeyCode::Char('D'))),
|
get_action(KeyEvent::from(KeyCode::Char('D')), &app),
|
||||||
Some(Action::Delete(true))
|
Some(Action::Delete(true))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
get_action(KeyEvent::from(KeyCode::Char(' '))),
|
get_action(KeyEvent::from(KeyCode::Char(' ')), &app),
|
||||||
Some(Action::Select)
|
Some(Action::Select)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_action_unhandled() {
|
fn test_get_action_unhandled() {
|
||||||
assert_eq!(get_action(KeyEvent::from(KeyCode::Char('x'))), None);
|
let config = Config::load().unwrap();
|
||||||
assert_eq!(get_action(KeyEvent::from(KeyCode::F(1))), None);
|
let app = App::new(config).unwrap();
|
||||||
|
assert_eq!(get_action(KeyEvent::from(KeyCode::Char('x')), &app), None);
|
||||||
|
assert_eq!(get_action(KeyEvent::from(KeyCode::F(1)), &app), None);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user