diff options
Diffstat (limited to 'alacritty_terminal/src')
-rw-r--r-- | alacritty_terminal/src/ansi.rs | 44 | ||||
-rw-r--r-- | alacritty_terminal/src/graphics/mod.rs | 146 | ||||
-rw-r--r-- | alacritty_terminal/src/graphics/sixel.rs | 772 | ||||
-rw-r--r-- | alacritty_terminal/src/lib.rs | 1 | ||||
-rw-r--r-- | alacritty_terminal/src/term/cell.rs | 29 | ||||
-rw-r--r-- | alacritty_terminal/src/term/mod.rs | 138 |
6 files changed, 1124 insertions, 6 deletions
diff --git a/alacritty_terminal/src/ansi.rs b/alacritty_terminal/src/ansi.rs index d5574f59..eaaf5d62 100644 --- a/alacritty_terminal/src/ansi.rs +++ b/alacritty_terminal/src/ansi.rs @@ -10,6 +10,7 @@ use vte::{Params, ParamsIter}; use alacritty_config_derive::ConfigDeserialize; +use crate::graphics::{sixel, GraphicData}; use crate::index::{Column, Line}; use crate::term::color::Rgb; @@ -136,6 +137,9 @@ enum Dcs { /// End of the synchronized update. SyncEnd, + + /// Sixel data + SixelData(Box<sixel::Parser>), } /// The processor wraps a `vte::Parser` to ultimately call methods on a Handler. @@ -240,6 +244,7 @@ impl Processor { self.state.sync_state.timeout = Some(Instant::now() + SYNC_UPDATE_TIMEOUT); }, Some(Dcs::SyncEnd) => self.stop_sync(handler), + Some(Dcs::SixelData(_)) => (), None => (), }, } @@ -456,6 +461,17 @@ pub trait Handler { /// Report text area size in characters. fn text_area_size_chars(&mut self) {} + + /// Report a graphics attribute. + fn graphics_attribute(&mut self, _: u16, _: u16) {} + + /// Create a parser for Sixel data. + fn start_sixel_graphic(&mut self, _params: &Params) -> Option<Box<sixel::Parser>> { + None + } + + /// Insert a new graphic item. + fn insert_graphic(&mut self, _data: GraphicData, _palette: Option<Vec<Rgb>>) {} } /// Terminal cursor configuration. @@ -529,6 +545,8 @@ pub enum Mode { LineFeedNewLine = 20, /// ?25 ShowCursor = 25, + /// ?80 + SixelScrolling = 80, /// ?1000 ReportMouseClicks = 1000, /// ?1002 @@ -547,6 +565,8 @@ pub enum Mode { UrgencyHints = 1042, /// ?1049 SwapScreenAndSetRestoreCursor = 1049, + /// Use a private palette for each new graphic. + SixelPrivateColorRegisters = 1070, /// ?2004 BracketedPaste = 2004, } @@ -568,6 +588,7 @@ impl Mode { 7 => Mode::LineWrap, 12 => Mode::BlinkingCursor, 25 => Mode::ShowCursor, + 80 => Mode::SixelScrolling, 1000 => Mode::ReportMouseClicks, 1002 => Mode::ReportCellMouseMotion, 1003 => Mode::ReportAllMouseMotion, @@ -577,6 +598,7 @@ impl Mode { 1007 => Mode::AlternateScroll, 1042 => Mode::UrgencyHints, 1049 => Mode::SwapScreenAndSetRestoreCursor, + 1070 => Mode::SixelPrivateColorRegisters, 2004 => Mode::BracketedPaste, _ => { trace!("[unimplemented] primitive mode: {}", num); @@ -918,6 +940,10 @@ where self.state.dcs = Some(Dcs::SyncStart); } }, + ('q', []) => { + let parser = self.handler.start_sixel_graphic(params); + self.state.dcs = parser.map(Dcs::SixelData); + }, _ => debug!( "[unhandled hook] params={:?}, ints: {:?}, ignore: {:?}, action: {:?}", params, intermediates, ignore, action @@ -927,16 +953,29 @@ where #[inline] fn put(&mut self, byte: u8) { - debug!("[unhandled put] byte={:?}", byte); + match self.state.dcs { + Some(Dcs::SixelData(ref mut parser)) => { + if let Err(err) = parser.put(byte) { + log::warn!("Failed to parse Sixel data: {}", err); + self.state.dcs = None; + } + }, + + _ => debug!("[unhandled put] byte={:?}", byte), + } } #[inline] fn unhook(&mut self) { - match self.state.dcs { + match self.state.dcs.take() { Some(Dcs::SyncStart) => { self.state.sync_state.timeout = Some(Instant::now() + SYNC_UPDATE_TIMEOUT); }, Some(Dcs::SyncEnd) => (), + Some(Dcs::SixelData(parser)) => match parser.finish() { + Ok((graphic, palette)) => self.handler.insert_graphic(graphic, Some(palette)), + Err(err) => log::warn!("Failed to parse Sixel data: {}", err), + }, _ => debug!("[unhandled unhook]"), } } @@ -1246,6 +1285,7 @@ where handler.set_scrolling_region(top, bottom); }, ('S', []) => handler.scroll_up(next_param_or(1) as usize), + ('S', [b'?']) => handler.graphics_attribute(next_param_or(0), next_param_or(0)), ('s', []) => handler.save_cursor_position(), ('T', []) => handler.scroll_down(next_param_or(1) as usize), ('t', []) => match next_param_or(1) as usize { diff --git a/alacritty_terminal/src/graphics/mod.rs b/alacritty_terminal/src/graphics/mod.rs new file mode 100644 index 00000000..a285228f --- /dev/null +++ b/alacritty_terminal/src/graphics/mod.rs @@ -0,0 +1,146 @@ +//! This module implements the logic to manage graphic items included in a +//! `Grid` instance. + +pub mod sixel; + +use std::mem; +use std::sync::{Arc, Weak}; + +use parking_lot::Mutex; +use serde::{Deserialize, Serialize}; + +use crate::term::color::Rgb; + +/// Max allowed dimensions (width, height) for the graphic, in pixels. +pub const MAX_GRAPHIC_DIMENSIONS: [usize; 2] = [4096, 4096]; + +/// Unique identifier for every graphic added to a grid. +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug, Copy, Hash, PartialOrd, Ord)] +pub struct GraphicId(u64); + +/// Reference to a texture stored in the display. +/// +/// When all references to a single texture are removed, its identifier is +/// added to the remove queue. +#[derive(Clone, Debug)] +pub struct TextureRef { + /// Graphic identifier. + pub id: GraphicId, + + /// Queue to track removed references. + pub remove_queue: Weak<Mutex<Vec<GraphicId>>>, +} + +impl PartialEq for TextureRef { + fn eq(&self, t: &Self) -> bool { + // Ignore remove_queue. + self.id == t.id + } +} + +impl Eq for TextureRef {} + +impl Drop for TextureRef { + fn drop(&mut self) { + if let Some(remove_queue) = self.remove_queue.upgrade() { + remove_queue.lock().push(self.id); + } + } +} + +/// Graphic data stored in a single cell. +#[derive(Eq, PartialEq, Clone, Debug)] +pub struct GraphicCell { + /// Texture to draw the graphic in this cell. + pub texture: Arc<TextureRef>, + + /// Offset in the x direction. + pub offset_x: u16, + + /// Offset in the y direction. + pub offset_y: u16, +} + +impl GraphicCell { + /// Graphic identifier of the texture in this cell. + #[inline] + pub fn graphic_id(&self) -> GraphicId { + self.texture.id + } +} + +/// Specifies the format of the pixel data. +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug, Copy)] +pub enum ColorType { + /// 3 bytes per pixel (red, green, blue). + Rgb, + + /// 4 bytes per pixel (red, green, blue, alpha). + Rgba, +} + +/// Defines a single graphic read from the PTY. +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] +pub struct GraphicData { + /// Graphics identifier. + pub id: GraphicId, + + /// Width, in pixels, of the graphic. + pub width: usize, + + /// Height, in pixels, of the graphic. + pub height: usize, + + /// Color type of the pixels. + pub color_type: ColorType, + + /// Pixels data. + pub pixels: Vec<u8>, +} + +/// Queues to add or to remove the textures in the display. +pub struct UpdateQueues { + /// Graphics read from the PTY. + pub pending: Vec<GraphicData>, + + /// Graphics removed from the grid. + pub remove_queue: Vec<GraphicId>, +} + +/// Track changes in the grid to add or to remove graphics. +#[derive(Clone, Debug, Default)] +pub struct Graphics { + /// Last generated identifier. + pub last_id: u64, + + /// New graphics, received from the PTY. + pub pending: Vec<GraphicData>, + + /// Graphics removed from the grid. + pub remove_queue: Arc<Mutex<Vec<GraphicId>>>, + + /// Shared palette for Sixel graphics. + pub sixel_shared_palette: Option<Vec<Rgb>>, +} + +impl Graphics { + /// Generate a new graphic identifier. + pub fn next_id(&mut self) -> GraphicId { + self.last_id += 1; + GraphicId(self.last_id) + } + + /// Get queues to update graphics in the grid. + /// + /// If all queues are empty, it returns `None`. + pub fn take_queues(&mut self) -> Option<UpdateQueues> { + let mut remove_queue = self.remove_queue.lock(); + if remove_queue.is_empty() && self.pending.is_empty() { + return None; + } + + let remove_queue = mem::take(&mut *remove_queue); + + Some(UpdateQueues { pending: mem::take(&mut self.pending), remove_queue }) + } +} diff --git a/alacritty_terminal/src/graphics/sixel.rs b/alacritty_terminal/src/graphics/sixel.rs new file mode 100644 index 00000000..d617e53b --- /dev/null +++ b/alacritty_terminal/src/graphics/sixel.rs @@ -0,0 +1,772 @@ +//! This module implements a parser for the Sixel protocol, and it is based on the +//! chapter [SIXEL GRAPHICS EXTENSION] of the DEC reference manual. +//! +//! [SIXEL GRAPHICS EXTENSION]: https://archive.org/details/bitsavers_decstandar0VideoSystemsReferenceManualDec91_74264381/page/n907/mode/2up +//! +//! # Limitations +//! +//! The parser have the following limitations: +//! +//! * A single image can use up to 1024 different colors. +//! +//! The Sixel reference requires 256, but allow more colors. +//! +//! * Image dimensions are limited to 4096 x 4096. +//! +//! * Pixel aspect ratio parameters are ignored. +//! +//! The Sixel references specifies some parameters to change the pixel +//! aspect ratio, but multiple implementations always use 1:1, so these +//! parameters have no real effect. +use std::cmp::max; +use std::fmt; +use std::mem; + +use crate::graphics::{ColorType, GraphicData, GraphicId, MAX_GRAPHIC_DIMENSIONS}; +use crate::term::color::Rgb; + +use log::trace; +use vte::Params; + +/// Type for color registers. +#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] +struct ColorRegister(u16); + +/// Number of color registers. +pub const MAX_COLOR_REGISTERS: usize = 1024; + +/// Color register for transparent pixels. +const REG_TRANSPARENT: ColorRegister = ColorRegister(u16::MAX); + +/// Number of parameters allowed in a single Sixel command. +const MAX_COMMAND_PARAMS: usize = 5; + +#[derive(Debug)] +pub enum Error { + /// Image dimensions are too big. + TooBigImage { width: usize, height: usize }, + + /// A component in a color introducer is not valid. + InvalidColorComponent { register: u16, component_value: u16 }, + + /// The coordinate system to define the color register is not valid. + InvalidColorCoordinateSystem { register: u16, coordinate_system: u16 }, +} + +impl fmt::Display for Error { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::TooBigImage { width, height } => { + write!(fmt, "The image dimensions are too big ({}, {})", width, height) + }, + + Error::InvalidColorComponent { register, component_value } => { + write!(fmt, "Invalid color component {} for register {}", component_value, register) + }, + + Error::InvalidColorCoordinateSystem { register, coordinate_system } => { + write!( + fmt, + "Invalid color coordinate system {} for register {}", + coordinate_system, register + ) + }, + } + } +} + +/// Commands found in the data stream. +#[derive(Debug)] +enum SixelCommand { + /// Specifies a repeat count before a sixel. + /// + /// Its only parameter is the repeat count. + RepeatIntroducer, + + /// Defines raster attributes for the following data. + /// + /// It expects 4 parameters: + /// + /// 1. Pixel aspect ratio numerator (relative height). + /// 2. Pixel aspect ratio denominator (relative width). + /// 3. Horizontal Extent. + /// 4. Vertical Extent. + SetRasterAttributes, + + /// Starts a color selection sequence. + /// + /// The first parameter is the register number. + /// + /// Optionally, it can receive 4 more parameters: + /// + /// 1. Color coordinate system. `1` for HLS, `2` for RGB. + /// 2. Hue angle, or red. + /// 3. Lightness, or green. + /// 4. Saturation, or blue. + ColorIntroducer, + + /// Moves the active position to the graphic left margin. + CarriageReturn, + + /// Moves the active position to the graphic left margin + /// and one row of sixels. + NextLine, +} + +/// Parser for commands found in the picture definition. +#[derive(Debug)] +struct CommandParser { + /// Active command. + command: SixelCommand, + + /// Parameter values. + /// + /// If a value is greater than `u16::MAX`, it will be kept as `u16::MAX`. + /// + /// Parameters after `MAX_COMMAND_PARAMS` are ignored. + params: [u16; MAX_COMMAND_PARAMS], + + /// Current position. + params_position: usize, +} + +impl CommandParser { + fn new(command: SixelCommand) -> CommandParser { + CommandParser { command, params: [0; MAX_COMMAND_PARAMS], params_position: 0 } + } + + fn put(&mut self, byte: u8) { + let pos = self.params_position; + if pos < MAX_COMMAND_PARAMS { + match byte { + b'0'..=b'9' => { + self.params[pos] = + self.params[pos].saturating_mul(10).saturating_add((byte - b'0') as u16); + }, + + b';' => { + self.params_position += 1; + }, + + _ => (), // Ignore unknown bytes. + } + } + } + + /// Apply the execution of the active command to the parser. + fn finish(self, parser: &mut Parser) -> Result<(), Error> { + match self.command { + SixelCommand::RepeatIntroducer => { + parser.repeat_count = self.params[0] as usize; + }, + + SixelCommand::SetRasterAttributes => { + if self.params_position >= 3 { + let width = self.params[2] as usize; + let height = self.params[3] as usize; + parser.ensure_size(width, height)?; + } + }, + + SixelCommand::ColorIntroducer => { + let register = ColorRegister(self.params[0]); + + if self.params_position >= 4 { + macro_rules! p { + ($index:expr) => { + match self.params[$index] { + x if x <= 100 => x, + x => { + return Err(Error::InvalidColorComponent { + register: register.0, + component_value: x, + }) + }, + } + }; + } + + let (r, g, b) = match self.params[1] { + // HLS. + 1 => hls_to_rgb(p!(2), p!(3), p!(4)), + + // RGB. + 2 => (p!(2), p!(3), p!(4)), + + // Invalid coordinate system. + x => { + return Err(Error::InvalidColorCoordinateSystem { + register: register.0, + coordinate_system: x, + }) + }, + }; + + parser.set_color_register(register, r, g, b); + } + + parser.selected_color_register = register; + }, + + SixelCommand::CarriageReturn => { + parser.x = 0; + }, + + SixelCommand::NextLine => { + parser.x = 0; + parser.y += 6; + }, + } + + Ok(()) + } +} + +/// A group of 6 vertical pixels. +struct Sixel(u8); + +impl Sixel { + /// Create a new sixel. + /// + /// It expects the byte value from the picture definition stream. + #[inline] + fn new(byte: u8) -> Sixel { + debug_assert!((0x3F..=0x7E).contains(&byte)); + Sixel(byte - 0x3F) + } + + /// Return how many rows are printed in the sixel. + #[inline] + fn height(&self) -> usize { + 8 - self.0.leading_zeros() as usize + } + + /// Return an iterator to get dots in the sixel. + #[inline] + fn dots(&self) -> impl Iterator<Item = bool> { + let sixel = self.0; + (0..6).map(move |position| sixel & (1 << position) != 0) + } +} + +/// Parser of the picture definition in a Sixel data stream. +#[derive(Default, Debug)] +pub struct Parser { + /// Active command to be parsed. + command_parser: Option<CommandParser>, + + /// Current picture width. + width: usize, + + /// Current picture height. + height: usize, + + /// Current picture pixels. + pixels: Vec<ColorRegister>, + + /// Indicates the register color for empty pixels. + background: ColorRegister, + + /// RGB values for every register. + color_registers: Vec<Rgb>, + + /// Selected color register. + selected_color_register: ColorRegister, + + /// Repeat count for the next sixel. + repeat_count: usize, + + /// Horizontal position of the active sixel. + x: usize, + + /// Vertical position of the active sixel. + y: usize, +} + +impl Parser { + /// Creates a new parser. + pub fn new(params: &Params, shared_palette: Option<Vec<Rgb>>) -> Parser { + trace!("Start Sixel parser"); + + let mut parser = Parser::default(); + + // According to the Sixel reference, the second parameter (Ps2) is + // the background selector. It controls how to show pixels without + // an explicit color, and it accepts the following values: + // + // 0 device default action + // 1 no action (don't change zero value pixels) + // 2 set zero value pixels to background color + // + // We replicate the xterm's behaviour: + // + // - If it is set to `1`, the background is transparent. + // - For any other value, the background is the color register 0. + + let ps2 = params.iter().nth(1).and_then(|param| param.iter().next().copied()).unwrap_or(0); + parser.background = if ps2 == 1 { REG_TRANSPARENT } else { ColorRegister(0) }; + + if let Some(color_registers) = shared_palette { + parser.color_registers = color_registers; + } else { + init_color_registers(&mut parser); + } + + parser + } + + /// Parse a byte from the Sixel stream. + pub fn put(&mut self, byte: u8) -> Result<(), Error> { + match byte { + b'!' => self.start_command(SixelCommand::RepeatIntroducer)?, + + b'"' => self.start_command(SixelCommand::SetRasterAttributes)?, + + b'#' => self.start_command(SixelCommand::ColorIntroducer)?, + + b'$' => self.start_command(SixelCommand::CarriageReturn)?, + + b'-' => self.start_command(SixelCommand::NextLine)?, + + b'0'..=b'9' | b';' => { + if let Some(command_parser) = &mut self.command_parser { + command_parser.put(byte); + } + }, + + 0x3F..=0x7E => self.add_sixel(Sixel::new(byte))?, + + _ => { + // Invalid bytes are ignored, but we still have to finish any + // active command. + + self.finish_command()?; + }, + } + + Ok(()) + } + + #[inline] + fn start_command(&mut self, command: SixelCommand) -> Result<(), Error> { + self.finish_command()?; + self.command_parser = Some(CommandParser::new(command)); + Ok(()) + } + + #[inline] + fn finish_command(&mut self) -> Result<(), Error> { + if let Some(command_parser) = self.command_parser.take() { + command_parser.finish(self)?; + } + + Ok(()) + } + + /// Set the RGB color for a register. + /// + /// Color components are expected to be in the range of 0..=100. + fn set_color_register(&mut self, register: ColorRegister, r: u16, g: u16, b: u16) { + let register = register.0 as usize; + + if register >= MAX_COLOR_REGISTERS { + return; + } + + if self.color_registers.len() <= register { + self.color_registers.resize(register + 1, Rgb { r: 0, g: 0, b: 0 }) + } + + let r = ((r * 255 + 50) / 100) as u8; + let g = ((g * 255 + 50) / 100) as u8; + let b = ((b * 255 + 50) / 100) as u8; + self.color_registers[register] = Rgb { r, g, b }; + } + + /// Check if the current picture is big enough for the given dimensions. If + /// not, the picture is resized. + fn ensure_size(&mut self, width: usize, height: usize) -> Result<(), Error> { + // Do nothing if the current picture is big enough. + if self.width >= width && self.height >= height { + return Ok(()); + } + + if width > MAX_GRAPHIC_DIMENSIONS[0] || height > MAX_GRAPHIC_DIMENSIONS[1] { + return Err(Error::TooBigImage { width, height }); + } + + trace!( + "Set Sixel image dimensions to {}x{}", + max(self.width, width), + max(self.height, height), + ); + + // If there is no current picture, creates a new one. + if self.pixels.is_empty() { + self.width = width; + self.height = height; + self.pixels = vec![self.background; width * height]; + return Ok(()); + } + + // If current width is big enough, we only need to add more pixels + // after the current buffer. + if self.width >= width { + self.pixels.resize(height * self.width, self.background); + self.height = height; + return Ok(()); + } + + // At this point, we know that the new width is greater than the + // current one, so we have to extend the buffer and move the rows to + // their new positions. + let height = usize::max(height, self.height); + + self.pixels.resize(height * width, self.background); + + for y in (0..self.height).rev() { + for x in (0..self.width).rev() { + let old = y * self.width + x; + let new = y * width + x; + self.pixels.swap(old, new); + } + } + + self.width = width; + self.height = height; + Ok(()) + } + + /// Add a sixel using the selected color register, and move the active + /// position. + fn add_sixel(&mut self, sixel: Sixel) -> Result<(), Error> { + self.finish_command()?; + + // Take the repeat count and reset it. + // + // `max` function is used because the Sixel reference specifies + // that a repeat count of zero implies a repeat count of 1. + let repeat = max(1, mem::take(&mut self.repeat_count)); + + self.ensure_size(self.x + repeat, self.y + sixel.height())?; + + if sixel.0 != 0 { + let mut index = self.width * self.y + self.x; + for dot in sixel.dots() { + if dot { + for pixel in &mut self.pixels[index..index + repeat] { + *pixel = self.selected_color_register; + } + } + + index += self.width; + } + } + + self.x += repeat; + + Ok(()) + } + + /// Returns the final graphic to append to the grid, with the palette + /// built in the process. + pub fn finish(mut self) -> Result<(GraphicData, Vec<Rgb>), Error> { + self.finish_command()?; + + trace!( + "Finish Sixel parser: width={}, height={}, color_registers={}", + self.width, + self.height, + self.color_registers.len() + ); + + let mut rgba_pixels = Vec::with_capacity(self.pixels.len() * 4); + + for ®ister in &self.pixels { + let pixel = { + if register == REG_TRANSPARENT { + [0; 4] + } else { + match self.color_registers.get(register.0 as usize) { + None => [0, 0, 0, 255], + Some(color) => [color.r, color.g, color.b, 255], + } + } + }; + + rgba_pixels.extend_from_slice(&pixel); + } + + let data = GraphicData { + id: GraphicId(0), + height: self.height, + width: self.width, + color_type: ColorType::Rgba, + pixels: rgba_pixels, + }; + + Ok((data, self.color_registers)) + } +} + +/// Compute a RGB value from HLS. +/// +/// Input and output values are in the range of `0..=100`. +/// +/// The implementation is a direct port of the same function in the +/// xterm's code. +#[allow(clippy::many_single_char_names)] +fn hls_to_rgb(h: u16, l: u16, s: u16) -> (u16, u16, u16) { + if s == 0 { + return (l, l, l); + } + + let hs = ((h + 240) / 60) % 6; + let lv = l as f64 / 100.0; + + let c2 = f64::abs((2.0 * lv as f64) - 1.0); + let c = (1.0 - c2) * (s as f64 / 100.0); + let x = if hs & 1 == 1 { c } else { 0.0 }; + + let rgb = match hs { + 0 => (c, x, 0.), + 1 => (x, c, 0.), + 2 => (0., c, x), + 3 => (0., x, c), + 4 => (x, 0., c), + _ => (c, 0., c), + }; + + fn clamp(x: f64) -> u16 { + let x = x * 100. + 0.5; + if x > 100. { + 100 + } else if x < 0. { + 0 + } else { + x as u16 + } + } + + let m = lv - 0.5 * c; + let r = clamp(rgb.0 + m); + let g = clamp(rgb.1 + m); + let b = clamp(rgb.2 + m); + + (r, g, b) +} + +/// Initialize the color registers using the colors from the VT-340 terminal. +/// +/// There is no official documentation about these colors, but multiple Sixel +/// implementations assume this palette. +fn init_color_registers(parser: &mut Parser) { + parser.set_color_register(ColorRegister(0), 0, 0, 0); + parser.set_color_register(ColorRegister(1), 20, 20, 80); + parser.set_color_register(ColorRegister(2), 80, 13, 13); + parser.set_color_register(ColorRegister(3), 20, 80, 20); + parser.set_color_register(ColorRegister(4), 80, 20, 80); + parser.set_color_register(ColorRegister(5), 20, 80, 80); + parser.set_color_register(ColorRegister(6), 80, 80, 20); + parser.set_color_register(ColorRegister(7), 53, 53, 53); + parser.set_color_register(ColorRegister(8), 26, 26, 26); + parser.set_color_register(ColorRegister(9), 33, 33, 60); + parser.set_color_register(ColorRegister(10), 60, 26, 26); + parser.set_color_register(ColorRegister(11), 33, 60, 33); + parser.set_color_register(ColorRegister(12), 60, 33, 60); + parser.set_color_register(ColorRegister(13), 33, 60, 60); + parser.set_color_register(ColorRegister(14), 60, 60, 33); + parser.set_color_register(ColorRegister(15), 80, 80, 80); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::path::Path; + + macro_rules! put_bytes { + ($parser:expr, $data:expr) => { + #[allow(clippy::string_lit_as_bytes)] + for &byte in $data.as_bytes() { + let _ = $parser.put(byte); + } + }; + } + + #[test] + fn parse_command_parameters() { + let mut command_parser = CommandParser::new(SixelCommand::ColorIntroducer); + put_bytes!(command_parser, "65535;1;2;3;4;5"); + + assert_eq!(command_parser.params_position, 5); + assert_eq!(command_parser.params[0], 65535); + assert_eq!(command_parser.params[1], 1); + assert_eq!(command_parser.params[2], 2); + assert_eq!(command_parser.params[3], 3); + assert_eq!(command_parser.params[4], 4); + } + + #[test] + fn set_color_registers() { + let mut parser = Parser::default(); + put_bytes!(parser, "#1;2;30;100;0#200;1;20;75;50."); + + assert!(parser.color_registers.len() >= 200); + + assert_eq!(parser.color_registers[1], Rgb { r: 77, g: 255, b: 0 }); + assert_eq!(parser.color_registers[200], Rgb { r: 161, g: 161, b: 224 }); + + assert_eq!(parser.selected_color_register.0, 200); + } + + #[test] + fn convert_hls_colors() { + // This test converts values from HLS to RBG, and compares those + // results with the values generated by the xterm implementation + // of the same function. + + assert_eq!(hls_to_rgb(100, 60, 60), (84, 36, 84)); + assert_eq!(hls_to_rgb(60, 100, 60), (100, 100, 100)); + assert_eq!(hls_to_rgb(30, 30, 60), (12, 12, 48)); + assert_eq!(hls_to_rgb(100, 90, 100), (100, 80, 100)); + assert_eq!(hls_to_rgb(100, 0, 90), (0, 0, 0)); + assert_eq!(hls_to_rgb(0, 90, 30), (87, 87, 93)); + assert_eq!(hls_to_rgb(60, 0, 60), (0, 0, 0)); + assert_eq!(hls_to_rgb(30, 0, 0), (0, 0, 0)); + assert_eq!(hls_to_rgb(30, 90, 30), (87, 87, 93)); + assert_eq!(hls_to_rgb(30, 30, 30), (21, 21, 39)); + assert_eq!(hls_to_rgb(90, 100, 60), (100, 100, 100)); + assert_eq!(hls_to_rgb(0, 0, 0), (0, 0, 0)); + assert_eq!(hls_to_rgb(30, 0, 90), (0, 0, 0)); + assert_eq!(hls_to_rgb(100, 60, 90), (96, 24, 96)); + assert_eq!(hls_to_rgb(30, 30, 0), (30, 30, 30)); + } + + #[test] + fn resize_picture() -> Result<(), Error> { + let mut parser = Parser { background: REG_TRANSPARENT, ..Parser::default() }; + + const WIDTH: usize = 30; + const HEIGHT: usize = 20; + + // Initialize a transparent picture with Set Raster Attributes. + put_bytes!(parser, format!("\"1;1;{};{}.", WIDTH, HEIGHT)); + + assert_eq!(parser.width, WIDTH); + assert_eq!(parser.height, HEIGHT); + assert_eq!(parser.pixels.len(), WIDTH * HEIGHT); + + assert!(parser.pixels.iter().all(|&pixel| pixel == REG_TRANSPARENT)); + + // Fill each row with a different color register. + for (n, row) in parser.pixels.chunks_mut(WIDTH).enumerate() { + row.iter_mut().for_each(|pixel| *pixel = ColorRegister(n as u16)); + } + + // Increase height. + // + // New rows must be transparent. + parser.ensure_size(WIDTH, HEIGHT + 5)?; + + assert_eq!(parser.width, WIDTH); + assert_eq!(parser.height, HEIGHT + 5); + assert_eq!(parser.pixels.len(), WIDTH * (HEIGHT + 5)); + + for (n, row) in parser.pixels.chunks(WIDTH).enumerate() { + let expected = if n < HEIGHT { ColorRegister(n as u16) } else { REG_TRANSPARENT }; + assert!(row.iter().all(|pixel| *pixel == expected)); + } + + // Increase both width and height. + // + // New rows and columns must be transparent. + parser.ensure_size(WIDTH + 5, HEIGHT + 10)?; + + assert_eq!(parser.width, WIDTH + 5); + assert_eq!(parser.height, HEIGHT + 10); + assert_eq!(parser.pixels.len(), (WIDTH + 5) * (HEIGHT + 10)); + + for (n, row) in parser.pixels.chunks(WIDTH + 5).enumerate() { + if n < HEIGHT { + assert!(row[..WIDTH].iter().all(|pixel| *pixel == ColorRegister(n as u16))); + assert!(row[WIDTH..].iter().all(|pixel| *pixel == REG_TRANSPARENT)); + } else { + assert!(row.iter().all(|pixel| *pixel == REG_TRANSPARENT)); + } + } + + Ok(()) + } + + #[test] + fn sixel_height() { + assert_eq!(Sixel(0b000000).height(), 0); + assert_eq!(Sixel(0b000001).height(), 1); + assert_eq!(Sixel(0b000100).height(), 3); + assert_eq!(Sixel(0b000101).height(), 3); + assert_eq!(Sixel(0b101111).height(), 6); + } + + #[test] + fn sixel_positions() { + macro_rules! dots { + ($sixel:expr) => { + Sixel($sixel).dots().collect::<Vec<_>>() + }; + } + + assert_eq!(dots!(0b000000), &[false, false, false, false, false, false,]); + assert_eq!(dots!(0b000001), &[true, false, false, false, false, false,]); + assert_eq!(dots!(0b000100), &[false, false, true, false, false, false,]); + assert_eq!(dots!(0b000101), &[true, false, true, false, false, false,]); + assert_eq!(dots!(0b101111), &[true, true, true, true, false, true,]); + } + + #[test] + fn load_sixel_files() { + let images_dir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/sixel")); + + let test_images = ["testimage_im6", "testimage_libsixel", "testimage_ppmtosixel"]; + + for test_image in &test_images { + // Load Sixel data. + let mut sixel = { + let mut path = images_dir.join(test_image); + path.set_extension("sixel"); + fs::read(path).unwrap() + }; + + // Remove DCS sequence from Sixel data. + let dcs_end = sixel.iter().position(|&byte| byte == b'q').unwrap(); + sixel.drain(..=dcs_end); + + // Remove ST, which can be either "1B 5C" or "9C". To simplify the + // code, we assume that any ESC byte is the start of the ST. + if let Some(pos) = sixel.iter().position(|&b| b == 0x1B || b == 0x9C) { + sixel.truncate(pos); + } + + // Parse the data and get the GraphicData item. + let mut parser = Parser::default(); + for byte in sixel { + parser.put(byte).unwrap(); + } + + let graphics = parser.finish().unwrap().0; + + assert_eq!(graphics.width, 64); + assert_eq!(graphics.height, 64); + + // Read the RGBA stream generated by ImageMagick and compare it + // with our picture. + let expected_rgba = { + let mut path = images_dir.join(test_image); + path.set_extension("rgba"); + fs::read(path).unwrap() + }; + + assert_eq!(graphics.pixels, expected_rgba); + } + } +} diff --git a/alacritty_terminal/src/lib.rs b/alacritty_terminal/src/lib.rs index c1ba3690..ea4a2a24 100644 --- a/alacritty_terminal/src/lib.rs +++ b/alacritty_terminal/src/lib.rs @@ -8,6 +8,7 @@ pub mod ansi; pub mod config; pub mod event; pub mod event_loop; +pub mod graphics; pub mod grid; pub mod index; pub mod selection; diff --git a/alacritty_terminal/src/term/cell.rs b/alacritty_terminal/src/term/cell.rs index a393b332..14229d15 100644 --- a/alacritty_terminal/src/term/cell.rs +++ b/alacritty_terminal/src/term/cell.rs @@ -4,6 +4,7 @@ use bitflags::bitflags; use serde::{Deserialize, Serialize}; use crate::ansi::{Color, NamedColor}; +use crate::graphics::GraphicCell; use crate::grid::{self, GridCell}; use crate::index::Column; @@ -24,9 +25,10 @@ bitflags! { const STRIKEOUT = 0b0000_0010_0000_0000; const LEADING_WIDE_CHAR_SPACER = 0b0000_0100_0000_0000; const DOUBLE_UNDERLINE = 0b0000_1000_0000_0000; - const UNDERCURL = 0b0001_0000_0000_0000; - const DOTTED_UNDERLINE = 0b0010_0000_0000_0000; - const OVERLINE = 0b0100_0000_0000_0000; + const GRAPHICS = 0b0001_0000_0000_0000; + const UNDERCURL = 0b0010_0000_0000_0000; + const DOTTED_UNDERLINE = 0b0100_0000_0000_0000; + const OVERLINE = 0b1000_0000_0000_0000; } } @@ -56,6 +58,9 @@ impl ResetDiscriminant<Color> for Cell { #[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)] struct CellExtra { zerowidth: Vec<char>, + + #[serde(skip)] + graphic: Option<Box<GraphicCell>>, } /// Content and attributes of a single cell in the terminal grid. @@ -112,6 +117,21 @@ impl Cell { self.drop_extra(); self.c = ' '; } + + /// Graphic present in the cell. + #[inline] + pub fn graphic(&self) -> Option<&GraphicCell> { + self.extra.as_deref().and_then(|extra| extra.graphic.as_deref()) + } + + /// Write the graphic data in the cell. + #[inline] + pub fn set_graphic(&mut self, graphic_cell: GraphicCell) { + let mut extra = self.extra.get_or_insert_with(Default::default); + extra.graphic = Some(Box::new(graphic_cell)); + + self.flags_mut().insert(Flags::GRAPHICS); + } } impl GridCell for Cell { @@ -131,7 +151,8 @@ impl GridCell for Cell { | Flags::STRIKEOUT | Flags::WRAPLINE | Flags::WIDE_CHAR_SPACER - | Flags::LEADING_WIDE_CHAR_SPACER, + | Flags::LEADING_WIDE_CHAR_SPACER + | Flags::GRAPHICS, ) && self.extra.as_ref().map(|extra| extra.zerowidth.is_empty()) != Some(false) } diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index ba170e32..17f64099 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -1,6 +1,7 @@ //! Exports the `Term` type which is a high-level API for the Grid. use std::cmp::{max, min}; +use std::fmt::Write; use std::ops::{Index, IndexMut, Range}; use std::sync::Arc; use std::{mem, ptr, str}; @@ -9,12 +10,16 @@ use bitflags::bitflags; use log::{debug, trace}; use serde::{Deserialize, Serialize}; use unicode_width::UnicodeWidthChar; +use vte::Params; use crate::ansi::{ self, Attr, CharsetIndex, Color, CursorShape, CursorStyle, Handler, NamedColor, StandardCharset, }; use crate::config::Config; use crate::event::{Event, EventListener}; +use crate::graphics::{ + sixel, GraphicCell, GraphicData, Graphics, TextureRef, UpdateQueues, MAX_GRAPHIC_DIMENSIONS, +}; use crate::grid::{Dimensions, Grid, GridIterator, Scroll}; use crate::index::{self, Boundary, Column, Direction, Line, Point, Side}; use crate::selection::{Selection, SelectionRange}; @@ -62,6 +67,8 @@ bitflags! { const ALTERNATE_SCROLL = 0b0000_1000_0000_0000_0000; const VI = 0b0001_0000_0000_0000_0000; const URGENCY_HINTS = 0b0010_0000_0000_0000_0000; + const SIXEL_SCROLLING = 0b0100_0000_0000_0000_0000; + const SIXEL_PRIV_PALETTE = 0b1000_0000_0000_0000_0000; const ANY = std::u32::MAX; } } @@ -72,6 +79,8 @@ impl Default for TermMode { | TermMode::LINE_WRAP | TermMode::ALTERNATE_SCROLL | TermMode::URGENCY_HINTS + | TermMode::SIXEL_SCROLLING + | TermMode::SIXEL_PRIV_PALETTE } } @@ -269,6 +278,9 @@ pub struct Term<T> { /// Information about cell dimensions. cell_width: usize, cell_height: usize, + + /// Data to add graphics to a grid. + graphics: Graphics, } impl<T> Term<T> { @@ -320,6 +332,7 @@ impl<T> Term<T> { selection: None, cell_width: size.cell_width as usize, cell_height: size.cell_height as usize, + graphics: Graphics::default(), } } @@ -470,6 +483,12 @@ impl<T> Term<T> { &mut self.grid } + /// Get queues to update graphic data. If both queues are empty, it returns + /// `None`. + pub fn graphics_take_queues(&mut self) -> Option<UpdateQueues> { + self.graphics.take_queues() + } + /// Resize terminal to new dimensions. pub fn resize(&mut self, size: SizeInfo) { self.cell_width = size.cell_width as usize; @@ -1608,6 +1627,10 @@ impl<T: EventListener> Handler for Term<T> { style.blinking = true; self.event_proxy.send_event(Event::CursorBlinkingChange(true)); }, + ansi::Mode::SixelScrolling => self.mode.insert(TermMode::SIXEL_SCROLLING), + ansi::Mode::SixelPrivateColorRegisters => { + self.mode.insert(TermMode::SIXEL_PRIV_PALETTE) + }, } } @@ -1650,6 +1673,11 @@ impl<T: EventListener> Handler for Term<T> { style.blinking = false; self.event_proxy.send_event(Event::CursorBlinkingChange(false)); }, + ansi::Mode::SixelScrolling => self.mode.remove(TermMode::SIXEL_SCROLLING), + ansi::Mode::SixelPrivateColorRegisters => { + self.graphics.sixel_shared_palette = None; + self.mode.remove(TermMode::SIXEL_PRIV_PALETTE); + }, } } @@ -1772,6 +1800,116 @@ impl<T: EventListener> Handler for Term<T> { let text = format!("\x1b[8;{};{}t", self.screen_lines(), self.columns()); self.event_proxy.send_event(Event::PtyWrite(text)); } + + #[inline] + fn graphics_attribute(&mut self, pi: u16, pa: u16) { + // From Xterm documentation: + // + // Pi = 1 -> item is number of color registers. + // Pi = 2 -> item is Sixel graphics geometry (in pixels). + // + // Pa = 1 -> read attribute. + // Pa = 4 -> read the maximum allowed value. + // + // Any other request reports an error. + + let (ps, pv) = if pa == 1 || pa == 4 { + match pi { + 1 => (0, &[sixel::MAX_COLOR_REGISTERS][..]), + 2 => (0, &MAX_GRAPHIC_DIMENSIONS[..]), + _ => (1, &[][..]), // Report error in Pi + } + } else { + (2, &[][..]) // Report error in Pa + }; + + let mut text = format!("\x1b[?{};{}", pi, ps); + + for item in pv { + let _ = write!(&mut text, ";{}", item); + } + + text.push('S'); + self.event_proxy.send_event(Event::PtyWrite(text)); + } + + fn start_sixel_graphic(&mut self, params: &Params) -> Option<Box<sixel::Parser>> { + let palette = self.graphics.sixel_shared_palette.take(); + Some(Box::new(sixel::Parser::new(params, palette))) + } + + fn insert_graphic(&mut self, graphic: GraphicData, palette: Option<Vec<Rgb>>) { + // Store last palette if we receive a new one, and it is shared. + if let Some(palette) = palette { + if !self.mode.contains(TermMode::SIXEL_PRIV_PALETTE) { + self.graphics.sixel_shared_palette = Some(palette); + } + } + + if graphic.width > MAX_GRAPHIC_DIMENSIONS[0] || graphic.height > MAX_GRAPHIC_DIMENSIONS[1] { + return; + } + + let width = graphic.width as u16; + let height = graphic.height as u16; + + if width == 0 || height == 0 { + return; + } + + // Add the graphic data to the pending queue. + let graphic_id = self.graphics.next_id(); + self.graphics.pending.push(GraphicData { id: graphic_id, ..graphic }); + + // If SIXEL_SCROLLING is enabled, the start of the graphic is the + // cursor position, and the grid can be scrolled if the graphic is + // larger than the screen. The cursor is moved to the next line + // after the graphic. + // + // If it is disabled, the graphic starts at (0, 0), the grid is never + // scrolled, and the cursor position is unmodified. + + let scrolling = self.mode.contains(TermMode::SIXEL_SCROLLING); + + // Fill the cells under the graphic. + // + // The cell in the first column contains a reference to the + // graphic, with the offset from the start. The rest of the + // cells are not overwritten, allowing any text behind + // transparent portions of the image to be visible. + + let left = if scrolling { self.grid.cursor.point.column.0 } else { 0 }; + + let texture = Arc::new(TextureRef { + id: graphic_id, + remove_queue: Arc::downgrade(&self.graphics.remove_queue), + }); + + for (top, offset_y) in (0..).zip((0..height).step_by(self.cell_height)) { + let line = if scrolling { + self.grid.cursor.point.line + } else { + // Check if the image is beyond the screen limit. + if top >= self.screen_lines() as i32 { + break; + } + + Line(top) + }; + + // Store a reference to the graphic in the first column. + let graphic_cell = GraphicCell { texture: texture.clone(), offset_x: 0, offset_y }; + self.grid[line][Column(left)].set_graphic(graphic_cell); + + if scrolling { + self.linefeed(); + } + } + + if scrolling { + self.carriage_return(); + } + } } /// Terminal version for escape sequence reports. |