diff options
Diffstat (limited to 'src/client/shell/render.rs')
| -rw-r--r-- | src/client/shell/render.rs | 313 |
1 files changed, 313 insertions, 0 deletions
diff --git a/src/client/shell/render.rs b/src/client/shell/render.rs new file mode 100644 index 00000000..0e7f3908 --- /dev/null +++ b/src/client/shell/render.rs @@ -0,0 +1,313 @@ +use super::*; + +#[path = "../shell/overlays.rs"] +mod overlays; +#[path = "../shell/sidebar.rs"] +mod sidebar; +#[path = "../shell/tabs.rs"] +mod tabs; + +pub(super) use super::agent_sidebar::{ordered_agent_pane_ids, render_agent_panel}; +pub(super) use overlays::{ + client_navigator_rows, render_client_overlay, render_context_menu, render_global_menu, +}; +pub(super) use sidebar::{render_collapsed_sidebar, render_sidebar, workspace_entries}; +pub(super) use tabs::{render_tab_bar, tab_bar_status_width}; +pub(super) fn render_mode_bar( + buffer: &mut Buffer, + pane_area: Rect, + mode: ClientShellMode, + copy_mode: Option<&ClientCopyModeState>, + endpoint_error: Option<&str>, + update_available: bool, + keybinds: &LiveKeybindConfig, + palette: &Palette, +) -> Option<Rect> { + if (mode == ClientShellMode::Terminal && endpoint_error.is_none()) || pane_area.is_empty() { + return None; + } + + let bar = Rect::new( + pane_area.x, + pane_area.y + pane_area.height.saturating_sub(1), + pane_area.width, + 1, + ); + let base = Style::default().fg(palette.overlay0).bg(palette.panel_bg); + for x in bar.x..bar.x + bar.width { + buffer[(x, bar.y)].set_symbol(" ").set_style(base); + } + + let key = Style::default() + .fg(palette.accent) + .bg(palette.panel_bg) + .add_modifier(Modifier::BOLD); + let mode_style = Style::default() + .fg(match palette.panel_bg { + ratatui::style::Color::Reset => palette.surface_dim, + color => color, + }) + .bg(if mode == ClientShellMode::Resize { + palette.mauve + } else { + palette.accent + }) + .add_modifier(Modifier::BOLD); + let prefix = crate::config::format_key_combo(keybinds.prefix); + let prefix_rhs = |bindings: &crate::config::ActionKeybinds| { + bindings + .prefix_rhs_label() + .unwrap_or_else(|| "unset".to_owned()) + }; + + let mut segments = Vec::<(String, Style)>::new(); + if let Some(error) = endpoint_error { + segments.extend([ + (" ERROR ".to_owned(), mode_style), + (format!(" {error}"), base), + ]); + } else { + match mode { + ClientShellMode::Prefix => { + segments.extend([ + (" PREFIX ".to_owned(), mode_style), + (" ".to_owned(), base), + ("esc".to_owned(), key), + (" cancel ".to_owned(), base), + (prefix, key), + (" send prefix ".to_owned(), base), + (prefix_rhs(&keybinds.keybinds.workspace_picker), key), + (" workspace nav ".to_owned(), base), + (prefix_rhs(&keybinds.keybinds.help), key), + (" keybinds".to_owned(), base), + ]); + } + ClientShellMode::Navigate => { + segments.extend([ + (" NAVIGATE ".to_owned(), mode_style), + (" esc back ".to_owned(), base), + ("↑/↓".to_owned(), key), + (" workspace ".to_owned(), base), + ("tab".to_owned(), key), + (" pane ".to_owned(), base), + (prefix_rhs(&keybinds.keybinds.help), key), + (" keybinds".to_owned(), base), + ]); + } + ClientShellMode::Resize => { + segments.extend([ + (" RESIZE ".to_owned(), mode_style), + (" ".to_owned(), base), + ("h/l".to_owned(), key), + (" width ".to_owned(), base), + ("j/k".to_owned(), key), + (" height ".to_owned(), base), + ("esc".to_owned(), key), + (" done".to_owned(), base), + ]); + } + ClientShellMode::Copy => { + let copy_mode = copy_mode?; + if let Some(prompt) = copy_mode.search_prompt.as_ref() { + let marker = match prompt.direction { + crate::api::schema::PaneCopySearchDirection::Forward => "/", + crate::api::schema::PaneCopySearchDirection::Backward => "?", + }; + segments.extend([ + (" COPY ".to_owned(), mode_style), + (" ".to_owned(), base), + (marker.to_owned(), key), + ( + prompt.query.clone(), + Style::default().fg(palette.text).bg(palette.panel_bg), + ), + ("█".to_owned(), key), + (" enter search esc cancel".to_owned(), base), + ]); + } else { + let select = if copy_mode.selection.is_some() { + "selecting" + } else { + "select" + }; + let match_status = copy_mode + .search_current_global + .map(|current| format!(" {}/{}", current + 1, copy_mode.search_total)) + .or_else(|| (!copy_mode.search_query.is_empty()).then(|| " 0/0".to_owned())) + .unwrap_or_default(); + let (exit_keys, exit_label) = + if copy_mode.search_query.is_empty() && copy_mode.selection.is_none() { + ("q/esc", " exit") + } else { + ("esc", " clear q exit") + }; + segments.extend([ + (" COPY ".to_owned(), mode_style), + (" ".to_owned(), base), + ("h/j/k/l w/b/e { }".to_owned(), key), + (" move ".to_owned(), base), + ("/ ?".to_owned(), key), + (" search ".to_owned(), base), + ("n/N".to_owned(), key), + (format!(" repeat{match_status} "), base), + ("v/space".to_owned(), key), + (format!(" {select} "), base), + ("y/enter".to_owned(), key), + (" copy ".to_owned(), base), + (exit_keys.to_owned(), key), + (exit_label.to_owned(), base), + ]); + } + } + ClientShellMode::Terminal => unreachable!(), + } + } + + let mut x = bar.x; + let end = bar.x + bar.width; + for (text, style) in segments { + if x >= end { + break; + } + let remaining = end - x; + buffer.set_stringn(x, bar.y, &text, usize::from(remaining), style); + x = x.saturating_add( + u16::try_from(UnicodeWidthStr::width(text.as_str())) + .unwrap_or(u16::MAX) + .min(remaining), + ); + } + if update_available && mode == ClientShellMode::Navigate { + let width = 13.min(bar.width); + let area = Rect::new(bar.right().saturating_sub(width), bar.y, width, 1); + buffer.set_style(area, Style::default().bg(palette.panel_bg)); + put_right_text( + buffer, + area, + area.y, + " update ready", + Style::default() + .fg(palette.accent) + .bg(palette.panel_bg) + .add_modifier(Modifier::BOLD), + ); + } + Some(bar) +} + +pub(super) struct ShellRenderState<'a> { + pub(super) collapsed_groups: &'a HashSet<String>, + pub(super) workspace_scroll: &'a mut usize, + pub(super) agent_scroll: &'a mut usize, + pub(super) tab_scroll: &'a mut usize, + pub(super) reveal_focused_tab: &'a mut bool, + pub(super) sidebar_collapsed: bool, + pub(super) sidebar_section_split: f32, + pub(super) tab_drag_insert_index: Option<usize>, + pub(super) selected_workspace_id: Option<&'a str>, + pub(super) dragged_workspace_id: Option<&'a str>, + pub(super) workspace_drop_indicator_row: Option<u16>, +} + +pub(super) fn render_shell( + buffer: &mut Buffer, + layout: ClientShellLayout, + snapshot: &ClientShellSnapshot, + config: &ClientShellConfig, + mut state: ShellRenderState<'_>, +) -> ShellHitMap { + let mut hits = ShellHitMap::default(); + if layout.mobile_header.height > 0 { + super::mobile::render_mobile_header( + buffer, + layout.mobile_header, + snapshot, + config, + &mut hits, + ); + } + if layout.sidebar.width > 0 { + if state.sidebar_collapsed { + render_collapsed_sidebar( + buffer, + layout.sidebar, + snapshot, + config, + state.selected_workspace_id, + &mut hits, + ); + } else { + render_sidebar( + buffer, + layout.sidebar, + snapshot, + config, + &mut state, + &mut hits, + ); + } + } + if layout.tab_bar.height > 0 { + render_tab_bar( + buffer, + layout.tab_bar, + snapshot, + config, + state.tab_scroll, + state.reveal_focused_tab, + state.tab_drag_insert_index, + &mut hits, + ); + } + if !config.mouse_capture { + hits.sidebar_divider = Rect::default(); + hits.sidebar_section_divider = Rect::default(); + hits.workspace_scrollbar = Rect::default(); + hits.agent_scrollbar = Rect::default(); + hits.agent_sort_toggle = Rect::default(); + hits.new_workspace = Rect::default(); + hits.workspaces.clear(); + hits.agents.clear(); + hits.tab_scroll_left = Rect::default(); + hits.tab_scroll_right = Rect::default(); + hits.new_tab = Rect::default(); + hits.pane_splits.clear(); + } + hits +} + +fn put_right_text(buffer: &mut Buffer, area: Rect, y: u16, text: &str, style: Style) { + let width = display_width(text).min(area.width); + put_text( + buffer, + area.right().saturating_sub(width), + y, + width, + text, + style, + ); +} + +pub(super) fn put_segment( + buffer: &mut Buffer, + x: u16, + y: u16, + right: u16, + text: &str, + style: Style, +) -> u16 { + let width = display_width(text).min(right.saturating_sub(x)); + put_text(buffer, x, y, width, text, style); + x.saturating_add(width) +} + +pub(super) fn put_text(buffer: &mut Buffer, x: u16, y: u16, width: u16, text: &str, style: Style) { + if width == 0 || y >= buffer.area.bottom() || x >= buffer.area.right() { + return; + } + buffer.set_stringn(x, y, text, width as usize, style); +} + +pub(super) fn display_width(text: &str) -> u16 { + UnicodeWidthStr::width(text).min(u16::MAX as usize) as u16 +} |
