mirror of
https://github.com/kristoferssolo/traxor.git
synced 2025-10-21 20:10:35 +00:00
refactor: use derive macro
This commit is contained in:
parent
41b3a03e80
commit
7d58d1b74c
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -301,6 +301,15 @@ dependencies = [
|
|||||||
"powerfmt",
|
"powerfmt",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_macro"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@ -1951,6 +1960,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"crossterm 0.29.0",
|
"crossterm 0.29.0",
|
||||||
|
"derive_macro",
|
||||||
"dirs",
|
"dirs",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@ -19,3 +19,4 @@ toml = "0.8"
|
|||||||
dirs = "6.0"
|
dirs = "6.0"
|
||||||
transmission-rpc = "0.5"
|
transmission-rpc = "0.5"
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
|
derive_macro = { path = "derive_macro" }
|
||||||
|
|||||||
47
derive_macro/Cargo.lock
generated
Normal file
47
derive_macro/Cargo.lock
generated
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_macro"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.95"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.104"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||||
12
derive_macro/Cargo.toml
Normal file
12
derive_macro/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "derive_macro"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["proc-macro"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc-macro2 = "1.0"
|
||||||
|
quote = "1.0"
|
||||||
|
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
||||||
11
derive_macro/src/lib.rs
Normal file
11
derive_macro/src/lib.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
mod merge;
|
||||||
|
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use syn::{DeriveInput, parse_macro_input};
|
||||||
|
|
||||||
|
#[proc_macro_derive(Merge)]
|
||||||
|
pub fn merge_derive(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
merge::impl_merge_derive(input)
|
||||||
|
}
|
||||||
|
|
||||||
58
derive_macro/src/merge.rs
Normal file
58
derive_macro/src/merge.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{Data, DeriveInput, Fields, Type};
|
||||||
|
|
||||||
|
pub fn impl_merge_derive(input: DeriveInput) -> TokenStream {
|
||||||
|
let name = &input.ident;
|
||||||
|
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
|
||||||
|
|
||||||
|
let fields = match input.data {
|
||||||
|
Data::Struct(data) => match data.fields {
|
||||||
|
Fields::Named(fields) => fields.named,
|
||||||
|
_ => unimplemented!("Only named fields are supported for Merge derive macro"),
|
||||||
|
},
|
||||||
|
_ => unimplemented!("Only structs are supported for Merge derive macro"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let merge_logic = fields.iter().map(|field| {
|
||||||
|
let field_name = &field.ident;
|
||||||
|
let field_type = &field.ty;
|
||||||
|
|
||||||
|
// Check if the field is an Option<T>
|
||||||
|
if let Type::Path(type_path) = field_type {
|
||||||
|
if let Some(segment) = type_path.path.segments.last() {
|
||||||
|
if segment.ident == "Option" {
|
||||||
|
// This is an Option<T> field
|
||||||
|
return quote! {
|
||||||
|
if let Some(o_val) = other.#field_name {
|
||||||
|
if let Some(s_val) = self.#field_name.as_mut() {
|
||||||
|
// If both are Some, attempt to merge recursively
|
||||||
|
s_val.merge(o_val);
|
||||||
|
} else {
|
||||||
|
// If self is None, take the other's Some value
|
||||||
|
self.#field_name = Some(o_val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If other is None, self remains unchanged
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-Option fields, attempt to merge recursively
|
||||||
|
quote! {
|
||||||
|
self.#field_name.merge(other.#field_name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let expanded = quote! {
|
||||||
|
impl #impl_generics Merge for #name #ty_generics #where_clause {
|
||||||
|
fn merge(&mut self, other: Self) {
|
||||||
|
#(#merge_logic)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
expanded.into()
|
||||||
|
}
|
||||||
|
|
||||||
@ -20,8 +20,9 @@ impl Torrents {
|
|||||||
self.client
|
self.client
|
||||||
.torrent_action(action, vec![id])
|
.torrent_action(action, vec![id])
|
||||||
.await
|
.await
|
||||||
.map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?;
|
.map_err(|e| {
|
||||||
|
color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string())
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -48,7 +49,9 @@ impl Torrents {
|
|||||||
self.client
|
self.client
|
||||||
.torrent_action(action, vec![id])
|
.torrent_action(action, vec![id])
|
||||||
.await
|
.await
|
||||||
.map_err(|e| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?;
|
.map_err(|e| {
|
||||||
|
color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string())
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -71,12 +74,18 @@ impl Torrents {
|
|||||||
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| color_eyre::eyre::eyre!("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) -> color_eyre::eyre::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
|
||||||
@ -89,7 +98,9 @@ impl Torrents {
|
|||||||
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| color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string()))?;
|
.map_err(|e| {
|
||||||
|
color_eyre::eyre::eyre!("Transmission RPC error: {}", e.to_string())
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
use crate::merge_fields;
|
use crate::merge::Merge;
|
||||||
|
use derive_macro::Merge;
|
||||||
use ratatui::prelude::*;
|
use ratatui::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize, Merge)]
|
||||||
pub struct ColorsConfig {
|
pub struct ColorsConfig {
|
||||||
pub highlight_background: Option<String>,
|
pub highlight_background: Option<String>,
|
||||||
pub highlight_foreground: Option<String>,
|
pub highlight_foreground: Option<String>,
|
||||||
@ -32,18 +33,6 @@ impl ColorsConfig {
|
|||||||
None => Color::Reset,
|
None => Color::Reset,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn merge(&mut self, other: Self) {
|
|
||||||
merge_fields!(
|
|
||||||
self,
|
|
||||||
other,
|
|
||||||
highlight_background,
|
|
||||||
highlight_foreground,
|
|
||||||
warning_foreground,
|
|
||||||
info_foreground,
|
|
||||||
error_foreground
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ColorsConfig {
|
impl Default for ColorsConfig {
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
|
use crate::merge::Merge;
|
||||||
|
use derive_macro::Merge;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::merge_fields;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize, Merge)]
|
||||||
pub struct KeybindsConfig {
|
pub struct KeybindsConfig {
|
||||||
pub quit: Option<String>,
|
pub quit: Option<String>,
|
||||||
pub next_tab: Option<String>,
|
pub next_tab: Option<String>,
|
||||||
@ -19,29 +20,6 @@ pub struct KeybindsConfig {
|
|||||||
pub toggle_help: Option<String>,
|
pub toggle_help: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeybindsConfig {
|
|
||||||
pub fn merge(&mut self, other: Self) {
|
|
||||||
merge_fields!(
|
|
||||||
self,
|
|
||||||
other,
|
|
||||||
quit,
|
|
||||||
next_tab,
|
|
||||||
prev_tab,
|
|
||||||
next_torrent,
|
|
||||||
prev_torrent,
|
|
||||||
switch_tab_1,
|
|
||||||
switch_tab_2,
|
|
||||||
switch_tab_3,
|
|
||||||
toggle_torrent,
|
|
||||||
toggle_all,
|
|
||||||
delete,
|
|
||||||
delete_force,
|
|
||||||
select,
|
|
||||||
toggle_help
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for KeybindsConfig {
|
impl Default for KeybindsConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@ -1,24 +1,15 @@
|
|||||||
mod colors;
|
mod colors;
|
||||||
mod keybinds;
|
mod keybinds;
|
||||||
|
|
||||||
|
use crate::merge::Merge;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use colors::ColorsConfig;
|
use colors::ColorsConfig;
|
||||||
|
use derive_macro::Merge;
|
||||||
use keybinds::KeybindsConfig;
|
use keybinds::KeybindsConfig;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[macro_export]
|
#[derive(Debug, Default, Deserialize, Serialize, Merge)]
|
||||||
macro_rules! merge_fields {
|
|
||||||
($self:ident, $other:ident, $($field:ident),*) => {
|
|
||||||
$(
|
|
||||||
if let Some($field) = $other.$field {
|
|
||||||
$self.$field = Some($field);
|
|
||||||
}
|
|
||||||
)*
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub keybinds: KeybindsConfig,
|
pub keybinds: KeybindsConfig,
|
||||||
pub colors: ColorsConfig,
|
pub colors: ColorsConfig,
|
||||||
@ -64,9 +55,4 @@ impl Config {
|
|||||||
});
|
});
|
||||||
Ok(config_dir.join("traxor").join("config.toml"))
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,8 +72,12 @@ pub fn get_action(key_event: KeyEvent, app: &App) -> Option<Action> {
|
|||||||
_ if matches_keybind(&key_event, &config_keybinds.quit) => Some(Action::Quit),
|
_ 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.next_tab) => Some(Action::NextTab),
|
||||||
_ if matches_keybind(&key_event, &config_keybinds.prev_tab) => Some(Action::PrevTab),
|
_ 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.next_torrent) => {
|
||||||
_ if matches_keybind(&key_event, &config_keybinds.prev_torrent) => Some(Action::PrevTorrent),
|
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) => {
|
_ if matches_keybind(&key_event, &config_keybinds.switch_tab_1) => {
|
||||||
Some(Action::SwitchTab(0))
|
Some(Action::SwitchTab(0))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
pub mod config;
|
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
pub mod config;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod handler;
|
pub mod handler;
|
||||||
pub mod log;
|
pub mod log;
|
||||||
|
pub mod merge;
|
||||||
pub mod tui;
|
pub mod tui;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
@ -53,4 +53,3 @@ async fn main() -> Result<()> {
|
|||||||
tui.exit()?;
|
tui.exit()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
src/merge.rs
Normal file
21
src/merge.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
pub trait Merge {
|
||||||
|
fn merge(&mut self, other: Self);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Merge for String {
|
||||||
|
fn merge(&mut self, other: Self) {
|
||||||
|
*self = other;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Merge for u32 {
|
||||||
|
fn merge(&mut self, other: Self) {
|
||||||
|
*self = other;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Merge for bool {
|
||||||
|
fn merge(&mut self, other: Self) {
|
||||||
|
*self = other;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
use ratatui::{prelude::*, widgets::*};
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
|
use ratatui::{prelude::*, widgets::*};
|
||||||
|
|
||||||
pub fn render_help(frame: &mut Frame, app: &App) {
|
pub fn render_help(frame: &mut Frame, app: &App) {
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
@ -11,23 +11,62 @@ pub fn render_help(frame: &mut Frame, app: &App) {
|
|||||||
let keybinds = &app.config.keybinds;
|
let keybinds = &app.config.keybinds;
|
||||||
|
|
||||||
let rows = vec![
|
let rows = vec![
|
||||||
Row::new(vec![Cell::from(keybinds.toggle_help.as_deref().unwrap_or("?")), Cell::from("Show help")]),
|
Row::new(vec![
|
||||||
Row::new(vec![Cell::from(keybinds.quit.as_deref().unwrap_or("q")), Cell::from("Quit")]),
|
Cell::from(keybinds.toggle_help.as_deref().unwrap_or("?")),
|
||||||
Row::new(vec![Cell::from(keybinds.prev_tab.as_deref().unwrap_or("h")), Cell::from("Left")]),
|
Cell::from("Show help"),
|
||||||
Row::new(vec![Cell::from(keybinds.next_tab.as_deref().unwrap_or("l")), Cell::from("Right")]),
|
]),
|
||||||
Row::new(vec![Cell::from(keybinds.next_torrent.as_deref().unwrap_or("j")), Cell::from("Down")]),
|
Row::new(vec![
|
||||||
Row::new(vec![Cell::from(keybinds.prev_torrent.as_deref().unwrap_or("k")), Cell::from("Up")]),
|
Cell::from(keybinds.quit.as_deref().unwrap_or("q")),
|
||||||
Row::new(vec![Cell::from(keybinds.switch_tab_1.as_deref().unwrap_or("1")), Cell::from("Switch to All tab")]),
|
Cell::from("Quit"),
|
||||||
Row::new(vec![Cell::from(keybinds.switch_tab_2.as_deref().unwrap_or("2")), Cell::from("Switch to Active tab")]),
|
]),
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::from(keybinds.prev_tab.as_deref().unwrap_or("h")),
|
||||||
|
Cell::from("Left"),
|
||||||
|
]),
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::from(keybinds.next_tab.as_deref().unwrap_or("l")),
|
||||||
|
Cell::from("Right"),
|
||||||
|
]),
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::from(keybinds.next_torrent.as_deref().unwrap_or("j")),
|
||||||
|
Cell::from("Down"),
|
||||||
|
]),
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::from(keybinds.prev_torrent.as_deref().unwrap_or("k")),
|
||||||
|
Cell::from("Up"),
|
||||||
|
]),
|
||||||
|
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(keybinds.switch_tab_2.as_deref().unwrap_or("2")),
|
||||||
|
Cell::from("Switch to Active tab"),
|
||||||
|
]),
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
Cell::from(keybinds.switch_tab_3.as_deref().unwrap_or("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(keybinds.toggle_torrent.as_deref().unwrap_or("t")), Cell::from("Toggle torrent")]),
|
Row::new(vec![
|
||||||
Row::new(vec![Cell::from(keybinds.toggle_all.as_deref().unwrap_or("a")), Cell::from("Toggle all torrents")]),
|
Cell::from(keybinds.toggle_torrent.as_deref().unwrap_or("t")),
|
||||||
Row::new(vec![Cell::from(keybinds.delete.as_deref().unwrap_or("d")), Cell::from("Delete torrent")]),
|
Cell::from("Toggle torrent"),
|
||||||
Row::new(vec![Cell::from(keybinds.delete_force.as_deref().unwrap_or("D")), Cell::from("Delete torrent and data")]),
|
]),
|
||||||
Row::new(vec![Cell::from(keybinds.select.as_deref().unwrap_or(" ")), Cell::from("Select torrent")]),
|
Row::new(vec![
|
||||||
|
Cell::from(keybinds.toggle_all.as_deref().unwrap_or("a")),
|
||||||
|
Cell::from("Toggle all torrents"),
|
||||||
|
]),
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::from(keybinds.delete.as_deref().unwrap_or("d")),
|
||||||
|
Cell::from("Delete torrent"),
|
||||||
|
]),
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::from(keybinds.delete_force.as_deref().unwrap_or("D")),
|
||||||
|
Cell::from("Delete torrent and data"),
|
||||||
|
]),
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::from(keybinds.select.as_deref().unwrap_or(" ")),
|
||||||
|
Cell::from("Select torrent"),
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
let table = Table::new(
|
let table = Table::new(
|
||||||
@ -35,7 +74,12 @@ pub fn render_help(frame: &mut Frame, app: &App) {
|
|||||||
&[Constraint::Percentage(20), Constraint::Percentage(80)],
|
&[Constraint::Percentage(20), Constraint::Percentage(80)],
|
||||||
)
|
)
|
||||||
.block(block)
|
.block(block)
|
||||||
.style(Style::default().fg(app.config.colors.get_color(&app.config.colors.info_foreground)));
|
.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
|
||||||
@ -51,4 +95,3 @@ pub fn render_help(frame: &mut Frame, app: &App) {
|
|||||||
frame.render_widget(Clear, popup_area);
|
frame.render_widget(Clear, popup_area);
|
||||||
frame.render_widget(table, popup_area);
|
frame.render_widget(table, popup_area);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,8 +32,18 @@ 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(app.config.colors.get_color(&app.config.colors.info_foreground)))
|
.style(
|
||||||
.highlight_style(Style::default().fg(app.config.colors.get_color(&app.config.colors.warning_foreground)))
|
Style::default().fg(app
|
||||||
|
.config
|
||||||
|
.colors
|
||||||
|
.get_color(&app.config.colors.info_foreground)),
|
||||||
|
)
|
||||||
|
.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
|
||||||
|
|||||||
@ -10,8 +10,14 @@ pub fn render_table<'a>(app: &mut App, tab: Tab) -> Table<'a> {
|
|||||||
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_bg = app.config.colors.get_color(&app.config.colors.highlight_background);
|
let highlight_bg = app
|
||||||
let highlight_fg = app.config.colors.get_color(&app.config.colors.highlight_foreground);
|
.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 highlight_style = Style::default().bg(highlight_bg).fg(highlight_fg);
|
||||||
|
|
||||||
let rows: Vec<Row<'_>> = torrents
|
let rows: Vec<Row<'_>> = torrents
|
||||||
@ -38,7 +44,10 @@ 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_fg = app
|
||||||
|
.config
|
||||||
|
.colors
|
||||||
|
.get_color(&app.config.colors.warning_foreground);
|
||||||
let header = Row::new(
|
let header = Row::new(
|
||||||
fields
|
fields
|
||||||
.iter()
|
.iter()
|
||||||
@ -47,8 +56,14 @@ pub fn render_table<'a>(app: &mut App, tab: Tab) -> Table<'a> {
|
|||||||
)
|
)
|
||||||
.style(Style::default().fg(header_fg));
|
.style(Style::default().fg(header_fg));
|
||||||
|
|
||||||
let row_highlight_bg = app.config.colors.get_color(&app.config.colors.info_foreground);
|
let row_highlight_bg = app
|
||||||
let row_highlight_fg = app.config.colors.get_color(&app.config.colors.highlight_foreground);
|
.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);
|
let row_highlight_style = Style::default().bg(row_highlight_bg).fg(row_highlight_fg);
|
||||||
|
|
||||||
Table::new(rows, widths)
|
Table::new(rows, widths)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use crossterm::event::{KeyCode, KeyEvent};
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
use traxor::{app::action::Action, handler::get_action, app::App, config::Config};
|
use traxor::{app::action::Action, app::App, config::Config, handler::get_action};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_action_quit() {
|
fn test_get_action_quit() {
|
||||||
|
|||||||
286
tests/merge.rs
Normal file
286
tests/merge.rs
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
use derive_macro::Merge;
|
||||||
|
use traxor::merge::Merge;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Merge)]
|
||||||
|
struct NestedStruct {
|
||||||
|
value: u32,
|
||||||
|
name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Merge)]
|
||||||
|
struct TestStruct {
|
||||||
|
a: u32,
|
||||||
|
b: Option<String>,
|
||||||
|
c: Option<bool>,
|
||||||
|
nested: NestedStruct,
|
||||||
|
optional_nested: Option<NestedStruct>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge_basic_fields() {
|
||||||
|
let mut s1 = TestStruct {
|
||||||
|
a: 1,
|
||||||
|
b: Some("hello".to_string()),
|
||||||
|
c: None,
|
||||||
|
nested: NestedStruct {
|
||||||
|
value: 10,
|
||||||
|
name: Some("original".to_string()),
|
||||||
|
},
|
||||||
|
optional_nested: None,
|
||||||
|
};
|
||||||
|
let s2 = TestStruct {
|
||||||
|
a: 2,
|
||||||
|
b: Some("world".to_string()),
|
||||||
|
c: Some(true),
|
||||||
|
nested: NestedStruct {
|
||||||
|
value: 20,
|
||||||
|
name: Some("new".to_string()),
|
||||||
|
},
|
||||||
|
optional_nested: Some(NestedStruct {
|
||||||
|
value: 30,
|
||||||
|
name: Some("optional".to_string()),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
s1.merge(s2);
|
||||||
|
|
||||||
|
assert_eq!(s1.a, 2);
|
||||||
|
assert_eq!(s1.b, Some("world".to_string()));
|
||||||
|
assert_eq!(s1.c, Some(true));
|
||||||
|
assert_eq!(s1.nested.value, 20);
|
||||||
|
assert_eq!(s1.nested.name, Some("new".to_string()));
|
||||||
|
assert_eq!(
|
||||||
|
s1.optional_nested,
|
||||||
|
Some(NestedStruct {
|
||||||
|
value: 30,
|
||||||
|
name: Some("optional".to_string())
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge_option_none_other() {
|
||||||
|
let mut s1 = TestStruct {
|
||||||
|
a: 1,
|
||||||
|
b: Some("hello".to_string()),
|
||||||
|
c: Some(false),
|
||||||
|
nested: NestedStruct {
|
||||||
|
value: 10,
|
||||||
|
name: Some("original".to_string()),
|
||||||
|
},
|
||||||
|
optional_nested: Some(NestedStruct {
|
||||||
|
value: 100,
|
||||||
|
name: Some("existing".to_string()),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let s2 = TestStruct {
|
||||||
|
a: 2,
|
||||||
|
b: None,
|
||||||
|
c: None,
|
||||||
|
nested: NestedStruct {
|
||||||
|
value: 20,
|
||||||
|
name: None,
|
||||||
|
},
|
||||||
|
optional_nested: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
s1.merge(s2);
|
||||||
|
|
||||||
|
assert_eq!(s1.a, 2);
|
||||||
|
assert_eq!(s1.b, Some("hello".to_string())); // Should remain Some("hello")
|
||||||
|
assert_eq!(s1.c, Some(false)); // Should remain Some(false)
|
||||||
|
assert_eq!(s1.nested.value, 20);
|
||||||
|
assert_eq!(s1.nested.name, Some("original".to_string())); // Should remain original
|
||||||
|
assert_eq!(
|
||||||
|
s1.optional_nested,
|
||||||
|
Some(NestedStruct {
|
||||||
|
value: 100,
|
||||||
|
name: Some("existing".to_string())
|
||||||
|
})
|
||||||
|
); // Should remain existing
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge_option_some_other() {
|
||||||
|
let mut s1 = TestStruct {
|
||||||
|
a: 1,
|
||||||
|
b: None,
|
||||||
|
c: None,
|
||||||
|
nested: NestedStruct {
|
||||||
|
value: 10,
|
||||||
|
name: None,
|
||||||
|
},
|
||||||
|
optional_nested: None,
|
||||||
|
};
|
||||||
|
let s2 = TestStruct {
|
||||||
|
a: 2,
|
||||||
|
b: Some("world".to_string()),
|
||||||
|
c: Some(true),
|
||||||
|
nested: NestedStruct {
|
||||||
|
value: 20,
|
||||||
|
name: Some("new".to_string()),
|
||||||
|
},
|
||||||
|
optional_nested: Some(NestedStruct {
|
||||||
|
value: 30,
|
||||||
|
name: Some("optional".to_string()),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
s1.merge(s2);
|
||||||
|
|
||||||
|
assert_eq!(s1.a, 2);
|
||||||
|
assert_eq!(s1.b, Some("world".to_string()));
|
||||||
|
assert_eq!(s1.c, Some(true));
|
||||||
|
assert_eq!(s1.nested.value, 20);
|
||||||
|
assert_eq!(s1.nested.name, Some("new".to_string()));
|
||||||
|
assert_eq!(
|
||||||
|
s1.optional_nested,
|
||||||
|
Some(NestedStruct {
|
||||||
|
value: 30,
|
||||||
|
name: Some("optional".to_string())
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge_nested_struct_with_none_name() {
|
||||||
|
let mut s1 = TestStruct {
|
||||||
|
a: 1,
|
||||||
|
b: None,
|
||||||
|
c: None,
|
||||||
|
nested: NestedStruct {
|
||||||
|
value: 10,
|
||||||
|
name: Some("original".to_string()),
|
||||||
|
},
|
||||||
|
optional_nested: None,
|
||||||
|
};
|
||||||
|
let s2 = TestStruct {
|
||||||
|
a: 2,
|
||||||
|
b: None,
|
||||||
|
c: None,
|
||||||
|
nested: NestedStruct {
|
||||||
|
value: 20,
|
||||||
|
name: None,
|
||||||
|
},
|
||||||
|
optional_nested: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
s1.merge(s2);
|
||||||
|
|
||||||
|
assert_eq!(s1.nested.value, 20);
|
||||||
|
assert_eq!(s1.nested.name, Some("original".to_string())); // Should remain original
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge_optional_nested_struct_some_to_some() {
|
||||||
|
let mut s1 = TestStruct {
|
||||||
|
a: 1,
|
||||||
|
b: None,
|
||||||
|
c: None,
|
||||||
|
nested: NestedStruct {
|
||||||
|
value: 10,
|
||||||
|
name: None,
|
||||||
|
},
|
||||||
|
optional_nested: Some(NestedStruct {
|
||||||
|
value: 100,
|
||||||
|
name: Some("existing".to_string()),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let s2 = TestStruct {
|
||||||
|
a: 2,
|
||||||
|
b: None,
|
||||||
|
c: None,
|
||||||
|
nested: NestedStruct {
|
||||||
|
value: 20,
|
||||||
|
name: None,
|
||||||
|
},
|
||||||
|
optional_nested: Some(NestedStruct {
|
||||||
|
value: 200,
|
||||||
|
name: Some("new_optional".to_string()),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
s1.merge(s2);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
s1.optional_nested,
|
||||||
|
Some(NestedStruct {
|
||||||
|
value: 200,
|
||||||
|
name: Some("new_optional".to_string())
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge_optional_nested_struct_none_to_some() {
|
||||||
|
let mut s1 = TestStruct {
|
||||||
|
a: 1,
|
||||||
|
b: None,
|
||||||
|
c: None,
|
||||||
|
nested: NestedStruct {
|
||||||
|
value: 10,
|
||||||
|
name: None,
|
||||||
|
},
|
||||||
|
optional_nested: None,
|
||||||
|
};
|
||||||
|
let s2 = TestStruct {
|
||||||
|
a: 2,
|
||||||
|
b: None,
|
||||||
|
c: None,
|
||||||
|
nested: NestedStruct {
|
||||||
|
value: 20,
|
||||||
|
name: None,
|
||||||
|
},
|
||||||
|
optional_nested: Some(NestedStruct {
|
||||||
|
value: 200,
|
||||||
|
name: Some("new_optional".to_string()),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
s1.merge(s2);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
s1.optional_nested,
|
||||||
|
Some(NestedStruct {
|
||||||
|
value: 200,
|
||||||
|
name: Some("new_optional".to_string())
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merge_optional_nested_struct_some_to_none() {
|
||||||
|
let mut s1 = TestStruct {
|
||||||
|
a: 1,
|
||||||
|
b: None,
|
||||||
|
c: None,
|
||||||
|
nested: NestedStruct {
|
||||||
|
value: 10,
|
||||||
|
name: None,
|
||||||
|
},
|
||||||
|
optional_nested: Some(NestedStruct {
|
||||||
|
value: 100,
|
||||||
|
name: Some("existing".to_string()),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let s2 = TestStruct {
|
||||||
|
a: 2,
|
||||||
|
b: None,
|
||||||
|
c: None,
|
||||||
|
nested: NestedStruct {
|
||||||
|
value: 20,
|
||||||
|
name: None,
|
||||||
|
},
|
||||||
|
optional_nested: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
s1.merge(s2);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
s1.optional_nested,
|
||||||
|
Some(NestedStruct {
|
||||||
|
value: 100,
|
||||||
|
name: Some("existing".to_string())
|
||||||
|
})
|
||||||
|
); // Should remain existing
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user