use ratatui::{ layout::{Alignment, Rect}, style::{Modifier, Style}, text::{Line, Span}, widgets::{Clear, List, ListItem, ListState, Paragraph}, Frame, }; use super::widgets::{panel_contrast_fg, render_panel_shell}; use crate::app::AppState; fn prefix_rhs_label(bindings: &crate::config::ActionKeybinds) -> String { bindings .prefix_rhs_label() .unwrap_or_else(|| "unset".to_string()) } fn keybind_label(bindings: &crate::config::ActionKeybinds) -> String { bindings.label().unwrap_or_else(|| "unset".to_string()) } fn render_bottom_bar(frame: &mut Frame, area: Rect, line: Line<'_>, bg: ratatui::style::Color) { frame.render_widget(Clear, area); let buf = frame.buffer_mut(); for x in area.x..area.x + area.width { buf[(x, area.y)].set_style(Style::default().bg(bg)); } frame.render_widget(Paragraph::new(line), area); } pub(super) fn render_prefix_overlay(app: &AppState, frame: &mut Frame, area: Rect) { let key = Style::default() .fg(app.palette.accent) .add_modifier(Modifier::BOLD); let dim = Style::default().fg(app.palette.overlay0); let mode_style = Style::default() .fg(panel_contrast_fg(&app.palette)) .bg(app.palette.accent) .add_modifier(Modifier::BOLD); let workspace_picker = prefix_rhs_label(&app.keybinds.workspace_picker); let help = prefix_rhs_label(&app.keybinds.help); let prefix = crate::config::format_key_combo((app.prefix_code, app.prefix_mods)); let line = Line::from(vec![ Span::styled(" PREFIX ", mode_style), Span::raw(" "), Span::styled("esc", key), Span::styled(" cancel ", dim), Span::styled(prefix, key), Span::styled(" send prefix ", dim), Span::styled(workspace_picker, key), Span::styled(" workspace nav ", dim), Span::styled(help, key), Span::styled(" keybinds", dim), ]); let overlay_y = area.y + area.height.saturating_sub(1); let overlay_area = Rect::new(area.x, overlay_y, area.width, 1); render_bottom_bar(frame, overlay_area, line, app.palette.panel_bg); } pub(super) fn render_copy_mode_overlay(app: &AppState, frame: &mut Frame, area: Rect) { let key = Style::default() .fg(app.palette.accent) .add_modifier(Modifier::BOLD); let dim = Style::default().fg(app.palette.overlay0); let mode_style = Style::default() .fg(panel_contrast_fg(&app.palette)) .bg(app.palette.accent) .add_modifier(Modifier::BOLD); let select = if app .copy_mode .is_some_and(|copy_mode| copy_mode.selection.is_some()) { "selecting" } else { "select" }; let line = Line::from(vec![ Span::styled(" COPY ", mode_style), Span::raw(" "), Span::styled("h/j/k/l w/b/e { }", key), Span::styled(" move ", dim), Span::styled("v/space", key), Span::styled(format!(" {select} "), dim), Span::styled("y/enter", key), Span::styled(" copy ", dim), Span::styled("q/esc", key), Span::styled(" exit", dim), ]); let overlay_y = area.y + area.height.saturating_sub(1); let overlay_area = Rect::new(area.x, overlay_y, area.width, 1); render_bottom_bar(frame, overlay_area, line, app.palette.panel_bg); } pub(super) fn render_navigate_overlay(app: &AppState, frame: &mut Frame, area: Rect) { let key = Style::default() .fg(app.palette.accent) .add_modifier(Modifier::BOLD); let dim = Style::default().fg(app.palette.overlay0); let mode_style = Style::default() .fg(panel_contrast_fg(&app.palette)) .bg(app.palette.accent) .add_modifier(Modifier::BOLD); let kb = &app.keybinds; let new_tab = prefix_rhs_label(&kb.new_tab); let split_vertical = prefix_rhs_label(&kb.split_vertical); let split_horizontal = prefix_rhs_label(&kb.split_horizontal); let close_pane = prefix_rhs_label(&kb.close_pane); let zoom = prefix_rhs_label(&kb.zoom); let resize = prefix_rhs_label(&kb.resize_mode); let help = prefix_rhs_label(&kb.help); let settings = prefix_rhs_label(&kb.settings); let goto = prefix_rhs_label(&kb.goto); let detach = prefix_rhs_label(&kb.detach); let workspace_nav = format!( "{} / {}", keybind_label(&kb.navigate.workspace_up), keybind_label(&kb.navigate.workspace_down) ); let line = Line::from(vec![ Span::styled(" NAVIGATE ", mode_style), Span::raw(" "), Span::styled("esc", key), Span::styled(" back ", dim), Span::styled(workspace_nav, key), Span::styled(" ws ", dim), Span::styled("⇥", key), Span::styled(" pane ", dim), Span::styled(goto, key), Span::styled(" navigator ", dim), Span::styled(new_tab, key), Span::styled(" new tab ", dim), Span::styled(split_vertical, key), Span::styled(" split│ ", dim), Span::styled(split_horizontal, key), Span::styled(" split─ ", dim), Span::styled(close_pane, key), Span::styled(" close ", dim), Span::styled(zoom, key), Span::styled(" zoom ", dim), Span::styled(resize, key), Span::styled(" resize ", dim), Span::styled(help, key), Span::styled(" keybinds ", dim), Span::styled(settings, key), Span::styled(" settings ", dim), Span::styled(detach, key), Span::styled(" detach", dim), ]); let overlay_y = area.y + area.height.saturating_sub(1); let overlay_area = Rect::new(area.x, overlay_y, area.width, 1); render_bottom_bar(frame, overlay_area, line, app.palette.panel_bg); if app.update_available.is_some() { let status = Line::from(vec![Span::styled( " update ready", Style::default() .fg(app.palette.accent) .add_modifier(Modifier::BOLD), )]); let width = 13u16.min(overlay_area.width); let status_area = Rect::new( overlay_area.x + overlay_area.width.saturating_sub(width), overlay_area.y, width, overlay_area.height, ); frame.render_widget(Clear, status_area); frame.render_widget( Paragraph::new(status).alignment(Alignment::Right), status_area, ); } } pub(super) fn render_global_launcher_menu(app: &AppState, frame: &mut Frame) { let rect = app.global_menu_rect(); let Some(inner) = render_panel_shell(frame, rect, app.palette.accent, app.palette.panel_bg) else { return; }; let items = app.global_menu_labels(); for (idx, item) in items.iter().enumerate() { let y = inner.y + idx as u16; if y >= inner.y + inner.height { break; } let selected = idx == app.global_menu.highlighted; let rect = Rect::new(inner.x, y, inner.width, 1); let selected_style = Style::default() .fg(panel_contrast_fg(&app.palette)) .bg(app.palette.accent) .add_modifier(Modifier::BOLD); let item_style = if selected { selected_style } else { Style::default().fg(app.palette.text) }; let badge_style = if selected { selected_style } else { Style::default() .fg(app.palette.accent) .add_modifier(Modifier::BOLD) }; let line = if app.global_menu_item_has_badge(item) { Line::from(vec![ Span::styled(" ●", badge_style), Span::styled(format!(" {item} "), item_style), ]) } else { Line::from(Span::styled(format!(" {item} "), item_style)) }; frame.render_widget(Paragraph::new(line).alignment(Alignment::Left), rect); } } pub(super) fn render_resize_overlay(app: &AppState, frame: &mut Frame, area: Rect) { let key = Style::default() .fg(app.palette.accent) .add_modifier(Modifier::BOLD); let dim = Style::default().fg(app.palette.overlay0); let mode_style = Style::default() .fg(panel_contrast_fg(&app.palette)) .bg(app.palette.mauve) .add_modifier(Modifier::BOLD); let line = Line::from(vec![ Span::styled(" RESIZE ", mode_style), Span::raw(" "), Span::styled("h/l", key), Span::styled(" width ", dim), Span::styled("j/k", key), Span::styled(" height ", dim), Span::styled("esc", key), Span::styled(" done", dim), ]); let overlay_y = area.y + area.height.saturating_sub(1); let overlay_area = Rect::new(area.x, overlay_y, area.width, 1); render_bottom_bar(frame, overlay_area, line, app.palette.panel_bg); } pub(super) fn render_context_menu(app: &AppState, frame: &mut Frame) { let Some(menu) = &app.context_menu else { return; }; let p = &app.palette; let Some(menu_rect) = app.context_menu_rect() else { return; }; let Some(inner) = render_panel_shell(frame, menu_rect, p.accent, p.panel_bg) else { return; }; let items: Vec = menu .items() .iter() .map(|item| ListItem::new(Line::from(*item))) .collect(); let list = List::new(items) .style(Style::default().fg(p.text)) .highlight_style( Style::default() .bg(p.accent) .fg(panel_contrast_fg(p)) .add_modifier(Modifier::BOLD), ) .highlight_symbol(" "); let mut state = ListState::default().with_selected(Some(menu.list.highlighted)); frame.render_stateful_widget(list, inner, &mut state); }