aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-05-21 15:12:58 +0800
committer魏曹先生 <1992414357@qq.com>2026-05-21 15:21:37 +0800
commit74b5a80475e2230c0a494beac5ec86a985c2974f (patch)
tree18892e7b6c0a0c6b4a3ac65fb497d7a55d5ae16f
parent4704f6d54108bcc8f9b2fb7f4b3c4e224b4a7809 (diff)
Refactor REPL hooks into modular setups and add new hook types
-rw-r--r--examples/example-repl/src/main.rs54
-rw-r--r--mingling/src/example_docs.rs68
-rw-r--r--mingling/src/setups.rs6
-rw-r--r--mingling/src/setups/repl_basic.rs86
-rw-r--r--mingling_core/src/program/hook.rs144
-rw-r--r--mingling_core/src/program/repl_exec.rs25
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>,