use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use super::cache::RenderCache;
use crate::screen::cell::Row;
#[cfg(test)]
use crate::screen::grid::Grid;
use crate::screen::grid::{ActiveCharset, Charset, MouseEncoding, TerminalModes};
use crate::screen::style::{write_u16, StyleId, StyleTable};
pub(super) fn hash_row(row: &Row) -> u64 {
let mut hasher = DefaultHasher::new();
row.hash(&mut hasher);
hasher.finish()
}
pub(in crate::screen) fn render_line_with(
row: &Row,
resolve: impl Fn(StyleId) -> crate::screen::style::Style,
) -> Vec<u8> {
let content_len = row.content_len();
if content_len == 0 {
return Vec::new();
}
let mut out = Vec::new();
let mut current_id = StyleId::default();
for g in row.graphemes() {
if (g.col as usize) >= content_len {
break;
}
if g.style_id != current_id {
resolve(g.style_id).write_sgr_with_reset_to(&mut out);
current_id = g.style_id;
}
let mut buf = [0u8; 4];
out.extend_from_slice(g.c.encode_utf8(&mut buf).as_bytes());
for &mark in g.combining {
out.extend_from_slice(mark.encode_utf8(&mut buf).as_bytes());
}
}
if !current_id.is_default() {
out.extend_from_slice(b"\x1b[0m");
}
out
}
pub(in crate::screen) fn render_line(row: &Row, styles: &StyleTable) -> Vec<u8> {
render_line_with(row, |id| styles.get(id))
}
fn emit_dec_mode(out: &mut Vec<u8>, code: u16, enabled: bool) {
out.extend_from_slice(b"\x1b[?");
write_u16(out, code);
out.push(if enabled { b'h' } else { b'l' });
}
fn emit_charset(out: &mut Vec<u8>, slot: u8, charset: Charset) {
out.push(0x1b);
out.push(slot);
out.push(match charset {
Charset::Ascii => b'B',
Charset::LineDrawing => b'0',
});
}
fn emit_mode(out: &mut Vec<u8>, modes: &TerminalModes) {
out.extend_from_slice(b"\x1b[");
out.push(b'0' + modes.cursor_shape.to_param());
out.extend_from_slice(b" q");
emit_dec_mode(out, 1, modes.cursor_key_mode);
emit_dec_mode(out, 6, modes.origin_mode);
emit_dec_mode(out, 7, modes.autowrap_mode);
emit_dec_mode(out, 2004, modes.bracketed_paste);
emit_dec_mode(out, 1000, modes.mouse_modes.click);
emit_dec_mode(out, 1002, modes.mouse_modes.button);
emit_dec_mode(out, 1003, modes.mouse_modes.any);
emit_mouse_encoding(out, modes.mouse_encoding);
emit_dec_mode(out, 1004, modes.focus_reporting);
out.extend_from_slice(if modes.keypad_app_mode {
b"\x1b="
} else {
b"\x1b>"
});
emit_charset(out, b'(', modes.g0_charset);
emit_charset(out, b')', modes.g1_charset);
out.push(match modes.active_charset {
ActiveCharset::G0 => 0x0F, ActiveCharset::G1 => 0x0E, });
}
fn emit_mouse_encoding(out: &mut Vec<u8>, encoding: MouseEncoding) {
match encoding {
MouseEncoding::Sgr => {
out.extend_from_slice(b"\x1b[?1005l");
out.extend_from_slice(b"\x1b[?1006h");
}
MouseEncoding::Utf8 => {
out.extend_from_slice(b"\x1b[?1006l");
out.extend_from_slice(b"\x1b[?1005h");
}
MouseEncoding::X10 => out.extend_from_slice(b"\x1b[?1006l\x1b[?1005l"),
}
}
fn emit_mode_delta(out: &mut Vec<u8>, modes: &TerminalModes, prev: &TerminalModes) {
if modes.cursor_shape != prev.cursor_shape {
out.extend_from_slice(b"\x1b[");
out.push(b'0' + modes.cursor_shape.to_param());
out.extend_from_slice(b" q");
}
if modes.cursor_key_mode != prev.cursor_key_mode {
emit_dec_mode(out, 1, modes.cursor_key_mode);
}
if modes.origin_mode != prev.origin_mode {
emit_dec_mode(out, 6, modes.origin_mode);
}
if modes.autowrap_mode != prev.autowrap_mode {
emit_dec_mode(out, 7, modes.autowrap_mode);
}
if modes.bracketed_paste != prev.bracketed_paste {
emit_dec_mode(out, 2004, modes.bracketed_paste);
}
if modes.mouse_modes.click != prev.mouse_modes.click {
emit_dec_mode(out, 1000, modes.mouse_modes.click);
}
if modes.mouse_modes.button != prev.mouse_modes.button {
emit_dec_mode(out, 1002, modes.mouse_modes.button);
}
if modes.mouse_modes.any != prev.mouse_modes.any {
emit_dec_mode(out, 1003, modes.mouse_modes.any);
}
if modes.mouse_encoding != prev.mouse_encoding {
emit_mouse_encoding(out, modes.mouse_encoding);
}
if modes.focus_reporting != prev.focus_reporting {
emit_dec_mode(out, 1004, modes.focus_reporting);
}
if modes.keypad_app_mode != prev.keypad_app_mode {
out.extend_from_slice(if modes.keypad_app_mode {
b"\x1b="
} else {
b"\x1b>"
});
}
if modes.g0_charset != prev.g0_charset {
emit_charset(out, b'(', modes.g0_charset);
}
if modes.g1_charset != prev.g1_charset {
emit_charset(out, b')', modes.g1_charset);
}
if modes.active_charset != prev.active_charset {
out.push(match modes.active_charset {
ActiveCharset::G0 => 0x0F, ActiveCharset::G1 => 0x0E, });
}
}
#[cfg(test)]
pub(in crate::screen) fn render_screen(
grid: &Grid,
title: &str,
full: bool,
cache: &mut RenderCache,
) -> Vec<u8> {
render_screen_impl(grid, title, &[], full, cache)
}
#[cfg(test)]
pub(in crate::screen) fn render_screen_with_scrollback(
grid: &Grid,
title: &str,
scrollback: &[Vec<u8>],
cache: &mut RenderCache,
) -> Vec<u8> {
render_screen_impl(grid, title, scrollback, true, cache)
}
fn render_scrollback(out: &mut Vec<u8>, rows: u16, scrollback: &[Vec<u8>]) {
let rows_usize = rows as usize;
out.extend_from_slice(b"\x1b[?25l\x1b[r");
for chunk in scrollback.chunks(rows_usize) {
for (i, line) in chunk.iter().enumerate() {
out.extend_from_slice(b"\x1b[");
write_u16(out, (i + 1) as u16);
out.extend_from_slice(b";1H\x1b[0m");
out.extend_from_slice(line);
out.extend_from_slice(b"\x1b[K");
}
if chunk.len() < rows_usize {
for i in chunk.len()..rows_usize {
out.extend_from_slice(b"\x1b[");
write_u16(out, (i + 1) as u16);
out.extend_from_slice(b";1H\x1b[2K");
}
}
out.extend_from_slice(b"\x1b[");
write_u16(out, rows);
out.extend_from_slice(b";1H");
out.extend(std::iter::repeat_n(b'\n', chunk.len()));
}
}
fn render_modes_title_cursor(
out: &mut Vec<u8>,
scroll_region: (u16, u16),
modes: &TerminalModes,
cursor_pos: (u16, u16),
title: &str,
full: bool,
cache: &mut RenderCache,
) {
if full || cache.last_scroll_region != Some(scroll_region) {
out.extend_from_slice(b"\x1b[");
write_u16(out, scroll_region.0 + 1);
out.push(b';');
write_u16(out, scroll_region.1 + 1);
out.push(b'r');
cache.last_scroll_region = Some(scroll_region);
}
match &cache.last_modes {
Some(prev) if !full => emit_mode_delta(out, modes, prev),
_ => emit_mode(out, modes),
}
cache.last_modes = Some(modes.clone());
if full || cache.last_cursor != Some(cursor_pos) {
let cup_row = if modes.origin_mode {
cursor_pos.1.saturating_sub(scroll_region.0)
} else {
cursor_pos.1
};
out.extend_from_slice(b"\x1b[");
write_u16(out, cup_row + 1);
out.push(b';');
write_u16(out, cursor_pos.0 + 1);
out.push(b'H');
cache.last_cursor = Some(cursor_pos);
}
if full || title != cache.last_title {
out.extend_from_slice(b"\x1b]2;");
for &b in title.as_bytes() {
if b >= 0x20 && b != 0x7f {
out.push(b);
}
}
out.push(0x07);
cache.last_title = title.to_string();
}
}
#[cfg(test)]
fn render_screen_impl(
grid: &Grid,
title: &str,
scrollback: &[Vec<u8>],
full: bool,
cache: &mut RenderCache,
) -> Vec<u8> {
let capacity = if full || !scrollback.is_empty() {
grid.cols() as usize * grid.rows() as usize * 4
} else {
1024
};
let mut out = Vec::with_capacity(capacity);
if !scrollback.is_empty() {
render_scrollback(&mut out, grid.rows(), scrollback);
cache.invalidate();
}
let full = full || !scrollback.is_empty();
render_core(
&mut out,
grid.visible_rows(),
grid.visible_row_count(),
|id| grid.style_table().get(id),
grid.scroll_region(),
grid.modes(),
grid.cursor_pos(),
grid.cursor_visible(),
title,
full,
cache,
);
out
}
pub(in crate::screen) fn render_emulator_impl<
E: crate::screen::traits::TerminalEmulator + ?Sized,
>(
emu: &E,
scrollback: &[Vec<u8>],
full: bool,
cache: &mut RenderCache,
) -> Vec<u8> {
let capacity = if full || !scrollback.is_empty() {
emu.cols() as usize * emu.rows() as usize * 4
} else {
1024
};
let mut out = Vec::with_capacity(capacity);
if !scrollback.is_empty() {
render_scrollback(&mut out, emu.rows(), scrollback);
cache.invalidate();
}
let full = full || !scrollback.is_empty();
render_core(
&mut out,
(0..emu.rows()).map(|y| emu.visible_row(y)),
emu.rows() as usize,
|id| emu.resolve_style(id),
emu.scroll_region(),
emu.modes(),
emu.cursor_position(),
emu.cursor_visible(),
emu.title(),
full,
cache,
);
out
}
#[allow(clippy::too_many_arguments)]
pub(super) fn render_core<'a>(
out: &mut Vec<u8>,
rows: impl Iterator<Item = &'a Row>,
num_rows: usize,
resolve_style: impl Fn(StyleId) -> crate::screen::style::Style,
scroll_region: (u16, u16),
modes: &TerminalModes,
cursor_pos: (u16, u16),
cursor_visible: bool,
title: &str,
full: bool,
cache: &mut RenderCache,
) {
use crate::screen::style::Style;
let pre_render_len = out.len();
out.extend_from_slice(b"\x1b[?2026h");
out.extend_from_slice(b"\x1b[?25l");
if full {
out.extend_from_slice(b"\x1b[0m\x1b[2J\x1b[H");
cache.invalidate();
}
cache.rows.ensure_len(num_rows);
for (y, row) in rows.enumerate() {
let row_dirty = cache.rows.check_row(y, row);
if !full && !row_dirty {
continue;
}
out.extend_from_slice(b"\x1b[");
write_u16(out, y as u16 + 1);
out.extend_from_slice(b";1H");
let write_len = row.content_len();
let mut last_style = Style::default();
for g in row.graphemes() {
if (g.col as usize) >= write_len {
break;
}
let style = resolve_style(g.style_id);
if style != last_style {
style.write_sgr_with_reset_to(out);
last_style = style;
}
let mut buf = [0u8; 4];
out.extend_from_slice(g.c.encode_utf8(&mut buf).as_bytes());
for &mark in g.combining {
out.extend_from_slice(mark.encode_utf8(&mut buf).as_bytes());
}
}
out.extend_from_slice(if full {
b"\x1b[0m" as &[u8]
} else {
b"\x1b[0m\x1b[K"
});
}
render_modes_title_cursor(out, scroll_region, modes, cursor_pos, title, full, cache);
let header_len = pre_render_len + b"\x1b[?2026h\x1b[?25l".len();
if out.len() == header_len && cache.last_cursor_visible == Some(cursor_visible) {
out.truncate(pre_render_len);
return;
}
cache.last_cursor_visible = Some(cursor_visible);
if cursor_visible {
out.extend_from_slice(b"\x1b[?25h");
}
out.extend_from_slice(b"\x1b[?2026l");
}