refactor(config): simplify config reading

!squash me

!squash me
This commit is contained in:
Kristofers Solo 2025-07-10 17:08:42 +03:00
parent b341b7a661
commit b988880c41
Signed by: kristoferssolo
GPG Key ID: 8687F2D3EEE6F0ED
20 changed files with 328 additions and 229 deletions

2
.gitignore vendored
View File

@ -19,4 +19,4 @@ target/
*.pdb *.pdb
# Log dir # Log dir
.log/ .logs/

File diff suppressed because one or more lines are too long

22
Cargo.lock generated
View File

@ -122,9 +122,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.27" version = "1.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@ -314,6 +314,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"
@ -610,9 +619,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.14" version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
@ -1326,9 +1335,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.20" version = "0.12.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
@ -2035,6 +2044,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"color-eyre", "color-eyre",
"crossterm 0.29.0", "crossterm 0.29.0",
"derive_macro",
"dirs", "dirs",
"merge", "merge",
"ratatui", "ratatui",

View File

@ -3,9 +3,10 @@ name = "traxor"
version = "0.1.0" version = "0.1.0"
authors = ["Kristofers Solo <dev@kristofers.xyz>"] authors = ["Kristofers Solo <dev@kristofers.xyz>"]
license = "GPLv3" license = "GPLv3"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
derive_macro = { path = "derive_macro" }
color-eyre = "0.6" color-eyre = "0.6"
crossterm = "0.29" crossterm = "0.29"
dirs = "6.0" dirs = "6.0"

47
derive_macro/Cargo.lock generated Normal file
View 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
View 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"] }

View File

@ -0,0 +1,42 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Fields, Ident};
pub fn impl_from_file(input: DeriveInput) -> TokenStream {
let name = &input.ident;
let file_name = format!("{name}File");
let file_ident = Ident::new(&file_name, name.span());
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => &fields.named,
_ => panic!("Only named fields are supported"),
},
_ => panic!("Only structs are supported"),
};
let field_idents = fields.iter().map(|f| &f.ident);
let field_idents2 = field_idents.clone();
quote! {
impl #name {
fn from_file(file: Option<#file_ident>) -> Self
where
#file_ident: Default + Clone
{
let default = #file_ident::default();
let file = file.unwrap_or_else(|| default.clone());
Self {
#(#field_idents: file.#field_idents2.unwrap_or_else(|| default.#field_idents.clone().unwrap())),*
}
}
}
impl From<Option<#file_ident>> for #name {
fn from(value: Option<#file_ident>) -> Self {
Self::from_file(value)
}
}
}.into()
}

10
derive_macro/src/lib.rs Normal file
View File

@ -0,0 +1,10 @@
mod from_file;
use proc_macro::TokenStream;
use syn::{DeriveInput, parse_macro_input};
#[proc_macro_derive(FromFile)]
pub fn derive_from_file(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
from_file::impl_from_file(input)
}

View File

