Skip to content

Commit e8f3444

Browse files
committed
feat: add config command
1 parent 28ea0c8 commit e8f3444

File tree

4 files changed

+124
-66
lines changed

4 files changed

+124
-66
lines changed

crates/ritobin-tools/src/commands/config.rs renamed to crates/ritobin-tools/src/commands/config_cmd.rs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ fn print_path_config(
4343
}
4444

4545
pub fn show_config() -> Result<()> {
46-
let cfg = config::load_config();
46+
let (cfg, _) = config::load_or_create_config()?;
4747
let config_path = config::default_config_path();
4848

4949
println!();
@@ -78,14 +78,50 @@ pub fn reset_config() -> Result<()> {
7878
println!();
7979
println!(" {} {}", "Config file:".bright_white().bold(), config_path);
8080
println!();
81+
82+
Ok(())
83+
}
84+
85+
pub fn set_config_value(key: &str, value: &str) -> Result<()> {
86+
let mut table = config::load_config_as_table()?;
87+
let toml_value = parse_toml_value(value);
88+
89+
table.insert(key.to_string(), toml_value);
90+
91+
let _: AppConfig = table
92+
.clone()
93+
.try_into()
94+
.map_err(|e| miette::miette!("Invalid configuration: {}", e))?;
95+
96+
config::save_config_table(&table)
97+
.map_err(|e| miette::miette!("Failed to save config: {}", e))?;
98+
8199
println!(
82-
" {}",
83-
"Run 'league-mod config auto-detect' to find your League installation".bright_cyan()
100+
"{}",
101+
format!("✓ Set '{}' = '{}'", key, value)
102+
.bright_green()
103+
.bold()
84104
);
85105

86106
Ok(())
87107
}
88108

109+
/// Parse a string value into an appropriate TOML value type
110+
fn parse_toml_value(value: &str) -> toml::Value {
111+
if let Ok(b) = value.parse::<bool>() {
112+
return toml::Value::Boolean(b);
113+
}
114+
if let Ok(i) = value.parse::<i64>() {
115+
return toml::Value::Integer(i);
116+
}
117+
if let Ok(f) = value.parse::<f64>()
118+
&& value.contains('.')
119+
{
120+
return toml::Value::Float(f);
121+
}
122+
toml::Value::String(value.to_string())
123+
}
124+
89125
/// Ensures config.toml exists.
90126
pub fn ensure_config_exists() -> Result<()> {
91127
let (_cfg, _path) = config::load_or_create_config()
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
mod config;
1+
pub mod config_cmd;
22
pub mod convert;
33
pub mod diff;
44

5-
pub use config::*;
5+
pub use config_cmd::ensure_config_exists;

crates/ritobin-tools/src/main.rs

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,34 @@
1-
use camino::Utf8Path;
21
use clap::builder::{Styles, styling::AnsiColor};
32
use clap::{ColorChoice, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
4-
use league_toolkit::file::LeagueFileKind;
53
use miette::Result;
6-
use serde::Deserialize;
7-
use serde::de::IntoDeserializer;
8-
use serde::de::value::Error;
94
use tracing::Level;
105
use tracing_indicatif::IndicatifLayer;
116
use tracing_subscriber::filter::LevelFilter;
127
use tracing_subscriber::layer::SubscriberExt;
138
use tracing_subscriber::prelude::*;
149
use tracing_subscriber::util::SubscriberInitExt;
1510
use tracing_subscriber::{filter, fmt};
16-
use utils::config::{default_config_path, load_or_create_config};
1711

18-
use crate::commands::{convert, diff};
12+
use crate::commands::{config_cmd, convert, diff};
1913

2014
mod commands;
2115
mod utils;
2216

17+
#[derive(Subcommand, Debug)]
18+
pub enum ConfigAction {
19+
/// Show current configuration
20+
Show,
21+
/// Set a configuration value
22+
Set {
23+
/// Configuration key to set (e.g., 'hashtable_dir')
24+
key: String,
25+
/// Value to set for the configuration key
26+
value: String,
27+
},
28+
/// Reset configuration to defaults
29+
Reset,
30+
}
31+
2332
#[derive(Copy, Clone, Debug, ValueEnum)]
2433
pub enum VerbosityLevel {
2534
/// Show errors and above
@@ -105,6 +114,12 @@ pub enum Commands {
105114
/// Disable colored output
106115
no_color: bool,
107116
},
117+
118+
/// Manage application configuration
119+
Config {
120+
#[command(subcommand)]
121+
action: ConfigAction,
122+
},
108123
}
109124

110125
fn parse_args() -> Args {
@@ -142,6 +157,11 @@ fn main() -> Result<()> {
142157
context,
143158
no_color,
144159
} => diff::diff(file1, file2, context, no_color),
160+
Commands::Config { action } => match action {
161+
ConfigAction::Show => config_cmd::show_config(),
162+
ConfigAction::Set { key, value } => config_cmd::set_config_value(&key, &value),
163+
ConfigAction::Reset => config_cmd::reset_config(),
164+
},
145165
}
146166
}
147167

@@ -210,19 +230,6 @@ fn initialize_tracing(verbosity: VerbosityLevel, show_progress: bool) -> Result<
210230
Ok(())
211231
}
212232

213-
fn parse_filter_type(s: &str) -> Result<LeagueFileKind, String> {
214-
let deserializer: serde::de::value::StrDeserializer<Error> = s.into_deserializer();
215-
if let Ok(kind) = LeagueFileKind::deserialize(deserializer) {
216-
return Ok(kind);
217-
}
218-
219-
// Fallback to extension
220-
match LeagueFileKind::from_extension(s) {
221-
LeagueFileKind::Unknown => Err(format!("Unknown file kind: {}", s)),
222-
other => Ok(other),
223-
}
224-
}
225-
226233
fn cli_styles() -> Styles {
227234
Styles::styled()
228235
.header(AnsiColor::Yellow.on_default().bold())

crates/ritobin-tools/src/utils/config.rs

Lines changed: 56 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use std::env;
99
use std::fs;
1010
use std::io;
1111
use std::path::Path;
12-
use std::time::{SystemTime, UNIX_EPOCH};
1312

1413
/// Application-wide configuration stored in config.toml.
1514
#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -43,21 +42,6 @@ pub fn default_config_path() -> Option<Utf8PathBuf> {
4342
config_path("config.toml")
4443
}
4544

46-
/// Loads the application configuration from config.toml.
47-
/// Returns default configuration if file doesn't exist or cannot be parsed.
48-
pub fn load_config() -> AppConfig {
49-
if let Some(path) = default_config_path() {
50-
if Path::new(path.as_str()).exists() {
51-
if let Ok(content) = fs::read_to_string(path.as_str()) {
52-
if let Ok(cfg) = toml::from_str(&content) {
53-
return cfg;
54-
}
55-
}
56-
}
57-
}
58-
AppConfig::default()
59-
}
60-
6145
/// Normalizes a path to use forward slashes
6246
fn normalize_path(path: &Utf8PathBuf) -> Utf8PathBuf {
6347
Utf8PathBuf::from(path.as_str().replace('\\', "/"))
@@ -82,16 +66,24 @@ pub fn save_config(cfg: &AppConfig) -> io::Result<()> {
8266
}
8367

8468
/// Loads existing configuration or creates a new one with defaults.
69+
/// Missing fields in the config file are filled with default values.
8570
pub fn load_or_create_config() -> Result<(AppConfig, Utf8PathBuf)> {
8671
let path = default_config_path().ok_or(miette::miette!("Could not determine config path"))?;
8772

8873
if Path::new(path.as_str()).exists() {
8974
let content = fs::read_to_string(path.as_str())
9075
.into_diagnostic()
9176
.wrap_err("Failed to read config file")?;
92-
let cfg = toml::from_str(&content)
77+
let mut cfg: AppConfig = toml::from_str(&content)
9378
.into_diagnostic()
9479
.wrap_err("Failed to parse config file")?;
80+
81+
// Fill in defaults for missing optional fields
82+
let defaults = AppConfig::default();
83+
if cfg.hashtable_dir.is_none() {
84+
cfg.hashtable_dir = defaults.hashtable_dir;
85+
}
86+
9587
Ok((cfg, path))
9688
} else {
9789
let cfg = AppConfig::default();
@@ -102,38 +94,61 @@ pub fn load_or_create_config() -> Result<(AppConfig, Utf8PathBuf)> {
10294
}
10395
}
10496

105-
/// Reads JSON from a path into type T. Returns Ok(None) if file cannot be read or parsed.
106-
pub fn read_json<T: serde::de::DeserializeOwned>(path: &Path) -> io::Result<Option<T>> {
107-
match fs::read(path) {
108-
Ok(bytes) => match serde_json::from_slice::<T>(&bytes) {
109-
Ok(v) => Ok(Some(v)),
110-
Err(_) => Ok(None),
111-
},
112-
Err(_) => Ok(None),
113-
}
114-
}
97+
/// Loads configuration as a raw TOML table for flexible editing.
98+
pub fn load_config_as_table() -> Result<toml::Table> {
99+
let path = default_config_path().ok_or(miette::miette!("Could not determine config path"))?;
100+
101+
if Path::new(path.as_str()).exists() {
102+
let content = fs::read_to_string(path.as_str())
103+
.into_diagnostic()
104+
.wrap_err("Failed to read config file")?;
115105

116-
/// Writes pretty-formatted JSON to the given path.
117-
pub fn write_json_pretty<T: serde::Serialize>(path: &Path, value: &T) -> io::Result<()> {
118-
let data = serde_json::to_vec_pretty(value).unwrap_or_else(|_| b"{}".to_vec());
119-
fs::write(path, data)
106+
toml::from_str(&content)
107+
.into_diagnostic()
108+
.wrap_err("Failed to parse config file")
109+
} else {
110+
let cfg = AppConfig::default();
111+
let content = toml::to_string_pretty(&cfg)
112+
.into_diagnostic()
113+
.wrap_err("Failed to serialize default config")?;
114+
toml::from_str(&content)
115+
.into_diagnostic()
116+
.wrap_err("Failed to parse default config")
117+
}
120118
}
121119

122-
/// Returns current UNIX epoch seconds.
123-
pub fn now_epoch_secs() -> u64 {
124-
SystemTime::now()
125-
.duration_since(UNIX_EPOCH)
126-
.unwrap_or_default()
127-
.as_secs()
120+
/// Saves a raw TOML table to the config file.
121+
pub fn save_config_table(table: &toml::Table) -> io::Result<()> {
122+
if let Some(path) = default_config_path() {
123+
let content = toml::to_string_pretty(table).map_err(io::Error::other)?;
124+
fs::write(path.as_str(), content)
125+
} else {
126+
Err(io::Error::new(
127+
io::ErrorKind::NotFound,
128+
"Could not determine config path",
129+
))
130+
}
128131
}
129132

130133
/// Returns the default directory where wad hashtables should be looked up.
131134
/// Uses the user's Documents folder: Documents/LeagueToolkit/bin_hashtables
135+
/// Falls back to ~/.local/share/LeagueToolkit/bin_hashtables on Linux if Documents isn't available
132136
pub fn default_hashtable_dir() -> Option<Utf8PathBuf> {
133-
let user_dirs = directories_next::UserDirs::new()?;
134-
let doc_dir = user_dirs.document_dir()?;
135-
let mut path = doc_dir.to_path_buf();
136-
path.push("LeagueToolkit");
137+
// Try Documents folder first (Windows, macOS, and some Linux setups)
138+
if let Some(doc_dir) =
139+
directories_next::UserDirs::new().and_then(|u| u.document_dir().map(|p| p.to_path_buf()))
140+
{
141+
let mut path = doc_dir;
142+
path.push("LeagueToolkit");
143+
path.push("bin_hashtables");
144+
if let Ok(utf8_path) = Utf8PathBuf::from_path_buf(path) {
145+
return Some(utf8_path);
146+
}
147+
}
148+
149+
// Fallback: use data directory (~/.local/share on Linux, AppData on Windows)
150+
let data_dirs = directories_next::ProjectDirs::from("", "", "LeagueToolkit")?;
151+
let mut path = data_dirs.data_dir().to_path_buf();
137152
path.push("bin_hashtables");
138153
Utf8PathBuf::from_path_buf(path).ok()
139154
}

0 commit comments

Comments
 (0)