use crate::app::{AppState, AttributeKind, AttributesForm, BranchList, Dialog, FindFiles, PromptKind, Screen, TextInput, TransferKind};
use crate::constants::*;
use crate::display::{palette, tab_width};
use crate::options::{Clock, DateFormat, IconStyle, OPTION_ROWS, Options, ScrollBarStyle, SortKey};
use crate::strip::{ARROW, Strip, StripHit};
use crate::utils::*;
use crate::viewer::ViewMode;
use chrono::Local;
use ratatui::{
Terminal,
backend::Backend,
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
style::{Modifier, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, Cell, Clear, Paragraph, Row, Table, TableState},
};
use std::path::{Path, PathBuf};
use unicode_width::UnicodeWidthChar;
mod bars;
mod checksums;
mod help;
mod options_popup;
mod panels;
mod popups;
mod screens;
mod scroll_bars;
mod spans;
#[cfg(test)]
mod tests;
pub use self::help::*;
use self::{bars::*, checksums::*, options_popup::*, panels::*, popups::*, screens::*, scroll_bars::*, spans::*};
fn style_border() -> Style {
Style::new().fg(palette().border)
}
fn style_title() -> Style {
Style::new().fg(palette().title)
}
fn style_columns() -> Style {
Style::new().fg(palette().columns)
}
fn style_file() -> Style {
Style::new().fg(palette().file)
}
fn style_dir() -> Style {
Style::new().fg(palette().directory)
}
fn style_dir_dark() -> Style {
Style::new().fg(palette().directory_dark)
}
fn style_input() -> Style {
Style::new().fg(palette().selected_foreground).bg(palette().rename_background).add_modifier(Modifier::BOLD)
}
fn style_input_cursor() -> Style {
style_input().add_modifier(FIELD_CURSOR)
}
const FIELD_CURSOR: Modifier = Modifier::SLOW_BLINK;
const EDITOR_CURSOR: Modifier = Modifier::RAPID_BLINK;
fn style_selection() -> Style {
Style::new().bg(palette().selected_background_inactive)
}
fn style_match() -> Style {
Style::new().add_modifier(Modifier::UNDERLINED | Modifier::BOLD)
}
pub fn render_ui<B: Backend>(terminal: &mut Terminal<B>, app_state: &mut AppState) {
let current_time = match app_state.options.clock {
Clock::Hours24 => Local::now().format(" %H:%M:%S ").to_string(),
Clock::Hours12 => Local::now().format(" %I:%M:%S %p ").to_string(),
Clock::Off => String::new(),
};
if app_state.cached_clock != current_time {
app_state.cached_clock = current_time;
}
crate::display::set_classic_cursor(app_state.options.cursor == crate::options::CursorShape::Classic);
let _ = terminal.draw(|f| {
draw_frame(f, app_state);
show_cursor(f, app_state);
});
}
fn show_cursor(f: &mut ratatui::Frame<'_>, app_state: &AppState) {
let classic = app_state.options.cursor == crate::options::CursorShape::Classic;
let buffer = f.buffer_mut();
let (mut field, mut editor) = (None, None);
for y in buffer.area.top()..buffer.area.bottom() {
for x in buffer.area.left()..buffer.area.right() {
let cell = &mut buffer[(x, y)];
if cell.modifier.contains(crate::app::PAST_END) {
cell.modifier.remove(crate::app::PAST_END);
if !classic && x + 1 < buffer.area.right() {
let after = buffer[(x + 1, y)].bg;
buffer[(x, y)].set_bg(after);
}
}
let cell = &mut buffer[(x, y)];
if cell.modifier.contains(crate::app::CURSOR_AFTER) {
cell.modifier.remove(crate::app::CURSOR_AFTER);
let width = display_width(cell.symbol()).max(1) as u16;
field.get_or_insert(ratatui::layout::Position::new((x + width).min(buffer.area.right() - 1), y));
}
let cell = &mut buffer[(x, y)];
if cell.modifier.contains(FIELD_CURSOR) {
cell.modifier.remove(FIELD_CURSOR);
field.get_or_insert(ratatui::layout::Position::new(x, y));
if classic {
cell.modifier.insert(Modifier::REVERSED);
}
}
if cell.modifier.contains(EDITOR_CURSOR) {
cell.modifier.remove(EDITOR_CURSOR);
editor.get_or_insert(ratatui::layout::Position::new(x, y));
if classic {
cell.set_fg(palette().selected_foreground).set_bg(palette().selected_background);
}
}
}
}
if !classic
&& app_state.job.is_none()
&& app_state.error.is_none()
&& let Some(position) = field.or(editor.filter(|_| app_state.dialog.is_none()))
{
f.set_cursor_position(position);
}
}
fn screen_layout(area: Rect) -> std::rc::Rc<[Rect]> {
Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Length(1), Constraint::Percentage(100), Constraint::Length(1), Constraint::Length(3)])
.split(area)
}
fn draw_frame(f: &mut ratatui::Frame<'_>, app_state: &mut AppState) {
{
let area = f.area();
paint_background(f, area);
if area.height < 10 || area.width < 30 {
let msg = Paragraph::new("Terminal too small").alignment(Alignment::Center).style(style_title());
let y = area.height / 2;
if y < area.height {
f.render_widget(msg, Rect::new(area.x, area.y + y, area.width, 1));
}
return;
}
let chunks_main = screen_layout(area);
render_top_panel(f, chunks_main[0], app_state);
let going_to = match &app_state.dialog {
Some(Dialog::Prompt(PromptKind::GoTo, input)) => Some(input),
_ => None,
};
app_state.branch_hits = render_path_bar(f, chunks_main[1], app_state, going_to);
app_state.path_bar_area = chunks_main[1];
app_state.scroll_bars.clear();
match app_state.screen {
Screen::Viewer(_) => {
let (height, width, content_area, bars) = render_viewer(f, chunks_main[2], app_state);
app_state.viewer_viewport_height = height;
app_state.viewer_viewport_width = width;
app_state.viewer_content_area = content_area;
app_state.scroll_bars = bars;
}
Screen::Editor(_) => app_state.editor_viewport_height = render_editor(f, chunks_main[2], app_state),
Screen::Diff(_) => {
let (height, width) = render_diff(f, chunks_main[2], app_state);
app_state.diff_viewport_height = height;
app_state.diff_viewport_width = width;
}
Screen::Panels => app_state.page_size = render_file_tables(f, chunks_main[2], app_state),
}
render_bottom_panel(f, chunks_main[3], app_state);
render_fkey_bar(f, chunks_main[4], app_state);
render_detail(f, chunks_main[4].inner(Margin { vertical: 0, horizontal: 2 }), app_state);
let mut find_files_page = None;
let mut attribute_rows = Vec::new();
let mut branch_rows = Vec::new();
match &app_state.dialog {
Some(Dialog::Help) => {
app_state.help_room = render_help_popup(f, area, app_state.help_scroll);
app_state.help_scroll = app_state.help_scroll.min(help_lines().len().saturating_sub(app_state.help_room));
}
Some(Dialog::Options { editing }) => render_options_popup(f, area, app_state, editing.as_ref()),
Some(Dialog::Create { is_dir, input }) => render_create_popup(f, area, *is_dir, input),
Some(Dialog::Delete { items, to_trash }) => render_delete_popup(f, area, items, *to_trash),
Some(Dialog::Transfer { sources, is_copy, input, .. }) => render_destination_popup(f, area, sources, None, if *is_copy { "Copy" } else { "Move" }, "to", input, None),
Some(Dialog::Symlink { sources, input, hard, .. }) => {
let (verb, other) = if *hard { ("Hard link", "Tab - Symlink") } else { ("Symlink", "Tab - Hard link") };
render_destination_popup(f, area, sources, None, verb, "at", input, Some(other));
}
Some(Dialog::Extract { archive, input }) => render_destination_popup(f, area, &[(archive.clone(), false)], Some("archive"), "Extract", "to", input, None),
Some(Dialog::Checksums(_)) => {
if let Some(Dialog::Checksums(view)) = &mut app_state.dialog {
render_checksums_popup(f, area, view);
}
}
Some(Dialog::Pack { sources, input }) => render_destination_popup(f, area, sources, None, "Pack", "into", input, Some("Tab - Format")),
Some(Dialog::LargeFile(large)) => render_large_file_popup(f, area, large),
Some(Dialog::SaveChanges { .. }) => render_editor_save_popup(f, area),
Some(Dialog::DiskChanged { opened, now, .. }) => render_disk_changed_popup(f, area, app_state, *opened, *now),
Some(Dialog::FindFiles(find)) => match find.as_ref() {
FindFiles::Asking { name, containing, on_containing } => {
let start = if app_state.is_left_active { &app_state.dir_left } else { &app_state.dir_right };
render_find_files_form(f, area, start, name, containing, *on_containing);
}
FindFiles::Found(search) => find_files_page = Some(render_find_files_results(f, area, search)),
},
Some(Dialog::Attributes(form)) => attribute_rows = render_attributes_popup(f, area, form),
Some(Dialog::Branches(list)) => branch_rows = render_branches_popup(f, area, list),
Some(Dialog::Bookmarks { list, index }) => render_bookmarks_popup(f, area, list, *index, &app_state.panel_dir(app_state.is_left_active)),
Some(Dialog::History(list)) => render_history_popup(f, area, list, &app_state.panel_dir(app_state.is_left_active)),
Some(Dialog::Rename(_) | Dialog::Prompt(..) | Dialog::DrivePicker { .. } | Dialog::Replacing { .. }) | None => {}
}
if let Some(page) = find_files_page {
app_state.find_files_page = page;
}
app_state.attribute_rows = attribute_rows;
app_state.branch_rows = branch_rows;
if app_state.job.is_some() {
render_transfer_popup(f, area, app_state);
} else if let Some(message) = &app_state.error {
render_error_popup(f, area, message);
}
}
}
fn shown_name(path: &std::path::Path) -> String {
path.file_name().map(|name| printable_name(&name.to_string_lossy())).unwrap_or_default()
}
fn paint_background(f: &mut ratatui::Frame<'_>, area: Rect) {
if let Some(background) = crate::display::background() {
f.render_widget(Block::new().style(Style::new().bg(background)), area);
}
}
fn clear(f: &mut ratatui::Frame<'_>, area: Rect) {
f.render_widget(Clear, area);
paint_background(f, area);
}
fn centered(area: Rect, width: usize, height: usize) -> Rect {
let width = width.min(area.width as usize) as u16;
let height = height.min(area.height as usize) as u16;
Rect::new(area.x + (area.width - width) / 2, area.y + (area.height - height) / 2, width, height)
}