From bdfc98b0bd08debf61cf488913685bf390514d97 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Tue, 16 Dec 2025 20:03:06 +0100 Subject: [PATCH] powerline? --- src/main.rs | 318 ++++++++++++++++++++++++++++++++++++++++- src/pty.rs | 12 ++ src/renderer.rs | 370 ++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 689 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index 834cd79..d11c45c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use zterm::config::{Action, Config}; use zterm::keyboard::{FunctionalKey, KeyEncoder, KeyEventType, KeyboardState, Modifiers}; use zterm::pty::Pty; -use zterm::renderer::{EdgeGlow, PaneRenderInfo, Renderer}; +use zterm::renderer::{EdgeGlow, PaneRenderInfo, Renderer, StatuslineComponent, StatuslineSection}; use zterm::terminal::{Direction, Terminal, TerminalCommand, MouseTrackingMode}; use std::collections::HashMap; @@ -755,6 +755,307 @@ fn remove_pid_file() { let _ = std::fs::remove_file(pid_file_path()); } +/// Build a statusline section for the current working directory. +/// +/// Transforms the path into styled segments within a section: +/// - Replaces $HOME prefix with "~" +/// - Each directory segment gets " " prefix and its own color +/// - Arrow separator "" between segments inherits previous segment's color +/// - Colors cycle through palette indices 2-7 (skipping 0-1 which are often close to white) +/// - Last segment is bold +/// - Section has a dark gray background color (#282828) +/// - Section ends with powerline arrow transition +fn build_cwd_section(cwd: &str) -> StatuslineSection { + // Colors to cycle through (skip 0 and 1 which are often near-white in custom schemes) + const COLORS: [u8; 6] = [2, 3, 4, 5, 6, 7]; + + let mut components = Vec::new(); + + // Get home directory and replace prefix with ~ + let display_path = if let Ok(home) = std::env::var("HOME") { + if cwd.starts_with(&home) { + format!("~{}", &cwd[home.len()..]) + } else { + cwd.to_string() + } + } else { + cwd.to_string() + }; + + // Split path into segments + let segments: Vec<&str> = display_path + .split('/') + .filter(|s| !s.is_empty()) + .collect(); + + if segments.is_empty() { + // Root directory + components.push(StatuslineComponent::new(" \u{F07C} / ").fg(COLORS[0])); + return StatuslineSection::with_rgb_bg(0x28, 0x28, 0x28).with_components(components); + } + + // Add leading space for padding + components.push(StatuslineComponent::new(" ")); + + let last_idx = segments.len() - 1; + + for (i, segment) in segments.iter().enumerate() { + // Cycle through colors for each segment + let color = COLORS[i % COLORS.len()]; + + if i > 0 { + // Add arrow separator with previous segment's color + // U+E0B1 is the powerline thin chevron right + let prev_color = COLORS[(i - 1) % COLORS.len()]; + components.push(StatuslineComponent::new(" \u{E0B1} ").fg(prev_color)); + } + + // Directory segment with folder icon prefix + // U+F07C is the folder-open icon from Nerd Fonts + let text = format!("\u{F07C} {}", segment); + let component = if i == last_idx { + // Last segment is bold + StatuslineComponent::new(text).fg(color).bold() + } else { + StatuslineComponent::new(text).fg(color) + }; + + components.push(component); + } + + // Add trailing space for padding before the powerline arrow + components.push(StatuslineComponent::new(" ")); + + // Use dark gray (#282828) as section background + StatuslineSection::with_rgb_bg(0x28, 0x28, 0x28).with_components(components) +} + +/// Git repository status information. +#[derive(Debug, Default)] +struct GitStatus { + /// Current branch or HEAD reference. + head: String, + /// Number of commits ahead of upstream. + ahead: usize, + /// Number of commits behind upstream. + behind: usize, + /// Working directory changes (modified, deleted, untracked, etc.). + working_changed: usize, + /// Working directory status string (e.g., "~1 +2 -1"). + working_string: String, + /// Staged changes count. + staging_changed: usize, + /// Staging status string. + staging_string: String, + /// Number of stashed changes. + stash_count: usize, +} + +/// Get git status for a directory. +/// Returns None if not in a git repository. +fn get_git_status(cwd: &str) -> Option { + use std::process::Command; + + // Check if we're in a git repo and get the branch name + let head_output = Command::new("git") + .args(["rev-parse", "--abbrev-ref", "HEAD"]) + .current_dir(cwd) + .output() + .ok()?; + + if !head_output.status.success() { + return None; + } + + let head = String::from_utf8_lossy(&head_output.stdout).trim().to_string(); + + // Get ahead/behind status + let mut ahead = 0; + let mut behind = 0; + if let Ok(output) = Command::new("git") + .args(["rev-list", "--left-right", "--count", "HEAD...@{upstream}"]) + .current_dir(cwd) + .output() + { + if output.status.success() { + let counts = String::from_utf8_lossy(&output.stdout); + let parts: Vec<&str> = counts.trim().split_whitespace().collect(); + if parts.len() == 2 { + ahead = parts[0].parse().unwrap_or(0); + behind = parts[1].parse().unwrap_or(0); + } + } + } + + // Get working directory and staging status using git status --porcelain + let mut working_modified = 0; + let mut working_added = 0; + let mut working_deleted = 0; + let mut staging_modified = 0; + let mut staging_added = 0; + let mut staging_deleted = 0; + + if let Ok(output) = Command::new("git") + .args(["status", "--porcelain"]) + .current_dir(cwd) + .output() + { + if output.status.success() { + let status = String::from_utf8_lossy(&output.stdout); + for line in status.lines() { + if line.len() < 2 { + continue; + } + let chars: Vec = line.chars().collect(); + let staging_char = chars[0]; + let working_char = chars[1]; + + // Staging status (first column) + match staging_char { + 'M' => staging_modified += 1, + 'A' => staging_added += 1, + 'D' => staging_deleted += 1, + 'R' => staging_modified += 1, // renamed + 'C' => staging_added += 1, // copied + _ => {} + } + + // Working directory status (second column) + match working_char { + 'M' => working_modified += 1, + 'D' => working_deleted += 1, + '?' => working_added += 1, // untracked + _ => {} + } + } + } + } + + // Build status strings like oh-my-posh format + let working_changed = working_modified + working_added + working_deleted; + let mut working_parts = Vec::new(); + if working_modified > 0 { + working_parts.push(format!("~{}", working_modified)); + } + if working_added > 0 { + working_parts.push(format!("+{}", working_added)); + } + if working_deleted > 0 { + working_parts.push(format!("-{}", working_deleted)); + } + let working_string = working_parts.join(" "); + + let staging_changed = staging_modified + staging_added + staging_deleted; + let mut staging_parts = Vec::new(); + if staging_modified > 0 { + staging_parts.push(format!("~{}", staging_modified)); + } + if staging_added > 0 { + staging_parts.push(format!("+{}", staging_added)); + } + if staging_deleted > 0 { + staging_parts.push(format!("-{}", staging_deleted)); + } + let staging_string = staging_parts.join(" "); + + // Get stash count + let mut stash_count = 0; + if let Ok(output) = Command::new("git") + .args(["stash", "list"]) + .current_dir(cwd) + .output() + { + if output.status.success() { + let stash = String::from_utf8_lossy(&output.stdout); + stash_count = stash.lines().count(); + } + } + + Some(GitStatus { + head, + ahead, + behind, + working_changed, + working_string, + staging_changed, + staging_string, + stash_count, + }) +} + +/// Build a statusline section for git status. +/// Returns None if not in a git repository. +fn build_git_section(cwd: &str) -> Option { + let status = get_git_status(cwd)?; + + // Determine foreground color based on state (matching oh-my-posh template) + // Priority order (last match wins in oh-my-posh): + // 1. Default: #0da300 (green) + // 2. If working or staging changed: #FF9248 (orange) + // 3. If both ahead and behind: #ff4500 (red-orange) + // 4. If ahead or behind: #B388FF (purple) + let fg_color: (u8, u8, u8) = if status.ahead > 0 && status.behind > 0 { + (0xff, 0x45, 0x00) // #ff4500 - red-orange + } else if status.ahead > 0 || status.behind > 0 { + (0xB3, 0x88, 0xFF) // #B388FF - purple + } else if status.working_changed > 0 || status.staging_changed > 0 { + (0xFF, 0x92, 0x48) // #FF9248 - orange + } else { + (0x0d, 0xa3, 0x00) // #0da300 - green + }; + + let mut components = Vec::new(); + + // Leading space + components.push(StatuslineComponent::new(" ").rgb_fg(fg_color.0, fg_color.1, fg_color.2)); + + // Branch name (HEAD) + // Use git branch icon U+E0A0 + let head_text = format!("\u{E0A0} {}", status.head); + components.push(StatuslineComponent::new(head_text).rgb_fg(fg_color.0, fg_color.1, fg_color.2)); + + // Branch status (ahead/behind) + if status.ahead > 0 || status.behind > 0 { + let mut branch_status = String::new(); + if status.ahead > 0 { + branch_status.push_str(&format!(" ↑{}", status.ahead)); + } + if status.behind > 0 { + branch_status.push_str(&format!(" ↓{}", status.behind)); + } + components.push(StatuslineComponent::new(branch_status).rgb_fg(fg_color.0, fg_color.1, fg_color.2)); + } + + // Working directory changes - U+F044 is the edit/pencil icon + if status.working_changed > 0 { + let working_text = format!(" \u{F044} {}", status.working_string); + components.push(StatuslineComponent::new(working_text).rgb_fg(fg_color.0, fg_color.1, fg_color.2)); + } + + // Separator between working and staging (if both have changes) + if status.working_changed > 0 && status.staging_changed > 0 { + components.push(StatuslineComponent::new(" |").rgb_fg(fg_color.0, fg_color.1, fg_color.2)); + } + + // Staged changes - U+F046 is the check/staged icon + if status.staging_changed > 0 { + let staging_text = format!(" \u{F046} {}", status.staging_string); + components.push(StatuslineComponent::new(staging_text).rgb_fg(fg_color.0, fg_color.1, fg_color.2)); + } + + // Stash count - U+EB4B is the stash icon + if status.stash_count > 0 { + let stash_text = format!(" \u{EB4B} {}", status.stash_count); + components.push(StatuslineComponent::new(stash_text).rgb_fg(fg_color.0, fg_color.1, fg_color.2)); + } + + // Trailing space + components.push(StatuslineComponent::new(" ").rgb_fg(fg_color.0, fg_color.1, fg_color.2)); + + // Background: #232323 + Some(StatuslineSection::with_rgb_bg(0x23, 0x23, 0x23).with_components(components)) +} + /// A cell position in the terminal grid. #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct CellPosition { @@ -2018,7 +2319,20 @@ impl ApplicationHandler for App { pane.terminal.image_storage.has_animations() }); - match renderer.render_panes(&pane_render_data, num_tabs, active_tab_idx, &self.edge_glows, self.config.edge_glow_intensity) { + // Get the cwd from the active pane for the statusline + let statusline_sections: Vec = tab.panes.get(&active_pane_id) + .and_then(|pane| pane.pty.foreground_cwd()) + .map(|cwd| { + let mut sections = vec![build_cwd_section(&cwd)]; + // Add git section if in a git repository + if let Some(git_section) = build_git_section(&cwd) { + sections.push(git_section); + } + sections + }) + .unwrap_or_default(); + + match renderer.render_panes(&pane_render_data, num_tabs, active_tab_idx, &self.edge_glows, self.config.edge_glow_intensity, &statusline_sections) { Ok(_) => {} Err(wgpu::SurfaceError::Lost) => { renderer.resize(renderer.width, renderer.height); diff --git a/src/pty.rs b/src/pty.rs index 2445487..b277060 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -213,6 +213,18 @@ impl Pty { .ok() .map(|s| s.trim().to_string()) } + + /// Get the current working directory of the foreground process. + /// Returns the path or None if unavailable. + pub fn foreground_cwd(&self) -> Option { + let pgid = self.foreground_pgid()?; + + // Read the cwd symlink from /proc//cwd + let cwd_path = format!("/proc/{}/cwd", pgid); + std::fs::read_link(&cwd_path) + .ok() + .and_then(|p| p.to_str().map(|s| s.to_string())) + } } impl AsRawFd for Pty { diff --git a/src/renderer.rs b/src/renderer.rs index efe9a53..6a89329 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -39,6 +39,126 @@ pub struct PaneRenderInfo { pub dim_factor: f32, } +// ═══════════════════════════════════════════════════════════════════════════════ +// STATUSLINE COMPONENTS +// ═══════════════════════════════════════════════════════════════════════════════ + +/// Color specification for statusline components. +/// Uses the terminal's indexed color palette (0-255), RGB, or default fg. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StatuslineColor { + /// Use the default foreground color. + Default, + /// Use an indexed color from the 256-color palette (0-15 for ANSI colors). + Indexed(u8), + /// Use an RGB color. + Rgb(u8, u8, u8), +} + +impl Default for StatuslineColor { + fn default() -> Self { + StatuslineColor::Default + } +} + +/// A single component/segment of the statusline. +/// Components are rendered left-to-right with optional separators. +#[derive(Debug, Clone)] +pub struct StatuslineComponent { + /// The text content of this component. + pub text: String, + /// Foreground color for this component. + pub fg: StatuslineColor, + /// Whether this text should be bold. + pub bold: bool, +} + +impl StatuslineComponent { + /// Create a new statusline component with default styling. + pub fn new(text: impl Into) -> Self { + Self { + text: text.into(), + fg: StatuslineColor::Default, + bold: false, + } + } + + /// Set the foreground color using an indexed palette color. + pub fn fg(mut self, color_index: u8) -> Self { + self.fg = StatuslineColor::Indexed(color_index); + self + } + + /// Set the foreground color using RGB values. + pub fn rgb_fg(mut self, r: u8, g: u8, b: u8) -> Self { + self.fg = StatuslineColor::Rgb(r, g, b); + self + } + + /// Set bold styling. + pub fn bold(mut self) -> Self { + self.bold = true; + self + } + + /// Create a separator component (e.g., "/", " > ", etc.). + pub fn separator(text: impl Into) -> Self { + Self { + text: text.into(), + fg: StatuslineColor::Indexed(8), // Dim gray by default + bold: false, + } + } +} + +/// A section of the statusline with its own background color. +/// Sections are rendered left-to-right and end with a powerline transition arrow. +#[derive(Debug, Clone)] +pub struct StatuslineSection { + /// The components within this section. + pub components: Vec, + /// Background color for this section. + pub bg: StatuslineColor, +} + +impl StatuslineSection { + /// Create a new section with the given indexed background color. + pub fn new(bg_color: u8) -> Self { + Self { + components: Vec::new(), + bg: StatuslineColor::Indexed(bg_color), + } + } + + /// Create a new section with an RGB background color. + pub fn with_rgb_bg(r: u8, g: u8, b: u8) -> Self { + Self { + components: Vec::new(), + bg: StatuslineColor::Rgb(r, g, b), + } + } + + /// Create a new section with the default (transparent) background. + pub fn transparent() -> Self { + Self { + components: Vec::new(), + bg: StatuslineColor::Default, + } + } + + /// Add a component to this section. + pub fn push(mut self, component: StatuslineComponent) -> Self { + self.components.push(component); + self + } + + /// Add multiple components to this section. + pub fn with_components(mut self, components: Vec) -> Self { + self.components = components; + self + } +} + /// Edge glow animation state for visual feedback when navigation fails. /// Creates an organic glow effect: a single light node appears at center, /// then splits into two that travel outward to the corners while fading. @@ -1528,10 +1648,27 @@ impl Renderer { } } - /// Returns the Y offset where the terminal content starts. - pub fn terminal_y_offset(&self) -> f32 { + /// Returns the height of the statusline in pixels (one cell height). + pub fn statusline_height(&self) -> f32 { + self.cell_height + } + + /// Returns the Y position where the statusline starts. + /// The statusline is rendered below the tab bar (if top) or above it (if bottom). + pub fn statusline_y(&self) -> f32 { match self.tab_bar_position { TabBarPosition::Top => self.tab_bar_height(), + TabBarPosition::Bottom => self.height as f32 - self.tab_bar_height() - self.statusline_height(), + TabBarPosition::Hidden => 0.0, + } + } + + /// Returns the Y offset where the terminal content starts. + /// Accounts for both the tab bar and the statusline. + pub fn terminal_y_offset(&self) -> f32 { + match self.tab_bar_position { + TabBarPosition::Top => self.tab_bar_height() + self.statusline_height(), + TabBarPosition::Hidden => self.statusline_height(), _ => 0.0, } } @@ -1554,34 +1691,43 @@ impl Renderer { } } - /// Calculates terminal dimensions in cells, accounting for tab bar. + /// Calculates terminal dimensions in cells, accounting for tab bar and statusline. pub fn terminal_size(&self) -> (usize, usize) { - let available_height = self.height as f32 - self.tab_bar_height(); + let available_height = self.height as f32 - self.tab_bar_height() - self.statusline_height(); let cols = (self.width as f32 / self.cell_width).floor() as usize; let rows = (available_height / self.cell_height).floor() as usize; (cols.max(1), rows.max(1)) } /// Converts a pixel position to a terminal cell position. - /// Returns None if the position is outside the terminal area (e.g., in the tab bar). + /// Returns None if the position is outside the terminal area (e.g., in the tab bar or statusline). pub fn pixel_to_cell(&self, x: f64, y: f64) -> Option<(usize, usize)> { let terminal_y_offset = self.terminal_y_offset(); let tab_bar_height = self.tab_bar_height(); + let statusline_height = self.statusline_height(); let height = self.height as f32; - // Check if position is in the tab bar area (which could be at top or bottom) + // Check if position is in the tab bar or statusline area match self.tab_bar_position { TabBarPosition::Top => { - if (y as f32) < tab_bar_height { + // Tab bar at top, statusline below it + if (y as f32) < tab_bar_height + statusline_height { return None; } } TabBarPosition::Bottom => { - if (y as f32) >= height - tab_bar_height { + // Statusline above tab bar, both at bottom + let statusline_y = height - tab_bar_height - statusline_height; + if (y as f32) >= statusline_y { + return None; + } + } + TabBarPosition::Hidden => { + // Just statusline at top + if (y as f32) < statusline_height { return None; } } - TabBarPosition::Hidden => {} } // Adjust y to be relative to terminal area @@ -4455,6 +4601,7 @@ impl Renderer { /// - `active_tab`: Index of the active tab /// - `edge_glows`: Active edge glow animations for visual feedback /// - `edge_glow_intensity`: Intensity of edge glow effect (0.0 = disabled, 1.0 = full) + /// - `statusline_sections`: Sections to render in the statusline pub fn render_panes( &mut self, panes: &[(&Terminal, PaneRenderInfo, Option<(usize, usize, usize, usize)>)], @@ -4462,6 +4609,7 @@ impl Renderer { active_tab: usize, edge_glows: &[EdgeGlow], edge_glow_intensity: f32, + statusline_sections: &[StatuslineSection], ) -> Result<(), wgpu::SurfaceError> { // Sync palette from first terminal if let Some((terminal, _, _)) = panes.first() { @@ -4603,6 +4751,210 @@ impl Renderer { } } + // ═══════════════════════════════════════════════════════════════════ + // RENDER STATUSLINE + // ═══════════════════════════════════════════════════════════════════ + { + let statusline_y = self.statusline_y(); + let statusline_height = self.statusline_height(); + + // Statusline background (slightly different shade than tab bar) + let statusline_bg = { + let [r, g, b] = self.palette.default_bg; + let factor = 0.9_f32; + [ + Self::srgb_to_linear((r as f32 / 255.0) * factor), + Self::srgb_to_linear((g as f32 / 255.0) * factor), + Self::srgb_to_linear((b as f32 / 255.0) * factor), + 1.0, + ] + }; + + // Draw statusline background + self.render_rect(0.0, statusline_y, width, statusline_height, statusline_bg); + + // Render statusline sections + let text_y = statusline_y + (statusline_height - self.cell_height) / 2.0; + let mut cursor_x = 0.0_f32; + + // Helper to convert StatuslineColor to linear RGBA + let color_to_rgba = |color: StatuslineColor, palette: &crate::terminal::ColorPalette| -> [f32; 4] { + match color { + StatuslineColor::Default => { + let [r, g, b] = palette.default_fg; + [ + Self::srgb_to_linear(r as f32 / 255.0), + Self::srgb_to_linear(g as f32 / 255.0), + Self::srgb_to_linear(b as f32 / 255.0), + 1.0, + ] + } + StatuslineColor::Indexed(idx) => { + let [r, g, b] = palette.colors[idx as usize]; + [ + Self::srgb_to_linear(r as f32 / 255.0), + Self::srgb_to_linear(g as f32 / 255.0), + Self::srgb_to_linear(b as f32 / 255.0), + 1.0, + ] + } + StatuslineColor::Rgb(r, g, b) => { + [ + Self::srgb_to_linear(r as f32 / 255.0), + Self::srgb_to_linear(g as f32 / 255.0), + Self::srgb_to_linear(b as f32 / 255.0), + 1.0, + ] + } + } + }; + + for (section_idx, section) in statusline_sections.iter().enumerate() { + let section_start_x = cursor_x; + + // Calculate section width by counting characters + let mut section_char_count = 0usize; + for component in §ion.components { + section_char_count += component.text.chars().count(); + } + let section_width = section_char_count as f32 * self.cell_width; + + // Draw section background if it has a color (Indexed or Rgb) + let has_bg = matches!(section.bg, StatuslineColor::Indexed(_) | StatuslineColor::Rgb(_, _, _)); + if has_bg { + let section_bg_color = color_to_rgba(section.bg, &self.palette); + self.render_rect(section_start_x, statusline_y, section_width, statusline_height, section_bg_color); + } + + // Render components within this section + for component in §ion.components { + let component_fg = color_to_rgba(component.fg, &self.palette); + + for c in component.text.chars() { + if cursor_x + self.cell_width > width { + break; + } + + if c == ' ' { + cursor_x += self.cell_width; + continue; + } + + let glyph = self.rasterize_char(c); + if glyph.size[0] > 0.0 && glyph.size[1] > 0.0 { + let is_powerline_char = ('\u{E0B0}'..='\u{E0BF}').contains(&c); + + let glyph_x = (cursor_x + glyph.offset[0]).round(); + let glyph_y = if is_powerline_char { + statusline_y + } else { + let baseline_y = (text_y + self.cell_height * 0.8).round(); + (baseline_y - glyph.offset[1] - glyph.size[1]).round() + }; + + let left = Self::pixel_to_ndc_x(glyph_x, width); + let right = Self::pixel_to_ndc_x(glyph_x + glyph.size[0], width); + let top = Self::pixel_to_ndc_y(glyph_y, height); + let bottom = Self::pixel_to_ndc_y(glyph_y + glyph.size[1], height); + + let base_idx = self.glyph_vertices.len() as u32; + self.glyph_vertices.push(GlyphVertex { + position: [left, top], + uv: [glyph.uv[0], glyph.uv[1]], + color: component_fg, + bg_color: [0.0, 0.0, 0.0, 0.0], + }); + self.glyph_vertices.push(GlyphVertex { + position: [right, top], + uv: [glyph.uv[0] + glyph.uv[2], glyph.uv[1]], + color: component_fg, + bg_color: [0.0, 0.0, 0.0, 0.0], + }); + self.glyph_vertices.push(GlyphVertex { + position: [right, bottom], + uv: [glyph.uv[0] + glyph.uv[2], glyph.uv[1] + glyph.uv[3]], + color: component_fg, + bg_color: [0.0, 0.0, 0.0, 0.0], + }); + self.glyph_vertices.push(GlyphVertex { + position: [left, bottom], + uv: [glyph.uv[0], glyph.uv[1] + glyph.uv[3]], + color: component_fg, + bg_color: [0.0, 0.0, 0.0, 0.0], + }); + self.glyph_indices.extend_from_slice(&[ + base_idx, base_idx + 1, base_idx + 2, + base_idx, base_idx + 2, base_idx + 3, + ]); + } + + cursor_x += self.cell_width; + } + } + + // Draw powerline transition arrow at end of section (if section has a background) + if has_bg { + // Determine the next section's background color (or statusline bg if last section) + let next_bg = if section_idx + 1 < statusline_sections.len() { + color_to_rgba(statusline_sections[section_idx + 1].bg, &self.palette) + } else { + statusline_bg + }; + + // The arrow's foreground is this section's background + let arrow_fg = color_to_rgba(section.bg, &self.palette); + + // Render the powerline arrow (U+E0B0) + let arrow_char = '\u{E0B0}'; + let glyph = self.rasterize_char(arrow_char); + if glyph.size[0] > 0.0 && glyph.size[1] > 0.0 { + let glyph_x = (cursor_x + glyph.offset[0]).round(); + let glyph_y = statusline_y; // Powerline chars at top + + // Draw background rectangle for the arrow cell + self.render_rect(cursor_x, statusline_y, self.cell_width, statusline_height, next_bg); + + let left = Self::pixel_to_ndc_x(glyph_x, width); + let right = Self::pixel_to_ndc_x(glyph_x + glyph.size[0], width); + let top = Self::pixel_to_ndc_y(glyph_y, height); + let bottom = Self::pixel_to_ndc_y(glyph_y + glyph.size[1], height); + + let base_idx = self.glyph_vertices.len() as u32; + self.glyph_vertices.push(GlyphVertex { + position: [left, top], + uv: [glyph.uv[0], glyph.uv[1]], + color: arrow_fg, + bg_color: [0.0, 0.0, 0.0, 0.0], + }); + self.glyph_vertices.push(GlyphVertex { + position: [right, top], + uv: [glyph.uv[0] + glyph.uv[2], glyph.uv[1]], + color: arrow_fg, + bg_color: [0.0, 0.0, 0.0, 0.0], + }); + self.glyph_vertices.push(GlyphVertex { + position: [right, bottom], + uv: [glyph.uv[0] + glyph.uv[2], glyph.uv[1] + glyph.uv[3]], + color: arrow_fg, + bg_color: [0.0, 0.0, 0.0, 0.0], + }); + self.glyph_vertices.push(GlyphVertex { + position: [left, bottom], + uv: [glyph.uv[0], glyph.uv[1] + glyph.uv[3]], + color: arrow_fg, + bg_color: [0.0, 0.0, 0.0, 0.0], + }); + self.glyph_indices.extend_from_slice(&[ + base_idx, base_idx + 1, base_idx + 2, + base_idx, base_idx + 2, base_idx + 3, + ]); + } + + cursor_x += self.cell_width; + } + } + } + // ═══════════════════════════════════════════════════════════════════ // RENDER PANE BORDERS (only between adjacent panes) // ═══════════════════════════════════════════════════════════════════