use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::Frame;
use crate::actions::fs_ops::DeleteMode;
use crate::app::mode::Mode;
use crate::app::state::{AppState, NoticeLevel};
use crate::ui::theme;
pub fn draw(frame: &mut Frame, area: Rect, state: &AppState) {
let styles = theme::resolve(&state.config.theme);
if let Mode::Command { buffer, cursor, .. } = &state.mode {
draw_command_line(frame, area, buffer, *cursor, styles.command);
return;
}
let sections = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(25), Constraint::Percentage(30), Constraint::Percentage(45), ])
.split(area);
let shell_focused = state.terminal.shell_focused();
let mode_label = if shell_focused {
"TERMINAL"
} else {
state.mode.label()
};
let mode_style = match &state.mode {
_ if shell_focused => Style::default()
.fg(Color::Black)
.bg(theme::parse_color(&state.config.theme.command))
.add_modifier(Modifier::BOLD),
Mode::Navigation => Style::default()
.fg(Color::Black)
.bg(theme::parse_color(&state.config.theme.git_clean))
.add_modifier(Modifier::BOLD),
Mode::Search { .. } => Style::default()
.fg(Color::Black)
.bg(theme::parse_color(&state.config.theme.search))
.add_modifier(Modifier::BOLD),
Mode::Visual { .. } => Style::default()
.fg(Color::Black)
.bg(theme::parse_color(&state.config.theme.search))
.add_modifier(Modifier::BOLD),
Mode::Command { .. } => Style::default()
.fg(Color::Black)
.bg(theme::parse_color(&state.config.theme.command))
.add_modifier(Modifier::BOLD),
};
let mut left_spans = vec![
Span::styled(format!(" {mode_label} "), mode_style),
Span::raw(" "),
];
if shell_focused {
let panel = &state.terminal;
if let Some(session) = panel.sessions().get(panel.active()) {
left_spans.push(Span::styled(
format!(
"{}/{} {} ",
panel.active() + 1,
panel.sessions().len(),
session.label()
),
styles.command,
));
}
} else if !state.tab_manager.is_single() {
left_spans.push(Span::styled(
format!(
"[{}/{}] ",
state.tab_manager.active + 1,
state.tab_manager.len()
),
styles.command,
));
}
if let Some(pending) = state.pending_nav_key {
left_spans.push(Span::styled(format!("{pending}… "), styles.command));
}
frame.render_widget(Paragraph::new(Line::from(left_spans)), sections[0]);
if let Some((text, style)) = message(state, &styles) {
let rest = Rect {
width: sections[1].width + sections[2].width,
..sections[1]
};
let elided = elide(&text, usize::from(rest.width));
frame.render_widget(
Paragraph::new(Line::from(Span::styled(elided, style))),
rest,
);
return;
}
if let Mode::Search { query, .. } = &state.mode {
let center = Paragraph::new(Line::from(Span::styled(format!("/{query}"), styles.search)));
frame.render_widget(center, sections[1]);
} else if let Some(text) = activity(state) {
let elided = elide(&text, usize::from(sections[1].width));
frame.render_widget(
Paragraph::new(Line::from(Span::styled(elided, styles.command))),
sections[1],
);
}
let right_text = if let Some(ref git) = state.git {
let dirty_marker = if git.is_dirty { " *" } else { "" };
format!(
" {} {}{} {} items ",
"\u{e0a0}", git.branch, dirty_marker, state.status.entry_count
)
} else {
format!("{} items ", state.status.entry_count)
};
let right = Paragraph::new(Line::from(Span::styled(right_text, styles.status)));
frame.render_widget(right, sections[2]);
}
fn message(state: &AppState, styles: &theme::ThemeStyles) -> Option<(String, Style)> {
if let Some(confirm) = state.terminal.pending() {
return Some((
confirm.prompt().to_owned(),
Style::default()
.fg(Color::Black)
.bg(theme::parse_color(&state.config.theme.error))
.add_modifier(Modifier::BOLD),
));
}
if state.pending_delete {
let name = if let Some((first, last)) = crate::app::marks::visual_bounds(state) {
format!("{} selected", last - first + 1)
} else if state.marks.is_empty() {
let name = state
.selected_entry()
.map(|e| e.file_name.as_str())
.unwrap_or("selected entry");
format!("'{name}'")
} else {
format!("{} marked", state.marks.len())
};
let verb = match DeleteMode::parse(&state.config.general.delete_mode) {
Some(DeleteMode::Permanent) => "Delete",
_ => "Recycle",
};
return Some((
format!(" {verb} {name}? [y/Enter=yes, n/Esc=cancel] "),
Style::default()
.fg(Color::Black)
.bg(theme::parse_color(&state.config.theme.error))
.add_modifier(Modifier::BOLD),
));
}
let notice = state.notice.as_ref()?;
Some(match notice.level {
NoticeLevel::Error => (format!(" Error: {} ", notice.text), styles.error),
NoticeLevel::Info => (format!(" {} ", notice.text), styles.git_clean),
})
}
fn activity(state: &AppState) -> Option<String> {
let marked = (!state.marks.is_empty()).then(|| format!("{} marked", state.marks.len()));
let selecting = crate::app::marks::visual_bounds(state)
.map(|(first, last)| format!("{} selected", last - first + 1));
let parts: Vec<String> = selecting
.into_iter()
.chain(marked)
.chain(
state
.transfers
.active()
.iter()
.map(|t| format!("{} {}/{}", t.kind.progressive(), t.done, t.total)),
)
.collect();
(!parts.is_empty()).then(|| parts.join(" · "))
}
fn draw_command_line(frame: &mut Frame, area: Rect, buffer: &str, cursor: usize, style: Style) {
let prompt = if buffer.starts_with('!') { "" } else { ":" };
let cursor_col = prompt.chars().count() + buffer[..cursor].chars().count();
let width = usize::from(area.width);
let offset = cursor_col.saturating_sub(width.saturating_sub(1));
let full: String = format!("{prompt}{buffer}");
let visible: String = full.chars().skip(offset).take(width).collect();
frame.render_widget(
Paragraph::new(Line::from(Span::styled(visible, style))),
area,
);
let column = area.x.saturating_add((cursor_col - offset) as u16);
frame.set_cursor_position((column, area.y));
}
fn elide(text: &str, width: usize) -> String {
if width == 0 {
return String::new();
}
if text.chars().count() <= width {
return text.to_owned();
}
let mut out: String = text.chars().take(width.saturating_sub(1)).collect();
out.push('…');
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn elide_leaves_text_that_fits_alone() {
assert_eq!(elide("12 items", 20), "12 items");
assert_eq!(elide("12 items", 8), "12 items");
}
#[test]
fn elide_marks_what_it_drops() {
assert_eq!(elide("destination already exists", 10), "destinati…");
assert_eq!(elide("abc", 1), "…");
}
#[test]
fn elide_of_zero_width_is_empty() {
assert_eq!(elide("anything", 0), "");
}
}