diff --git a/Cargo.lock b/Cargo.lock index fbbda1e..2a23c6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,6 +99,12 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.10.1" @@ -447,7 +453,8 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn", + "thiserror 2.0.12", + "unsynn", ] [[package]] @@ -504,6 +511,15 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -1002,6 +1018,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "mutants" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0287524726960e07b119cebd01678f852f147742ae0d925e6a520dca956126" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1553,6 +1575,12 @@ dependencies = [ "serde", ] +[[package]] +name = "shadow_counted" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65da48d447333cebe1aadbdd3662f3ba56e76e67f53bc46f3dd5f67c74629d6b" + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2122,6 +2150,18 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsynn" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7940603a9e25cf11211cc43b81f4fcad2b8ab4df291ca855f32c40e1ac22d5bc" +dependencies = [ + "fxhash", + "mutants", + "proc-macro2", + "shadow_counted", +] + [[package]] name = "untrusted" version = "0.9.0" diff --git a/src/config/mod.rs b/src/config/mod.rs index 7d63787..d025f35 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -17,6 +17,8 @@ use std::{ }; use tracing::{debug, info, warn}; +const DEFAULT_CONFIG_STR: &str = include_str!("../../config/default.toml"); + #[derive(Debug, Clone, FromFile)] pub struct Config { pub keybinds: KeybindsConfig, @@ -25,12 +27,23 @@ pub struct Config { } impl Config { + /// Load configuration with fallback to embedded defaults. + /// + /// Merge order: + /// 1. Embedded defaults + /// 2. System-wide config (`/etc/xdg/traxor/config.toml`) + /// 3. User config (`~/.config/traxor/config.toml`) + /// /// # Errors /// - /// TODO: add error types + /// Returns an error if: + /// - The embedded default config cannot be parsed (should never happen unless corrupted at build time). + /// - The system-wide or user config file cannot be read due to I/O errors. + /// - The TOML in any config file is invalid and cannot be parsed. #[tracing::instrument(name = "Loading configuration")] pub fn load() -> Result { - let mut cfg_file = ConfigFile::default(); + let mut cfg_file = toml::from_str::(DEFAULT_CONFIG_STR) + .context("Failed to parse embedded default config")?; let candidates = [ ("system-wide", PathBuf::from("/etc/xdg/traxor/config.toml")), diff --git a/src/main.rs b/src/main.rs index 72bf5ae..a8eb997 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,11 @@ use color_eyre::Result; use ratatui::{Terminal, backend::CrosstermBackend}; -use std::io; -use tracing::{debug, trace}; +use std::{io, sync::Arc}; +use tokio::{ + sync::Mutex, + time::{self, Duration}, +}; +use tracing::{trace, warn}; use traxor::{ app::App, config::Config, @@ -15,38 +19,54 @@ use traxor::{ async fn main() -> Result<()> { color_eyre::install()?; - debug!("Loading configuration..."); let config = Config::load()?; - debug!("Configuration loaded."); - - // Setup the logger. setup_logger(&config)?; - // Create an application. - let mut app = App::new(config)?; + // Wrap App in Arc> so we can share it between UI and updater + let app = Arc::new(Mutex::new(App::new(config)?)); - // Initialize the terminal user interface. + // Clone for updater task + let app_clone = app.clone(); + + tokio::spawn(async move { + let mut interval = time::interval(Duration::from_secs(2)); + loop { + interval.tick().await; + + let mut app = app_clone.lock().await; + if let Err(e) = app.torrents.update().await { + warn!("Failed to update torrents: {e}"); + } + } + }); + + // TUI setup let backend = CrosstermBackend::new(io::stderr()); let terminal = Terminal::new(backend)?; let events = EventHandler::new(250); // Update time in ms let mut tui = Tui::new(terminal, events); tui.init()?; - // Start the main loop. - while app.running { - // Render the user interface. - tui.draw(&mut app)?; - // Handle events. - match tui.events.next()? { - Event::Tick => { - trace!(target: "app", "Event::Tick"); - app.tick().await?; + // Main loop + loop { + { + let app_guard = app.lock().await; + if !app_guard.running { + break; } + } + + { + let mut app_guard = app.lock().await; + tui.draw(&mut app_guard)?; + } + + match tui.events.next()? { + Event::Tick => {} Event::Key(key_event) => { - trace!(target: "app", "Event::Key: {:?}", key_event); - if let Some(action) = get_action(key_event, &mut app).await? { - trace!(target: "app", "Action: {:?}", action); - update(&mut app, action).await?; + let mut app_guard = app.lock().await; + if let Some(action) = get_action(key_event, &mut app_guard).await? { + update(&mut app_guard, action).await?; } } Event::Mouse(mouse_event) => { @@ -58,7 +78,6 @@ async fn main() -> Result<()> { } } - // Exit the user interface. tui.exit()?; Ok(()) } diff --git a/src/ui/table.rs b/src/ui/table.rs index 5381400..9fa1826 100644 --- a/src/ui/table.rs +++ b/src/ui/table.rs @@ -67,10 +67,10 @@ fn make_row<'a>( highlight: Style, ) -> Row<'a> { let cells = fields.iter().map(|&field| { - if let Some(id) = torrent.id { - if selected.contains(&id) { - return field.value(torrent).set_style(highlight); - } + if let Some(id) = torrent.id + && selected.contains(&id) + { + return field.value(torrent).set_style(highlight); } field.value(torrent).into() });