All checks were successful
Deploy Website / deploy (push) Has been skipped
181 lines
5.4 KiB
Rust
181 lines
5.4 KiB
Rust
//! File/directory browser modal widget.
|
|
|
|
use crate::theme;
|
|
use ratatui::layout::{Constraint, Layout, Rect};
|
|
use ratatui::style::{Color, Style};
|
|
use ratatui::text::{Line, Span};
|
|
use ratatui::widgets::Paragraph;
|
|
use ratatui::Frame;
|
|
|
|
use super::ModalFrame;
|
|
|
|
/// Modal listing files and directories with a filter input line.
|
|
pub struct FileBrowserModal<'a> {
|
|
title: &'a str,
|
|
input: &'a str,
|
|
entries: &'a [(String, bool, bool)],
|
|
audio_counts: &'a [Option<usize>],
|
|
selected: usize,
|
|
scroll_offset: usize,
|
|
border_color: Option<Color>,
|
|
width: u16,
|
|
height: u16,
|
|
hints: Option<Line<'a>>,
|
|
color_path: bool,
|
|
}
|
|
|
|
impl<'a> FileBrowserModal<'a> {
|
|
pub fn new(title: &'a str, input: &'a str, entries: &'a [(String, bool, bool)]) -> Self {
|
|
Self {
|
|
title,
|
|
input,
|
|
entries,
|
|
audio_counts: &[],
|
|
selected: 0,
|
|
scroll_offset: 0,
|
|
border_color: None,
|
|
width: 60,
|
|
height: 16,
|
|
hints: None,
|
|
color_path: false,
|
|
}
|
|
}
|
|
|
|
pub fn selected(mut self, idx: usize) -> Self {
|
|
self.selected = idx;
|
|
self
|
|
}
|
|
|
|
pub fn scroll_offset(mut self, offset: usize) -> Self {
|
|
self.scroll_offset = offset;
|
|
self
|
|
}
|
|
|
|
pub fn border_color(mut self, c: Color) -> Self {
|
|
self.border_color = Some(c);
|
|
self
|
|
}
|
|
|
|
pub fn width(mut self, w: u16) -> Self {
|
|
self.width = w;
|
|
self
|
|
}
|
|
|
|
pub fn height(mut self, h: u16) -> Self {
|
|
self.height = h;
|
|
self
|
|
}
|
|
|
|
pub fn hints(mut self, hints: Line<'a>) -> Self {
|
|
self.hints = Some(hints);
|
|
self
|
|
}
|
|
|
|
pub fn audio_counts(mut self, counts: &'a [Option<usize>]) -> Self {
|
|
self.audio_counts = counts;
|
|
self
|
|
}
|
|
|
|
pub fn color_path(mut self) -> Self {
|
|
self.color_path = true;
|
|
self
|
|
}
|
|
|
|
pub fn render_centered(self, frame: &mut Frame, term: Rect) -> Rect {
|
|
let colors = theme::get();
|
|
let border_color = self.border_color.unwrap_or(colors.ui.text_primary);
|
|
|
|
let inner = ModalFrame::new(self.title)
|
|
.width(self.width)
|
|
.height(self.height)
|
|
.border_color(border_color)
|
|
.render_centered(frame, term);
|
|
|
|
let has_hints = self.hints.is_some();
|
|
let constraints = if has_hints {
|
|
vec![
|
|
Constraint::Length(1),
|
|
Constraint::Min(1),
|
|
Constraint::Length(1),
|
|
]
|
|
} else {
|
|
vec![Constraint::Length(1), Constraint::Min(1)]
|
|
};
|
|
let rows = Layout::vertical(constraints).split(inner);
|
|
|
|
// Input line
|
|
let input_spans = if self.color_path {
|
|
let (path_part, filter_part) = match self.input.rfind('/') {
|
|
Some(pos) => (&self.input[..=pos], &self.input[pos + 1..]),
|
|
None => ("", self.input),
|
|
};
|
|
vec![
|
|
Span::raw("> "),
|
|
Span::styled(path_part.to_string(), Style::new().fg(colors.browser.directory)),
|
|
Span::styled(filter_part.to_string(), Style::new().fg(colors.input.text)),
|
|
Span::styled("█", Style::new().fg(colors.input.cursor)),
|
|
]
|
|
} else {
|
|
vec![
|
|
Span::raw("> "),
|
|
Span::styled(self.input, Style::new().fg(colors.input.text)),
|
|
Span::styled("█", Style::new().fg(colors.input.cursor)),
|
|
]
|
|
};
|
|
frame.render_widget(Paragraph::new(Line::from(input_spans)), rows[0]);
|
|
|
|
// Hints bar
|
|
if let Some(hints) = self.hints {
|
|
let hint_row = rows[2];
|
|
frame.render_widget(
|
|
Paragraph::new(hints).alignment(ratatui::layout::Alignment::Right),
|
|
hint_row,
|
|
);
|
|
}
|
|
|
|
// Entries list
|
|
let visible_height = rows[1].height as usize;
|
|
let visible_entries = self
|
|
.entries
|
|
.iter()
|
|
.enumerate()
|
|
.skip(self.scroll_offset)
|
|
.take(visible_height);
|
|
|
|
let lines: Vec<Line> = visible_entries
|
|
.map(|(abs_idx, (name, is_dir, is_cagire))| {
|
|
let is_selected = abs_idx == self.selected;
|
|
let prefix = if is_selected { "> " } else { " " };
|
|
let color = if is_selected {
|
|
colors.browser.selected
|
|
} else if *is_dir {
|
|
colors.browser.directory
|
|
} else if *is_cagire {
|
|
colors.browser.project_file
|
|
} else {
|
|
colors.browser.file
|
|
};
|
|
let display = if *is_dir {
|
|
format!("{prefix}{name}/")
|
|
} else {
|
|
format!("{prefix}{name}")
|
|
};
|
|
let mut spans = vec![Span::styled(display, Style::new().fg(color))];
|
|
if *is_dir && name != ".." {
|
|
if let Some(Some(count)) = self.audio_counts.get(abs_idx) {
|
|
spans.push(Span::styled(
|
|
format!(" ({count})"),
|
|
Style::new().fg(colors.browser.file),
|
|
));
|
|
}
|
|
}
|
|
Line::from(spans)
|
|
})
|
|
.collect();
|
|
|
|
frame.render_widget(Paragraph::new(lines), rows[1]);
|
|
|
|
inner
|
|
}
|
|
}
|