diff --git a/Cargo.toml b/Cargo.toml index 4bcf33c..4e85cf4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,3 +24,8 @@ tracing-log = "0.2.0" tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] } transmission-rpc = "0.5" url = "2.5" + +[lints.clippy] +pedantic = "warn" +nursery = "warn" +unwrap_used = "warn" diff --git a/config/default.toml b/config/default.toml index 082c229..996bb5a 100644 --- a/config/default.toml +++ b/config/default.toml @@ -18,7 +18,7 @@ move = "m" [colors] highlight_background = "magenta" highlight_foreground = "black" -warning_foreground = "yellow" +header_foreground = "yellow" info_foreground = "blue" error_foreground = "red" diff --git a/src/app/action.rs b/src/app/action.rs index 4a9458c..c860747 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -1,6 +1,6 @@ use derive_more::Display; -#[derive(Debug, Clone, PartialEq, Display)] +#[derive(Debug, Clone, PartialEq, Eq, Display)] pub enum Action { #[display("Quit")] Quit, diff --git a/src/app/command.rs b/src/app/command.rs index 26097d0..1850041 100644 --- a/src/app/command.rs +++ b/src/app/command.rs @@ -1,15 +1,18 @@ -use super::{types::Selected, Torrents}; -use color_eyre::{eyre::eyre, Result}; +use super::{Torrents, types::Selected}; +use color_eyre::{Result, eyre::eyre}; use std::{collections::HashSet, path::Path}; use transmission_rpc::types::{Torrent, TorrentAction, TorrentStatus}; impl Torrents { + /// # Errors + /// + /// TODO: add error types pub async fn toggle(&mut self, ids: Selected) -> Result<()> { let ids: HashSet<_> = ids.into(); let torrents_to_toggle: Vec<_> = self .torrents .iter() - .filter(|torrent| torrent.id.map_or(false, |id| ids.contains(&id))) + .filter(|torrent| torrent.id.is_some_and(|id| ids.contains(&id))) .collect(); for torrent in torrents_to_toggle { @@ -27,6 +30,9 @@ impl Torrents { Ok(()) } + /// # Errors + /// + /// TODO: add error types pub async fn toggle_all(&mut self) -> Result<()> { let torrents_to_toggle: Vec<_> = self .torrents @@ -53,14 +59,23 @@ impl Torrents { Ok(()) } + /// # Errors + /// + /// TODO: add error types pub async fn start_all(&mut self) -> Result<()> { self.action_all(TorrentAction::StartNow).await } + /// # Errors + /// + /// TODO: add error types pub async fn stop_all(&mut self) -> Result<()> { self.action_all(TorrentAction::Stop).await } + /// # Errors + /// + /// TODO: add error types pub async fn move_dir( &mut self, torrent: &Torrent, @@ -76,6 +91,9 @@ impl Torrents { Ok(()) } + /// # Errors + /// + /// TODO: add error types pub async fn delete(&mut self, ids: Selected, delete_local_data: bool) -> Result<()> { self.client .torrent_remove(ids.into(), delete_local_data) @@ -84,6 +102,9 @@ impl Torrents { Ok(()) } + /// # Errors + /// + /// TODO: add error types pub async fn rename(&mut self, torrent: &Torrent, name: &Path) -> Result<()> { if let (Some(id), Some(old_name)) = (torrent.id(), torrent.name.clone()) { self.client @@ -94,11 +115,14 @@ impl Torrents { Ok(()) } + /// # Errors + /// + /// TODO: add error types async fn action_all(&mut self, action: TorrentAction) -> Result<()> { let ids = self .torrents .iter() - .filter_map(|torrent| torrent.id()) + .filter_map(Torrent::id) .collect::>(); self.client diff --git a/src/app/mod.rs b/src/app/mod.rs index cb55e59..261f1b0 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -30,9 +30,13 @@ pub struct App<'a> { pub completion_idx: usize, } -impl<'a> App<'a> { +impl App<'_> { /// Constructs a new instance of [`App`]. /// Returns instance of `Self`. + /// + /// # Errors + /// + /// TODO: add error types pub fn new(config: Config) -> Result { Ok(Self { running: true, @@ -50,6 +54,9 @@ impl<'a> App<'a> { }) } + /// # Errors + /// + /// TODO: add error types pub async fn complete_input(&mut self) -> Result<()> { let path = PathBuf::from(&self.input); let (base_path, partial_name) = split_path_components(path); @@ -62,13 +69,18 @@ impl<'a> App<'a> { } /// Handles the tick event of the terminal. + /// + /// # Errors + /// + /// TODO: add error types pub async fn tick(&mut self) -> Result<()> { self.torrents.update().await?; Ok(()) } /// Set running to false to quit the application. - pub fn quit(&mut self) { + #[inline] + pub const fn quit(&mut self) { self.running = false; } @@ -103,13 +115,15 @@ impl<'a> App<'a> { } /// Switches to the next tab. - pub fn next_tab(&mut self) { + #[inline] + pub const fn next_tab(&mut self) { self.close_help(); self.index = (self.index + 1) % self.tabs.len(); } /// Switches to the previous tab. - pub fn prev_tab(&mut self) { + #[inline] + pub const fn prev_tab(&mut self) { self.close_help(); if self.index > 0 { self.index -= 1; @@ -119,33 +133,44 @@ impl<'a> App<'a> { } /// Switches to the tab whose index is `idx`. - pub fn switch_tab(&mut self, idx: usize) { + #[inline] + pub const fn switch_tab(&mut self, idx: usize) { self.close_help(); - self.index = idx + self.index = idx; } /// Returns current active [`Tab`] number - pub fn index(&self) -> usize { + #[inline] + #[must_use] + pub const fn index(&self) -> usize { self.index } /// Returns [`Tab`] slice - pub fn tabs(&self) -> &[Tab] { + #[inline] + #[must_use] + pub const fn tabs(&self) -> &[Tab] { self.tabs } - pub fn toggle_help(&mut self) { + #[inline] + pub const fn toggle_help(&mut self) { self.show_help = !self.show_help; } - pub fn close_help(&mut self) { + #[inline] + pub const fn close_help(&mut self) { self.show_help = false; } - pub fn open_help(&mut self) { + #[inline] + pub const fn open_help(&mut self) { self.show_help = true; } + /// # Errors + /// + /// TODO: add error types pub async fn toggle_torrents(&mut self) -> Result<()> { let ids = self.selected(false); self.torrents.toggle(ids).await?; @@ -153,6 +178,9 @@ impl<'a> App<'a> { Ok(()) } + /// # Errors + /// + /// TODO: add error types pub async fn delete(&mut self, delete_local_data: bool) -> Result<()> { let ids = self.selected(false); self.torrents.delete(ids, delete_local_data).await?; @@ -160,6 +188,9 @@ impl<'a> App<'a> { Ok(()) } + /// # Errors + /// + /// TODO: add error types pub async fn move_torrent(&mut self) -> Result<()> { self.torrents.move_selection(&self.input).await?; self.input.clear(); @@ -170,8 +201,7 @@ impl<'a> App<'a> { pub fn prepare_move_action(&mut self) { if let Some(download_dir) = self.get_current_downlaod_dir() { - let path_buf = PathBuf::from(download_dir); - self.update_cursor(path_buf); + self.update_cursor(&download_dir); } self.input_mode = true; } @@ -237,11 +267,11 @@ impl<'a> App<'a> { .find(|&t| t.id == Some(current_id)) .and_then(|t| t.download_dir.as_ref()) .map(PathBuf::from), - _ => None, + Selected::List(_) => None, } } - fn update_cursor(&mut self, path: PathBuf) { + fn update_cursor(&mut self, path: &Path) { self.input = path.to_string_lossy().to_string(); self.cursor_position = self.input.len(); } diff --git a/src/app/tab.rs b/src/app/tab.rs index b16d1e6..526c627 100644 --- a/src/app/tab.rs +++ b/src/app/tab.rs @@ -1,3 +1,4 @@ +use std::fmt::Display; use transmission_rpc::types::TorrentGetField; /// Available tabs. @@ -11,9 +12,10 @@ pub enum Tab { impl Tab { /// Returns slice [`TorrentGetField`] apropriate variants. - pub fn fields(&self) -> &[TorrentGetField] { + #[must_use] + pub const fn fields(&self) -> &[TorrentGetField] { match self { - Tab::All => &[ + Self::All => &[ TorrentGetField::Status, TorrentGetField::PeersGettingFromUs, TorrentGetField::UploadRatio, @@ -22,7 +24,7 @@ impl Tab { TorrentGetField::DownloadDir, TorrentGetField::Name, ], - Tab::Active => &[ + Self::Active => &[ TorrentGetField::TotalSize, TorrentGetField::UploadedEver, TorrentGetField::UploadRatio, @@ -35,7 +37,7 @@ impl Tab { TorrentGetField::RateUpload, TorrentGetField::Name, ], - Tab::Downloading => &[ + Self::Downloading => &[ TorrentGetField::TotalSize, TorrentGetField::LeftUntilDone, TorrentGetField::PercentDone, @@ -48,18 +50,30 @@ impl Tab { } } -impl AsRef for Tab { - fn as_ref(&self) -> &str { - match self { - Tab::All => "All", - Tab::Active => "Active", - Tab::Downloading => "Downloading", +impl From for Tab { + fn from(value: usize) -> Self { + #[allow(clippy::match_same_arms)] + match value { + 0 => Self::All, + 1 => Self::Active, + 2 => Self::Downloading, + _ => Self::All, } } } -impl ToString for Tab { - fn to_string(&self) -> String { - self.as_ref().into() +impl AsRef for Tab { + fn as_ref(&self) -> &str { + match self { + Self::All => "All", + Self::Active => "Active", + Self::Downloading => "Downloading", + } + } +} + +impl Display for Tab { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_ref()) } } diff --git a/src/app/torrent.rs b/src/app/torrent.rs index 7862602..0cff121 100644 --- a/src/app/torrent.rs +++ b/src/app/torrent.rs @@ -1,8 +1,8 @@ -use color_eyre::{eyre::eyre, Result}; +use color_eyre::{Result, eyre::eyre}; use std::{collections::HashSet, fmt::Debug}; use transmission_rpc::{ - types::{Id, Torrent, TorrentGetField}, TransClient, + types::{Id, Torrent, TorrentGetField}, }; use url::Url; @@ -17,7 +17,11 @@ pub struct Torrents { impl Torrents { /// Constructs a new instance of [`Torrents`]. - pub fn new() -> Result { + /// + /// # Errors + /// + /// TODO: add error types + pub fn new() -> Result { let url = Url::parse("http://localhost:9091/transmission/rpc")?; Ok(Self { client: TransClient::new(url), @@ -28,10 +32,19 @@ impl Torrents { } /// Returns the number of [`Torrent`]s in [`Torrents`] - pub fn len(&self) -> usize { + #[inline] + #[must_use] + pub const fn len(&self) -> usize { self.torrents.len() } + /// Returns `true` if the `torrents` contains no elements. + #[inline] + #[must_use] + pub const fn is_empty(&self) -> bool { + self.torrents.is_empty() + } + /// Sets `self.fields` pub fn set_fields(&mut self, fields: Option>) -> &mut Self { self.fields = fields; @@ -39,12 +52,20 @@ impl Torrents { } /// Sets + /// + /// # Errors + /// + /// TODO: add error types pub fn url(&mut self, url: &str) -> Result<&mut Self> { self.client = TransClient::new(Url::parse(url)?); Ok(self) } /// Updates [`Torrent`] values. + /// + /// # Errors + /// + /// TODO: add error types pub async fn update(&mut self) -> Result<&mut Self> { self.torrents = self .client @@ -56,6 +77,9 @@ impl Torrents { Ok(self) } + /// # Errors + /// + /// TODO: add error types pub async fn move_selection(&mut self, location: &str) -> Result<()> { let ids: Vec = self.selected.iter().map(|id| Id::Id(*id)).collect(); self.client @@ -68,10 +92,10 @@ impl Torrents { impl Debug for Torrents { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let fields: Vec = match &self.fields { - Some(fields) => fields.iter().map(|field| field.to_str()).collect(), - None => vec![String::from("None")], - }; + let fields = self.fields.as_ref().map_or_else( + || vec!["None".into()], + |fields| fields.iter().map(TorrentGetField::to_str).collect(), + ); write!( f, "fields: diff --git a/src/app/types.rs b/src/app/types.rs index 082f07c..24c1c1f 100644 --- a/src/app/types.rs +++ b/src/app/types.rs @@ -1,4 +1,8 @@ -use std::collections::HashSet; +use std::{ + collections::{HashSet, hash_set::IntoIter}, + hash::BuildHasher, + iter::Once, +}; use transmission_rpc::types::Id; #[derive(Debug, Clone, PartialEq, Eq)] @@ -7,29 +11,51 @@ pub enum Selected { List(HashSet), } -impl Into> for Selected { - fn into(self) -> HashSet { +#[derive(Debug)] +pub enum SelectedIntoIter { + One(Once), + Many(IntoIter), +} + +impl Iterator for SelectedIntoIter { + type Item = i64; + + fn next(&mut self) -> Option { match self { - Selected::Current(id) => std::iter::once(id).collect(), - Selected::List(ids) => ids, + Self::One(it) => it.next(), + Self::Many(it) => it.next(), } } } -impl Into> for Selected { - fn into(self) -> Vec { +impl IntoIterator for Selected { + type Item = i64; + type IntoIter = SelectedIntoIter; + + fn into_iter(self) -> Self::IntoIter { match self { - Selected::Current(id) => vec![id], - Selected::List(ids) => ids.into_iter().collect(), + Self::Current(id) => SelectedIntoIter::One(std::iter::once(id)), + Self::List(set) => SelectedIntoIter::Many(set.into_iter()), } } } -impl Into> for Selected { - fn into(self) -> Vec { - match self { - Selected::Current(id) => vec![Id::Id(id)], - Selected::List(ids) => ids.into_iter().map(Id::Id).collect(), - } +impl From for HashSet +where + S: BuildHasher + Default, +{ + fn from(value: Selected) -> Self { + value.into_iter().collect() + } +} + +impl From for Vec { + fn from(value: Selected) -> Self { + value.into_iter().collect() + } +} +impl From for Vec { + fn from(value: Selected) -> Self { + value.into_iter().map(Id::Id).collect() } } diff --git a/src/app/utils/filesize.rs b/src/app/utils/filesize.rs index d2a558d..3e82174 100644 --- a/src/app/utils/filesize.rs +++ b/src/app/utils/filesize.rs @@ -7,7 +7,9 @@ pub struct FileSize(Unit); impl_unit_newtype!(FileSize); impl FileSize { - pub fn new(bytes: u64) -> Self { + #[inline] + #[must_use] + pub const fn new(bytes: u64) -> Self { Self(Unit::from_raw(bytes)) } } diff --git a/src/app/utils/mod.rs b/src/app/utils/mod.rs index b84ec36..ef367ea 100644 --- a/src/app/utils/mod.rs +++ b/src/app/utils/mod.rs @@ -68,7 +68,7 @@ impl Wrapper for TorrentGetField { Self::Peers => "Peers", Self::PeersConnected => "Connected", Self::PeersFrom => "Peers From", - Self::PeersGettingFromUs => "Peers", + Self::PeersGettingFromUs => "Peers Receiving", Self::PeersSendingToUs => "Seeds", Self::PercentComplete => "Percent Complete", Self::PercentDone => "%", @@ -203,6 +203,7 @@ impl Wrapper for TorrentGetField { } fn width(&self) -> u16 { + #![allow(clippy::match_same_arms)] match self { Self::ActivityDate => 20, Self::AddedDate => 20, @@ -292,8 +293,8 @@ fn format_option_string(value: Option) -> String { fn format_eta(value: Option) -> String { match value { Some(-2) => "?".into(), - None | Some(-1) | Some(..0) => String::new(), - Some(v) => format!("{} s", v), + None | Some(-1 | ..0) => String::new(), + Some(v) => format!("{v} s"), } } @@ -303,7 +304,7 @@ trait Formatter { impl Formatter for Option { fn format(&self) -> String { - self.map(|v| format!("{:.2}", v)).unwrap_or_default() + self.map(|v| format!("{v:.2}")).unwrap_or_default() } } diff --git a/src/app/utils/netspeed.rs b/src/app/utils/netspeed.rs index 699809f..ea25299 100644 --- a/src/app/utils/netspeed.rs +++ b/src/app/utils/netspeed.rs @@ -7,7 +7,9 @@ pub struct NetSpeed(Unit); impl_unit_newtype!(NetSpeed); impl NetSpeed { - pub fn new(bytes_per_second: u64) -> Self { + #[inline] + #[must_use] + pub const fn new(bytes_per_second: u64) -> Self { Self(Unit::from_raw(bytes_per_second)) } } diff --git a/src/app/utils/unit.rs b/src/app/utils/unit.rs index 6adebf6..b892942 100644 --- a/src/app/utils/unit.rs +++ b/src/app/utils/unit.rs @@ -50,10 +50,14 @@ where } impl Unit { + #[inline] + #[must_use] pub const fn from_raw(value: u64) -> Self { Self(value) } + #[inline] + #[must_use] pub const fn value(&self) -> u64 { self.0 } @@ -66,15 +70,18 @@ pub struct UnitDisplay<'a> { } impl<'a> UnitDisplay<'a> { - pub fn new(unit: &'a Unit, units: &'a [&'a str]) -> Self { + #[inline] + #[must_use] + pub const fn new(unit: &'a Unit, units: &'a [&'a str]) -> Self { Self { unit, units } } } -impl<'a> Display for UnitDisplay<'a> { +impl Display for UnitDisplay<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { const THRESHOLD: f64 = 1024.0; + #[allow(clippy::cast_precision_loss)] let value = self.unit.0 as f64; if value < THRESHOLD { @@ -111,7 +118,9 @@ macro_rules! impl_unit_newtype { } impl $wrapper { - pub fn unit(&self) -> &Unit { + #[inline] + #[must_use] + pub const fn unit(&self) -> &Unit { &self.0 } } diff --git a/src/config/color.rs b/src/config/color.rs new file mode 100644 index 0000000..86be3d1 --- /dev/null +++ b/src/config/color.rs @@ -0,0 +1,34 @@ +use derive_macro::FromFile; +use merge::{Merge, option::overwrite_none}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize, Merge)] +pub struct ColorConfigFile { + #[merge(strategy = overwrite_none)] + pub highlight_background: Option, + #[merge(strategy = overwrite_none)] + pub highlight_foreground: Option, + #[merge(strategy = overwrite_none)] + pub header_foreground: Option, + #[merge(strategy = overwrite_none)] + pub info_foreground: Option, +} + +#[derive(Debug, Clone, FromFile)] +pub struct ColorConfig { + pub highlight_background: String, + pub highlight_foreground: String, + pub header_foreground: String, + pub info_foreground: String, +} + +impl Default for ColorConfigFile { + fn default() -> Self { + Self { + highlight_background: Some("magenta".to_string()), + highlight_foreground: Some("black".to_string()), + header_foreground: Some("yellow".to_string()), + info_foreground: Some("blue".to_string()), + } + } +} diff --git a/src/config/colors.rs b/src/config/colors.rs deleted file mode 100644 index fb461b6..0000000 --- a/src/config/colors.rs +++ /dev/null @@ -1,59 +0,0 @@ -use derive_macro::FromFile; -use merge::{Merge, option::overwrite_none}; -use ratatui::prelude::*; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Deserialize, Serialize, Merge)] -pub struct ColorsConfigFile { - #[merge(strategy = overwrite_none)] - pub highlight_background: Option, - #[merge(strategy = overwrite_none)] - pub highlight_foreground: Option, - #[merge(strategy = overwrite_none)] - pub warning_foreground: Option, - #[merge(strategy = overwrite_none)] - pub info_foreground: Option, - #[merge(strategy = overwrite_none)] - pub error_foreground: Option, -} - -#[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: &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 ColorsConfigFile { - 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()), - } - } -} diff --git a/src/config/mod.rs b/src/config/mod.rs index 330f4ee..9c8ff8d 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,14 +1,20 @@ -mod colors; -mod keybinds; -mod log; +pub mod color; +pub mod keybinds; +pub mod log; -use color_eyre::Result; -use colors::{ColorsConfig, ColorsConfigFile}; +use color::{ColorConfig, ColorConfigFile}; +use color_eyre::{ + Result, + eyre::{Context, ContextCompat, Ok}, +}; use keybinds::{KeybindsConfig, KeybindsConfigFile}; use log::{LogConfig, LogConfigFile}; use merge::{Merge, option::overwrite_none}; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::{ + fs::read_to_string, + path::{Path, PathBuf}, +}; use tracing::{debug, info, warn}; #[derive(Debug, Clone, Default, Deserialize, Serialize, Merge)] @@ -16,7 +22,7 @@ pub struct ConfigFile { #[merge(strategy = overwrite_none)] pub keybinds: Option, #[merge(strategy = overwrite_none)] - pub colors: Option, + pub colors: Option, #[merge(strategy = overwrite_none)] pub log: Option, } @@ -24,53 +30,29 @@ pub struct ConfigFile { #[derive(Debug, Clone)] pub struct Config { pub keybinds: KeybindsConfig, - pub colors: ColorsConfig, + pub colors: ColorConfig, pub log: LogConfig, } impl Config { + /// # Errors + /// + /// TODO: add error types #[tracing::instrument(name = "Loading configuration")] pub fn load() -> Result { - let mut config = ConfigFile::default(); + let mut cfg_file = ConfigFile::default(); - // Load system-wide config - let system_config_path = PathBuf::from("/etc/xdg/traxor/config.toml"); - if system_config_path.exists() { - info!("Loading system-wide config from: {:?}", system_config_path); - let config_str = std::fs::read_to_string(&system_config_path)?; - let system_config = toml::from_str::(&config_str)?; - config.merge(system_config); - info!("Successfully loaded system-wide config."); - } else { - warn!("System-wide config not found at: {:?}", system_config_path); - } + let candidates = [ + ("system-wide", PathBuf::from("/etc/xdg/traxor/config.toml")), + ("user-specific", get_config_path()?), + ]; - // Load user-specific config - let user_config_path = Self::get_config_path()?; - if user_config_path.exists() { - info!("Loading user-specific config from: {:?}", user_config_path); - let config_str = std::fs::read_to_string(&user_config_path)?; - let user_config = toml::from_str::(&config_str)?; - config.merge(user_config); - info!("Successfully loaded user-specific config."); - } else { - warn!("User-specific config not found at: {:?}", user_config_path); + for (label, path) in &candidates { + merge_config(&mut cfg_file, label, path)?; } debug!("Configuration loaded successfully."); - Ok(config.into()) - } - - #[tracing::instrument(name = "Getting config path")] - fn get_config_path() -> Result { - 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")) + Ok(cfg_file.into()) } } @@ -83,3 +65,47 @@ impl From for Config { } } } + +#[tracing::instrument(name = "Getting config path")] +fn get_config_path() -> Result { + let config_dir = + dirs::config_dir().context("Could not determine user configuration directory")?; + Ok(config_dir.join("traxor").join("config.toml")) +} + +#[tracing::instrument(name = "Merging config", skip(cfg_file, path))] +fn merge_config(cfg_file: &mut ConfigFile, label: &str, path: &Path) -> Result<()> { + if !exists_and_log(label, path) { + return Ok(()); + } + + info!("Loading {} config from: {:?}", label, path); + let s = read_config_str(label, path)?; + let other = parse_config_toml(label, &s)?; + + cfg_file.merge(other); + info!("Successfully loaded {} config.", label); + Ok(()) +} + +fn exists_and_log(label: &str, path: &Path) -> bool { + if !path.exists() { + warn!("{} config not found at: {:?}", label, path); + return false; + } + true +} + +fn read_config_str(label: &str, path: &Path) -> Result { + read_to_string(path).with_context(|| { + format!( + "Failed to read {label} config file at {}", + path.to_string_lossy() + ) + }) +} + +fn parse_config_toml(label: &str, s: &str) -> Result { + toml::from_str::(s) + .with_context(|| format!("Failed to parse TOML in {label} config")) +} diff --git a/src/event.rs b/src/event.rs index f536760..dce5d9d 100644 --- a/src/event.rs +++ b/src/event.rs @@ -3,9 +3,10 @@ use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent}; use std::sync::mpsc; use std::thread; use std::time::{Duration, Instant}; +use tracing::error; /// Terminal events. -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Event { /// Terminal tick. Tick, @@ -31,6 +32,11 @@ pub struct EventHandler { impl EventHandler { /// Constructs a new instance of [`EventHandler`]. + /// + /// # Panics + /// + /// TODO: add panic + #[must_use] pub fn new(tick_rate: u64) -> Self { let tick_rate = Duration::from_millis(tick_rate); let (sender, receiver) = mpsc::channel(); @@ -49,12 +55,12 @@ impl EventHandler { Ok(CrosstermEvent::Mouse(e)) => sender.send(Event::Mouse(e)), Ok(CrosstermEvent::Resize(w, h)) => sender.send(Event::Resize(w, h)), Err(e) => { - eprintln!("Error reading event: {:?}", e); + error!("Error reading event: {:?}", e); break; } _ => Ok(()), // Ignore other events } - .expect("failed to send terminal event") + .expect("failed to send terminal event"); } if last_tick.elapsed() >= tick_rate { @@ -75,6 +81,10 @@ impl EventHandler { /// /// This function will always block the current thread if /// there is no data available and it's possible for more data to be sent. + /// + /// # Errors + /// + /// TODO: add error types pub fn next(&self) -> Result { Ok(self.receiver.recv()?) } diff --git a/src/handler.rs b/src/handler.rs index fefd9f5..603d7de 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -27,6 +27,10 @@ async fn handle_input(key_event: KeyEvent, app: &mut App<'_>) -> Result) -> Result> { if app.input_mode { @@ -64,6 +68,10 @@ pub async fn get_action(key_event: KeyEvent, app: &mut App<'_>) -> Result