aboutsummaryrefslogtreecommitdiff
path: root/mingling_core
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_core')
-rw-r--r--mingling_core/Cargo.toml1
-rw-r--r--mingling_core/src/any.rs15
-rw-r--r--mingling_core/src/any/group.rs24
-rw-r--r--mingling_core/src/asset/dispatcher.rs40
-rw-r--r--mingling_core/src/asset/global_resource.rs6
-rw-r--r--mingling_core/src/comp/shell_ctx.rs34
-rw-r--r--mingling_core/src/comp/suggest.rs8
-rw-r--r--mingling_core/src/lib.rs14
-rw-r--r--mingling_core/src/program.rs20
-rw-r--r--mingling_core/src/program/collection.rs8
-rw-r--r--mingling_core/src/program/hook.rs3
-rw-r--r--mingling_core/src/program/once_exec.rs38
-rw-r--r--mingling_core/src/program/setup.rs3
-rw-r--r--mingling_core/src/renderer/render_result.rs502
-rw-r--r--mingling_core/src/renderer/structural.rs16
-rw-r--r--mingling_core/src/renderer/structural/error.rs1
-rw-r--r--mingling_core/src/renderer/structural/structural_data.rs8
-rw-r--r--mingling_core/tests/test-all/tests/integration.rs17
-rw-r--r--mingling_core/tests/test-basic/tests/integration.rs2
-rw-r--r--mingling_core/tests/test-dispatch-tree/tests/integration.rs2
-rw-r--r--mingling_core/tests/test-repl/tests/integration.rs2
-rw-r--r--mingling_core/tests/test-structural-renderer/tests/integration.rs8
22 files changed, 621 insertions, 151 deletions
diff --git a/mingling_core/Cargo.toml b/mingling_core/Cargo.toml
index 85afd55..14140c6 100644
--- a/mingling_core/Cargo.toml
+++ b/mingling_core/Cargo.toml
@@ -15,6 +15,7 @@ nightly = []
default = []
async = []
builds = []
+picker = []
dispatch_tree = []
structural_renderer = ["dep:serde"]
diff --git a/mingling_core/src/any.rs b/mingling_core/src/any.rs
index e6b7406..ec29a1b 100644
--- a/mingling_core/src/any.rs
+++ b/mingling_core/src/any.rs
@@ -17,7 +17,18 @@ pub mod group;
#[derive(Debug)]
pub struct AnyOutput<G> {
pub(crate) inner: Box<dyn std::any::Any + Send + 'static>,
+
+ /// The [`TypeId`] of the concrete type stored in `inner`.
+ ///
+ /// This is set during construction and used for type-checking
+ /// in downcast, restore, and is methods.
pub type_id: std::any::TypeId,
+
+ /// The variant identifier returned by [`Grouped::member_id`] for the
+ /// concrete type stored in `inner`.
+ ///
+ /// This is used by the scheduler to dispatch on the correct enum
+ /// variant when routing the output.
pub member_id: G,
}
@@ -106,7 +117,9 @@ impl<G> std::ops::DerefMut for AnyOutput<G> {
/// - Returns `Ok((`[`AnyOutput`](./struct.AnyOutput.html)`, `[`NextProcess::Renderer`](./enum.NextProcess.html)`))` to render this type next and output to the terminal
/// - Returns `Err(`[`ChainProcessError`](./error/enum.ChainProcessError.html)`]` to terminate the program directly
pub enum ChainProcess<G> {
+ /// Indicates success, containing the output value and the next step to execute.
Ok((AnyOutput<G>, NextProcess)),
+ /// Indicates a processing failure, containing the error that occurred.
Err(ChainProcessError),
}
@@ -117,7 +130,9 @@ pub enum ChainProcess<G> {
#[derive(Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum NextProcess {
+ /// Continue execution to the next chain
Chain,
+ /// Send output to renderer and end execution
Renderer,
}
diff --git a/mingling_core/src/any/group.rs b/mingling_core/src/any/group.rs
index 90ef9fc..2813ad5 100644
--- a/mingling_core/src/any/group.rs
+++ b/mingling_core/src/any/group.rs
@@ -11,26 +11,6 @@ where
{
/// Returns the specific enum value representing its ID within that enum
fn member_id() -> Group;
-
- /// Converts the grouped item into a `ChainProcess` directed to the chain route.
- ///
- /// This wraps the item into an `AnyOutput` and routes it to the chain processing pipeline.
- fn to_chain(self) -> ChainProcess<Group>
- where
- Self: Send,
- {
- AnyOutput::new(self).route_chain()
- }
-
- /// Converts the grouped item into a `ChainProcess` directed to the render route.
- ///
- /// This wraps the item into an `AnyOutput` and routes it to the render processing pipeline.
- fn to_render(self) -> ChainProcess<Group>
- where
- Self: Send,
- {
- AnyOutput::new(self).route_renderer()
- }
}
impl<T, C> Routable<C> for T
@@ -39,10 +19,10 @@ where
T: Grouped<C> + Send,
{
fn to_chain(self) -> ChainProcess<C> {
- T::to_chain(self)
+ AnyOutput::new(self).route_chain()
}
fn to_render(self) -> ChainProcess<C> {
- T::to_render(self)
+ AnyOutput::new(self).route_renderer()
}
}
diff --git a/mingling_core/src/asset/dispatcher.rs b/mingling_core/src/asset/dispatcher.rs
index 8f04955..cb0987d 100644
--- a/mingling_core/src/asset/dispatcher.rs
+++ b/mingling_core/src/asset/dispatcher.rs
@@ -32,22 +32,48 @@ where
C: ProgramCollect<Enum = C>,
{
/// Adds a dispatcher to the program.
- #[cfg(not(feature = "dispatch_tree"))]
- pub fn with_dispatcher<Disp>(&mut self, dispatcher: Disp)
+ #[cfg_attr(
+ feature = "dispatch_tree",
+ deprecated(
+ note = "When the `dispatch_tree` feature is enabled, the `dispatcher` field no longer exists inside Program. All types are collected at compile time by the `gen_program!()` macro, so the `with_dispatcher` function is no longer needed"
+ )
+ )]
+ pub fn with_dispatcher<Disp>(&mut self, dispatcher: Disp) -> &mut Self
where
Disp: Dispatcher<C> + Send + Sync + 'static,
{
- self.dispatcher.push(Box::new(dispatcher));
+ #[cfg(not(feature = "dispatch_tree"))]
+ {
+ self.dispatcher.push(Box::new(dispatcher));
+ }
+ #[cfg(feature = "dispatch_tree")]
+ {
+ let _ = dispatcher;
+ }
+ self
}
/// Add some dispatchers to the program.
- #[cfg(not(feature = "dispatch_tree"))]
- pub fn with_dispatchers<D>(&mut self, dispatchers: D)
+ #[cfg_attr(
+ feature = "dispatch_tree",
+ deprecated(
+ note = "When the `dispatch_tree` feature is enabled, the `dispatcher` field no longer exists inside Program. All types are collected at compile time by the `gen_program!()` macro, so the `with_dispatcher` function is no longer needed"
+ )
+ )]
+ pub fn with_dispatchers<D>(&mut self, dispatchers: D) -> &mut Self
where
D: Into<Dispatchers<C>>,
{
- let dispatchers = dispatchers.into();
- self.dispatcher.extend(dispatchers.dispatcher);
+ #[cfg(not(feature = "dispatch_tree"))]
+ {
+ let dispatchers = dispatchers.into();
+ self.dispatcher.extend(dispatchers.dispatcher);
+ }
+ #[cfg(feature = "dispatch_tree")]
+ {
+ let _ = dispatchers;
+ }
+ self
}
}
diff --git a/mingling_core/src/asset/global_resource.rs b/mingling_core/src/asset/global_resource.rs
index 29e1136..a610378 100644
--- a/mingling_core/src/asset/global_resource.rs
+++ b/mingling_core/src/asset/global_resource.rs
@@ -13,10 +13,14 @@ where
C: ProgramCollect<Enum = C>,
{
/// Insert a resource of the given type, cloning the provided value into the store
- pub fn with_resource<Res: 'static + Send + Sync + ResourceMarker>(&mut self, res: Res) {
+ pub fn with_resource<Res: 'static + Send + Sync + ResourceMarker>(
+ &mut self,
+ res: Res,
+ ) -> &mut Self {
if let Ok(mut guard) = self.resources.lock() {
guard.insert(TypeId::of::<Res>(), Box::new(Arc::new(res)));
}
+ self
}
/// Modify a resource by type, applying a closure to the resource if present
diff --git a/mingling_core/src/comp/shell_ctx.rs b/mingling_core/src/comp/shell_ctx.rs
index cfa4700..1fca325 100644
--- a/mingling_core/src/comp/shell_ctx.rs
+++ b/mingling_core/src/comp/shell_ctx.rs
@@ -1,3 +1,5 @@
+#![allow(deprecated)]
+
use std::collections::HashSet;
use crate::{Flag, ShellFlag, Suggest};
@@ -115,6 +117,13 @@ impl ShellContext {
/// // }
/// }
/// ```
+ #[must_use]
+ #[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When using the `picker` feature, this method does not work under all ParserStyle settings"
+ )
+ )]
pub fn filling_argument_first(&self, flag: impl Into<Flag>) -> bool {
let flag = flag.into();
if self.filling_argument(&flag) {
@@ -153,6 +162,13 @@ impl ShellContext {
/// // }
/// }
/// ```
+ #[must_use]
+ #[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When using the `picker` feature, this method does not work under all ParserStyle settings"
+ )
+ )]
pub fn filling_argument(&self, flag: impl Into<Flag>) -> bool {
for f in flag.into().iter() {
if self.previous_word == **f {
@@ -190,6 +206,12 @@ impl ShellContext {
/// }
/// ```
#[must_use]
+ #[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When using the `picker` feature, this method does not work under all ParserStyle settings"
+ )
+ )]
pub fn typing_argument(&self) -> bool {
#[cfg(target_os = "windows")]
{
@@ -208,6 +230,12 @@ impl ShellContext {
/// when the user has already typed certain flags. The method processes both
/// regular suggestion sets and file completion suggestions differently.
#[must_use]
+ #[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When using the `picker` feature, this method does not work under all ParserStyle settings"
+ )
+ )]
pub fn strip_typed_argument(&self, suggest: Suggest) -> Suggest {
let typed = Self::get_typed_arguments(self);
match suggest {
@@ -225,6 +253,12 @@ impl ShellContext {
/// which typically represent command-line flags or options. It returns a vector
/// containing these flag strings, converted to owned `String` values.
#[must_use]
+ #[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When using the `picker` feature, this method does not work under all ParserStyle settings"
+ )
+ )]
pub fn get_typed_arguments(&self) -> HashSet<String> {
self.all_words
.iter()
diff --git a/mingling_core/src/comp/suggest.rs b/mingling_core/src/comp/suggest.rs
index a81de64..99afc54 100644
--- a/mingling_core/src/comp/suggest.rs
+++ b/mingling_core/src/comp/suggest.rs
@@ -1,3 +1,5 @@
+#![allow(deprecated)]
+
use std::collections::BTreeSet;
use crate::ShellContext;
@@ -30,6 +32,12 @@ impl Suggest {
/// Filters out already typed flag arguments from suggestion results.
#[must_use]
+ #[cfg_attr(
+ feature = "picker",
+ deprecated(
+ note = "When using the `picker` feature, this method does not work under all ParserStyle settings"
+ )
+ )]
pub fn strip_typed_argument(self, ctx: &ShellContext) -> Self {
ctx.strip_typed_argument(self)
}
diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs
index 4996b19..31476c9 100644
--- a/mingling_core/src/lib.rs
+++ b/mingling_core/src/lib.rs
@@ -8,6 +8,8 @@
//!
//! Recommended to import [mingling](https://crates.io/crates/mingling) to use its features.
+#![deny(missing_docs)]
+
mod any;
mod asset;
mod program;
@@ -77,6 +79,10 @@ pub mod comp;
#[cfg(feature = "comp")]
pub use crate::comp::*;
+/// Module for setting up a `Mingling` program.
+///
+/// This module provides the [`ProgramSetup`] type, which allows users to configure
+/// and initialize the program's execution environment.
pub mod setup {
pub use crate::program::setup::ProgramSetup;
}
@@ -84,8 +90,14 @@ pub mod setup {
/// Private API — not intended for direct use.
#[doc(hidden)]
pub mod __private {
+ use crate::ProgramCollect;
+
/// Sealed trait for `StructuralData` — only implementable via derive macro.
- pub trait StructuralDataSealed {}
+ pub trait StructuralDataSealed<C>
+ where
+ C: ProgramCollect<Enum = C>,
+ {
+ }
/// Re-export so the derive macro can reference the trait without
/// conflicting with the derive macro name at `::mingling::StructuralData`.
diff --git a/mingling_core/src/program.rs b/mingling_core/src/program.rs
index 0d409b7..11e1bbf 100644
--- a/mingling_core/src/program.rs
+++ b/mingling_core/src/program.rs
@@ -53,10 +53,30 @@ where
#[cfg(not(feature = "dispatch_tree"))]
pub(crate) dispatcher: Vec<Box<dyn Dispatcher<C> + Send + Sync>>,
+ /// Program stdout settings.
+ ///
+ /// This struct controls the program's output behavior, including whether
+ /// to output errors, render results, suppress panic messages, enable
+ /// verbose/quiet/debug modes, colored output, and progress indicators.
+ ///
+ /// # Convention-only fields
+ ///
+ /// Fields marked with convention only are not enforced by the framework;
+ /// they serve as a shared convention for applications to follow consistently.
pub stdout_setting: ProgramStdoutSetting,
+
+ /// User-defined context that can be accessed throughout the program.
+ ///
+ /// This field holds a user-defined context value that can be
+ /// accessed and modified at any point during program execution. It is
+ /// useful for passing shared state or configuration across different
+ /// parts of the program.
pub user_context: ProgramUserContext,
#[cfg(feature = "structural_renderer")]
+ /// Setting for the structural renderer.
+ ///
+ /// This field is only available when the `structural_renderer` feature is enabled.
pub structural_renderer_name: StructuralRendererSetting,
pub(crate) hooks: Vec<ProgramHook<C>>,
diff --git a/mingling_core/src/program/collection.rs b/mingling_core/src/program/collection.rs
index fe78979..14705ac 100644
--- a/mingling_core/src/program/collection.rs
+++ b/mingling_core/src/program/collection.rs
@@ -21,8 +21,16 @@ pub use mock::*;
pub trait ProgramCollect {
/// Enum type representing internal IDs for the program
type Enum;
+ /// Error type when a dispatcher is not found for the given member
type ErrorDispatcherNotFound: Grouped<Self::Enum>;
+
+ /// Error type when a renderer is not found for the given member
type ErrorRendererNotFound: Grouped<Self::Enum>;
+
+ /// Result type for an empty chain result
+ ///
+ /// When the `extra_macros` feature is enabled,
+ /// you can use the `empty_result!()` macro to create this
type ResultEmpty: Grouped<Self::Enum>;
/// Use a prefix tree to quickly match arguments and dispatch to an Entry
diff --git a/mingling_core/src/program/hook.rs b/mingling_core/src/program/hook.rs
index db1691b..48f632f 100644
--- a/mingling_core/src/program/hook.rs
+++ b/mingling_core/src/program/hook.rs
@@ -144,8 +144,9 @@ where
{
/// Adds a typed hook to the program. The hook will be called at the appropriate
/// lifecycle events.
- pub fn with_hook(&mut self, hook: ProgramHook<C>) {
+ pub fn with_hook(&mut self, hook: ProgramHook<C>) -> &mut Self {
self.hooks.push(hook);
+ self
}
pub(crate) fn run_hook_on_begin(&self, info: HookBeginInfo) {
diff --git a/mingling_core/src/program/once_exec.rs b/mingling_core/src/program/once_exec.rs
index 9d6f1e4..a846d04 100644
--- a/mingling_core/src/program/once_exec.rs
+++ b/mingling_core/src/program/once_exec.rs
@@ -79,23 +79,12 @@ where
};
// Read exit code
- let exit_code = result.exit_code;
-
// Render result
- if stdout_setting.render_output && !result.is_empty() {
- print!("{result}");
-
- if let Err(e) = std::io::Write::flush(&mut std::io::stdout())
- && stdout_setting.error_output
- {
- eprintln!("{e}");
- 1
- } else {
- exit_code
- }
- } else {
- exit_code
+ if stdout_setting.render_output {
+ result.std_print();
}
+
+ result.exit_code
}
/// Run the command line program, then exit
@@ -217,23 +206,12 @@ where
};
// Read exit code
- let exit_code = result.exit_code;
-
// Render result
- if stdout_setting.render_output && !result.is_empty() {
- print!("{result}");
-
- if let Err(e) = std::io::Write::flush(&mut std::io::stdout())
- && stdout_setting.error_output
- {
- eprintln!("{e}");
- 1
- } else {
- exit_code
- }
- } else {
- exit_code
+ if stdout_setting.render_output {
+ result.std_print();
}
+
+ result.exit_code
}
/// Run the command line program, then exit
diff --git a/mingling_core/src/program/setup.rs b/mingling_core/src/program/setup.rs
index 838c29a..a8ff114 100644
--- a/mingling_core/src/program/setup.rs
+++ b/mingling_core/src/program/setup.rs
@@ -29,8 +29,9 @@ where
C: ProgramCollect<Enum = C>,
{
/// Load and execute init logic
- pub fn with_setup<S: ProgramSetup<C> + 'static>(&mut self, setup: S) {
+ pub fn with_setup<S: ProgramSetup<C> + 'static>(&mut self, setup: S) -> &mut Self {
S::setup(setup, self);
+ self
}
}
diff --git a/mingling_core/src/renderer/render_result.rs b/mingling_core/src/renderer/render_result.rs
index e57a5b9..c38522a 100644
--- a/mingling_core/src/renderer/render_result.rs
+++ b/mingling_core/src/renderer/render_result.rs
@@ -1,22 +1,67 @@
use std::{
fmt::{Display, Formatter},
io::Write,
- ops::Deref,
};
+use crate::RenderResultMode::{Stderr, Stdout};
+
/// Render result, containing the rendered text content.
#[derive(Default, Debug, PartialEq)]
pub struct RenderResult {
- render_text: String,
+ /// Whether the output should be written immediately.
+ ///
+ /// When set to `true`, rendered content will be flushed to stdout/stderr
+ /// in real time while also being collected in the render buffer.
+ immediate_output: bool,
+
+ /// The buffered render output, stored as a list of (text, mode) pairs.
+ ///
+ /// Each entry contains a rendered string together with a `RenderResultMode`
+ /// indicating whether it should be output to stdout or stderr.
+ render_buffer: Vec<(String, RenderResultMode)>,
+
+ /// The exit code to return from the rendering process.
+ ///
+ /// A value of `0` indicates success, while non-zero values indicate
+ /// various error conditions.
pub exit_code: i32,
}
+/// Enum representing the output mode for render results.
+///
+/// This determines whether the rendered content should be directed to standard
+/// output or standard error.
+///
+/// # Variants
+///
+/// * `Stdout` - Output will be written to standard output (stdout).
+/// * `Stderr` - Output will be written to standard error (stderr).
+#[repr(u8)]
+#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
+pub enum RenderResultMode {
+ /// Standard output (stdout).
+ #[default]
+ Stdout = 0,
+
+ /// Standard error (stderr).
+ Stderr = 1,
+}
+
+impl<F> From<F> for RenderResult
+where
+ F: FnOnce() -> RenderResult,
+{
+ fn from(value: F) -> Self {
+ value()
+ }
+}
+
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);
+ self.append_to_buffer(s, Stdout);
Ok(buf.len())
}
@@ -27,15 +72,7 @@ impl Write for RenderResult {
impl Display for RenderResult {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- writeln!(f, "{}", self.render_text.trim())
- }
-}
-
-impl Deref for RenderResult {
- type Target = str;
-
- fn deref(&self) -> &Self::Target {
- &self.render_text
+ write!(f, "{}", render_result_to_string(self).trim())
}
}
@@ -64,40 +101,31 @@ impl_from_int!(i32, i16, i8, u32, u16, u8, usize);
impl From<&String> for RenderResult {
fn from(value: &String) -> Self {
- RenderResult {
- render_text: value.clone(),
- exit_code: 0,
- }
+ string_to_render_result(value, Stdout)
}
}
impl From<String> for RenderResult {
fn from(value: String) -> Self {
- RenderResult {
- render_text: value,
- exit_code: 0,
- }
+ string_to_render_result(value, Stdout)
}
}
impl From<&str> for RenderResult {
fn from(value: &str) -> Self {
- RenderResult {
- render_text: value.to_string(),
- exit_code: 0,
- }
+ string_to_render_result(value, Stdout)
}
}
impl From<RenderResult> for String {
fn from(result: RenderResult) -> Self {
- result.render_text
+ render_result_to_string(&result)
}
}
impl From<&RenderResult> for String {
fn from(result: &RenderResult) -> Self {
- result.render_text.clone()
+ render_result_to_string(result)
}
}
@@ -119,21 +147,139 @@ impl RenderResult {
Self::default()
}
+ /// Marks the render result for immediate output, bypassing any buffering or
+ /// deferred rendering.
+ ///
+ /// When set, the rendered content will be both collected in the result and
+ /// immediately flushed to stdout/stderr in real time, rather than being
+ /// deferred for later display.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.immediate_output();
+ /// ```
+ pub fn immediate_output(&mut self) -> &mut Self {
+ self.immediate_output = true;
+ self
+ }
+
+ /// Appends the given text and mode to the render buffer.
+ ///
+ /// Unlike `print` and `println` which only store plain text in a single string,
+ /// this method stores the text along with a `RenderResultMode` that indicates
+ /// whether the output should be directed to stdout or stderr. This allows for
+ /// more fine-grained control over output routing when the buffer is later flushed.
+ ///
+ /// # Arguments
+ ///
+ /// * `text` - The text content to append to the buffer.
+ /// * `mode` - The output mode (`Stdout` or `Stderr`) indicating where the text
+ /// should be directed.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::{RenderResult, RenderResultMode};
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.append_to_buffer("Hello", RenderResultMode::Stdout);
+ /// result.append_to_buffer("Error message", RenderResultMode::Stderr);
+ /// ```
+ pub fn append_to_buffer(&mut self, text: impl Into<String>, mode: RenderResultMode) {
+ self.render_buffer.push((text.into(), mode));
+ }
+
+ /// Appends the given text followed by a newline, along with the mode, to the render buffer.
+ ///
+ /// This is a convenience method that calls `append_to_buffer` for the text and then
+ /// appends a newline with the same mode.
+ ///
+ /// # Arguments
+ ///
+ /// * `text` - The text content to append to the buffer.
+ /// * `mode` - The output mode (`Stdout` or `Stderr`) indicating where the text
+ /// should be directed.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::{RenderResult, RenderResultMode};
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.append_line_to_buffer("Hello", RenderResultMode::Stdout);
+ /// result.append_line_to_buffer("Warning", RenderResultMode::Stderr);
+ /// ```
+ pub fn append_line_to_buffer(&mut self, text: impl Into<String>, mode: RenderResultMode) {
+ self.append_to_buffer(text, mode);
+ self.append_to_buffer("\n", mode);
+ }
+
+ /// Appends the contents of another `RenderResult` to this one.
+ ///
+ /// If this `RenderResult` has `immediate_output` enabled but the other does not,
+ /// the other's content will be immediately flushed to the appropriate output stream
+ /// (stdout/stderr) while also being appended to the render buffer.
+ ///
+ /// The `exit_code` of the other result is **not** transferred — only the buffered
+ /// content and the `immediate_output` flag of the other result are merged.
+ ///
+ /// # Arguments
+ ///
+ /// * `other` - The `RenderResult` whose contents should be appended.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::{RenderResult, RenderResultMode};
+ ///
+ /// let mut dest = RenderResult::default();
+ /// let mut src = RenderResult::default();
+ ///
+ /// src.append_to_buffer("Hello", RenderResultMode::Stdout);
+ /// src.append_to_buffer(" Error", RenderResultMode::Stderr);
+ ///
+ /// dest.append_other(src);
+ /// assert_eq!(dest.to_string(), "Hello Error");
+ /// ```
+ pub fn append_other(&mut self, other: impl Into<RenderResult>) {
+ let other = other.into();
+
+ // If self has immediate output enabled, but the input does not, the input needs immediate output.
+ let immediate_output = !other.immediate_output && self.immediate_output;
+
+ for i in other.render_buffer {
+ if immediate_output {
+ match &i.1 {
+ Stdout => print!("{}", i.0),
+ Stderr => eprint!("{}", i.0),
+ }
+ }
+ self.render_buffer.push(i);
+ }
+ }
+
/// Appends the given text to the rendered content.
///
/// # Examples
///
/// ```
/// use mingling_core::RenderResult;
- /// use std::ops::Deref;
///
/// let mut result = RenderResult::default();
/// result.print("Hello");
/// result.print(", world!");
- /// assert_eq!(result.deref(), "Hello, world!");
+ /// assert_eq!(result.to_string(), "Hello, world!");
/// ```
- pub fn print(&mut self, text: &str) {
- self.render_text.push_str(text);
+ pub fn print(&mut self, text: impl Into<String>) {
+ let text = text.into();
+ if self.immediate_output {
+ print!("{}", text)
+ }
+ self.append_to_buffer(text, Stdout);
}
/// Appends the given text followed by a newline to the rendered content.
@@ -142,16 +288,58 @@ impl RenderResult {
///
/// ```
/// use mingling_core::RenderResult;
- /// use std::ops::Deref;
///
/// let mut result = RenderResult::default();
/// result.println("First line");
/// result.println("Second line");
- /// assert_eq!(result.deref(), "First line\nSecond line\n");
+ /// assert_eq!(result.to_string(), "First line\nSecond line");
+ /// ```
+ pub fn println(&mut self, text: impl Into<String>) {
+ let text = text.into();
+ if self.immediate_output {
+ println!("{}", text)
+ }
+ self.append_line_to_buffer(text, Stdout);
+ }
+
+ /// Appends the given text to the rendered content, marking it for stderr output.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.eprint("Hello");
+ /// result.eprint(", world!");
+ /// assert_eq!(result.to_string(), "Hello, world!");
/// ```
- pub fn println(&mut self, text: &str) {
- self.render_text.push_str(text);
- self.render_text.push('\n');
+ pub fn eprint(&mut self, text: impl Into<String>) {
+ let text = text.into();
+ if self.immediate_output {
+ eprint!("{}", text)
+ }
+ self.append_to_buffer(text, Stderr);
+ }
+
+ /// Appends the given text followed by a newline to the rendered content, marking it for stderr output.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.eprintln("First line");
+ /// result.eprintln("Second line");
+ /// assert_eq!(result.to_string(), "First line\nSecond line");
+ /// ```
+ pub fn eprintln(&mut self, text: impl Into<String>) {
+ let text = text.into();
+ if self.immediate_output {
+ println!("{}", text)
+ }
+ self.append_line_to_buffer(text, Stderr);
}
/// Clears all rendered content.
@@ -169,7 +357,148 @@ impl RenderResult {
/// assert!(result.is_empty());
/// ```
pub fn clear(&mut self) {
- self.render_text.clear();
+ self.render_buffer.clear();
+ }
+
+ /// Outputs all buffered content to stdout and stderr according to their respective modes.
+ ///
+ /// Iterates through the render buffer and prints each buffered string to the appropriate
+ /// output stream — stdout for `Stdout` entries and stderr for `Stderr` entries.
+ ///
+ /// This method is typically used to flush the buffered output at the end of rendering,
+ /// ensuring that all output is displayed in the correct order and to the correct stream.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::{RenderResult, RenderResultMode};
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.append_to_buffer("Hello", RenderResultMode::Stdout);
+ /// result.append_to_buffer("Error", RenderResultMode::Stderr);
+ /// result.std_print(); // prints "Hello" to stdout and "Error" to stderr
+ /// ```
+ pub fn std_print(&self) {
+ for (content, mode) in self.render_buffer.iter() {
+ match mode {
+ Stdout => print!("{}", content),
+ Stderr => eprint!("{}", content),
+ }
+ }
+ }
+
+ /// Returns the total number of characters (in terms of `char` count) in the buffered render output.
+ ///
+ /// This counts the length across all buffered entries, regardless of whether they are
+ /// destined for stdout or stderr.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.print("Hello");
+ /// result.print(", 世界");
+ /// assert_eq!(result.len(), 9); // "Hello, 世界" has 9 chars
+ /// ```
+ pub fn len(&self) -> usize {
+ self.render_buffer
+ .iter()
+ .map(|(s, _)| s.chars().count())
+ .sum()
+ }
+
+ /// Returns `true` if the buffered render output contains no characters.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// assert!(result.is_empty());
+ /// result.print("Hello");
+ /// assert!(!result.is_empty());
+ /// ```
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ /// Trims leading and trailing whitespace from the buffered render output.
+ ///
+ /// This method processes the render buffer as follows:
+ /// - If the buffer is empty, it returns `self` unchanged.
+ /// - If there is only one entry, whitespace is trimmed from both the start and end of that
+ /// single entry.
+ /// - If there are multiple entries, whitespace is trimmed from the start of the first entry
+ /// and the end of the last entry.
+ ///
+ /// Whitespace in the middle entries is preserved. This is useful for cleaning up output
+ /// without removing intentional spacing between separately buffered segments.
+ ///
+ /// # Returns
+ ///
+ /// A new `RenderResult` with the same `immediate_output` flag and `exit_code`, but with
+ /// trimmed text content.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_core::RenderResult;
+ ///
+ /// let mut result = RenderResult::default();
+ /// result.print(" Hello, world! ");
+ /// let trimmed = result.trim_buffer();
+ /// assert_eq!(trimmed.to_string().trim(), "Hello, world!");
+ /// ```
+ pub fn trim_buffer(self) -> RenderResult {
+ if self.render_buffer.is_empty() {
+ return self;
+ }
+
+ let mut buffer = self.render_buffer;
+ if buffer.len() == 1 {
+ // Only one entry: trim both start and end of this single entry
+ let (text, mode) = buffer.remove(0);
+ buffer.push((text.trim().to_string(), mode));
+ } else {
+ // Multiple entries: trim start of first, trim end of last
+ let first_len = buffer.len();
+
+ // Trim start of first entry
+ let (first_text, first_mode) = buffer.remove(0);
+ let trimmed_first = first_text.trim_start().to_string();
+ buffer.insert(0, (trimmed_first, first_mode));
+
+ // Trim end of last entry
+ let (last_text, last_mode) = buffer.remove(first_len - 1);
+ let trimmed_last = last_text.trim_end().to_string();
+ buffer.push((trimmed_last, last_mode));
+ }
+
+ RenderResult {
+ render_buffer: buffer,
+ immediate_output: self.immediate_output,
+ exit_code: self.exit_code,
+ }
+ }
+}
+
+#[inline(always)]
+fn render_result_to_string(result: &RenderResult) -> String {
+ let mut buffer = String::new();
+ for item in result.render_buffer.iter() {
+ buffer += &item.0;
+ }
+ buffer
+}
+
+#[inline(always)]
+fn string_to_render_result(string: impl Into<String>, mode: RenderResultMode) -> RenderResult {
+ RenderResult {
+ render_buffer: vec![(string.into(), mode)],
+ ..Default::default()
}
}
@@ -186,20 +515,6 @@ mod tests {
}
#[test]
- fn print_appends_text() {
- let mut result = RenderResult::default();
- result.print("Hello");
- assert_eq!(result.deref(), "Hello");
- }
-
- #[test]
- fn println_appends_text_with_newline() {
- let mut result = RenderResult::default();
- result.println("Hello");
- assert_eq!(result.deref(), "Hello\n");
- }
-
- #[test]
fn clear_empties_content() {
let mut result = RenderResult::default();
result.print("something");
@@ -217,14 +532,6 @@ mod tests {
}
#[test]
- fn write_appends_utf8_bytes() {
- let mut result = RenderResult::default();
- let n = IoWrite::write(&mut result, b"hello").unwrap();
- assert_eq!(n, 5);
- assert_eq!(result.deref(), "hello");
- }
-
- #[test]
fn write_with_invalid_utf8_returns_error() {
let mut result = RenderResult::default();
let err = IoWrite::write(&mut result, &[0xff, 0xfe]).unwrap_err();
@@ -236,16 +543,7 @@ mod tests {
let mut result = RenderResult::default();
result.print(" hello world \n");
let formatted = format!("{}", result);
- assert_eq!(formatted, "hello world\n");
- }
-
- #[test]
- fn deref_exposes_inner_text_as_str() {
- let mut result = RenderResult::default();
- result.print("test");
-
- let s: &str = &result;
- assert_eq!(s, "test");
+ assert_eq!(formatted, "hello world");
}
#[test]
@@ -265,4 +563,72 @@ mod tests {
// original is still usable
assert!(!result.is_empty());
}
+
+ #[test]
+ fn trim_empty_buffer_returns_self() {
+ let result = RenderResult::default();
+ let trimmed = result.trim_buffer();
+ assert!(trimmed.is_empty());
+ assert_eq!(trimmed.exit_code, 0);
+ }
+
+ #[test]
+ fn trim_single_entry_trims_both_ends() {
+ let mut result = RenderResult::default();
+ result.print(" Hello, world! ");
+ let trimmed = result.trim_buffer();
+ assert_eq!(trimmed.to_string(), "Hello, world!");
+ }
+
+ #[test]
+ fn trim_single_entry_nothing_to_trim() {
+ let mut result = RenderResult::default();
+ result.print("Hello");
+ let trimmed = result.trim_buffer();
+ assert_eq!(trimmed.to_string(), "Hello");
+ }
+
+ #[test]
+ fn trim_multiple_entries_trims_first_start_and_last_end() {
+ let mut result = RenderResult::default();
+ result.print(" Hello");
+ result.print(" World ");
+ result.print("! ");
+ let trimmed = result.trim_buffer();
+ // first entry trim_start: "Hello"
+ // middle entry unchanged: " World "
+ // last entry trim_end: "!"
+ assert_eq!(trimmed.to_string(), "Hello World !");
+ }
+
+ #[test]
+ fn trim_multiple_entries_only_whitespace_first_entry() {
+ let mut result = RenderResult::default();
+ result.print(" ");
+ result.print("Hello");
+ result.print(" ");
+ let trimmed = result.trim_buffer();
+ // first entry trim_start: ""
+ // middle entry unchanged: "Hello"
+ // last entry trim_end: ""
+ assert_eq!(trimmed.to_string(), "Hello");
+ }
+
+ #[test]
+ fn trim_preserves_exit_code() {
+ let mut result = RenderResult::new();
+ result.exit_code = 42;
+ result.print(" test ");
+ let trimmed = result.trim_buffer();
+ assert_eq!(trimmed.exit_code, 42);
+ }
+
+ #[test]
+ fn trim_preserves_stderr_mode() {
+ let mut result = RenderResult::default();
+ result.eprint(" error ");
+ let trimmed = result.trim_buffer();
+ assert_eq!(trimmed.render_buffer[0].1, RenderResultMode::Stderr);
+ assert_eq!(trimmed.to_string(), "error");
+ }
}
diff --git a/mingling_core/src/renderer/structural.rs b/mingling_core/src/renderer/structural.rs
index 30255aa..0449e72 100644
--- a/mingling_core/src/renderer/structural.rs
+++ b/mingling_core/src/renderer/structural.rs
@@ -1,5 +1,5 @@
use crate::{
- RenderResult, StructuralRendererSetting,
+ ProgramCollect, RenderResult, StructuralRendererSetting,
renderer::structural::error::StructuralRendererSerializeError,
};
use serde::Serialize;
@@ -24,11 +24,15 @@ impl StructuralRenderer {
///
/// Returns `Err(StructuralRendererSerializeError)` if serialization fails.
#[allow(unused_variables)]
- pub fn render<T: StructuralData + Send>(
+ pub fn render<T, C>(
data: &T,
setting: &StructuralRendererSetting,
r: &mut RenderResult,
- ) -> Result<(), StructuralRendererSerializeError> {
+ ) -> Result<(), StructuralRendererSerializeError>
+ where
+ T: StructuralData<C> + Send,
+ C: ProgramCollect<Enum = C>,
+ {
match setting {
StructuralRendererSetting::Disable => Ok(()),
#[cfg(feature = "json_serde_fmt")]
@@ -148,7 +152,7 @@ impl StructuralRenderer {
#[cfg(test)]
mod tests {
use super::*;
- use crate::RenderResult;
+ use crate::{MockProgramCollect, RenderResult};
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Serialize)]
@@ -157,8 +161,8 @@ mod tests {
value: i32,
}
- impl crate::__private::StructuralDataSealed for TestData {}
- impl StructuralData for TestData {}
+ impl crate::__private::StructuralDataSealed<MockProgramCollect> for TestData {}
+ impl StructuralData<MockProgramCollect> for TestData {}
fn test_data() -> TestData {
TestData {
diff --git a/mingling_core/src/renderer/structural/error.rs b/mingling_core/src/renderer/structural/error.rs
index a7fbc75..63ded81 100644
--- a/mingling_core/src/renderer/structural/error.rs
+++ b/mingling_core/src/renderer/structural/error.rs
@@ -9,6 +9,7 @@ pub struct StructuralRendererSerializeError {
}
impl StructuralRendererSerializeError {
+ /// Creates a new `StructuralRendererSerializeError` with the given error message.
#[must_use]
pub fn new(error: String) -> Self {
Self { error }
diff --git a/mingling_core/src/renderer/structural/structural_data.rs b/mingling_core/src/renderer/structural/structural_data.rs
index 1cafac3..583808c 100644
--- a/mingling_core/src/renderer/structural/structural_data.rs
+++ b/mingling_core/src/renderer/structural/structural_data.rs
@@ -1,5 +1,7 @@
use serde::Serialize;
+use crate::ProgramCollect;
+
/// Marker trait for types that support structured output (JSON / YAML / TOML / RON).
///
/// This trait is a **supertrait** of `serde::Serialize` and is sealed via
@@ -12,4 +14,8 @@ use serde::Serialize;
/// These entry points also register the type in the global `STRUCTURED_TYPES`
/// registry, which is required for the `structural_render` match arm to be generated.
#[doc(hidden)]
-pub trait StructuralData: Serialize + crate::__private::StructuralDataSealed {}
+pub trait StructuralData<C>: Serialize + crate::__private::StructuralDataSealed<C>
+where
+ C: ProgramCollect<Enum = C>,
+{
+}
diff --git a/mingling_core/tests/test-all/tests/integration.rs b/mingling_core/tests/test-all/tests/integration.rs
index 3581702..d36b9df 100644
--- a/mingling_core/tests/test-all/tests/integration.rs
+++ b/mingling_core/tests/test-all/tests/integration.rs
@@ -1,13 +1,12 @@
use mingling::Flag;
-use mingling::StructuralRenderer;
-use mingling::StructuralRendererSetting;
-use mingling::MockProgramCollect;
use mingling::NextProcess;
-use mingling::StructuralData;
use mingling::Node;
use mingling::Program;
use mingling::RenderResult;
use mingling::StringVec;
+use mingling::StructuralData;
+use mingling::StructuralRenderer;
+use mingling::StructuralRendererSetting;
use mingling::comp::{ShellContext, ShellFlag, Suggest};
use mingling::core_res::ResREPL;
use mingling::hook::ProgramHook;
@@ -86,7 +85,7 @@ fn test_render_result_default() {
fn test_render_result_print() {
let mut r = RenderResult::default();
r.print("hello");
- assert_eq!(&*r, "hello");
+ assert_eq!(r.to_string().as_str(), "hello");
}
// StructuralRenderer
@@ -125,13 +124,13 @@ fn test_structural_renderer_json() {
#[test]
fn test_is_completing() {
- let program: Program<MockProgramCollect> = Program::new_with_args(["app", "__comp"]);
+ let program: Program<crate::ThisProgram> = Program::new_with_args(["app", "__comp"]);
assert!(program.is_completing());
}
#[test]
fn test_is_not_completing() {
- let program: Program<MockProgramCollect> = Program::new_with_args(["app", "greet"]);
+ let program: Program<crate::ThisProgram> = Program::new_with_args(["app", "greet"]);
assert!(!program.is_completing());
}
@@ -141,7 +140,7 @@ fn test_is_not_completing() {
fn test_hook_setup() {
static CALLED: AtomicBool = AtomicBool::new(false);
- let hook = ProgramHook::<MockProgramCollect>::empty().on_begin::<_, ()>(|_| {
+ let hook = ProgramHook::<crate::ThisProgram>::empty().on_begin::<_, ()>(|_| {
CALLED.store(true, Ordering::SeqCst);
});
@@ -166,3 +165,5 @@ fn test_string_vec_from_array() {
let v: Vec<String> = sv.into();
assert_eq!(v, vec!["a", "b", "c"]);
}
+
+mingling::macros::gen_program!();
diff --git a/mingling_core/tests/test-basic/tests/integration.rs b/mingling_core/tests/test-basic/tests/integration.rs
index 7cd7b8c..e51992c 100644
--- a/mingling_core/tests/test-basic/tests/integration.rs
+++ b/mingling_core/tests/test-basic/tests/integration.rs
@@ -60,7 +60,7 @@ fn test_render_result_default() {
fn test_render_result_print() {
let mut r = RenderResult::default();
r.print("hello");
- assert_eq!(&*r, "hello");
+ assert_eq!(r.to_string().as_str(), "hello");
}
#[test]
diff --git a/mingling_core/tests/test-dispatch-tree/tests/integration.rs b/mingling_core/tests/test-dispatch-tree/tests/integration.rs
index 7cd7b8c..e51992c 100644
--- a/mingling_core/tests/test-dispatch-tree/tests/integration.rs
+++ b/mingling_core/tests/test-dispatch-tree/tests/integration.rs
@@ -60,7 +60,7 @@ fn test_render_result_default() {
fn test_render_result_print() {
let mut r = RenderResult::default();
r.print("hello");
- assert_eq!(&*r, "hello");
+ assert_eq!(r.to_string().as_str(), "hello");
}
#[test]
diff --git a/mingling_core/tests/test-repl/tests/integration.rs b/mingling_core/tests/test-repl/tests/integration.rs
index 1792525..4de0e8f 100644
--- a/mingling_core/tests/test-repl/tests/integration.rs
+++ b/mingling_core/tests/test-repl/tests/integration.rs
@@ -59,5 +59,5 @@ fn test_render_result_default() {
fn test_render_result_print() {
let mut r = RenderResult::default();
r.print("hello");
- assert_eq!(&*r, "hello");
+ assert_eq!(r.to_string().as_str(), "hello");
}
diff --git a/mingling_core/tests/test-structural-renderer/tests/integration.rs b/mingling_core/tests/test-structural-renderer/tests/integration.rs
index 3c3c6db..e4057f8 100644
--- a/mingling_core/tests/test-structural-renderer/tests/integration.rs
+++ b/mingling_core/tests/test-structural-renderer/tests/integration.rs
@@ -1,4 +1,4 @@
-use mingling::{StructuralRenderer, StructuralRendererSetting, RenderResult, StructuralData};
+use mingling::{RenderResult, StructuralData, StructuralRenderer, StructuralRendererSetting};
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Serialize, StructuralData)]
@@ -17,7 +17,8 @@ fn test_data() -> TestData {
#[test]
fn test_render_disable() {
let mut r = RenderResult::default();
- let result = StructuralRenderer::render(&test_data(), &StructuralRendererSetting::Disable, &mut r);
+ let result =
+ StructuralRenderer::render(&test_data(), &StructuralRendererSetting::Disable, &mut r);
assert!(result.is_ok());
assert!(r.is_empty());
}
@@ -73,3 +74,6 @@ fn test_render_ron() {
assert!(output.contains("value:"));
assert!(output.contains("42"));
}
+
+mingling::macros::gen_program!();
+