many fixed and changes

This commit is contained in:
Zacharias-Brohn
2025-12-15 22:16:15 +01:00
parent e4d742cadf
commit 052724e347
8 changed files with 1885 additions and 1898 deletions
Generated
+22
View File
@@ -514,6 +514,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "fontconfig"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b19c4bca8c705ea23bfb3e3403a9e699344d1ee3205b631f03fe4dbf1e52429f"
dependencies = [
"yeslogic-fontconfig-sys",
]
[[package]]
name = "fontdue"
version = "0.9.3"
@@ -2580,6 +2589,17 @@ version = "0.8.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
[[package]]
name = "yeslogic-fontconfig-sys"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd"
dependencies = [
"dlib",
"once_cell",
"pkg-config",
]
[[package]]
name = "zerocopy"
version = "0.8.31"
@@ -2609,6 +2629,7 @@ dependencies = [
"bytemuck",
"dirs",
"env_logger",
"fontconfig",
"fontdue",
"libc",
"log",
@@ -2624,4 +2645,5 @@ dependencies = [
"ttf-parser 0.25.1",
"wgpu",
"winit",
"yeslogic-fontconfig-sys",
]
+2
View File
@@ -39,6 +39,8 @@ bitflags = "2"
fontdue = "0.9"
rustybuzz = "0.20"
ttf-parser = "0.25"
fontconfig = "0.10"
fontconfig-sys = { package = "yeslogic-fontconfig-sys", version = "6.0" }
# Configuration
serde = { version = "1", features = ["derive"] }
+31
View File
@@ -0,0 +1,31 @@
# Maintainer: Zach <zach@brohn.se>
pkgname=zterm
pkgver=0.1.0
pkgrel=1
pkgdesc="A GPU-accelerated terminal emulator for Wayland"
arch=('x86_64')
url="https://github.com/Zacharias-Brohn/zterm"
license=('MIT')
depends=(
'fontconfig'
'freetype2'
'wayland'
'libxkbcommon'
'vulkan-icd-loader'
)
makedepends=('rust' 'cargo')
source=()
build() {
cd "$startdir"
cargo build --release --locked
}
package() {
cd "$startdir"
install -Dm755 "target/release/zterm" "$pkgdir/usr/bin/zterm"
install -Dm644 "zterm.terminfo" "$pkgdir/usr/share/zterm/zterm.terminfo"
# Compile and install terminfo
tic -x -o "$pkgdir/usr/share/terminfo" zterm.terminfo
}
+87
View File
@@ -0,0 +1,87 @@
//! Test ligature detection - compare individual vs combined shaping
use rustybuzz::{Face, UnicodeBuffer, Feature};
use ttf_parser::Tag;
use fontdue::Font;
use std::fs;
fn main() {
let path = "/usr/share/fonts/TTF/0xProtoNerdFontMono-Regular.ttf";
println!("Using font: {}", path);
let font_data = fs::read(path).expect("Failed to read font");
let face = Face::from_slice(&font_data, 0).expect("Failed to parse font");
let fontdue_font = Font::from_bytes(&font_data[..], fontdue::FontSettings::default()).unwrap();
let font_size = 16.0;
let units_per_em = face.units_per_em() as f32;
println!("Font units per em: {}", units_per_em);
// Get cell width from a regular character
let (hyphen_metrics, _) = fontdue_font.rasterize('-', font_size);
let cell_width = hyphen_metrics.advance_width;
println!("Cell width (from '-'): {:.2}px", cell_width);
let features = vec![
Feature::new(Tag::from_bytes(b"liga"), 1, ..),
Feature::new(Tag::from_bytes(b"calt"), 1, ..),
Feature::new(Tag::from_bytes(b"dlig"), 1, ..),
];
let test_strings = ["->", "=>", "==", "!=", ">=", "<="];
for s in &test_strings {
// Shape combined string
let mut buffer = UnicodeBuffer::new();
buffer.push_str(s);
let combined = rustybuzz::shape(&face, &features, buffer);
let combined_infos = combined.glyph_infos();
let combined_positions = combined.glyph_positions();
// Shape each character individually
let mut individual_glyphs = Vec::new();
for c in s.chars() {
let mut buf = UnicodeBuffer::new();
buf.push_str(&c.to_string());
let shaped = rustybuzz::shape(&face, &features, buf);
individual_glyphs.push(shaped.glyph_infos()[0].glyph_id);
}
println!("\n'{}' analysis:", s);
println!(" Combined glyphs: {:?}", combined_infos.iter().map(|i| i.glyph_id).collect::<Vec<_>>());
println!(" Individual glyphs: {:?}", individual_glyphs);
// Show advances for each glyph
for (i, (info, pos)) in combined_infos.iter().zip(combined_positions.iter()).enumerate() {
let advance_px = pos.x_advance as f32 * font_size / units_per_em;
println!(" Glyph {}: id={}, advance={} units ({:.2}px)", i, info.glyph_id, pos.x_advance, advance_px);
// Rasterize and show metrics
let (metrics, _) = fontdue_font.rasterize_indexed(info.glyph_id as u16, font_size);
println!(" Rasterized: {}x{} px, xmin={}, ymin={}, advance_width={:.2}",
metrics.width, metrics.height, metrics.xmin, metrics.ymin, metrics.advance_width);
}
// Check if any glyph was substituted
let has_substitution = combined_infos.iter().zip(individual_glyphs.iter())
.any(|(combined, &individual)| combined.glyph_id != individual);
println!(" Has substitution: {}", has_substitution);
}
// Also test what Kitty does - check glyph names
println!("\n=== Checking glyph names via ttf-parser ===");
let ttf_face = ttf_parser::Face::parse(&font_data, 0).unwrap();
// Shape "->" and get glyph names
let mut buffer = UnicodeBuffer::new();
buffer.push_str("->");
let combined = rustybuzz::shape(&face, &features, buffer);
for info in combined.glyph_infos() {
let glyph_id = ttf_parser::GlyphId(info.glyph_id as u16);
if let Some(name) = ttf_face.glyph_name(glyph_id) {
println!(" Glyph {} name: {}", info.glyph_id, name);
} else {
println!(" Glyph {} has no name", info.glyph_id);
}
}
}
+7
View File
@@ -362,6 +362,11 @@ pub struct Config {
pub background_opacity: f32,
/// Number of lines to keep in scrollback buffer.
pub scrollback_lines: usize,
/// Duration in milliseconds for the inactive pane fade animation.
/// Set to 0 for instant transitions.
pub inactive_pane_fade_ms: u64,
/// Dim factor for inactive panes (0.0 = fully dimmed/black, 1.0 = no dimming).
pub inactive_pane_dim: f32,
/// Keybindings.
pub keybindings: Keybindings,
}
@@ -373,6 +378,8 @@ impl Default for Config {
tab_bar_position: TabBarPosition::Top,
background_opacity: 1.0,
scrollback_lines: 50_000,
inactive_pane_fade_ms: 150,
inactive_pane_dim: 0.6,
keybindings: Keybindings::default(),
}
}
+110 -26
View File
@@ -6,7 +6,7 @@
use zterm::config::{Action, Config};
use zterm::keyboard::{FunctionalKey, KeyEncoder, KeyEventType, KeyboardState, Modifiers};
use zterm::pty::Pty;
use zterm::renderer::Renderer;
use zterm::renderer::{PaneRenderInfo, Renderer};
use zterm::terminal::{Terminal, MouseTrackingMode};
use std::collections::HashMap;
@@ -146,6 +146,10 @@ struct Pane {
is_selecting: bool,
/// Last scrollback length for tracking changes.
last_scrollback_len: u32,
/// When the focus animation started (for smooth fade).
focus_animation_start: std::time::Instant,
/// Whether this pane was focused before the current animation.
was_focused: bool,
}
impl Pane {
@@ -170,6 +174,8 @@ impl Pane {
selection: None,
is_selecting: false,
last_scrollback_len: 0,
focus_animation_start: std::time::Instant::now(),
was_focused: true, // New panes start as focused
})
}
@@ -192,6 +198,36 @@ impl Pane {
fn child_exited(&self) -> bool {
self.pty.child_exited()
}
/// Calculate the current dim factor based on animation progress.
/// 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 {
// Detect focus change
if is_focused != self.was_focused {
self.focus_animation_start = std::time::Instant::now();
self.was_focused = is_focused;
}
// If no animation (instant), return target value immediately
if fade_duration_ms == 0 {
return if is_focused { 1.0 } else { inactive_dim };
}
let elapsed = self.focus_animation_start.elapsed().as_millis() as f32;
let duration = fade_duration_ms as f32;
let progress = (elapsed / duration).min(1.0);
// Smooth easing (ease-out cubic)
let eased = 1.0 - (1.0 - progress).powi(3);
if is_focused {
// Fading in: from inactive_dim to 1.0
inactive_dim + (1.0 - inactive_dim) * eased
} else {
// Fading out: from 1.0 to inactive_dim
1.0 - (1.0 - inactive_dim) * eased
}
}
}
/// Geometry of a pane in pixels.
@@ -308,17 +344,6 @@ impl SplitNode {
}
}
/// Collect all pane IDs.
fn collect_pane_ids(&self, ids: &mut Vec<PaneId>) {
match self {
SplitNode::Leaf { pane_id, .. } => ids.push(*pane_id),
SplitNode::Split { first, second, .. } => {
first.collect_pane_ids(ids);
second.collect_pane_ids(ids);
}
}
}
/// Collect all pane geometries.
fn collect_geometries(&self, geometries: &mut Vec<(PaneId, PaneGeometry)>) {
match self {
@@ -507,6 +532,7 @@ impl TabId {
/// A single tab containing one or more panes arranged in a split tree.
struct Tab {
/// Unique identifier for this tab.
#[allow(dead_code)]
id: TabId,
/// All panes in this tab, keyed by PaneId.
panes: HashMap<PaneId, Pane>,
@@ -515,6 +541,7 @@ struct Tab {
/// Currently active pane ID.
active_pane: PaneId,
/// Tab title (from OSC or shell).
#[allow(dead_code)]
title: String,
}
@@ -637,11 +664,6 @@ impl Tab {
}
}
/// Get all pane IDs.
fn pane_ids(&self) -> Vec<PaneId> {
self.panes.keys().copied().collect()
}
/// Get pane by ID.
fn get_pane(&self, pane_id: PaneId) -> Option<&Pane> {
self.panes.get(&pane_id)
@@ -1191,6 +1213,8 @@ impl App {
if let Some(renderer) = &self.renderer {
let (cols, rows) = renderer.terminal_size();
self.create_tab(cols, rows);
// Resize the new tab to calculate pane geometries
self.resize_all_panes();
if let Some(window) = &self.window {
window.request_redraw();
}
@@ -1751,22 +1775,83 @@ impl ApplicationHandler<UserEvent> for App {
}
}
// Render
// Render all panes
let render_start = std::time::Instant::now();
let num_tabs = self.tabs.len();
let active_tab_idx = self.active_tab;
let fade_duration_ms = self.config.inactive_pane_fade_ms;
let inactive_dim = self.config.inactive_pane_dim;
if let Some(renderer) = &mut self.renderer {
if let Some(tab) = self.tabs.get(active_tab_idx) {
if let Some(pane) = tab.active_pane() {
if let Some(tab) = self.tabs.get_mut(active_tab_idx) {
// Collect all pane geometries
let geometries = tab.collect_pane_geometries();
let active_pane_id = tab.active_pane;
// First pass: calculate dim factors (needs mutable access)
let mut dim_factors: Vec<(PaneId, f32)> = Vec::new();
for (pane_id, _) in &geometries {
if let Some(pane) = tab.panes.get_mut(pane_id) {
let is_active = *pane_id == active_pane_id;
let dim_factor = pane.calculate_dim_factor(is_active, fade_duration_ms, inactive_dim);
dim_factors.push((*pane_id, dim_factor));
}
}
// Build render info for all panes
let mut pane_render_data: Vec<(&Terminal, PaneRenderInfo, Option<(usize, usize, usize, usize)>)> = Vec::new();
for (pane_id, geom) in &geometries {
if let Some(pane) = tab.panes.get(pane_id) {
let is_active = *pane_id == active_pane_id;
let scroll_offset = pane.terminal.scroll_offset;
let visible_rows = renderer.terminal_size().1;
let renderer_selection = pane.selection.as_ref()
.and_then(|sel| sel.to_screen_coords(scroll_offset, visible_rows));
renderer.set_selection(renderer_selection);
// Get pre-calculated dim factor
let dim_factor = dim_factors.iter()
.find(|(id, _)| id == pane_id)
.map(|(_, f)| *f)
.unwrap_or(if is_active { 1.0 } else { inactive_dim });
match renderer.render_from_terminal(&pane.terminal, num_tabs, active_tab_idx) {
// Convert selection to screen coords for this pane
let selection = if is_active {
pane.selection.as_ref()
.and_then(|sel| sel.to_screen_coords(scroll_offset, geom.rows))
} else {
None
};
let render_info = PaneRenderInfo {
x: geom.x,
y: geom.y,
width: geom.width,
height: geom.height,
cols: geom.cols,
rows: geom.rows,
is_active,
dim_factor,
};
pane_render_data.push((&pane.terminal, render_info, selection));
}
}
// Request redraw if any animation is in progress
let animation_in_progress = dim_factors.iter().any(|(id, factor)| {
let is_active = *id == active_pane_id;
if is_active {
*factor < 1.0
} else {
*factor > inactive_dim
}
});
if animation_in_progress {
if let Some(window) = &self.window {
window.request_redraw();
}
}
match renderer.render_panes(&pane_render_data, num_tabs, active_tab_idx) {
Ok(_) => {}
Err(wgpu::SurfaceError::Lost) => {
renderer.resize(renderer.width, renderer.height);
@@ -1781,7 +1866,6 @@ impl ApplicationHandler<UserEvent> for App {
}
}
}
}
let render_time = render_start.elapsed();
let frame_time = frame_start.elapsed();
+1506 -1501
View File
File diff suppressed because it is too large Load Diff
+39 -290
View File
@@ -144,292 +144,6 @@ impl Default for ColorPalette {
}
}
/// Packed color value for GPU transfer (Kitty-style encoding).
/// Layout: type in low 8 bits, RGB value in upper 24 bits.
/// - Type 0: Default (use color table entries 256/257 for fg/bg)
/// - Type 1: Indexed (index in bits 8-15, look up in color table)
/// - Type 2: RGB (R in bits 8-15, G in bits 16-23, B in bits 24-31)
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[repr(transparent)]
pub struct PackedColor(pub u32);
impl PackedColor {
/// Color type: default (resolved from color table)
pub const TYPE_DEFAULT: u8 = 0;
/// Color type: indexed (look up in 256-color palette)
pub const TYPE_INDEXED: u8 = 1;
/// Color type: direct RGB
pub const TYPE_RGB: u8 = 2;
/// Create a default color (resolved at render time from palette).
#[inline]
pub const fn default_color() -> Self {
Self(Self::TYPE_DEFAULT as u32)
}
/// Create an indexed color (0-255 palette index).
#[inline]
pub const fn indexed(index: u8) -> Self {
Self(Self::TYPE_INDEXED as u32 | ((index as u32) << 8))
}
/// Create a direct RGB color.
#[inline]
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
Self(Self::TYPE_RGB as u32 | ((r as u32) << 8) | ((g as u32) << 16) | ((b as u32) << 24))
}
/// Get the color type.
#[inline]
pub const fn color_type(self) -> u8 {
(self.0 & 0xFF) as u8
}
/// Get the index for indexed colors.
#[inline]
pub const fn index(self) -> u8 {
((self.0 >> 8) & 0xFF) as u8
}
/// Get RGB components for RGB colors.
#[inline]
pub const fn rgb_components(self) -> (u8, u8, u8) {
(
((self.0 >> 8) & 0xFF) as u8,
((self.0 >> 16) & 0xFF) as u8,
((self.0 >> 24) & 0xFF) as u8,
)
}
}
impl From<Color> for PackedColor {
fn from(color: Color) -> Self {
match color {
Color::Default => Self::default_color(),
Color::Indexed(idx) => Self::indexed(idx),
Color::Rgb(r, g, b) => Self::rgb(r, g, b),
}
}
}
impl From<&Color> for PackedColor {
fn from(color: &Color) -> Self {
(*color).into()
}
}
/// Packed cell attributes for GPU transfer (Kitty-style).
/// Layout (32-bit bitfield):
/// - bits 0-2: decoration (underline style, 0=none, 1=single, 2=double, 3=curly, etc.)
/// - bit 3: bold
/// - bit 4: italic
/// - bit 5: reverse
/// - bit 6: strike
/// - bit 7: dim
/// - bits 8-31: reserved for future use
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[repr(transparent)]
pub struct CellAttrs(pub u32);
impl CellAttrs {
pub const DECORATION_MASK: u32 = 0b111;
pub const BOLD_BIT: u32 = 1 << 3;
pub const ITALIC_BIT: u32 = 1 << 4;
pub const REVERSE_BIT: u32 = 1 << 5;
pub const STRIKE_BIT: u32 = 1 << 6;
pub const DIM_BIT: u32 = 1 << 7;
/// Decoration values
pub const DECO_NONE: u32 = 0;
pub const DECO_SINGLE: u32 = 1;
pub const DECO_DOUBLE: u32 = 2;
pub const DECO_CURLY: u32 = 3;
pub const DECO_DOTTED: u32 = 4;
pub const DECO_DASHED: u32 = 5;
#[inline]
pub const fn new() -> Self {
Self(0)
}
#[inline]
pub const fn with_underline(self, style: u32) -> Self {
Self((self.0 & !Self::DECORATION_MASK) | (style & Self::DECORATION_MASK))
}
#[inline]
pub const fn with_bold(self, bold: bool) -> Self {
if bold {
Self(self.0 | Self::BOLD_BIT)
} else {
Self(self.0 & !Self::BOLD_BIT)
}
}
#[inline]
pub const fn with_italic(self, italic: bool) -> Self {
if italic {
Self(self.0 | Self::ITALIC_BIT)
} else {
Self(self.0 & !Self::ITALIC_BIT)
}
}
#[inline]
pub const fn with_reverse(self, reverse: bool) -> Self {
if reverse {
Self(self.0 | Self::REVERSE_BIT)
} else {
Self(self.0 & !Self::REVERSE_BIT)
}
}
#[inline]
pub const fn with_strike(self, strike: bool) -> Self {
if strike {
Self(self.0 | Self::STRIKE_BIT)
} else {
Self(self.0 & !Self::STRIKE_BIT)
}
}
#[inline]
pub const fn with_dim(self, dim: bool) -> Self {
if dim {
Self(self.0 | Self::DIM_BIT)
} else {
Self(self.0 & !Self::DIM_BIT)
}
}
#[inline]
pub const fn decoration(self) -> u32 {
self.0 & Self::DECORATION_MASK
}
#[inline]
pub const fn is_bold(self) -> bool {
(self.0 & Self::BOLD_BIT) != 0
}
#[inline]
pub const fn is_italic(self) -> bool {
(self.0 & Self::ITALIC_BIT) != 0
}
#[inline]
pub const fn is_reverse(self) -> bool {
(self.0 & Self::REVERSE_BIT) != 0
}
#[inline]
pub const fn is_strike(self) -> bool {
(self.0 & Self::STRIKE_BIT) != 0
}
#[inline]
pub const fn is_dim(self) -> bool {
(self.0 & Self::DIM_BIT) != 0
}
}
/// GPU cell data for instanced rendering (Kitty-style).
///
/// This struct is uploaded directly to the GPU for each cell.
/// The shader uses instanced rendering where each cell is one instance.
///
/// Layout: 20 bytes total
/// - fg: 4 bytes (packed color)
/// - bg: 4 bytes (packed color)
/// - decoration_fg: 4 bytes (packed color for underline/strikethrough)
/// - sprite_idx: 4 bytes (glyph atlas index, bit 31 = colored glyph flag)
/// - attrs: 4 bytes (packed attributes)
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GPUCell {
/// Foreground color (packed)
pub fg: u32,
/// Background color (packed)
pub bg: u32,
/// Decoration color for underline/strikethrough (packed)
pub decoration_fg: u32,
/// Sprite index in glyph atlas (bit 31 = colored glyph flag)
pub sprite_idx: u32,
/// Packed attributes (bold, italic, underline style, etc.)
pub attrs: u32,
}
impl GPUCell {
/// Flag indicating this glyph is colored (e.g., emoji) and should not be tinted
pub const COLORED_GLYPH_FLAG: u32 = 1 << 31;
/// Sprite index indicating no glyph (space/empty)
pub const NO_GLYPH: u32 = 0;
/// Create an empty cell (space with default colors)
#[inline]
pub const fn empty() -> Self {
Self {
fg: PackedColor::TYPE_DEFAULT as u32,
bg: PackedColor::TYPE_DEFAULT as u32,
decoration_fg: PackedColor::TYPE_DEFAULT as u32,
sprite_idx: Self::NO_GLYPH,
attrs: 0,
}
}
/// Create a GPUCell from terminal Cell and a sprite index
#[inline]
pub fn from_cell(cell: &Cell, sprite_idx: u32) -> Self {
let fg = PackedColor::from(&cell.fg_color);
let bg = PackedColor::from(&cell.bg_color);
let mut attrs = CellAttrs::new();
if cell.bold {
attrs = attrs.with_bold(true);
}
if cell.italic {
attrs = attrs.with_italic(true);
}
if cell.underline {
attrs = attrs.with_underline(CellAttrs::DECO_SINGLE);
}
Self {
fg: fg.0,
bg: bg.0,
decoration_fg: fg.0, // Use fg color for decoration by default
sprite_idx,
attrs: attrs.0,
}
}
/// Set the sprite index
#[inline]
pub fn with_sprite(mut self, idx: u32) -> Self {
self.sprite_idx = idx;
self
}
/// Mark this glyph as colored (emoji)
#[inline]
pub fn with_colored_glyph(mut self) -> Self {
self.sprite_idx |= Self::COLORED_GLYPH_FLAG;
self
}
/// Get the sprite index (without the colored flag)
#[inline]
pub const fn get_sprite_idx(self) -> u32 {
self.sprite_idx & !Self::COLORED_GLYPH_FLAG
}
/// Check if this is a colored glyph
#[inline]
pub const fn is_colored_glyph(self) -> bool {
(self.sprite_idx & Self::COLORED_GLYPH_FLAG) != 0
}
}
impl ColorPalette {
/// Parse a color specification like "#RRGGBB" or "rgb:RR/GG/BB".
pub fn parse_color_spec(spec: &str) -> Option<[u8; 3]> {
@@ -886,7 +600,10 @@ impl Terminal {
#[inline]
fn clear_grid_row(&mut self, grid_row: usize) {
let blank = self.blank_cell();
self.grid[grid_row].fill(blank);
let row = &mut self.grid[grid_row];
// Ensure row has correct width (may differ after swap with scrollback post-resize)
row.resize(self.cols, blank.clone());
row.fill(blank);
}
/// Create a blank cell with the current background color (BCE - Background Color Erase).
@@ -930,13 +647,18 @@ impl Terminal {
return;
}
let old_cols = self.cols;
let old_rows = self.rows;
// Create new grid
let mut new_grid = vec![vec![Cell::default(); cols]; rows];
// Copy existing content using line_map for correct visual ordering
for visual_row in 0..rows.min(self.rows) {
let old_grid_row = self.line_map[visual_row];
for col in 0..cols.min(self.cols) {
// Use actual row length - may differ from self.cols after scrollback swap
let old_row_len = self.grid[old_grid_row].len();
for col in 0..cols.min(old_row_len) {
new_grid[visual_row][col] = self.grid[old_grid_row][col].clone();
}
}
@@ -954,6 +676,28 @@ impl Terminal {
// Adjust cursor position
self.cursor_col = self.cursor_col.min(cols.saturating_sub(1));
self.cursor_row = self.cursor_row.min(rows.saturating_sub(1));
// Also resize the saved alternate screen if it exists
if let Some(ref mut saved) = self.alternate_screen {
let mut new_saved_grid = vec![vec![Cell::default(); cols]; rows];
for visual_row in 0..rows.min(old_rows) {
let old_grid_row = saved.line_map.get(visual_row).copied().unwrap_or(visual_row);
if old_grid_row < saved.grid.len() {
for col in 0..cols.min(old_cols) {
if col < saved.grid[old_grid_row].len() {
new_saved_grid[visual_row][col] = saved.grid[old_grid_row][col].clone();
}
}
}
}
saved.grid = new_saved_grid;
saved.line_map = (0..rows).collect();
saved.cursor_col = saved.cursor_col.min(cols.saturating_sub(1));
saved.cursor_row = saved.cursor_row.min(rows.saturating_sub(1));
saved.scroll_top = 0;
saved.scroll_bottom = rows.saturating_sub(1);
}
self.dirty = true;
self.mark_all_lines_dirty();
}
@@ -1004,11 +748,12 @@ impl Terminal {
if let Some(saved) = self.alternate_screen.take() {
self.grid = saved.grid;
self.line_map = saved.line_map;
self.cursor_col = saved.cursor_col;
self.cursor_row = saved.cursor_row;
self.saved_cursor = saved.saved_cursor;
self.scroll_top = saved.scroll_top;
self.scroll_bottom = saved.scroll_bottom;
// Clamp cursor positions to current grid dimensions (defensive)
self.cursor_col = saved.cursor_col.min(self.cols.saturating_sub(1));
self.cursor_row = saved.cursor_row.min(self.rows.saturating_sub(1));
}
self.using_alternate_screen = false;
@@ -1486,6 +1231,10 @@ impl Handler for Terminal {
}
// Write character directly using cached grid_row
// Safety: ensure grid row has correct width (may differ after scrollback swap)
if self.grid[grid_row].len() != self.cols {
self.grid[grid_row].resize(self.cols, Cell::default());
}
self.grid[grid_row][self.cursor_col] = Cell {
character: c,
fg_color: self.current_fg,