///|
/// Core type aliases and scaffolding for the Pippa TUI framework.
///
/// Pippa follows the Elm Architecture (Model–Update–View):
///
/// - **Model**: application state stored in a plain struct.
/// - **Msg**: typed messages that describe events (key presses, resizes, etc.).
/// - **update**: a pure function that produces the next Model and optional Cmds.
/// - **view**: a pure function that renders the Model to a string of ANSI output.
/// Apps that need per-render terminal state can opt into `View` without
/// changing the common string-view shape.
///
/// The runtime (Program) handles raw terminal mode, input parsing, and
/// efficient diffing / re-rendering.
///
/// # Example
/// ```moonbit
/// let _batch : Cmd[Nothing] = Cmd::batch([])
/// let _none : Cmd[Nothing] = Cmd::none()
/// let _tick : Cmd[Nothing] = Cmd::tick(100)
/// ```
///|
/// Sentinel type for commands that carry no payload.
pub(all) struct Nothing {} derive(Debug, Eq)
///|
/// Typed payload delivered to scheduler callbacks.
pub(all) struct TimerTick {
/// Milliseconds elapsed since the command was scheduled.
elapsed_ms : Int64
} derive(Debug, Eq)
///|
/// Lifecycle messages emitted by the runtime and lifecycle commands.
pub(all) enum LifecycleMsg {
/// The program should quit cleanly.
Quit
/// The program was interrupted, typically by Ctrl+C / SIGINT.
Interrupt
/// The program is temporarily releasing the terminal for suspension.
Suspend
/// The program has resumed and re-acquired the terminal.
Resume
} derive(Debug, Eq)
///|
/// External process request run by `Cmd::exec_process`.
pub(all) struct ExecProcess {
/// Executable name or path passed to `execvp`.
command : String
/// Arguments passed after `command`.
args : Array[String]
} derive(Debug, Eq)
///|
/// Create an external process request.
pub fn ExecProcess::new(
command~ : String,
args? : Array[String] = [],
) -> ExecProcess {
{ command, args }
}
///|
/// Result of an external process run.
pub(all) enum ExecResult {
/// The child process exited and supplied an exit code.
ExecCompleted(Int)
/// The child process was cancelled before completion.
ExecCancelled
/// The child process was terminated by a signal.
ExecSignaled(Int)
/// The child process could not be started or failed at the runner boundary.
ExecError(String)
} derive(Debug, Eq)
///|
/// Typed result returned by `Program::run`.
pub(all) enum RunResult[Model] {
/// The program ended without a quit, interrupt, runtime error, or exec result.
RunCompleted(Model)
/// The program quit cleanly and carries the final model.
RunQuit(Model)
/// The program was interrupted and carries the final model.
RunInterrupted(Model)
/// The runtime stopped with an error.
RunRuntimeError(String)
/// The program quit in response to an external process result.
RunExec(Model, ExecResult)
} derive(Debug)
///|
/// Cursor position requested by a structured `View`.
pub(all) struct ViewCursorPosition {
/// 1-based terminal row.
row : Int
/// 1-based terminal column.
col : Int
} derive(Debug, Eq)
///|
fn view_cursor_coordinate(value : Int) -> Int {
if value < 1 {
1
} else {
value
}
}
///|
/// Create a 1-based cursor position, clamping invalid terminal coordinates to 1.
pub fn ViewCursorPosition::new(row~ : Int, col~ : Int) -> ViewCursorPosition {
{ row: view_cursor_coordinate(row), col: view_cursor_coordinate(col) }
}
///|
/// Per-render terminal progress state carried by a structured `View`.
pub(all) enum ViewProgress {
/// Clear or suppress terminal progress reporting for this frame.
ViewProgressNone
/// Show an indeterminate progress indicator.
ViewProgressIndeterminate
/// Show progress as an integer percentage.
ViewProgressPercent(Int)
/// Show paused progress with an optional integer percentage.
ViewProgressPaused(Int?)
/// Show errored progress with an optional integer percentage.
ViewProgressError(Int?)
} derive(Debug, Eq)
///|
/// Mouse reporting mode requested by a structured `View`.
pub(all) enum ViewMouseMode {
/// Disable per-frame mouse reporting.
ViewMouseOff
/// Enable normal mouse press/release reporting.
ViewMousePress
/// Enable button-event mouse reporting, including button-held drag motion.
ViewMouseCellMotion
/// Enable full mouse motion reporting.
ViewMouseAllMotion
} derive(Debug, Eq)
///|
/// Structured render output for a single frame.
///
/// `content` is rendered exactly like the string returned by a traditional
/// `view` function. The optional fields describe terminal state that should
/// travel with the frame; emitting escape sequences for those fields is handled
/// by later render plumbing.
pub(all) struct View {
/// The frame content to diff and render.
content : String
/// Optional cursor position for this frame.
cursor_position : ViewCursorPosition?
/// Optional cursor visibility for this frame.
cursor_visible : Bool?
/// Optional terminal window title for this frame.
window_title : String?
/// Optional real terminal cursor shape/blink style for this frame.
cursor_style : CursorStyle?
/// Optional default foreground color for this frame.
foreground_color : Color?
/// Optional default background color for this frame.
background_color : Color?
/// Optional terminal progress state for this frame.
progress : ViewProgress?
/// Optional mouse reporting mode for this frame.
mouse_mode : ViewMouseMode?
} derive(Debug, Eq)
///|
/// Create a structured `View` with explicit optional terminal state.
pub fn View::new(
content~ : String,
cursor_position? : ViewCursorPosition? = None,
cursor_visible? : Bool? = None,
window_title? : String? = None,
foreground_color? : Color? = None,
background_color? : Color? = None,
progress? : ViewProgress? = None,
mouse_mode? : ViewMouseMode? = None,
cursor_style? : CursorStyle? = None,
) -> View {
let cursor_position = cursor_position.map(fn(pos) {
ViewCursorPosition::new(row=pos.row, col=pos.col)
})
{
content,
cursor_position,
cursor_visible,
window_title,
cursor_style,
foreground_color,
background_color,
progress,
mouse_mode,
}
}
///|
/// Wrap plain string content as a structured `View`.
pub fn View::text(content : String) -> View {
View::new(content~)
}
///|
/// Set the cursor position for this frame.
pub fn View::with_cursor_position(self : View, row~ : Int, col~ : Int) -> View {
{ ..self, cursor_position: Some(ViewCursorPosition::new(row~, col~)) }
}
///|
/// Set cursor visibility for this frame.
pub fn View::with_cursor_visible(self : View, visible : Bool) -> View {
{ ..self, cursor_visible: Some(visible) }
}
///|
/// Set the terminal window title for this frame.
pub fn View::with_window_title(self : View, title : String) -> View {
{ ..self, window_title: Some(title) }
}
///|
/// Set the real terminal cursor shape/blink style for this frame.
pub fn View::with_cursor_style(self : View, style : CursorStyle) -> View {
{ ..self, cursor_style: Some(style) }
}
///|
/// Set the default foreground color for this frame.
pub fn View::with_foreground_color(self : View, color : Color) -> View {
{ ..self, foreground_color: Some(color) }
}
///|
/// Set the default background color for this frame.
pub fn View::with_background_color(self : View, color : Color) -> View {
{ ..self, background_color: Some(color) }
}
///|
/// Set terminal progress state for this frame.
pub fn View::with_progress(self : View, progress : ViewProgress) -> View {
{ ..self, progress: Some(progress) }
}
///|
/// Set the mouse reporting mode for this frame.
pub fn View::with_mouse_mode(self : View, mouse_mode : ViewMouseMode) -> View {
{ ..self, mouse_mode: Some(mouse_mode) }
}
///|
/// A batch of I/O commands to be performed by the runtime.
///
/// `Cmd[Msg]` represents one-shot runtime effects that may later produce a
/// message of type `Msg` and feed it back into the update loop.
///
/// Commands are additive: when several updates happen in one runtime turn, the
/// resulting commands are all enqueued rather than the "last" one replacing the
/// others. Use `Cmd::none()` for no-ops and `Cmd::batch` to combine several
/// commands.
/// A batch of one-shot runtime effects.
struct Cmd[Msg] {
// Phantom field — carries no data, ensures the type parameter is consumed.
_phantom : Array[Msg]
// Runtime-managed one-shot actions.
actions : Array[CmdAction[Msg]]
}
///|
priv enum CmdAction[Msg] {
// Execute a runtime effect immediately after the current update cycle.
Perform(() -> Msg?)
// Schedule a one-shot `on_tick` callback after `interval_ms`.
Tick(Int)
// Schedule a one-shot runtime effect after `interval_ms`.
After(Int, () -> Msg)
// Schedule a one-shot callback aligned to `interval_ms`.
Every(Int, (TimerTick) -> Msg)
// Enter the alternate screen buffer.
EnterAltScreen
// Exit the alternate screen buffer.
ExitAltScreen
// Clear the screen and move the cursor home.
ClearScreen
// Invalidate the renderer cache and repaint on the next render pass.
Repaint
// Alias for `Repaint`, mirroring Bubble Tea's force redraw command.
ForceRedraw
// Hide the terminal cursor.
HideCursor
// Show the terminal cursor.
ShowCursor
// Ask the runtime to report the current terminal size.
RequestWindowSize((WindowSize) -> Msg)
// Request primary device attributes from the terminal.
RequestDeviceAttributes
// Request secondary device attributes / terminal version from the terminal.
RequestSecondaryDeviceAttributes
// Request the current cursor position from the terminal.
RequestCursorPosition
// Request the current default foreground color.
RequestForegroundColor
// Request the current default background color.
RequestBackgroundColor
// Request a terminal capability with XTGETTCAP.
RequestCapability(String)
// Request clipboard contents with OSC 52.
RequestClipboard(ClipboardSelection)
// Set clipboard contents with OSC 52.
SetClipboard(ClipboardSelection, String)
// Enable or disable focus reporting.
SetFocusReporting(Bool)
// Enable or disable bracketed paste mode.
SetBracketedPaste(Bool)
// Set terminal mouse reporting mode.
SetMouseMode(ViewMouseMode)
// Set the terminal window title.
SetWindowTitle(String)
// Set the real terminal cursor shape/blink style.
SetCursorStyle(CursorStyle)
// Deliver a typed lifecycle message through the runtime.
LifecycleAction(LifecycleMsg)
// Run an external process while the terminal is released.
ExecProcessAction(ExecProcess, (ExecResult) -> Msg)
// Stop the runtime with a typed error result.
RuntimeErrorAction(String)
// Run commands in order, waiting for immediate effects from each step to settle
// before starting the next one.
Sequence(Array[Cmd[Msg]])
}
///|
pub impl[Msg] Debug for Cmd[Msg] with fn to_repr(self) {
let mut perform_count = 0
let mut tick_count = 0
let mut after_count = 0
let mut every_count = 0
let mut screen_count = 0
let mut window_size_count = 0
let mut terminal_count = 0
let mut lifecycle_count = 0
let mut exec_count = 0
let mut error_count = 0
let mut sequence_count = 0
for action in self.actions {
match action {
Perform(_) => perform_count = perform_count + 1
Tick(_) => tick_count = tick_count + 1
After(_, _) => after_count = after_count + 1
Every(_, _) => every_count = every_count + 1
EnterAltScreen
| ExitAltScreen
| ClearScreen
| Repaint
| ForceRedraw
| HideCursor
| ShowCursor => screen_count = screen_count + 1
RequestWindowSize(_) => window_size_count = window_size_count + 1
RequestDeviceAttributes
| RequestSecondaryDeviceAttributes
| RequestCursorPosition
| RequestForegroundColor
| RequestBackgroundColor
| RequestCapability(_)
| RequestClipboard(_)
| SetClipboard(_, _)
| SetFocusReporting(_)
| SetBracketedPaste(_)
| SetMouseMode(_)
| SetWindowTitle(_)
| SetCursorStyle(_) => terminal_count = terminal_count + 1
LifecycleAction(_) => lifecycle_count = lifecycle_count + 1
ExecProcessAction(_, _) => exec_count = exec_count + 1
RuntimeErrorAction(_) => error_count = error_count + 1
Sequence(_) => sequence_count = sequence_count + 1
}
}
let buf = StringBuilder::new()
buf.write_string("Cmd(actions=")
buf.write_string(self.actions.length().to_string())
if perform_count > 0 {
buf.write_string(", perform=")
buf.write_string(perform_count.to_string())
}
if tick_count > 0 {
buf.write_string(", tick=")
buf.write_string(tick_count.to_string())
}
if after_count > 0 {
buf.write_string(", after=")
buf.write_string(after_count.to_string())
}
if every_count > 0 {
buf.write_string(", every=")
buf.write_string(every_count.to_string())
}
if screen_count > 0 {
buf.write_string(", screen=")
buf.write_string(screen_count.to_string())
}
if window_size_count > 0 {
buf.write_string(", window_size=")
buf.write_string(window_size_count.to_string())
}
if terminal_count > 0 {
buf.write_string(", terminal=")
buf.write_string(terminal_count.to_string())
}
if lifecycle_count > 0 {
buf.write_string(", lifecycle=")
buf.write_string(lifecycle_count.to_string())
}
if exec_count > 0 {
buf.write_string(", exec=")
buf.write_string(exec_count.to_string())
}
if error_count > 0 {
buf.write_string(", error=")
buf.write_string(error_count.to_string())
}
if sequence_count > 0 {
buf.write_string(", sequence=")
buf.write_string(sequence_count.to_string())
}
buf.write_string(")")
Repr::literal(buf.to_string())
}
///|
/// The result of an `update` call: the new model and an optional command.
pub(all) struct UpdateResult[Model, Msg] {
model : Model
cmd : Cmd[Msg]
} derive(Debug)
///|
/// Create an `UpdateResult` from a model and a command.
pub fn[Model, Msg] UpdateResult::new(
model~ : Model,
cmd~ : Cmd[Msg],
) -> UpdateResult[Model, Msg] {
{ model, cmd }
}
///|
/// A window resize event payload.
pub(all) struct WindowSize {
width : Int
height : Int
} derive(Debug, Eq)
///|
/// Built-in messages that the Pippa runtime can emit.
///
/// Users extend their own `Msg` enum to wrap `InternalMsg` when they need
/// to react to terminal events.
pub(all) enum InternalMsg {
/// A keyboard event (raw bytes pending further parsing).
KeyPress(Array[Byte])
/// The terminal was resized.
WindowResize(WindowSize)
/// A typed lifecycle event.
Lifecycle(LifecycleMsg)
/// An explicit quit request.
QuitRequested
} derive(Debug, Eq)