From 613511c2750c948e5f1cb0a21946110a11d7665a Mon Sep 17 00:00:00 2001 From: Andrew Hamilton Date: Wed, 5 Aug 2026 08:37:19 +1000 Subject: [PATCH 1/7] Bump version to 2026.8.5 --- Cargo.toml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 76a611b..bac6103 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textgrid" -version = "2026.8.4" +version = "2026.8.5" edition = "2021" [lib] diff --git a/pyproject.toml b/pyproject.toml index 356e0ab..f3799a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "textgrid" -version = "2026.8.4" +version = "2026.8.5" requires-python = ">=3.11" [tool.maturin] From 0fc36c2314a9a5b187e91728a7f7c8911f9f3f2b Mon Sep 17 00:00:00 2001 From: Andrew Hamilton Date: Wed, 5 Aug 2026 10:44:55 +1000 Subject: [PATCH 2/7] Use a 16px default font scale instead of 32px Cell metrics halve to 8x12px, matching the text size of a typical terminal at the default 120x30 grid. --- Cargo.lock | 2 +- src/font.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef5b2a8..7382790 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1651,7 +1651,7 @@ dependencies = [ [[package]] name = "textgrid" -version = "2026.8.3" +version = "2026.8.5" dependencies = [ "bytemuck", "glfw", diff --git a/src/font.rs b/src/font.rs index 7883ac4..c935dfd 100644 --- a/src/font.rs +++ b/src/font.rs @@ -363,7 +363,7 @@ mod tests { ("IosevkaTerm", "/usr/local/share/fonts/truetype/iosevka/IosevkaTerm-Regular.ttf"), ("DejaVuSansMono", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"), ]; - let scale = 32.0; + let scale = 16.0; let slot_w = 16u32; let slot_h = 40u32; From 4690fbbdf11714d8e8d2906b3e52d7c180fa6a38 Mon Sep 17 00:00:00 2001 From: Andrew Hamilton Date: Wed, 5 Aug 2026 11:50:21 +1000 Subject: [PATCH 3/7] Start one Ctrl-- notch below the old 32px default font scale --- src/font.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/font.rs b/src/font.rs index c935dfd..8145558 100644 --- a/src/font.rs +++ b/src/font.rs @@ -181,7 +181,7 @@ pub fn compute_cell_metrics(font_data: &[u8], scale: f32) -> (u32, u32) { } pub fn load_font(font_path: Option<&str>) -> (FontSet, f32) { - let scale = 32.0; + let scale = 32.0 / 1.2; match font_path { Some(path) => { let regular = read_font_data(path); @@ -363,7 +363,7 @@ mod tests { ("IosevkaTerm", "/usr/local/share/fonts/truetype/iosevka/IosevkaTerm-Regular.ttf"), ("DejaVuSansMono", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"), ]; - let scale = 16.0; + let scale = 32.0; let slot_w = 16u32; let slot_h = 40u32; From edaa1ea8cdbe26f67e18d936902518448f58dc71 Mon Sep 17 00:00:00 2001 From: Andrew Hamilton Date: Thu, 6 Aug 2026 07:15:36 +1000 Subject: [PATCH 4/7] Request input focus when the window opens --- src/renderer.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer.rs b/src/renderer.rs index 85f5a9f..86827ba 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -79,6 +79,7 @@ pub fn start( w.set_close_polling(true); w.set_key_polling(true); w.set_char_polling(true); + w.focus(); let fb = w.get_framebuffer_size(); fb_w = fb.0.max(1) as u32; fb_h = fb.1.max(1) as u32; From a287ce72813eee3b48a9ec34d55ab7277010491b Mon Sep 17 00:00:00 2001 From: Andrew Hamilton Date: Thu, 6 Aug 2026 07:27:40 +1000 Subject: [PATCH 5/7] Retry requesting input focus until the window is focused at startup --- src/renderer.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/renderer.rs b/src/renderer.rs index 86827ba..691ea82 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,6 +1,7 @@ use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::mpsc::Receiver; use std::sync::{Arc, Condvar, Mutex}; +use std::time::{Duration, Instant}; use bytemuck::{Pod, Zeroable}; use pyo3::prelude::*; @@ -79,7 +80,14 @@ pub fn start( w.set_close_polling(true); w.set_key_polling(true); w.set_char_polling(true); + w.set_focus_polling(true); w.focus(); + let focus_deadline = Instant::now() + Duration::from_secs(2); + while Instant::now() < focus_deadline && !w.is_focused() { + glfw_ctx.poll_events(); + w.focus(); + std::thread::sleep(Duration::from_millis(10)); + } let fb = w.get_framebuffer_size(); fb_w = fb.0.max(1) as u32; fb_h = fb.1.max(1) as u32; From 17c54d89216c51c9427513ca6179c42d86fd9157 Mon Sep 17 00:00:00 2001 From: Andrew Hamilton Date: Thu, 6 Aug 2026 08:04:26 +1000 Subject: [PATCH 6/7] Publish the real window-fit grid to the TUI before the first frame --- src/renderer.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/renderer.rs b/src/renderer.rs index 691ea82..2a7ba72 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -94,6 +94,9 @@ pub fn start( (w, r) }; + cols_atomic.store((fb_w / cell_w).max(1), Ordering::Release); + rows_atomic.store((fb_h / cell_h).max(1), Ordering::Release); + // ----- wgpu ----- let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: { From fd89732a72567cdbe1b1e2f3034e37288428cedf Mon Sep 17 00:00:00 2001 From: Andrew Hamilton Date: Thu, 6 Aug 2026 08:44:47 +1000 Subject: [PATCH 7/7] Translate Alt/Shift/Ctrl-modified keys and F-keys to terminal escape sequences --- src/renderer.rs | 87 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 18 deletions(-) diff --git a/src/renderer.rs b/src/renderer.rs index 2a7ba72..ed571ad 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -79,7 +79,7 @@ pub fn start( w.set_size_polling(true); w.set_close_polling(true); w.set_key_polling(true); - w.set_char_polling(true); + w.set_char_mods_polling(true); w.set_focus_polling(true); w.focus(); let focus_deadline = Instant::now() + Duration::from_secs(2); @@ -306,9 +306,12 @@ pub fn start( }); } } - glfw::WindowEvent::Char(codepoint) => { + glfw::WindowEvent::CharModifiers(codepoint, mods) => { if let Some(ref cb) = on_key { - let s: String = codepoint.into(); + let mut s: String = codepoint.into(); + if mods.contains(glfw::Modifiers::Alt) && !s.is_empty() { + s.insert(0, '\x1b'); + } Python::with_gil(|py| { let _ = cb.call1(py, (s,)); }); @@ -410,26 +413,74 @@ pub fn start( Some(String::from('\x16')) } } else if mods.contains(glfw::Modifiers::Control) { - let code = key as i32; - if (65..=90).contains(&code) { - Some(String::from((code - 64) as u8 as char)) - } else { - None + match key { + glfw::Key::Up => Some("\x1b[1;5A".into()), + glfw::Key::Down => Some("\x1b[1;5B".into()), + glfw::Key::Left => Some("\x1b[1;5D".into()), + glfw::Key::Right => Some("\x1b[1;5C".into()), + _ => { + let code = key as i32; + if (65..=90).contains(&code) { + Some(String::from((code - 64) as u8 as char)) + } else { + None + } + } } } else { + let mod_num = if mods.contains(glfw::Modifiers::Shift) + && mods.contains(glfw::Modifiers::Alt) + { 4 } + else if mods.contains(glfw::Modifiers::Alt) { 3 } + else if mods.contains(glfw::Modifiers::Shift) { 2 } + else { 1 }; match key { + glfw::Key::Up => if mod_num > 1 { + Some(format!("\x1b[1;{}A", mod_num)) + } else { Some("\x1b[A".into()) }, + glfw::Key::Down => if mod_num > 1 { + Some(format!("\x1b[1;{}B", mod_num)) + } else { Some("\x1b[B".into()) }, + glfw::Key::Left => if mod_num > 1 { + Some(format!("\x1b[1;{}D", mod_num)) + } else { Some("\x1b[D".into()) }, + glfw::Key::Right => if mod_num > 1 { + Some(format!("\x1b[1;{}C", mod_num)) + } else { Some("\x1b[C".into()) }, + glfw::Key::Home => if mod_num > 1 { + Some(format!("\x1b[1;{}H", mod_num)) + } else { Some("\x1b[H".into()) }, + glfw::Key::End => if mod_num > 1 { + Some(format!("\x1b[1;{}F", mod_num)) + } else { Some("\x1b[F".into()) }, + glfw::Key::PageUp => if mod_num > 1 { + Some(format!("\x1b[5;{}~", mod_num)) + } else { Some("\x1b[5~".into()) }, + glfw::Key::PageDown => if mod_num > 1 { + Some(format!("\x1b[6;{}~", mod_num)) + } else { Some("\x1b[6~".into()) }, + glfw::Key::Tab => if mods.contains(glfw::Modifiers::Shift) { + Some("\x1b[Z".into()) + } else { Some("\t".into()) }, + glfw::Key::Backspace => if mods.contains(glfw::Modifiers::Alt) { + Some("\x1b\x7f".into()) + } else { Some("\x7f".into()) }, + glfw::Key::Insert => Some("\x1b[2~".into()), + glfw::Key::Delete => Some("\x1b[3~".into()), + glfw::Key::F1 => Some("\x1bOP".into()), + glfw::Key::F2 => Some("\x1bOQ".into()), + glfw::Key::F3 => Some("\x1bOR".into()), + glfw::Key::F4 => Some("\x1bOS".into()), + glfw::Key::F5 => Some("\x1b[15~".into()), + glfw::Key::F6 => Some("\x1b[17~".into()), + glfw::Key::F7 => Some("\x1b[18~".into()), + glfw::Key::F8 => Some("\x1b[19~".into()), + glfw::Key::F9 => Some("\x1b[20~".into()), + glfw::Key::F10 => Some("\x1b[21~".into()), + glfw::Key::F11 => Some("\x1b[23~".into()), + glfw::Key::F12 => Some("\x1b[24~".into()), glfw::Key::Enter => Some("\n".into()), glfw::Key::Escape => Some("\x1b".into()), - glfw::Key::Backspace => Some("\x7f".into()), - glfw::Key::Tab => Some("\t".into()), - glfw::Key::Up => Some("\x1b[A".into()), - glfw::Key::Down => Some("\x1b[B".into()), - glfw::Key::Left => Some("\x1b[D".into()), - glfw::Key::Right => Some("\x1b[C".into()), - glfw::Key::Home => Some("\x1b[H".into()), - glfw::Key::End => Some("\x1b[F".into()), - glfw::Key::PageUp => Some("\x1b[5~".into()), - glfw::Key::PageDown => Some("\x1b[6~".into()), _ => None, } };