refactor(config): simplify config reading

!squash me

!squash me
This commit is contained in:
2025-07-10 17:08:42 +03:00
parent b341b7a661
commit b988880c41
20 changed files with 328 additions and 229 deletions

View File

@@ -1,45 +1,52 @@
use merge::Merge;
use derive_macro::FromFile;
use merge::{Merge, option::overwrite_none};
use ratatui::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Merge)]
pub struct ColorsConfig {
#[merge(strategy = merge::option::overwrite_none)]
#[derive(Debug, Clone, Deserialize, Serialize, Merge)]
pub struct ColorsConfigFile {
#[merge(strategy = overwrite_none)]
pub highlight_background: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
pub highlight_foreground: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
pub warning_foreground: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
pub info_foreground: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
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 {
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 get_color(&self, color_name: &str) -> Color {
match color_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
}
}
}
impl Default for ColorsConfig {
impl Default for ColorsConfigFile {
fn default() -> Self {
Self {
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};
#[derive(Debug, Deserialize, Serialize, Merge)]
pub struct KeybindsConfig {
#[merge(strategy = merge::option::overwrite_none)]
#[derive(Debug, Clone, Deserialize, Serialize, Merge)]
pub struct KeybindsConfigFile {
#[merge(strategy = overwrite_none)]
pub quit: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
pub next_tab: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
pub prev_tab: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
pub next_torrent: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
pub prev_torrent: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
pub switch_tab_1: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
pub switch_tab_2: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
pub switch_tab_3: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
pub toggle_torrent: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
pub toggle_all: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
pub delete: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
pub delete_force: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
pub select: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
pub toggle_help: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[merge(strategy = overwrite_none)]
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 {
Self {
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 keybinds;
mod log;
use color_eyre::Result;
use colors::ColorsConfig;
use keybinds::KeybindsConfig;
use merge::Merge;
use colors::{ColorsConfig, ColorsConfigFile};
use keybinds::{KeybindsConfig, KeybindsConfigFile};
use log::{LogConfig, LogConfigFile};
use merge::{Merge, option::overwrite_none};
use serde::{Deserialize, Serialize};
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 keybinds: KeybindsConfig,
pub colors: ColorsConfig,
pub log: LogConfig,
}
impl Config {
pub fn load() -> Result<Self> {
let mut config = Self::default();
let mut config = ConfigFile::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)?;
let system_config = toml::from_str::<ConfigFile>(&config_str)?;
config.merge(system_config);
}
@@ -30,18 +43,11 @@ impl 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)?;
let user_config = toml::from_str::<ConfigFile>(&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(())
Ok(config.into())
}
fn get_config_path() -> Result<PathBuf> {
@@ -55,3 +61,13 @@ impl Config {
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 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>> {
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
fn matches_keybind(event: &KeyEvent, config_key: &Option<String>) -> bool {
let Some(key_str) = config_key else {
return false;
};
let (modifiers, key_code) = parse_keybind(key_str);
fn matches_keybind(event: &KeyEvent, config_key: &str) -> bool {
let (modifiers, key_code) = parse_keybind(config_key);
let Some(key_code) = key_code else {
return false;
};

View File

@@ -2,6 +2,7 @@ pub mod app;
pub mod config;
pub mod event;
pub mod handler;
pub mod log;
pub mod telemetry;
pub mod tui;
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 log::setup_logger;
use ratatui::{backend::CrosstermBackend, Terminal};
use ratatui::{Terminal, backend::CrosstermBackend};
use std::io;
use traxor::{
app::App,
config::Config,
event::{Event, EventHandler},
handler::{get_action, update},
telemetry::setup_logger,
tui::Tui,
};
@@ -16,12 +14,12 @@ use traxor::{
async fn main() -> Result<()> {
color_eyre::install()?;
// Setup the logger.
setup_logger()?;
// Load configuration.
let config = Config::load()?;
// Setup the logger.
setup_logger(&config)?;
// Create an application.
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)
.border_type(BorderType::Rounded);
let keybinds = &app.config.keybinds;
let keybinds = app.config.keybinds.clone();
let rows = vec![
Row::new(vec![
Cell::from(keybinds.toggle_help.as_deref().unwrap_or("?")),
Cell::from(keybinds.toggle_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![
Cell::from(keybinds.quit.as_deref().unwrap_or("q")),
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(keybinds.switch_tab_1),
Cell::from("Switch to All tab"),
]),
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"),
]),
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"),
]),
Row::new(vec![
Cell::from(keybinds.toggle_torrent.as_deref().unwrap_or("t")),
Cell::from(keybinds.toggle_torrent),
Cell::from("Toggle torrent"),
]),
Row::new(vec![
Cell::from(keybinds.toggle_all.as_deref().unwrap_or("a")),
Cell::from(keybinds.toggle_all),
Cell::from("Toggle all torrents"),
]),
Row::new(vec![
Cell::from(keybinds.delete.as_deref().unwrap_or("d")),
Cell::from(keybinds.delete),
Cell::from("Delete torrent"),
]),
Row::new(vec![
Cell::from(keybinds.delete_force.as_deref().unwrap_or("D")),
Cell::from(keybinds.delete_force),
Cell::from("Delete torrent and data"),
]),
Row::new(vec![
Cell::from(keybinds.select.as_deref().unwrap_or(" ")),
Cell::from(keybinds.select),
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::{
layout::Constraint,
style::{Style, Styled},