Skip to main content

tui_lipan/
lib.rs

1#![deny(unsafe_code)]
2#![warn(missing_docs)]
3#![cfg_attr(target_arch = "wasm32", allow(dead_code, unused_imports))]
4
5//! tui-lipan: opinionated, component-based, modern TUI framework.
6//!
7//! This crate is built on top of `ratatui` + `crossterm` internally, but the
8//! public API is backend-agnostic (no `ratatui` types leak).
9
10#[cfg(all(target_arch = "wasm32", any(feature = "image", feature = "terminal")))]
11compile_error!(
12    "tui-lipan: wasm32 builds cannot enable image or terminal features; use --no-default-features"
13);
14
15pub mod prelude;
16
17#[macro_use]
18mod widget_manifest;
19
20pub mod core;
21
22pub mod animation;
23pub mod capture;
24pub mod debug;
25#[cfg(feature = "devtools")]
26pub(crate) mod devtools;
27pub mod input;
28#[cfg(not(target_arch = "wasm32"))]
29pub mod process;
30pub mod ui_snapshot;
31
32mod clipboard;
33
34mod app;
35mod backend;
36mod callback;
37pub mod callbacks;
38mod layout;
39mod mockup;
40mod overlay;
41mod runtime;
42mod ui;
43
44pub mod style;
45
46mod test_backend;
47mod text;
48pub mod utils;
49pub mod validation;
50mod widgets;
51
52/// Temporarily release the interactive terminal for an external program (e.g. `$EDITOR`).
53///
54/// Run suspend/resume on the **UI thread** (see [`Command::new`](crate::core::component::Command::new));
55/// use [`Context::request_full_repaint`](crate::core::component::Context::request_full_repaint) after
56/// return when a full frame redraw is needed. Human-oriented guide: `docs/external-programs.md` in the repo.
57#[cfg(not(target_arch = "wasm32"))]
58pub mod terminal_handoff {
59    pub use crate::backend::ratatui_backend::terminal_handoff::{
60        resume_after_external_process, suspend_for_external_process,
61    };
62}
63
64#[cfg(not(target_arch = "wasm32"))]
65pub use crate::app::AppRunner;
66#[cfg(feature = "devtools")]
67pub use crate::app::DevToolsConfig;
68pub use crate::app::input::command_registry::{
69    CommandBuilder, CommandEntry, CommandId, CommandRegistry,
70};
71#[cfg(all(target_arch = "wasm32", feature = "web"))]
72pub use crate::app::web_runner::{WebTerminal, mount_web};
73pub use crate::app::{
74    App, ContrastPolicy, InlineStartupPolicy, ScreenBackground, SurfaceMode, TextAreaNewlineBinding,
75};
76pub use crate::mockup::Mockup;
77
78pub use crate::callback::{Callback, CancellationToken, CommandLink, KeyHandler, Link};
79pub use crate::capture::{CapturedCell, CapturedFrame, CellModifiers, CursorState};
80#[cfg(feature = "ui-snapshot-png")]
81pub use crate::capture::{PngOptions, PngTextRenderer};
82pub use crate::clipboard::{
83    ClipboardConfig, ClipboardError, ClipboardProvider, ImageContent, ImageFormat,
84    PasteShiftInsertBehavior,
85};
86pub use crate::core::component::{
87    Breakpoint, Command, Component, Context, KeyUpdate, ScrollbarVisibility, TaskPolicy, Update,
88};
89pub use crate::core::context_value::ContextValue;
90pub use crate::core::element::{Element, IntoElement, Key};
91pub use crate::core::event::{
92    KeyCode, KeyEvent, KeyMods, MouseDragEvent, MouseEvent, MouseMoveEvent,
93};
94pub use crate::core::mask::CellMask;
95pub use crate::core::memo::Memo;
96pub use crate::core::nested::any_props::ThemableProps;
97pub use crate::core::node::NodeId;
98pub use crate::input::{
99    ChordMatcher, ChordResult, KeyBinding, KeyBindingParseError, KeyBindings, format_binding,
100    format_binding_lowercase, format_bindings, format_bindings_lowercase,
101};
102pub use crate::overlay::{OverlayId, OverlayScope, ToastHandle, ToastPlacement};
103#[cfg(not(target_arch = "wasm32"))]
104pub use crate::process::{
105    ProcessEvent, ProcessExitStatus, ProcessSpec, process_command, process_command_keyed,
106    stream_process, stream_process_until,
107};
108pub use crate::style::Theme;
109pub use crate::style::{
110    Align, BorderEdges, BorderStyle, CaretShape, CellEffect, Color, ColorTransform, DiffPalette,
111    DocumentPalette, DocumentViewPalette, Edge, EffectAxis, EffectCell, EffectContext,
112    EffectPalette, EffectPrepareContext, FileIconPalette, FloatRect, GitStatusPalette,
113    HexAreaPalette, HostTerminalColors, InputPalette, Justify, LayoutConstraints, Length, Padding,
114    Paint, PreparedCellEffect, Rect, RetroPreset, RichText, RippleRadius, ScrollbarConfig,
115    ScrollbarPalette, ScrollbarVariant, ShrinkPriority, Size, Span, StatusPalette, Style,
116    SurfacePalette, SyntaxPalette, TerminalColor, TerminalPalette, TextAreaPalette, ThemeExtension,
117    ThemePalette, VisualEffect, query_host_colors,
118};
119#[cfg(feature = "theme-reload")]
120pub use crate::style::{ThemeWatcher, load_theme_from_toml};
121pub use crate::test_backend::TestBackend;
122pub use crate::text::edit::{TextEditEvent, TextEditKind};
123pub use crate::text::editor::TextEditor;
124pub use crate::text::line_index::{LineIndex, TextEncoding, TextPosition, TextRange};
125pub use crate::ui_snapshot::{
126    UiSnapshot, UiSnapshotFileFormat, UiSnapshotFormatOptions, UiSnapshotOptions, UiSnapshotSlot,
127    UiWidgetDesc, UiWidgetKind,
128};
129pub use crate::validation::{StringValidator, ValidationError, Validator};
130pub use crate::widgets::{
131    Canvas, CanvasItem, ClassDiagram, ClassDiagramTheme, ClassMember, ClassRelation,
132    ClassRelationKind, ClassSpec, ClassVisibility, ContextProvider, DEFAULT_PREVIEW_MAX_HEIGHT,
133    DEFAULT_PREVIEW_MAX_WIDTH, DiagramClassMemberSpec, DiagramClassNodeSpec,
134    DiagramClassRelationSpec, DiagramClassSpec, DiagramClassVisibilitySpec, DiagramDirection,
135    DiagramErAttributeSpec, DiagramErEntitySpec, DiagramErRelationSpec, DiagramErSpec,
136    DiagramFlowEdgeSpec, DiagramFlowNodeShape, DiagramFlowNodeSpec, DiagramFlowchartSpec,
137    DiagramGanttDate, DiagramGanttDuration, DiagramGanttSection, DiagramGanttSpec,
138    DiagramGanttTask, DiagramGanttTaskStart, DiagramGanttTaskStatus, DiagramPieSliceSpec,
139    DiagramPieSpec, DiagramSequenceMessageSpec, DiagramSequenceParticipantSpec,
140    DiagramSequenceSpec, DiagramStateKindSpec, DiagramStateNodeSpec, DiagramStateSpec,
141    DiagramStateTransitionSpec, DraggableTabBarOverflow, ErAttribute, ErCardinality, ErDiagram,
142    ErDiagramTheme, ErEntity, ErRelation, FileKind, FileTree, FileTreeChange, FileTreeChangeSource,
143    FileTreeChangeStatus, FileTreeChangeView, FileTreeEvent, FileTreeGitView, FileTreeItemStyle,
144    FileTreeSuffixPriority, FileTreeToggleEvent, FormattedDiagramBlock, GanttDate, GanttDiagram,
145    GanttDiagramTheme, GanttDuration, GanttSection, GanttSpec, GanttTask, GanttTaskStart,
146    GanttTaskStatus, Heatmap, HeatmapCellMode, HeatmapLegendWidth, HexArea, HexAreaChangeEvent,
147    HexAreaCursorEvent, HexAreaEditEvent, HexAreaEditKind, IMAGE_SENTINEL_BASE, PanEvent,
148    PanKeymap, PanMetrics, PanView, ParsedDiagram, SENTINEL_BASE, ScrollAxis, ScrollBehavior,
149    ScrollChildExitDirection, ScrollChildVisibility, ScrollDistanceConfig, ScrollEvent,
150    ScrollExitedChild, ScrollMetrics, ScrollTarget, ScrollViewportEvent, ScrollVisibleChild,
151    ScrollWheelBehavior, ScrollWheelConfig, SentinelEvent, SentinelId, StateDiagram,
152    StateDiagramTheme, StateKind, StateSpec, StateTransition, TextArea, TextAreaColorInput,
153    TextAreaColorLines, TextAreaColorStrategy, TextAreaCursorMetrics, TextAreaDecoration,
154    TextAreaDecorationKind, TextAreaEvent, TextAreaGutter, TextAreaGutterColumn,
155    TextAreaGutterSign, TextAreaImageMode, TextAreaLineNumberMode, TextAreaMetrics,
156    TextAreaPasteEvent, TextAreaSentinel, TextAreaSentinelClickEvent, TextAreaSentinelClickKind,
157    TextAreaSnapshot, TextAreaStateChangeEvent, TextAreaStateChangeReason, TextAreaVimConfig,
158    TextAreaVimCurrentLineHighlight, TextAreaVimKeyBinding, TextAreaVimKeymap, TextAreaVimMode,
159    TextAreaVirtualText, Toast, ToastCopyAffordance, TripleClickSelectionMode,
160    VirtualTextPlacement, insert_sentinel, rank_search_palette_indices,
161    rank_search_palette_indices_with_score,
162};
163
164#[cfg(feature = "terminal")]
165pub use crate::widgets::{
166    MouseEncoding, MouseMode, MouseModeState, TerminalColorPalette, TerminalRenderSnapshot,
167};
168
169#[cfg(feature = "diff-view")]
170pub use crate::widgets::{
171    DiffContextExpansion, DiffContextRange, DiffContextSeparatorDirection,
172    DiffContextSeparatorEvent, DiffData, DiffDataConfig, DiffHunkAnchor,
173};
174
175#[cfg(feature = "syntax-syntect")]
176pub use crate::widgets::{
177    SyntectDocumentFormatter, SyntectStrategy, apply_syntect_strategy_app_theme, language_from_path,
178};
179/// Macro for building `Element`s with struct-literal syntax.
180pub use tui_lipan_macro::rsx;
181/// Autocomplete-friendly macro for building `Element`s with builder chains.
182///
183/// Uses standard Rust builder syntax (full rust-analyzer autocomplete) with
184/// `=> { children }` sugar for nesting.
185pub use tui_lipan_macro::ui;
186
187/// One-liner macro for previewing a TUI layout without any component boilerplate.
188///
189/// The body expression is automatically converted via `.into()`, so you can
190/// return any widget builder directly (e.g. `Frame::new()...`) without calling
191/// `.into()` yourself.  The closure uses `move` capture, so local data can be
192/// referenced freely inside the body.
193///
194/// Press `Esc` or `q` to quit the preview.
195///
196/// # Basic usage
197///
198/// ```rust,no_run
199/// use tui_lipan::prelude::*;
200///
201/// fn main() -> Result<()> {
202///     mockup!("Dashboard", {
203///         Frame::new()
204///             .title("Panel")
205///             .border(true)
206///             .child(Text::new("Hello!"))
207///     })
208/// }
209/// ```
210///
211/// # With sample data (mockup → app workflow)
212///
213/// Extract your UI into plain functions that return `Element`, then reuse them
214/// in both mockup previews and real components:
215///
216/// ```rust,no_run
217/// use tui_lipan::prelude::*;
218///
219/// fn sidebar(items: &[&str], sel: usize) -> Element {
220///     Frame::new()
221///         .title("Nav")
222///         .border(true)
223///         .child(List::new()
224///             .items(items.iter().map(|s| ListItem::new(*s)))
225///             .selected(sel))
226///         .into()
227/// }
228///
229/// fn main() -> Result<()> {
230///     let items = vec!["Home", "Settings"];
231///     mockup!("Preview", {
232///         sidebar(&items, 0)
233///     })
234/// }
235/// ```
236#[cfg(not(target_arch = "wasm32"))]
237#[macro_export]
238macro_rules! mockup {
239    ($title:expr, $body:expr) => {
240        $crate::App::new()
241            .title($title)
242            .mount($crate::Mockup::new(move || { $body }.into()))
243            .run()
244    };
245}
246
247/// Create a nested component element.
248///
249/// This does not require `Component: Default`; pass a factory closure to construct the instance.
250pub fn child<C, F>(factory: F, props: C::Properties) -> Element
251where
252    C: Component,
253    F: Fn() -> C + 'static,
254{
255    Element::new(crate::core::element::ElementKind::Component(
256        crate::core::nested::ComponentElement::new::<C, F>(factory, props),
257    ))
258}
259
260/// Crate-wide result type.
261pub type Result<T> = std::result::Result<T, Error>;
262
263/// Crate-wide error type.
264#[derive(thiserror::Error, Debug)]
265pub enum Error {
266    /// I/O error.
267    #[error(transparent)]
268    Io(#[from] std::io::Error),
269
270    /// Syntax theme loading error.
271    #[error("failed to load syntax theme `{name}`: {message}")]
272    SyntaxThemeLoad {
273        /// Theme name.
274        name: String,
275        /// Error details.
276        message: String,
277        /// Error message.
278        #[source]
279        error: Option<Box<dyn std::error::Error + Send + Sync>>,
280    },
281
282    /// Internal message routing error.
283    #[error(
284        "message type mismatch for component `{component}` (expected `{expected}`, got `{actual}`)"
285    )]
286    MessageTypeMismatch {
287        /// Type name of the component that received the message.
288        component: &'static str,
289        /// Expected message type name.
290        expected: &'static str,
291        /// Actual message type name.
292        actual: &'static str,
293    },
294
295    /// Internal properties routing error.
296    #[error(
297        "props type mismatch for component `{component}` (expected `{expected}`, got `{actual}`)"
298    )]
299    PropsTypeMismatch {
300        /// Type name of the component that received the props.
301        component: &'static str,
302        /// Expected props type name.
303        expected: &'static str,
304        /// Actual props type name.
305        actual: &'static str,
306    },
307
308    /// Component expansion failure (props mismatch or mount failure during tree expansion).
309    #[error("component expansion failed: {reason}")]
310    ComponentExpansion {
311        /// Human-readable description of the failure.
312        reason: String,
313    },
314
315    /// Theme reload error.
316    #[error("theme reload error: {message}")]
317    ThemeReload {
318        /// Error details.
319        message: String,
320    },
321}
322
323#[cfg(test)]
324#[test]
325fn inline_transcript_append_rejects_component_nodes() {
326    crate::runtime::assert_inline_transcript_append_rejects_component_nodes();
327}
328
329#[cfg(test)]
330#[test]
331fn inline_surface_commit_render_path_is_unified() {
332    crate::runtime::assert_inline_surface_commit_render_path_is_unified();
333}
334
335#[cfg(all(test, not(target_arch = "wasm32")))]
336#[test]
337fn inline_surface_internal_wrap_policy_is_opaque() {
338    crate::backend::ratatui_backend::assert_inline_surface_internal_wrap_policy_is_opaque();
339}