rich_interact/components/
theme.rs1use 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
18pub 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
30struct 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#[must_use = "a component does nothing until it is run or mounted"]
86pub struct ThemePicker {
87 select: Select<String>,
88}
89
90impl ThemePicker {
91 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 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 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 pub fn height(mut self, rows: usize) -> Self {
144 self.select = self.select.height(rows);
145 self
146 }
147
148 pub fn preview(mut self, layout: PreviewLayout) -> Self {
150 self.select = self.select.preview(layout);
151 self
152 }
153
154 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 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 pub fn with_mouse(mut self, on: bool) -> Self {
181 self.select = self.select.with_mouse(on);
182 self
183 }
184
185 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}