diff options
author | Josh Rahm <rahm@google.com> | 2021-10-05 14:36:31 -0600 |
---|---|---|
committer | Josh Rahm <rahm@google.com> | 2021-10-05 14:36:31 -0600 |
commit | 7a209fa45f1f4d07cb4a885e8ea3d03e47cf48ae (patch) | |
tree | 026d75fdc19e19952cfba3020c118f24df4ac412 | |
parent | 1725e30e144b04e2e2e30efc76eb968c97a0eabf (diff) | |
parent | 98fbb3f9285d8c00836e3bcfa6e1e13bf809e2a2 (diff) | |
download | r-alacritty-7a209fa45f1f4d07cb4a885e8ea3d03e47cf48ae.tar.gz r-alacritty-7a209fa45f1f4d07cb4a885e8ea3d03e47cf48ae.tar.bz2 r-alacritty-7a209fa45f1f4d07cb4a885e8ea3d03e47cf48ae.zip |
Merge remote-tracking branch 'betaboon/graphics' into experimental
27 files changed, 2283 insertions, 7 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 1694f367..023eaec5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - The vi mode cursor is now created in the top-left if the terminal cursor is invisible - Focused search match will use cell instead of match colors for CellForeground/CellBackground - URL highlighting has moved from `mouse.url` to the `hints` config section +- Support for Sixel protocol ### Fixed @@ -31,6 +31,7 @@ dependencies = [ "glutin", "libc", "log", + "memoffset", "notify", "objc", "parking_lot", diff --git a/alacritty/Cargo.toml b/alacritty/Cargo.toml index b1fee0c2..6ac20526 100644 --- a/alacritty/Cargo.toml +++ b/alacritty/Cargo.toml @@ -34,6 +34,7 @@ libc = "0.2" unicode-width = "0.1" bitflags = "1" dirs = "3.0.1" +memoffset = "0.6.1" [build-dependencies] gl_generator = "0.14.0" diff --git a/alacritty/res/graphics.f.glsl b/alacritty/res/graphics.f.glsl new file mode 100644 index 00000000..28da2a68 --- /dev/null +++ b/alacritty/res/graphics.f.glsl @@ -0,0 +1,40 @@ +#version 330 core + +// Index in the textures[] uniform. +flat in int texId; + +// Texture coordinates. +in vec2 texCoords; + +// Array with graphics data. +uniform sampler2D textures[16]; + +// Computed color. +out vec4 color; + +void main() { + // The expression `textures[texId]` can't be used in OpenGL 3.3. + // If we try to use it, the compiler throws this error: + // + // sampler arrays indexed with non-constant expressions + // are forbidden in GLSL 1.30 and later + // + // To overcome this limitation we use a switch for every valid + // value of `texId`. + // + // The first expression (`textures[texId]`) works with OpenGL 4.0 + // or later (using `#version 400 core`). If Alacritty drops support + // for OpenGL 3.3, this switch block can be replaced with it. + + +#define TEX(N) case N: color = texture(textures[N], texCoords); break; + + switch(texId) { + TEX( 0) TEX( 1) TEX( 2) TEX( 3) + TEX( 4) TEX( 5) TEX( 6) TEX( 7) + TEX( 8) TEX( 9) TEX(10) TEX(11) + TEX(12) TEX(13) TEX(14) TEX(15) + default: + discard; + } +} diff --git a/alacritty/res/graphics.v.glsl b/alacritty/res/graphics.v.glsl new file mode 100644 index 00000000..56998e9d --- /dev/null +++ b/alacritty/res/graphics.v.glsl @@ -0,0 +1,79 @@ +#version 330 core + +// ------ +// INPUTS + +// Texture associated to the graphic. +layout(location = 0) in int textureId; + +// Sides where the vertex is located. +// +// Bit 0 (LSB) is 0 for top and 1 for bottom. +// Bit 1 is 0 for left and 1 for right. +layout(location = 1) in int sides; + +// Column number in the grid where the left vertex is set. +layout(location = 2) in float column; + +// Line number in the grid where the left vertex is set. +layout(location = 3) in float line; + +// Height in pixels of the texture. +layout(location = 4) in float height; + +// Width in pixels of the texture. +layout(location = 5) in float width; + +// Offset in the X direction. +layout(location = 6) in float offsetX; + +// Offset in the Y direction. +layout(location = 7) in float offsetY; + +// Height in pixels of a single cell when the graphic was added. +layout(location = 8) in float baseCellHeight; + +// ------- +// OUTPUTS + +// Texture sent to the fragment shader. +flat out int texId; + +// Coordinates sent to the fragment shader. +out vec2 texCoords; + +// -------- +// UNIFORMS + +// Width and height of a single cell. +uniform vec2 cellDimensions; + +// Width and height of the view. +uniform vec2 viewDimensions; + + +#define IS_RIGHT_SIDE ((sides & 1) == 1) +#define IS_BOTTOM_SIDE ((sides & 2) == 2) + +void main() { + float scale = cellDimensions.y / baseCellHeight; + float x = (column * cellDimensions.x - offsetX * scale) / (viewDimensions.x / 2) - 1; + float y = -(line * cellDimensions.y - offsetY * scale) / (viewDimensions.y / 2) + 1; + + vec4 position = vec4(x, y, 0, 1); + vec2 coords = vec2(0, 0); + + if(IS_RIGHT_SIDE) { + position.x += scale * width / (viewDimensions.x / 2); + coords.x = 1; + } + + if(IS_BOTTOM_SIDE) { + position.y += -scale * height / (viewDimensions.y / 2); + coords.y = 1; + } + + gl_Position = position; + texCoords = coords; + texId = textureId; +} diff --git a/alacritty/src/display/content.rs b/alacritty/src/display/content.rs index 6fef9574..297aefd6 100644 --- a/alacritty/src/display/content.rs +++ b/alacritty/src/display/content.rs @@ -6,6 +6,7 @@ use std::ops::{Deref, DerefMut, RangeInclusive}; use alacritty_terminal::ansi::{Color, CursorShape, NamedColor}; use alacritty_terminal::config::Config; use alacritty_terminal::event::EventListener; +use alacritty_terminal::graphics::GraphicCell; use alacritty_terminal::grid::{Dimensions, Indexed}; use alacritty_terminal::index::{Column, Direction, Line, Point}; use alacritty_terminal::term::cell::{Cell, Flags}; @@ -187,6 +188,7 @@ pub struct RenderableCell { pub character: char, pub zerowidth: Option<Vec<char>>, pub point: Point<usize>, + pub graphic: Option<GraphicCell>, pub fg: Rgb, pub bg: Rgb, pub sp: Rgb, // Special @@ -263,6 +265,7 @@ impl RenderableCell { RenderableCell { zerowidth: cell.zerowidth().map(|zerowidth| zerowidth.to_vec()), + graphic: cell.graphic().cloned(), flags: cell.flags, character, bg_alpha, diff --git a/alacritty/src/display/mod.rs b/alacritty/src/display/mod.rs index d4c5c274..5e958f03 100644 --- a/alacritty/src/display/mod.rs +++ b/alacritty/src/display/mod.rs @@ -479,7 +479,7 @@ impl Display { /// This call may block if vsync is enabled. pub fn draw<T: EventListener>( &mut self, - terminal: MutexGuard<'_, Term<T>>, + mut terminal: MutexGuard<'_, Term<T>>, message_buffer: &MessageBuffer, config: &Config, search_state: &SearchState, @@ -502,6 +502,8 @@ impl Display { let vi_mode = terminal.mode().contains(TermMode::VI); let vi_mode_cursor = if vi_mode { Some(terminal.vi_mode_cursor) } else { None }; + let graphics_queues = terminal.graphics_take_queues(); + // Drop terminal as early as possible to free lock. drop(terminal); @@ -509,7 +511,12 @@ impl Display { api.clear(background_color); }); + if let Some(graphics_queues) = graphics_queues { + self.renderer.graphics_run_updates(graphics_queues, &size_info); + } + let mut lines = RenderLines::new(); + let mut graphics_list = renderer::graphics::RenderList::default(); // Draw grid. { @@ -532,12 +539,17 @@ impl Display { // Update underline/strikeout. lines.update(&cell); + // Track any graphic present in the cell. + graphics_list.update(&cell); + // Draw the cell. api.render_cell(cell, glyph_cache); } }); } + self.renderer.graphics_draw(graphics_list, &size_info); + let mut rects = lines.rects(&metrics, &size_info); if let Some(vi_mode_cursor) = vi_mode_cursor { diff --git a/alacritty/src/renderer/graphics/draw.rs b/alacritty/src/renderer/graphics/draw.rs new file mode 100644 index 00000000..e07cc078 --- /dev/null +++ b/alacritty/src/renderer/graphics/draw.rs @@ -0,0 +1,242 @@ +//! This module implements the functionality to render graphic textures +//! in the display. +//! +//! [`RenderList`] is used to track graphics in the visible cells. When all +//! cells in the grid are read, graphics are rendered using the positions +//! found in those cells. + +use std::collections::BTreeMap; +use std::mem::{self, MaybeUninit}; + +use crate::display::content::RenderableCell; +use crate::gl::{self, types::*}; +use crate::renderer::graphics::{shader, GraphicsRenderer}; + +use alacritty_terminal::graphics::GraphicId; +use alacritty_terminal::index::{Column, Line}; +use alacritty_terminal::term::SizeInfo; + +use log::trace; + +/// Position to render each texture in the grid. +struct RenderPosition { + column: Column, + line: Line, + offset_x: u16, + offset_y: u16, +} + +/// Track textures to be rendered in the display. +#[derive(Default)] +pub struct RenderList { + items: BTreeMap<GraphicId, RenderPosition>, +} + +impl RenderList { + /// Detects if the cell contains a graphic, then add it to the render list. + /// + /// The graphic is added only the first time it is found in a cell. + #[inline] + pub fn update(&mut self, cell: &RenderableCell) { + if let Some(graphic) = &cell.graphic { + let graphic_id = graphic.graphic_id(); + if self.items.contains_key(&graphic_id) { + return; + } + + let render_item = RenderPosition { + column: cell.point.column, + line: Line(cell.point.line as i32), + offset_x: graphic.offset_x, + offset_y: graphic.offset_y, + }; + + self.items.insert(graphic_id, render_item); + } + } + + /// Returns `true` if there are no items to render. + #[inline] + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } + + /// Builds a list of vertex for the shader program. + pub fn build_vertices(self, renderer: &GraphicsRenderer) -> Vec<shader::Vertex> { + use shader::VertexSide::{BottomLeft, BottomRight, TopLeft, TopRight}; + + let mut vertices = Vec::new(); + + for (graphics_id, render_item) in self.items { + let graphic_texture = match renderer.graphic_textures.get(&graphics_id) { + Some(tex) => tex, + None => continue, + }; + + vertices.reserve(6); + + let vertex = shader::Vertex { + texture_id: graphic_texture.texture.0, + sides: TopLeft, + column: render_item.column.0 as GLuint, + line: render_item.line.0 as GLuint, + height: graphic_texture.height, + width: graphic_texture.width, + offset_x: render_item.offset_x, + offset_y: render_item.offset_y, + base_cell_height: graphic_texture.cell_height, + }; + + vertices.push(vertex); + + for &sides in &[TopRight, BottomLeft, TopRight, BottomRight, BottomLeft] { + vertices.push(shader::Vertex { sides, ..vertex }); + } + } + + vertices + } + + /// Draw graphics in the display, using the graphics rendering shader + /// program. + pub fn draw(self, renderer: &GraphicsRenderer, size_info: &SizeInfo) { + let vertices = self.build_vertices(renderer); + + // Initialize the rendering program. + unsafe { + gl::BindBuffer(gl::ARRAY_BUFFER, renderer.program.vbo); + gl::BindVertexArray(renderer.program.vao); + + gl::UseProgram(renderer.program.id); + + gl::Uniform2f( + renderer.program.u_cell_dimensions, + size_info.cell_width(), + size_info.cell_height(), + ); + gl::Uniform2f( + renderer.program.u_view_dimensions, + size_info.width(), + size_info.height(), + ); + + gl::BlendFuncSeparate(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA, gl::SRC_ALPHA, gl::ONE); + } + + // Array for storing the batch to render multiple graphics in a single call to the + // shader program. + // + // Each graphic requires 6 vertices (2 triangles to make a rectangle), and we will + // never have more than `TEXTURES_ARRAY_SIZE` graphics in a single call, so we set + // the array size to the maximum value that we can use. + let mut batch = [MaybeUninit::uninit(); shader::TEXTURES_ARRAY_SIZE * 6]; + let mut batch_size = 0; + + macro_rules! send_batch { + () => { + #[allow(unused_assignments)] + if batch_size > 0 { + trace!("Call glDrawArrays with {} items", batch_size); + + unsafe { + gl::BufferData( + gl::ARRAY_BUFFER, + (batch_size * mem::size_of::<shader::Vertex>()) as isize, + batch.as_ptr().cast(), + gl::STREAM_DRAW, + ); + + gl::DrawArrays(gl::TRIANGLES, 0, batch_size as GLint); + } + + batch_size = 0; + } + }; + } + + // In order to send textures to the shader program we need to get a _slot_ + // for every texture associated to a graphic. + // + // We have `u_textures.len()` slots available in each execution of the + // shader. + // + // For each slot we need three values: + // + // - The texture unit for `glActiveTexture` (`GL_TEXTUREi`). + // - The uniform location for `textures[i]`. + // - The index `i`, used to set the value of the uniform. + // + // These values are generated using the `tex_slots_generator` iterator. + // + // A single graphic has 6 vertices. All vertices will use the same texture + // slot. To detect if a texture has already a slot, we only need to compare + // with the last texture (`last_tex_slot`) because all the vertices of a + // single graphic are consecutive. + // + // When all slots are occupied, or the batch array is full, the current + // batch is sent and the iterator is reset. + // + // This logic could be simplified using the [Bindless Texture extension], + // but it is not a core feature of any OpenGL version, so hardware support + // is uncertain. + // + // [Bindless Texture extension]: https://www.khronos.org/opengl/wiki/Bindless_Texture + + let tex_slots_generator = (gl::TEXTURE0..=gl::TEXTURE31) + .zip(renderer.program.u_textures.iter()) + .zip(0_u32..) + .map(|((tex_enum, &u_texture), index)| (tex_enum, u_texture, index)); + + let mut tex_slots = tex_slots_generator.clone(); + + // Keep the last allocated slot in a `(texture id, index)` tuple. + let mut last_tex_slot = (0, 0); + + for mut vertex in vertices { + // Check if we can reuse the last texture slot. + if last_tex_slot.0 != vertex.texture_id { + last_tex_slot = loop { + match tex_slots.next() { + None => { + // No more slots. Send the batch and reset the iterator. + send_batch!(); + tex_slots = tex_slots_generator.clone(); + }, + + Some((tex_enum, u_texture, index)) => { + unsafe { + gl::ActiveTexture(tex_enum); + gl::BindTexture(gl::TEXTURE_2D, vertex.texture_id); + gl::Uniform1i(u_texture, index as GLint); + } + + break (vertex.texture_id, index); + }, + } + }; + } + + vertex.texture_id = last_tex_slot.1; + batch[batch_size] = MaybeUninit::new(vertex); + batch_size += 1; + + if batch_size == batch.len() { + send_batch!(); + } + } + + send_batch!(); + + // Reset state. + unsafe { + gl::BlendFunc(gl::SRC1_COLOR, gl::ONE_MINUS_SRC1_COLOR); + + gl::ActiveTexture(gl::TEXTURE0); + gl::BindTexture(gl::TEXTURE_2D, 0); + + gl::UseProgram(0); + gl::BindVertexArray(0); + gl::BindBuffer(gl::ARRAY_BUFFER, 0); + } + } +} diff --git a/alacritty/src/renderer/graphics/mod.rs b/alacritty/src/renderer/graphics/mod.rs new file mode 100644 index 00000000..3fecd549 --- /dev/null +++ b/alacritty/src/renderer/graphics/mod.rs @@ -0,0 +1,149 @@ +//! This module implements the functionality to support graphics in the grid. + +use std::mem; + +use alacritty_terminal::graphics::{ColorType, GraphicData, GraphicId, UpdateQueues}; +use alacritty_terminal::term::SizeInfo; + +use log::trace; +use serde::{Deserialize, Serialize}; + +use crate::gl; +use crate::gl::types::*; +use crate::renderer; + +use std::collections::HashMap; + +mod draw; +mod shader; + +pub use draw::RenderList; + +/// Type for texture names generated in the GPU. +#[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] +pub struct TextureName(GLuint); + +// In debug mode, check if the inner value was set to zero, so we can detect if +// the associated texture was deleted from the GPU. +#[cfg(debug_assertions)] +impl Drop for TextureName { + fn drop(&mut self) { + if self.0 != 0 { + log::error!("Texture {} was not deleted.", self.0); + } + } +} + +/// Texture for a graphic in the grid. +#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] +pub struct GraphicTexture { + /// Texture in the GPU where the graphic pixels are stored. + texture: TextureName, + + /// Cell height at the moment graphic was created. + /// + /// Used to scale it if the user increases or decreases the font size. + cell_height: f32, + + /// Width in pixels of the graphic. + width: u16, + + /// Height in pixels of the graphic. + height: u16, +} + +#[derive(Debug)] +pub struct GraphicsRenderer { + /// Program in the GPU to render graphics. + program: shader::GraphicsShaderProgram, + + /// Collection to associate graphic identifiers with their textures. + graphic_textures: HashMap<GraphicId, GraphicTexture>, +} + +impl GraphicsRenderer { + pub fn new() -> Result<GraphicsRenderer, renderer::Error> { + let program = shader::GraphicsShaderProgram::new()?; + Ok(GraphicsRenderer { program, graphic_textures: HashMap::default() }) + } + + /// Run the required actions to apply changes for the graphics in the grid. + #[inline] + pub fn run_updates(&mut self, update_queues: UpdateQueues, size_info: &SizeInfo) { + self.remove_graphics(update_queues.remove_queue); + self.upload_pending_graphics(update_queues.pending, size_info); + } + + /// Release resources used by removed graphics. + fn remove_graphics(&mut self, removed_ids: Vec<GraphicId>) { + let mut textures = Vec::with_capacity(removed_ids.len()); + for id in removed_ids { + if let Some(mut graphic_texture) = self.graphic_textures.remove(&id) { + // Reset the inner value of TextureName, so the Drop implementation + // (in debug mode) can verify that the texture was deleted. + textures.push(mem::take(&mut graphic_texture.texture.0)); + } + } + + trace!("Call glDeleteTextures with {} items", textures.len()); + + unsafe { + gl::DeleteTextures(textures.len() as GLint, textures.as_ptr()); + } + } + + /// Create new textures in the GPU, and upload the pixels to them. + fn upload_pending_graphics(&mut self, graphics: Vec<GraphicData>, size_info: &SizeInfo) { + for graphic in graphics { + let mut texture = 0; + + unsafe { + gl::GenTextures(1, &mut texture); + trace!("Texture generated: {}", texture); + + gl::BindTexture(gl::TEXTURE_2D, texture); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAX_LEVEL, 0); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as GLint); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as GLint); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as GLint); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as GLint); + + let pixel_format = match graphic.color_type { + ColorType::Rgb => gl::RGB, + ColorType::Rgba => gl::RGBA, + }; + + gl::TexImage2D( + gl::TEXTURE_2D, + 0, + gl::RGBA as GLint, + graphic.width as GLint, + graphic.height as GLint, + 0, + pixel_format, + gl::UNSIGNED_BYTE, + graphic.pixels.as_ptr().cast(), + ); + + gl::BindTexture(gl::TEXTURE_2D, 0); + } + + let graphic_texture = GraphicTexture { + texture: TextureName(texture), + cell_height: size_info.cell_height(), + width: graphic.width as u16, + height: graphic.height as u16, + }; + + self.graphic_textures.insert(graphic.id, graphic_texture); + } + } + + /// Draw graphics in the display. + #[inline] + pub fn draw(&mut self, render_list: RenderList, size_info: &SizeInfo) { + if !render_list.is_empty() { + render_list.draw(self, size_info); + } + } +} diff --git a/alacritty/src/renderer/graphics/shader.rs b/alacritty/src/renderer/graphics/shader.rs new file mode 100644 index 00000000..8d30d2a3 --- /dev/null +++ b/alacritty/src/renderer/graphics/shader.rs @@ -0,0 +1,199 @@ +use std::mem; + +use crate::gl; +use crate::gl::types::*; +use crate::renderer; + +/// Number of elements of the `textures[]` uniform. +/// +/// If the file `graphics.f.glsl` is modified, this value has to be updated. +pub(super) const TEXTURES_ARRAY_SIZE: usize = 16; + +/// Sides where the vertex is located. +/// +/// * Bit 0 (LSB) is 0 for top and 1 for bottom. +/// * Bit 1 is 0 for left and 1 for right. +#[derive(Debug, Copy, Clone, PartialEq)] +#[repr(u8)] +pub enum VertexSide { + TopLeft = 0b00, + TopRight = 0b10, + BottomLeft = 0b01, + BottomRight = 0b11, +} + +/// Vertex data to execute the graphics rendering program. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Vertex { + /// Texture associated to the graphic. + pub texture_id: GLuint, + + /// Sides where the vertex is located. + pub sides: VertexSide, + + /// Column number in the grid where the left vertex is set. + pub column: GLuint, + + /// Line where the top vertex is set. + pub line: GLuint, + + /// Height, in pixels, of the texture. + pub height: u16, + + /// Width, in pixels, of the texture. + pub width: u16, + + /// Offset in the x direction. + pub offset_x: u16, + + /// Offset in the y direction. + pub offset_y: u16, + + /// Height, in pixels, of a single cell when the graphic was added. + pub base_cell_height: f32, +} + +/// Sources for the graphics rendering program. +static GRAPHICS_SHADER_F: &str = include_str!("../../../res/graphics.f.glsl"); +static GRAPHICS_SHADER_V: &str = include_str!("../../../res/graphics.v.glsl"); + +/// Graphics rendering program. +#[derive(Debug)] +pub struct GraphicsShaderProgram { + /// Program id. + pub id: GLuint, + + /// Uniform of the cell dimensions. + pub u_cell_dimensions: GLint, + + /// Uniform of the view dimensions. + pub u_view_dimensions: GLint, + + /// Uniform array of the textures. + pub u_textures: Vec<GLint>, + + /// Vertex Array Object (VAO) for the fields of `Vertex`. + pub vao: GLuint, + + /// Vertex Buffer Object (VBO) to send instances of `Vertex`. + pub vbo: GLuint, +} + +impl GraphicsShaderProgram { + pub fn new() -> Result<Self, renderer::ShaderCreationError> { + let vertex_shader = renderer::create_shader(gl::VERTEX_SHADER, GRAPHICS_SHADER_V)?; + let fragment_shader = renderer::create_shader(gl::FRAGMENT_SHADER, GRAPHICS_SHADER_F)?; + let program = renderer::create_program(vertex_shader, fragment_shader)?; + + let u_cell_dimensions; + let u_view_dimensions; + let u_textures; + + unsafe { + gl::DeleteShader(fragment_shader); + gl::DeleteShader(vertex_shader); + + gl::UseProgram(program); + + // Uniform locations. + + macro_rules! uniform { + ($name:literal) => { + gl::GetUniformLocation( + program, + concat!($name, "\0").as_bytes().as_ptr().cast(), + ) + }; + + ($fmt:literal, $($arg:tt)+) => { + match format!(concat!($fmt, "\0"), $($arg)+) { + name => gl::GetUniformLocation( + program, + name.as_bytes().as_ptr().cast(), + ) + } + }; + } + + u_cell_dimensions = uniform!("cellDimensions"); + u_view_dimensions = uniform!("viewDimensions"); + u_textures = + (0..TEXTURES_ARRAY_SIZE).map(|unit| uniform!("textures[{}]", unit)).collect(); + + gl::UseProgram(0); + } + + let (vao, vbo) = define_vertex_attributes(); + + let shader = + Self { id: program, u_cell_dimensions, u_view_dimensions, u_textures, vao, vbo }; + + Ok(shader) + } +} + +/// Build a Vertex Array Object (VAO) and a Vertex Buffer Object (VBO) for +/// instances of the `Vertex` type. +fn define_vertex_attributes() -> (GLuint, GLuint) { + let mut vao = 0; + let mut vbo = 0; + + unsafe { + gl::GenVertexArrays(1, &mut vao); + gl::GenBuffers(1, &mut vbo); + + gl::BindVertexArray(vao); + gl::BindBuffer(gl::ARRAY_BUFFER, vbo); + + let mut attr_index = 0; + + macro_rules! int_attr { + ($type:ident, $field:ident) => { + gl::VertexAttribIPointer( + attr_index, + 1, + gl::$type, + mem::size_of::<Vertex>() as i32, + memoffset::offset_of!(Vertex, $field) as *const _, + ); + + attr_index += 1; + }; + } + + macro_rules! float_attr { + ($type:ident, $field:ident) => { + gl::VertexAttribPointer( + attr_index, + 1, + gl::$type, + gl::FALSE, + mem::size_of::<Vertex>() as i32, + memoffset::offset_of!(Vertex, $field) as *const _, + ); + + attr_index += 1; + }; + } + + int_attr!(UNSIGNED_INT, texture_id); + int_attr!(UNSIGNED_BYTE, sides); + + float_attr!(UNSIGNED_INT, column); + float_attr!(UNSIGNED_INT, line); + float_attr!(UNSIGNED_SHORT, height); + float_attr!(UNSIGNED_SHORT, width); + float_attr!(UNSIGNED_SHORT, offset_x); + float_attr!(UNSIGNED_SHORT, offset_y); + float_attr!(FLOAT, base_cell_height); + + for index in 0..attr_index { + gl::EnableVertexAttribArray(index); + } + + gl::BindVertexArray(0); + gl::BindBuffer(gl::ARRAY_BUFFER, 0); + } + + (vao, vbo) +} diff --git a/alacritty/src/renderer/mod.rs b/alacritty/src/renderer/mod.rs index 0173769c..fba47c40 100644 --- a/alacritty/src/renderer/mod.rs +++ b/alacritty/src/renderer/mod.rs @@ -14,6 +14,7 @@ use fnv::FnvHasher; use log::{error, info}; use unicode_width::UnicodeWidthChar; +use alacritty_terminal::graphics::UpdateQueues; use alacritty_terminal::index::Point; use alacritty_terminal::term::cell::Flags; use alacritty_terminal::term::color::Rgb; @@ -24,8 +25,10 @@ use crate::config::ui_config::{Delta, UiConfig}; use crate::display::content::RenderableCell; use crate::gl; use crate::gl::types::*; +use crate::renderer::graphics::GraphicsRenderer; use crate::renderer::rects::{RectRenderer, RenderRect}; +pub mod graphics; pub mod rects; // Shader source. @@ -439,6 +442,7 @@ pub struct QuadRenderer { batch: Batch, rect_renderer: RectRenderer, + graphics_renderer: GraphicsRenderer, } #[derive(Debug)] @@ -638,6 +642,7 @@ impl QuadRenderer { let mut renderer = Self { program, rect_renderer: RectRenderer::new()?, + graphics_renderer: GraphicsRenderer::new()?, vao, ebo, vbo_instance, @@ -682,6 +687,19 @@ impl QuadRenderer { } } + /// Run the required actions to apply changes for the graphics in the grid. + #[inline] + pub fn graphics_run_updates(&mut self, update_queues: UpdateQueues, size_info: &SizeInfo) { + self.graphics_renderer.run_updates(update_queues, size_info); + } + + /// Draw graphics visible in the display. + #[inline] + pub fn graphics_draw(&mut self, render_list: graphics::RenderList, size_info: &SizeInfo) { + self.graphics_renderer.draw(render_list, size_info); + self.active_tex = 0; + } + pub fn with_api<F, T>(&mut self, config: &UiConfig, props: &SizeInfo, func: F) -> T where F: FnOnce(RenderApi<'_>) -> T, @@ -843,6 +861,7 @@ impl<'a> RenderApi<'a> { point: Point::new(point.line, point.column + i), character, zerowidth: None, + graphic: None, flags: Flags::empty(), bg_alpha: 1.0, fg, diff --git a/alacritty/src/url.rs b/alacritty/src/url.rs new file mode 100644 index 00000000..84b8584f --- /dev/null +++ b/alacritty/src/url.rs @@ -0,0 +1,276 @@ +use std::cmp::min; +use std::mem; + +use crossfont::Metrics; +use glutin::event::{ElementState, ModifiersState}; +use urlocator::{UrlLocation, UrlLocator}; + +use alacritty_terminal::index::{Column, Point}; +use alacritty_terminal::term::cell::Flags; +use alacritty_terminal::term::color::Rgb; +use alacritty_terminal::term::SizeInfo; + +use crate::config::Config; +use crate::display::content::RenderableCell; +use crate::event::Mouse; +use crate::renderer::rects::{RenderLine, RenderRect}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Url { + lines: Vec<RenderLine>, + end_offset: u16, + num_cols: Column, +} + +impl Url { + pub fn rects(&self, metrics: &Metrics, size: &SizeInfo) -> Vec<RenderRect> { + let end = self.end(); + self.lines + .iter() + .filter(|line| line.start <= end) + .map(|line| { + let mut rect_line = *line; + rect_line.end = min(line.end, end); + rect_line.rects(Flags::UNDERLINE, metrics, size) + }) + .flatten() + .collect() + } + + pub fn start(&self) -> Point { + self.lines[0].start + } + + pub fn end(&self) -> Point { + self.lines[self.lines.len() - 1].end.sub(self.num_cols, self.end_offset as usize) + } +} + +pub struct Urls { + locator: UrlLocator, + urls: Vec<Url>, + scheme_buffer: Vec<(Point, Rgb)>, + last_point: Option<Point>, + state: UrlLocation, +} + +impl Default for Urls { + fn default() -> Self { + Self { + locator: UrlLocator::new(), + scheme_buffer: Vec::new(), + urls: Vec::new(), + state: UrlLocation::Reset, + last_point: None, + } + } +} + +impl Urls { + pub fn new() -> Self { + Self::default() + } + + // Update tracked URLs. + pub fn update(&mut self, num_cols: Column, cell: &RenderableCell) { + let point = cell.point; + let mut end = point; + + // Include the following wide char spacer. + if cell.flags.contains(Flags::WIDE_CHAR) { + end.column += 1; + } + + // Reset URL when empty cells have been skipped. + if point != Point::default() && Some(point.sub(num_cols, 1)) != self.last_point { + self.reset(); + } + + self.last_point = Some(end); + + // Extend current state if a leading wide char spacer is encountered. + if cell.flags.intersects(Flags::LEADING_WIDE_CHAR_SPACER) { + if let UrlLocation::Url(_, mut end_offset) = self.state { + if end_offset != 0 { + end_offset += 1; + } + + self.extend_url(point, end, cell.fg, end_offset); + } + + return; + } + + // Advance parser. + let last_state = mem::replace(&mut self.state, self.locator.advance(cell.character)); + match (self.state, last_state) { + (UrlLocation::Url(_length, end_offset), UrlLocation::Scheme) => { + // Create empty URL. + self.urls.push(Url { lines: Vec::new(), end_offset, num_cols }); + + // Push schemes into URL. + for (scheme_point, scheme_fg) in self.scheme_buffer.split_off(0) { + self.extend_url(scheme_point, scheme_point, scheme_fg, end_offset); + } + + // Push the new cell into URL. + self.extend_url(point, end, cell.fg, end_offset); + }, + (UrlLocation::Url(_length, end_offset), UrlLocation::Url(..)) => { + self.extend_url(point, end, cell.fg, end_offset); + }, + (UrlLocation::Scheme, _) => self.scheme_buffer.push((cell.point, cell.fg)), + (UrlLocation::Reset, _) => self.reset(), + _ => (), + } + + // Reset at un-wrapped linebreak. + if cell.point.column + 1 == num_cols && !cell.flags.contains(Flags::WRAPLINE) { + self.reset(); + } + } + + /// Extend the last URL. + fn extend_url(&mut self, start: Point, end: Point, color: Rgb, end_offset: u16) { + let url = self.urls.last_mut().unwrap(); + + // If color changed, we need to insert a new line. + if url.lines.last().map(|last| last.color) == Some(color) { + url.lines.last_mut().unwrap().end = end; + } else { + url.lines.push(RenderLine { color, start, end }); + } + + // Update excluded cells at the end of the URL. + url.end_offset = end_offset; + } + + /// Find URL below the mouse cursor. + pub fn highlighted( + &self, + config: &Config, + mouse: &Mouse, + mods: ModifiersState, + mouse_mode: bool, + selection: bool, + ) -> Option<Url> { + // Require additional shift in mouse mode. + let mut required_mods = config.ui_config.mouse.url.mods(); + if mouse_mode { + required_mods |= ModifiersState::SHIFT; + } + + // Make sure all prerequisites for highlighting are met. + if selection + || !mouse.inside_text_area + || config.ui_config.mouse.url.launcher.is_none() + || required_mods != mods + || mouse.left_button_state == ElementState::Pressed + { + return None; + } + + self.find_at(Point::new(mouse.line, mouse.column)) + } + + /// Find URL at location. + pub fn find_at(&self, point: Point) -> Option<Url> { + for url in &self.urls { + if (url.start()..=url.end()).contains(&point) { + return Some(url.clone()); + } + } + None + } + + fn reset(&mut self) { + self.locator = UrlLocator::new(); + self.state = UrlLocation::Reset; + self.scheme_buffer.clear(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use alacritty_terminal::index::{Column, Line}; + + fn text_to_cells(text: &str) -> Vec<RenderableCell> { + text.chars() + .enumerate() + .map(|(i, character)| RenderableCell { + character, + zerowidth: None, + graphic: None, + point: Point::new(Line(0), Column(i)), + fg: Default::default(), + bg: Default::default(), + bg_alpha: 0., + flags: Flags::empty(), + is_match: false, + }) + .collect() + } + + #[test] + fn multi_color_url() { + let mut input = text_to_cells("test https://example.org ing"); + let num_cols = input.len(); + + input[10].fg = Rgb { r: 0xff, g: 0x00, b: 0xff }; + + let mut urls = Urls::new(); + + for cell in input { + urls.update(Column(num_cols), &cell); + } + + let url = urls.urls.first().unwrap(); + assert_eq!(url.start().column, Column(5)); + assert_eq!(url.end().column, Column(23)); + } + + #[test] + fn multiple_urls() { + let input = text_to_cells("test git:a git:b git:c ing"); + let num_cols = input.len(); + + let mut urls = Urls::new(); + + for cell in input { + urls.update(Column(num_cols), &cell); + } + + assert_eq!(urls.urls.len(), 3); + + assert_eq!(urls.urls[0].start().column, Column(5)); + assert_eq!(urls.urls[0].end().column, Column(9)); + + assert_eq!(urls.urls[1].start().column, Column(11)); + assert_eq!(urls.urls[1].end().column, Column(15)); + + assert_eq!(urls.urls[2].start().column, Column(17)); + assert_eq!(urls.urls[2].end().column, Column(21)); + } + + #[test] + fn wide_urls() { + let input = text_to_cells("test https://こんにちは (http:여보세요) ing"); + let num_cols = input.len() + 9; + + let mut urls = Urls::new(); + + for cell in input { + urls.update(Column(num_cols), &cell); + } + + assert_eq!(urls.urls.len(), 2); + + assert_eq!(urls.urls[0].start().column, Column(5)); + assert_eq!(urls.urls[0].end().column, Column(17)); + + assert_eq!(urls.urls[1].start().column, Column(20)); + assert_eq!(urls.urls[1].end().column, Column(28)); + } +} 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. diff --git a/alacritty_terminal/tests/sixel/README.md b/alacritty_terminal/tests/sixel/README.md new file mode 100644 index 00000000..c01deae9 --- /dev/null +++ b/alacritty_terminal/tests/sixel/README.md @@ -0,0 +1,50 @@ +# Images for Sixel tests + +The files in this directory are used to test the Sixel parser against images +generated by real-world software: + +* `testimage_im6.sixel` + + Generated with ImageMagick 6.9.10. + + ```bash + convert "$SOURCE" testimage_im6.sixel + ``` + +* `testimage_libsixel.sixel` + + Generated with libsixel 1.8.6. + + ```bash + img2sixel -o testimage_libsixel.sixel "$SOURCE" + ``` + +* `testimage_ppmtosixel.sixel` + + Generated with Netpbm 10. + + ```bash + pngtopnm "$SOURCE" \ + | ppmquant 256 \ + | ppmtosixel \ + > testimage_ppmtosixel.sixel + ``` + +In the previous commands, `$SOURCE` is defined as: + + SOURCE="$(git rev-parse --show-toplevel)/alacritty/alacritty.png" + +The image is the [Alacritty icon](../../../extra/logo/compat/alacritty-term.png). + +To verify the results of the parser we include the raw RGBA data for each +image. It was generated with this script: + +```bash +for IMG in *.sixel +do + convert "$IMG" png:- | convert -depth 8 png:- rgba:"${IMG/.sixel/.rgba}" +done +``` + +We have to convert the Sixel to PNG before RGBA because, for some reason, +ImageMagick can't transform from Sixel to RGBA directly. diff --git a/alacritty_terminal/tests/sixel/testimage_im6.rgba b/alacritty_terminal/tests/sixel/testimage_im6.rgba Binary files differnew file mode 100644 index 00000000..af6abfa3 --- /dev/null +++ b/alacritty_terminal/tests/sixel/testimage_im6.rgba diff --git a/alacritty_terminal/tests/sixel/testimage_im6.sixel b/alacritty_terminal/tests/sixel/testimage_im6.sixel new file mode 100644 index 00000000..80752a75 --- /dev/null +++ b/alacritty_terminal/tests/sixel/testimage_im6.sixel @@ -0,0 +1 @@ +P0;0;0q"1;1;64;64#0;2;3;5;6#1;2;3;6;7#2;2;4;6;8#3;2;8;8;8#4;2;11;11;11#5;2;6;11;13#6;2;8;14;17#7;2;10;15;16#8;2;11;16;16#9;2;9;15;19#10;2;8;16;21#11;2;8;17;22#12;2;14;14;14#13;2;16;15;15#14;2;18;16;15#15;2;17;17;17#16;2;20;18;15#17;2;19;14;15#18;2;20;19;15#19;2;22;21;14#20;2;20;20;20#21;2;23;23;23#22;2;36;14;12#23;2;33;18;12#24;2;35;22;12#25;2;48;17;9#26;2;39;22;11#27;2;50;20;9#28;2;26;18;13#29;2;32;17;13#30;2;26;24;13#31;2;29;22;13#32;2;35;26;12#33;2;40;28;11#34;2;41;32;11#35;2;46;36;10#36;2;44;31;10#37;2;7;24;35#38;2;18;30;37#39;2;6;29;43#40;2;7;27;39#41;2;6;30;45#42;2;19;31;38#43;2;27;27;27#44;2;30;30;30#45;2;33;33;33#46;2;36;36;36#47;2;39;39;39#48;2;42;42;42#49;2;45;45;45#50;2;49;49;49#51;2;51;23;9#52;2;65;16;6#53;2;66;20;6#54;2;82;20;3#55;2;88;17;1#56;2;93;18;1#57;2;88;15;1#58;2;89;20;1#59;2;93;20;1#60;2;89;24;1#61;2;93;23;1#62;2;53;27;9#63;2;53;27;9#64;2;55;32;8#65;2;57;36;8#66;2;68;25;5#67;2;70;31;5#68;2;72;36;5#69;2;63;25;7#70;2;66;32;6#71;2;58;39;7#72;2;68;39;6#73;2;73;41;5#74;2;71;47;5#75;2;76;30;4#76;2;84;26;3#77;2;86;34;2#78;2;89;27;1#79;2;94;26;1#80;2;90;29;0#81;2;92;30;1#82;2;94;28;1#83;2;94;30;1#84;2;90;33;0#85;2;90;36;0#86;2;93;31;1#87;2;95;33;0#88;2;95;36;0#89;2;78;38;4#90;2;87;39;2#91;2;77;47;4#92;2;82;47;3#93;2;92;40;1#94;2;91;42;0#95;2;91;38;0#96;2;95;39;0#97;2;96;42;0#98;2;89;45;2#99;2;91;45;0#100;2;92;49;0#101;2;96;45;0#102;2;96;48;0#103;2;81;49;41#104;2;81;47;40#105;2;78;53;4#106;2;85;56;3#107;2;92;52;0#108;2;92;55;0#109;2;91;53;2#110;2;96;52;0#111;2;97;54;0#112;2;97;55;0#113;2;92;58;0#114;2;93;61;0#115;2;97;58;0#116;2;98;61;0#117;2;97;59;0#118;2;93;64;0#119;2;98;64;0#120;2;98;67;0#121;2;94;66;0#122;2;81;51;41#123;2;82;52;41#124;2;82;55;41#125;2;82;58;41#126;2;83;61;41#127;2;83;64;41#128;2;83;67;41#129;2;84;70;41#130;2;84;71;40#131;2;5;35;53#132;2;5;40;62#133;2;5;42;64#134;2;4;46;71#135;2;8;45;69#136;2;4;48;75#137;2;35;51;61#138;2;35;53;64#139;2;4;51;80#140;2;5;56;88#141;2;3;60;97#142;2;2;62;100#143;2;4;62;100#144;2;4;59;95#145;2;6;63;100#146;2;10;65;100#147;2;13;66;100#148;2;22;70;100#149;2;35;75;100#150;2;43;78;100#151;2;49;80;100#152;2;52;52;52#153;2;54;54;54#154;2;58;58;58#155;2;61;61;61#156;2;65;65;65#157;2;66;66;66#158;2;71;71;71#159;2;54;71;82#160;2;73;85;93#161;2;65;85;98#162;2;67;84;94#163;2;75;90;100#164;2;78;78;78#165;2;76;91;100#166;2;87;95;100#167;2;91;96;100#168;2;94;98;100#169;2;91;96;100#170;2;100;100;100#171;2;96;98;100#172;2;0;0;0#172~~~N!56FN~~~$#57???OO#55OO#58!4O#60OOO#78OOO#80!4O#84O#81O#88!4G#96GGG#97GG#94!4O#99!5O#107O?OO?O#128?!7_#129___#130___$#104???_#103!5_??_#123!7_!4?_#125_?!5_?_#102???GGG#127_?!7_#116??GGG#114!4O#118OOO#121O$#56!4?GG#59GGG#61GGG#79GG?G#83GGG#124!4_??_#95???OO#126_?!6_?_#100?O#111?G#108O?!4O#113OO#119!4G#120GGG$#122!9?__#82???G#87!4?!4G#86O#85!5O#93??O#101!4G#110???!5G#112?G#115!4G-#156G][MCEECCEECECEECEECEC!4ECECEECECEECECEEECACCEECECECCEECECM[]KW$#157C?A@B@@BB@@B@B@@B@@B@B!4@B@B@@B@B@@B@B@@@B@BB@@B@B@BB@@B@B@B?AC$#155o__oG#4O?_??_#152!4?G!5?G!5?GG#2!4?O!9?O!6?O!6?O#0_#12_#155__o_$#164A@#158@#12?_#0_#153!9G?!5G?!5G??!19G?!10G#47O#158?@#164@A$#172@#47???O#154G#5_?__?!4_?!9_?!9_?__?!14_?!4_#4O#172!4?@$#2!6?O??O?O?O??O#0!6?O#1!11?O!9?O#152??G$#3!7?OO?O?O?Oo?!6O?Oo!7O?O?OOo!4O?O?!4O?OOoOOO$#4!35?_#155!7?C-#154wgoo#4@#2~#6!15~n~~{~^B!8?B^!5~n!14~#1B#4A#154wgow$#155EVNN#7A#9!16?O??A#29?_??@@!6?_#9!5?O#2!14?{#7O#155FVNE$#156@#12???{#5!19?@#13??C!7?@C#12!22?l#156???@$#25!27?G!8?G$#54!27?O!8?O$#61!27?!7_o__$#52!28?A!6?A$#56!28?CEEEMEEC$#59!28?!4WOWGW$#17!28?@#22??!4@-#153{wY{#12B#1K#6!6~v!11~F@#79EECAAA!4?CE#6@N!18~#2~#12T#153]ww{$#154BFDB#13{#2r#9!6?G#7!11?G#13A!5?__!5?A#23O#13!19?i#154@FFB$#152??_#29!21?O#76G!5?GG!5?G#66_#152!20?_$#66!24?_#83Oooo_O??Oo_ooO$#27!25?C#82GGG[GCCMG]GG#27C$#87!25?_#53@#61@B@D@B@F@B#53@#87_$#81!30?_??_$#51!31?OO-#50goo_#13E#0O#6!12~n~~^F!6?_{^^{_!6?F^!8~v!6~#1B#14s#50o_oo$#152VMH^#14g#2N#9!12?O#14??_?@#87AFBABB!4?BBABBE#17@#24G#15_#9!8?G#2!6?{#15J#152N^EN$#153?@A#15?@#3_#96!16?___o__!8?__o___#137!20?G$??C#16?O#24!17?G#88W[G[[K!6?K[[KWW#67O#153!20?@$#67!22?O#51A#86@#83??@!8?@#86??@#51A$#77!23?C#62!4?_!6?_$#85!28?O#7O!4?O#77O!4?C$#26!29?G!4?G$#75!29?C!4?C$#28!30?A#10_#9_#19A$#69!30?@#53??@-#49{wuw#14B#2~#6~~z!8~^~N@!6?w}NB??BN}g!6?@N!13~#0A#15[#49ysws$#50BFHF#15S#9???C#7!8?_??A#97CFEEME#31C!8?C#7_!5?A#18O#1!13?@#16@#50DJFJ$#20!4?g#16!14?O#101o!4WO#8_#70A!8?A#97!5EC#24C#64_#2!13?{#20_$#64!19?_#24C#102!4_#98_#33O#13?@#138_!4?_#14@#5O#96!5@#63@#68G#28!15?A$#68!20?G#63@#93?@???@!8?@#33O#101WWWwWO$#90!21?A#96??@@@#10??O#159G??G#11O#89??G#98_#102__?__$#89!26?G#170???_{{_#90!8?A$#38!30?C#137@@#42C$#167!30?O#160AA#169O-#47o__O#15@#2~#6!10~^F!6?o{~~@!6?@~~wo!6?F^!7~z~~#2~#15H#48}]K^$M[^m#20}#8!11?_#14G#110GIMKKC#31G#18A#37??A!6?A#9??C#102BBB@B#98@#7@#19G#8_#9!7?C#20???u#49@@B$@B?@#65!13?O#111o!4OG#72C#64@#40??_!6?_#16??A#31G#110CKMKMG#65O#47!14?_o_$#109!17?_#7@#102C@!4B#133???O!6?O#64??@#72C#111G!4Oo#109_$#33!18?A#112!4_#36_#135!4?C!6?C#36!4?_#112!4_#26A$#73!18?C#98@#92???O#140!4?G!6?G#92!4?O#73!4?C$#145!29?_!4?_$#146!29?O!4?O$#148!29?G!4?G$#151!29?C!4?C$#162!29?@!4?@$#165!29?A!4?A$#149!30?_??_$#161!30?O??O$#166!30?G??G$#168!30?C??C$#170!30?BNNB$#167!31?__$#171!31?OO-#46!4w#20b#1G#6!7~|NB!5?_w}!4~}o!4?o}!4~}w!6?BN|!7~#2~#20N#46wwws$#47!4F#21[#2v#9!7?A#8O!8?@#9!4?@!6?@!6?_!7?A#21!8?o#47FFFJ$#34!14?_#116_WIW[[#32O#18C#11!6?G!4?G#8!5?@!8?O$#19!15?C#74A!4?G#65A#134!6?A???OA#71!6?A!6?G#34_$#71!15?G#115CDEBAF#108@#144!6?@!4?@#14!6?C#74G!4?A#19C$#113!15?O#119_o__#106_#41!8?C#131_??_#39C#109!6?@#115@?AFF#33@#114O$#33!16?@#112?@?@#141!9?G??G#117!8?EEC$#145!30?A??E#32!8?O#119O!5_$#147!30?@??@#116!9?GWWW[$#143!30?C?G#112!10?@@$#136!30?O#142wo#106!10?_$#146!31?CC$#150!31?AA$#165!31?@#163@-#44O___#21~#2~#6!8~{{{[{{{}!8~W??{!9~!6{s!5~^~~#2~#21~#44_?O_$#45m][^#30!10?!6A#8A#37!9?@_?@#8!9?A#30!6A#5!5?_#45!4?]}n]$#46@@B#105!11?@#119!5@#35@#5!9?_#39OO#35!10?@#119!5@#9G#46!10?@@?@$#9!17?_???@!8?E??A#105!15?@$#132!31?GG$#141!31?AA$#136!31?C#11_$#142!31?@#139C$#145!32?@-#44BJBBC#48C?C??C#9!5?@#5@??@?@!7?@@#38??_#30__#48???C?C!8?C!6?C#44KFFJF$#46G?_#49?A?C?CC?!10C?!5C?!12C?C?!8C?!6C?A#45?_#172_o$o_#1???@#3!25A?AAA?!8A?!4A?!8A#2@#31_#46???G$#45C#43S[{x!26wowwWOW!21wWPwWS$#15!5?A#6!10@??@@?@?!7@??!12@?@?@?!9@#15A$#48!21?C!5?C#44???G???G#4!8?A!4?A#38!8?_$#4!31?A#2???A#5!8?@?@?@-#172!64N-\
\ No newline at end of file diff --git a/alacritty_terminal/tests/sixel/testimage_libsixel.rgba b/alacritty_terminal/tests/sixel/testimage_libsixel.rgba Binary files differnew file mode 100644 index 00000000..a9e1d6c7 --- /dev/null +++ b/alacritty_terminal/tests/sixel/testimage_libsixel.rgba diff --git a/alacritty_terminal/tests/sixel/testimage_libsixel.sixel b/alacritty_terminal/tests/sixel/testimage_libsixel.sixel new file mode 100644 index 00000000..7063a4b0 --- /dev/null +++ b/alacritty_terminal/tests/sixel/testimage_libsixel.sixel @@ -0,0 +1 @@ +Pq"1;1;64;64#0;2;0;0;0#1;2;16;3;0#2;2;25;6;0#3;2;25;9;0#4;2;28;16;0#5;2;28;19;0#6;2;50;19;6#7;2;91;16;0#8;2;91;19;0#9;2;91;22;0#10;2;94;25;0#11;2;94;28;0#12;2;94;31;0#13;2;94;35;0#14;2;94;38;0#15;2;94;41;0#16;2;94;44;0#17;2;94;47;0#18;2;94;50;0#19;2;97;53;0#20;2;97;56;0#21;2;97;60;0#22;2;97;63;0#23;2;63;44;0#24;2;16;0;0#25;2;88;19;0#26;2;88;25;0#27;2;88;28;0#28;2;91;31;0#29;2;88;35;0#30;2;91;38;0#31;2;91;41;0#32;2;91;44;0#33;2;91;50;0#34;2;94;53;0#35;2;91;56;0#36;2;91;60;0#37;2;25;13;0#38;2;16;16;16#39;2;50;50;50#40;2;78;47;41#41;2;82;50;41#42;2;82;53;41#43;2;82;56;41#44;2;82;60;41#45;2;82;63;41#46;2;82;66;41#47;2;82;69;41#48;2;53;53;53#49;2;28;28;28#50;2;66;66;66#51;2;56;56;56#52;2;38;38;38#53;2;6;9;13#54;2;6;6;6#55;2;63;63;63#56;2;3;6;6#57;2;6;13;16#58;2;19;16;13#59;2;35;13;9#60;2;63;25;6#61;2;47;16;9#62;2;28;22;13#63;2;50;25;6#64;2;25;22;13#65;2;85;31;0#66;2;75;28;3#67;2;9;13;16#68;2;41;25;9#69;2;9;16;16#70;2;35;50;63#71;2;72;85;91#72;2;47;47;47#73;2;6;28;41#74;2;97;97;97#75;2;53;69;82#76;2;19;19;13#77;2;94;97;97#78;2;56;38;6#79;2;44;44;44#80;2;19;19;19#81;2;6;22;35#82;2;85;94;97#83;2;6;25;38#84;2;69;44;3#85;2;6;44;69#86;2;47;78;97#87;2;0;60;94#88;2;13;66;97#89;2;88;50;0#90;2;44;28;9#91;2;3;63;97#92;2;41;75;97#93;2;22;22;22#94;2;35;35;35#95;2;31;31;31#0~^NF!56BFN^~$#38?_#24O#25!8O#10??GGG#11!4G#12GGG#13GGG#14!4G#31!6O#17GGGWOOO#33OOO#34!4O#21GGG#22!4GWWWO#37O#38_$#39??_#6G#41!13_#42!8_#43!7_#44!8_#45!8_#46!7_#47!6_#48_$#40???_#7GG#8GGG#9GGWWO#26!4O#27OOO#3!12C#4!19C#5!7C#1C#23G$#1!4?C#2!16C#28!4O#29OOO#30OO#15GGG#16GGG#32OOO#18?GGG#19GG#20!5G#35OOO#36!4O-#50]^^^N!54FN^^^]$#55!4_#38_#53O!52_O#38_#55!4_$#49@#52???O#51G#48!52G#51G#52O#49???@$#56!5?_#54!52O#56_-#51!4o#38}#56~#57!20~^B!8?B^!20~#56~#38}#51!4o$#55!4N#53@#59!21?_??!6@??_#53!21?@#55!4N$#9!27?!10_$#25!27?O!8?O$#38!27?C!8?C$#61!27?G!8?G$#7!28?C!6EC$#8!28?!8W$#58!28?@!6?@$#60!28?A!6?A-#48!4{#38~#56~#57!18~N@#10!12E#57@N!18~#56~#38~#48!4{$#51!4B#60!20?_?@#9!10@#60@?_#51!20?!4B$#62!24?O#6C!5?OO!5?C#59O$#11!25?o!4wW??W!4wo$#26!25?G!5?GG!5?G$#38!25?A!5?__!5?A$#28!30?_??_-#39!4o#38~#56~#57!15~^F!6?_{^^{_!6?B^!15~#56~#38~#39!4o$#48!4N#38!17?_?@#12!6B!4?!6B#38@?_#48!17?!4N$#14!22?!6_!8?!6_$#66!22?O!6?C!4?C!6?O$#62!22?G#13W!4[K!6?K!4[W#62G$#63!23?A!4?_!6?_!4?A$#65!23?C#29!4?O!6?O#65!4?C$#67!29?O!4?O!6?C$#68!29?G!4?G$#60!30?@??@$#64!30?A??A$#69!31?__-#39!4B#38~#56~#57!13~N@!6?w}NB??BN}w!6?@N!13~#56~#38~#39!4B$#72!4{#76!15?O#67A!5?_#31@!8?@#66G!5?G#76O#72!15?!4{$#78!19?_#68C!5?O#60A!8?A#67_!5?A#78_$#16!20?O!5W#65G#62C!8?C#68O!5?C$#17!20?!5_#32_#38??@!6?@#15?!5EC$#66!20?G#15C!5E#69??O!4?O#14??!5@#29A$!21?A#14!5@#70??_?@@?_#16???!5WO$#63!21?@#73!8?C??C#32!4?_#17!5_$#74!30?_{{_#63!8?@$#75!30?G??G$#77!30?O??O$#71!31?AA-#52!4_#80~#56~#57!10~^F!6?o{~~@!6?@~~{o!6?F^!10~#56~#80~#52!4_$#79!4^#67!12?_?@#17A!5B#83???_!6?a#78??@!7?O#79!13?!4^$#76!17?G!7?A#85??S!6?S#38??A#84C!5?C#33_$#78!17?O!7?@#87??G!6?G#17???!5BA#67@?_$#33!17?_#84C!5?C#81???A#71@O??O@#62!4?G#18!5KG#76G$#18!18?G!5K#62G#82!4?AG??GA#89!5?O#19!5O$!18?!5O#89O#86!5?C!4?C#90!5?_#20!5_$!18?!5_#90_#88!5?W!4?W#32!9?@#68A$!18?A#32@#91!9?_!4?_$#74!30?B^^B$#77!30?C__C$#92!30?_??_-#52!4~#80N#56~#57!8~NB!5?_w}!5~o!4?o!5~}w_!5?BN!8~#56~#80N#52!4~$#93!4?o#69!9?O#78G!6?A#67@#69!5?G!4?G!5?@!8?O#93!9?o$#90!14?_#35O!4?_#62O#34@#73!6?C_??_C#78!6?A!6?G#90_$#22!15?!5_#76??C#85!6?AO??OA#33!6?@#20B!4FC#35O$#64!15?C#20C!4FB#87!7?@!4?@#76!6?C#21C!5W#64C$#21!16?!5WC#88!8?@CC@#84!8?G!4?A$!16?A!4?G#91!8?MwwM#62!8?O#89_#22!5_$#68!16?@#92!14?AA#68!14?@$#71!31?@#82@-#52!4@#93~#56~#57!8~!7{!9~}??{!9~!7{!8~#56~#93~#52!4@$#94!4]#64!10?!6A#69A#83!9?@#69__A!9?A#64!6A#94!10?!4]$#95!4_#89!10?@#22!5@#90@#73!10?OO#81@#90!9?@#22!5@#89@#95!10?!4_$#85!31?KK$#91!31?BB-#95FfFFC#38A#54!52A#38A#95CFFfF$#94O?_#72?A?!52C?A#94?_?O$#0_#49WWwx!54wxwWW#0_$#52G#56!4?@#57!52@#56@#52!4?G$#79!5?C!52?C-#0!64N\
\ No newline at end of file diff --git a/alacritty_terminal/tests/sixel/testimage_ppmtosixel.rgba b/alacritty_terminal/tests/sixel/testimage_ppmtosixel.rgba Binary files differnew file mode 100644 index 00000000..902b150d --- /dev/null +++ b/alacritty_terminal/tests/sixel/testimage_ppmtosixel.rgba diff --git a/alacritty_terminal/tests/sixel/testimage_ppmtosixel.sixel b/alacritty_terminal/tests/sixel/testimage_ppmtosixel.sixel new file mode 100644 index 00000000..951f6028 --- /dev/null +++ b/alacritty_terminal/tests/sixel/testimage_ppmtosixel.sixel @@ -0,0 +1,77 @@ +0;0;8q"1;1 +#0;2;0;0;0#1;2;49;49;49#2;2;28;28;28#3;2;14;15;16#4;2;12;12;12#5;2;95;38;0#6;2;61;61;61#7;2;100;100;100#8;2;90;33;0#9;2;59;17;8#10;2;53;22;11#11;2;9;9;9#12;2;36;21;12#13;2;52;72;81#14;2;94;29;1#15;2;96;51;0#16;2;96;45;0#17;2;26;24;13#18;2;54;54;54#19;2;98;62;0#20;2;33;33;33#21;2;38;38;38#22;2;66;66;66#23;2;51;51;51#24;2;29;29;29#25;2;93;20;1#26;2;95;36;0#27;2;97;58;0#28;2;93;59;0#29;2;67;31;5#30;2;95;52;1#31;2;44;44;44#32;2;4;6;8#33;2;56;56;56#34;2;35;35;35#35;2;40;40;40#36;2;8;14;17#37;2;52;52;52#38;2;34;23;16#39;2;91;43;0#40;2;31;31;31#41;2;19;18;16#42;2;93;24;1#43;2;93;18;1#44;2;95;40;0#45;2;89;24;1#46;2;95;35;0#47;2;97;56;0#48;2;70;81;87#49;2;83;64;41#50;2;89;22;1#51;2;85;39;2#52;2;82;58;41#53;2;74;28;4#54;2;97;55;0#55;2;29;21;16#56;2;45;45;45#57;2;94;31;0#58;2;57;57;57#59;2;36;36;36#60;2;42;42;42#61;2;80;39;3#62;2;40;21;13#63;2;33;33;33#64;2;87;17;2#65;2;7;13;16#66;2;32;18;13#67;2;13;13;13#68;2;83;62;41#69;2;93;35;0#70;2;62;62;62#71;2;47;47;47#72;2;15;15;15#73;2;59;59;59#74;2;22;22;22#75;2;93;53;1#76;2;94;28;1#77;2;96;50;0#78;2;51;27;14#79;2;64;64;64#80;2;48;48;48#81;2;87;95;100#82;2;27;27;27#83;2;95;33;0#84;2;48;64;60#85;2;60;60;60#86;2;88;55;21#87;2;77;88;95#88;2;90;34;0#89;2;11;14;16#90;2;97;54;0#91;2;96;48;0#92;2;98;65;0#93;2;27;15;14#94;2;53;53;53#95;2;38;38;38#96;2;44;27;16#97;2;93;22;1#98;2;76;46;14#99;2;96;44;0#100;2;94;24;0#101;2;65;65;65#102;2;95;39;0#103;2;98;61;0#104;2;6;11;13#105;2;50;50;50#106;2;29;29;29#107;2;62;62;62#108;2;12;15;16#109;2;100;59;0#110;2;68;30;5#111;2;98;99;100#112;2;43;43;43#113;2;11;16;16#114;2;76;38;4#115;2;94;41;0#116;2;55;55;55#117;2;71;32;5#118;2;34;34;34#119;2;39;39;39#120;2;2;62;100#121;2;3;5;6#122;2;94;26;1#123;2;93;21;1#124;2;96;43;0#125;2;98;65;0#126;2;51;51;51#127;2;98;59;0#128;2;91;44;0#129;2;30;30;30#130;2;83;61;41#131;2;97;57;0#132;2;89;49;21#133;2;65;32;6#134;2;95;33;0#135;2;44;44;44#136;2;56;56;56#137;2;35;35;35#138;2;91;30;1#139;2;41;41;41#140;2;52;18;9#141;2;3;60;97#142;2;67;26;6#143;2;96;41;0#144;2;32;32;32#145;2;79;38;3#146;2;93;62;0#147;2;8;14;17#148;2;36;14;12#149;2;95;37;0#150;2;95;32;0#151;2;22;16;15#152;2;5;42;64#153;2;46;46;46#154;2;13;13;13#155;2;19;50;70#156;2;34;34;34#157;2;82;55;41#158;2;97;53;0#159;2;58;58;58#160;2;42;42;42#161;2;81;68;43#162;2;69;70;61#163;2;89;29;0#164;2;8;14;17#165;2;96;43;0#166;2;100;100;100#167;2;94;98;100#168;2;15;16;16#169;2;14;14;14#170;2;91;47;0#171;2;63;63;63#172;2;94;29;0#173;2;47;47;47#174;2;26;26;26#175;2;68;85;95#176;2;95;34;0#177;2;7;24;35#178;2;60;60;60#179;2;44;44;44#180;2;7;7;7#181;2;91;53;1#182;2;81;50;41#183;2;94;25;1#184;2;96;47;0#185;2;96;42;0#186;2;98;64;0#187;2;53;53;53#188;2;37;37;37#189;2;94;47;0#190;2;66;36;6#191;2;16;17;17#192;2;17;29;32#193;2;65;65;65 +#0!64@$ +#0!64A$ +#0!4C#123C#43!3C#100!6C#172!7C#69!6C#115!6C#189!7C#30!5C#158C#109!6C#92!7C#109C#0!4C$ +#0!3G#43!3G#25!2G#123G#97!2G#42G#183!2G#122G#76!2G#172G#57!2G#150G#83G#176G#46G#26G#149G#5G#102G#44!2G#143G#124G#165G#99G#16!2G#184G#91!2G#77G#15G#158!2G#90!2G#54G#47G#131G#27G#127G#103!2G#19!2G#186G#125G#92!5G#0!3G$ +#0!2O#64!6O#50!4O#45!4O#163!4O#138O#8!3O#88!4O#69!2O#39!5O#128!2O#170!6O#75!4O#181O#75O#181O#28!5O#146!6O#28O#0!2O$ +#0_#76_#162_#182!12_#157!10_#52!7_#130!5_#68!3_#49!8_#120!2_#161!11_#162_#76_#0_$ +- +#88@#43@#22!60@#43@#88@$ +#48A#101!62A#48A$ +#22C#193!62C#22C$ +#193G#79!4G#136G#18!52G#136G#79!4G#193G$ +#171!4O#21O#11O#180!52O#11O#119O#171!4O$ +#70!4_#169_#121_#104!52_#121_#169_#70!4_$ +- +#107!4@#4@#32@#147!22@#151@#148!6@#151@#147!22@#32@#4@#107!4@$ +#6!4A#154A#32A#147!22A#9A#43!6A#9A#147!22A#32A#154A#6!4A$ +#85!4C#154C#32C#147!21C#72C#43!8C#72C#147!21C#32C#154C#85!4C$ +#178!4G#154G#32G#147!21G#140G#25!8G#140G#147!21G#32G#154G#178!4G$ +#73!4O#67O#32O#147!21O#64O#123!8O#64O#164O#147!20O#32O#67O#73!4O$ +#159!4_#169_#32_#147!20_#66_#97!10_#66_#147!20_#32_#169_#159!4_$ +- +#58!4@#169@#32@#147!20@#142@#42!10@#142@#147!20@#32@#169@#58!4@$ +#136!4A#169A#32A#147!19A#72A#183!12A#72A#147!19A#32A#169A#136!4A$ +#33!4C#72C#32C#147!19C#140C#122!12C#140C#147!19C#32C#72C#33!4C$ +#116!4G#72G#32G#147!18G#164G#45G#76!5G#45!2G#76!5G#45G#164G#147!18G#32G#72G#116!4G$ +#18!4O#72O#32O#147!18O#66O#14!6O#10!2O#14!6O#66O#147!18O#32O#72O#18!4O$ +#94!4_#72_#32_#147!18_#142_#57!5_#138_#72!2_#138_#57!5_#142_#147!18_#32_#72_#94!4_$ +- +#187!4@#168@#32@#147!17@#191@#57@#150!5@#142@#147!2@#142@#150!5@#57@#41@#147!17@#32@#168@#187!4@$ +#37!4A#168A#32A#147!17A#10A#134!6A#93A#147!2A#93A#134!6A#10A#147!17A#32A#168A#37!4A$ +#126!4C#191C#32C#147!16C#164C#8C#46!5C#53C#147!4C#53C#46!5C#8C#164C#147!16C#32C#191C#126!4C$ +#23!4G#191G#32G#147!16G#12G#26!6G#62G#147!4G#62G#26!6G#12G#147!16G#32G#191G#23!4G$ +#105!4O#191O#32O#147!16O#110O#149!5O#88O#89O#147!4O#89O#88O#149!5O#110O#147!16O#32O#191O#105!4O$ +#1!4_#191_#32_#147!15_#41_#5_#102!5_#10_#147!2_#36!2_#147!2_#10_#102!6_#41_#147!15_#32_#191_#1!4_$ +- +#80!4@#191@#32@#147!15@#10@#44!5@#115@#191@#147!2@#84@#80@#147!2@#191@#115@#44!5@#10@#147!15@#32@#191@#80!4@$ +#173!4A#191A#32A#147!14A#164A#51A#185!5A#133A#147!3A#127A#175A#147!3A#133A#185!5A#51A#89A#147!14A#32A#191A#173!4A$ +#71!4C#191C#32C#147!14C#12C#124!6C#55C#147!2C#192C#111C#7C#192C#147!2C#55C#124!6C#12C#147!14C#32C#191C#71!4C$ +#153!4G#41G#32G#147!14G#117G#99!5G#145G#147!3G#13G#166!2G#13G#147!3G#114G#99!5G#117G#147!14G#32G#41G#153!4G$ +#56!4O#41O#32O#147!13O#41O#99O#16!5O#62O#147!2O#36O#81O#166!2O#167O#127O#147!2O#96O#16!6O#41O#147!13O#32O#41O#56!4O$ +#135!4_#41_#32_#147!13_#78_#184!5_#128_#113_#147!2_#155_#7_#166!2_#7_#155_#147!2_#89_#128_#184!5_#78_#147!13_#32_#41_#135!4_$ +- +#31!4@#41@#32@#147!12@#89@#128@#91!5@#78@#147!3@#175@#7@#166!2@#7@#190@#147!3@#78@#91!5@#170@#89@#147!12@#32@#41@#31!4@$ +#112!4A#41A#32A#147!12A#12A#77!6A#41A#147!2A#177A#87A#111A#166!2A#111A#87A#177A#147!2A#41A#77!6A#12A#147!12A#32A#41A#112!4A$ +#160!4C#41C#32C#147!12C#114C#15!5C#190C#147!3C#152C#78C#167C#7!2C#167C#78C#152C#147!3C#190C#15!5C#114C#147!12C#32C#41C#160!4C$ +#60!4G#41G#32G#147!11G#41G#158!6G#17G#147!3G#141G#3G#81G#111!2G#81G#36G#141G#147!3G#38G#158!6G#41G#147!11G#32G#41G#60!4G$ +#139!4O#74O#32O#147!11O#190O#90!5O#175O#164O#147!3O#152O#93O#175O#167!2O#82O#93O#152O#147!4O#61O#90!5O#175O#147!11O#32O#74O#139!4O$ +#35!4_#74_#32_#147!10_#89_#181_#54!5_#96_#147!4_#28_#120_#17_#167!2_#17_#120_#177_#147!4_#96_#54!5_#181_#89_#147!10_#32_#74_#35!4_$ +- +#119!4@#74@#32@#147!10@#96@#47!5@#181@#108@#147!4@#36@#141@#169@#87!2@#169@#141@#36@#147!4@#113@#181@#47!5@#96@#147!10@#32@#74@#119!4@$ +#21!4A#74A#32A#147!10A#114A#27!5A#190A#147!6A#145A#120A#38!2A#120A#152A#147!6A#190A#27!5A#114A#147!10A#32A#74A#21!4A$ +#95!4C#74C#32C#147!9C#41C#127!5C#27C#41C#147!6C#75C#120C#93!2C#120C#185C#147!6C#41C#27C#127!5C#17C#147!9C#32C#74C#95!4C$ +#188!4G#74G#32G#147!9G#190G#103!5G#114G#147!7G#36G#141G#120!2G#141G#36G#147!7G#114G#103!5G#190G#147!9G#32G#74G#188!4G$ +#59!4O#74O#32O#147!8O#113O#28O#19!5O#38O#147!8O#86O#120!2O#132O#147!8O#38O#19!5O#28O#113O#147!8O#32O#74O#59!4O$ +#137!4_#74_#32_#147!8_#96_#186!5_#181_#164_#147!8_#88_#120!2_#138_#147!8_#164_#181_#186!5_#96_#147!8_#32_#74_#137!4_$ +- +#34!4@#74@#32@#147!8@#98@#92!5@#141@#147!9@#177@#120!2@#177@#147!9@#141@#125!5@#98@#147!8@#32@#74@#34!4@$ +#118!4A#74A#32A#147!7A#164A#17!6A#113A#147!9A#36A#141!2A#36A#147!9A#113A#17!6A#164A#147!7A#32A#74A#118!4A$ +#20!4C#74C#32C#147!25C#29C#110C#147!25C#32C#74C#20!4C$ +#63!4G#74G#32G#147!25G#152!2G#147!25G#32G#74G#63!4G$ +#144!4O#74O#32O#147!25O#124!2O#147!25O#32O#74O#144!4O$ +#40!4_#74_#32_#147!25_#54!2_#147!25_#32_#74_#40!4_$ +- +#129!4@#82@#121@#65!52@#121@#82@#129!4@$ +#24!4A#71A#168A#11!52A#168A#71A#24!4A$ +#129C#106!3C#129C#160C#179!52C#160C#129C#106!3C#129C$ +#188G#2!62G#188G$ +#153O#82!62O#56O$ +#0_#160_#156_#82_#174!56_#82_#156_#139_#0_$ +- +#0!64@$ +#0!64A$ +#0!64C$ +#0!64G$ + diff --git a/docs/escape_support.md b/docs/escape_support.md index 4cb6c6f2..bc951753 100644 --- a/docs/escape_support.md +++ b/docs/escape_support.md @@ -71,6 +71,7 @@ brevity. | `CSI SP q` | IMPLEMENTED | | | `CSI r` | IMPLEMENTED | | | `CSI S` | IMPLEMENTED | | +| `CSI ? S` | PARTIAL | Only for reading attributes. | | `CSI s` | IMPLEMENTED | | | `CSI T` | IMPLEMENTED | | | `CSI t` | PARTIAL | Only parameters `22` and `23` are supported | @@ -102,3 +103,4 @@ brevity. | ESCAPE | STATUS | NOTE | | --------- | ----------- | -------------------------------------------------- | | `DCS = s` | IMPLEMENTED | | +| `DCS q` | IMPLEMENTED | | diff --git a/docs/features.md b/docs/features.md index fd9a9ad2..094210fd 100644 --- a/docs/features.md +++ b/docs/features.md @@ -77,4 +77,9 @@ file. If an application captures your mouse clicks, which is indicated by a change in mouse cursor shape, you're required to hold <kbd>Shift</kbd> to bypass that. +## Graphics with the Sixel protocol + +Graphics can be added to the terminal using the Sixel protocol. Every graphic can +have up to 1024 colors, and it is limited to 4096x4096 pixels. + [configuration file]: ../alacritty.yml |