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}