aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-06-21 23:12:02 +0800
committer魏曹先生 <1992414357@qq.com>2026-06-21 23:12:37 +0800
commite703c9376cd000161672ff225ff8e0f3c167ed9f (patch)
tree7d93889eb145e09c6a46f5bbee898ed43cc83e92 /src
parent2f0c1f3fda6025da64636a4cfd01d4f98bc7d50b (diff)
feat: add TOML config file support with CLI fallback
Diffstat (limited to 'src')
-rw-r--r--src/args.rs165
-rw-r--r--src/main.rs8
2 files changed, 164 insertions, 9 deletions
diff --git a/src/args.rs b/src/args.rs
index 9fe76d6..9a4bd0d 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -1,7 +1,6 @@
use std::path::PathBuf;
/// Whether verbose debug output is enabled.
-/// Set by `DMVOPArguments::verbose`.
pub static VERBOSE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
/// Print a debug message only when `--verbose` is set.
@@ -17,6 +16,10 @@ macro_rules! debug_log {
/// The full help text embedded at compile time.
pub static HELP_TEXT: &str = include_str!("../help.txt");
+// ===========================================================================
+// CLI arguments (clap)
+// ===========================================================================
+
#[derive(clap::Parser)]
#[command(name = "dmvop", disable_help_flag = true, disable_version_flag = true)]
pub struct DMVOPArguments {
@@ -32,6 +35,10 @@ pub struct DMVOPArguments {
#[arg(long = "verbose", short = 'V')]
pub verbose: bool,
+ // Config file path
+ #[arg(long = "config", require_equals = true)]
+ pub config: Option<PathBuf>,
+
// List all available input devices and exit
#[arg(long = "list-devices", alias = "list", short = 'L')]
pub list_devices: bool,
@@ -127,6 +134,151 @@ pub struct DMVOPArguments {
pub post: Option<String>,
}
+// ===========================================================================
+// Configuration file (serde) — mirrors DMVOPArguments with all-Option fields
+// ===========================================================================
+
+/// Mirrors [`DMVOPArguments`] as a TOML-serialisable config.
+/// Every field is `Option` so the merge logic can tell what was explicitly set.
+#[derive(serde::Deserialize, Default)]
+#[serde(default)]
+pub struct DMVOPConfig {
+ pub instant: Option<bool>,
+ pub lang: Option<String>,
+ pub model: Option<String>,
+ pub device: Option<String>,
+ pub format: Option<String>,
+ pub format_file: Option<PathBuf>,
+ pub output: Option<Vec<String>>,
+ pub port: Option<u16>,
+ pub socket_file: Option<PathBuf>,
+ pub models_dir: Option<PathBuf>,
+ pub subnet_mask: Option<String>,
+ pub post: Option<String>,
+}
+
+/// Resolve a path in the config file relative to the config file's directory.
+fn resolve_config_path(config_file: &PathBuf, path: &PathBuf) -> PathBuf {
+ if path.is_absolute() {
+ path.clone()
+ } else if let Some(parent) = config_file.parent() {
+ parent.join(path)
+ } else {
+ path.clone()
+ }
+}
+
+/// Load config file, then merge CLI args on top.
+/// Config paths are resolved relative to the config file's directory.
+pub fn load_and_merge_config(config: Option<&PathBuf>) -> Option<DMVOPConfig> {
+ let config_path = config.cloned().or_else(|| {
+ let fallback = PathBuf::from("./dmvop.toml");
+ if fallback.exists() {
+ Some(fallback)
+ } else {
+ None
+ }
+ });
+
+ let config_path = config_path?;
+ let content = match std::fs::read_to_string(&config_path) {
+ Ok(c) => c,
+ Err(e) => {
+ eprintln!(
+ "[dmvop] Failed to read config '{}': {}",
+ config_path.display(),
+ e
+ );
+ std::process::exit(1);
+ }
+ };
+
+ let mut cfg: DMVOPConfig = match toml::from_str(&content) {
+ Ok(c) => c,
+ Err(e) => {
+ eprintln!(
+ "[dmvop] Failed to parse config '{}': {}",
+ config_path.display(),
+ e
+ );
+ std::process::exit(1);
+ }
+ };
+
+ // Resolve relative paths
+ if let Some(ref mut f) = cfg.format_file {
+ *f = resolve_config_path(&config_path, f);
+ }
+ if let Some(ref mut d) = cfg.models_dir {
+ *d = resolve_config_path(&config_path, d);
+ }
+ if let Some(ref mut s) = cfg.socket_file {
+ *s = resolve_config_path(&config_path, s);
+ }
+
+ Some(cfg)
+}
+
+/// Merge config file values into CLI args (CLI wins).
+pub fn apply_config(args: &mut DMVOPArguments, cfg: &DMVOPConfig) {
+ if let Some(v) = cfg.instant {
+ if !args.instant {
+ args.instant = v;
+ }
+ }
+ if let Some(ref v) = cfg.lang {
+ if args.lang.is_none() {
+ args.lang = Some(v.clone());
+ }
+ }
+ if let Some(ref v) = cfg.model {
+ args.model = v.clone();
+ }
+ if let Some(ref v) = cfg.device {
+ if args.device_name.is_none() {
+ args.device_name = Some(v.clone());
+ }
+ }
+ if let Some(ref v) = cfg.format {
+ if args.format_pattern.is_none() {
+ args.format_pattern = Some(v.clone());
+ }
+ }
+ if let Some(ref v) = cfg.format_file {
+ if args.format_file.is_none() {
+ args.format_file = Some(v.clone());
+ }
+ }
+ if let Some(ref v) = cfg.output {
+ if args.output.is_empty() {
+ args.output = v.iter().filter_map(|s| s.parse().ok()).collect();
+ }
+ }
+ if let Some(v) = cfg.port {
+ args.port = v;
+ }
+ if let Some(ref v) = cfg.socket_file {
+ args.socket_file = v.clone();
+ }
+ if let Some(ref v) = cfg.models_dir {
+ if args.models_dir.is_none() {
+ args.models_dir = Some(v.clone());
+ }
+ }
+ if let Some(ref v) = cfg.subnet_mask {
+ args.subnet_mask = v.clone();
+ }
+ if let Some(ref v) = cfg.post {
+ if args.post.is_none() {
+ args.post = Some(v.clone());
+ }
+ }
+}
+
+// ===========================================================================
+// Output mode enum
+// ===========================================================================
+
#[derive(Clone, Debug)]
#[allow(non_camel_case_types)]
pub enum OutputMode {
@@ -163,13 +315,10 @@ impl std::str::FromStr for OutputMode {
}
}
-/// Parse a format pattern string and produce a formatted output string
-/// from the provided values.
-///
-/// Supported placeholders:
-/// - `%{vol}` — volume 0–100
-/// - `%{word}` — transcribed word/text
-/// - `%{confid}` / `%{confidence}` — confidence score
+// ===========================================================================
+// Format helpers
+// ===========================================================================
+
pub fn format_output(pattern: &str, word: &str, confidence: f32, volume: f32) -> String {
let mut result = pattern.to_string();
diff --git a/src/main.rs b/src/main.rs
index 66af45f..2aa68ac 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -58,7 +58,7 @@ async fn main() {
.with_target(false)
.init();
- let args = match DMVOPArguments::try_parse() {
+ let mut args = match DMVOPArguments::try_parse() {
Ok(a) => a,
Err(_) => {
eprintln!("error: invalid arguments. Use --help for usage.");
@@ -72,6 +72,12 @@ async fn main() {
return;
}
+ // Load config (--config or ./dmvop.toml) and merge (CLI wins)
+ if let Some(cfg) = load_and_merge_config(args.config.as_ref()) {
+ apply_config(&mut args, &cfg);
+ debug_log!("[dmvop] Loaded config");
+ }
+
// Set global verbose flag
VERBOSE.store(args.verbose, std::sync::atomic::Ordering::Relaxed);