aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-04-25 23:41:36 +0800
committer魏曹先生 <1992414357@qq.com>2026-04-25 23:41:36 +0800
commit4ac7d7dc9e6abec2f3f84dd5baf8b642727f19c3 (patch)
treea36a182869f035e52ec7f6b7e64826d90869f438
parent7625655d474f6f12e04a11a067f87287badce9f2 (diff)
Add help system with `#[help]` macro and `HelpRequest` trait
-rw-r--r--CHANGELOG.md9
-rw-r--r--mingling/src/lib.rs4
-rw-r--r--mingling_core/src/asset.rs3
-rw-r--r--mingling_core/src/asset/help.rs10
-rw-r--r--mingling_core/src/lib.rs1
-rw-r--r--mingling_core/src/program.rs3
-rw-r--r--mingling_core/src/program/exec.rs35
-rw-r--r--mingling_core/src/renderer/render_result.rs15
-rw-r--r--mingling_macros/src/help.rs202
-rw-r--r--mingling_macros/src/lib.rs27
10 files changed, 309 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2ba927..cd2ffc9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,15 @@ struct YourCommandEntry {
2. **\[core\]** Added function `new_with_args` to `Program`
3. **\[core\]** Added function `dispatch_args_dynamic` to `Program`
+4. **\[core\]** Impl `std::io::Write` trait for `RenderResult`
+5. **\[core\]** Added Help system, which allows binding an event for `--help` to an `Entry` via the `help!` macro
+
+```rust
+#[help]
+fn your_command_help(_prev: YourEntry) {
+ r_println!("Your help docs");
+}
+```
#### **BREAKING CHANGES**:
diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs
index d712ff0..5240682 100644
--- a/mingling/src/lib.rs
+++ b/mingling/src/lib.rs
@@ -84,6 +84,8 @@ pub mod macros {
pub use mingling_macros::dispatcher_clap;
/// Used to collect data and create a command-line context
pub use mingling_macros::gen_program;
+ /// Used to generate a struct implementing the `HelpRequest` trait via a method
+ pub use mingling_macros::help;
/// Used to create a `Node` struct via a literal
pub use mingling_macros::node;
/// Used to create a wrapper type for use with `Chain` and `Renderer`
@@ -103,6 +105,8 @@ pub mod macros {
pub use mingling_macros::r_println;
/// Used to register a chain
pub use mingling_macros::register_chain;
+ /// Used to register a help
+ pub use mingling_macros::register_help;
/// Used to register a renderer
pub use mingling_macros::register_renderer;
/// Used to register a type into the context
diff --git a/mingling_core/src/asset.rs b/mingling_core/src/asset.rs
index a4254ef..234fec1 100644
--- a/mingling_core/src/asset.rs
+++ b/mingling_core/src/asset.rs
@@ -16,3 +16,6 @@ pub mod node;
#[doc(hidden)]
pub mod renderer;
+
+#[doc(hidden)]
+pub mod help;
diff --git a/mingling_core/src/asset/help.rs b/mingling_core/src/asset/help.rs
new file mode 100644
index 0000000..ff2b3d4
--- /dev/null
+++ b/mingling_core/src/asset/help.rs
@@ -0,0 +1,10 @@
+use crate::RenderResult;
+
+/// Handles help rendering for command-line arguments
+pub trait HelpRequest {
+ /// The entry type
+ type Entry;
+
+ /// Process the previous value and write the result into the provided [`RenderResult`](./struct.RenderResult.html)
+ fn render_help(p: Self::Entry, r: &mut RenderResult);
+}
diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs
index 87be806..aee0df6 100644
--- a/mingling_core/src/lib.rs
+++ b/mingling_core/src/lib.rs
@@ -25,6 +25,7 @@ pub use crate::asset::chain::*;
pub use crate::asset::comp::*;
pub use crate::asset::dispatcher::*;
pub use crate::asset::enum_tag::*;
+pub use crate::asset::help::*;
pub use crate::asset::node::*;
pub use crate::asset::renderer::*;
diff --git a/mingling_core/src/program.rs b/mingling_core/src/program.rs
index c9cffbe..949dd45 100644
--- a/mingling_core/src/program.rs
+++ b/mingling_core/src/program.rs
@@ -289,6 +289,9 @@ pub trait ProgramCollect {
/// Render the input [AnyOutput](./struct.AnyOutput.html)
fn render(any: AnyOutput<Self::Enum>, r: &mut RenderResult);
+ /// Render help for Entry
+ fn render_help(any: AnyOutput<Self::Enum>, r: &mut RenderResult);
+
/// Find a matching chain to continue execution based on the input [AnyOutput](./struct.AnyOutput.html), returning a new [AnyOutput](./struct.AnyOutput.html)
#[cfg(feature = "async")]
fn do_chain(
diff --git a/mingling_core/src/program/exec.rs b/mingling_core/src/program/exec.rs
index c1eada5..469f6d2 100644
--- a/mingling_core/src/program/exec.rs
+++ b/mingling_core/src/program/exec.rs
@@ -16,6 +16,11 @@ where
let mut current = dispatch_args_dynamic(program, program.args.clone())?;
let mut stop_next = false;
+ // If the program has Help enabled, skip actual logic and jump to Help
+ if program.user_context.help {
+ return Ok(render_help::<C>(program, current));
+ }
+
loop {
let final_exec = stop_next;
@@ -56,6 +61,11 @@ where
let mut current = dispatch_args_dynamic(program, program.args.clone())?;
let mut stop_next = false;
+ // If the program has Help enabled, skip actual logic and jump to Help
+ if program.user_context.help {
+ return Ok(render_help::<C>(program, current));
+ }
+
loop {
let final_exec = stop_next;
@@ -179,3 +189,28 @@ fn render<C: ProgramCollect<Enum = C>>(program: &Program<C>, any: AnyOutput<C>)
}
}
}
+
+#[inline(always)]
+#[allow(unused_variables)]
+fn render_help<C: ProgramCollect<Enum = C>>(
+ program: &Program<C>,
+ entry: AnyOutput<C>,
+) -> RenderResult {
+ #[cfg(not(feature = "general_renderer"))]
+ {
+ let mut render_result = RenderResult::default();
+ C::render_help(entry, &mut render_result);
+ render_result
+ }
+ #[cfg(feature = "general_renderer")]
+ {
+ match program.general_renderer_name {
+ super::GeneralRendererSetting::Disable => {
+ let mut render_result = RenderResult::default();
+ C::render_help(entry, &mut render_result);
+ render_result
+ }
+ _ => RenderResult::default(),
+ }
+ }
+}
diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs
index d9da7b7..3bde1ab 100644
--- a/mingling_core/src/renderer/render_result.rs
+++ b/mingling_core/src/renderer/render_result.rs
@@ -1,5 +1,6 @@
use std::{
fmt::{Display, Formatter},
+ io::Write,
ops::Deref,
};
@@ -9,6 +10,20 @@ pub struct RenderResult {
render_text: String,
}
+impl Write for RenderResult {
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+ let s = std::str::from_utf8(buf).map_err(|_| {
+ std::io::Error::new(std::io::ErrorKind::InvalidInput, "not valid UTF-8")
+ })?;
+ self.render_text.push_str(s);
+ Ok(buf.len())
+ }
+
+ fn flush(&mut self) -> std::io::Result<()> {
+ Ok(())
+ }
+}
+
impl Display for RenderResult {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{}", self.render_text.trim())
diff --git a/mingling_macros/src/help.rs b/mingling_macros/src/help.rs
new file mode 100644
index 0000000..c0d8b99
--- /dev/null
+++ b/mingling_macros/src/help.rs
@@ -0,0 +1,202 @@
+//! Help Attribute Macro
+//!
+//! This module provides the `#[help]` attribute macro for automatically
+//! generating structs that implement the `HelpRequest` trait from functions.
+//!
+//! # Syntax
+//!
+//! ```rust,ignore
+//! #[help]
+//! fn help_my_entry(prev: MyEntry) {
+//! // use r_println! here
+//! r_println!("Help: ...");
+//! }
+//! ```
+//!
+//! This expands to:
+//! - A struct `HelpMyEntry` implementing `HelpRequest` with `Entry = MyEntry`
+//! - The original function with injected `RenderResult` dummy context
+
+use proc_macro::TokenStream;
+use quote::{ToTokens, quote};
+use syn::spanned::Spanned;
+use syn::{
+ FnArg, Ident, ItemFn, Pat, PatType, ReturnType, Signature, Type, TypePath, parse_macro_input,
+};
+
+/// Extracts the previous type and parameter name from function arguments
+fn extract_previous_info(sig: &Signature) -> syn::Result<(Pat, TypePath)> {
+ // The function should have exactly one parameter
+ if sig.inputs.len() != 1 {
+ return Err(syn::Error::new(
+ sig.inputs.span(),
+ "Help function must have exactly one parameter (the entry type)",
+ ));
+ }
+
+ // First and only parameter is the entry type
+ let arg = &sig.inputs[0];
+ match arg {
+ FnArg::Typed(PatType { pat, ty, .. }) => {
+ // Extract the pattern (parameter name)
+ let param_pat = (**pat).clone();
+
+ // Extract the type
+ match &**ty {
+ Type::Path(type_path) => Ok((param_pat, type_path.clone())),
+ _ => Err(syn::Error::new(
+ ty.span(),
+ "Parameter type must be a type path",
+ )),
+ }
+ }
+ FnArg::Receiver(_) => Err(syn::Error::new(
+ arg.span(),
+ "Help function cannot have self parameter",
+ )),
+ }
+}
+
+/// Validates the return type is () or empty
+fn validate_return_type(sig: &Signature) -> syn::Result<()> {
+ match &sig.output {
+ ReturnType::Type(_, ty) => match &**ty {
+ Type::Tuple(tuple) if tuple.elems.is_empty() => Ok(()),
+ _ => Err(syn::Error::new(
+ ty.span(),
+ "Help function must return () or have no return type",
+ )),
+ },
+ ReturnType::Default => Ok(()),
+ }
+}
+
+pub fn help_attr(item: TokenStream) -> TokenStream {
+ // Parse the function item
+ let input_fn = parse_macro_input!(item as ItemFn);
+
+ // Validate the function is not async
+ if input_fn.sig.asyncness.is_some() {
+ return syn::Error::new(input_fn.sig.span(), "Help function cannot be async")
+ .to_compile_error()
+ .into();
+ }
+
+ // Extract the entry type and parameter name from function arguments
+ let (prev_param, entry_type) = match extract_previous_info(&input_fn.sig) {
+ Ok(info) => info,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ // Validate return type
+ if let Err(e) = validate_return_type(&input_fn.sig) {
+ return e.to_compile_error().into();
+ }
+
+ // Get the function body
+ let fn_body = &input_fn.block;
+
+ // Get function attributes (excluding the help attribute)
+ let mut fn_attrs = input_fn.attrs.clone();
+ fn_attrs.retain(|attr| !attr.path().is_ident("help"));
+
+ // Get function visibility
+ let vis = &input_fn.vis;
+
+ // Get function name
+ let fn_name = &input_fn.sig.ident;
+
+ // Generate struct name from function name using pascal_case
+ let pascal_case_name = just_fmt::pascal_case!(fn_name.to_string());
+ let struct_name = Ident::new(&pascal_case_name, fn_name.span());
+
+ // Register the help request mapping
+ let help_entry = build_help_entry(&struct_name, &entry_type);
+ let entry_str = help_entry.to_string();
+ crate::HELP_REQUESTS.lock().unwrap().insert(entry_str);
+
+ // Generate the struct and HelpRequest implementation
+ let expanded = quote! {
+ #(#fn_attrs)*
+ #[doc(hidden)]
+ #vis struct #struct_name;
+
+ impl ::mingling::HelpRequest for #struct_name {
+ type Entry = #entry_type;
+
+ fn render_help(#prev_param: Self::Entry, r: &mut ::mingling::RenderResult) {
+ // Create a local wrapper function that includes r parameter
+ // This allows r_println! to access r
+ #[allow(non_snake_case)]
+ fn help_wrapper(#prev_param: #entry_type, r: &mut ::mingling::RenderResult) {
+ #fn_body
+ }
+
+ // Call the wrapper function
+ help_wrapper(#prev_param, r);
+ }
+ }
+
+ ::mingling::macros::register_help!(#entry_type, #struct_name);
+
+ // Keep the original function for internal use (without r parameter)
+ #(#fn_attrs)*
+ #vis fn #fn_name(#prev_param: #entry_type) {
+ let mut dummy_r = ::mingling::RenderResult::default();
+ let r = &mut dummy_r;
+ #fn_body
+ }
+ };
+
+ expanded.into()
+}
+
+/// Builds a help request entry for the global help requests list
+fn build_help_entry(struct_name: &Ident, entry_type: &TypePath) -> proc_macro2::TokenStream {
+ let enum_variant = &entry_type.path.segments.last().unwrap().ident;
+ quote! {
+ Self::#enum_variant => {
+ // SAFETY: The member_id check ensures that `any` contains a value of type `#entry_type`,
+ // so downcasting to `#entry_type` is safe.
+ let value = unsafe { any.downcast::<#entry_type>().unwrap_unchecked() };
+ <#struct_name as ::mingling::HelpRequest>::render_help(value, r);
+ }
+ }
+}
+
+pub fn register_help(input: TokenStream) -> TokenStream {
+ // Parse the input as a comma-separated list of arguments
+ let input_parsed = syn::parse_macro_input!(input with syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>::parse_terminated);
+
+ // Check that we have exactly two elements
+ if input_parsed.len() != 2 {
+ return syn::Error::new(
+ input_parsed.span(),
+ "Expected exactly two comma-separated arguments: `EntryType, StructName`",
+ )
+ .to_compile_error()
+ .into();
+ }
+
+ // Extract the two elements
+ let entry_type_expr = &input_parsed[0];
+ let struct_name_expr = &input_parsed[1];
+
+ // Convert expressions to TypePath and Ident
+ let entry_type = match syn::parse2::<TypePath>(entry_type_expr.to_token_stream()) {
+ Ok(ty) => ty,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ let struct_name = match syn::parse2::<syn::Ident>(struct_name_expr.to_token_stream()) {
+ Ok(ident) => ident,
+ Err(e) => return e.to_compile_error().into(),
+ };
+
+ // Register the help request mapping
+ let help_entry = build_help_entry(&struct_name, &entry_type);
+ let entry_str = help_entry.to_string();
+ crate::HELP_REQUESTS.lock().unwrap().insert(entry_str);
+
+ quote! {}.into()
+}
diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs
index da3bf47..dbbc333 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -19,6 +19,7 @@ mod dispatcher;
mod dispatcher_clap;
mod enum_tag;
mod groupped;
+mod help;
mod node;
mod pack;
mod program_setup;
@@ -44,6 +45,8 @@ pub(crate) static CHAINS_EXIST: Lazy<Mutex<BTreeSet<String>>> =
Lazy::new(|| Mutex::new(BTreeSet::new()));
pub(crate) static RENDERERS_EXIST: Lazy<Mutex<BTreeSet<String>>> =
Lazy::new(|| Mutex::new(BTreeSet::new()));
+pub(crate) static HELP_REQUESTS: Lazy<Mutex<BTreeSet<String>>> =
+ Lazy::new(|| Mutex::new(BTreeSet::new()));
#[proc_macro]
pub fn node(input: TokenStream) -> TokenStream {
@@ -97,6 +100,16 @@ pub fn dispatcher_clap(attr: TokenStream, item: TokenStream) -> TokenStream {
dispatcher_clap::dispatcher_clap_attr(attr, item)
}
+#[proc_macro]
+pub fn register_help(input: TokenStream) -> TokenStream {
+ help::register_help(input)
+}
+
+#[proc_macro_attribute]
+pub fn help(_attr: TokenStream, item: TokenStream) -> TokenStream {
+ help::help_attr(item)
+}
+
#[proc_macro_derive(Groupped, attributes(group))]
pub fn derive_groupped(input: TokenStream) -> TokenStream {
groupped::derive_groupped(input)
@@ -308,6 +321,14 @@ pub fn program_final_gen(input: TokenStream) -> TokenStream {
#[cfg(not(feature = "comp"))]
let comp = quote! {};
+ let help_tokens: Vec<proc_macro2::TokenStream> = HELP_REQUESTS
+ .lock()
+ .unwrap()
+ .clone()
+ .iter()
+ .map(|s| syn::parse_str::<proc_macro2::TokenStream>(s).unwrap())
+ .collect();
+
let expanded = quote! {
#[derive(Debug, Default, PartialEq, Eq, Clone)]
#[repr(u32)]
@@ -340,6 +361,12 @@ pub fn program_final_gen(input: TokenStream) -> TokenStream {
::mingling::__dispatch_program_chains!(
#(#chain_tokens)*
);
+ fn render_help(any: ::mingling::AnyOutput<Self::Enum>, r: &mut ::mingling::RenderResult) {
+ match any.member_id {
+ #(#help_tokens)*
+ _ => (),
+ }
+ }
fn has_renderer(any: &::mingling::AnyOutput<Self::Enum>) -> bool {
match any.member_id {
#(#renderer_exist_tokens)*