aboutsummaryrefslogtreecommitdiff
path: root/mingling_core
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-04-11 16:50:57 +0800
committer魏曹先生 <1992414357@qq.com>2026-04-11 16:50:57 +0800
commit58ef8a8f42a68c7a81118ef9120705730ce3f458 (patch)
tree80f302b07f011d2e636f5f8d3ec815fe6a4dafab /mingling_core
parent839326946560166da84c04d4770385795d96cff0 (diff)
Add shell completion script generation feature
Diffstat (limited to 'mingling_core')
-rw-r--r--mingling_core/Cargo.toml3
-rw-r--r--mingling_core/src/asset/comp.rs27
-rw-r--r--mingling_core/src/builds.rs3
-rw-r--r--mingling_core/src/builds/comp.rs80
-rw-r--r--mingling_core/src/lib.rs7
-rw-r--r--mingling_core/src/program.rs24
-rw-r--r--mingling_core/src/program/exec.rs23
-rw-r--r--mingling_core/tmpls/comps/bash.sh48
-rw-r--r--mingling_core/tmpls/comps/fish.fish128
-rw-r--r--mingling_core/tmpls/comps/pwsl.ps143
-rw-r--r--mingling_core/tmpls/comps/zsh.zsh68
11 files changed, 428 insertions, 26 deletions
diff --git a/mingling_core/Cargo.toml b/mingling_core/Cargo.toml
index ef0ea5d..0d4baaa 100644
--- a/mingling_core/Cargo.toml
+++ b/mingling_core/Cargo.toml
@@ -9,7 +9,7 @@ repository = "https://github.com/catilgrass/mingling"
[features]
default = []
full = ["comp", "general_renderer"]
-comp = []
+comp = ["dep:just_template"]
general_renderer = [
"dep:serde",
"dep:ron",
@@ -20,6 +20,7 @@ general_renderer = [
[dependencies]
just_fmt = "0.1.2"
+just_template = { version = "0.1.3", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
thiserror = "2"
tokio = { version = "1", features = ["io-std", "io-util"] }
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<P>(ctx: &ShellContext) -> Suggest
where
- P: ProgramCollect + Display + 'static,
+ P: ProgramCollect<Enum = P> + Display + 'static,
{
let program = this::<P>();
- 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<P>(ctx: ShellContext, suggest: Suggest)
where
- P: ProgramCollect + Display + 'static,
+ P: ProgramCollect<Enum = P> + 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<dyn Dispatcher<G> + 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<C: ProgramCollect<Enum = G>, G: Display>(
+ program: &Program<C, G>,
+) -> Vec<(String, &Box<dyn Dispatcher<G> + Send + Sync>)> {
+ program
+ .dispatcher
+ .iter()
+ .map(|disp| {
+ let node_str = disp
+ .node()
+ .to_string()
+ .split('.')
+ .collect::<Vec<_>>()
+ .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<C, G>(
+pub fn match_user_input<C, G>(
program: &Program<C, G>,
) -> Result<(&Box<dyn Dispatcher<G> + Send + Sync>, Vec<String>), ProgramInternalExecuteError>
where
C: ProgramCollect<Enum = G>,
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<C: ProgramCollect<Enum = G>, G: Display>(
}
}
}
-
-// Get all registered dispatcher names from the program
-fn get_nodes<C: ProgramCollect<Enum = G>, G: Display>(
- program: &Program<C, G>,
-) -> Vec<(String, &Box<dyn Dispatcher<G> + Send + Sync>)> {
- program
- .dispatcher
- .iter()
- .map(|disp| {
- let node_str = disp
- .node()
- .to_string()
- .split('.')
- .collect::<Vec<_>>()
- .join(" ");
- (node_str, disp)
- })
- .collect()
-}
diff --git a/mingling_core/tmpls/comps/bash.sh b/mingling_core/tmpls/comps/bash.sh
new file mode 100644
index 0000000..1af4f6c
--- /dev/null
+++ b/mingling_core/tmpls/comps/bash.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+_<<<bin_name>>>_bash_completion() {
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ local prev=""
+ [ $COMP_CWORD -gt 0 ] && prev="${COMP_WORDS[COMP_CWORD-1]}"
+
+ local word_index=$((COMP_CWORD + 1))
+
+ local args=()
+ args+=(-f="${COMP_LINE//-/^}")
+ args+=(-C="$COMP_POINT")
+ args+=(-w="${cur//-/^}")
+ args+=(-p="${prev//-/^}")
+ args+=(-c="${COMP_WORDS[0]//-/^}")
+ args+=(-i="$word_index")
+ args+=(-F="bash")
+
+ for word in "${COMP_WORDS[@]}"; do
+ args+=(-a="${word//-/^}")
+ done
+
+ local suggestions
+ if suggestions=$(<<<bin_name>>> __comp "${args[@]}" 2>/dev/null); then
+ if [ $? -eq 0 ]; then
+ if [ "$suggestions" = "_file_" ]; then
+ compopt -o default
+ COMPREPLY=()
+ return
+ fi
+
+ if [ -n "$suggestions" ]; then
+ local -a all_suggestions filtered
+ mapfile -t all_suggestions < <(printf '%s\n' "$suggestions")
+
+ for suggestion in "${all_suggestions[@]}"; do
+ [ -z "$cur" ] || [[ "$suggestion" == "$cur"* ]] && filtered+=("$suggestion")
+ done
+
+ [ ${#filtered[@]} -gt 0 ] && COMPREPLY=("${filtered[@]}")
+ return
+ fi
+ fi
+ fi
+
+ COMPREPLY=()
+}
+
+complete -F _<<<bin_name>>>_bash_completion <<<bin_name>>>
diff --git a/mingling_core/tmpls/comps/fish.fish b/mingling_core/tmpls/comps/fish.fish
new file mode 100644
index 0000000..8be948a
--- /dev/null
+++ b/mingling_core/tmpls/comps/fish.fish
@@ -0,0 +1,128 @@
+#!/usr/bin/env fish
+function __<<<bin_name>>>_fish_complete
+ set -l cmdline (commandline -opc)
+ set -l buffer (commandline -b)
+ set -l cursor (commandline -C)
+ set -l current_token (commandline -ct)
+
+ # Calculate current word and word index
+ set -l current_word ""
+ set -l previous_word ""
+ set -l word_index 0
+ set -l char_count 0
+
+ set -l found false
+ if test -n "$current_token"
+ for i in (seq (count $cmdline))
+ if test "$cmdline[$i]" = "$current_token"
+ set word_index $i
+ set current_word $current_token
+ if test $i -gt 1
+ set previous_word $cmdline[(math $i - 1)]
+ end
+ set found true
+ break
+ end
+ end
+ end
+
+ if not $found
+ for i in (seq (count $cmdline))
+ set word $cmdline[$i]
+ if test $i -gt 1
+ set char_count (math $char_count + 1)
+ end
+ set char_count (math $char_count + (string length -- "$word"))
+
+ if test $cursor -le $char_count
+ set word_index $i
+ set current_word $word
+ if test $i -gt 1
+ set previous_word $cmdline[(math $i - 1)]
+ end
+ break
+ end
+ end
+ end
+
+ # Handle cursor after last word
+ if test $word_index -eq 0 -a (count $cmdline) -gt 0
+ set word_index (count $cmdline)
+ if test -n "$current_token" -a "$current_token" != "$cmdline[-1]"
+ set current_word $current_token
+ else
+ set current_word ""
+ end
+ set previous_word $cmdline[-1]
+ end
+
+ # Ensure word_index is within bounds
+ if test $word_index -gt (count $cmdline)
+ set word_index (count $cmdline)
+ end
+
+ # Replace hyphens with carets for jvn_comp
+ set -l buffer_replaced (string replace -a "-" "^" -- "$buffer")
+ set -l current_word_replaced (string replace -a "-" "^" -- "$current_word")
+ set -l previous_word_replaced (string replace -a "-" "^" -- "$previous_word")
+
+ # Build args array
+ set -l args
+ set -a args -f "$buffer_replaced" -C "$cursor" -w "$current_word_replaced" -p "$previous_word_replaced"
+
+ if test (count $cmdline) -gt 0
+ set -a args -c "$cmdline[1]"
+ else
+ set -a args -c ""
+ end
+
+ set -a args -i "$word_index"
+
+ # Replace hyphens in all words
+ if test (count $cmdline) -gt 0
+ set -l all_words_replaced
+ for word in $cmdline
+ set -a all_words_replaced (string replace -a "-" "^" -- "$word")
+ end
+
+ if test -n "$current_token" -a "$current_word" = "$current_token"
+ set -l found_in_cmdline false
+ for word in $cmdline
+ if test "$word" = "$current_token"
+ set found_in_cmdline true
+ break
+ end
+ end
+ if not $found_in_cmdline -a $word_index -eq (math (count $cmdline) + 1)
+ set -a all_words_replaced (string replace -a "-" "^" -- "$current_token")
+ end
+ end
+
+ set -a args -a $all_words_replaced
+ else
+ set -a args -a ""
+ end
+
+ # Add shell type argument
+ set -a args -F "fish"
+
+ # Call jvn_comp and handle output
+ set -l output
+ if not <<<bin_name>>> __comp $args 2>/dev/null | read -z output
+ return
+ end
+
+ set -l trimmed_output (string trim -- "$output")
+ if test "$trimmed_output" = "_file_"
+ __fish_complete_path "$current_word"
+ return 0
+ else if test -n "$trimmed_output"
+ string split -n \n -- "$output" | while read -l line
+ test -n "$line" && echo "$line"
+ end
+ return 0
+ end
+ return 1
+end
+
+complete -c <<<bin_name>>> -a '(__<<<bin_name>>>_fish_complete)' -f
diff --git a/mingling_core/tmpls/comps/pwsl.ps1 b/mingling_core/tmpls/comps/pwsl.ps1
new file mode 100644
index 0000000..6d7d91d
--- /dev/null
+++ b/mingling_core/tmpls/comps/pwsl.ps1
@@ -0,0 +1,43 @@
+Register-ArgumentCompleter -CommandName <<<bin_name>>> -ScriptBlock {
+ param($wordToComplete, $commandAst, $cursorPosition)
+
+ $line = $commandAst.ToString()
+ $commandName = if ($commandAst.CommandElements.Count -gt 0) {
+ $commandAst.CommandElements[0].Value
+ } else { "" }
+
+ $words = @()
+ $currentIndex = 0
+ $parser = [System.Management.Automation.PSParser]
+ $tokens = $parser::Tokenize($line, [ref]$null)
+
+ foreach ($token in $tokens) {
+ if ($token.Type -in 'CommandArgument', 'CommandParameter') {
+ $words += $token.Content
+ }
+ }
+
+ $args = @(
+ "-f", ($line -replace '-', '^')
+ "-C", $cursorPosition.ToString()
+ "-w", ($wordToComplete -replace '-', '^')
+ "-p", (if ($words.Count -gt 1) { $words[-2] } else { "" }) -replace '-', '^'
+ "-c", $commandName
+ "-i", ($words.Count - 1).ToString()
+ "-a", ($words | ForEach-Object { $_ -replace '-', '^' })
+ "-F", "powershell"
+ )
+
+ $suggestions = <<<bin_name>>> __comp $args 2>$null
+
+ if ($suggestions) {
+ $suggestions | ForEach-Object {
+ if ($_ -eq "_file_") {
+ $completionType = 'ProviderItem'
+ } else {
+ $completionType = 'ParameterValue'
+ }
+ [System.Management.Automation.CompletionResult]::new($_, $_, $completionType, $_)
+ }
+ }
+}
diff --git a/mingling_core/tmpls/comps/zsh.zsh b/mingling_core/tmpls/comps/zsh.zsh
new file mode 100644
index 0000000..c1c18bb
--- /dev/null
+++ b/mingling_core/tmpls/comps/zsh.zsh
@@ -0,0 +1,68 @@
+#!/usr/bin/env zsh
+_<<<bin_name>>>_completion() {
+ local -a args
+ local suggestions
+
+ local buffer="$BUFFER"
+ local cursor="$CURSOR"
+ local current_word="${words[$CURRENT]}"
+ local previous_word=""
+ local command_name="${words[1]}"
+ local word_index="$CURRENT"
+
+ if [[ $CURRENT -gt 1 ]]; then
+ previous_word="${words[$((CURRENT-1))]}"
+ fi
+
+ args=(
+ -f "${buffer//-/^}"
+ -C "$cursor"
+ -w "${current_word//-/^}"
+ -p "${previous_word//-/^}"
+ -c "$command_name"
+ -i "$word_index"
+ -a "${(@)words//-/^}"
+ -F "zsh"
+ )
+
+ suggestions=$(<<<bin_name>>> __comp "${args[@]}" 2>/dev/null)
+
+ if [[ $? -eq 0 ]] && [[ -n "$suggestions" ]]; then
+ local -a completions
+ completions=(${(f)suggestions})
+
+ if [[ "${completions[1]}" == "_file_" ]]; then
+ shift completions
+ _files
+ else
+ local -a parsed_completions
+ for item in "${completions[@]}"; do
+ if [[ "$item" =~ '^([^$]+)\$\((.+)\)$' ]]; then
+ parsed_completions+=("${match[1]}:${match[2]}")
+ else
+ parsed_completions+=("$item")
+ fi
+ done
+
+ if (( $+functions[_describe] )); then
+ _describe '<<<bin_name>>> commands' parsed_completions
+ else
+ local -a simple_completions
+ for item in "${parsed_completions[@]}"; do
+ if [[ "$item" =~ '^([^:]+):(.+)$' ]]; then
+ simple_completions+=("${match[1]}")
+ else
+ simple_completions+=("$item")
+ fi
+ done
+ compadd -a simple_completions
+ fi
+ fi
+ fi
+}
+
+compdef _<<<bin_name>>>_completion <<<bin_name>>>
+
+if [[ $? -ne 0 ]]; then
+ compctl -K _<<<bin_name>>>_completion <<<bin_name>>>
+fi