~animations~
This commit is contained in:
@@ -367,6 +367,11 @@ pub struct Config {
|
|||||||
pub inactive_pane_fade_ms: u64,
|
pub inactive_pane_fade_ms: u64,
|
||||||
/// Dim factor for inactive panes (0.0 = fully dimmed/black, 1.0 = no dimming).
|
/// Dim factor for inactive panes (0.0 = fully dimmed/black, 1.0 = no dimming).
|
||||||
pub inactive_pane_dim: f32,
|
pub inactive_pane_dim: f32,
|
||||||
|
/// Process names that should receive pane navigation keys instead of zterm handling them.
|
||||||
|
/// When the foreground process matches one of these names, Alt+Arrow keys are passed
|
||||||
|
/// to the application (e.g., for Neovim buffer navigation) instead of switching panes.
|
||||||
|
/// Example: ["nvim", "vim", "helix"]
|
||||||
|
pub pass_keys_to_programs: Vec<String>,
|
||||||
/// Keybindings.
|
/// Keybindings.
|
||||||
pub keybindings: Keybindings,
|
pub keybindings: Keybindings,
|
||||||
}
|
}
|
||||||
@@ -380,6 +385,7 @@ impl Default for Config {
|
|||||||
scrollback_lines: 50_000,
|
scrollback_lines: 50_000,
|
||||||
inactive_pane_fade_ms: 150,
|
inactive_pane_fade_ms: 150,
|
||||||
inactive_pane_dim: 0.6,
|
inactive_pane_dim: 0.6,
|
||||||
|
pass_keys_to_programs: vec!["nvim".to_string(), "vim".to_string()],
|
||||||
keybindings: Keybindings::default(),
|
keybindings: Keybindings::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+101
-21
@@ -6,8 +6,8 @@
|
|||||||
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::{PaneRenderInfo, Renderer};
|
use zterm::renderer::{EdgeGlow, PaneRenderInfo, Renderer};
|
||||||
use zterm::terminal::{Terminal, MouseTrackingMode};
|
use zterm::terminal::{Direction, Terminal, TerminalCommand, MouseTrackingMode};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
@@ -199,6 +199,19 @@ impl Pane {
|
|||||||
self.pty.child_exited()
|
self.pty.child_exited()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if the foreground process matches any of the given program names.
|
||||||
|
/// Used for pass-through keybindings (e.g., passing Alt+Arrow to Neovim).
|
||||||
|
fn foreground_matches(&self, programs: &[String]) -> bool {
|
||||||
|
if programs.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Some(fg_name) = self.pty.foreground_process_name() {
|
||||||
|
programs.iter().any(|p| p == &fg_name)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Calculate the current dim factor based on animation progress.
|
/// Calculate the current dim factor based on animation progress.
|
||||||
/// Returns a value between `inactive_dim` (for unfocused) and 1.0 (for focused).
|
/// Returns a value between `inactive_dim` (for unfocused) and 1.0 (for focused).
|
||||||
fn calculate_dim_factor(&mut self, is_focused: bool, fade_duration_ms: u64, inactive_dim: f32) -> f32 {
|
fn calculate_dim_factor(&mut self, is_focused: bool, fade_duration_ms: u64, inactive_dim: f32) -> f32 {
|
||||||
@@ -508,15 +521,6 @@ impl SplitNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Direction for pane navigation.
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
enum Direction {
|
|
||||||
Up,
|
|
||||||
Down,
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unique identifier for a tab.
|
/// Unique identifier for a tab.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
struct TabId(u64);
|
struct TabId(u64);
|
||||||
@@ -812,6 +816,8 @@ struct App {
|
|||||||
last_frame_log: std::time::Instant,
|
last_frame_log: std::time::Instant,
|
||||||
/// Whether window should be created on next opportunity.
|
/// Whether window should be created on next opportunity.
|
||||||
should_create_window: bool,
|
should_create_window: bool,
|
||||||
|
/// Edge glow animation state (for when navigation fails).
|
||||||
|
edge_glow: Option<EdgeGlow>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const PTY_KEY: usize = 1;
|
const PTY_KEY: usize = 1;
|
||||||
@@ -842,6 +848,7 @@ impl App {
|
|||||||
frame_count: 0,
|
frame_count: 0,
|
||||||
last_frame_log: std::time::Instant::now(),
|
last_frame_log: std::time::Instant::now(),
|
||||||
should_create_window: false,
|
should_create_window: false,
|
||||||
|
edge_glow: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1045,7 +1052,10 @@ impl App {
|
|||||||
/// Process PTY data for a specific pane.
|
/// Process PTY data for a specific pane.
|
||||||
/// Returns true if any data was processed.
|
/// Returns true if any data was processed.
|
||||||
fn poll_pane(&mut self, pane_id: PaneId) -> bool {
|
fn poll_pane(&mut self, pane_id: PaneId) -> bool {
|
||||||
// Find the pane across all tabs
|
// Find the pane across all tabs and process data
|
||||||
|
let mut processed = false;
|
||||||
|
let mut commands = Vec::new();
|
||||||
|
|
||||||
for tab in &mut self.tabs {
|
for tab in &mut self.tabs {
|
||||||
if let Some(pane) = tab.get_pane_mut(pane_id) {
|
if let Some(pane) = tab.get_pane_mut(pane_id) {
|
||||||
// Take all pending data atomically
|
// Take all pending data atomically
|
||||||
@@ -1066,10 +1076,29 @@ impl App {
|
|||||||
len);
|
len);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
// Collect any commands from the terminal
|
||||||
|
commands = pane.terminal.take_commands();
|
||||||
|
processed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle commands outside the borrow
|
||||||
|
for cmd in commands {
|
||||||
|
self.handle_terminal_command(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
processed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle a command from the terminal (triggered by OSC sequences).
|
||||||
|
fn handle_terminal_command(&mut self, cmd: TerminalCommand) {
|
||||||
|
match cmd {
|
||||||
|
TerminalCommand::NavigatePane(direction) => {
|
||||||
|
log::debug!("Terminal requested pane navigation: {:?}", direction);
|
||||||
|
self.focus_pane(direction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send bytes to the active tab's PTY.
|
/// Send bytes to the active tab's PTY.
|
||||||
@@ -1259,16 +1288,16 @@ impl App {
|
|||||||
self.split_pane(false);
|
self.split_pane(false);
|
||||||
}
|
}
|
||||||
Action::FocusPaneUp => {
|
Action::FocusPaneUp => {
|
||||||
self.focus_pane(Direction::Up);
|
self.focus_pane_or_pass_key(Direction::Up, b'A');
|
||||||
}
|
}
|
||||||
Action::FocusPaneDown => {
|
Action::FocusPaneDown => {
|
||||||
self.focus_pane(Direction::Down);
|
self.focus_pane_or_pass_key(Direction::Down, b'B');
|
||||||
}
|
}
|
||||||
Action::FocusPaneLeft => {
|
Action::FocusPaneLeft => {
|
||||||
self.focus_pane(Direction::Left);
|
self.focus_pane_or_pass_key(Direction::Left, b'D');
|
||||||
}
|
}
|
||||||
Action::FocusPaneRight => {
|
Action::FocusPaneRight => {
|
||||||
self.focus_pane(Direction::Right);
|
self.focus_pane_or_pass_key(Direction::Right, b'C');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1314,14 +1343,48 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Focus neighbor pane or pass keys through to applications like Neovim.
|
||||||
|
/// If the foreground process matches `pass_keys_to_programs`, send the Alt+Arrow
|
||||||
|
/// escape sequence to the PTY. Otherwise, focus the neighboring pane.
|
||||||
|
fn focus_pane_or_pass_key(&mut self, direction: Direction, arrow_letter: u8) {
|
||||||
|
// Check if we should pass keys to the foreground process
|
||||||
|
let should_pass = if let Some(tab) = self.tabs.get(self.active_tab) {
|
||||||
|
if let Some(pane) = tab.active_pane() {
|
||||||
|
pane.foreground_matches(&self.config.pass_keys_to_programs)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_pass {
|
||||||
|
// Send Alt+Arrow escape sequence: \x1b[1;3X where X is A/B/C/D
|
||||||
|
let escape_seq = [0x1b, b'[', b'1', b';', b'3', arrow_letter];
|
||||||
|
self.write_to_pty(&escape_seq);
|
||||||
|
} else {
|
||||||
|
self.focus_pane(direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn focus_pane(&mut self, direction: Direction) {
|
fn focus_pane(&mut self, direction: Direction) {
|
||||||
if let Some(tab) = self.tabs.get_mut(self.active_tab) {
|
let navigated = if let Some(tab) = self.tabs.get_mut(self.active_tab) {
|
||||||
|
let old_pane = tab.active_pane;
|
||||||
tab.focus_neighbor(direction);
|
tab.focus_neighbor(direction);
|
||||||
|
tab.active_pane != old_pane
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if !navigated {
|
||||||
|
// No neighbor in that direction - trigger edge glow animation
|
||||||
|
self.edge_glow = Some(EdgeGlow::new(direction));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(window) = &self.window {
|
if let Some(window) = &self.window {
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn close_active_pane(&mut self) {
|
fn close_active_pane(&mut self) {
|
||||||
let should_close_tab = if let Some(tab) = self.tabs.get_mut(self.active_tab) {
|
let should_close_tab = if let Some(tab) = self.tabs.get_mut(self.active_tab) {
|
||||||
@@ -1851,7 +1914,11 @@ impl ApplicationHandler<UserEvent> for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match renderer.render_panes(&pane_render_data, num_tabs, active_tab_idx) {
|
// Handle edge glow animation
|
||||||
|
let edge_glow_ref = self.edge_glow.as_ref();
|
||||||
|
let glow_in_progress = edge_glow_ref.map(|g| !g.is_finished()).unwrap_or(false);
|
||||||
|
|
||||||
|
match renderer.render_panes(&pane_render_data, num_tabs, active_tab_idx, edge_glow_ref) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(wgpu::SurfaceError::Lost) => {
|
Err(wgpu::SurfaceError::Lost) => {
|
||||||
renderer.resize(renderer.width, renderer.height);
|
renderer.resize(renderer.width, renderer.height);
|
||||||
@@ -1864,8 +1931,21 @@ impl ApplicationHandler<UserEvent> for App {
|
|||||||
log::error!("Render error: {:?}", e);
|
log::error!("Render error: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Request redraw if edge glow is animating
|
||||||
|
if glow_in_progress {
|
||||||
|
if let Some(window) = &self.window {
|
||||||
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up finished edge glow animation
|
||||||
|
if self.edge_glow.as_ref().map(|g| g.is_finished()).unwrap_or(false) {
|
||||||
|
self.edge_glow = None;
|
||||||
|
}
|
||||||
|
|
||||||
let render_time = render_start.elapsed();
|
let render_time = render_start.elapsed();
|
||||||
let frame_time = frame_start.elapsed();
|
let frame_time = frame_start.elapsed();
|
||||||
|
|
||||||
|
|||||||
+25
@@ -188,6 +188,31 @@ impl Pty {
|
|||||||
// If it returns -1, there was an error (child might have already been reaped)
|
// If it returns -1, there was an error (child might have already been reaped)
|
||||||
result != 0
|
result != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the foreground process group ID of this PTY.
|
||||||
|
/// Returns None if the query fails.
|
||||||
|
pub fn foreground_pgid(&self) -> Option<i32> {
|
||||||
|
let fd = self.master.as_raw_fd();
|
||||||
|
let pgid = unsafe { libc::tcgetpgrp(fd) };
|
||||||
|
if pgid > 0 {
|
||||||
|
Some(pgid)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the name of the foreground process running in this PTY.
|
||||||
|
/// Returns the process name (e.g., "nvim", "zsh") or None if unavailable.
|
||||||
|
pub fn foreground_process_name(&self) -> Option<String> {
|
||||||
|
let pgid = self.foreground_pgid()?;
|
||||||
|
|
||||||
|
// Read the command line from /proc/<pid>/comm
|
||||||
|
// (comm gives just the process name, cmdline gives full command)
|
||||||
|
let comm_path = format!("/proc/{}/comm", pgid);
|
||||||
|
std::fs::read_to_string(&comm_path)
|
||||||
|
.ok()
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRawFd for Pty {
|
impl AsRawFd for Pty {
|
||||||
|
|||||||
+230
-1
@@ -2,7 +2,7 @@
|
|||||||
//! Uses rustybuzz (HarfBuzz port) for text shaping to support font features.
|
//! Uses rustybuzz (HarfBuzz port) for text shaping to support font features.
|
||||||
|
|
||||||
use crate::config::TabBarPosition;
|
use crate::config::TabBarPosition;
|
||||||
use crate::terminal::{Color, ColorPalette, CursorShape, Terminal};
|
use crate::terminal::{Color, ColorPalette, CursorShape, Direction, Terminal};
|
||||||
use fontdue::Font as FontdueFont;
|
use fontdue::Font as FontdueFont;
|
||||||
use rustybuzz::UnicodeBuffer;
|
use rustybuzz::UnicodeBuffer;
|
||||||
use ttf_parser::Tag;
|
use ttf_parser::Tag;
|
||||||
@@ -37,6 +37,43 @@ pub struct PaneRenderInfo {
|
|||||||
pub dim_factor: f32,
|
pub dim_factor: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
/// Animation logic is handled in the shader (shader.wgsl).
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct EdgeGlow {
|
||||||
|
/// Which edge to glow (based on the direction the user tried to navigate).
|
||||||
|
pub direction: Direction,
|
||||||
|
/// When the animation started.
|
||||||
|
pub start_time: std::time::Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EdgeGlow {
|
||||||
|
/// Duration of the glow animation in milliseconds.
|
||||||
|
pub const DURATION_MS: u64 = 500;
|
||||||
|
|
||||||
|
/// Create a new edge glow animation.
|
||||||
|
pub fn new(direction: Direction) -> Self {
|
||||||
|
Self {
|
||||||
|
direction,
|
||||||
|
start_time: std::time::Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current animation progress (0.0 to 1.0).
|
||||||
|
pub fn progress(&self) -> f32 {
|
||||||
|
let elapsed = self.start_time.elapsed().as_millis() as f32;
|
||||||
|
let duration = Self::DURATION_MS as f32;
|
||||||
|
(elapsed / duration).min(1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the animation has completed.
|
||||||
|
pub fn is_finished(&self) -> bool {
|
||||||
|
self.progress() >= 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Size of the glyph atlas texture.
|
/// Size of the glyph atlas texture.
|
||||||
const ATLAS_SIZE: u32 = 1024;
|
const ATLAS_SIZE: u32 = 1024;
|
||||||
|
|
||||||
@@ -96,6 +133,25 @@ impl GlyphVertex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GPU-compatible edge glow uniform data.
|
||||||
|
/// Must match the layout in shader.wgsl exactly.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
struct EdgeGlowUniforms {
|
||||||
|
screen_width: f32,
|
||||||
|
screen_height: f32,
|
||||||
|
terminal_y_offset: f32,
|
||||||
|
direction: u32,
|
||||||
|
progress: f32,
|
||||||
|
color_r: f32,
|
||||||
|
color_g: f32,
|
||||||
|
color_b: f32,
|
||||||
|
enabled: u32,
|
||||||
|
_padding1: u32,
|
||||||
|
_padding2: u32,
|
||||||
|
_padding3: u32,
|
||||||
|
}
|
||||||
|
|
||||||
/// The terminal renderer.
|
/// The terminal renderer.
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
surface: wgpu::Surface<'static>,
|
surface: wgpu::Surface<'static>,
|
||||||
@@ -107,6 +163,11 @@ pub struct Renderer {
|
|||||||
glyph_pipeline: wgpu::RenderPipeline,
|
glyph_pipeline: wgpu::RenderPipeline,
|
||||||
glyph_bind_group: wgpu::BindGroup,
|
glyph_bind_group: wgpu::BindGroup,
|
||||||
|
|
||||||
|
// Edge glow rendering pipeline
|
||||||
|
edge_glow_pipeline: wgpu::RenderPipeline,
|
||||||
|
edge_glow_bind_group: wgpu::BindGroup,
|
||||||
|
edge_glow_uniform_buffer: wgpu::Buffer,
|
||||||
|
|
||||||
// Atlas texture
|
// Atlas texture
|
||||||
atlas_texture: wgpu::Texture,
|
atlas_texture: wgpu::Texture,
|
||||||
atlas_data: Vec<u8>,
|
atlas_data: Vec<u8>,
|
||||||
@@ -873,6 +934,96 @@ impl Renderer {
|
|||||||
cache: None,
|
cache: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// EDGE GLOW PIPELINE SETUP
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// Create edge glow shader
|
||||||
|
let edge_glow_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some("Edge Glow Shader"),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create uniform buffer for edge glow parameters
|
||||||
|
let edge_glow_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("Edge Glow Uniform Buffer"),
|
||||||
|
size: std::mem::size_of::<EdgeGlowUniforms>() as u64,
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create bind group layout for edge glow
|
||||||
|
let edge_glow_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
label: Some("Edge Glow Bind Group Layout"),
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create bind group for edge glow
|
||||||
|
let edge_glow_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("Edge Glow Bind Group"),
|
||||||
|
layout: &edge_glow_bind_group_layout,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: edge_glow_uniform_buffer.as_entire_binding(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create pipeline layout for edge glow
|
||||||
|
let edge_glow_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("Edge Glow Pipeline Layout"),
|
||||||
|
bind_group_layouts: &[&edge_glow_bind_group_layout],
|
||||||
|
push_constant_ranges: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create edge glow render pipeline
|
||||||
|
let edge_glow_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("Edge Glow Pipeline"),
|
||||||
|
layout: Some(&edge_glow_pipeline_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &edge_glow_shader,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[], // Fullscreen triangle, no vertex buffer needed
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &edge_glow_shader,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: surface_config.format,
|
||||||
|
// Premultiplied alpha blending for proper glow compositing
|
||||||
|
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||||
|
strip_index_format: None,
|
||||||
|
front_face: wgpu::FrontFace::Ccw,
|
||||||
|
cull_mode: None,
|
||||||
|
polygon_mode: wgpu::PolygonMode::Fill,
|
||||||
|
unclipped_depth: false,
|
||||||
|
conservative: false,
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: wgpu::MultisampleState::default(),
|
||||||
|
multiview: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
|
||||||
// Create initial buffers with some capacity
|
// Create initial buffers with some capacity
|
||||||
let initial_vertex_capacity = 4096;
|
let initial_vertex_capacity = 4096;
|
||||||
let initial_index_capacity = 6144;
|
let initial_index_capacity = 6144;
|
||||||
@@ -898,6 +1049,9 @@ impl Renderer {
|
|||||||
surface_config,
|
surface_config,
|
||||||
glyph_pipeline,
|
glyph_pipeline,
|
||||||
glyph_bind_group,
|
glyph_bind_group,
|
||||||
|
edge_glow_pipeline,
|
||||||
|
edge_glow_bind_group,
|
||||||
|
edge_glow_uniform_buffer,
|
||||||
atlas_texture,
|
atlas_texture,
|
||||||
atlas_data: vec![0u8; (ATLAS_SIZE * ATLAS_SIZE) as usize],
|
atlas_data: vec![0u8; (ATLAS_SIZE * ATLAS_SIZE) as usize],
|
||||||
atlas_dirty: false,
|
atlas_dirty: false,
|
||||||
@@ -3271,17 +3425,51 @@ impl Renderer {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prepare edge glow uniform data for shader-based rendering.
|
||||||
|
/// Returns the uniform data to be uploaded to the GPU.
|
||||||
|
fn prepare_edge_glow_uniforms(&self, glow: &EdgeGlow, terminal_y_offset: f32) -> EdgeGlowUniforms {
|
||||||
|
// Use the same color as the active pane border (palette color 4 - typically blue)
|
||||||
|
let [r, g, b] = self.palette.colors[4];
|
||||||
|
let color_r = Self::srgb_to_linear(r as f32 / 255.0);
|
||||||
|
let color_g = Self::srgb_to_linear(g as f32 / 255.0);
|
||||||
|
let color_b = Self::srgb_to_linear(b as f32 / 255.0);
|
||||||
|
|
||||||
|
let direction = match glow.direction {
|
||||||
|
Direction::Up => 0,
|
||||||
|
Direction::Down => 1,
|
||||||
|
Direction::Left => 2,
|
||||||
|
Direction::Right => 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
EdgeGlowUniforms {
|
||||||
|
screen_width: self.width as f32,
|
||||||
|
screen_height: self.height as f32,
|
||||||
|
terminal_y_offset,
|
||||||
|
direction,
|
||||||
|
progress: glow.progress(),
|
||||||
|
color_r,
|
||||||
|
color_g,
|
||||||
|
color_b,
|
||||||
|
enabled: 1,
|
||||||
|
_padding1: 0,
|
||||||
|
_padding2: 0,
|
||||||
|
_padding3: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Render multiple panes with borders.
|
/// Render multiple panes with borders.
|
||||||
///
|
///
|
||||||
/// Arguments:
|
/// Arguments:
|
||||||
/// - `panes`: List of (terminal, pane_info, selection) tuples
|
/// - `panes`: List of (terminal, pane_info, selection) tuples
|
||||||
/// - `num_tabs`: Number of tabs for the tab bar
|
/// - `num_tabs`: Number of tabs for the tab bar
|
||||||
/// - `active_tab`: Index of the active tab
|
/// - `active_tab`: Index of the active tab
|
||||||
|
/// - `edge_glow`: Optional edge glow animation for visual feedback
|
||||||
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)>)],
|
||||||
num_tabs: usize,
|
num_tabs: usize,
|
||||||
active_tab: usize,
|
active_tab: usize,
|
||||||
|
edge_glow: Option<&EdgeGlow>,
|
||||||
) -> 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() {
|
||||||
@@ -3560,6 +3748,15 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// PREPARE EDGE GLOW UNIFORMS (if navigation failed)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
let edge_glow_uniforms = if let Some(glow) = edge_glow {
|
||||||
|
Some(self.prepare_edge_glow_uniforms(glow, terminal_y_offset))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
// SUBMIT TO GPU
|
// SUBMIT TO GPU
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
@@ -3698,6 +3895,38 @@ impl Renderer {
|
|||||||
render_pass.draw_indexed(0..total_index_count as u32, 0, 0..1);
|
render_pass.draw_indexed(0..total_index_count as u32, 0, 0..1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// EDGE GLOW PASS (shader-based, after main rendering)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
if let Some(uniforms) = edge_glow_uniforms {
|
||||||
|
// Upload uniforms
|
||||||
|
self.queue.write_buffer(
|
||||||
|
&self.edge_glow_uniform_buffer,
|
||||||
|
0,
|
||||||
|
bytemuck::cast_slice(&[uniforms]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Second render pass for edge glow (load existing content)
|
||||||
|
let mut glow_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: Some("Edge Glow Pass"),
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: &view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Load, // Preserve existing content
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
occlusion_query_set: None,
|
||||||
|
timestamp_writes: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
glow_pass.set_pipeline(&self.edge_glow_pipeline);
|
||||||
|
glow_pass.set_bind_group(0, &self.edge_glow_bind_group, &[]);
|
||||||
|
glow_pass.draw(0..3, 0..1); // Fullscreen triangle
|
||||||
|
}
|
||||||
|
|
||||||
self.queue.submit(std::iter::once(encoder.finish()));
|
self.queue.submit(std::iter::once(encoder.finish()));
|
||||||
output.present();
|
output.present();
|
||||||
|
|
||||||
|
|||||||
+186
-10
@@ -1,26 +1,202 @@
|
|||||||
// Vertex shader
|
// Edge Glow Shader
|
||||||
|
// Renders a soft glow effect at terminal edges for failed pane navigation feedback.
|
||||||
|
// The glow appears as a light node at center that splits into two and travels to corners.
|
||||||
|
|
||||||
struct VertexInput {
|
// Uniform buffer with glow parameters
|
||||||
@location(0) position: vec2<f32>,
|
struct EdgeGlowParams {
|
||||||
@location(1) color: vec4<f32>,
|
// Screen dimensions in pixels
|
||||||
|
screen_width: f32,
|
||||||
|
screen_height: f32,
|
||||||
|
// Terminal area offset (for tab bar)
|
||||||
|
terminal_y_offset: f32,
|
||||||
|
// Direction: 0=Up, 1=Down, 2=Left, 3=Right
|
||||||
|
direction: u32,
|
||||||
|
// Animation progress (0.0 to 1.0)
|
||||||
|
progress: f32,
|
||||||
|
// Glow color (linear RGB) - stored as separate floats to avoid vec3 alignment issues
|
||||||
|
color_r: f32,
|
||||||
|
color_g: f32,
|
||||||
|
color_b: f32,
|
||||||
|
// Whether glow is enabled (1 = yes, 0 = no)
|
||||||
|
enabled: u32,
|
||||||
|
// Padding to align to 16 bytes
|
||||||
|
_padding1: u32,
|
||||||
|
_padding2: u32,
|
||||||
|
_padding3: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> params: EdgeGlowParams;
|
||||||
|
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
@builtin(position) clip_position: vec4<f32>,
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
@location(0) color: vec4<f32>,
|
@location(0) uv: vec2<f32>, // 0-1 normalized screen coordinates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fullscreen triangle vertex shader
|
||||||
|
// Uses vertex_index 0,1,2 to create a triangle that covers the screen
|
||||||
@vertex
|
@vertex
|
||||||
fn vs_main(in: VertexInput) -> VertexOutput {
|
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||||
var out: VertexOutput;
|
var out: VertexOutput;
|
||||||
out.clip_position = vec4<f32>(in.position, 0.0, 1.0);
|
|
||||||
out.color = in.color;
|
// Generate fullscreen triangle vertices
|
||||||
|
// This creates a triangle that covers [-1,1] in clip space
|
||||||
|
let x = f32(i32(vertex_index) - 1);
|
||||||
|
let y = f32(i32(vertex_index & 1u) * 2 - 1);
|
||||||
|
|
||||||
|
// Positions for a fullscreen triangle
|
||||||
|
var pos: vec2<f32>;
|
||||||
|
switch vertex_index {
|
||||||
|
case 0u: { pos = vec2<f32>(-1.0, -1.0); }
|
||||||
|
case 1u: { pos = vec2<f32>(3.0, -1.0); }
|
||||||
|
case 2u: { pos = vec2<f32>(-1.0, 3.0); }
|
||||||
|
default: { pos = vec2<f32>(0.0, 0.0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
out.clip_position = vec4<f32>(pos, 0.0, 1.0);
|
||||||
|
// Convert to 0-1 UV (flip Y since clip space Y is up, pixel Y is down)
|
||||||
|
out.uv = vec2<f32>((pos.x + 1.0) * 0.5, (1.0 - pos.y) * 0.5);
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fragment shader
|
// Constants
|
||||||
|
const PI: f32 = 3.14159265359;
|
||||||
|
const PHASE1_END: f32 = 0.15; // Phase 1 ends at 15% progress
|
||||||
|
const GLOW_RADIUS: f32 = 90.0; // Base radius of glow
|
||||||
|
const GLOW_ASPECT: f32 = 2.0; // Stretch factor along edge (ellipse)
|
||||||
|
|
||||||
|
// Smooth gaussian-like falloff
|
||||||
|
fn glow_falloff(dist: f32, radius: f32) -> f32 {
|
||||||
|
let normalized = dist / radius;
|
||||||
|
if normalized > 1.0 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
// Smooth falloff: (1 - x^2)^3 gives nice soft edges
|
||||||
|
let t = 1.0 - normalized * normalized;
|
||||||
|
return t * t * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ease-out cubic
|
||||||
|
fn ease_out_cubic(t: f32) -> f32 {
|
||||||
|
let t1 = 1.0 - t;
|
||||||
|
return 1.0 - t1 * t1 * t1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate distance from point to glow center, accounting for ellipse shape
|
||||||
|
fn ellipse_distance(point: vec2<f32>, center: vec2<f32>, radius_along: f32, radius_perp: f32, is_horizontal: bool) -> f32 {
|
||||||
|
let delta = point - center;
|
||||||
|
var normalized: vec2<f32>;
|
||||||
|
if is_horizontal {
|
||||||
|
normalized = vec2<f32>(delta.x / radius_along, delta.y / radius_perp);
|
||||||
|
} else {
|
||||||
|
normalized = vec2<f32>(delta.x / radius_perp, delta.y / radius_along);
|
||||||
|
}
|
||||||
|
return length(normalized) * min(radius_along, radius_perp);
|
||||||
|
}
|
||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
return in.color;
|
// Early out if not enabled
|
||||||
|
if params.enabled == 0u {
|
||||||
|
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let progress = params.progress;
|
||||||
|
|
||||||
|
// Convert UV to pixel coordinates
|
||||||
|
let pixel = vec2<f32>(
|
||||||
|
in.uv.x * params.screen_width,
|
||||||
|
in.uv.y * params.screen_height
|
||||||
|
);
|
||||||
|
|
||||||
|
let terminal_height = params.screen_height - params.terminal_y_offset;
|
||||||
|
let is_horizontal = params.direction == 0u || params.direction == 1u;
|
||||||
|
|
||||||
|
// Calculate glow parameters based on animation phase
|
||||||
|
var alpha: f32;
|
||||||
|
var size_factor: f32;
|
||||||
|
var split: f32;
|
||||||
|
|
||||||
|
if progress < PHASE1_END {
|
||||||
|
// Phase 1: Fade in, grow
|
||||||
|
let t = progress / PHASE1_END;
|
||||||
|
let ease = ease_out_cubic(t);
|
||||||
|
alpha = ease * 0.8;
|
||||||
|
size_factor = 0.3 + 0.7 * ease;
|
||||||
|
split = 0.0;
|
||||||
|
} else {
|
||||||
|
// Phase 2: Split and fade out
|
||||||
|
let t = (progress - PHASE1_END) / (1.0 - PHASE1_END);
|
||||||
|
let fade = 1.0 - t;
|
||||||
|
alpha = fade * fade * 0.8;
|
||||||
|
size_factor = 1.0 - 0.3 * t;
|
||||||
|
split = ease_out_cubic(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
let base_radius = GLOW_RADIUS * size_factor;
|
||||||
|
let radius_along = base_radius * GLOW_ASPECT;
|
||||||
|
let radius_perp = base_radius;
|
||||||
|
|
||||||
|
// Calculate edge center and travel distance based on direction
|
||||||
|
var edge_center: vec2<f32>;
|
||||||
|
var travel: vec2<f32>;
|
||||||
|
|
||||||
|
switch params.direction {
|
||||||
|
// Up - top edge
|
||||||
|
case 0u: {
|
||||||
|
edge_center = vec2<f32>(params.screen_width / 2.0, params.terminal_y_offset);
|
||||||
|
travel = vec2<f32>(params.screen_width / 2.0, 0.0);
|
||||||
|
}
|
||||||
|
// Down - bottom edge
|
||||||
|
case 1u: {
|
||||||
|
edge_center = vec2<f32>(params.screen_width / 2.0, params.screen_height);
|
||||||
|
travel = vec2<f32>(params.screen_width / 2.0, 0.0);
|
||||||
|
}
|
||||||
|
// Left - left edge
|
||||||
|
case 2u: {
|
||||||
|
edge_center = vec2<f32>(0.0, params.terminal_y_offset + terminal_height / 2.0);
|
||||||
|
travel = vec2<f32>(0.0, terminal_height / 2.0);
|
||||||
|
}
|
||||||
|
// Right - right edge
|
||||||
|
case 3u: {
|
||||||
|
edge_center = vec2<f32>(params.screen_width, params.terminal_y_offset + terminal_height / 2.0);
|
||||||
|
travel = vec2<f32>(0.0, terminal_height / 2.0);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
edge_center = vec2<f32>(0.0, 0.0);
|
||||||
|
travel = vec2<f32>(0.0, 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var glow_intensity: f32 = 0.0;
|
||||||
|
|
||||||
|
if split < 0.01 {
|
||||||
|
// Single glow at center
|
||||||
|
let dist = ellipse_distance(pixel, edge_center, radius_along, radius_perp, is_horizontal);
|
||||||
|
glow_intensity = glow_falloff(dist, base_radius);
|
||||||
|
} else {
|
||||||
|
// Two glows splitting apart
|
||||||
|
let split_radius = base_radius * (1.0 - 0.2 * split);
|
||||||
|
let split_radius_along = radius_along * (1.0 - 0.2 * split);
|
||||||
|
let split_radius_perp = radius_perp * (1.0 - 0.2 * split);
|
||||||
|
|
||||||
|
let center1 = edge_center - travel * split;
|
||||||
|
let center2 = edge_center + travel * split;
|
||||||
|
|
||||||
|
let dist1 = ellipse_distance(pixel, center1, split_radius_along, split_radius_perp, is_horizontal);
|
||||||
|
let dist2 = ellipse_distance(pixel, center2, split_radius_along, split_radius_perp, is_horizontal);
|
||||||
|
|
||||||
|
// Combine both glows (additive but capped)
|
||||||
|
let glow1 = glow_falloff(dist1, split_radius);
|
||||||
|
let glow2 = glow_falloff(dist2, split_radius);
|
||||||
|
glow_intensity = min(glow1 + glow2, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply alpha
|
||||||
|
let final_alpha = glow_intensity * alpha;
|
||||||
|
|
||||||
|
// Output with premultiplied alpha for proper blending
|
||||||
|
let color = vec3<f32>(params.color_r, params.color_g, params.color_b);
|
||||||
|
return vec4<f32>(color * final_alpha, final_alpha);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,24 @@
|
|||||||
use crate::keyboard::{query_response, KeyboardState};
|
use crate::keyboard::{query_response, KeyboardState};
|
||||||
use crate::vt_parser::{CsiParams, Handler, Parser};
|
use crate::vt_parser::{CsiParams, Handler, Parser};
|
||||||
|
|
||||||
|
/// Commands that the terminal can send to the application.
|
||||||
|
/// These are triggered by special escape sequences from programs like Neovim.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum TerminalCommand {
|
||||||
|
/// Navigate to a neighboring pane in the given direction.
|
||||||
|
/// Triggered by OSC 51;navigate;<direction> ST
|
||||||
|
NavigatePane(Direction),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Direction for pane navigation.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum Direction {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
/// A single cell in the terminal grid.
|
/// A single cell in the terminal grid.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct Cell {
|
pub struct Cell {
|
||||||
@@ -467,6 +485,9 @@ pub struct Terminal {
|
|||||||
parser: Option<Parser>,
|
parser: Option<Parser>,
|
||||||
/// Performance timing stats (for debugging).
|
/// Performance timing stats (for debugging).
|
||||||
pub stats: ProcessingStats,
|
pub stats: ProcessingStats,
|
||||||
|
/// Command queue for terminal-to-application communication.
|
||||||
|
/// Commands are added by OSC handlers and consumed by the application.
|
||||||
|
command_queue: Vec<TerminalCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Terminal {
|
impl Terminal {
|
||||||
@@ -523,6 +544,7 @@ impl Terminal {
|
|||||||
line_pool,
|
line_pool,
|
||||||
parser: Some(Parser::new()),
|
parser: Some(Parser::new()),
|
||||||
stats: ProcessingStats::default(),
|
stats: ProcessingStats::default(),
|
||||||
|
command_queue: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,6 +592,13 @@ impl Terminal {
|
|||||||
self.dirty_lines = [0u64; 4];
|
self.dirty_lines = [0u64; 4];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Take all pending commands from the queue.
|
||||||
|
/// Returns an empty Vec if no commands are pending.
|
||||||
|
#[inline]
|
||||||
|
pub fn take_commands(&mut self) -> Vec<TerminalCommand> {
|
||||||
|
std::mem::take(&mut self.command_queue)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the dirty lines bitmap (for passing to shm).
|
/// Get the dirty lines bitmap (for passing to shm).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get_dirty_lines(&self) -> u64 {
|
pub fn get_dirty_lines(&self) -> u64 {
|
||||||
@@ -1336,6 +1365,38 @@ impl Handler for Terminal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// OSC 51 - ZTerm custom commands
|
||||||
|
// Format: OSC 51;command;args ST
|
||||||
|
// Currently supported:
|
||||||
|
// OSC 51;navigate;up/down/left/right ST - Navigate to neighboring pane
|
||||||
|
51 => {
|
||||||
|
if parts.len() >= 2 {
|
||||||
|
if let Ok(command) = std::str::from_utf8(parts[1]) {
|
||||||
|
match command {
|
||||||
|
"navigate" => {
|
||||||
|
if parts.len() >= 3 {
|
||||||
|
if let Ok(direction_str) = std::str::from_utf8(parts[2]) {
|
||||||
|
let direction = match direction_str {
|
||||||
|
"up" => Some(Direction::Up),
|
||||||
|
"down" => Some(Direction::Down),
|
||||||
|
"left" => Some(Direction::Left),
|
||||||
|
"right" => Some(Direction::Right),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(dir) = direction {
|
||||||
|
log::debug!("OSC 51: Navigate {:?}", dir);
|
||||||
|
self.command_queue.push(TerminalCommand::NavigatePane(dir));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::debug!("OSC 51: Unknown command '{}'", command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::debug!("Unhandled OSC {}", osc_num);
|
log::debug!("Unhandled OSC {}", osc_num);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user