powerline?
This commit is contained in:
+316
-2
@@ -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<GitStatus> {
|
||||
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<char> = 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<StatuslineSection> {
|
||||
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<UserEvent> 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<StatuslineSection> = 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);
|
||||
|
||||
+12
@@ -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<String> {
|
||||
let pgid = self.foreground_pgid()?;
|
||||
|
||||
// Read the cwd symlink from /proc/<pid>/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 {
|
||||
|
||||
+361
-9
@@ -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<String>) -> 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<String>) -> 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<StatuslineComponent>,
|
||||
/// 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<StatuslineComponent>) -> 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)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user