ratatui_explorer/widget.rs
1use std::sync::Arc;
2
3use ratatui::{
4 buffer::Buffer,
5 layout::Rect,
6 style::{Color, Style},
7 text::{Line, Span, Text},
8 widgets::{Block, Borders, HighlightSpacing, List, ListState, WidgetRef},
9};
10
11use crate::{File, FileExplorer};
12
13type LineFactory = Arc<dyn Fn(&FileExplorer) -> Line<'_> + Send + Sync>;
14
15pub struct Renderer<'a>(pub(crate) &'a FileExplorer);
16
17impl WidgetRef for Renderer<'_> {
18 fn render_ref(&self, area: Rect, buf: &mut Buffer)
19 where
20 Self: Sized,
21 {
22 let mut state = ListState::default().with_selected(Some(self.0.selected_idx()));
23
24 let highlight_style = if self.0.current().is_dir {
25 self.0.theme().highlight_dir_style
26 } else {
27 self.0.theme().highlight_item_style
28 };
29
30 let mut list = List::new(self.0.files().iter().map(|file| file.text(self.0.theme())))
31 .style(self.0.theme().style)
32 .highlight_spacing(self.0.theme().highlight_spacing.clone())
33 .highlight_style(highlight_style)
34 .scroll_padding(self.0.theme().scroll_padding);
35
36 if let Some(symbol) = self.0.theme().highlight_symbol.as_deref() {
37 list = list.highlight_symbol(symbol);
38 }
39
40 if let Some(block) = self.0.theme().block.as_ref() {
41 let mut block = block.clone();
42
43 for title_top in self.0.theme().title_top(self.0) {
44 block = block.title_top(title_top);
45 }
46 for title_bottom in self.0.theme().title_bottom(self.0) {
47 block = block.title_bottom(title_bottom);
48 }
49
50 list = list.block(block);
51 }
52
53 ratatui::widgets::StatefulWidget::render(&list, area, buf, &mut state);
54 }
55}
56
57impl File {
58 /// Returns the text with the appropriate style to be displayed for the file.
59 fn text(&self, theme: &Theme) -> Text<'_> {
60 let style = if self.is_dir {
61 *theme.dir_style()
62 } else {
63 *theme.item_style()
64 };
65 Span::styled(&self.name, style).into()
66 }
67}
68
69/// The theme of the file explorer.
70///
71/// This struct is used to customize the look of the file explorer.
72/// It allows to set the style of the widget and the style of the files.
73/// You can also wrap the widget in a block with the [`with_block`](Theme::with_block)
74/// method and add dinamic titles to it with [`with_title_top`](Theme::with_title_top)
75/// and [`with_title_bottom`](Theme::with_title_bottom).
76#[derive(Clone, educe::Educe)]
77#[educe(Debug, PartialEq, Eq, Hash)]
78pub struct Theme {
79 block: Option<Block<'static>>,
80 #[educe(Debug(ignore), PartialEq(ignore), Hash(ignore))]
81 title_top: Vec<LineFactory>,
82 #[educe(Debug(ignore), PartialEq(ignore), Hash(ignore))]
83 title_bottom: Vec<LineFactory>,
84 style: Style,
85 item_style: Style,
86 dir_style: Style,
87 highlight_spacing: HighlightSpacing,
88 highlight_item_style: Style,
89 highlight_dir_style: Style,
90 highlight_symbol: Option<String>,
91 scroll_padding: usize,
92}
93
94impl Theme {
95 /// Create a new empty theme.
96 ///
97 /// The theme will not have any style set. To get a theme with the default style, use [`default`](Theme::default).
98 ///
99 /// # Example
100 /// ```no_run
101 /// # use ratatui_explorer::Theme;
102 /// let theme = Theme::new();
103 /// ```
104 #[must_use]
105 pub const fn new() -> Self {
106 Self {
107 block: None,
108 title_top: Vec::new(),
109 title_bottom: Vec::new(),
110 style: Style::new(),
111 item_style: Style::new(),
112 dir_style: Style::new(),
113 highlight_spacing: HighlightSpacing::WhenSelected,
114 highlight_item_style: Style::new(),
115 highlight_dir_style: Style::new(),
116 highlight_symbol: None,
117 scroll_padding: 0,
118 }
119 }
120
121 /// Add a top title to the theme.
122 /// The title is the current working directory.
123 ///
124 /// # Example
125 /// Suppose you have this tree file, with `passport.png` selected inside `file_explorer`:
126 /// ```plaintext
127 /// /
128 /// ├── .git
129 /// └── Documents
130 /// ├── passport.png <- selected
131 /// └── resume.pdf
132 /// ```
133 /// You will end up with something like this:
134 /// ```plaintext
135 /// ┌/Documents────────────────────────┐
136 /// │ ../ │
137 /// │ passport.png │
138 /// │ resume.pdf │
139 /// └──────────────────────────────────┘
140 /// ```
141 /// With this code:
142 /// ```no_run
143 /// use ratatui::widgets::*;
144 /// use ratatui_explorer::{FileExplorerBuilder, Theme};
145 ///
146 /// let theme = Theme::default()
147 /// .with_block(Block::default().borders(Borders::ALL))
148 /// .add_default_title();
149 ///
150 /// let file_explorer = FileExplorerBuilder::build_with_theme(theme).unwrap();
151 ///
152 /// /* user select `password.png` */
153 ///
154 /// let widget = file_explorer.widget();
155 /// /* render the widget */
156 /// ```
157 #[inline]
158 #[must_use = "method moves the value of self and returns the modified value"]
159 pub fn add_default_title(self) -> Self {
160 self.with_title_top(|file_explorer: &FileExplorer| {
161 Line::from(file_explorer.cwd().display().to_string())
162 })
163 }
164
165 /// Wrap the file explorer with a custom [`Block`](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html) widget.
166 ///
167 /// Behind the scene, it use the [`List::block`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html#method.block) method.
168 /// See its documentation for more.
169 ///
170 /// You can use [`with_title_top`](Theme::with_title_top) and [`with_title_bottom`](Theme::with_title_top) to add dynamic titles to the block.
171 ///
172 /// # Example
173 /// ```no_run
174 /// # use ratatui::widgets::*;
175 /// # use ratatui_explorer::Theme;
176 /// let theme = Theme::default().with_block(Block::default().borders(Borders::ALL));
177 /// ```
178 #[inline]
179 #[must_use = "method moves the value of self and returns the modified value"]
180 pub fn with_block(mut self, block: Block<'static>) -> Self {
181 self.block = Some(block);
182 self
183 }
184
185 /// Set the style of the widget.
186 ///
187 /// Behind the scene, it use the [`List::style`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html#method.style) method.
188 /// See its documentation for more.
189 ///
190 /// # Example
191 /// ```no_run
192 /// # use ratatui::prelude::*;
193 /// # use ratatui_explorer::Theme;
194 /// let theme = Theme::default().with_style(Style::default().fg(Color::Yellow));
195 /// ```
196 #[inline]
197 #[must_use = "method moves the value of self and returns the modified value"]
198 pub fn with_style<S: Into<Style>>(mut self, style: S) -> Self {
199 self.style = style.into();
200 self
201 }
202
203 /// Set the style of all non directories items. To set the style of the directories, use [`with_dir_style`](Theme::with_dir_style).
204 ///
205 /// Behind the scene, it use the [`Span::styled`](https://docs.rs/ratatui/latest/ratatui/text/struct.Span.html#method.styled) method.
206 /// See its documentation for more.
207 ///
208 /// # Example
209 /// ```no_run
210 /// # use ratatui::prelude::*;
211 /// # use ratatui_explorer::Theme;
212 /// let theme = Theme::default().with_item_style(Style::default().fg(Color::White));
213 /// ```
214 #[inline]
215 #[must_use = "method moves the value of self and returns the modified value"]
216 pub fn with_item_style<S: Into<Style>>(mut self, item_style: S) -> Self {
217 self.item_style = item_style.into();
218 self
219 }
220
221 /// Set the style of all directories items. To set the style of the non directories, use [`with_item_style`](Theme::with_item_style).
222 ///
223 /// Behind the scene, it use the [`Span::styled`](https://docs.rs/ratatui/latest/ratatui/text/struct.Span.html#method.styled) method.
224 /// See its documentation for more.
225 ///
226 /// # Example
227 /// ```no_run
228 /// # use ratatui::prelude::*;
229 /// # use ratatui_explorer::Theme;
230 /// let theme = Theme::default().with_dir_style(Style::default().fg(Color::Blue));
231 /// ```
232 #[inline]
233 #[must_use = "method moves the value of self and returns the modified value"]
234 pub fn with_dir_style<S: Into<Style>>(mut self, dir_style: S) -> Self {
235 self.dir_style = dir_style.into();
236 self
237 }
238
239 /// Set the style of all highlighted non directories items. To set the style of the highlighted directories, use [`with_highlight_dir_style`](Theme::with_highlight_dir_style).
240 ///
241 /// Behind the scene, it use the [`List::highlight_style`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html#method.highlight_style) method.
242 /// See its documentation for more.
243 ///
244 /// # Example
245 /// ```no_run
246 /// # use ratatui::prelude::*;
247 /// # use ratatui_explorer::Theme;
248 /// let theme = Theme::default().with_highlight_item_style(Style::default().add_modifier(Modifier::BOLD));
249 /// ```
250 #[inline]
251 #[must_use = "method moves the value of self and returns the modified value"]
252 pub fn with_highlight_item_style<S: Into<Style>>(mut self, highlight_item_style: S) -> Self {
253 self.highlight_item_style = highlight_item_style.into();
254 self
255 }
256
257 /// Set the style of all highlighted directories items. To set the style of the highlighted non directories, use [`with_highlight_item_style`](Theme::with_highlight_item_style).
258 ///
259 /// Behind the scene, it use the [`List::highlight_style`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html#method.highlight_style) method.
260 /// See its documentation for more.
261 ///
262 /// # Example
263 /// ```no_run
264 /// # use ratatui::prelude::*;
265 /// # use ratatui_explorer::Theme;
266 /// let theme = Theme::default().with_highlight_dir_style(Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD));
267 /// ```
268 #[inline]
269 #[must_use = "method moves the value of self and returns the modified value"]
270 pub fn with_highlight_dir_style<S: Into<Style>>(mut self, highlight_dir_style: S) -> Self {
271 self.highlight_dir_style = highlight_dir_style.into();
272 self
273 }
274
275 /// Set the symbol used to highlight the selected item.
276 ///
277 /// Behind the scene, it use the [`List::highlight_symbol`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html#method.highlight_symbol) method.
278 /// See its documentation for more.
279 ///
280 /// # Example
281 /// ```no_run
282 /// # use ratatui_explorer::Theme;
283 /// let theme = Theme::default().with_highlight_symbol("> ");
284 /// ```
285 #[inline]
286 #[must_use = "method moves the value of self and returns the modified value"]
287 pub fn with_highlight_symbol(mut self, highlight_symbol: &str) -> Self {
288 self.highlight_symbol = Some(highlight_symbol.to_owned());
289 self
290 }
291
292 /// Set the spacing between the highlighted item and the other items.
293 ///
294 /// Behind the scene, it use the [`List::highlight_spacing`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html#method.highlight_spacing) method.
295 /// See its documentation for more.
296 ///
297 /// # Example
298 /// ```no_run
299 /// # use ratatui::widgets::*;
300 /// # use ratatui_explorer::Theme;
301 /// let theme = Theme::default().with_highlight_spacing(HighlightSpacing::Never);
302 /// ```
303 #[inline]
304 #[must_use = "method moves the value of self and returns the modified value"]
305 pub fn with_highlight_spacing(mut self, highlight_spacing: HighlightSpacing) -> Self {
306 self.highlight_spacing = highlight_spacing;
307 self
308 }
309
310 /// Sets the number of items around the currently selected item that should be kept visible.
311 ///
312 /// /// Behind the scene, it use the [`List::scroll_padding`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html#method.scroll_padding) method.
313 /// See its documentation for more.
314 ///
315 /// # Example
316 /// ```no_run
317 /// # use ratatui::widgets::*;
318 /// # use ratatui_explorer::Theme;
319 /// let theme = Theme::default().with_scroll_padding(1);
320 /// ```
321 #[inline]
322 #[must_use = "method moves the value of self and returns the modified value"]
323 pub fn with_scroll_padding(mut self, scroll_padding: usize) -> Self {
324 self.scroll_padding = scroll_padding;
325 self
326 }
327
328 /// Add a top title factory to the theme.
329 ///
330 /// `title_top` is a function that take a reference to the current [`FileExplorer`] and returns
331 /// a [`Line`](https://docs.rs/ratatui/latest/ratatui/text/struct.Line.html)
332 /// to be displayed as a title at the top of the wrapping block (if it exist) of the file explorer. You can call
333 /// this function multiple times to add multiple titles.
334 ///
335 /// Behind the scene, it use the [`Block::title_top`](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html#method.title_top) method.
336 /// See its documentation for more.
337 ///
338 /// # Example
339 /// ```no_run
340 /// # use ratatui::prelude::*;
341 /// # use ratatui_explorer::{FileExplorer, Theme};
342 /// let theme = Theme::default()
343 /// .with_title_top(|file_explorer: &FileExplorer| {
344 /// Line::from(format!("cwd - {}", file_explorer.cwd().display()))
345 /// })
346 /// .with_title_top(|file_explorer: &FileExplorer| {
347 /// Line::from(format!("{} files", file_explorer.files().len() - 1)).right_aligned()
348 /// });
349 /// ```
350 #[inline]
351 #[must_use = "method moves the value of self and returns the modified value"]
352 pub fn with_title_top(
353 mut self,
354 title_top: impl Fn(&FileExplorer) -> Line<'_> + 'static + Send + Sync,
355 ) -> Self {
356 self.title_top.push(Arc::new(title_top));
357 self
358 }
359
360 /// Add a bottom title factory to the theme.
361 ///
362 /// `title_bottom` is a function that take a reference to the current [`FileExplorer`] and returns
363 /// a [`Line`](https://docs.rs/ratatui/latest/ratatui/text/struct.Line.html)
364 /// to be displayed as a title at the bottom of the wrapping block (if it exist) of the file explorer. You can call
365 /// this function multiple times to add multiple titles.
366 ///
367 /// Behind the scene, it use the [`Block::title_bottom`](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html#method.title_bottom) method.
368 /// See its documentation for more.
369 ///
370 /// # Example
371 /// ```no_run
372 /// # use ratatui::prelude::*;
373 /// # use ratatui_explorer::{FileExplorer, Theme};
374 /// let theme = Theme::default()
375 /// .with_title_bottom(|file_explorer: &FileExplorer| {
376 /// Line::from(format!("cwd - {}", file_explorer.cwd().display()))
377 /// })
378 /// .with_title_bottom(|file_explorer: &FileExplorer| {
379 /// Line::from(format!("{} files", file_explorer.files().len() - 1)).right_aligned()
380 /// });
381 /// ```
382 #[inline]
383 #[must_use = "method moves the value of self and returns the modified value"]
384 pub fn with_title_bottom(
385 mut self,
386 title_bottom: impl Fn(&FileExplorer) -> Line<'_> + 'static + Send + Sync,
387 ) -> Self {
388 self.title_bottom.push(Arc::new(title_bottom));
389 self
390 }
391
392 /// Returns the wrapping block (if it exist) of the file explorer of the theme.
393 #[inline]
394 #[must_use]
395 pub const fn block(&self) -> Option<&Block<'static>> {
396 self.block.as_ref()
397 }
398
399 /// Returns the style of the widget of the theme.
400 #[inline]
401 #[must_use]
402 pub const fn style(&self) -> &Style {
403 &self.style
404 }
405
406 /// Returns the style of the non directories items of the theme.
407 #[inline]
408 #[must_use]
409 pub const fn item_style(&self) -> &Style {
410 &self.item_style
411 }
412
413 /// Returns the style of the directories items of the theme.
414 #[inline]
415 #[must_use]
416 pub const fn dir_style(&self) -> &Style {
417 &self.dir_style
418 }
419
420 /// Returns the style of the highlighted non directories items of the theme.
421 #[inline]
422 #[must_use]
423 pub const fn highlight_item_style(&self) -> &Style {
424 &self.highlight_item_style
425 }
426
427 /// Returns the style of the highlighted directories items of the theme.
428 #[inline]
429 #[must_use]
430 pub const fn highlight_dir_style(&self) -> &Style {
431 &self.highlight_dir_style
432 }
433
434 /// Returns the symbol used to highlight the selected item of the theme.
435 #[inline]
436 #[must_use]
437 pub fn highlight_symbol(&self) -> Option<&str> {
438 self.highlight_symbol.as_deref()
439 }
440
441 /// Returns the spacing between the highlighted item and the other items of the theme.
442 #[inline]
443 #[must_use]
444 pub const fn highlight_spacing(&self) -> &HighlightSpacing {
445 &self.highlight_spacing
446 }
447
448 /// Returns the number of items around the currently selected item that should be kept visible.
449 #[inline]
450 #[must_use]
451 pub const fn scroll_padding(&self) -> usize {
452 self.scroll_padding
453 }
454
455 /// Returns the generated top titles of the theme.
456 #[inline]
457 #[must_use]
458 pub fn title_top<'a>(&self, file_explorer: &'a FileExplorer) -> Vec<Line<'a>> {
459 self.title_top
460 .iter()
461 .map(|title_top| title_top(file_explorer))
462 .collect()
463 }
464
465 /// Returns the generated bottom titles of the theme.
466 #[inline]
467 #[must_use]
468 pub fn title_bottom<'a>(&self, file_explorer: &'a FileExplorer) -> Vec<Line<'a>> {
469 self.title_bottom
470 .iter()
471 .map(|title_bottom| title_bottom(file_explorer))
472 .collect()
473 }
474}
475
476impl Default for Theme {
477 /// Return a slightly customized default theme. To get a theme with no style set, use [`new`](Theme::new).
478 ///
479 /// The theme will have a block with all borders, a white style for the items, a light blue style for the directories,
480 /// a dark gray background for all the highlighted items.
481 ///
482 /// # Example
483 /// ```no_run
484 /// # use ratatui_explorer::Theme;
485 /// let theme = Theme::default();
486 /// ```
487 fn default() -> Self {
488 Self {
489 block: Some(Block::default().borders(Borders::ALL)),
490 title_top: Vec::new(),
491 title_bottom: Vec::new(),
492 style: Style::default(),
493 item_style: Style::default().fg(Color::White),
494 dir_style: Style::default().fg(Color::LightBlue),
495 highlight_spacing: HighlightSpacing::Always,
496 highlight_item_style: Style::default().fg(Color::White).bg(Color::DarkGray),
497 highlight_dir_style: Style::default().fg(Color::LightBlue).bg(Color::DarkGray),
498 highlight_symbol: None,
499 scroll_padding: 0,
500 }
501 }
502}