diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-15 02:32:52 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-15 02:32:52 +0300 |
| commit | 88370e1516182ab519d22a12fa82d5780a40fee6 (patch) | |
| tree | b07be9b77abe14cdd3c1ff0e9acc1268729d8723 /src/app/api | |
| parent | 5d24d0d214d05858e344a9e15a63856dc1328eae (diff) | |
feat: add isolated pane graphics streaming
Diffstat (limited to 'src/app/api')
| -rw-r--r-- | src/app/api/pane_graphics.rs | 702 | ||||
| -rw-r--r-- | src/app/api/plugins/mod.rs | 9 |
2 files changed, 710 insertions, 1 deletions
diff --git a/src/app/api/pane_graphics.rs b/src/app/api/pane_graphics.rs new file mode 100644 index 00000000..7851f69a --- /dev/null +++ b/src/app/api/pane_graphics.rs @@ -0,0 +1,702 @@ +use base64::Engine; + +use crate::api::schema::{ + PaneGraphicsClearParams, PaneGraphicsSetParams, PaneGraphicsStreamParams, ResponseResult, + PANE_GRAPHICS_SET_MAX_BYTES, PANE_GRAPHICS_STREAM_MAX_BYTES, +}; +use crate::app::state::PaneGraphicsLayer; +use crate::app::App; +use crate::layout::PaneId; + +use super::responses::{encode_error, encode_success}; + +impl App { + pub(super) fn handle_pane_graphics_info( + &mut self, + id: String, + target: crate::api::schema::PaneTarget, + ) -> String { + if let Err(response) = require_pane_graphics_enabled(self, &id) { + return response; + } + if self.parse_pane_id(&target.pane_id).is_none() { + return pane_not_found(id, &target.pane_id); + } + if !self.state.host_cell_size.is_known() { + return encode_error(id, "cell_size_unavailable", "host cell size is unavailable"); + } + encode_success( + id, + ResponseResult::PaneGraphicsInfo { + cell_width_px: self.state.host_cell_size.width_px, + cell_height_px: self.state.host_cell_size.height_px, + }, + ) + } + + pub(super) fn handle_pane_graphics_set( + &mut self, + id: String, + params: PaneGraphicsSetParams, + ) -> String { + if let Err(response) = require_pane_graphics_enabled(self, &id) { + return response; + } + let Some((_ws_idx, pane_id)) = self.parse_pane_id(¶ms.pane_id) else { + return pane_not_found(id, ¶ms.pane_id); + }; + if self.state.pane_graphics_streams.contains_key(&pane_id) { + return encode_error( + id, + "stream_conflict", + "pane already has an active graphics stream", + ); + } + self.set_pane_graphics_layer(id, pane_id, params) + } + + fn set_pane_graphics_layer( + &mut self, + id: String, + pane_id: PaneId, + params: PaneGraphicsSetParams, + ) -> String { + if params.image_width == 0 || params.image_height == 0 { + return encode_error( + id, + "invalid_image", + "image_width and image_height must be greater than zero", + ); + } + let max_bytes = if params.data.is_some() { + PANE_GRAPHICS_STREAM_MAX_BYTES + } else { + PANE_GRAPHICS_SET_MAX_BYTES + }; + let data = match params.data { + Some(data) => data, + None => match base64::engine::general_purpose::STANDARD.decode(params.data_base64) { + Ok(data) => data, + Err(_) => { + return encode_error(id, "invalid_image", "data_base64 is not valid base64"); + } + }, + }; + if data.is_empty() { + return encode_error(id, "invalid_image", "image data must not be empty"); + } + if data.len() > max_bytes { + return encode_error(id, "image_too_large", "image data is too large"); + } + match pane_graphics_expected_data_len( + params.format, + params.image_width, + params.image_height, + ) { + Ok(Some(expected_len)) if data.len() != expected_len => { + return encode_error( + id, + "invalid_image", + "image data length does not match format and dimensions", + ); + } + Ok(_) => {} + Err(()) => { + return encode_error(id, "invalid_image", "image dimensions are too large"); + } + } + + let layer = PaneGraphicsLayer::new( + params.format, + params.image_width, + params.image_height, + data, + params.placement, + ); + self.state.pane_graphics_layers.insert(pane_id, layer); + self.state.pane_graphics_revision = self.state.pane_graphics_revision.wrapping_add(1); + + encode_success(id, ResponseResult::Ok {}) + } + + pub(super) fn handle_pane_graphics_clear( + &mut self, + id: String, + params: PaneGraphicsClearParams, + ) -> String { + if let Err(response) = require_pane_graphics_enabled(self, &id) { + return response; + } + let Some((_ws_idx, pane_id)) = self.parse_pane_id(¶ms.pane_id) else { + return pane_not_found(id, ¶ms.pane_id); + }; + if self.state.pane_graphics_streams.contains_key(&pane_id) { + return encode_error( + id, + "stream_conflict", + "pane already has an active graphics stream", + ); + } + if self.state.pane_graphics_layers.remove(&pane_id).is_some() { + self.state.pane_graphics_revision = self.state.pane_graphics_revision.wrapping_add(1); + } + + encode_success(id, ResponseResult::Ok {}) + } + + pub(super) fn handle_pane_graphics_stream_set( + &mut self, + id: String, + params: PaneGraphicsSetParams, + ) -> String { + if let Err(response) = require_pane_graphics_enabled(self, &id) { + return response; + } + let Some((_ws_idx, pane_id)) = self.parse_pane_id(¶ms.pane_id) else { + return pane_not_found(id, ¶ms.pane_id); + }; + match self.state.pane_graphics_streams.get(&pane_id) { + Some(owner) if owner == ¶ms.owner => {} + Some(_) => { + return encode_error( + id, + "stream_conflict", + "pane graphics stream owner does not match active stream", + ); + } + None => return encode_error(id, "stream_closed", "pane graphics stream is not active"), + } + self.set_pane_graphics_layer(id, pane_id, params) + } + + pub(super) fn handle_pane_graphics_stream_open( + &mut self, + id: String, + params: PaneGraphicsStreamParams, + ) -> String { + if let Err(response) = require_pane_graphics_enabled(self, &id) { + return response; + } + if params.owner.is_empty() { + return encode_error( + id, + "invalid_stream", + "pane graphics stream owner is required", + ); + } + let Some((_ws_idx, pane_id)) = self.parse_pane_id(¶ms.pane_id) else { + return pane_not_found(id, ¶ms.pane_id); + }; + if self.state.pane_graphics_streams.contains_key(&pane_id) { + return encode_error( + id, + "stream_conflict", + "pane already has an active graphics stream", + ); + } + self.state + .pane_graphics_streams + .insert(pane_id, params.owner); + self.state.pane_graphics_layers.remove(&pane_id); + self.state.pane_graphics_revision = self.state.pane_graphics_revision.wrapping_add(1); + + encode_success(id, ResponseResult::Ok {}) + } + + pub(super) fn handle_pane_graphics_stream_close( + &mut self, + id: String, + params: PaneGraphicsStreamParams, + ) -> String { + let Some((_ws_idx, pane_id)) = self.parse_pane_id(¶ms.pane_id) else { + return pane_not_found(id, ¶ms.pane_id); + }; + if self + .state + .pane_graphics_streams + .get(&pane_id) + .is_some_and(|owner| owner == ¶ms.owner) + { + self.state.pane_graphics_streams.remove(&pane_id); + self.state.pane_graphics_layers.remove(&pane_id); + self.state.pane_graphics_revision = self.state.pane_graphics_revision.wrapping_add(1); + } + + encode_success(id, ResponseResult::Ok {}) + } +} + +fn require_pane_graphics_enabled(app: &App, id: &str) -> Result<(), String> { + if app.state.kitty_graphics_enabled { + return Ok(()); + } + Err(encode_error( + id.to_owned(), + "feature_disabled", + "pane graphics require experimental.kitty_graphics", + )) +} + +fn pane_graphics_expected_data_len( + format: crate::api::schema::PaneGraphicsFormat, + image_width: u32, + image_height: u32, +) -> Result<Option<usize>, ()> { + let bytes_per_pixel = match format { + crate::api::schema::PaneGraphicsFormat::Png => return Ok(None), + crate::api::schema::PaneGraphicsFormat::Rgb => 3_u64, + crate::api::schema::PaneGraphicsFormat::Rgba => 4_u64, + }; + let pixels = u64::from(image_width) + .checked_mul(u64::from(image_height)) + .ok_or(())?; + pixels + .checked_mul(bytes_per_pixel) + .and_then(|bytes| usize::try_from(bytes).ok()) + .map(Some) + .ok_or(()) +} + +fn pane_not_found(id: String, pane_id: &str) -> String { + encode_error(id, "pane_not_found", format!("pane {pane_id} not found")) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::schema::{ErrorResponse, SuccessResponse}; + use crate::config::Config; + use crate::workspace::Workspace; + + fn app_with_test_workspace() -> (App, String) { + let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut app = App::new( + &Config::default(), + true, + None, + api_rx, + crate::api::EventHub::default(), + ); + app.state.workspaces = vec![Workspace::test_new("pane-graphics")]; + app.state.ensure_test_terminals(); + let pane_id = app.state.workspaces[0].tabs[0].root_pane; + let public_pane_id = app.public_pane_id(0, pane_id).unwrap(); + (app, public_pane_id) + } + + #[test] + fn api_pane_graphics_info_is_explicit_and_side_effect_free() { + let (mut app, public_pane_id) = app_with_test_workspace(); + let request = |pane_id: String| crate::api::schema::Request { + id: "graphics-info".into(), + method: crate::api::schema::Method::PaneGraphicsInfo(crate::api::schema::PaneTarget { + pane_id, + }), + }; + + let response = app.handle_api_request(request(public_pane_id.clone())); + let error: ErrorResponse = serde_json::from_str(&response).unwrap(); + assert_eq!(error.error.code, "feature_disabled"); + + app.state.kitty_graphics_enabled = true; + app.state.host_cell_size = crate::kitty_graphics::HostCellSize { + width_px: 11, + height_px: 22, + }; + let response = app.handle_api_request(request(public_pane_id)); + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + assert!(matches!( + success.result, + ResponseResult::PaneGraphicsInfo { + cell_width_px: 11, + cell_height_px: 22, + } + )); + assert!(app.state.pane_graphics_layers.is_empty()); + assert!(app.state.pane_graphics_streams.is_empty()); + } + + #[test] + fn api_pane_graphics_set_and_clear_updates_runtime_layers() { + let (mut app, public_pane_id) = app_with_test_workspace(); + app.state.kitty_graphics_enabled = true; + let (_, pane_id) = app.parse_pane_id(&public_pane_id).unwrap(); + let data = base64::engine::general_purpose::STANDARD.encode([1_u8, 2, 3, 4]); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "graphics-set".into(), + method: crate::api::schema::Method::PaneGraphicsSet(PaneGraphicsSetParams { + pane_id: public_pane_id.clone(), + owner: String::new(), + format: crate::api::schema::PaneGraphicsFormat::Rgba, + image_width: 1, + image_height: 1, + data: None, + data_base64: data, + placement: crate::api::schema::PaneGraphicsPlacementParams { + grid_cols: 10, + grid_rows: 4, + ..Default::default() + }, + }), + }); + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + assert!(matches!(success.result, ResponseResult::Ok {})); + + let layer = app + .state + .pane_graphics_layers + .get(&pane_id) + .expect("graphics layer stored"); + assert_eq!(layer.image_width, 1); + assert_eq!(layer.image_height, 1); + assert_eq!(layer.render.grid_cols, 10); + assert_eq!(layer.render.grid_rows, 4); + assert!(!app.state.session_dirty); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "graphics-clear".into(), + method: crate::api::schema::Method::PaneGraphicsClear(PaneGraphicsClearParams { + pane_id: public_pane_id, + }), + }); + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + assert!(matches!(success.result, ResponseResult::Ok {})); + assert!(!app.state.pane_graphics_layers.contains_key(&pane_id)); + } + + #[test] + fn api_pane_graphics_rejects_when_kitty_graphics_disabled() { + let (mut app, public_pane_id) = app_with_test_workspace(); + let (_, pane_id) = app.parse_pane_id(&public_pane_id).unwrap(); + let data = base64::engine::general_purpose::STANDARD.encode([1_u8, 2, 3, 4]); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "graphics-set".into(), + method: crate::api::schema::Method::PaneGraphicsSet(PaneGraphicsSetParams { + pane_id: public_pane_id.clone(), + owner: String::new(), + format: crate::api::schema::PaneGraphicsFormat::Rgba, + image_width: 1, + image_height: 1, + data: None, + data_base64: data, + placement: crate::api::schema::PaneGraphicsPlacementParams::default(), + }), + }); + let error: ErrorResponse = serde_json::from_str(&response).unwrap(); + assert_eq!(error.error.code, "feature_disabled"); + assert!(!app.state.pane_graphics_layers.contains_key(&pane_id)); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "graphics-clear".into(), + method: crate::api::schema::Method::PaneGraphicsClear(PaneGraphicsClearParams { + pane_id: public_pane_id, + }), + }); + let error: ErrorResponse = serde_json::from_str(&response).unwrap(); + assert_eq!(error.error.code, "feature_disabled"); + assert!(!app.state.pane_graphics_layers.contains_key(&pane_id)); + } + + #[test] + fn api_pane_graphics_stream_claim_uses_resolved_pane_identity() { + let (mut app, public_pane_id) = app_with_test_workspace(); + app.state.kitty_graphics_enabled = true; + let (_, pane_id) = app.parse_pane_id(&public_pane_id).unwrap(); + let alias = format!("{public_pane_id}:alias"); + app.state + .public_pane_id_aliases + .insert(alias.clone(), pane_id); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "graphics-stream-open".into(), + method: crate::api::schema::Method::PaneGraphicsStreamOpen( + crate::api::schema::PaneGraphicsStreamParams { + pane_id: public_pane_id, + owner: "stream-1".into(), + }, + ), + }); + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + assert!(matches!(success.result, ResponseResult::Ok {})); + assert_eq!( + app.state.pane_graphics_streams.get(&pane_id), + Some(&"stream-1".to_string()) + ); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "graphics-stream-open-alias".into(), + method: crate::api::schema::Method::PaneGraphicsStreamOpen( + crate::api::schema::PaneGraphicsStreamParams { + pane_id: alias.clone(), + owner: "stream-2".into(), + }, + ), + }); + let error: ErrorResponse = serde_json::from_str(&response).unwrap(); + assert_eq!(error.error.code, "stream_conflict"); + assert_eq!( + app.state.pane_graphics_streams.get(&pane_id), + Some(&"stream-1".to_string()) + ); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "graphics-stream-close-wrong-owner".into(), + method: crate::api::schema::Method::PaneGraphicsStreamClose( + crate::api::schema::PaneGraphicsStreamParams { + pane_id: alias.clone(), + owner: "stream-2".into(), + }, + ), + }); + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + assert!(matches!(success.result, ResponseResult::Ok {})); + assert_eq!( + app.state.pane_graphics_streams.get(&pane_id), + Some(&"stream-1".to_string()) + ); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "graphics-stream-close-alias".into(), + method: crate::api::schema::Method::PaneGraphicsStreamClose( + crate::api::schema::PaneGraphicsStreamParams { + pane_id: alias, + owner: "stream-1".into(), + }, + ), + }); + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + assert!(matches!(success.result, ResponseResult::Ok {})); + assert!(!app.state.pane_graphics_streams.contains_key(&pane_id)); + } + + #[test] + fn api_pane_graphics_stream_owns_layer_while_active() { + let (mut app, public_pane_id) = app_with_test_workspace(); + app.state.kitty_graphics_enabled = true; + let (_, pane_id) = app.parse_pane_id(&public_pane_id).unwrap(); + let public_data = base64::engine::general_purpose::STANDARD.encode([1_u8, 2, 3, 4]); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "graphics-stream-open".into(), + method: crate::api::schema::Method::PaneGraphicsStreamOpen( + crate::api::schema::PaneGraphicsStreamParams { + pane_id: public_pane_id.clone(), + owner: "stream-1".into(), + }, + ), + }); + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + assert!(matches!(success.result, ResponseResult::Ok {})); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "graphics-set-public".into(), + method: crate::api::schema::Method::PaneGraphicsSet(PaneGraphicsSetParams { + pane_id: public_pane_id.clone(), + owner: String::new(), + format: crate::api::schema::PaneGraphicsFormat::Rgba, + image_width: 1, + image_height: 1, + data: None, + data_base64: public_data.clone(), + placement: crate::api::schema::PaneGraphicsPlacementParams::default(), + }), + }); + let error: ErrorResponse = serde_json::from_str(&response).unwrap(); + assert_eq!(error.error.code, "stream_conflict"); + assert!(!app.state.pane_graphics_layers.contains_key(&pane_id)); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "graphics-set-stream".into(), + method: crate::api::schema::Method::PaneGraphicsStreamSet(PaneGraphicsSetParams { + pane_id: public_pane_id.clone(), + owner: "stream-1".into(), + format: crate::api::schema::PaneGraphicsFormat::Rgba, + image_width: 1, + image_height: 1, + data: None, + data_base64: public_data, + placement: crate::api::schema::PaneGraphicsPlacementParams::default(), + }), + }); + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + assert!(matches!(success.result, ResponseResult::Ok {})); + assert!(app.state.pane_graphics_layers.contains_key(&pane_id)); + + let wrong_owner_data = base64::engine::general_purpose::STANDARD.encode([5_u8, 6, 7, 8]); + let response = app.handle_api_request(crate::api::schema::Request { + id: "graphics-set-stream-wrong-owner".into(), + method: crate::api::schema::Method::PaneGraphicsStreamSet(PaneGraphicsSetParams { + pane_id: public_pane_id.clone(), + owner: "stream-2".into(), + format: crate::api::schema::PaneGraphicsFormat::Rgba, + image_width: 1, + image_height: 1, + data: None, + data_base64: wrong_owner_data, + placement: crate::api::schema::PaneGraphicsPlacementParams::default(), + }), + }); + let error: ErrorResponse = serde_json::from_str(&response).unwrap(); + assert_eq!(error.error.code, "stream_conflict"); + assert_eq!( + app.state.pane_graphics_layers.get(&pane_id).unwrap().data, + vec![1_u8, 2, 3, 4] + ); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "graphics-clear-public".into(), + method: crate::api::schema::Method::PaneGraphicsClear(PaneGraphicsClearParams { + pane_id: public_pane_id.clone(), + }), + }); + let error: ErrorResponse = serde_json::from_str(&response).unwrap(); + assert_eq!(error.error.code, "stream_conflict"); + assert!(app.state.pane_graphics_layers.contains_key(&pane_id)); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "graphics-stream-close".into(), + method: crate::api::schema::Method::PaneGraphicsStreamClose( + crate::api::schema::PaneGraphicsStreamParams { + pane_id: public_pane_id, + owner: "stream-1".into(), + }, + ), + }); + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + assert!(matches!(success.result, ResponseResult::Ok {})); + assert!(!app.state.pane_graphics_layers.contains_key(&pane_id)); + } + + #[test] + fn api_pane_graphics_rejects_overflowing_raw_dimensions() { + for format in [ + crate::api::schema::PaneGraphicsFormat::Rgb, + crate::api::schema::PaneGraphicsFormat::Rgba, + ] { + let (mut app, public_pane_id) = app_with_test_workspace(); + app.state.kitty_graphics_enabled = true; + let (_, pane_id) = app.parse_pane_id(&public_pane_id).unwrap(); + + let response = app.handle_api_request(crate::api::schema::Request { + id: "graphics-overflow".into(), + method: crate::api::schema::Method::PaneGraphicsSet(PaneGraphicsSetParams { + pane_id: public_pane_id, + owner: String::new(), + format, + image_width: u32::MAX, + image_height: u32::MAX, + data: None, + data_base64: base64::engine::general_purpose::STANDARD.encode([1_u8]), + placement: crate::api::schema::PaneGraphicsPlacementParams::default(), + }), + }); + + let error: ErrorResponse = serde_json::from_str(&response).unwrap(); + assert_eq!(error.error.code, "invalid_image"); + assert!(!app.state.pane_graphics_layers.contains_key(&pane_id)); + } + } + + #[test] + fn api_pane_graphics_validates_exact_raw_lengths() { + for (format, exact_len) in [ + (crate::api::schema::PaneGraphicsFormat::Rgb, 6_usize), + (crate::api::schema::PaneGraphicsFormat::Rgba, 8_usize), + ] { + for data_len in [exact_len, exact_len - 1] { + let (mut app, public_pane_id) = app_with_test_workspace(); + app.state.kitty_graphics_enabled = true; + let (_, pane_id) = app.parse_pane_id(&public_pane_id).unwrap(); + let response = app.handle_api_request(crate::api::schema::Request { + id: "graphics-raw-length".into(), + method: crate::api::schema::Method::PaneGraphicsSet(PaneGraphicsSetParams { + pane_id: public_pane_id, + owner: String::new(), + format, + image_width: 2, + image_height: 1, + data: None, + data_base64: base64::engine::general_purpose::STANDARD + .encode(vec![1_u8; data_len]), + placement: crate::api::schema::PaneGraphicsPlacementParams::default(), + }), + }); + + if data_len == exact_len { + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + assert!(matches!(success.result, ResponseResult::Ok {})); + assert!(app.state.pane_graphics_layers.contains_key(&pane_id)); + } else { + let error: ErrorResponse = serde_json::from_str(&response).unwrap(); + assert_eq!(error.error.code, "invalid_image"); + assert!(!app.state.pane_graphics_layers.contains_key(&pane_id)); + } + } + } + } + + #[test] + fn api_pane_graphics_uses_transport_specific_limits() { + let (mut app, public_pane_id) = app_with_test_workspace(); + app.state.kitty_graphics_enabled = true; + let (_, pane_id) = app.parse_pane_id(&public_pane_id).unwrap(); + let response = app.handle_api_request(crate::api::schema::Request { + id: "graphics-public-too-large".into(), + method: crate::api::schema::Method::PaneGraphicsSet(PaneGraphicsSetParams { + pane_id: public_pane_id, + owner: String::new(), + format: crate::api::schema::PaneGraphicsFormat::Png, + image_width: 1, + image_height: 1, + data: None, + data_base64: base64::engine::general_purpose::STANDARD.encode(vec![ + 1_u8; + PANE_GRAPHICS_SET_MAX_BYTES + + 1 + ]), + placement: crate::api::schema::PaneGraphicsPlacementParams::default(), + }), + }); + + let error: ErrorResponse = serde_json::from_str(&response).unwrap(); + assert_eq!(error.error.code, "image_too_large"); + assert!(!app.state.pane_graphics_layers.contains_key(&pane_id)); + } + + #[test] + fn api_pane_graphics_stream_methods_require_kitty_graphics() { + let (mut app, public_pane_id) = app_with_test_workspace(); + let (_, pane_id) = app.parse_pane_id(&public_pane_id).unwrap(); + let open = app.handle_api_request(crate::api::schema::Request { + id: "graphics-stream-open".into(), + method: crate::api::schema::Method::PaneGraphicsStreamOpen( + crate::api::schema::PaneGraphicsStreamParams { + pane_id: public_pane_id.clone(), + owner: "stream-1".into(), + }, + ), + }); + let error: ErrorResponse = serde_json::from_str(&open).unwrap(); + assert_eq!(error.error.code, "feature_disabled"); + + let set = app.handle_api_request(crate::api::schema::Request { + id: "graphics-stream-set".into(), + method: crate::api::schema::Method::PaneGraphicsStreamSet(PaneGraphicsSetParams { + pane_id: public_pane_id, + owner: "stream-1".into(), + format: crate::api::schema::PaneGraphicsFormat::Png, + image_width: 1, + image_height: 1, + data: Some(vec![1_u8]), + data_base64: String::new(), + placement: crate::api::schema::PaneGraphicsPlacementParams::default(), + }), + }); + let error: ErrorResponse = serde_json::from_str(&set).unwrap(); + assert_eq!(error.error.code, "feature_disabled"); + assert!(!app.state.pane_graphics_streams.contains_key(&pane_id)); + assert!(!app.state.pane_graphics_layers.contains_key(&pane_id)); + } +} diff --git a/src/app/api/plugins/mod.rs b/src/app/api/plugins/mod.rs index 1e0ec7d8..b7f90df9 100644 --- a/src/app/api/plugins/mod.rs +++ b/src/app/api/plugins/mod.rs @@ -1164,6 +1164,11 @@ command = ["echo", "b"] app.state.active = Some(0); app.state.selected = 0; app.state.mode = crate::app::Mode::Terminal; + app.state.kitty_graphics_enabled = true; + app.state.host_cell_size = crate::kitty_graphics::HostCellSize { + width_px: 11, + height_px: 22, + }; app.state.terminals.get_mut(&root_terminal).unwrap().cwd = "/tmp".into(); let target_public_pane_id = app.public_pane_id(0, root_pane).unwrap(); @@ -1182,7 +1187,7 @@ platforms = ["linux", "macos"] [[panes]] id = "board" title = "Plugin Board" -command = ["sh", "-c", "printf '%s\n%s\n%s\n%s\n%s\n%s\n%s\n' \"$PWD\" \"$HERDR_PLUGIN_ID\" \"$HERDR_PLUGIN_ENTRYPOINT_ID\" \"$HERDR_WORKSPACE_ID\" \"$HERDR_PANE_ID\" \"$HERDR_BIN_PATH\" \"$HERDR_PLUGIN_CONTEXT_JSON\" > {}"] +command = ["sh", "-c", "printf '%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n' \"$PWD\" \"$HERDR_PLUGIN_ID\" \"$HERDR_PLUGIN_ENTRYPOINT_ID\" \"$HERDR_WORKSPACE_ID\" \"$HERDR_PANE_ID\" \"$HERDR_BIN_PATH\" \"$HERDR_PLUGIN_CONTEXT_JSON\" \"${{HERDR_CELL_WIDTH_PX-unset}}\" \"${{HERDR_CELL_HEIGHT_PX-unset}}\" > {}"] "#, capture.display() ), @@ -1254,6 +1259,8 @@ command = ["sh", "-c", "printf '%s\n%s\n%s\n%s\n%s\n%s\n%s\n' \"$PWD\" \"$HERDR_ context.focused_pane_id.as_deref(), Some(target_public_pane_id.as_str()) ); + assert_eq!(lines.next(), Some("unset")); + assert_eq!(lines.next(), Some("unset")); for (_, runtime) in app.terminal_runtimes.drain() { runtime.shutdown(); |
