mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Enable pets in fullscreen tmux panes with Kitty passthrough (#59580)
## Why Pets were unavailable in tmux even when fullscreen Codex could render Kitty images through passthrough. ## What changed - Enable pet images and picker previews in fullscreen tmux panes with Kitty passthrough, and update the unavailable notice to recommend fullscreen mode with `allow-passthrough all`. - Use pane-specific image IDs and virtual placements for pets and previews, reserving separate IDs from transcript images. Repaint underlying text when pet placeholders move or disappear. - Draw pets and previews when refreshing the fullscreen transcript, and consume preview areas once per render. ## Testing Extend the owned-screen lifecycle test to verify that preparing fullscreen enables the Kitty protocol in tmux and returning to scrollback disables it. Update `/pets` notice assertions and the popup snapshot. GitOrigin-RevId: 02c838ceb06abcbb45314e1d38ff4cde77221426
This commit is contained in:
+4
-27
@@ -1289,24 +1289,7 @@ impl App {
|
||||
{
|
||||
self.discard_startup_modal_input(tui)?;
|
||||
}
|
||||
if self.chat_widget.ambient_pet_image_enabled() {
|
||||
let ambient_pet_area = self.conversation_area(screen_size.into());
|
||||
if let Err(err) = tui.draw_ambient_pet_image(
|
||||
self.chat_widget
|
||||
.ambient_pet_draw(ambient_pet_area, rendered_area.bottom()),
|
||||
) {
|
||||
self.handle_ambient_pet_image_render_error(tui, err)?;
|
||||
}
|
||||
}
|
||||
if let Some(request) = self.chat_widget.pet_picker_preview_draw() {
|
||||
if let Err(err) = tui.draw_pet_picker_preview_image(Some(request)) {
|
||||
self.handle_pet_picker_preview_image_render_error(tui, err)?;
|
||||
}
|
||||
} else if self.chat_widget.should_clear_pet_picker_preview_image()
|
||||
&& let Err(err) = tui.draw_pet_picker_preview_image(/*request*/ None)
|
||||
{
|
||||
self.handle_pet_picker_preview_image_render_error(tui, err)?;
|
||||
}
|
||||
self.draw_pet_images(tui, screen_size, rendered_area)?;
|
||||
if self.chat_widget.external_editor_state() == ExternalEditorState::Requested {
|
||||
self.chat_widget
|
||||
.set_external_editor_state(ExternalEditorState::Active);
|
||||
@@ -1374,14 +1357,8 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare_frame_images(&mut self, tui: &tui::Tui) -> Option<crate::pets::ImageProtocol> {
|
||||
let tmux_kitty = tui.is_owned_screen() && tui.terminal.tmux_kitty_image_base.is_some();
|
||||
let protocol = self
|
||||
.chat_widget
|
||||
.pet_image_support()
|
||||
.protocol()
|
||||
.or(tmux_kitty.then_some(crate::pets::ImageProtocol::Kitty));
|
||||
let protocol = protocol.filter(|_| {
|
||||
fn prepare_frame_images(&mut self) -> Option<crate::pets::ImageProtocol> {
|
||||
let protocol = self.chat_widget.pet_image_support().protocol().filter(|_| {
|
||||
!self.app_server_target.uses_remote_workspace()
|
||||
&& crate::projectless::has_only_local_environments(&self.environment_manager)
|
||||
});
|
||||
@@ -1395,7 +1372,7 @@ impl App {
|
||||
if tui.is_owned_screen() {
|
||||
return self.render_owned_transcript(tui, screen_size);
|
||||
}
|
||||
self.prepare_frame_images(tui);
|
||||
self.prepare_frame_images();
|
||||
self.chat_widget
|
||||
.empty_state_animation
|
||||
.borrow_mut()
|
||||
|
||||
@@ -201,7 +201,7 @@ impl App {
|
||||
tui: &mut tui::Tui,
|
||||
screen_size: Size,
|
||||
) -> Result<Rect> {
|
||||
self.transcript_view.image_protocol = self.prepare_frame_images(tui);
|
||||
self.transcript_view.image_protocol = self.prepare_frame_images();
|
||||
let frames = tui.frame_requester();
|
||||
if self.conversation_frame.side_pane.is_some() {
|
||||
// Recompute our own inset on every frame, including the first narrow frame.
|
||||
@@ -581,8 +581,8 @@ impl App {
|
||||
|
||||
pub(super) fn refresh_owned_transcript(&mut self, tui: &mut tui::Tui) -> Result<()> {
|
||||
let size = tui.prepare_draw_size()?;
|
||||
self.render_owned_transcript(tui, size)?;
|
||||
Ok(())
|
||||
let rendered_area = self.render_owned_transcript(tui, size)?;
|
||||
self.draw_pet_images(tui, size, rendered_area)
|
||||
}
|
||||
|
||||
/// Keep modal input ownership while allowing selection, copying, and disclosure in the visible transcript.
|
||||
|
||||
@@ -3,6 +3,33 @@
|
||||
use super::*;
|
||||
|
||||
impl App {
|
||||
pub(super) fn draw_pet_images(
|
||||
&mut self,
|
||||
tui: &mut tui::Tui,
|
||||
screen_size: Size,
|
||||
rendered_area: Rect,
|
||||
) -> Result<()> {
|
||||
if self.chat_widget.ambient_pet_image_enabled() {
|
||||
let ambient_pet_area = self.conversation_area(screen_size.into());
|
||||
if let Err(err) = tui.draw_ambient_pet_image(
|
||||
self.chat_widget
|
||||
.ambient_pet_draw(ambient_pet_area, rendered_area.bottom()),
|
||||
) {
|
||||
self.handle_ambient_pet_image_render_error(tui, err)?;
|
||||
}
|
||||
}
|
||||
if let Some(request) = self.chat_widget.pet_picker_preview_draw() {
|
||||
if let Err(err) = tui.draw_pet_picker_preview_image(Some(request)) {
|
||||
self.handle_pet_picker_preview_image_render_error(tui, err)?;
|
||||
}
|
||||
} else if self.chat_widget.should_clear_pet_picker_preview_image()
|
||||
&& let Err(err) = tui.draw_pet_picker_preview_image(/*request*/ None)
|
||||
{
|
||||
self.handle_pet_picker_preview_image_render_error(tui, err)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn disable_ambient_pet_before_shutdown(&mut self, tui: &mut tui::Tui) -> Result<()> {
|
||||
self.chat_widget.disable_ambient_pet_for_session();
|
||||
if let Err(clear_err) = tui.clear_ambient_pet_image() {
|
||||
|
||||
@@ -138,7 +138,7 @@ impl ChatWidget {
|
||||
pub(crate) fn pet_picker_preview_draw(&self) -> Option<crate::pets::AmbientPetDraw> {
|
||||
self.bottom_pane
|
||||
.selected_index_for_active_view(crate::pets::PET_PICKER_VIEW_ID)?;
|
||||
let area = self.pet_picker_preview_state.area()?;
|
||||
let area = self.pet_picker_preview_state.take_area()?;
|
||||
let request = self
|
||||
.pet_picker_preview_pet
|
||||
.as_ref()?
|
||||
|
||||
+6
-6
@@ -1,13 +1,13 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/slash_commands.rs
|
||||
expression: "render_bottom_popup(&chat, width)"
|
||||
assertion_line: 3212
|
||||
expression: "render_bottom_popup(&chat, 40)"
|
||||
---
|
||||
Pets unavailable
|
||||
Pets are disabled in tmux. Terminal
|
||||
images don’t stay pane-local in tmux
|
||||
and can corrupt scrollback or move
|
||||
between panes. Run Codex outside
|
||||
tmux to use pets.
|
||||
Pets are unavailable in this tmux
|
||||
pane. Use fullscreen Codex with
|
||||
allow-passthrough all, or run Codex
|
||||
outside tmux.
|
||||
|
||||
|
||||
› 1. Close
|
||||
|
||||
@@ -3219,7 +3219,7 @@ async fn slash_pets_in_tmux_shows_notice_and_preserves_draft() {
|
||||
.map(|lines| lines_to_single_string(lines))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(rendered.contains("Pets are disabled in tmux."));
|
||||
assert!(rendered.contains("Pets are unavailable in this tmux pane."));
|
||||
assert!(rendered.contains("outside tmux"));
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Esc));
|
||||
@@ -3238,14 +3238,17 @@ async fn slash_pets_with_arg_on_unsupported_terminal_shows_notice_without_select
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
|
||||
assert!(chat.bottom_pane.has_active_view());
|
||||
assert!(render_bottom_popup(&chat, /*width*/ 80).contains("Pets are disabled in tmux."));
|
||||
assert!(
|
||||
render_bottom_popup(&chat, /*width*/ 80)
|
||||
.contains("Pets are unavailable in this tmux pane.")
|
||||
);
|
||||
let cells = drain_insert_history_transcript(&mut rx);
|
||||
let rendered = cells
|
||||
.iter()
|
||||
.map(|lines| lines_to_single_string(lines))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(rendered.contains("Pets are disabled in tmux."));
|
||||
assert!(rendered.contains("Pets are unavailable in this tmux pane."));
|
||||
assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));
|
||||
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
|
||||
|
||||
|
||||
@@ -401,7 +401,7 @@ where
|
||||
let cursor_style = frame.cursor_style;
|
||||
let mut images = std::mem::take(&mut frame.image_placements);
|
||||
let first_id = if let Some(base) = self.tmux_kitty_image_base {
|
||||
images.truncate(256);
|
||||
images.truncate(254); // The last two pane IDs belong to the pet and picker.
|
||||
base
|
||||
} else {
|
||||
crate::transcript_image::FIRST_IMAGE_ID
|
||||
@@ -542,6 +542,18 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// The pet draws after the text frame. Mark its placeholder cells so the next diff
|
||||
// restores the underlying text when the sprite moves or disappears.
|
||||
pub(crate) fn mark_pet_placeholders(&mut self, request: Option<&crate::pets::AmbientPetDraw>) {
|
||||
if self.tmux_kitty_image_base.is_some()
|
||||
&& let Some(sprite) = request
|
||||
{
|
||||
for pos in sprite.area().intersection(self.viewport_area).positions() {
|
||||
self.previous_buffer_mut()[pos].set_diff_option(CellDiffOption::AlwaysUpdate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Force the next draw pass to repaint the entire viewport after raw terminal
|
||||
/// operations move screen content outside ratatui's knowledge. Resetting the
|
||||
/// diff buffer alone would leave default-style spaces equal to their previous
|
||||
|
||||
@@ -123,6 +123,12 @@ pub(crate) struct AmbientPetDraw {
|
||||
pub(crate) sixel_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl AmbientPetDraw {
|
||||
pub(crate) fn area(&self) -> Rect {
|
||||
Rect::new(self.x, self.y, self.columns, self.rows)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AmbientPet {
|
||||
pet: Pet,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
@@ -13,7 +14,10 @@ use codex_terminal_detection::Multiplexer;
|
||||
use codex_terminal_detection::TerminalInfo;
|
||||
use codex_terminal_detection::TerminalName;
|
||||
use codex_terminal_detection::terminal_info;
|
||||
use crossterm::cursor::MoveTo;
|
||||
use crossterm::queue;
|
||||
use image::imageops::FilterType;
|
||||
use ratatui::layout::Rect;
|
||||
|
||||
use super::sixel;
|
||||
|
||||
@@ -40,6 +44,9 @@ impl PetImageSupport {
|
||||
pub(crate) fn protocol(self) -> Option<ImageProtocol> {
|
||||
match self {
|
||||
Self::Supported(protocol) => Some(protocol),
|
||||
Self::Unsupported(PetImageUnsupportedReason::Tmux) => {
|
||||
crate::tui::tmux_fullscreen_image_base().map(|_| ImageProtocol::Kitty)
|
||||
}
|
||||
Self::Unsupported(_) => None,
|
||||
}
|
||||
}
|
||||
@@ -47,7 +54,7 @@ impl PetImageSupport {
|
||||
pub(crate) fn unsupported_message(self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::Supported(_) => None,
|
||||
Self::Unsupported(reason) => Some(reason.message()),
|
||||
Self::Unsupported(reason) => self.protocol().is_none().then(|| reason.message()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +71,7 @@ impl PetImageUnsupportedReason {
|
||||
fn message(self) -> &'static str {
|
||||
match self {
|
||||
Self::Tmux => {
|
||||
"Pets are disabled in tmux. Terminal images don’t stay pane-local in tmux and can corrupt scrollback or move between panes. Run Codex outside tmux to use pets."
|
||||
"Pets are unavailable in this tmux pane. Use fullscreen Codex with allow-passthrough all, or run Codex outside tmux."
|
||||
}
|
||||
Self::Zellij => {
|
||||
"Pets are disabled in Zellij. Terminal images don’t stay reliably pane-local in Zellij. Run Codex outside Zellij to use pets."
|
||||
@@ -226,6 +233,32 @@ pub(crate) enum KittyPlacement {
|
||||
Virtual(u32),
|
||||
}
|
||||
|
||||
impl KittyPlacement {
|
||||
pub(crate) fn draw(
|
||||
self,
|
||||
writer: &mut impl Write,
|
||||
area: Rect,
|
||||
payload: &[u8],
|
||||
) -> std::io::Result<()> {
|
||||
queue!(writer, MoveTo(area.x, area.y))?;
|
||||
writer.write_all(payload)?;
|
||||
if let KittyPlacement::Virtual(id) = self {
|
||||
// tmux moves these cells with the pane. Kitty inherits subsequent columns in each row.
|
||||
let [_, r, g, b] = id.to_be_bytes();
|
||||
write!(writer, "\x1b[38;2;{r};{g};{b}m")?;
|
||||
let remaining_cells = "\u{10eeee}".repeat(usize::from(area.width.saturating_sub(1)));
|
||||
for (y, mark) in (area.y..area.bottom()).zip([
|
||||
'\u{305}', '\u{30d}', '\u{30e}', '\u{310}', '\u{312}', '\u{33d}',
|
||||
]) {
|
||||
queue!(writer, MoveTo(area.x, y))?;
|
||||
write!(writer, "\u{10eeee}{mark}{remaining_cells}")?;
|
||||
}
|
||||
writer.write_all(b"\x1b[39m")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kitty_transmit_png_with_id(
|
||||
path: &Path,
|
||||
columns: u16,
|
||||
|
||||
@@ -131,7 +131,9 @@ pub(crate) fn render_ambient_pet_image(
|
||||
state: &mut PetImageRenderState,
|
||||
request: Option<AmbientPetDraw>,
|
||||
) -> std::result::Result<(), PetImageRenderError> {
|
||||
render_pet_image(writer, state, /*image_id*/ 0xC0DE, request)
|
||||
render_pet_image(
|
||||
writer, state, /*image_id*/ 0xC0DE, /*pane_slot*/ 254, request,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn render_pet_picker_preview_image(
|
||||
@@ -139,29 +141,31 @@ pub(crate) fn render_pet_picker_preview_image(
|
||||
state: &mut PetImageRenderState,
|
||||
request: Option<AmbientPetDraw>,
|
||||
) -> std::result::Result<(), PetImageRenderError> {
|
||||
render_pet_image(writer, state, /*image_id*/ 0xC0DF, request)
|
||||
render_pet_image(
|
||||
writer, state, /*image_id*/ 0xC0DF, /*pane_slot*/ 255, request,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct PetImageRenderState {
|
||||
last_sixel_clear_area: Option<SixelClearArea>,
|
||||
last_protocol: Option<image_protocol::ImageProtocol>,
|
||||
last_kitty_id: Option<u32>,
|
||||
}
|
||||
|
||||
fn render_pet_image(
|
||||
writer: &mut impl Write,
|
||||
state: &mut PetImageRenderState,
|
||||
image_id: u32,
|
||||
pane_slot: u32,
|
||||
request: Option<AmbientPetDraw>,
|
||||
) -> std::result::Result<(), PetImageRenderError> {
|
||||
use crossterm::cursor::MoveTo;
|
||||
use crossterm::cursor::RestorePosition;
|
||||
use crossterm::cursor::SavePosition;
|
||||
use crossterm::queue;
|
||||
use image_protocol::ImageProtocol;
|
||||
|
||||
let Some(request) = request else {
|
||||
if state.last_protocol.take().is_some_and(is_kitty_protocol) {
|
||||
if let Some(image_id) = state.last_kitty_id.take() {
|
||||
write!(writer, "{}", image_protocol::kitty_delete_image(image_id))?;
|
||||
}
|
||||
if let Some(area) = state.last_sixel_clear_area.take() {
|
||||
@@ -173,40 +177,41 @@ fn render_pet_image(
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if state.last_protocol.take().is_some_and(is_kitty_protocol)
|
||||
|| is_kitty_protocol(request.protocol)
|
||||
{
|
||||
write!(writer, "{}", image_protocol::kitty_delete_image(image_id))?;
|
||||
let pane_base = crate::tui::tmux_fullscreen_image_base();
|
||||
let image_id = pane_base.map_or(image_id, |base| base + pane_slot);
|
||||
let placement = if pane_base.is_some() {
|
||||
KittyPlacement::Virtual(image_id)
|
||||
} else {
|
||||
KittyPlacement::Cursor(image_id)
|
||||
};
|
||||
let current_id = is_kitty_protocol(request.protocol).then_some(image_id);
|
||||
if let Some(id) = std::mem::replace(&mut state.last_kitty_id, current_id).or(current_id) {
|
||||
write!(writer, "{}", image_protocol::kitty_delete_image(id))?;
|
||||
}
|
||||
state.last_protocol = Some(request.protocol);
|
||||
|
||||
let payload = match request.protocol {
|
||||
ImageProtocol::Kitty => AmbientPetPayload::Text(
|
||||
image_protocol::kitty_transmit_png_with_id(
|
||||
&request.frame,
|
||||
request.columns,
|
||||
request.rows,
|
||||
KittyPlacement::Cursor(image_id),
|
||||
)
|
||||
.map_err(PetImageRenderError::Asset)?,
|
||||
),
|
||||
ImageProtocol::KittyLocalFile => AmbientPetPayload::Text(
|
||||
image_protocol::kitty_transmit_png_file_with_id(
|
||||
&request.frame,
|
||||
request.columns,
|
||||
request.rows,
|
||||
KittyPlacement::Cursor(image_id),
|
||||
)
|
||||
.map_err(PetImageRenderError::Asset)?,
|
||||
),
|
||||
ImageProtocol::Kitty => image_protocol::kitty_transmit_png_with_id(
|
||||
&request.frame,
|
||||
request.columns,
|
||||
request.rows,
|
||||
placement,
|
||||
)
|
||||
.map_err(PetImageRenderError::Asset)?
|
||||
.into_bytes(),
|
||||
ImageProtocol::KittyLocalFile => image_protocol::kitty_transmit_png_file_with_id(
|
||||
&request.frame,
|
||||
request.columns,
|
||||
request.rows,
|
||||
placement,
|
||||
)
|
||||
.map_err(PetImageRenderError::Asset)?
|
||||
.into_bytes(),
|
||||
ImageProtocol::Sixel => {
|
||||
let path =
|
||||
image_protocol::sixel_frame(&request.frame, &request.sixel_dir, request.height_px)
|
||||
.map_err(PetImageRenderError::Asset)?;
|
||||
let sixel = std::fs::read(&path)
|
||||
std::fs::read(&path)
|
||||
.with_context(|| format!("read {}", path.display()))
|
||||
.map_err(PetImageRenderError::Asset)?;
|
||||
AmbientPetPayload::Bytes(sixel)
|
||||
.map_err(PetImageRenderError::Asset)?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -225,21 +230,12 @@ fn render_pet_image(
|
||||
clear_sixel_area(writer, area)?;
|
||||
state.last_sixel_clear_area = Some(area);
|
||||
}
|
||||
queue!(writer, MoveTo(request.x, request.y))?;
|
||||
match payload {
|
||||
AmbientPetPayload::Text(payload) => write!(writer, "{payload}")?,
|
||||
AmbientPetPayload::Bytes(payload) => writer.write_all(&payload)?,
|
||||
}
|
||||
placement.draw(writer, request.area(), &payload)?;
|
||||
queue!(writer, RestorePosition)?;
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
enum AmbientPetPayload {
|
||||
Text(String),
|
||||
Bytes(Vec<u8>),
|
||||
}
|
||||
|
||||
fn is_kitty_protocol(protocol: image_protocol::ImageProtocol) -> bool {
|
||||
matches!(
|
||||
protocol,
|
||||
@@ -485,7 +481,7 @@ mod tests {
|
||||
|
||||
let mut writer = FailingWriter;
|
||||
let mut state = PetImageRenderState {
|
||||
last_protocol: Some(ImageProtocol::Kitty),
|
||||
last_kitty_id: Some(0xC0DE),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
@@ -67,8 +67,11 @@ impl PetPickerPreviewState {
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn area(&self) -> Option<Rect> {
|
||||
self.inner.lock().ok().and_then(|inner| inner.last_area)
|
||||
pub(crate) fn take_area(&self) -> Option<Rect> {
|
||||
self.inner
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|mut inner| inner.last_area.take())
|
||||
}
|
||||
|
||||
fn update(&self, f: impl FnOnce(&mut PetPickerPreviewInner)) {
|
||||
|
||||
@@ -11,7 +11,6 @@ use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use crossterm::cursor::MoveTo;
|
||||
use crossterm::cursor::RestorePosition;
|
||||
use crossterm::cursor::SavePosition;
|
||||
use crossterm::queue;
|
||||
@@ -186,21 +185,8 @@ impl TranscriptImage {
|
||||
let Ok(payload) = payload else {
|
||||
return Ok(None);
|
||||
};
|
||||
queue!(writer, SavePosition, MoveTo(area.x, area.y))?;
|
||||
writer.write_all(&payload)?;
|
||||
if let KittyPlacement::Virtual(id) = placement {
|
||||
// tmux moves these cells with the pane. Kitty inherits subsequent columns in each row.
|
||||
let [_, r, g, b] = id.to_be_bytes();
|
||||
write!(writer, "\x1b[38;2;{r};{g};{b}m")?;
|
||||
let remaining_cells = "\u{10eeee}".repeat(usize::from(area.width.saturating_sub(1)));
|
||||
for (y, mark) in (area.y..area.bottom()).zip([
|
||||
'\u{305}', '\u{30d}', '\u{30e}', '\u{310}', '\u{312}', '\u{33d}',
|
||||
]) {
|
||||
queue!(writer, MoveTo(area.x, y))?;
|
||||
write!(writer, "\u{10eeee}{mark}{remaining_cells}")?;
|
||||
}
|
||||
writer.write_all(b"\x1b[39m")?;
|
||||
}
|
||||
queue!(writer, SavePosition)?;
|
||||
placement.draw(writer, area, &payload)?;
|
||||
queue!(writer, RestorePosition)?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
+26
-3
@@ -11,6 +11,7 @@ use std::panic;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -697,6 +698,14 @@ impl MouseCapture {
|
||||
}
|
||||
}
|
||||
|
||||
// Zero denotes scrollback mode or a pane without Kitty passthrough.
|
||||
static TMUX_FULLSCREEN_IMAGE_BASE: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
pub(crate) fn tmux_fullscreen_image_base() -> Option<u32> {
|
||||
std::num::NonZeroU32::new(TMUX_FULLSCREEN_IMAGE_BASE.load(Ordering::Relaxed))
|
||||
.map(std::num::NonZeroU32::get)
|
||||
}
|
||||
|
||||
pub struct Tui {
|
||||
pub(crate) link_hover: link_pointer::LinkHover,
|
||||
frame_requester: FrameRequester,
|
||||
@@ -815,10 +824,22 @@ impl Tui {
|
||||
self.alt_screen_enabled = enabled;
|
||||
}
|
||||
|
||||
fn record_owned_screen(&mut self, owned: bool) {
|
||||
self.owned_screen = owned;
|
||||
TMUX_FULLSCREEN_IMAGE_BASE.store(
|
||||
if owned {
|
||||
self.terminal.tmux_kitty_image_base.unwrap_or_default()
|
||||
} else {
|
||||
0
|
||||
},
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
}
|
||||
|
||||
/// Defer a fresh fullscreen launch until its first synchronized frame is ready.
|
||||
pub(crate) fn prepare_owned_screen(&mut self, owned: bool) -> Result<()> {
|
||||
if owned && self.alt_screen_enabled && !self.is_alt_screen_active() {
|
||||
self.owned_screen = true;
|
||||
self.record_owned_screen(/*owned*/ true);
|
||||
self.frame_requester.schedule_frame();
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -847,11 +868,11 @@ impl Tui {
|
||||
self.leave_alt_screen_for_handoff()
|
||||
};
|
||||
if let Err(error) = transition {
|
||||
self.owned_screen = self.is_alt_screen_active();
|
||||
self.record_owned_screen(self.is_alt_screen_active());
|
||||
self.terminal.invalidate_viewport();
|
||||
return Err(error);
|
||||
}
|
||||
self.owned_screen = owned;
|
||||
self.record_owned_screen(owned);
|
||||
self.frame_requester.schedule_frame();
|
||||
Ok(())
|
||||
}
|
||||
@@ -1428,6 +1449,7 @@ impl Tui {
|
||||
}
|
||||
|
||||
let terminal = &mut self.terminal;
|
||||
terminal.mark_pet_placeholders(request.as_ref());
|
||||
let state = &mut self.ambient_pet_image_state;
|
||||
stdout().sync_update(|_| {
|
||||
match crate::pets::render_ambient_pet_image(terminal.backend_mut(), state, request) {
|
||||
@@ -1447,6 +1469,7 @@ impl Tui {
|
||||
}
|
||||
|
||||
let terminal = &mut self.terminal;
|
||||
terminal.mark_pet_placeholders(request.as_ref());
|
||||
let state = &mut self.pet_picker_preview_image_state;
|
||||
stdout().sync_update(|_| {
|
||||
match crate::pets::render_pet_picker_preview_image(
|
||||
|
||||
@@ -205,4 +205,15 @@ async fn owned_screen_lifecycle_child() {
|
||||
tui.set_owned_screen(/*owned*/ false)
|
||||
.expect("release session screen");
|
||||
assert_eq!(tui.terminal.viewport_area, inline);
|
||||
|
||||
// A fresh fullscreen launch defers terminal entry until its first frame.
|
||||
tui.terminal.tmux_kitty_image_base = Some(0x80_0100);
|
||||
let support =
|
||||
crate::pets::PetImageSupport::Unsupported(crate::pets::PetImageUnsupportedReason::Tmux);
|
||||
tui.prepare_owned_screen(/*owned*/ true)
|
||||
.expect("prepare fullscreen");
|
||||
assert_eq!(support.protocol(), Some(crate::pets::ImageProtocol::Kitty));
|
||||
tui.set_owned_screen(/*owned*/ false)
|
||||
.expect("return to scrollback");
|
||||
assert_eq!(support.protocol(), None);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user