mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-08-07 01:33:52 +00:00
feat(config): implement config init/get/set/path commands
This commit is contained in:
parent
2b5a1b28de
commit
d2fa081266
4 changed files with 134 additions and 7 deletions
|
|
@ -1,5 +1,8 @@
|
||||||
use clap::{Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
|
|
||||||
|
use crate::config::{config_path, ensure_default_config, get_config_value, set_config_value};
|
||||||
|
use crate::error::IdxError;
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
pub struct ConfigCmd {
|
pub struct ConfigCmd {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
|
|
@ -13,3 +16,25 @@ pub enum ConfigSubcommand {
|
||||||
Set { key: String, value: String },
|
Set { key: String, value: String },
|
||||||
Path,
|
Path,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn handle(cmd: &ConfigCmd) -> Result<(), IdxError> {
|
||||||
|
match &cmd.command {
|
||||||
|
ConfigSubcommand::Init => {
|
||||||
|
let path = ensure_default_config()?;
|
||||||
|
println!("{}", path.display());
|
||||||
|
}
|
||||||
|
ConfigSubcommand::Get { key } => {
|
||||||
|
let value = get_config_value(key)?
|
||||||
|
.ok_or_else(|| IdxError::ConfigError(format!("key not found: {key}")))?;
|
||||||
|
println!("{value}");
|
||||||
|
}
|
||||||
|
ConfigSubcommand::Set { key, value } => {
|
||||||
|
set_config_value(key, value)?;
|
||||||
|
println!("ok");
|
||||||
|
}
|
||||||
|
ConfigSubcommand::Path => {
|
||||||
|
println!("{}", config_path()?.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,18 @@ pub enum Shell {
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
#[command(name = "idx", about = "Indonesian stock analysis CLI")]
|
#[command(name = "idx", about = "Indonesian stock analysis CLI")]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
#[arg(short, long, value_enum, global = true, default_value_t = OutputFormat::Table)]
|
#[arg(short, long, value_enum, global = true)]
|
||||||
pub output: OutputFormat,
|
pub output: Option<OutputFormat>,
|
||||||
#[arg(long, global = true)]
|
#[arg(long, global = true)]
|
||||||
pub no_color: bool,
|
pub no_color: bool,
|
||||||
#[arg(short, long, global = true)]
|
#[arg(short, long, global = true)]
|
||||||
pub quiet: bool,
|
pub quiet: bool,
|
||||||
#[arg(short, long, global = true, action = clap::ArgAction::Count)]
|
#[arg(short, long, global = true, action = clap::ArgAction::Count)]
|
||||||
pub verbose: u8,
|
pub verbose: u8,
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
pub offline: bool,
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
pub no_cache: bool,
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Commands,
|
pub command: Commands,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,20 @@ impl IdxConfig {
|
||||||
if let Ok(no_color) = std::env::var("IDX_NO_COLOR") {
|
if let Ok(no_color) = std::env::var("IDX_NO_COLOR") {
|
||||||
cfg.no_color = no_color == "1" || no_color.eq_ignore_ascii_case("true");
|
cfg.no_color = no_color == "1" || no_color.eq_ignore_ascii_case("true");
|
||||||
}
|
}
|
||||||
|
if let Ok(v) = std::env::var("IDX_CACHE_QUOTE_TTL")
|
||||||
|
&& let Ok(parsed) = v.parse::<u64>()
|
||||||
|
{
|
||||||
|
cfg.quote_ttl = parsed;
|
||||||
|
}
|
||||||
|
if let Ok(v) = std::env::var("IDX_CACHE_FUNDAMENTAL_TTL")
|
||||||
|
&& let Ok(parsed) = v.parse::<u64>()
|
||||||
|
{
|
||||||
|
cfg.fundamental_ttl = parsed;
|
||||||
|
}
|
||||||
|
|
||||||
cfg.output = cli.output;
|
if let Some(output) = cli.output {
|
||||||
|
cfg.output = output;
|
||||||
|
}
|
||||||
cfg.no_color = cfg.no_color || cli.no_color;
|
cfg.no_color = cfg.no_color || cli.no_color;
|
||||||
|
|
||||||
Ok(cfg)
|
Ok(cfg)
|
||||||
|
|
@ -102,12 +114,89 @@ impl IdxConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_config_toml() -> String {
|
||||||
|
"[general]\nexchange = \"JK\"\noutput = \"table\"\ncolor = true\n\n[cache]\nquote_ttl = 300\nfundamental_ttl = 3600\n".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn config_path() -> Result<PathBuf, IdxError> {
|
pub fn config_path() -> Result<PathBuf, IdxError> {
|
||||||
ProjectDirs::from("com", "idx", "idx")
|
ProjectDirs::from("", "", "idx")
|
||||||
.map(|d| d.config_dir().join("config.toml"))
|
.map(|d| d.config_dir().join("config.toml"))
|
||||||
.ok_or_else(|| IdxError::ConfigError("unable to resolve config dir".to_string()))
|
.ok_or_else(|| IdxError::ConfigError("unable to resolve config dir".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ensure_default_config() -> Result<PathBuf, IdxError> {
|
||||||
|
let path = config_path()?;
|
||||||
|
if path.exists() {
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent).map_err(|e| IdxError::Io(e.to_string()))?;
|
||||||
|
}
|
||||||
|
fs::write(&path, default_config_toml()).map_err(|e| IdxError::Io(e.to_string()))?;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_config_value(key: &str) -> Result<Option<String>, IdxError> {
|
||||||
|
let path = config_path()?;
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let raw = fs::read_to_string(path).map_err(|e| IdxError::Io(e.to_string()))?;
|
||||||
|
let value: toml::Value = toml::from_str(&raw).map_err(|e| IdxError::ConfigError(e.to_string()))?;
|
||||||
|
|
||||||
|
let mut cur = &value;
|
||||||
|
for part in key.split('.') {
|
||||||
|
let Some(next) = cur.get(part) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
cur = next;
|
||||||
|
}
|
||||||
|
Ok(Some(cur.to_string().trim_matches('"').to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_config_value(key: &str, value: &str) -> Result<(), IdxError> {
|
||||||
|
let path = ensure_default_config()?;
|
||||||
|
let raw = fs::read_to_string(&path).map_err(|e| IdxError::Io(e.to_string()))?;
|
||||||
|
let mut root: toml::Value = toml::from_str(&raw).map_err(|e| IdxError::ConfigError(e.to_string()))?;
|
||||||
|
|
||||||
|
let mut parts = key.split('.').peekable();
|
||||||
|
let mut current = root
|
||||||
|
.as_table_mut()
|
||||||
|
.ok_or_else(|| IdxError::ConfigError("config root is not a table".to_string()))?;
|
||||||
|
|
||||||
|
while let Some(part) = parts.next() {
|
||||||
|
if parts.peek().is_none() {
|
||||||
|
current.insert(part.to_string(), parse_toml_value(value));
|
||||||
|
} else {
|
||||||
|
let entry = current
|
||||||
|
.entry(part.to_string())
|
||||||
|
.or_insert_with(|| toml::Value::Table(toml::map::Map::new()));
|
||||||
|
if !entry.is_table() {
|
||||||
|
*entry = toml::Value::Table(toml::map::Map::new());
|
||||||
|
}
|
||||||
|
current = entry
|
||||||
|
.as_table_mut()
|
||||||
|
.ok_or_else(|| IdxError::ConfigError("invalid config path".to_string()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::write(&path, toml::to_string_pretty(&root).map_err(|e| IdxError::ConfigError(e.to_string()))?)
|
||||||
|
.map_err(|e| IdxError::Io(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_toml_value(value: &str) -> toml::Value {
|
||||||
|
if let Ok(v) = value.parse::<i64>() {
|
||||||
|
return toml::Value::Integer(v);
|
||||||
|
}
|
||||||
|
if let Ok(v) = value.parse::<f64>() {
|
||||||
|
return toml::Value::Float(v);
|
||||||
|
}
|
||||||
|
if let Ok(v) = value.parse::<bool>() {
|
||||||
|
return toml::Value::Boolean(v);
|
||||||
|
}
|
||||||
|
toml::Value::String(value.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::IdxConfig;
|
use super::IdxConfig;
|
||||||
|
|
|
||||||
15
src/main.rs
15
src/main.rs
|
|
@ -40,13 +40,22 @@ fn run() -> Result<(), IdxError> {
|
||||||
}
|
}
|
||||||
Commands::Stocks(stocks) => {
|
Commands::Stocks(stocks) => {
|
||||||
let provider = default_provider();
|
let provider = default_provider();
|
||||||
if let Err(err) = cli::stocks::handle(stocks, &config, provider.as_ref()) {
|
if let Err(err) = cli::stocks::handle(stocks, &config, provider.as_ref(), cli.offline, cli.no_cache) {
|
||||||
emit_error(&err, &config.output);
|
emit_error(&err, &config.output);
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Config(_) | Commands::Cache(_) => {
|
Commands::Config(cfg) => {
|
||||||
println!("Not implemented yet");
|
if let Err(err) = cli::config::handle(cfg) {
|
||||||
|
emit_error(&err, &config.output);
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Commands::Cache(cache) => {
|
||||||
|
if let Err(err) = cli::cache::handle(cache) {
|
||||||
|
emit_error(&err, &config.output);
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue