Skip to main content

rich_interact/components/
theme.rs

1//! A live theme picker (#460): each theme's preview is the same sample,
2//! rendered with that theme's styles, so moving through the list shows the
3//! difference as you go.
4
5use std::sync::Arc;
6use std::time::Duration;
7
8use rich::measure::Measurement;
9use rich::{Console, ConsoleOptions, Renderable, Segment, Text};
10
11use crate::component::{Component, Context, Flow, View};
12use crate::components::{PreviewLayout, Select, Theme};
13use crate::event::Event;
14use crate::item::{Item, Preview};
15use crate::keymap::Keymap;
16use crate::policy::{LineIo, NotInteractive};
17
18/// The markup a [`ThemePicker`] previews by default: the semantic names
19/// (`info` … `error`), repr and JSON highlighting, logging and a rule, so
20/// most of what a theme restyles shows.
21pub const THEME_SAMPLE: &str = "[bold]Theme preview[/]
22[info]info[/]  [success]success[/]  [warning]warning[/]  [error]error[/]
23[repr.number]1234[/] [repr.str]'a string'[/] [repr.bool_true]True[/] [repr.bool_false]False[/] [repr.none]None[/]
24[repr.url]https://example.com[/]  [repr.path]/usr/local/bin[/]
25[json.key]\"name\"[/]: [json.str]\"rich\"[/], [json.key]\"ok\"[/]: [json.bool_true]true[/]
26[log.time][12:00:01][/] [logging.level.info]INFO[/]     [log.message]started[/]
27[logging.level.warning]WARNING[/]  [logging.level.error]ERROR[/]  [rule.line]────────[/]
28[markdown.h1]Heading[/]  [markdown.code]code[/]  [markdown.link]link[/]";
29
30/// `sample` rendered with `theme` layered over the extended theme
31/// ([`rich_ext::theme::extended_theme`]), in the colours of the console
32/// rendering it.
33struct Themed {
34    theme: rich::Theme,
35    sample: Arc<dyn Renderable + Send + Sync>,
36}
37
38impl Themed {
39    fn console(&self, outer: &Console, options: &ConsoleOptions) -> Console {
40        let mut theme = rich_ext::theme::extended_theme();
41        theme.extend_from(&self.theme);
42        Console::builder()
43            .width(options.max_width.max(1))
44            .force_terminal(true)
45            .color_system(outer.color_system())
46            .no_color(outer.no_color())
47            .theme(theme)
48            .build()
49    }
50}
51
52impl Renderable for Themed {
53    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
54        let inner = self.console(console, options);
55        let options = inner.options().update_width(options.max_width.max(1));
56        inner.render(&*self.sample, Some(&options))
57    }
58
59    fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
60        let inner = self.console(console, options);
61        self.sample.measure(&inner, options)
62    }
63}
64
65/// Pick a theme while its preview shows a sample rendered in it (#460).
66/// Returns the theme's name.
67///
68/// Themes come from anywhere: a configuration file's `[themes.NAME]`, a
69/// plugin's (`ExtensionRegistry::theme`), or code. Each is layered over
70/// the [extended theme](rich_ext::theme::extended_theme), so a theme that
71/// sets only a few names previews with the rest at their defaults.
72///
73/// ```
74/// use rich::{Style, Theme};
75/// use rich_interact::components::ThemePicker;
76/// use rich_interact::headless::{self, Script};
77///
78/// let mut night = Theme::new();
79/// night.insert("info", Style::parse("bold blue").unwrap());
80/// let picker = ThemePicker::new("Theme", [("default", Theme::new()), ("night", night)]);
81/// let (outcome, record) = headless::run(picker, Script::new().keys("down enter"), 80, 14);
82/// assert_eq!(outcome.unwrap().value().as_deref(), Some("night"));
83/// assert!(record.frames[0].contains("Theme preview"));
84/// ```
85#[must_use = "a component does nothing until it is run or mounted"]
86pub struct ThemePicker {
87    select: Select<String>,
88}
89
90impl ThemePicker {
91    /// Pick one of `themes`, by name, previewing [`THEME_SAMPLE`].
92    pub fn new<I, S>(prompt: impl Into<String>, themes: I) -> ThemePicker
93    where
94        I: IntoIterator<Item = (S, rich::Theme)>,
95        S: Into<String>,
96    {
97        let sample = Text::from_markup(THEME_SAMPLE).expect("the sample is valid markup");
98        Self::with_sample(prompt, themes, Arc::new(sample))
99    }
100
101    /// Pick one of `themes`, previewing `sample` in each.
102    pub fn with_sample<I, S>(
103        prompt: impl Into<String>,
104        themes: I,
105        sample: Arc<dyn Renderable + Send + Sync>,
106    ) -> ThemePicker
107    where
108        I: IntoIterator<Item = (S, rich::Theme)>,
109        S: Into<String>,
110    {
111        let items: Vec<Item<String>> = themes
112            .into_iter()
113            .map(|(name, theme)| {
114                let name = name.into();
115                let preview = Preview::Renderable(Arc::new(Themed {
116                    theme,
117                    sample: Arc::clone(&sample),
118                }));
119                Item::new(name.clone(), name).preview(preview)
120            })
121            .collect();
122        ThemePicker {
123            select: Select::new(prompt, items),
124        }
125    }
126
127    /// Pick one of the themes plugins registered in `registry` (theme
128    /// packs), after rich's own default theme, listed as `default`.
129    pub fn from_registry(
130        prompt: impl Into<String>,
131        registry: &rich_ext::registry::ExtensionRegistry,
132    ) -> ThemePicker {
133        let themes = std::iter::once(("default".to_string(), rich::Theme::new())).chain(
134            registry.theme_names().into_iter().filter_map(|name| {
135                let theme = registry.theme(name)?.clone();
136                Some((name.to_string(), theme))
137            }),
138        );
139        ThemePicker::new(prompt, themes)
140    }
141
142    /// Show at most `rows` themes at once (default 10).
143    pub fn height(mut self, rows: usize) -> Self {
144        self.select = self.select.height(rows);
145        self
146    }
147
148    /// Where the preview goes (default: beside the list from 72 columns).
149    pub fn preview(mut self, layout: PreviewLayout) -> Self {
150        self.select = self.select.preview(layout);
151        self
152    }
153
154    /// Rows the preview takes when it is below the list (default 10).
155    pub fn preview_height(mut self, rows: usize) -> Self {
156        self.select = self.select.preview_height(rows);
157        self
158    }
159
160    pub fn theme(mut self, theme: Theme) -> Self {
161        self.select = self.select.theme(theme);
162        self
163    }
164
165    /// Focus the theme called `name` first; it is also the answer without
166    /// a terminal.
167    pub fn default(mut self, name: &str) -> Self {
168        if let Some(index) = self
169            .select
170            .items()
171            .iter()
172            .position(|item| item.label == name)
173        {
174            self.select = self.select.default(index);
175        }
176        self
177    }
178
179    /// Report the mouse: see [`Select::with_mouse`].
180    pub fn with_mouse(mut self, on: bool) -> Self {
181        self.select = self.select.with_mouse(on);
182        self
183    }
184
185    /// The themes' names, in order.
186    pub fn names(&self) -> Vec<&str> {
187        self.select
188            .items()
189            .iter()
190            .map(|item| item.label.as_str())
191            .collect()
192    }
193}
194
195impl Component for ThemePicker {
196    type Output = String;
197
198    fn keymap(&self) -> Keymap {
199        self.select.visible_keymap()
200    }
201
202    fn handle(&mut self, event: &Event, context: &Context<'_>) -> Flow<String> {
203        self.select.handle(event, context)
204    }
205
206    fn render(&self, context: &Context<'_>) -> View {
207        self.select.render(context)
208    }
209
210    fn tick(&self) -> Option<Duration> {
211        self.select.tick_interval()
212    }
213
214    fn mouse(&self) -> bool {
215        Component::mouse(&self.select)
216    }
217
218    fn default_value(&self) -> Option<String> {
219        self.select.default_value()
220    }
221
222    fn prompt(&mut self, io: &mut dyn LineIo) -> Result<Option<String>, NotInteractive> {
223        self.select.prompt(io)
224    }
225}