use super::*;
pub(super) fn popup_inner(area: Rect) -> Rect {
area.inner(Margin { vertical: 1, horizontal: 2 })
}
fn popup_body(f: &mut ratatui::Frame<'_>, area: Rect, lines: Vec<Line<'static>>) -> Vec<Option<u16>> {
let inner = popup_inner(area);
let room = inner.height as usize;
let blocks = fit_rows(lines.into_iter().map(|line| wrap_line(line, inner.width as usize)).collect(), room);
let rows: usize = blocks.iter().map(Vec::len).sum();
let airy = (rows + blocks.len()).saturating_sub(1);
let mut body: Vec<Line> = Vec::with_capacity(airy.max(rows));
let mut starts = Vec::with_capacity(blocks.len());
for (index, block) in blocks.into_iter().enumerate() {
if index > 0 && airy <= room {
body.push(Line::from(""));
}
starts.push(body.len());
body.extend(block);
}
let padding = room.saturating_sub(body.len()) / 2;
let mut out = vec![Line::from(""); padding];
out.extend(body);
f.render_widget(Paragraph::new(out).alignment(Alignment::Center), inner);
starts.into_iter().map(|start| (padding + start < room).then(|| inner.y + (padding + start) as u16)).collect()
}
pub(super) fn wrap_line(line: Line<'static>, width: usize) -> Vec<Line<'static>> {
if width == 0 || line.width() <= width || line.spans.len() != 1 {
return vec![line];
}
let style = line.spans[0].style;
wrap_text(&line.spans[0].content, width)
.into_iter()
.map(|row| Line {
spans: vec![Span::styled(row, style)],
style: line.style,
alignment: line.alignment,
})
.collect()
}
pub(super) fn wrap_text(text: &str, width: usize) -> Vec<String> {
let mut rows = Vec::new();
let mut row = String::new();
let mut used = 0;
for word in text.split(' ') {
if used > 0 && used + 1 + display_width(word) > width {
rows.push(std::mem::take(&mut row));
used = 0;
}
if used > 0 {
row.push(' ');
used += 1;
}
for character in word.chars() {
let cell = UnicodeWidthChar::width(character).unwrap_or(0);
if used > 0 && used + cell > width {
rows.push(std::mem::take(&mut row));
used = 0;
}
row.push(character);
used += cell;
}
}
if !row.is_empty() || rows.is_empty() {
rows.push(row);
}
rows
}
pub(super) fn fit_rows(mut blocks: Vec<Vec<Line<'static>>>, room: usize) -> Vec<Vec<Line<'static>>> {
let rows: usize = blocks.iter().map(Vec::len).sum();
if rows <= room {
return blocks;
}
let Some(longest) = (0..blocks.len()).max_by_key(|&index| blocks[index].len()) else {
return blocks;
};
let others = rows - blocks[longest].len();
let keep = room.saturating_sub(others).max(1);
let block = &mut blocks[longest];
if keep < block.len() {
block.truncate(keep);
if let Some(span) = block.last_mut().and_then(|line| line.spans.last_mut()) {
let mut text = span.content.to_string();
text.pop();
text.push('…');
span.content = text.into();
}
}
blocks
}
pub(super) fn render_error_popup(f: &mut ratatui::Frame<'_>, area: Rect, message: &str) {
let popup_area = popup_rect(20, area);
let popup_block = Block::default().title(Line::from(Span::styled(" Error ", style_title())).centered()).borders(Borders::ALL).style(style_border());
clear(f, popup_area);
f.render_widget(popup_block, popup_area);
popup_body(f, popup_area, vec![Line::from(Span::styled(printable_name(message), style_title()))]);
}
pub(super) fn render_create_popup(f: &mut ratatui::Frame<'_>, area: Rect, is_dir: bool, input: &TextInput) {
let popup_area = popup_rect(20, area);
let popup_block = Block::default()
.title(Line::from(Span::styled(if is_dir { " Create Directory " } else { " Create File " }, style_title())).centered())
.borders(Borders::ALL)
.style(style_border());
clear(f, popup_area);
f.render_widget(popup_block, popup_area);
let typed = input.cursor_spans(style_input(), style_input_cursor());
let width = popup_inner(popup_area).width as usize;
let typed_width: usize = typed.iter().map(|span| display_width(&span.content)).sum();
let left = width.saturating_sub(typed_width) / 2;
let mut spans = vec![Span::raw(" ".repeat(left))];
spans.extend(typed);
spans.push(Span::raw(" ".repeat(width.saturating_sub(left + typed_width))));
let input_line = Line::from(spans).style(style_input());
popup_body(f, popup_area, vec![input_line, Line::from(Span::styled("Enter - Create Esc - Cancel", style_columns()))]);
}
pub(super) fn render_delete_popup(f: &mut ratatui::Frame<'_>, area: Rect, items: &[(PathBuf, bool)], to_trash: bool) {
let count = items.len();
let popup_area = popup_rect(30, area);
let verb = if to_trash { "Move to trash" } else { "Delete" };
let title = if count == 1 {
let item_type = if items[0].1 { "directory" } else { "file" };
format!(" {verb}: {item_type} ")
} else {
format!(" {verb}: {count} items ")
};
let popup_block = Block::default().title(Line::from(Span::styled(title, style_title())).centered()).borders(Borders::ALL).style(style_border());
clear(f, popup_area);
f.render_widget(popup_block, popup_area);
let mut lines = Vec::new();
let question = if to_trash { "Move {} to the trash?" } else { "Delete {} permanently?" };
if count == 1 {
let name = format!("\"{}\"", shown_name(&items[0].0));
lines.push(Line::from(Span::styled(question.replace("{}", &name), style_title())));
} else {
let names: Vec<String> = items.iter().map(|(path, _)| shown_name(path)).collect();
lines.push(Line::from(Span::styled(question.replace("{}", &format!("{count} items")), style_title())));
lines.push(Line::from(Span::styled(names.join(", "), style_file())));
}
if !to_trash {
lines.push(Line::from(Span::styled("This cannot be undone", style_file())));
}
lines.push(Line::from(Span::styled("Y / Enter - Yes N / Esc - No", style_columns())));
popup_body(f, popup_area, lines);
}
fn render_job_clash(f: &mut ratatui::Frame<'_>, area: Rect, app_state: &AppState, clash: &crate::app::JobClash) {
let popup_area = popup_rect(40, area);
let popup_block = Block::default().title(Line::from(Span::styled(" Already there ", style_title())).centered()).borders(Borders::ALL).style(style_border());
clear(f, popup_area);
f.render_widget(popup_block, popup_area);
let folder = clash.dest.parent().map(|parent| printable_name(&parent.to_string_lossy())).unwrap_or_default();
let exact = clash.source_size != clash.dest_size && clash.source_size.map(format_size) == clash.dest_size.map(format_size);
let width = if exact { 15 } else { 9 };
let describe = |label: &str, size: Option<u64>, modified: Option<std::time::SystemTime>, other: Option<std::time::SystemTime>| {
let size = size.map(|bytes| if exact { format!("{} B", grouped(bytes)) } else { format_size(bytes) }).unwrap_or_else(|| "?".to_string());
let date = modified.map(|at| format_exact(at, app_state.options.date_format)).unwrap_or_else(|| "?".to_string());
let newer = matches!((modified, other), (Some(this), Some(that)) if this > that);
format!("{label} {size:>width$} {date}{}", if newer { " newer" } else { " " })
};
let mut lines = vec![Line::from(Span::styled(format!("\"{}\" is already in {folder}", shown_name(&clash.dest)), style_title()))];
if clash.source.file_name() != clash.dest.file_name() {
lines.push(Line::from(Span::styled(format!("to be replaced by \"{}\"", shown_name(&clash.source)), style_file())));
}
lines.extend([
Line::from(Span::styled(describe("New ", clash.source_size, clash.source_modified, clash.dest_modified), style_file())),
Line::from(Span::styled(describe("Existing", clash.dest_size, clash.dest_modified, clash.source_modified), style_file())),
Line::from(Span::styled("Y - Overwrite N - Skip O - Overwrite all older", style_columns())),
Line::from(Span::styled("A - Overwrite all S - Skip all Esc - Abort", style_columns())),
]);
if app_state.quit_armed {
lines.push(Line::from(Span::styled("F10 again - Quit", style_file())));
}
popup_body(f, popup_area, lines);
}
fn progress_bar(fraction: f64, width: usize) -> String {
let filled = ((fraction.clamp(0.0, 1.0) * width as f64).round() as usize).min(width);
"\u{25aa}".repeat(filled) + &"\u{25ab}".repeat(width - filled)
}
pub(super) fn render_transfer_popup(f: &mut ratatui::Frame<'_>, area: Rect, app_state: &AppState) {
let Some(job) = &app_state.job else {
return;
};
if let Some(clash) = &job.clash {
render_job_clash(f, area, app_state, clash);
return;
}
if let Some(problem) = &job.problem {
render_job_problem(f, area, job.kind, problem, app_state.quit_armed);
return;
}
let elapsed = job.started.elapsed();
if elapsed < TRANSFER_POPUP_DELAY {
return;
}
let title = if job.is_cancelling() {
" Cancelling ".to_string()
} else {
match job.kind {
TransferKind::Copy => " Copying ".to_string(),
TransferKind::Move => " Moving ".to_string(),
TransferKind::Delete => " Deleting ".to_string(),
TransferKind::Trash => " Moving to trash ".to_string(),
TransferKind::Extract => " Extracting ".to_string(),
TransferKind::Pack => " Packing ".to_string(),
}
};
let popup_area = popup_rect(30, area);
let popup_block = Block::default().title(Line::from(Span::styled(title, style_title())).centered()).borders(Borders::ALL).style(style_border());
clear(f, popup_area);
f.render_widget(popup_block, popup_area);
let name = shown_name(&job.current);
let bar_width = popup_area.width.saturating_sub(14) as usize;
let bar = match job.fraction() {
Some(fraction) => format!("{} {:>3.0}%", progress_bar(fraction, bar_width), fraction * 100.0),
None => "Counting...".to_string(),
};
let hint = if app_state.quit_armed { "Esc - Cancel F10 again - Quit" } else { "Esc - Cancel" };
let detail = match (job.kind, job.total) {
(TransferKind::Delete, Some(total)) if total > 0 => {
format!("{} of {} entries {hint}", job.done, total)
}
(TransferKind::Trash, Some(total)) if total > 0 => {
format!("{} of {} items {hint}", job.done, total)
}
(_, Some(total)) if total > 0 => {
let rate = job.done as f64 / elapsed.as_secs_f64().max(0.001);
format!("{} of {} at {}/s {hint}", format_size(job.done), format_size(total), format_size(rate as u64))
}
_ => hint.to_string(),
};
let lines = vec![
Line::from(""),
Line::from(Span::styled(name.to_string(), style_title())),
Line::from(""),
Line::from(Span::styled(bar, style_columns())),
Line::from(""),
Line::from(Span::styled(detail, style_file())),
];
f.render_widget(Paragraph::new(lines).alignment(Alignment::Center), popup_area.inner(Margin { vertical: 1, horizontal: 2 }));
}
fn render_job_problem(f: &mut ratatui::Frame<'_>, area: Rect, kind: TransferKind, problem: &crate::app::JobProblem, quit_armed: bool) {
let title = match kind {
TransferKind::Copy => " Cannot copy ",
TransferKind::Move => " Cannot move ",
TransferKind::Delete => " Cannot delete ",
TransferKind::Trash => " Cannot move to trash ",
TransferKind::Extract => " Cannot extract ",
TransferKind::Pack => " Cannot pack ",
};
let popup_area = popup_rect(30, area);
let popup_block = Block::default().title(Line::from(Span::styled(title, style_title())).centered()).borders(Borders::ALL).style(style_border());
clear(f, popup_area);
f.render_widget(popup_block, popup_area);
let hint = "R - Retry S - Skip A - Skip all Esc - Abort";
let mut lines = vec![
Line::from(""),
Line::from(Span::styled(shown_name(&problem.path), style_title())),
Line::from(""),
Line::from(Span::styled(printable_name(&problem.message), style_columns())),
Line::from(""),
Line::from(Span::styled(hint, style_file())),
];
if kind == TransferKind::Trash {
lines.push(Line::from(Span::styled("D - Delete permanently", style_file())));
}
if quit_armed {
lines.push(Line::from(Span::styled("F10 again - Quit", style_file())));
}
f.render_widget(Paragraph::new(lines).alignment(Alignment::Center).wrap(ratatui::widgets::Wrap { trim: true }), popup_area.inner(Margin { vertical: 1, horizontal: 2 }));
}
pub(super) fn render_disk_changed_popup(f: &mut ratatui::Frame<'_>, area: Rect, app_state: &AppState, opened: Option<crate::app::DiskStamp>, now: Option<crate::app::DiskStamp>) {
let popup_area = popup_rect(40, area);
let popup_block = Block::default().title(Line::from(Span::styled(" Changed on disk ", style_title())).centered()).borders(Borders::ALL).style(style_border());
clear(f, popup_area);
f.render_widget(popup_block, popup_area);
let name = app_state.editor().map(|state| shown_name(&state.file_path)).unwrap_or_default();
let stamp = |label: &str, stamp: Option<crate::app::DiskStamp>| {
let shown = match stamp {
Some(stamp) => {
let date = stamp.modified.map(|at| format_exact(at, app_state.options.date_format)).unwrap_or_else(|| "?".to_string());
format!("{:>9} {date}", format_size(stamp.size))
}
None => "not there".to_string(),
};
Line::from(Span::styled(format!("{label:<12}{shown}"), style_file()))
};
let mut lines = match now {
Some(_) => vec![Line::from(Span::styled(format!("\"{name}\" has changed since it was opened"), style_title()))],
None => vec![Line::from(Span::styled(format!("\"{name}\" is no longer there"), style_title()))],
};
lines.push(stamp("When opened", opened));
lines.push(stamp("Now", now));
let keys = match now {
Some(_) => "O - Overwrite it R - Reload, losing your edits",
None => "O - Save it again",
};
lines.push(Line::from(Span::styled(keys, style_columns())));
lines.push(Line::from(Span::styled("Esc - Back to editing", style_columns())));
popup_body(f, popup_area, lines);
}
pub(super) fn render_find_files_form(f: &mut ratatui::Frame<'_>, area: Rect, start: &Path, name: &TextInput, containing: &TextInput, on_containing: bool) {
let popup_area = popup_rect(45, area);
let popup_block = Block::default().title(Line::from(Span::styled(" Find files ", style_title())).centered()).borders(Borders::ALL).style(style_border());
clear(f, popup_area);
f.render_widget(popup_block, popup_area);
let width = popup_inner(popup_area).width as usize;
const LABEL: usize = 12;
let field = |label: &str, input: &TextInput, focused: bool| {
let room = width.saturating_sub(LABEL);
let mut spans = vec![Span::styled(format!("{label:<LABEL$}"), style_columns())];
let typed = if focused {
input.cursor_spans_within(room, style_input(), style_input_cursor())
} else {
vec![Span::styled(tail_of(&input.text, room), style_file())]
};
let typed_width: usize = typed.iter().map(|span| display_width(&span.content)).sum();
spans.extend(typed);
let padding = " ".repeat(room.saturating_sub(typed_width));
spans.push(if focused { Span::styled(padding, style_input()) } else { Span::raw(padding) });
Line::from(spans)
};
popup_body(
f,
popup_area,
vec![
Line::from(Span::styled(
format!("{:<LABEL$}{:<room$}", "In", tail_of(&start.to_string_lossy(), width.saturating_sub(LABEL)), room = width.saturating_sub(LABEL)),
style_file(),
)),
field("Name", name, !on_containing),
field("Containing", containing, on_containing),
Line::from(Span::styled("Name as + takes it: *.jpg;*.png, or */ for directories. Lower case matches either case", style_columns())),
Line::from(Span::styled("Tab - Next field Enter - Find Esc - Cancel", style_columns())),
],
);
}
pub(super) fn render_find_files_results(f: &mut ratatui::Frame<'_>, area: Rect, search: &crate::app::Search) -> usize {
let popup_area = popup_rect(80, area);
let title = format!(" Find: {} ", printable_name(&search.label));
let popup_block = Block::default().title(Line::from(Span::styled(title, style_title())).centered()).borders(Borders::ALL).style(style_border());
clear(f, popup_area);
f.render_widget(popup_block, popup_area);
let inner = popup_area.inner(Margin { vertical: 1, horizontal: 2 });
let width = inner.width as usize;
let rows = (inner.height as usize).saturating_sub(3);
let found = search.results.len();
let status = match &search.looking_in {
Some(dir) => {
let said = format!("{found} found, searching ");
format!("{said}{}", tail_of(&dir.to_string_lossy(), width.saturating_sub(display_width(&said) + 1)))
}
None if found == 0 => "Nothing found".to_string(),
None => format!("{found} found"),
};
let status = match search.unreadable {
0 => status,
1 => format!("{status} - 1 directory could not be read"),
many => format!("{status} - {many} directories could not be read"),
};
let mut lines = vec![Line::from(Span::styled(status, style_title())), Line::from("")];
let mut first = search.scroll.min(found.saturating_sub(rows)).min(search.cursor);
if rows > 0 && search.cursor >= first + rows {
first = search.cursor + 1 - rows;
}
for (index, (path, is_dir)) in search.results.iter().enumerate().skip(first).take(rows) {
let shown = path.strip_prefix(&search.start).unwrap_or(path).to_string_lossy().into_owned();
let shown = if *is_dir { format!("{shown}{}", std::path::MAIN_SEPARATOR) } else { shown };
let text = tail_of(&shown, width);
let pad = " ".repeat(width.saturating_sub(display_width(&text)));
let style = match (index == search.cursor, *is_dir) {
(true, _) => style_title().bg(palette().selected_background),
(false, true) => style_dir(),
(false, false) => style_file(),
};
lines.push(Line::from(Span::styled(format!("{text}{pad}"), style)));
}
while lines.len() < rows + 2 {
lines.push(Line::from(""));
}
let hint = if search.done { "Enter - Go to Esc - Close" } else { "Enter - Go to Esc - Stop and close" };
lines.push(Line::from(Span::styled(hint, style_columns())).centered());
f.render_widget(Paragraph::new(lines), inner);
rows
}
pub(super) fn render_attributes_popup(f: &mut ratatui::Frame<'_>, area: Rect, form: &AttributesForm) -> Vec<(Rect, u16)> {
let popup_area = popup_rect(50, area);
let popup_block = Block::default().title(Line::from(Span::styled(" Attributes ", style_title())).centered()).borders(Borders::ALL).style(style_border());
clear(f, popup_area);
f.render_widget(popup_block, popup_area);
let width = popup_inner(popup_area).width as usize;
const LABEL: usize = 10;
const BECOMES: usize = 12;
let what = match form.items.as_slice() {
[(path, is_dir)] => format!("{} \"{}\"", if *is_dir { "Directory" } else { "File" }, shown_name(path)),
items => format!("{} items - a field left as it is, or empty, changes nothing", items.len()),
};
let mut lines = vec![Line::from(Span::styled(tail_of(&what, width), style_file()))];
for (index, field) in form.fields.iter().enumerate() {
let focused = index == form.focus;
let becomes = match (field.kind, form.items.as_slice(), form.modes.first()) {
(AttributeKind::Mode, [(_, is_dir)], Some(&mode)) => crate::attributes::ModeChange::parse(&field.input.text).ok().map(|change| mode_preview(change.apply(mode, *is_dir))),
_ => None,
};
let room = width.saturating_sub(LABEL + if field.kind == AttributeKind::Mode { BECOMES } else { 0 });
let mut spans = vec![Span::styled(format!("{:<LABEL$}", field.kind.label()), style_columns())];
let typed = if focused {
field.input.cursor_spans_within(room, style_input(), style_input_cursor())
} else {
vec![Span::styled(tail_of(&field.input.text, room), style_file())]
};
let typed_width: usize = typed.iter().map(|span| display_width(&span.content)).sum();
spans.extend(typed);
let padding = " ".repeat(room.saturating_sub(typed_width));
spans.push(if focused { Span::styled(padding, style_input()) } else { Span::raw(padding) });
if field.kind == AttributeKind::Mode {
spans.push(Span::styled(format!("{:>BECOMES$}", becomes.unwrap_or_default()), style_file()));
}
lines.push(Line::from(spans));
}
if form.fields.len() > 1 {
lines.push(Line::from(Span::styled("Mode as 755 or u+x,go-w; owner and group by name or number", style_columns())));
}
lines.push(Line::from(Span::styled("Tab - Next field Enter - Apply Esc - Cancel", style_columns())));
let inner = popup_inner(popup_area);
let starts = popup_body(f, popup_area, lines);
starts[1..=form.fields.len()].iter().flatten().map(|&y| (Rect::new(inner.x, y, inner.width, 1), inner.x + LABEL as u16)).collect()
}
#[cfg(unix)]
fn mode_preview(mode: u32) -> String {
crate::fs_ops::mode_string(mode)
}
#[cfg(not(unix))]
fn mode_preview(_mode: u32) -> String {
String::new()
}
pub(super) fn render_bookmarks_popup(f: &mut ratatui::Frame<'_>, area: Rect, list: &[PathBuf], index: usize, here: &Path) {
let popup_area = popup_rect(60, area);
let popup_block = Block::default().title(Line::from(Span::styled(" Bookmarks ", style_title())).centered()).borders(Borders::ALL).style(style_border());
clear(f, popup_area);
f.render_widget(popup_block, popup_area);
let inner = popup_area.inner(Margin { vertical: 1, horizontal: 2 });
let width = inner.width as usize;
let rows = (inner.height as usize).saturating_sub(3);
let home = std::env::var_os("HOME").map(PathBuf::from).filter(|home| !home.as_os_str().is_empty());
let mut lines = Vec::new();
if list.is_empty() {
lines.push(Line::from(Span::styled("No bookmarks yet - A keeps the panel's directory", style_file())));
}
let first = if rows > 0 && index >= rows { index + 1 - rows } else { 0 };
for (number, path) in list.iter().enumerate().skip(first).take(rows) {
let shown = match home.as_ref().and_then(|home| path.strip_prefix(home).ok()) {
Some(rest) if rest.as_os_str().is_empty() => "~".to_string(),
Some(rest) => format!("~{}{}", std::path::MAIN_SEPARATOR, rest.to_string_lossy()),
None => path.to_string_lossy().into_owned(),
};
let label = if number < 9 { format!("{} ", number + 1) } else { " ".to_string() };
let mark = if path == here { "• " } else { " " };
let room = width.saturating_sub(label.len() + mark.len());
let text = format!("{label}{mark}{}", tail_of(&printable_name(&shown), room));
let pad = " ".repeat(width.saturating_sub(display_width(&text)));
let style = if number == index { style_title().bg(palette().selected_background) } else { style_dir() };
lines.push(Line::from(Span::styled(format!("{text}{pad}"), style)));
}
while lines.len() < rows + 1 {
lines.push(Line::from(""));
}
lines.push(Line::from(Span::styled("Enter/1-9 - Go A/Ins - Add here D/Del - Remove", style_columns())).centered());
lines.push(Line::from(Span::styled("Shift+Up/Down - Move Esc - Close", style_columns())).centered());
f.render_widget(Paragraph::new(lines), inner);
}
pub(super) fn render_branches_popup(f: &mut ratatui::Frame<'_>, area: Rect, list: &BranchList) -> Vec<(Rect, usize)> {
let most = popup_rect(60, area);
let popup_area = centered(most, most.width as usize, list.names.len() + 6);
let title = format!(" Branches: {} ", shown_name(&list.dir));
let popup_block = Block::default().title(Line::from(Span::styled(title, style_title())).centered()).borders(Borders::ALL).style(style_border());
clear(f, popup_area);
f.render_widget(popup_block, popup_area);
let inner = popup_area.inner(Margin { vertical: 1, horizontal: 2 });
let width = inner.width as usize;
let rows = (inner.height as usize).saturating_sub(2);
let first = if rows > 0 && list.index >= rows { list.index + 1 - rows } else { 0 };
let mut lines = Vec::new();
let mut hits = Vec::new();
for (number, name) in list.names.iter().enumerate().skip(first).take(rows) {
let mark = if Some(number) == list.current { "• " } else { " " };
let text = format!("{mark}{}", tail_of(&printable_name(name), width.saturating_sub(mark.len())));
let pad = " ".repeat(width.saturating_sub(display_width(&text)));
let style = if number == list.index { style_title().bg(palette().selected_background) } else { style_dir() };
hits.push((Rect::new(inner.x, inner.y + lines.len() as u16, inner.width, 1), number));
lines.push(Line::from(Span::styled(format!("{text}{pad}"), style)));
}
while lines.len() < rows + 1 {
lines.push(Line::from(""));
}
lines.push(Line::from(Span::styled("Enter/Click - Switch Esc - Close", style_columns())).centered());
f.render_widget(Paragraph::new(lines), inner);
hits
}
#[allow(clippy::too_many_arguments)]
pub(super) fn render_destination_popup(f: &mut ratatui::Frame<'_>, area: Rect, sources: &[(PathBuf, bool)], what: Option<&str>, verb: &str, preposition: &str, input: &TextInput, also: Option<&str>) {
let popup_area = popup_rect(35, area);
let count = sources.len();
let title = if count == 1 {
let item_type = what.unwrap_or(if sources[0].1 { "directory" } else { "file" });
format!(" {} {} ", verb, item_type)
} else {
format!(" {} {} items ", verb, count)
};
let popup_block = Block::default().title(Line::from(Span::styled(title, style_title())).centered()).borders(Borders::ALL).style(style_border());
clear(f, popup_area);
f.render_widget(popup_block, popup_area);
let source_msg = if count == 1 {
format!("{} \"{}\" {}:", verb, shown_name(&sources[0].0), preposition)
} else {
let names: Vec<String> = sources.iter().map(|(source, _)| shown_name(source)).collect();
format!("{} {} items {}: {}", verb, count, preposition, names.join(", "))
};
let width = popup_inner(popup_area).width as usize;
let typed = input.cursor_spans_within(width, style_input(), style_input_cursor());
let typed_width: usize = typed.iter().map(|span| display_width(&span.content)).sum();
let mut spans = typed;
spans.push(Span::raw(" ".repeat(width.saturating_sub(typed_width))));
let input_line = Line::from(spans).style(style_input());
popup_body(
f,
popup_area,
vec![
Line::from(Span::styled(source_msg, style_title())),
input_line,
Line::from(Span::styled(
[Some(format!("Enter - {verb}")), also.map(str::to_string), Some("Esc - Cancel".to_string())].into_iter().flatten().collect::<Vec<_>>().join(" "),
style_columns(),
)),
],
);
}
pub(super) fn render_large_file_popup(f: &mut ratatui::Frame<'_>, area: Rect, large: &crate::app::LargeFile) {
let name = shown_name(&large.path);
let verb = if large.is_edit { "Edit" } else { "View" };
let popup_area = popup_rect(30, area);
let title = if large.dimensions.is_some() { " Large Picture " } else { " Large File " };
let popup_block = Block::default().title(Line::from(Span::styled(title, style_title())).centered()).borders(Borders::ALL).style(style_border());
clear(f, popup_area);
f.render_widget(popup_block, popup_area);
let message = match large.dimensions {
Some((width, height)) => format!("{} \"{}\" ({}x{}, {})?", verb, name, width, height, format_size(large.size)),
None => format!("{} \"{}\" ({})?", verb, name, format_size(large.size)),
};
let note = if large.dimensions.is_some() {
"Decoding a picture this size needs that much memory."
} else {
"Reading a file this large may take a while."
};
popup_body(
f,
popup_area,
vec![
Line::from(Span::styled(message, style_title())),
Line::from(Span::styled(note, style_file())),
Line::from(Span::styled("Y / Enter - Yes N / Esc - No", style_columns())),
],
);
}
pub(super) fn render_editor_save_popup(f: &mut ratatui::Frame<'_>, area: Rect) {
let popup_area = popup_rect(25, area);
let popup_block = Block::default().title(Line::from(Span::styled(" Unsaved Changes ", style_title())).centered()).borders(Borders::ALL).style(style_border());
clear(f, popup_area);
f.render_widget(popup_block, popup_area);
popup_body(
f,
popup_area,
vec![
Line::from(Span::styled("Save changes before closing?", style_title())),
Line::from(Span::styled("Y - Save N - Discard Esc - Cancel", style_columns())),
],
);
}
pub(super) const POPUP_WIDTH: u16 = 60;
fn popup_rect(percent_y: u16, area: Rect) -> Rect {
let panels = screen_layout(area)[2];
let width = area.width as usize * POPUP_WIDTH as usize / 100;
let height = area.height as usize * percent_y as usize / 100;
centered(panels, width.max(1), height.max(1))
}