fix: force redraw on terminal focus gain

This commit is contained in:
Ogulcan Celik 2026-05-15 21:17:55 +03:00
parent 73dba97521
commit e1a84a8e34
5 changed files with 99 additions and 9 deletions

View File

@ -80,6 +80,7 @@ pub struct App {
HashSet<(crossterm::event::KeyCode, crossterm::event::KeyModifiers)>,
pub render_notify: Arc<Notify>,
pub render_dirty: Arc<AtomicBool>,
pub(crate) full_redraw_pending: bool,
pub(crate) overlay_panes: HashMap<crate::layout::PaneId, OverlayPaneState>,
pub(crate) local_terminal_notifications: bool,
}
@ -441,11 +442,16 @@ impl App {
last_terminal_size: terminal::size().ok(),
render_notify,
render_dirty,
full_redraw_pending: false,
overlay_panes: HashMap::new(),
local_terminal_notifications: true,
}
}
fn request_full_redraw(&mut self) {
self.full_redraw_pending = true;
}
pub async fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
if self.input_rx.is_none() {
self.input_rx = Some(crate::raw_input::spawn_input_reader());
@ -507,6 +513,10 @@ impl App {
if needs_render && self.can_render_now(now) {
self.render_dirty.swap(false, Ordering::AcqRel);
let _sync_output = SyncOutputGuard::begin()?;
if self.full_redraw_pending {
terminal.clear()?;
self.full_redraw_pending = false;
}
let kitty_graphics_enabled = self.state.kitty_graphics_enabled;
let mut cell_size = crate::kitty_graphics::HostCellSize::default();
terminal.draw(|frame| {

View File

@ -95,8 +95,10 @@ impl App {
true
}
crate::raw_input::RawInputEvent::OuterFocusGained => {
self.request_full_redraw();
self.state.outer_terminal_focus = Some(true);
self.state.mark_active_tab_seen()
self.state.mark_active_tab_seen();
true
}
crate::raw_input::RawInputEvent::OuterFocusLost => {
self.state.outer_terminal_focus = Some(false);

View File

@ -56,6 +56,12 @@ struct ClientState {
kitty_graphics_enabled: bool,
}
impl ClientState {
fn request_full_redraw(&mut self) {
self.blit_encoder = blit::BlitEncoder::new();
}
}
// ---------------------------------------------------------------------------
// Error types
// ---------------------------------------------------------------------------
@ -497,6 +503,10 @@ async fn run_client_loop(
match event {
ClientLoopEvent::StdinInput(data) => {
let events = crate::raw_input::parse_raw_input_bytes_sync(&data);
if crate::raw_input::events_require_host_surface_redraw(&events) {
state.request_full_redraw();
}
let msg = ClientMessage::Input { data };
if let Err(e) = write_to_server(&mut write_stream, &msg) {
return Err(ClientError::ConnectionLost(e));

View File

@ -128,6 +128,12 @@ pub enum RawInputEvent {
Unsupported,
}
pub(crate) fn events_require_host_surface_redraw(events: &[RawInputEvent]) -> bool {
events
.iter()
.any(|event| matches!(event, RawInputEvent::OuterFocusGained))
}
pub fn spawn_input_reader() -> mpsc::Receiver<RawInputEvent> {
let (tx, rx) = mpsc::channel(256);
@ -648,6 +654,15 @@ mod tests {
assert!(matches!(event, RawInputEvent::OuterFocusLost));
}
#[test]
fn outer_focus_gained_requests_host_surface_redraw() {
let events = parse_raw_input_bytes_sync(b"\x1b[I");
assert!(events_require_host_surface_redraw(&events));
let events = parse_raw_input_bytes_sync(b"\x1b[O");
assert!(!events_require_host_surface_redraw(&events));
}
#[test]
fn parses_xterm_alt_up_arrow() {
let (RawInputEvent::Key(key), consumed) = extract_one_event(b"\x1b[1;3A").unwrap() else {

View File

@ -244,6 +244,14 @@ impl ClientConnection {
writer,
}
}
fn request_full_redraw(&mut self) {
self.render_state.reset_baseline();
}
fn request_semantic_redraw_after_input(&mut self) {
self.render_state.reset_semantic_input_baseline();
}
}
// ---------------------------------------------------------------------------
@ -529,7 +537,7 @@ impl HeadlessServer {
// rendering semantics. Force one fresh frame to every remaining client
// even if the next rendered buffer compares equal to its cached frame.
for client in self.clients.values_mut() {
client.render_state.reset_baseline();
client.request_full_redraw();
}
}
@ -1154,14 +1162,20 @@ impl HeadlessServer {
}
ServerEvent::ClientInput { client_id, data } => {
debug!(client_id, len = data.len(), "client input received");
if let Some(client) = self.clients.get_mut(&client_id) {
// Ensure semantic clients receive one post-input frame even if the
// semantic buffer compares equal. Terminal-ANSI clients must keep their
// server-side blit baseline; resetting it here forces a full redraw on
// every keypress and makes remote sessions feel extremely slow.
client.render_state.reset_semantic_input_baseline();
}
let events = crate::raw_input::parse_raw_input_bytes_sync(&data);
let host_surface_redraw =
crate::raw_input::events_require_host_surface_redraw(&events);
if let Some(client) = self.clients.get_mut(&client_id) {
if host_surface_redraw {
client.request_full_redraw();
} else {
// Ensure semantic clients receive one post-input frame even if the
// semantic buffer compares equal. Terminal-ANSI clients must keep their
// server-side blit baseline; resetting it here forces a full redraw on
// every keypress and makes remote sessions feel extremely slow.
client.request_semantic_redraw_after_input();
}
}
self.update_client_outer_focus_from_events(client_id, &events);
let interaction = Self::events_include_interaction(&events);
let foreground_changed = if interaction {
@ -2619,6 +2633,45 @@ mod tests {
assert!(client_rx.recv_timeout(Duration::from_millis(50)).is_err());
}
#[test]
fn outer_focus_gained_forces_terminal_ansi_full_redraw() {
let mut server = test_headless_server();
let (client_tx, _client_control_rx, client_rx) = test_client_writer();
server.clients.insert(
1,
ClientConnection::new(
(80, 24),
crate::kitty_graphics::HostCellSize::default(),
crate::terminal_theme::TerminalTheme::default(),
None,
1,
RenderEncoding::TerminalAnsi,
Some(client_tx),
),
);
server.foreground_client_id = Some(1);
server.render_and_stream();
let _ = client_rx
.recv_timeout(Duration::from_millis(100))
.expect("initial terminal frame");
assert!(server.handle_server_event(ServerEvent::ClientInput {
client_id: 1,
data: b"\x1b[I".to_vec(),
}));
server.render_and_stream();
match read_server_message(client_rx.recv_timeout(Duration::from_millis(100)).unwrap()) {
ServerMessage::Terminal(frame) => {
assert_eq!(frame.seq, 2);
assert!(frame.full);
}
other => panic!("expected terminal frame, got {other:?}"),
}
}
#[test]
fn full_render_queue_does_not_advance_terminal_ansi_baseline() {
let mut server = test_headless_server();