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 text_motion;
49pub mod utils;
50pub mod validation;
51mod widgets;
52
53/// The host terminal's cell size in pixels.
54///
55/// A terminal deals in cells, but a program drawing a picture needs pixels: it reads the PTY's
56/// `TIOCGWINSZ` pixel fields, or asks with `CSI 14 t`, and sizes its output against the answer.
57/// Feed this to [`TerminalPtyConfig::cell_size`](crate::widgets::TerminalPtyConfig::cell_size) and
58/// [`TerminalScreen::set_cell_size`](crate::widgets::TerminalScreen::set_cell_size) so the child's
59/// arithmetic and the pane's layout agree.
60///
61/// Detected once at startup. Hosts that answer no size query get the image encoder's fallback
62/// guess rather than a refusal, so this always names a usable cell.
63#[cfg(all(feature = "terminal-images", not(target_arch = "wasm32")))]
64pub fn host_cell_size() -> crate::widgets::TerminalCellSize {
65    crate::backend::ratatui_backend::image_support::host_cell_size()
66}
67
68/// Temporarily release the interactive terminal for an external program (e.g. `$EDITOR`).
69///
70/// Run suspend/resume on the **UI thread** (see [`Command::new`](crate::core::component::Command::new));
71/// use [`Context::request_full_repaint`](crate::core::component::Context::request_full_repaint) after
72/// return when a full frame redraw is needed. Human-oriented guide: `docs/external-programs.md` in the repo.
73#[cfg(not(target_arch = "wasm32"))]
74pub mod terminal_handoff {
75    pub use crate::backend::ratatui_backend::terminal_handoff::{
76        resume_after_external_process, suspend_for_external_process,
77    };
78}
79
80#[cfg(not(target_arch = "wasm32"))]
81pub use crate::app::AppRunner;
82#[cfg(feature = "devtools")]
83pub use crate::app::DevToolsConfig;
84pub use crate::app::input::command_registry::{
85    CommandBuilder, CommandEntry, CommandId, CommandRegistry,
86};
87pub use crate::app::input::key_dispatch::{
88    ChordMismatchPolicy, CommandConflictPolicy, KeyDispatchPolicy, TerminalKeyPolicy,
89};
90pub use crate::app::input::keymap::{FrameworkAction, FrameworkKeymap, UserKeymapPolicy};
91#[cfg(all(target_arch = "wasm32", feature = "web"))]
92pub use crate::app::web_runner::{WebTerminal, mount_web};
93pub use crate::app::{
94    App, ContrastPolicy, DevToolsMetric, FocusChanged, FocusEntry, FocusPolicy, InlineHeight,
95    InlineStartupPolicy, ScreenBackground, SurfaceMode, TextAreaNewlineBinding,
96};
97pub use crate::mockup::Mockup;
98
99pub use crate::animation::{ExitAnimation, ExitQueue, ExitTransfer};
100pub use crate::callback::{Callback, CancellationToken, CommandLink, KeyHandler, Link};
101pub use crate::capture::{CapturedCell, CapturedFrame, CastRecording, CellModifiers, CursorState};
102#[cfg(feature = "ui-snapshot-png")]
103pub use crate::capture::{PngOptions, PngTextRenderer};
104pub use crate::clipboard::{
105    ClipboardConfig, ClipboardError, ClipboardHandle, ClipboardPasteContent, ClipboardProvider,
106    ImageContent, ImageFormat, PasteShiftInsertBehavior,
107};
108pub use crate::core::component::{
109    Breakpoint, Command, Component, Context, KeyUpdate, ScrollbarVisibility, TaskPolicy, Update,
110    UpdateLevel,
111};
112pub use crate::core::context_value::ContextValue;
113pub use crate::core::element::{Element, IntoElement, Key};
114pub use crate::core::event::{
115    KeyCode, KeyEvent, KeyMods, MouseDragEvent, MouseEvent, MouseMoveEvent,
116};
117pub use crate::core::mask::CellMask;
118pub use crate::core::memo::Memo;
119pub use crate::core::nested::any_props::ThemableProps;
120pub use crate::core::node::NodeId;
121pub use crate::input::{
122    ChordMatcher, ChordResult, KeyBinding, KeyBindingParseError, KeyBindings,
123    KeyEventExpansionError, format_binding, format_binding_compact, format_binding_lowercase,
124    format_bindings, format_bindings_compact, format_bindings_lowercase,
125};
126pub use crate::layout::tag::Tag;
127pub use crate::overlay::{OverlayId, OverlayScope, ToastHandle, ToastPlacement};
128#[cfg(not(target_arch = "wasm32"))]
129pub use crate::process::{
130    ProcessEvent, ProcessExitStatus, ProcessSpec, process_command, process_command_keyed,
131    stream_process, stream_process_until,
132};
133pub use crate::style::Theme;
134pub use crate::style::{
135    Align, BorderEdges, BorderStyle, CaretPalette, CaretShape, CellEffect, Color, ColorTransform,
136    DiffPalette, DocumentPalette, DocumentViewPalette, Edge, EffectAxis, EffectCell, EffectContext,
137    EffectPalette, EffectPrepareContext, FileIconPalette, FloatRect, GitStatusPalette,
138    HexAreaPalette, HostTerminalColors, InputPalette, Justify, LayoutConstraints, Length, Padding,
139    Paint, PreparedCellEffect, Rect, RetroPreset, RichText, RippleRadius, ScrollbarConfig,
140    ScrollbarPalette, ScrollbarVariant, ShrinkPriority, Size, Span, StatusPalette, Style,
141    SurfacePalette, SyntaxPalette, TerminalColor, TerminalPalette, TextAreaPalette, ThemeExtension,
142    ThemePalette, VisualEffect, query_host_colors,
143};
144#[cfg(feature = "theme-reload")]
145pub use crate::style::{ThemeWatcher, load_theme_from_toml};
146pub use crate::test_backend::TestBackend;
147pub use crate::text::edit::{TextEditEvent, TextEditKind};
148pub use crate::text::editor::TextEditor;
149pub use crate::text::line_index::{LineIndex, TextEncoding, TextPosition, TextRange};
150pub use crate::ui_snapshot::{
151    Action, FocusStep, Recording, ScrollDirection, Sketch, Target, UiSnapshot,
152    UiSnapshotFileFormat, UiSnapshotFormatOptions, UiSnapshotOptions, UiSnapshotSlot, UiWidgetDesc,
153    UiWidgetKind,
154};
155#[cfg(feature = "ui-snapshot-png")]
156pub use crate::ui_snapshot::{BaselineComparison, BaselineOutcome, SnapshotBaseline};
157pub use crate::validation::{StringValidator, ValidationError, Validator};
158pub use crate::widgets::{Badge, BadgePosition, CapSides, CapStyle};
159pub use crate::widgets::{
160    BorderLabels, Canvas, CanvasItem, ClassDiagram, ClassDiagramTheme, ClassMember, ClassRelation,
161    ClassRelationKind, ClassSpec, ClassVisibility, ContextProvider, DEFAULT_PREVIEW_MAX_HEIGHT,
162    DEFAULT_PREVIEW_MAX_WIDTH, DiagramClassMemberSpec, DiagramClassNodeSpec,
163    DiagramClassRelationSpec, DiagramClassSpec, DiagramClassVisibilitySpec, DiagramDirection,
164    DiagramErAttributeSpec, DiagramErEntitySpec, DiagramErRelationSpec, DiagramErSpec,
165    DiagramFlowEdgeSpec, DiagramFlowNodeShape, DiagramFlowNodeSpec, DiagramFlowchartSpec,
166    DiagramGanttDate, DiagramGanttDuration, DiagramGanttSection, DiagramGanttSpec,
167    DiagramGanttTask, DiagramGanttTaskStart, DiagramGanttTaskStatus, DiagramPieSliceSpec,
168    DiagramPieSpec, DiagramSequenceMessageSpec, DiagramSequenceParticipantSpec,
169    DiagramSequenceSpec, DiagramStateKindSpec, DiagramStateNodeSpec, DiagramStateSpec,
170    DiagramStateTransitionSpec, DraggableTabBarOverflow, ErAttribute, ErCardinality, ErDiagram,
171    ErDiagramTheme, ErEntity, ErRelation, FileKind, FileTree, FileTreeChange, FileTreeChangeSource,
172    FileTreeChangeStatus, FileTreeChangeView, FileTreeDirectoryListing, FileTreeEntry,
173    FileTreeEntryRequest, FileTreeEntrySource, FileTreeEvent, FileTreeExplorerFocusOrigin,
174    FileTreeGitView, FileTreeItemStyle, FileTreeSuffixPriority, FileTreeToggleEvent, FocusScope,
175    FormattedDiagramBlock, Frame, FrameLabel, GanttDate, GanttDiagram, GanttDiagramTheme,
176    GanttDuration, GanttSection, GanttSpec, GanttTask, GanttTaskStart, GanttTaskStatus, Heatmap,
177    HeatmapCellMode, HeatmapLegendWidth, HexArea, HexAreaChangeEvent, HexAreaCursorEvent,
178    HexAreaEditEvent, HexAreaEditKind, IMAGE_SENTINEL_BASE, PanEvent, PanKeymap, PanMetrics,
179    PanView, ParsedDiagram, SENTINEL_BASE, ScrollAxis, ScrollBehavior, ScrollChildExitDirection,
180    ScrollChildVisibility, ScrollDistanceConfig, ScrollEvent, ScrollExitedChild, ScrollMetrics,
181    ScrollTarget, ScrollViewportEvent, ScrollVisibleChild, ScrollWheelBehavior, ScrollWheelConfig,
182    SentinelEvent, SentinelId, StateDiagram, StateDiagramTheme, StateKind, StateSpec,
183    StateTransition, TextArea, TextAreaColorInput, TextAreaColorLines, TextAreaColorStrategy,
184    TextAreaCursorMetrics, TextAreaDecoration, TextAreaDecorationKind, TextAreaEvent,
185    TextAreaGutter, TextAreaGutterColumn, TextAreaGutterSign, TextAreaImageMode,
186    TextAreaLineNumberMode, TextAreaMetrics, TextAreaPasteEvent, TextAreaSentinel,
187    TextAreaSentinelClickEvent, TextAreaSentinelClickKind, TextAreaSnapshot,
188    TextAreaStateChangeEvent, TextAreaStateChangeReason, TextAreaVimConfig,
189    TextAreaVimCurrentLineHighlight, TextAreaVimKeyBinding, TextAreaVimKeymap, TextAreaVimMode,
190    TextAreaVirtualText, Toast, ToastCopyAffordance, TripleClickSelectionMode,
191    VirtualTextPlacement, insert_sentinel, rank_search_palette_indices,
192    rank_search_palette_indices_with_mode, rank_search_palette_indices_with_score,
193};
194#[cfg(feature = "qr-code")]
195pub use crate::widgets::{QrCode, QrEcc, QrRender};
196
197#[cfg(all(feature = "terminal", unix))]
198pub use crate::widgets::TerminalPtyHandoff;
199#[cfg(feature = "terminal")]
200pub use crate::widgets::{
201    CopyModeAction, CopyModeGrid, KittyKeyboardFlags, MouseEncoding, MouseMode, MouseModeState,
202    TerminalCellSize, TerminalColorPalette, TerminalCopyMode, TerminalDecoration, TerminalKeyModes,
203    TerminalPasteShortcutBehavior, TerminalRenderSnapshot, terminal_selection_text,
204};
205#[cfg(feature = "terminal-images")]
206pub use crate::widgets::{TerminalImage, TerminalImageCrop, TerminalImagePlacement};
207
208#[cfg(feature = "diff-view")]
209pub use crate::widgets::{
210    DiffContextExpansion, DiffContextRange, DiffContextSeparatorDirection,
211    DiffContextSeparatorEvent, DiffData, DiffDataConfig, DiffHunkAnchor,
212};
213
214#[cfg(feature = "syntax-syntect")]
215pub use crate::widgets::{
216    SyntectDocumentFormatter, SyntectStrategy, apply_syntect_strategy_app_theme, language_from_path,
217};
218/// Macro for building `Element`s with struct-literal syntax.
219pub use tui_lipan_macro::rsx;
220/// Autocomplete-friendly macro for building `Element`s with builder chains.
221///
222/// Uses standard Rust builder syntax (full rust-analyzer autocomplete) with
223/// `=> { children }` sugar for nesting.
224pub use tui_lipan_macro::ui;
225
226/// One-liner macro for previewing a TUI layout without any component boilerplate.
227///
228/// The body expression is automatically converted via `.into()`, so you can
229/// return any widget builder directly (e.g. `Frame::new()...`) without calling
230/// `.into()` yourself.  The closure uses `move` capture, so local data can be
231/// referenced freely inside the body.
232///
233/// Press `Esc` or `q` to quit the preview.
234///
235/// # Basic usage
236///
237/// ```rust,no_run
238/// use tui_lipan::prelude::*;
239///
240/// fn main() -> Result<()> {
241///     mockup!("Dashboard", {
242///         Frame::new()
243///             .header_left("Panel")
244///             .border(true)
245///             .child(Text::new("Hello!"))
246///     })
247/// }
248/// ```
249///
250/// # With sample data (mockup → app workflow)
251///
252/// Extract your UI into plain functions that return `Element`, then reuse them
253/// in both mockup previews and real components:
254///
255/// ```rust,no_run
256/// use tui_lipan::prelude::*;
257///
258/// fn sidebar(items: &[&str], sel: usize) -> Element {
259///     Frame::new()
260///         .header_left("Nav")
261///         .border(true)
262///         .child(List::new()
263///             .items(items.iter().map(|s| ListItem::new(*s)))
264///             .selected(sel))
265///         .into()
266/// }
267///
268/// fn main() -> Result<()> {
269///     let items = vec!["Home", "Settings"];
270///     mockup!("Preview", {
271///         sidebar(&items, 0)
272///     })
273/// }
274/// ```
275#[cfg(not(target_arch = "wasm32"))]
276#[macro_export]
277macro_rules! mockup {
278    ($title:expr, $body:expr) => {
279        $crate::App::new()
280            .title($title)
281            .mount($crate::Mockup::new(move || { $body }.into()))
282            .run()
283    };
284}
285
286/// Create a nested component element.
287///
288/// This does not require `Component: Default`; pass a factory closure to construct the instance.
289pub fn child<C, F>(factory: F, props: C::Properties) -> Element
290where
291    C: Component,
292    F: Fn() -> C + 'static,
293{
294    Element::new(crate::core::element::ElementKind::Component(
295        crate::core::nested::ComponentElement::new::<C, F>(factory, props),
296    ))
297}
298
299/// Crate-wide result type.
300pub type Result<T> = std::result::Result<T, Error>;
301
302/// Crate-wide error type.
303#[derive(thiserror::Error, Debug)]
304pub enum Error {
305    /// I/O error.
306    #[error(transparent)]
307    Io(#[from] std::io::Error),
308
309    /// Syntax theme loading error.
310    #[error("failed to load syntax theme `{name}`: {message}")]
311    SyntaxThemeLoad {
312        /// Theme name.
313        name: String,
314        /// Error details.
315        message: String,
316        /// Error message.
317        #[source]
318        error: Option<Box<dyn std::error::Error + Send + Sync>>,
319    },
320
321    /// Internal message routing error.
322    #[error(
323        "message type mismatch for component `{component}` (expected `{expected}`, got `{actual}`)"
324    )]
325    MessageTypeMismatch {
326        /// Type name of the component that received the message.
327        component: &'static str,
328        /// Expected message type name.
329        expected: &'static str,
330        /// Actual message type name.
331        actual: &'static str,
332    },
333
334    /// Internal properties routing error.
335    #[error(
336        "props type mismatch for component `{component}` (expected `{expected}`, got `{actual}`)"
337    )]
338    PropsTypeMismatch {
339        /// Type name of the component that received the props.
340        component: &'static str,
341        /// Expected props type name.
342        expected: &'static str,
343        /// Actual props type name.
344        actual: &'static str,
345    },
346
347    /// Component expansion failure (props mismatch or mount failure during tree expansion).
348    #[error("component expansion failed: {reason}")]
349    ComponentExpansion {
350        /// Human-readable description of the failure.
351        reason: String,
352    },
353
354    /// Theme reload error.
355    #[error("theme reload error: {message}")]
356    ThemeReload {
357        /// Error details.
358        message: String,
359    },
360}
361
362#[cfg(test)]
363#[test]
364fn inline_transcript_append_rejects_component_nodes() {
365    crate::runtime::assert_inline_transcript_append_rejects_component_nodes();
366}
367
368#[cfg(test)]
369#[test]
370fn inline_surface_commit_render_path_is_unified() {
371    crate::runtime::assert_inline_surface_commit_render_path_is_unified();
372}
373
374#[cfg(all(test, not(target_arch = "wasm32")))]
375#[test]
376fn inline_surface_internal_wrap_policy_is_opaque() {
377    crate::backend::ratatui_backend::assert_inline_surface_internal_wrap_policy_is_opaque();
378}