@ -1,45 +1,52 @@
use merge::Merge; use derive_macro::FromFile;
use merge::{Merge, option::overwrite_none};
use ratatui::prelude::*; use ratatui::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Merge)] #[derive(Debug, Clone, Deserialize, Serialize, Merge)]
pub struct ColorsConfig { pub struct ColorsConfigFile {
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub highlight_background: Option<String>, pub highlight_background: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub highlight_foreground: Option<String>, pub highlight_foreground: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub warning_foreground: Option<String>, pub warning_foreground: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub info_foreground: Option<String>, pub info_foreground: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub error_foreground: Option<String>, pub error_foreground: Option<String>,
} }
#[derive(Debug, Clone, FromFile)]
pub struct ColorsConfig {
pub highlight_background: String,
pub highlight_foreground: String,
pub warning_foreground: String,
pub info_foreground: String,
pub error_foreground: String,
}
impl ColorsConfig { impl ColorsConfig {
pub fn get_color(&self, color_name: &Option<String>) -> Color { pub fn get_color(&self, color_name: &str) -> Color {
match color_name { match color_name.to_lowercase().as_str() {
Some(name) => match name.to_lowercase().as_str() { "black" => Color::Black,
"black" => Color::Black, "blue" => Color::Blue,
"blue" => Color::Blue, "cyan" => Color::Cyan,
"cyan" => Color::Cyan, "darkgray" => Color::DarkGray,
"darkgray" => Color::DarkGray, "gray" => Color::Gray,
"gray" => Color::Gray, "green" => Color::Green,
"green" => Color::Green, "lightgreen" => Color::LightGreen,
"lightgreen" => Color::LightGreen, "lightred" => Color::LightRed,
"lightred" => Color::LightRed, "magenta" => Color::Magenta,
"magenta" => Color::Magenta, "red" => Color::Red,
"red" => Color::Red, "white" => Color::White,
"white" => Color::White, "yellow" => Color::Yellow,
"yellow" => Color::Yellow, _ => Color::Reset, // Default to reset, if color name is not recognized
_ => Color::Reset, // Default to reset, if color name is not recognized
},
None => Color::Reset,
} }
} }
} }
impl Default for ColorsConfig { impl Default for ColorsConfigFile {
fn default() -> Self { fn default() -> Self {
Self { Self {
highlight_background: Some("magenta".to_string()), highlight_background: Some("magenta".to_string()),

View File

@ -1,41 +1,61 @@
use merge::Merge; use derive_macro::FromFile;
use merge::{Merge, option::overwrite_none};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Merge)] #[derive(Debug, Clone, Deserialize, Serialize, Merge)]
pub struct KeybindsConfig { pub struct KeybindsConfigFile {
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub quit: Option<String>, pub quit: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub next_tab: Option<String>, pub next_tab: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub prev_tab: Option<String>, pub prev_tab: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub next_torrent: Option<String>, pub next_torrent: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub prev_torrent: Option<String>, pub prev_torrent: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub switch_tab_1: Option<String>, pub switch_tab_1: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub switch_tab_2: Option<String>, pub switch_tab_2: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub switch_tab_3: Option<String>, pub switch_tab_3: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub toggle_torrent: Option<String>, pub toggle_torrent: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub toggle_all: Option<String>, pub toggle_all: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub delete: Option<String>, pub delete: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub delete_force: Option<String>, pub delete_force: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub select: Option<String>, pub select: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub toggle_help: Option<String>, pub toggle_help: Option<String>,
#[merge(strategy = merge::option::overwrite_none)] #[merge(strategy = overwrite_none)]
pub move_torrent: Option<String>, pub move_torrent: Option<String>,
} }
impl Default for KeybindsConfig { #[derive(Debug, Clone, FromFile)]
pub struct KeybindsConfig {
pub quit: String,
pub next_tab: String,
pub prev_tab: String,
pub next_torrent: String,
pub prev_torrent: String,
pub switch_tab_1: String,
pub switch_tab_2: String,
pub switch_tab_3: String,
pub toggle_torrent: String,
pub toggle_all: String,
pub delete: String,
pub delete_force: String,
pub select: String,
pub toggle_help: String,
pub move_torrent: String,
}
impl Default for KeybindsConfigFile {
fn default() -> Self { fn default() -> Self {
Self { Self {
quit: Some("q".to_string()), quit: Some("q".to_string()),

30
src/config/log.rs Normal file
View File

@ -0,0 +1,30 @@
use derive_macro::FromFile;
use merge::{option::overwrite_none, Merge};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize, Merge)]
pub struct LogConfigFile {
#[merge(strategy = overwrite_none)]
pub traxor: Option<String>,
#[merge(strategy = overwrite_none)]
pub ratatui: Option<String>,
#[merge(strategy = overwrite_none)]
pub transmission_rpc: Option<String>,
}
#[derive(Debug, Clone, FromFile)]
pub struct LogConfig {
pub traxor: String,
pub ratatui: String,
pub transmission_rpc: String,
}
impl Default for LogConfigFile {
fn default() -> Self {
Self {
traxor: Some("warn".to_string()),
ratatui: Some("warn".to_string()),
transmission_rpc: Some("warn".to_string()),
}
}
}

View File

@ -1,28 +1,41 @@
mod colors; mod colors;
mod keybinds; mod keybinds;
mod log;
use color_eyre::Result; use color_eyre::Result;
use colors::ColorsConfig; use colors::{ColorsConfig, ColorsConfigFile};
use keybinds::KeybindsConfig; use keybinds::{KeybindsConfig, KeybindsConfigFile};
use merge::Merge; use log::{LogConfig, LogConfigFile};
use merge::{Merge, option::overwrite_none};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Debug, Default, Deserialize, Serialize, Merge)] #[derive(Debug, Clone, Default, Deserialize, Serialize, Merge)]
pub struct ConfigFile {
#[merge(strategy = overwrite_none)]
pub keybinds: Option<KeybindsConfigFile>,
#[merge(strategy = overwrite_none)]
pub colors: Option<ColorsConfigFile>,
#[merge(strategy = overwrite_none)]
pub log: Option<LogConfigFile>,
}
#[derive(Debug, Clone)]
pub struct Config { pub struct Config {
pub keybinds: KeybindsConfig, pub keybinds: KeybindsConfig,
pub colors: ColorsConfig, pub colors: ColorsConfig,
pub log: LogConfig,
} }
impl Config { impl Config {
pub fn load() -> Result<Self> { pub fn load() -> Result<Self> {
let mut config = Self::default(); let mut config = ConfigFile::default();
// Load system-wide config // Load system-wide config
let system_config_path = PathBuf::from("/etc/xdg/traxor/config.toml"); let system_config_path = PathBuf::from("/etc/xdg/traxor/config.toml");
if system_config_path.exists() { if system_config_path.exists() {
let config_str = std::fs::read_to_string(&system_config_path)?; let config_str = std::fs::read_to_string(&system_config_path)?;
let system_config: Config = toml::from_str(&config_str)?; let system_config = toml::from_str::<ConfigFile>(&config_str)?;
config.merge(system_config); config.merge(system_config);
} }
@ -30,18 +43,11 @@ impl Config {
let user_config_path = Self::get_config_path()?; let user_config_path = Self::get_config_path()?;
if user_config_path.exists() { if user_config_path.exists() {
let config_str = std::fs::read_to_string(&user_config_path)?; let config_str = std::fs::read_to_string(&user_config_path)?;
let user_config: Config = toml::from_str(&config_str)?; let user_config = toml::from_str::<ConfigFile>(&config_str)?;
config.merge(user_config); config.merge(user_config);
} }
Ok(config) Ok(config.into())
}
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> { fn get_config_path() -> Result<PathBuf> {
@ -55,3 +61,13 @@ impl Config {
Ok(config_dir.join("traxor").join("config.toml")) Ok(config_dir.join("traxor").join("config.toml"))
} }
} }
impl From<ConfigFile> for Config {
fn from(value: ConfigFile) -> Self {
Self {
keybinds: value.keybinds.into(),
colors: value.colors.into(),
log: value.log.into(),
}
}
}

View File

@ -1,7 +1,7 @@
use crate::app::{action::Action, App}; use crate::app::{App, action::Action};
use color_eyre::Result; use color_eyre::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tracing::{event, info_span, Level}; use tracing::{Level, event, info_span};
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 { match key_event.code {
@ -96,12 +96,8 @@ pub async fn update(app: &mut App<'_>, action: Action) -> Result<()> {
} }
/// Check if a KeyEvent matches a configured keybind string /// Check if a KeyEvent matches a configured keybind string
fn matches_keybind(event: &KeyEvent, config_key: &Option<String>) -> bool { fn matches_keybind(event: &KeyEvent, config_key: &str) -> bool {
let Some(key_str) = config_key else { let (modifiers, key_code) = parse_keybind(config_key);
return false;
};
let (modifiers, key_code) = parse_keybind(key_str);
let Some(key_code) = key_code else { let Some(key_code) = key_code else {
return false; return false;
}; };

View File

@ -2,6 +2,7 @@ pub mod app;
pub mod config; pub mod config;
pub mod event; pub mod event;
pub mod handler; pub mod handler;
pub mod log; pub mod telemetry;
pub mod tui; pub mod tui;
pub mod ui; pub mod ui;

View File

@ -1,18 +0,0 @@
use color_eyre::Result;
use tracing_appender::rolling;
use tracing_subscriber::{self, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
pub fn setup_logger() -> Result<()> {
std::fs::create_dir_all(".logs")?;
let logfile = rolling::daily(".log", "traxor.log");
let log_layer = tracing_subscriber::fmt::layer()
.with_writer(logfile)
.with_ansi(false);
tracing_subscriber::registry()
.with(log_layer)
.with(EnvFilter::from_default_env())
.init();
Ok(())
}

View File

@ -1,14 +1,12 @@
mod log;
use color_eyre::Result; use color_eyre::Result;
use log::setup_logger; use ratatui::{Terminal, backend::CrosstermBackend};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io; use std::io;
use traxor::{ use traxor::{
app::App, app::App,
config::Config, config::Config,
event::{Event, EventHandler}, event::{Event, EventHandler},
handler::{get_action, update}, handler::{get_action, update},
telemetry::setup_logger,
tui::Tui, tui::Tui,
}; };
@ -16,12 +14,12 @@ use traxor::{
async fn main() -> Result<()> { async fn main() -> Result<()> {
color_eyre::install()?; color_eyre::install()?;
// Setup the logger.
setup_logger()?;
// Load configuration. // Load configuration.
let config = Config::load()?; let config = Config::load()?;
// Setup the logger.
setup_logger(&config)?;
// Create an application. // Create an application.
let mut app = App::new(config)?; let mut app = App::new(config)?;

37
src/telemetry.rs Normal file
View File

@ -0,0 +1,37 @@
use crate::config::Config;
use color_eyre::{Result, eyre::eyre};
use std::{fs::create_dir_all, path::PathBuf};
use tracing_appender::rolling;
use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
pub fn setup_logger(config: &Config) -> Result<()> {
let log_dir_path = if cfg!(debug_assertions) {
PathBuf::from(".logs")
} else {
let mut path =
dirs::data_local_dir().ok_or_else(|| eyre!("Failed to get local data directory"))?;
path.push("traxor/logs");
path
};
create_dir_all(&log_dir_path)?;
let logfile = if cfg!(debug_assertions) {
rolling::daily(log_dir_path, "traxor.log")
} else {
rolling::never(log_dir_path, "traxor.log")
};
let formatter = BunyanFormattingLayer::new("traxor".into(), logfile);
tracing_subscriber::registry()
.with(JsonStorageLayer)
.with(formatter)
.with(EnvFilter::new(format!(
"traxor={},ratatui={},transmission_rpc={}",
config.log.traxor, config.log.ratatui, config.log.transmission_rpc,
)))
.init();
Ok(())
}

View File

@ -8,63 +8,48 @@ pub fn render_help(frame: &mut Frame, app: &App) {
.borders(Borders::ALL) .borders(Borders::ALL)
.border_type(BorderType::Rounded); .border_type(BorderType::Rounded);
let keybinds = &app.config.keybinds; let keybinds = app.config.keybinds.clone();
let rows = vec![ let rows = vec![
Row::new(vec![ Row::new(vec![
Cell::from(keybinds.toggle_help.as_deref().unwrap_or("?")), Cell::from(keybinds.toggle_help),
Cell::from("Show help"), Cell::from("Show help"),
]), ]),
Row::new(vec![Cell::from(keybinds.quit), Cell::from("Quit")]),
Row::new(vec![Cell::from(keybinds.prev_tab), Cell::from("Left")]),
Row::new(vec![Cell::from(keybinds.next_tab), Cell::from("Right")]),
Row::new(vec![Cell::from(keybinds.next_torrent), Cell::from("Down")]),
Row::new(vec![Cell::from(keybinds.prev_torrent), Cell::from("Up")]),
Row::new(vec![ Row::new(vec![
Cell::from(keybinds.quit.as_deref().unwrap_or("q")), Cell::from(keybinds.switch_tab_1),
Cell::from("Quit"),
]),
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"), Cell::from("Switch to All tab"),
]), ]),
Row::new(vec![ Row::new(vec![
Cell::from(keybinds.switch_tab_2.as_deref().unwrap_or("2")), Cell::from(keybinds.switch_tab_2),
Cell::from("Switch to Active tab"), 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),
Cell::from("Switch to Downloading tab"), Cell::from("Switch to Downloading tab"),
]), ]),
Row::new(vec![ Row::new(vec![
Cell::from(keybinds.toggle_torrent.as_deref().unwrap_or("t")), Cell::from(keybinds.toggle_torrent),
Cell::from("Toggle torrent"), Cell::from("Toggle torrent"),
]), ]),
Row::new(vec![ Row::new(vec![
Cell::from(keybinds.toggle_all.as_deref().unwrap_or("a")), Cell::from(keybinds.toggle_all),
Cell::from("Toggle all torrents"), Cell::from("Toggle all torrents"),
]), ]),
Row::new(vec![ Row::new(vec![
Cell::from(keybinds.delete.as_deref().unwrap_or("d")), Cell::from(keybinds.delete),
Cell::from("Delete torrent"), Cell::from("Delete torrent"),
]), ]),
Row::new(vec![ Row::new(vec![
Cell::from(keybinds.delete_force.as_deref().unwrap_or("D")), Cell::from(keybinds.delete_force),
Cell::from("Delete torrent and data"), Cell::from("Delete torrent and data"),
]), ]),
Row::new(vec![ Row::new(vec![
Cell::from(keybinds.select.as_deref().unwrap_or(" ")), Cell::from(keybinds.select),
Cell::from("Select torrent"), Cell::from("Select torrent"),
]), ]),
]; ];

View File

@ -1,4 +1,4 @@
use crate::app::{utils::Wrapper, App, Tab}; use crate::app::{App, Tab, utils::Wrapper};
use ratatui::{ use ratatui::{
layout::Constraint, layout::Constraint,
style::{Style, Styled}, style::{Style, Styled},