powerline but better
This commit is contained in:
@@ -55,6 +55,9 @@ memmap2 = "0.9"
|
|||||||
# Fast byte searching
|
# Fast byte searching
|
||||||
memchr = "2"
|
memchr = "2"
|
||||||
|
|
||||||
|
# Base64 decoding for OSC statusline
|
||||||
|
base64 = "0.22"
|
||||||
|
|
||||||
# Image processing (Kitty graphics protocol)
|
# Image processing (Kitty graphics protocol)
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "gif"] }
|
image = { version = "0.25", default-features = false, features = ["png", "gif"] }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
|
|||||||
+49
-14
@@ -6,7 +6,7 @@
|
|||||||
use zterm::config::{Action, Config};
|
use zterm::config::{Action, Config};
|
||||||
use zterm::keyboard::{FunctionalKey, KeyEncoder, KeyEventType, KeyboardState, Modifiers};
|
use zterm::keyboard::{FunctionalKey, KeyEncoder, KeyEventType, KeyboardState, Modifiers};
|
||||||
use zterm::pty::Pty;
|
use zterm::pty::Pty;
|
||||||
use zterm::renderer::{EdgeGlow, PaneRenderInfo, Renderer, StatuslineComponent, StatuslineSection};
|
use zterm::renderer::{EdgeGlow, PaneRenderInfo, Renderer, StatuslineComponent, StatuslineContent, StatuslineSection};
|
||||||
use zterm::terminal::{Direction, Terminal, TerminalCommand, MouseTrackingMode};
|
use zterm::terminal::{Direction, Terminal, TerminalCommand, MouseTrackingMode};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -160,6 +160,10 @@ struct Pane {
|
|||||||
focus_animation_start: std::time::Instant,
|
focus_animation_start: std::time::Instant,
|
||||||
/// Whether this pane was focused before the current animation.
|
/// Whether this pane was focused before the current animation.
|
||||||
was_focused: bool,
|
was_focused: bool,
|
||||||
|
/// Custom statusline content set by applications (e.g., neovim).
|
||||||
|
/// Contains raw ANSI escape sequences for colors.
|
||||||
|
/// When Some, this overrides the default CWD/git statusline.
|
||||||
|
custom_statusline: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pane {
|
impl Pane {
|
||||||
@@ -193,6 +197,7 @@ impl Pane {
|
|||||||
last_scrollback_len: 0,
|
last_scrollback_len: 0,
|
||||||
focus_animation_start: std::time::Instant::now(),
|
focus_animation_start: std::time::Instant::now(),
|
||||||
was_focused: true, // New panes start as focused
|
was_focused: true, // New panes start as focused
|
||||||
|
custom_statusline: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1409,7 +1414,7 @@ impl App {
|
|||||||
let cell_width = renderer.cell_width;
|
let cell_width = renderer.cell_width;
|
||||||
let cell_height = renderer.cell_height;
|
let cell_height = renderer.cell_height;
|
||||||
let width = renderer.width as f32;
|
let width = renderer.width as f32;
|
||||||
let height = renderer.height as f32 - renderer.tab_bar_height();
|
let height = renderer.height as f32 - renderer.tab_bar_height() - renderer.statusline_height();
|
||||||
let border_width = 2.0; // Border width in pixels
|
let border_width = 2.0; // Border width in pixels
|
||||||
|
|
||||||
for tab in &mut self.tabs {
|
for tab in &mut self.tabs {
|
||||||
@@ -1458,19 +1463,29 @@ impl App {
|
|||||||
|
|
||||||
// Handle commands outside the borrow
|
// Handle commands outside the borrow
|
||||||
for cmd in commands {
|
for cmd in commands {
|
||||||
self.handle_terminal_command(cmd);
|
self.handle_terminal_command(pane_id, cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
processed
|
processed
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle a command from the terminal (triggered by OSC sequences).
|
/// Handle a command from the terminal (triggered by OSC sequences).
|
||||||
fn handle_terminal_command(&mut self, cmd: TerminalCommand) {
|
fn handle_terminal_command(&mut self, pane_id: PaneId, cmd: TerminalCommand) {
|
||||||
match cmd {
|
match cmd {
|
||||||
TerminalCommand::NavigatePane(direction) => {
|
TerminalCommand::NavigatePane(direction) => {
|
||||||
log::debug!("Terminal requested pane navigation: {:?}", direction);
|
log::debug!("Terminal requested pane navigation: {:?}", direction);
|
||||||
self.focus_pane(direction);
|
self.focus_pane(direction);
|
||||||
}
|
}
|
||||||
|
TerminalCommand::SetStatusline(statusline) => {
|
||||||
|
log::debug!("Pane {:?} set statusline: {:?}", pane_id, statusline.as_ref().map(|s| s.len()));
|
||||||
|
// Find the pane and set its custom statusline
|
||||||
|
for tab in &mut self.tabs {
|
||||||
|
if let Some(pane) = tab.get_pane_mut(pane_id) {
|
||||||
|
pane.custom_statusline = statusline;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2258,6 +2273,19 @@ impl ApplicationHandler<UserEvent> for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear custom statusline if the foreground process is no longer neovim/vim
|
||||||
|
// This handles the case where neovim exits but didn't send a clear command
|
||||||
|
if let Some(pane) = tab.panes.get_mut(&active_pane_id) {
|
||||||
|
if pane.custom_statusline.is_some() {
|
||||||
|
if let Some(proc_name) = pane.pty.foreground_process_name() {
|
||||||
|
let is_vim = proc_name == "nvim" || proc_name == "vim" || proc_name == "vi";
|
||||||
|
if !is_vim {
|
||||||
|
pane.custom_statusline = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build render info for all panes
|
// Build render info for all panes
|
||||||
let mut pane_render_data: Vec<(&Terminal, PaneRenderInfo, Option<(usize, usize, usize, usize)>)> = Vec::new();
|
let mut pane_render_data: Vec<(&Terminal, PaneRenderInfo, Option<(usize, usize, usize, usize)>)> = Vec::new();
|
||||||
|
|
||||||
@@ -2319,20 +2347,27 @@ impl ApplicationHandler<UserEvent> for App {
|
|||||||
pane.terminal.image_storage.has_animations()
|
pane.terminal.image_storage.has_animations()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get the cwd from the active pane for the statusline
|
// Get the statusline content for the active pane
|
||||||
let statusline_sections: Vec<StatuslineSection> = tab.panes.get(&active_pane_id)
|
// If the pane has a custom statusline (from neovim), use raw ANSI content
|
||||||
.and_then(|pane| pane.pty.foreground_cwd())
|
let statusline_content: StatuslineContent = tab.panes.get(&active_pane_id)
|
||||||
.map(|cwd| {
|
.map(|pane| {
|
||||||
let mut sections = vec![build_cwd_section(&cwd)];
|
if let Some(ref custom) = pane.custom_statusline {
|
||||||
// Add git section if in a git repository
|
// Use raw ANSI content directly - no parsing into sections
|
||||||
if let Some(git_section) = build_git_section(&cwd) {
|
StatuslineContent::Raw(custom.clone())
|
||||||
sections.push(git_section);
|
} else if let Some(cwd) = pane.pty.foreground_cwd() {
|
||||||
|
// Default: CWD and git sections
|
||||||
|
let mut sections = vec![build_cwd_section(&cwd)];
|
||||||
|
if let Some(git_section) = build_git_section(&cwd) {
|
||||||
|
sections.push(git_section);
|
||||||
|
}
|
||||||
|
StatuslineContent::Sections(sections)
|
||||||
|
} else {
|
||||||
|
StatuslineContent::Sections(Vec::new())
|
||||||
}
|
}
|
||||||
sections
|
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.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) {
|
match renderer.render_panes(&pane_render_data, num_tabs, active_tab_idx, &self.edge_glows, self.config.edge_glow_intensity, &statusline_content) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(wgpu::SurfaceError::Lost) => {
|
Err(wgpu::SurfaceError::Lost) => {
|
||||||
renderer.resize(renderer.width, renderer.height);
|
renderer.resize(renderer.width, renderer.height);
|
||||||
|
|||||||
+260
-3
@@ -159,6 +159,23 @@ impl StatuslineSection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Content to display in the statusline.
|
||||||
|
/// Either structured sections (for ZTerm's default CWD/git display) or raw ANSI
|
||||||
|
/// content (from neovim or other programs that provide their own statusline).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum StatuslineContent {
|
||||||
|
/// Structured sections with powerline-style transitions.
|
||||||
|
Sections(Vec<StatuslineSection>),
|
||||||
|
/// Raw ANSI-formatted string (rendered as-is without section styling).
|
||||||
|
Raw(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StatuslineContent {
|
||||||
|
fn default() -> Self {
|
||||||
|
StatuslineContent::Sections(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Edge glow animation state for visual feedback when navigation fails.
|
/// Edge glow animation state for visual feedback when navigation fails.
|
||||||
/// Creates an organic glow effect: a single light node appears at center,
|
/// Creates an organic glow effect: a single light node appears at center,
|
||||||
/// then splits into two that travel outward to the corners while fading.
|
/// then splits into two that travel outward to the corners while fading.
|
||||||
@@ -1663,6 +1680,231 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render raw ANSI-formatted content in the statusline.
|
||||||
|
/// This parses ANSI escape sequences inline and renders text with colors.
|
||||||
|
fn render_raw_ansi_statusline(
|
||||||
|
&mut self,
|
||||||
|
content: &str,
|
||||||
|
statusline_y: f32,
|
||||||
|
statusline_height: f32,
|
||||||
|
text_y: f32,
|
||||||
|
screen_width: f32,
|
||||||
|
screen_height: f32,
|
||||||
|
) {
|
||||||
|
let mut cursor_x = 4.0_f32; // Small left padding
|
||||||
|
|
||||||
|
// Current text attributes
|
||||||
|
let mut fg_color: [f32; 4] = {
|
||||||
|
let [r, g, b] = self.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,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
let default_fg = fg_color;
|
||||||
|
let mut bg_color: Option<[f32; 4]> = None;
|
||||||
|
|
||||||
|
let mut chars = content.chars().peekable();
|
||||||
|
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
if c == '\x1b' {
|
||||||
|
// Parse escape sequence
|
||||||
|
if chars.peek() == Some(&'[') {
|
||||||
|
chars.next(); // consume '['
|
||||||
|
|
||||||
|
// Collect the CSI parameters
|
||||||
|
let mut params = String::new();
|
||||||
|
while let Some(&ch) = chars.peek() {
|
||||||
|
if ch.is_ascii_digit() || ch == ';' {
|
||||||
|
params.push(chars.next().unwrap());
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the final character
|
||||||
|
if let Some(final_char) = chars.next() {
|
||||||
|
if final_char == 'm' {
|
||||||
|
// SGR sequence - parse color codes
|
||||||
|
let parts: Vec<&str> = params.split(';').collect();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < parts.len() {
|
||||||
|
let code: u32 = parts[i].parse().unwrap_or(0);
|
||||||
|
match code {
|
||||||
|
0 => {
|
||||||
|
// Reset
|
||||||
|
fg_color = default_fg;
|
||||||
|
bg_color = None;
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
// Bold - could brighten color
|
||||||
|
}
|
||||||
|
30..=37 => {
|
||||||
|
// Standard foreground colors
|
||||||
|
let idx = (code - 30) as usize;
|
||||||
|
let [r, g, b] = self.palette.colors[idx];
|
||||||
|
fg_color = [
|
||||||
|
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
38 => {
|
||||||
|
// Extended foreground color
|
||||||
|
if i + 1 < parts.len() {
|
||||||
|
let mode: u32 = parts[i + 1].parse().unwrap_or(0);
|
||||||
|
if mode == 5 && i + 2 < parts.len() {
|
||||||
|
// 256 color
|
||||||
|
let idx: usize = parts[i + 2].parse().unwrap_or(0);
|
||||||
|
let [r, g, b] = self.palette.colors[idx.min(255)];
|
||||||
|
fg_color = [
|
||||||
|
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,
|
||||||
|
];
|
||||||
|
i += 2;
|
||||||
|
} else if mode == 2 && i + 4 < parts.len() {
|
||||||
|
// RGB color
|
||||||
|
let r: u8 = parts[i + 2].parse().unwrap_or(0);
|
||||||
|
let g: u8 = parts[i + 3].parse().unwrap_or(0);
|
||||||
|
let b: u8 = parts[i + 4].parse().unwrap_or(0);
|
||||||
|
fg_color = [
|
||||||
|
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,
|
||||||
|
];
|
||||||
|
i += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
40..=47 => {
|
||||||
|
// Standard background colors
|
||||||
|
let idx = (code - 40) as usize;
|
||||||
|
let [r, g, b] = self.palette.colors[idx];
|
||||||
|
bg_color = Some([
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
48 => {
|
||||||
|
// Extended background color
|
||||||
|
if i + 1 < parts.len() {
|
||||||
|
let mode: u32 = parts[i + 1].parse().unwrap_or(0);
|
||||||
|
if mode == 5 && i + 2 < parts.len() {
|
||||||
|
// 256 color
|
||||||
|
let idx: usize = parts[i + 2].parse().unwrap_or(0);
|
||||||
|
let [r, g, b] = self.palette.colors[idx.min(255)];
|
||||||
|
bg_color = Some([
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
i += 2;
|
||||||
|
} else if mode == 2 && i + 4 < parts.len() {
|
||||||
|
// RGB color
|
||||||
|
let r: u8 = parts[i + 2].parse().unwrap_or(0);
|
||||||
|
let g: u8 = parts[i + 3].parse().unwrap_or(0);
|
||||||
|
let b: u8 = parts[i + 4].parse().unwrap_or(0);
|
||||||
|
bg_color = Some([
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
i += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
49 => {
|
||||||
|
// Default background
|
||||||
|
bg_color = None;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the character
|
||||||
|
if cursor_x + self.cell_width > screen_width {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw background if set
|
||||||
|
if let Some(bg) = bg_color {
|
||||||
|
self.render_rect(cursor_x, statusline_y, self.cell_width, statusline_height, bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// Powerline characters (U+E0B0-U+E0BF) need to start at y=0 to fill the statusline height
|
||||||
|
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, screen_width);
|
||||||
|
let right = Self::pixel_to_ndc_x(glyph_x + glyph.size[0], screen_width);
|
||||||
|
let top = Self::pixel_to_ndc_y(glyph_y, screen_height);
|
||||||
|
let bottom = Self::pixel_to_ndc_y(glyph_y + glyph.size[1], screen_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: fg_color,
|
||||||
|
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: fg_color,
|
||||||
|
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: fg_color,
|
||||||
|
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: fg_color,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the Y offset where the terminal content starts.
|
/// Returns the Y offset where the terminal content starts.
|
||||||
/// Accounts for both the tab bar and the statusline.
|
/// Accounts for both the tab bar and the statusline.
|
||||||
pub fn terminal_y_offset(&self) -> f32 {
|
pub fn terminal_y_offset(&self) -> f32 {
|
||||||
@@ -4601,7 +4843,7 @@ impl Renderer {
|
|||||||
/// - `active_tab`: Index of the active tab
|
/// - `active_tab`: Index of the active tab
|
||||||
/// - `edge_glows`: Active edge glow animations for visual feedback
|
/// - `edge_glows`: Active edge glow animations for visual feedback
|
||||||
/// - `edge_glow_intensity`: Intensity of edge glow effect (0.0 = disabled, 1.0 = full)
|
/// - `edge_glow_intensity`: Intensity of edge glow effect (0.0 = disabled, 1.0 = full)
|
||||||
/// - `statusline_sections`: Sections to render in the statusline
|
/// - `statusline_content`: Content to render in the statusline
|
||||||
pub fn render_panes(
|
pub fn render_panes(
|
||||||
&mut self,
|
&mut self,
|
||||||
panes: &[(&Terminal, PaneRenderInfo, Option<(usize, usize, usize, usize)>)],
|
panes: &[(&Terminal, PaneRenderInfo, Option<(usize, usize, usize, usize)>)],
|
||||||
@@ -4609,7 +4851,7 @@ impl Renderer {
|
|||||||
active_tab: usize,
|
active_tab: usize,
|
||||||
edge_glows: &[EdgeGlow],
|
edge_glows: &[EdgeGlow],
|
||||||
edge_glow_intensity: f32,
|
edge_glow_intensity: f32,
|
||||||
statusline_sections: &[StatuslineSection],
|
statusline_content: &StatuslineContent,
|
||||||
) -> Result<(), wgpu::SurfaceError> {
|
) -> Result<(), wgpu::SurfaceError> {
|
||||||
// Sync palette from first terminal
|
// Sync palette from first terminal
|
||||||
if let Some((terminal, _, _)) = panes.first() {
|
if let Some((terminal, _, _)) = panes.first() {
|
||||||
@@ -4809,7 +5051,20 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (section_idx, section) in statusline_sections.iter().enumerate() {
|
match statusline_content {
|
||||||
|
StatuslineContent::Raw(ansi_content) => {
|
||||||
|
// Render raw ANSI content directly without section styling
|
||||||
|
self.render_raw_ansi_statusline(
|
||||||
|
ansi_content,
|
||||||
|
statusline_y,
|
||||||
|
statusline_height,
|
||||||
|
text_y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
StatuslineContent::Sections(statusline_sections) => {
|
||||||
|
for (section_idx, section) in statusline_sections.iter().enumerate() {
|
||||||
let section_start_x = cursor_x;
|
let section_start_x = cursor_x;
|
||||||
|
|
||||||
// Calculate section width by counting characters
|
// Calculate section width by counting characters
|
||||||
@@ -4953,6 +5208,8 @@ impl Renderer {
|
|||||||
cursor_x += self.cell_width;
|
cursor_x += self.cell_width;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ pub enum TerminalCommand {
|
|||||||
/// Navigate to a neighboring pane in the given direction.
|
/// Navigate to a neighboring pane in the given direction.
|
||||||
/// Triggered by OSC 51;navigate;<direction> ST
|
/// Triggered by OSC 51;navigate;<direction> ST
|
||||||
NavigatePane(Direction),
|
NavigatePane(Direction),
|
||||||
|
/// Set custom statusline content for this pane.
|
||||||
|
/// Triggered by OSC 51;statusline;<content> ST
|
||||||
|
/// Empty content clears the statusline (restores default).
|
||||||
|
SetStatusline(Option<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Direction for pane navigation.
|
/// Direction for pane navigation.
|
||||||
@@ -1382,6 +1386,7 @@ impl Handler for Terminal {
|
|||||||
// Format: OSC 51;command;args ST
|
// Format: OSC 51;command;args ST
|
||||||
// Currently supported:
|
// Currently supported:
|
||||||
// OSC 51;navigate;up/down/left/right ST - Navigate to neighboring pane
|
// OSC 51;navigate;up/down/left/right ST - Navigate to neighboring pane
|
||||||
|
// OSC 51;statusline;<content> ST - Set custom statusline (empty to clear)
|
||||||
51 => {
|
51 => {
|
||||||
if parts.len() >= 2 {
|
if parts.len() >= 2 {
|
||||||
if let Ok(command) = std::str::from_utf8(parts[1]) {
|
if let Ok(command) = std::str::from_utf8(parts[1]) {
|
||||||
@@ -1403,6 +1408,37 @@ impl Handler for Terminal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"statusline" => {
|
||||||
|
// OSC 51;statusline;<content> ST
|
||||||
|
// If content is empty or missing, clear the statusline
|
||||||
|
// Content may be base64-encoded (prefixed with "b64:") to avoid
|
||||||
|
// escape sequence interpretation issues in the terminal
|
||||||
|
let prefix = b"51;statusline;";
|
||||||
|
let raw_content = if data.len() > prefix.len() && data.starts_with(prefix) {
|
||||||
|
std::str::from_utf8(&data[prefix.len()..]).ok().map(|s| s.to_string())
|
||||||
|
} else if parts.len() >= 3 {
|
||||||
|
std::str::from_utf8(parts[2]).ok().map(|s| s.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decode base64 if prefixed with "b64:"
|
||||||
|
let content = raw_content.and_then(|s| {
|
||||||
|
if let Some(encoded) = s.strip_prefix("b64:") {
|
||||||
|
use base64::Engine;
|
||||||
|
base64::engine::general_purpose::STANDARD
|
||||||
|
.decode(encoded)
|
||||||
|
.ok()
|
||||||
|
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||||
|
} else {
|
||||||
|
Some(s)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let statusline = content.filter(|s| !s.is_empty());
|
||||||
|
log::info!("OSC 51: Set statusline: {:?}", statusline.as_ref().map(|s| format!("{} bytes", s.len())));
|
||||||
|
self.command_queue.push(TerminalCommand::SetStatusline(statusline));
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::debug!("OSC 51: Unknown command '{}'", command);
|
log::debug!("OSC 51: Unknown command '{}'", command);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user