From 58ef8a8f42a68c7a81118ef9120705730ce3f458 Mon Sep 17 00:00:00 2001
From: 魏曹先生 <1992414357@qq.com>
Date: Sat, 11 Apr 2026 16:50:57 +0800
Subject: Add shell completion script generation feature
---
mingling_core/src/asset/comp.rs | 27 +++++++++++--
mingling_core/src/builds.rs | 3 ++
mingling_core/src/builds/comp.rs | 80 +++++++++++++++++++++++++++++++++++++++
mingling_core/src/lib.rs | 7 ++++
mingling_core/src/program.rs | 24 ++++++++++++
mingling_core/src/program/exec.rs | 23 +----------
6 files changed, 139 insertions(+), 25 deletions(-)
create mode 100644 mingling_core/src/builds.rs
create mode 100644 mingling_core/src/builds/comp.rs
(limited to 'mingling_core/src')
diff --git a/mingling_core/src/asset/comp.rs b/mingling_core/src/asset/comp.rs
index eeef0c0..3c22e12 100644
--- a/mingling_core/src/asset/comp.rs
+++ b/mingling_core/src/asset/comp.rs
@@ -11,7 +11,7 @@ pub use shell_ctx::*;
#[doc(hidden)]
pub use suggest::*;
-use crate::{ProgramCollect, this};
+use crate::{ProgramCollect, exec::match_user_input, this};
/// Trait for implementing completion logic.
///
@@ -36,15 +36,34 @@ pub struct CompletionHelper;
impl CompletionHelper {
pub fn exec_completion
(ctx: &ShellContext) -> Suggest
where
- P: ProgramCollect + Display + 'static,
+ P: ProgramCollect + Display + 'static,
{
let program = this::();
- Suggest::FileCompletion
+ let suggest = if let Some((dispatcher, args)) = match_user_input(program).ok() {
+ let begin = dispatcher.begin(args);
+ if let crate::ChainProcess::Ok((any, _)) = begin {
+ Some(P::do_comp(&any, ctx))
+ } else {
+ None
+ }
+ } else {
+ None
+ };
+
+ match suggest {
+ Some(suggest) => suggest,
+ None => default_completion(ctx),
+ }
}
pub fn render_suggest
(ctx: ShellContext, suggest: Suggest)
where
- P: ProgramCollect + Display + 'static,
+ P: ProgramCollect + Display + 'static,
{
+ todo!()
}
}
+
+fn default_completion(ctx: &ShellContext) -> Suggest {
+ todo!()
+}
diff --git a/mingling_core/src/builds.rs b/mingling_core/src/builds.rs
new file mode 100644
index 0000000..0123c82
--- /dev/null
+++ b/mingling_core/src/builds.rs
@@ -0,0 +1,3 @@
+#[doc(hidden)]
+#[cfg(feature = "comp")]
+pub mod comp;
diff --git a/mingling_core/src/builds/comp.rs b/mingling_core/src/builds/comp.rs
new file mode 100644
index 0000000..694af0c
--- /dev/null
+++ b/mingling_core/src/builds/comp.rs
@@ -0,0 +1,80 @@
+use just_template::tmpl_param;
+
+use crate::ShellFlag;
+
+const TMPL_COMP_BASH: &str = include_str!("../../tmpls/comps/bash.sh");
+const TMPL_COMP_ZSH: &str = include_str!("../../tmpls/comps/zsh.zsh");
+const TMPL_COMP_FISH: &str = include_str!("../../tmpls/comps/fish.fish");
+const TMPL_COMP_PWSL: &str = include_str!("../../tmpls/comps/pwsl.ps1");
+
+/// Generate shell completion scripts for the current binary.
+/// On Windows, generates PowerShell completion.
+/// On Linux, generates Zsh, Bash, and Fish completions.
+/// Scripts are written to the `OUT_DIR` (or `target/` if `OUT_DIR` is not set).
+///
+/// # Example
+/// ```
+/// // Typically called from a build script (`build.rs`):
+/// build_comp_scripts().unwrap();
+/// // Or, to specify a custom binary name:
+/// build_comp_scripts_with_bin_name("myapp").unwrap();
+/// ```
+pub fn build_comp_scripts() -> Result<(), std::io::Error> {
+ let bin_name = env!("CARGO_PKG_NAME");
+ build_comp_scripts_with_bin_name(bin_name)
+}
+
+/// Generate shell completion scripts for a given binary name.
+/// On Windows, generates PowerShell completion.
+/// On Linux, generates Zsh, Bash, and Fish completions.
+/// Scripts are written to the `OUT_DIR` (or `target/` if `OUT_DIR` is not set).
+///
+/// # Example
+/// ```
+/// // Generate completion scripts for "myapp"
+/// build_comp_scripts_with_bin_name("myapp").unwrap();
+/// ```
+pub fn build_comp_scripts_with_bin_name(name: &str) -> Result<(), std::io::Error> {
+ #[cfg(target_os = "windows")]
+ {
+ build_comp_script(&ShellFlag::Powershell, name)?;
+ Ok(())
+ }
+
+ #[cfg(target_os = "linux")]
+ {
+ build_comp_script(&ShellFlag::Zsh, name)?;
+ build_comp_script(&ShellFlag::Bash, name)?;
+ build_comp_script(&ShellFlag::Fish, name)?;
+ Ok(())
+ }
+
+ #[cfg(target_os = "macos")]
+ {
+ build_comp_script(&ShellFlag::Zsh, name)?;
+ build_comp_script(&ShellFlag::Bash, name)?;
+ build_comp_script(&ShellFlag::Fish, name)?;
+ Ok(())
+ }
+}
+
+fn build_comp_script(shell_flag: &ShellFlag, bin_name: &str) -> Result<(), std::io::Error> {
+ let (tmpl_str, ext) = get_tmpl(shell_flag);
+ let mut tmpl = just_template::Template::from(tmpl_str);
+ tmpl_param!(tmpl, bin_name = bin_name);
+ let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
+ let target_dir = out_dir.join("../../../").to_path_buf();
+ let output_path = target_dir.join(format!("{}_comp{}", bin_name, ext));
+ std::fs::create_dir_all(&target_dir)?;
+ std::fs::write(&output_path, tmpl.to_string())
+}
+
+fn get_tmpl(shell_flag: &ShellFlag) -> (&'static str, &'static str) {
+ match shell_flag {
+ ShellFlag::Bash => (TMPL_COMP_BASH, ".sh"),
+ ShellFlag::Zsh => (TMPL_COMP_ZSH, ".zsh"),
+ ShellFlag::Fish => (TMPL_COMP_FISH, ".fish"),
+ ShellFlag::Powershell => (TMPL_COMP_PWSL, ".ps1"),
+ ShellFlag::Other(_) => (TMPL_COMP_BASH, ".sh"),
+ }
+}
diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs
index 7801d34..dacc1b4 100644
--- a/mingling_core/src/lib.rs
+++ b/mingling_core/src/lib.rs
@@ -48,3 +48,10 @@ pub mod marker {
pub mod setup {
pub use crate::program::setup::*;
}
+
+#[doc(hidden)]
+pub mod builds;
+pub mod build {
+ #[cfg(feature = "comp")]
+ pub use crate::builds::comp::*;
+}
diff --git a/mingling_core/src/program.rs b/mingling_core/src/program.rs
index 7b9f8d4..42ca531 100644
--- a/mingling_core/src/program.rs
+++ b/mingling_core/src/program.rs
@@ -164,6 +164,11 @@ where
}
}
}
+
+ // Get all registered dispatcher names from the program
+ pub fn get_nodes(&self) -> Vec<(String, &Box + Send + Sync>)> {
+ get_nodes(self)
+ }
}
/// Collected program context
@@ -251,3 +256,22 @@ macro_rules! __dispatch_program_chains {
}
};
}
+
+// Get all registered dispatcher names from the program
+pub fn get_nodes, G: Display>(
+ program: &Program,
+) -> Vec<(String, &Box + Send + Sync>)> {
+ program
+ .dispatcher
+ .iter()
+ .map(|disp| {
+ let node_str = disp
+ .node()
+ .to_string()
+ .split('.')
+ .collect::>()
+ .join(" ");
+ (node_str, disp)
+ })
+ .collect()
+}
diff --git a/mingling_core/src/program/exec.rs b/mingling_core/src/program/exec.rs
index f578064..072f4cb 100644
--- a/mingling_core/src/program/exec.rs
+++ b/mingling_core/src/program/exec.rs
@@ -73,14 +73,14 @@ where
/// Match user input against registered dispatchers and return the matched dispatcher and remaining arguments.
#[allow(clippy::type_complexity)]
-fn match_user_input(
+pub fn match_user_input(
program: &Program,
) -> Result<(&Box + Send + Sync>, Vec), ProgramInternalExecuteError>
where
C: ProgramCollect,
G: Display,
{
- let nodes = get_nodes(program);
+ let nodes = program.get_nodes();
let command = format!("{} ", program.args.join(" "));
// Find all nodes that match the command prefix
@@ -140,22 +140,3 @@ fn render, G: Display>(
}
}
}
-
-// Get all registered dispatcher names from the program
-fn get_nodes, G: Display>(
- program: &Program,
-) -> Vec<(String, &Box + Send + Sync>)> {
- program
- .dispatcher
- .iter()
- .map(|disp| {
- let node_str = disp
- .node()
- .to_string()
- .split('.')
- .collect::>()
- .join(" ");
- (node_str, disp)
- })
- .collect()
-}
--
cgit