Skip to content

Commit

Permalink
feat(cell): add voluntary skipping capability for sixel (#215)
Browse files Browse the repository at this point in the history
> Sixel is a bitmap graphics format supported by terminals.
> "Sixel mode" is entered by sending the sequence ESC+Pq.
> The "String Terminator" sequence ESC+\ exits the mode.

The graphics are then rendered with the top left positioned at the
cursor position.

It is actually possible to render sixels in ratatui with just
`buf.get_mut(x, y).set_symbol("^[Pq ... ^[\")`. But any buffer covering
the "image area" will overwrite the graphics. This is most likely the same
buffer, even though it consists of empty characters `' '`, except for
the top-left character that starts the sequence.

Thus, either the buffer or cells must be specialized to avoid drawing
over the graphics. This patch specializes the `Cell` with a
`set_skip(bool)` method, based on James' patch:
https://github.com/TurtleTheSeaHobo/tui-rs/tree/sixel-support
I unsuccessfully tried specializing the `Buffer`, but as far as I can tell
buffers get merged all the way "up" and thus skipping must be set on the
Cells. Otherwise some kind of "skipping area" state would be required,
which I think is too complicated.

Having access to the buffer now it is possible to skipp all cells but the
first one which can then `set_symbol(sixel)`. It is up to the user to
deal with the graphics size and buffer area size. It is possible to get
the terminal's font size in pixels with a syscall.

An image widget for ratatui that uses this `skip` flag is available at
https://github.com/benjajaja/ratatu-image.

Co-authored-by: James <james@rectangle.pizza>
  • Loading branch information
benjajaja and James committed Aug 25, 2023
1 parent d0ee04a commit e4bcf78
Showing 1 changed file with 78 additions and 3 deletions.
81 changes: 78 additions & 3 deletions src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub struct Cell {
#[cfg(feature = "crossterm")]
pub underline_color: Color,
pub modifier: Modifier,
pub skip: bool,
}

impl Cell {
Expand Down Expand Up @@ -81,6 +82,15 @@ impl Cell {
.add_modifier(self.modifier)
}

/// Sets the cell to be skipped when copying (diffing) the buffer to the screen.
///
/// This is helpful when it is necessary to prevent the buffer from overwriting a cell that is
/// covered by an image from some terminal graphics protocol (Sixel / iTerm / Kitty ...).
pub fn set_skip(&mut self, skip: bool) -> &mut Cell {
self.skip = skip;
self
}

pub fn reset(&mut self) {
self.symbol.clear();
self.symbol.push(' ');
Expand All @@ -91,6 +101,7 @@ impl Cell {
self.underline_color = Color::Reset;
}
self.modifier = Modifier::empty();
self.skip = false;
}
}

Expand All @@ -103,6 +114,7 @@ impl Default for Cell {
#[cfg(feature = "crossterm")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
}
}
}
Expand Down Expand Up @@ -131,7 +143,8 @@ impl Default for Cell {
/// bg: Color::White,
/// #[cfg(feature = "crossterm")]
/// underline_color: Color::Reset,
/// modifier: Modifier::empty()
/// modifier: Modifier::empty(),
/// skip: false
/// });
/// buf.get_mut(5, 0).set_char('x');
/// assert_eq!(buf.get(5, 0).symbol, "x");
Expand Down Expand Up @@ -488,10 +501,10 @@ impl Buffer {
// Cells invalidated by drawing/replacing preceding multi-width characters:
let mut invalidated: usize = 0;
// Cells from the current buffer to skip due to preceding multi-width characters taking
// their place (the skipped cells should be blank anyway):
// their place (the skipped cells should be blank anyway), or due to per-cell-skipping:
let mut to_skip: usize = 0;
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
if (current != previous || invalidated > 0) && to_skip == 0 {
if !current.skip && (current != previous || invalidated > 0) && to_skip == 0 {
let (x, y) = self.pos_of(i);
updates.push((x, y, &next_buffer[i]));
}
Expand Down Expand Up @@ -916,6 +929,18 @@ mod tests {
);
}

#[test]
fn buffer_diffing_skip() {
let prev = Buffer::with_lines(vec!["123"]);
let mut next = Buffer::with_lines(vec!["456"]);
for i in 1..3 {
next.content[i].set_skip(true);
}

let diff = prev.diff(&next);
assert_eq!(diff, vec![(0, 0, &cell("4"))],);
}

#[test]
fn buffer_merge() {
let mut one = Buffer::filled(
Expand Down Expand Up @@ -997,4 +1022,54 @@ mod tests {
};
assert_buffer_eq!(one, merged);
}

#[test]
fn buffer_merge_skip() {
let mut one = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("1"),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 1,
width: 2,
height: 2,
},
Cell::default().set_symbol("2").set_skip(true),
);
one.merge(&two);
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
assert_eq!(skipped, vec![false, false, true, true, true, true]);
}

#[test]
fn buffer_merge_skip2() {
let mut one = Buffer::filled(
Rect {
x: 0,
y: 0,
width: 2,
height: 2,
},
Cell::default().set_symbol("1").set_skip(true),
);
let two = Buffer::filled(
Rect {
x: 0,
y: 1,
width: 2,
height: 2,
},
Cell::default().set_symbol("2"),
);
one.merge(&two);
let skipped: Vec<bool> = one.content().iter().map(|c| c.skip).collect();
assert_eq!(skipped, vec![true, true, false, false, false, false]);
}
}

0 comments on commit e4bcf78

Please sign in to comment.