diff options
| -rw-r--r-- | Cargo.lock | 10 | ||||
| -rw-r--r-- | mingling_core/Cargo.toml | 3 | ||||
| -rw-r--r-- | mingling_core/src/asset/comp.rs | 27 | ||||
| -rw-r--r-- | mingling_core/src/builds.rs | 3 | ||||
| -rw-r--r-- | mingling_core/src/builds/comp.rs | 80 | ||||
| -rw-r--r-- | mingling_core/src/lib.rs | 7 | ||||
| -rw-r--r-- | mingling_core/src/program.rs | 24 | ||||
| -rw-r--r-- | mingling_core/src/program/exec.rs | 23 | ||||
| -rw-r--r-- | mingling_core/tmpls/comps/bash.sh | 48 | ||||
| -rw-r--r-- | mingling_core/tmpls/comps/fish.fish | 128 | ||||
| -rw-r--r-- | mingling_core/tmpls/comps/pwsl.ps1 | 43 | ||||
| -rw-r--r-- | mingling_core/tmpls/comps/zsh.zsh | 68 |
12 files changed, 438 insertions, 26 deletions
@@ -95,6 +95,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" [[package]] +name = "just_template" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3edb658c34b10b69c4b3b58f7ba989cd09c82c0621dee1eef51843c2327225" +dependencies = [ + "just_fmt", +] + +[[package]] name = "libc" version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -145,6 +154,7 @@ name = "mingling_core" version = "0.1.4" dependencies = [ "just_fmt", + "just_template", "once_cell", "ron", "serde", 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 |
