diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-05-21 15:12:58 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-05-21 15:21:37 +0800 |
| commit | 74b5a80475e2230c0a494beac5ec86a985c2974f (patch) | |
| tree | 18892e7b6c0a0c6b4a3ac65fb497d7a55d5ae16f | |
| parent | 4704f6d54108bcc8f9b2fb7f4b3c4e224b4a7809 (diff) | |
Refactor REPL hooks into modular setups and add new hook types
| -rw-r--r-- | examples/example-repl/src/main.rs | 54 | ||||
| -rw-r--r-- | mingling/src/example_docs.rs | 68 | ||||
| -rw-r--r-- | mingling/src/setups.rs | 6 | ||||
| -rw-r--r-- | mingling/src/setups/repl_basic.rs | 86 | ||||
| -rw-r--r-- | mingling_core/src/program/hook.rs | 144 | ||||
| -rw-r--r-- | mingling_core/src/program/repl_exec.rs | 25 |
6 files changed, 316 insertions, 67 deletions
diff --git a/examples/example-repl/src/main.rs b/examples/example-repl/src/main.rs index dab5b15..2d8d9b0 100644 --- a/examples/example-repl/src/main.rs +++ b/examples/example-repl/src/main.rs @@ -1,4 +1,10 @@ -use mingling::{REPL, hook::ProgramHook, prelude::*, this}; +use mingling::{ + REPL, + hook::ProgramHook, + prelude::*, + setup::{BasicREPLOutputSetup, BasicREPLPromptSetup, BasicREPLReadlineSetup}, + this, +}; use std::{env::current_dir, path::PathBuf}; // Resource to store the current directory @@ -27,33 +33,27 @@ fn main() { program.with_dispatcher(ExitCommand); program.with_dispatcher(ClearCommand); + // Add setups + program.with_setup(BasicREPLReadlineSetup); + program.with_setup(BasicREPLOutputSetup); + program.with_setup(BasicREPLPromptSetup::func(|| { + let res = this::<ThisProgram>().res::<CurrentDir>().unwrap(); + let dir_str: String = res.dir.to_string_lossy().into(); + let prompt = format!( + "{}> ", + dir_str + .replace(&['/', '\\'][..], ">") + .trim_start_matches('>') + .trim_end_matches('>') + ); + prompt + })); + // Add hooks to handle REPL-related events - program.with_hook( - ProgramHook::empty() - .on_repl_begin(|| { - // Print welcome message - println!("Welcome!") - }) - .on_repl_pre_readline(|| { - // Print prompt - let res = this::<ThisProgram>().res::<CurrentDir>().unwrap(); - let dir_str: String = res.dir.to_string_lossy().into(); - let prompt = format!( - "{}> ", - dir_str - .replace(&['/', '\\'][..], ">") - .trim_start_matches('>') - .trim_end_matches('>') - ); - print!("{}", prompt) - }) - .on_repl_receive_result(|r| { - // Print output - if !r.is_empty() { - println!("{}", r.trim()) - } - }), - ); + program.with_hook(ProgramHook::empty().on_repl_begin(|| { + // Print welcome message + println!("Welcome!") + })); // Start the REPL loop program.exec_repl(); diff --git a/mingling/src/example_docs.rs b/mingling/src/example_docs.rs index ab725de..8c93f4b 100644 --- a/mingling/src/example_docs.rs +++ b/mingling/src/example_docs.rs @@ -584,7 +584,12 @@ pub mod example_picker {} /// /// main.rs /// ```ignore -/// use mingling::{REPL, hook::ProgramHook, prelude::*, this}; +/// use mingling::{ +/// hook::ProgramHook, +/// prelude::*, +/// setup::{BasicREPLOutputSetup, BasicREPLPromptSetup, BasicREPLReadlineSetup}, +/// this, REPL, +/// }; /// use std::{env::current_dir, path::PathBuf}; /// /// // Resource to store the current directory @@ -611,34 +616,29 @@ pub mod example_picker {} /// program.with_dispatcher(ChangeDirectoryCommand); /// program.with_dispatcher(ListCommand); /// program.with_dispatcher(ExitCommand); +/// program.with_dispatcher(ClearCommand); +/// +/// // Add setups +/// program.with_setup(BasicREPLReadlineSetup); +/// program.with_setup(BasicREPLOutputSetup); +/// program.with_setup(BasicREPLPromptSetup::func(|| { +/// let res = this::<ThisProgram>().res::<CurrentDir>().unwrap(); +/// let dir_str: String = res.dir.to_string_lossy().into(); +/// let prompt = format!( +/// "{}> ", +/// dir_str +/// .replace(&['/', '\\'][..], ">") +/// .trim_start_matches('>') +/// .trim_end_matches('>') +/// ); +/// prompt +/// })); /// /// // Add hooks to handle REPL-related events -/// program.with_hook( -/// ProgramHook::empty() -/// .on_repl_begin(|| { -/// // Print welcome message -/// println!("Welcome!") -/// }) -/// .on_repl_pre_readline(|| { -/// // Print prompt -/// let res = this::<ThisProgram>().res::<CurrentDir>().unwrap(); -/// let dir_str: String = res.dir.to_string_lossy().into(); -/// let prompt = format!( -/// "{}> ", -/// dir_str -/// .replace(&['/', '\\'][..], ">") -/// .trim_start_matches('>') -/// .trim_end_matches('>') -/// ); -/// print!("{}", prompt) -/// }) -/// .on_repl_receive_result(|r| { -/// // Print output -/// if !r.is_empty() { -/// println!("{}", r.trim()) -/// } -/// }), -/// ); +/// program.with_hook(ProgramHook::empty().on_repl_begin(|| { +/// // Print welcome message +/// println!("Welcome!") +/// })); /// /// // Start the REPL loop /// program.exec_repl(); @@ -651,6 +651,7 @@ pub mod example_picker {} /// dispatcher!("cd", ChangeDirectoryCommand => ChangeDirectoryEntry); /// dispatcher!("ls", ListCommand => ListEntry); /// dispatcher!("exit", ExitCommand => ExitEntry); +/// dispatcher!("clear", ClearCommand => ClearEntry); /// /// // Define data needed for the cd command's execution phase /// pack!(StateChangeDirectory = String); @@ -719,12 +720,25 @@ pub mod example_picker {} /// repl.exit = true; /// } /// +/// // Handle clear command event +/// #[chain] +/// fn handle_clear(_prev: ClearEntry) { +/// // Clear the terminal screen +/// print!("\x1B[2J\x1B[1;1H"); +/// } +/// /// // Handle path not found event /// #[renderer] /// fn render_error_directory_not_exist(err: ErrorDirectoryNotExist) { /// r_println!("Directory not found: {}", err.inner.display()) /// } /// +/// // Handle dispatcher not found event +/// #[renderer] +/// fn dispatcher_not_found(prev: DispatcherNotFound) { +/// r_println!("Command not found: \"{}\"", prev.join(", ")) +/// } +/// /// gen_program!(); /// ``` pub mod example_repl {} diff --git a/mingling/src/setups.rs b/mingling/src/setups.rs index 351112b..b4fad58 100644 --- a/mingling/src/setups.rs +++ b/mingling/src/setups.rs @@ -9,3 +9,9 @@ mod general_renderer; #[cfg(feature = "general_renderer")] pub use general_renderer::*; + +#[cfg(feature = "repl")] +mod repl_basic; + +#[cfg(feature = "repl")] +pub use repl_basic::*; diff --git a/mingling/src/setups/repl_basic.rs b/mingling/src/setups/repl_basic.rs new file mode 100644 index 0000000..5884489 --- /dev/null +++ b/mingling/src/setups/repl_basic.rs @@ -0,0 +1,86 @@ +use std::io::Write; + +use mingling_core::{Program, ProgramCollect, hook::ProgramHook, setup::ProgramSetup}; + +/// Provides basic Readline capability for the REPL. +pub struct BasicREPLReadlineSetup; +impl<C> ProgramSetup<C> for BasicREPLReadlineSetup +where + C: ProgramCollect<Enum = C>, +{ + fn setup(&mut self, program: &mut Program<C>) { + program.with_hook(ProgramHook::empty().on_repl_readline(|| readline().ok())); + } +} + +/// A basic REPL prompt that displays a prompt string and reads input from the user. +/// +/// **Note:** This setup uses static [`OnceLock`](std::sync::OnceLock) internally, +/// meaning only the last configured instance will take effect globally. +/// Do not configure multiple prompts with different values — only one will be used. +pub enum BasicREPLPromptSetup { + Prompt(String), + Func(fn() -> String), +} + +impl BasicREPLPromptSetup { + /// Creates a new [`BasicREPLPrompt`] with the given prompt string. + pub fn simple(prompt: impl Into<String>) -> Self { + Self::Prompt(prompt.into()) + } + + /// Creates a new [`BasicREPLPrompt`] with the given function. + pub fn func(func: fn() -> String) -> Self { + Self::Func(func) + } +} + +impl<C> ProgramSetup<C> for BasicREPLPromptSetup +where + C: ProgramCollect<Enum = C>, +{ + fn setup(&mut self, program: &mut Program<C>) { + match self { + BasicREPLPromptSetup::Prompt(prompt) => { + static PROMPT: std::sync::OnceLock<String> = std::sync::OnceLock::new(); + let _ = PROMPT.set(prompt.clone()); + fn print_prompt() { + print!("{}", PROMPT.get().unwrap()); + let _ = std::io::stdout().flush(); + } + program.with_hook(ProgramHook::empty().on_repl_pre_readline(print_prompt)); + } + BasicREPLPromptSetup::Func(f) => { + static FUNC: std::sync::OnceLock<fn() -> String> = std::sync::OnceLock::new(); + let _ = FUNC.set(*f); + fn print_func_prompt() { + print!("{}", FUNC.get().unwrap()()); + let _ = std::io::stdout().flush(); + } + program.with_hook(ProgramHook::empty().on_repl_pre_readline(print_func_prompt)); + } + } + } +} + +/// Prints the result of each REPL command to stdout. +pub struct BasicREPLOutputSetup; +impl<C> ProgramSetup<C> for BasicREPLOutputSetup +where + C: ProgramCollect<Enum = C>, +{ + fn setup(&mut self, program: &mut Program<C>) { + program.with_hook(ProgramHook::empty().on_repl_receive_result(|r| { + if !r.is_empty() { + println!("{}", r.trim()) + } + })); + } +} + +fn readline() -> Result<String, std::io::Error> { + let mut input = String::new(); + std::io::stdout().flush()?; + std::io::stdin().read_line(&mut input)?; + Ok(input.trim().to_string()) +} diff --git a/mingling_core/src/program/hook.rs b/mingling_core/src/program/hook.rs index 8e231f1..f93b022 100644 --- a/mingling_core/src/program/hook.rs +++ b/mingling_core/src/program/hook.rs @@ -48,10 +48,22 @@ where #[cfg(feature = "repl")] pub repl_pre_readline: Option<fn()>, + /// Custom REPL line reader (only available with `repl` feature) + #[cfg(feature = "repl")] + pub repl_readline: Option<fn() -> Option<String>>, + /// Executes after reading a REPL line (only available with `repl` feature) #[cfg(feature = "repl")] pub repl_post_readline: Option<fn(line: &str)>, + /// Executes before executing a REPL command (only available with `repl` feature) + #[cfg(feature = "repl")] + pub repl_pre_exec: Option<fn(args: &[String])>, + + /// Executes after executing a REPL command (only available with `repl` feature) + #[cfg(feature = "repl")] + pub repl_post_exec: Option<fn()>, + /// Executes when the REPL receives a render result (only available with `repl` feature) #[cfg(feature = "repl")] pub repl_on_receive_result: Option<fn(&RenderResult)>, @@ -59,6 +71,14 @@ where /// Executes when the REPL panics (only available with `repl` feature) #[cfg(all(feature = "repl", not(feature = "async")))] pub repl_on_panic: Option<fn(&ProgramPanic)>, + + /// Executes when the REPL exits (only available with `repl` feature) + #[cfg(feature = "repl")] + pub repl_exit: Option<fn()>, + + /// Executes after each REPL loop iteration (only available with `repl` feature) + #[cfg(feature = "repl")] + pub repl_loop_once: Option<fn()>, } impl<C> Program<C> @@ -214,6 +234,22 @@ where } } + /// Runs the custom REPL readline hook (only available with `repl` feature) + /// Returns `Some(line)` if a hook was set and returned Some, otherwise `None`. + #[cfg(feature = "repl")] + pub(crate) fn run_hook_repl_readline(&self) -> Option<String> { + if !self.user_context.run_hook { + return None; + } + + for hook in &self.hooks { + if let Some(repl_readline) = hook.repl_readline { + return repl_readline(); + } + } + None + } + /// Runs the REPL post-readline hooks (only available with `repl` feature) #[cfg(feature = "repl")] pub(crate) fn run_hook_repl_post_readline(&self, line: &str) { @@ -228,6 +264,34 @@ where } } + /// Runs the REPL pre-exec hooks (only available with `repl` feature) + #[cfg(feature = "repl")] + pub(crate) fn run_hook_repl_pre_exec(&self, args: &[String]) { + if !self.user_context.run_hook { + return; + } + + for hook in &self.hooks { + if let Some(repl_pre_exec) = hook.repl_pre_exec { + repl_pre_exec(args) + } + } + } + + /// Runs the REPL post-exec hooks (only available with `repl` feature) + #[cfg(feature = "repl")] + pub(crate) fn run_hook_repl_post_exec(&self) { + if !self.user_context.run_hook { + return; + } + + for hook in &self.hooks { + if let Some(repl_post_exec) = hook.repl_post_exec { + repl_post_exec() + } + } + } + /// Runs the REPL receive result hooks (only available with `repl` feature) #[cfg(feature = "repl")] pub(crate) fn run_hook_repl_on_receive_result(&self, result: &RenderResult) { @@ -255,6 +319,34 @@ where } } } + + /// Runs the REPL exit hooks (only available with `repl` feature) + #[cfg(feature = "repl")] + pub(crate) fn run_hook_repl_exit(&self) { + if !self.user_context.run_hook { + return; + } + + for hook in &self.hooks { + if let Some(repl_exit) = hook.repl_exit { + repl_exit() + } + } + } + + /// Runs the REPL loop_once hooks (only available with `repl` feature) + #[cfg(feature = "repl")] + pub(crate) fn run_hook_repl_loop_once(&self) { + if !self.user_context.run_hook { + return; + } + + for hook in &self.hooks { + if let Some(repl_loop_once) = hook.repl_loop_once { + repl_loop_once() + } + } + } } impl<C> ProgramHook<C> @@ -279,11 +371,21 @@ where #[cfg(feature = "repl")] repl_pre_readline: None, #[cfg(feature = "repl")] + repl_readline: None, + #[cfg(feature = "repl")] repl_post_readline: None, #[cfg(feature = "repl")] + repl_pre_exec: None, + #[cfg(feature = "repl")] + repl_post_exec: None, + #[cfg(feature = "repl")] repl_on_receive_result: None, #[cfg(all(feature = "repl", not(feature = "async")))] repl_on_panic: None, + #[cfg(feature = "repl")] + repl_exit: None, + #[cfg(feature = "repl")] + repl_loop_once: None, } } @@ -357,6 +459,15 @@ where self } + /// Sets the custom REPL line reader (only available with `repl` feature). + /// If set, this function will be called to read a line instead of the default mechanism. + /// Returning `None` signals that there is no input (e.g., EOF). + #[cfg(feature = "repl")] + pub fn on_repl_readline(mut self, handler: fn() -> Option<String>) -> Self { + let _ = self.repl_readline.insert(handler); + self + } + /// Sets the handler for the REPL post-readline event (only available with `repl` feature). /// This hook runs after reading a line of input and receives the line as a `&str`. #[cfg(feature = "repl")] @@ -365,7 +476,24 @@ where self } + /// Sets the handler for the REPL pre-exec event (only available with `repl` feature). + /// This hook runs before executing a REPL command, receiving the parsed arguments. + #[cfg(feature = "repl")] + pub fn on_repl_pre_exec(mut self, handler: fn(args: &[String])) -> Self { + let _ = self.repl_pre_exec.insert(handler); + self + } + + /// Sets the handler for the REPL post-exec event (only available with `repl` feature). + /// This hook runs after executing a REPL command. + #[cfg(feature = "repl")] + pub fn on_repl_post_exec(mut self, handler: fn()) -> Self { + let _ = self.repl_post_exec.insert(handler); + self + } + /// Sets the handler for the REPL receive result event (only available with `repl` feature). + /// This hook runs after a command is executed, receiving the render result on success. #[cfg(feature = "repl")] pub fn on_repl_receive_result(mut self, handler: fn(result: &RenderResult)) -> Self { let _ = self.repl_on_receive_result.insert(handler); @@ -378,4 +506,20 @@ where let _ = self.repl_on_panic.insert(handler); self } + + /// Sets the handler for the REPL exit event (only available with `repl` feature). + /// This hook runs when the REPL is about to exit. + #[cfg(feature = "repl")] + pub fn on_repl_exit(mut self, handler: fn()) -> Self { + let _ = self.repl_exit.insert(handler); + self + } + + /// Sets the handler for the REPL loop_once event (only available with `repl` feature). + /// This hook runs after each REPL loop iteration. + #[cfg(feature = "repl")] + pub fn on_repl_loop_once(mut self, handler: fn()) -> Self { + let _ = self.repl_loop_once.insert(handler); + self + } } diff --git a/mingling_core/src/program/repl_exec.rs b/mingling_core/src/program/repl_exec.rs index 5417252..c4232ab 100644 --- a/mingling_core/src/program/repl_exec.rs +++ b/mingling_core/src/program/repl_exec.rs @@ -31,11 +31,12 @@ where self.exec_wrapper(|p| -> () { loop { p.run_hook_repl_pre_readline(); - let readline = readline_or_empty(); + let readline = p.run_hook_repl_readline().unwrap_or_default(); p.run_hook_repl_post_readline(&readline); let args = split_input_string(readline.clone()); + p.run_hook_repl_pre_exec(&args); match exec_once(p, args) { Ok(r) => { p.run_hook_repl_on_receive_result(&r); @@ -45,10 +46,14 @@ where } _ => {} } + p.run_hook_repl_post_exec(); if this::<C>().res::<REPL>().unwrap().exit { + p.run_hook_repl_exit(); break; } + + p.run_hook_repl_loop_once(); } }); } @@ -75,38 +80,32 @@ where self.exec_wrapper(async |p| -> () { loop { p.run_hook_repl_pre_readline(); - let readline = readline_or_empty(); + let readline = p.run_hook_repl_readline().unwrap_or_default(); p.run_hook_repl_post_readline(&readline); let args = split_input_string(readline.clone()); + p.run_hook_repl_pre_exec(&args); match exec_once(p, args).await { Ok(r) => { p.run_hook_repl_on_receive_result(&r); } _ => {} } + p.run_hook_repl_post_exec(); if this::<C>().res::<REPL>().unwrap().exit { + p.run_hook_repl_exit(); break; } + + p.run_hook_repl_loop_once(); } }) .await; } } -fn readline() -> Result<String, std::io::Error> { - let mut input = String::new(); - std::io::stdout().flush()?; - std::io::stdin().read_line(&mut input)?; - Ok(input.trim().to_string()) -} - -fn readline_or_empty() -> String { - readline().unwrap_or("".to_string()) -} - #[cfg(not(feature = "async"))] fn exec_once<C>( p: &'static Program<C>, |
