Skip to main content

tui_lipan/app/
context.rs

1use crate::app::input::key_dispatch::{
2    ChordMismatchPolicy, CommandConflictPolicy, KeyDispatchPolicy, TerminalKeyPolicy,
3};
4use crate::app::input::keymap::{FrameworkAction, FrameworkKeymap, UserKeymapPolicy};
5#[cfg(not(target_arch = "wasm32"))]
6use crate::app::runner::AppRunner;
7use crate::clipboard::{ClipboardConfig, ClipboardError, ClipboardProvider, ClipboardReporter};
8#[cfg(not(target_arch = "wasm32"))]
9use crate::core::component::Component;
10use crate::input::KeyBindings;
11use crate::layout::tag::Tag;
12use crate::overlay::ToastPlacement;
13use crate::style::Padding;
14use crate::style::{Color, Paint, Style, Theme};
15use std::path::PathBuf;
16use std::sync::Arc;
17use std::time::Duration;
18
19/// How the app occupies terminal space.
20#[derive(Clone, Debug, Default, PartialEq, Eq)]
21pub(crate) enum ViewportMode {
22    /// Take over the full terminal using the alternate screen.
23    #[default]
24    Fullscreen,
25    /// Render inline at the current cursor position.
26    Inline { height: InlineHeight },
27}
28
29/// Height policy for inline viewports.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum InlineHeight {
32    /// A fixed number of rows, clamped to at least one row.
33    Fixed(u16),
34    /// Follow the content's measured height, re-sizing the viewport as the
35    /// view changes.
36    Auto {
37        /// Optional row cap. Regardless of the cap, the viewport never grows
38        /// past the host terminal height.
39        max: Option<u16>,
40    },
41}
42
43impl InlineHeight {
44    /// Content-sized height, capped only by the host terminal height.
45    pub const fn auto() -> Self {
46        Self::Auto { max: None }
47    }
48
49    /// Content-sized height, capped at `max` rows.
50    pub const fn auto_capped(max: u16) -> Self {
51        Self::Auto { max: Some(max) }
52    }
53
54    pub(crate) fn normalized(self) -> Self {
55        match self {
56            Self::Fixed(rows) => Self::Fixed(rows.max(1)),
57            Self::Auto { max } => Self::Auto {
58                max: max.map(|rows| rows.max(1)),
59            },
60        }
61    }
62
63    /// Rows to reserve for the viewport before the first frame is measured.
64    pub(crate) fn initial_rows(self) -> u16 {
65        match self {
66            Self::Fixed(rows) => rows.max(1),
67            // Auto starts minimal: the first render measures the content and
68            // grows the viewport before anything is painted.
69            Self::Auto { .. } => 1,
70        }
71    }
72}
73
74impl From<u16> for InlineHeight {
75    fn from(rows: u16) -> Self {
76        Self::Fixed(rows)
77    }
78}
79
80/// Startup behavior for transcript inline mode.
81#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
82pub enum InlineStartupPolicy {
83    /// Preserve host terminal content above the inline viewport.
84    #[default]
85    PreserveHost,
86    /// Clear the host terminal before the first inline render.
87    ClearHost,
88}
89
90/// Public surface mode taxonomy for app rendering.
91#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
92pub enum SurfaceMode {
93    /// Take over the full terminal using the alternate screen.
94    #[default]
95    Fullscreen,
96    /// Inline viewport intended for ephemeral UI sessions.
97    InlineEphemeral {
98        /// Requested inline viewport height.
99        height: InlineHeight,
100    },
101    /// Inline viewport intended for transcript-friendly sessions.
102    InlineTranscript {
103        /// Requested inline viewport height.
104        height: InlineHeight,
105        /// Startup behavior for the host terminal.
106        startup: InlineStartupPolicy,
107    },
108}
109
110impl SurfaceMode {
111    pub(crate) fn is_inline(&self) -> bool {
112        !matches!(self, Self::Fullscreen)
113    }
114
115    pub(crate) fn normalized(self) -> Self {
116        match self {
117            Self::Fullscreen => Self::Fullscreen,
118            Self::InlineEphemeral { height } => Self::InlineEphemeral {
119                height: height.normalized(),
120            },
121            Self::InlineTranscript { height, startup } => Self::InlineTranscript {
122                height: height.normalized(),
123                startup,
124            },
125        }
126    }
127
128    pub(crate) fn viewport_mode(self) -> ViewportMode {
129        match self.normalized() {
130            Self::Fullscreen => ViewportMode::Fullscreen,
131            Self::InlineEphemeral { height } | Self::InlineTranscript { height, .. } => {
132                ViewportMode::Inline { height }
133            }
134        }
135    }
136
137    pub(crate) fn clear_on_start(self) -> bool {
138        matches!(
139            self,
140            Self::InlineTranscript {
141                startup: InlineStartupPolicy::ClearHost,
142                ..
143            }
144        )
145    }
146}
147
148/// Controls which Enter key combinations insert new lines in `TextArea`.
149#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
150pub enum TextAreaNewlineBinding {
151    /// Use plain Enter.
152    #[default]
153    Enter,
154    /// Use Shift+Enter only.
155    ShiftEnter,
156    /// Accept both Enter and Shift+Enter.
157    EnterOrShiftEnter,
158}
159
160/// Controls framework-initiated focus movement.
161#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
162pub enum FocusPolicy {
163    /// Focus the first focusable widget at startup and whenever focus cannot be restored.
164    Auto,
165    /// Start unfocused, then allow Tab and pointer interaction to establish focus.
166    #[default]
167    OnDemand,
168    /// Never move focus through global traversal or pointer interaction.
169    ///
170    /// Explicit focus requests and focus traversal helpers remain available. Capturing overlays
171    /// also continue to establish and trap focus.
172    Manual,
173}
174
175/// Public identity of a focused widget at a focus transition boundary.
176#[derive(Clone, Debug, PartialEq, Eq)]
177pub struct FocusEntry {
178    /// Optional stable widget key.
179    pub key: Option<crate::core::element::Key>,
180    /// Widget kind.
181    pub tag: Tag,
182}
183
184/// App-level focus transition payload.
185#[derive(Clone, Debug, PartialEq, Eq)]
186pub struct FocusChanged {
187    /// Previously focused widget, if any.
188    pub old: Option<FocusEntry>,
189    /// Newly focused widget, if any.
190    pub new: Option<FocusEntry>,
191}
192
193pub(crate) type FocusChangedHook = std::rc::Rc<dyn Fn(&FocusChanged)>;
194
195/// One host-application metric displayed by the built-in DevTools panel.
196///
197/// Rows are intentionally just a terse label/value pair. The application
198/// supplies their display order when calling
199/// [`Context::set_devtools_metrics`](crate::Context::set_devtools_metrics).
200#[derive(Clone, Debug, PartialEq, Eq)]
201pub struct DevToolsMetric {
202    /// Short metric label, such as `"Panes"` or `"Queue"`.
203    pub label: Arc<str>,
204    /// Already-formatted metric value, such as `"12"` or `"3.2 MiB"`.
205    pub value: Arc<str>,
206}
207
208impl DevToolsMetric {
209    /// Create a metric row from a label and its formatted value.
210    pub fn new(label: impl Into<Arc<str>>, value: impl Into<Arc<str>>) -> Self {
211        Self {
212            label: label.into(),
213            value: value.into(),
214        }
215    }
216}
217
218/// Controls automatic foreground contrast adjustments for widget text.
219#[cfg_attr(
220    feature = "terminal-serde",
221    derive(serde::Serialize, serde::Deserialize)
222)]
223#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
224pub enum ContrastPolicy {
225    /// Keep user-provided foreground colors unchanged.
226    Off,
227    /// Auto-adjust foreground on colored backgrounds when contrast is too low
228    /// using WCAG 2.1 contrast ratio (AA normal text: >= 4.5:1).
229    #[default]
230    Wcag,
231    /// Keep the current foreground when it is readable under WCAG 2.1;
232    /// otherwise snap to black or white, whichever has higher contrast.
233    BlackOrWhite,
234    /// Auto-adjust using APCA perceptual contrast (WCAG 3.0 draft).
235    ///
236    /// Better for dark themes and polarity-aware readability. Uses `|Lc|` >= 60
237    /// as the minimum body-text threshold.
238    Apca,
239}
240
241#[cfg(all(test, feature = "terminal-serde"))]
242mod terminal_serde_tests {
243    use super::*;
244
245    #[test]
246    fn contrast_policy_round_trips() {
247        let policy = ContrastPolicy::BlackOrWhite;
248        let json = serde_json::to_string(&policy).unwrap();
249        assert_eq!(
250            serde_json::from_str::<ContrastPolicy>(&json).unwrap(),
251            policy
252        );
253    }
254}
255
256/// Runtime devtools subsystem configuration.
257#[cfg(feature = "devtools")]
258#[derive(Clone, Copy, Debug, PartialEq, Eq)]
259pub struct DevToolsConfig {
260    /// Enable devtools log ingestion and sink wiring.
261    pub logs: bool,
262    /// Enable runtime frame metrics collection.
263    pub metrics: bool,
264    /// Show tui-lipan's own framework-internal logs in the log view.
265    ///
266    /// Set to `false` so the devtools log view starts with framework noise
267    /// (key events, dirty tracking, etc.) hidden, showing only the host
268    /// application's own `debug_log!` lines. Can still be toggled at runtime
269    /// from the "tui-lipan" button in the Logs tab.
270    pub show_framework_logs: bool,
271}
272
273#[cfg(feature = "devtools")]
274impl Default for DevToolsConfig {
275    fn default() -> Self {
276        Self {
277            logs: true,
278            metrics: true,
279            show_framework_logs: true,
280        }
281    }
282}
283
284/// How the root viewport background is painted before the UI tree renders.
285///
286/// By default the framework paints nothing behind the tree, so the host
287/// terminal background shows through ([`Transparent`](Self::Transparent)). Opt
288/// into a filled background when you want a fully "designed" surface rather than
289/// text floating on the terminal color — useful for kiosk-style apps, themes
290/// with a strong identity, or matching a brand backdrop.
291///
292/// ```no_run
293/// use tui_lipan::prelude::*;
294///
295/// // Fill with the active theme's backdrop surface:
296/// let app = App::new().theme(Theme::lipan()).fill_background();
297///
298/// // Or an explicit color:
299/// let app = App::new().screen_background(Color::hex_u24(0x04090D));
300/// ```
301#[derive(Clone, Copy, Debug, Default, PartialEq)]
302pub enum ScreenBackground {
303    /// Leave the host terminal background untouched. (default)
304    #[default]
305    Transparent,
306    /// Fill the viewport with the active root theme's backdrop surface
307    /// ([`Theme::surface`]`.backdrop`).
308    ///
309    /// This tracks the theme of the realized root node, so apps that swap themes
310    /// at runtime via a root `ThemeProvider` (rather than rebuilding the `App`)
311    /// keep the backdrop in sync without any extra wiring.
312    Theme,
313    /// Fill the viewport with an explicit style (typically just a background color).
314    Custom(Style),
315}
316
317impl ScreenBackground {
318    /// Resolve to the concrete fill style for `theme`, or `None` when nothing
319    /// should be painted.
320    pub(crate) fn resolve(self, theme: &Theme) -> Option<Style> {
321        match self {
322            Self::Transparent => None,
323            Self::Theme => Some(Style::new().bg(theme.surface.backdrop)),
324            Self::Custom(style) => (!style.is_empty()).then_some(style),
325        }
326    }
327}
328
329impl From<Style> for ScreenBackground {
330    fn from(style: Style) -> Self {
331        Self::Custom(style)
332    }
333}
334
335impl From<Color> for ScreenBackground {
336    fn from(color: Color) -> Self {
337        Self::Custom(Style::new().bg(color))
338    }
339}
340
341impl From<Paint> for ScreenBackground {
342    fn from(paint: Paint) -> Self {
343        Self::Custom(Style::new().bg(paint))
344    }
345}
346
347/// Application builder.
348pub struct App {
349    pub(crate) title: Option<String>,
350    pub(crate) surface_mode: SurfaceMode,
351    pub(crate) mouse_enabled: Option<bool>,
352    pub(crate) scroll_wheel_multiplier: u16,
353    pub(crate) theme: Theme,
354    pub(crate) toast_placement: ToastPlacement,
355    pub(crate) toast_gap: u16,
356    pub(crate) toast_margin: Padding,
357    pub(crate) clipboard_config: ClipboardConfig,
358    pub(crate) keymap_path: Option<PathBuf>,
359    pub(crate) framework_keymap: FrameworkKeymap,
360    pub(crate) user_keymap_policy: UserKeymapPolicy,
361    pub(crate) key_dispatch_policy: KeyDispatchPolicy,
362    pub(crate) focus_policy: FocusPolicy,
363    pub(crate) on_focus_changed: Option<FocusChangedHook>,
364    pub(crate) terminal_key_policy: TerminalKeyPolicy,
365    pub(crate) command_conflict_policy: CommandConflictPolicy,
366    pub(crate) chord_mismatch_policy: ChordMismatchPolicy,
367    pub(crate) command_chord_reveal_delay: Duration,
368    pub(crate) text_area_newline_binding: TextAreaNewlineBinding,
369    pub(crate) contrast_policy: ContrastPolicy,
370    pub(crate) clipboard_provider: Option<Box<dyn ClipboardProvider>>,
371    pub(crate) clipboard_reporter: ClipboardReporter,
372    pub(crate) terminal_bg: Option<Color>,
373    pub(crate) live_host_terminal_colors: bool,
374    pub(crate) system_theme: bool,
375    pub(crate) screen_background: ScreenBackground,
376    #[cfg(feature = "devtools")]
377    pub(crate) devtools_config: DevToolsConfig,
378}
379
380impl Default for App {
381    fn default() -> Self {
382        let theme = Theme::default();
383        Self {
384            title: None,
385            surface_mode: SurfaceMode::default(),
386            mouse_enabled: None,
387            scroll_wheel_multiplier: 1,
388            theme,
389            toast_placement: ToastPlacement::default(),
390            toast_gap: 1,
391            toast_margin: Padding::BORDER,
392            clipboard_config: ClipboardConfig::default(),
393            keymap_path: None,
394            framework_keymap: FrameworkKeymap::default(),
395            user_keymap_policy: UserKeymapPolicy::default(),
396            key_dispatch_policy: KeyDispatchPolicy::WidgetFirst,
397            focus_policy: FocusPolicy::default(),
398            on_focus_changed: None,
399            terminal_key_policy: TerminalKeyPolicy::FrameworkFirst,
400            command_conflict_policy: CommandConflictPolicy::default(),
401            chord_mismatch_policy: ChordMismatchPolicy::default(),
402            command_chord_reveal_delay: Duration::ZERO,
403            text_area_newline_binding: TextAreaNewlineBinding::default(),
404            contrast_policy: ContrastPolicy::default(),
405            clipboard_provider: None,
406            clipboard_reporter: crate::clipboard::default_clipboard_reporter(),
407            terminal_bg: None,
408            live_host_terminal_colors: false,
409            system_theme: false,
410            screen_background: ScreenBackground::default(),
411            #[cfg(feature = "devtools")]
412            devtools_config: DevToolsConfig::default(),
413        }
414    }
415}
416
417impl App {
418    /// Create a new app.
419    pub fn new() -> Self {
420        Self::default()
421    }
422
423    /// Set the terminal window title (via OSC 2 escape sequence).
424    pub fn title(mut self, title: impl Into<String>) -> Self {
425        self.title = Some(title.into());
426        self
427    }
428
429    /// Set the app surface mode explicitly.
430    pub fn surface(mut self, mode: SurfaceMode) -> Self {
431        self.surface_mode = mode.normalized();
432        self
433    }
434
435    /// Use fullscreen alternate-screen rendering.
436    pub fn fullscreen(self) -> Self {
437        self.surface(SurfaceMode::Fullscreen)
438    }
439
440    /// Render the app inline for ephemeral (non-transcript) sessions.
441    ///
442    /// Accepts a fixed row count (clamped to at least one row) or an
443    /// [`InlineHeight`] policy such as [`InlineHeight::auto()`], which sizes
444    /// the viewport to the content every frame.
445    pub fn inline_ephemeral(self, height: impl Into<InlineHeight>) -> Self {
446        self.surface(SurfaceMode::InlineEphemeral {
447            height: height.into(),
448        })
449    }
450
451    /// Render the app inline for transcript sessions.
452    ///
453    /// Accepts a fixed row count or an [`InlineHeight`] policy such as
454    /// [`InlineHeight::auto()`]. Defaults to preserving host terminal content
455    /// on startup.
456    pub fn inline_transcript(self, height: impl Into<InlineHeight>) -> Self {
457        self.surface(SurfaceMode::InlineTranscript {
458            height: height.into(),
459            startup: InlineStartupPolicy::PreserveHost,
460        })
461    }
462
463    /// Render the app inline for transcript sessions with explicit startup behavior.
464    ///
465    /// Accepts a fixed row count (clamped to at least one row) or an
466    /// [`InlineHeight`] policy such as [`InlineHeight::auto()`].
467    pub fn inline_transcript_with_startup(
468        self,
469        height: impl Into<InlineHeight>,
470        startup: InlineStartupPolicy,
471    ) -> Self {
472        self.surface(SurfaceMode::InlineTranscript {
473            height: height.into(),
474            startup,
475        })
476    }
477
478    /// Configure mouse capture behavior.
479    ///
480    /// This sets the initial runtime state. Components can later change it with
481    /// `Context::set_mouse_capture(...)` or `Context::toggle_mouse_capture()`.
482    ///
483    /// Defaults:
484    /// - fullscreen mode: enabled
485    /// - inline mode: disabled
486    pub fn mouse(mut self, enabled: bool) -> Self {
487        self.mouse_enabled = Some(enabled);
488        self
489    }
490
491    /// Set the app-wide mouse wheel step multiplier.
492    ///
493    /// Each wheel tick scrolls `multiplier` lines instead of the default single
494    /// line. Coalesced wheel bursts multiply by this value too, so two ticks with
495    /// `multiplier = 3` scroll six lines total.
496    pub fn scroll_wheel_multiplier(mut self, multiplier: u16) -> Self {
497        self.scroll_wheel_multiplier = multiplier.max(1);
498        self
499    }
500
501    /// Set the app-wide default theme.
502    ///
503    /// This theme is applied to the root tree every render.
504    /// Use `ThemeProvider` to override a specific subtree.
505    pub fn theme(mut self, theme: Theme) -> Self {
506        self.theme = theme;
507        self
508    }
509
510    /// Paint the root viewport background before rendering the UI tree.
511    ///
512    /// By default the framework paints nothing behind the tree (the host
513    /// terminal background shows through). This opts into a filled background so
514    /// the UI reads as a designed surface. Accepts a [`Color`], [`Paint`],
515    /// [`Style`], or a [`ScreenBackground`] directly:
516    ///
517    /// ```no_run
518    /// use tui_lipan::prelude::*;
519    ///
520    /// let app = App::new().screen_background(Color::hex_u24(0x04090D));
521    /// ```
522    ///
523    /// Use [`fill_background`](Self::fill_background) to track the active theme's
524    /// backdrop automatically.
525    pub fn screen_background(mut self, background: impl Into<ScreenBackground>) -> Self {
526        self.screen_background = background.into();
527        self
528    }
529
530    /// Fill the root viewport with the active theme's backdrop surface.
531    ///
532    /// Shorthand for `screen_background(ScreenBackground::Theme)`. The fill tracks
533    /// the app theme, so swapping themes keeps the backdrop in sync.
534    pub fn fill_background(mut self) -> Self {
535        self.screen_background = ScreenBackground::Theme;
536        self
537    }
538
539    /// Set where toasts appear on screen.
540    pub fn toast_placement(mut self, placement: ToastPlacement) -> Self {
541        self.toast_placement = placement;
542        self
543    }
544
545    /// Set vertical gap between stacked toasts.
546    pub fn toast_gap(mut self, gap: u16) -> Self {
547        self.toast_gap = gap;
548        self
549    }
550
551    /// Set outside margin between toasts and the viewport edge.
552    pub fn toast_margin(mut self, margin: impl Into<Padding>) -> Self {
553        self.toast_margin = margin.into();
554        self
555    }
556
557    /// Configure clipboard behavior.
558    pub fn clipboard_config(mut self, config: ClipboardConfig) -> Self {
559        self.clipboard_config = config;
560        self
561    }
562
563    /// Use a specific keymap file path for this app instance.
564    ///
565    /// This path has higher priority than `TUI_LIPAN_KEYMAP` and the default
566    /// `$XDG_CONFIG_HOME/tui-lipan/keymap.conf` fallback.
567    pub fn keymap_path(mut self, path: impl Into<PathBuf>) -> Self {
568        self.keymap_path = Some(path.into());
569        self
570    }
571
572    /// Override framework key bindings from Rust after file and built-in keymaps are loaded.
573    pub fn framework_keymap(mut self, keymap: FrameworkKeymap) -> Self {
574        self.framework_keymap = keymap;
575        self
576    }
577
578    /// Configure the global quit shortcut. `None` disables framework quit bindings.
579    pub fn global_quit(mut self, bindings: Option<KeyBindings>) -> Self {
580        self.framework_keymap = match bindings {
581            Some(bindings) => self.framework_keymap.bind(FrameworkAction::Quit, bindings),
582            None => self.framework_keymap.unbind(FrameworkAction::Quit),
583        };
584        self
585    }
586
587    /// Enable or disable loading user keymap files.
588    pub fn user_keymap_policy(mut self, policy: UserKeymapPolicy) -> Self {
589        self.user_keymap_policy = policy;
590        self
591    }
592
593    /// Configure app command versus widget key dispatch ordering.
594    pub fn key_dispatch_policy(mut self, policy: KeyDispatchPolicy) -> Self {
595        self.key_dispatch_policy = policy;
596        self
597    }
598
599    /// Delay before a pending command chord is reported as *revealed*.
600    ///
601    /// [`Context::command_chord_pending`](crate::Context::command_chord_pending) stays immediate;
602    /// this governs [`Context::command_chord_revealed`](crate::Context::command_chord_revealed)
603    /// only, which is the signal a which-key panel or similar chord affordance should read. The
604    /// runtime schedules a frame at the moment the delay elapses, so such a view needs no timer of
605    /// its own. The default is [`Duration::ZERO`] - revealed as soon as it is pending.
606    pub fn command_chord_reveal_delay(mut self, delay: Duration) -> Self {
607        self.command_chord_reveal_delay = delay;
608        self
609    }
610
611    /// Configure framework-initiated focus movement.
612    pub fn focus_policy(mut self, policy: FocusPolicy) -> Self {
613        self.focus_policy = policy;
614        self
615    }
616
617    /// Observe completed focus transitions after widget blur/focus callbacks are emitted.
618    pub fn on_focus_changed(mut self, hook: impl Fn(&FocusChanged) + 'static) -> Self {
619        self.on_focus_changed = Some(std::rc::Rc::new(hook));
620        self
621    }
622
623    /// Configure key dispatch behavior while terminal widgets are focused.
624    pub fn terminal_key_policy(mut self, policy: TerminalKeyPolicy) -> Self {
625        self.terminal_key_policy = policy;
626        self
627    }
628
629    /// Configure how app command shortcut conflicts are resolved.
630    pub fn command_conflict_policy(mut self, policy: CommandConflictPolicy) -> Self {
631        self.command_conflict_policy = policy;
632        self
633    }
634
635    /// Configure how mismatched keys are handled during pending command chords.
636    pub fn chord_mismatch_policy(mut self, policy: ChordMismatchPolicy) -> Self {
637        self.chord_mismatch_policy = policy;
638        self
639    }
640
641    /// Configure which Enter key combination inserts new lines in `TextArea`.
642    ///
643    /// This policy is scoped to `TextArea` and does not change single-line
644    /// `Input` behavior.
645    pub fn text_area_newline_binding(mut self, binding: TextAreaNewlineBinding) -> Self {
646        self.text_area_newline_binding = binding;
647        self
648    }
649
650    /// Configure app-wide text contrast behavior for interactive widget states.
651    ///
652    /// Individual styles can override this per-state by setting
653    /// `Style::contrast_policy(...)` on the relevant style (base, hover,
654    /// selection, focus, theme role, etc.).
655    pub fn contrast_policy(mut self, policy: ContrastPolicy) -> Self {
656        self.contrast_policy = policy;
657        self
658    }
659
660    /// Provide a custom clipboard provider implementation.
661    pub fn clipboard_provider(mut self, provider: impl ClipboardProvider + 'static) -> Self {
662        self.clipboard_provider = Some(Box::new(provider));
663        self
664    }
665
666    /// Provide a custom clipboard error reporter.
667    pub fn clipboard_reporter(mut self, reporter: impl Fn(ClipboardError) + 'static) -> Self {
668        self.clipboard_reporter = std::rc::Rc::new(reporter);
669        self
670    }
671
672    /// Set the resolved terminal background color.
673    ///
674    /// When set, [`crate::style::ColorTransform::Opacity`] can blend foreground colors
675    /// toward the real terminal background even when a cell's background is
676    /// [`Color::Reset`].  Obtain this value from [`crate::style::query_host_colors()`]
677    /// before starting the app:
678    ///
679    /// ```ignore
680    /// let bg = query_host_colors().map(|c| c.bg);
681    /// App::new().terminal_bg(bg).run(MyComponent);
682    /// ```
683    pub fn terminal_bg(mut self, color: Option<Color>) -> Self {
684        self.terminal_bg = color;
685        self
686    }
687
688    /// Enable runner-managed host terminal color refreshes.
689    ///
690    /// When enabled, the runner queries the host terminal palette before component
691    /// init, refreshes on terminal focus gained, and services
692    /// `Context::request_host_terminal_color_refresh()` on the UI thread while
693    /// coordinating with tui-lipan's input reader. Refreshed colors are exposed
694    /// through `Context::host_terminal_colors()` and the resolved terminal
695    /// background is kept in sync for opacity blending.
696    ///
697    /// On Unix fullscreen surfaces, compatible terminals that implement DEC
698    /// private mode 2031 also trigger an immediate refresh when their palette
699    /// changes. Inline, non-Unix, and unsupported terminals retain startup,
700    /// focus-gained, and manual refresh behavior.
701    ///
702    /// Disabled by default so static apps do not poll the terminal.
703    pub fn live_host_terminal_colors(mut self, enabled: bool) -> Self {
704        self.live_host_terminal_colors = enabled;
705        self
706    }
707
708    /// Use the host terminal palette as the app theme once colors are probed.
709    ///
710    /// The current app theme remains the fallback until the runner successfully
711    /// receives host colors. Later failed refreshes keep the last applied theme.
712    /// Unix fullscreen surfaces also subscribe to compatible terminals' DEC mode
713    /// 2031 palette-change notifications while the app is running.
714    pub fn system_theme(mut self) -> Self {
715        self.system_theme = true;
716        self
717    }
718
719    /// Configure runtime devtools subsystem behavior.
720    #[cfg(feature = "devtools")]
721    pub fn devtools_config(mut self, config: DevToolsConfig) -> Self {
722        self.devtools_config = config;
723        self
724    }
725
726    /// Mount the root component with default properties.
727    #[cfg(not(target_arch = "wasm32"))]
728    pub fn mount<C>(self, component: C) -> AppRunner<C>
729    where
730        C: Component,
731        C::Properties: Default,
732    {
733        self.mount_with_props(component, C::Properties::default())
734    }
735
736    /// Mount the root component with explicit properties.
737    #[cfg(not(target_arch = "wasm32"))]
738    pub fn mount_with_props<C>(self, component: C, props: C::Properties) -> AppRunner<C>
739    where
740        C: Component,
741    {
742        AppRunner::new(self, component, props)
743    }
744}