use crate::core::buffer::Buffer;
use crate::core::rect::Rect;
use crate::core::style::Style;
use crate::ontology::{
AgentAction, AgentCapability, Discoverable, PropertySchema, PropertyType, SemanticRole,
WidgetSchema,
};
use crate::widget::block::Block;
use crate::widget::Widget;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageProtocol {
Kitty,
ITerm2,
Fallback,
}
#[derive(Debug, Clone)]
pub struct Image {
data: Vec<u8>,
mime_type: String,
protocol: ImageProtocol,
block: Option<Block>,
style: Style,
fallback_text: Option<String>,
max_width: Option<u16>,
max_height: Option<u16>,
}
impl Image {
pub fn new(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
Self {
data,
mime_type: mime_type.into(),
protocol: ImageProtocol::Fallback,
block: None,
style: Style::default(),
fallback_text: None,
max_width: None,
max_height: None,
}
}
pub fn protocol(mut self, protocol: ImageProtocol) -> Self {
self.protocol = protocol;
self
}
pub fn block(mut self, block: Block) -> Self {
self.block = Some(block);
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn fallback_text(mut self, text: impl Into<String>) -> Self {
self.fallback_text = Some(text.into());
self
}
pub fn max_width(mut self, w: u16) -> Self {
self.max_width = Some(w);
self
}
pub fn max_height(mut self, h: u16) -> Self {
self.max_height = Some(h);
self
}
pub fn detect_protocol() -> ImageProtocol {
if std::env::var("KITTY_WINDOW_ID").is_ok() {
return ImageProtocol::Kitty;
}
if let Ok(term) = std::env::var("TERM_PROGRAM") {
match term.as_str() {
"WezTerm" | "ghostty" => return ImageProtocol::Kitty,
"iTerm.app" => return ImageProtocol::ITerm2,
_ => {}
}
}
ImageProtocol::Fallback
}
fn base64_data(&self) -> String {
use std::fmt::Write;
let bytes = &self.data;
const CHARS: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut result = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
let triple = (b0 << 16) | (b1 << 8) | b2;
let _ = result.write_char(CHARS[((triple >> 18) & 0x3F) as usize] as char);
let _ = result.write_char(CHARS[((triple >> 12) & 0x3F) as usize] as char);
if chunk.len() > 1 {
let _ = result.write_char(CHARS[((triple >> 6) & 0x3F) as usize] as char);
} else {
result.push('=');
}
if chunk.len() > 2 {
let _ = result.write_char(CHARS[(triple & 0x3F) as usize] as char);
} else {
result.push('=');
}
}
result
}
fn kitty_sequence(&self, cols: u16, rows: u16) -> String {
let b64 = self.base64_data();
format!("\x1b_Ga=T,f=100,t=d,c={},r={};{}\x1b\\", cols, rows, b64)
}
fn iterm2_sequence(&self, cols: u16, rows: u16) -> String {
let b64 = self.base64_data();
format!(
"\x1b]1337;File=inline=1;width={};height={};preserveAspectRatio=1:{}\x07",
cols, rows, b64
)
}
}
impl Widget for Image {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.is_empty() {
return;
}
let inner = if let Some(ref block) = self.block {
let inner = block.inner(area);
block.clone().render(area, buf);
inner
} else {
area
};
if inner.is_empty() {
return;
}
let display_w = self.max_width.unwrap_or(inner.width).min(inner.width);
let display_h = self.max_height.unwrap_or(inner.height).min(inner.height);
match self.protocol {
ImageProtocol::Kitty => {
let seq = self.kitty_sequence(display_w, display_h);
buf[(inner.x, inner.y)].set_symbol(&seq);
}
ImageProtocol::ITerm2 => {
let seq = self.iterm2_sequence(display_w, display_h);
buf[(inner.x, inner.y)].set_symbol(&seq);
}
ImageProtocol::Fallback => {
let text = self.fallback_text.as_deref().unwrap_or({
"[image]"
});
let display = if text.len() as u16 > inner.width {
&text[..inner.width as usize]
} else {
text
};
let x = inner.x + (inner.width.saturating_sub(display.len() as u16)) / 2;
let y = inner.y + inner.height / 2;
buf.set_string(x, y, display, self.style);
}
}
}
}
impl Discoverable for Image {
fn schema() -> WidgetSchema {
WidgetSchema {
name: "Image".into(),
description: "An inline image widget using Kitty/iTerm2 graphics protocols.".into(),
default_role: SemanticRole::Display,
properties: vec![
PropertySchema {
name: "data".into(),
description: "Raw image bytes (e.g., PNG).".into(),
property_type: PropertyType::String, required: true,
default_value: None,
constraints: vec![],
},
PropertySchema {
name: "mime_type".into(),
description: "MIME type of the image data (e.g., image/png).".into(),
property_type: PropertyType::String,
required: true,
default_value: None,
constraints: vec![],
},
PropertySchema {
name: "protocol".into(),
description: "Image protocol: Kitty, ITerm2, or Fallback.".into(),
property_type: PropertyType::Enum(vec![
"Kitty".into(),
"ITerm2".into(),
"Fallback".into(),
]),
required: false,
default_value: Some(serde_json::json!("Fallback")),
constraints: vec![],
},
],
actions: vec![],
usage_hint: Some(
"Use Image::detect_protocol() to auto-select the best rendering method.".into(),
),
tags: vec![
"image".into(),
"graphics".into(),
"visual".into(),
"media".into(),
],
}
}
fn capabilities(&self) -> Vec<AgentCapability> {
vec![]
}
fn actions(&self) -> Vec<AgentAction> {
vec![]
}
fn semantic_role(&self) -> SemanticRole {
SemanticRole::Display
}
fn agent_state(&self) -> serde_json::Value {
serde_json::json!({
"mime_type": self.mime_type,
"data_size": self.data.len(),
"protocol": format!("{:?}", self.protocol),
})
}
fn execute_action(
&mut self,
_action: &str,
_params: &serde_json::Value,
) -> Result<serde_json::Value, String> {
Err("Image has no executable actions".into())
}
}