use unicode_width::UnicodeWidthChar;
pub type CellColor = u32;
pub fn color_default() -> CellColor {
0
}
pub fn color_ansi(i: u8) -> CellColor {
(1 << 24) | (i as u32 & 0xF)
}
pub fn color_indexed(i: u8) -> CellColor {
(2 << 24) | i as u32
}
pub fn color_rgb(r: u8, g: u8, b: u8) -> CellColor {
(3 << 24) | ((r as u32) << 16) | ((g as u32) << 8) | b as u32
}
pub fn color_type(c: CellColor) -> u8 {
(c >> 24) as u8
}
pub fn color_value(c: CellColor) -> u32 {
c & 0x00FF_FFFF
}
pub(crate) const DEFAULT_FG: CellColor = 0;
pub(crate) const DEFAULT_BG: CellColor = 0;
pub type CellAttrs = u16;
pub const ATTR_BOLD: CellAttrs = 1 << 0;
pub const ATTR_DIM: CellAttrs = 1 << 1;
pub const ATTR_ITALIC: CellAttrs = 1 << 2;
pub const ATTR_UNDERLINE: CellAttrs = 1 << 3;
pub const ATTR_BLINK: CellAttrs = 1 << 4;
pub const ATTR_INVERSE: CellAttrs = 1 << 5;
pub const ATTR_HIDDEN: CellAttrs = 1 << 6;
pub const ATTR_STRIKE: CellAttrs = 1 << 7;
pub struct Cell {
pub c: char,
pub extra: Option<Box<str>>,
pub fg: CellColor,
pub bg: CellColor,
pub attrs: CellAttrs,
pub width: u8,
pub continuation: bool,
pub hyperlink_id: u32,
}
impl Clone for Cell {
fn clone(&self) -> Self {
Self {
c: self.c,
extra: self.extra.clone(),
fg: self.fg,
bg: self.bg,
attrs: self.attrs,
width: self.width,
continuation: self.continuation,
hyperlink_id: self.hyperlink_id,
}
}
}
impl std::fmt::Debug for Cell {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Cell")
.field("c", &self.c)
.field("extra", &self.extra)
.field("fg", &self.fg)
.field("bg", &self.bg)
.field("attrs", &self.attrs)
.field("width", &self.width)
.field("continuation", &self.continuation)
.field("hyperlink_id", &self.hyperlink_id)
.finish()
}
}
impl PartialEq for Cell {
fn eq(&self, other: &Self) -> bool {
self.c == other.c
&& self.extra == other.extra
&& self.fg == other.fg
&& self.bg == other.bg
&& self.attrs == other.attrs
&& self.width == other.width
&& self.continuation == other.continuation
&& self.hyperlink_id == other.hyperlink_id
}
}
impl Cell {
pub fn new(c: char) -> Self {
Self::with_attrs(c, DEFAULT_FG, DEFAULT_BG, 0)
}
pub fn with_attrs(c: char, fg: CellColor, bg: CellColor, attrs: CellAttrs) -> Self {
let width = char_display_width(c);
Self {
c,
extra: None,
fg: DEFAULT_FG,
bg,
attrs,
width,
continuation: false,
hyperlink_id: 0,
}
.with_fg(fg)
}
pub fn ascii_with_attrs(byte: u8, fg: CellColor, bg: CellColor, attrs: CellAttrs) -> Self {
debug_assert!(byte.is_ascii() && !byte.is_ascii_control());
Self {
c: byte as char,
extra: None,
fg,
bg,
attrs,
width: 1,
continuation: false,
hyperlink_id: 0,
}
}
fn with_fg(mut self, fg: CellColor) -> Self {
self.fg = fg;
self
}
pub fn blank() -> Self {
Self {
c: ' ',
extra: None,
fg: DEFAULT_FG,
bg: DEFAULT_BG,
attrs: 0,
width: 1,
continuation: false,
hyperlink_id: 0,
}
}
pub fn blank_with_attrs(fg: CellColor, bg: CellColor, attrs: CellAttrs) -> Self {
Self {
c: ' ',
extra: None,
fg,
bg,
attrs,
width: 1,
continuation: false,
hyperlink_id: 0,
}
}
pub fn continuation_of(cell: &Cell) -> Self {
Self {
c: ' ',
extra: None,
fg: cell.fg,
bg: cell.bg,
attrs: cell.attrs,
width: 0,
continuation: true,
hyperlink_id: cell.hyperlink_id,
}
}
pub fn with_hyperlink_id(mut self, hyperlink_id: u32) -> Self {
self.hyperlink_id = hyperlink_id;
self
}
pub fn append_combining(&mut self, c: char) {
self.append_to_cluster(c);
}
pub fn append_to_cluster(&mut self, c: char) {
let mut text = self.extra.take().map(String::from).unwrap_or_default();
text.push(c);
self.extra = Some(text.into_boxed_str());
let width = if is_emoji_modifier(c) {
self.width.saturating_add(char_display_width(c))
} else {
self.width.max(char_display_width(c))
};
self.width = width.clamp(1, 8);
}
pub fn text(&self) -> String {
let mut text = String::new();
self.push_text(&mut text);
text
}
pub fn text_ends_with_zwj(&self) -> bool {
self.c == ZERO_WIDTH_JOINER
|| self
.extra
.as_deref()
.is_some_and(|text| text.ends_with(ZERO_WIDTH_JOINER))
}
pub fn is_single_regional_indicator(&self) -> bool {
is_regional_indicator(self.c) && self.extra.is_none()
}
pub fn display_width(&self) -> usize {
if self.continuation {
0
} else {
self.width as usize
}
}
pub fn push_text(&self, out: &mut String) {
if self.continuation {
return;
}
out.push(self.c);
if let Some(extra) = &self.extra {
out.push_str(extra);
}
}
}
pub fn char_display_width(c: char) -> u8 {
if is_emoji_modifier(c) {
return 2;
}
if is_zero_width_cluster_part(c) {
return 0;
}
if c.is_ascii() {
return 1;
}
UnicodeWidthChar::width(c).unwrap_or(0).min(2) as u8
}
pub const ZERO_WIDTH_JOINER: char = '\u{200d}';
pub fn is_regional_indicator(c: char) -> bool {
matches!(c as u32, 0x1F1E6..=0x1F1FF)
}
pub fn is_emoji_modifier(c: char) -> bool {
matches!(c as u32, 0x1F3FB..=0x1F3FF)
}
pub fn is_variation_selector(c: char) -> bool {
matches!(c as u32, 0xFE00..=0xFE0F | 0xE0100..=0xE01EF)
}
pub fn is_tag_char(c: char) -> bool {
matches!(c as u32, 0xE0020..=0xE007F)
}
pub fn is_zero_width_cluster_part(c: char) -> bool {
c == ZERO_WIDTH_JOINER || c == '\u{20e3}' || is_variation_selector(c) || is_tag_char(c)
}
fn ansi_color(index: u8) -> u32 {
const PALETTE: [u32; 16] = [
0x000000, 0xCC0000, 0x00CC00, 0xCCCC00, 0x0000CC, 0xCC00CC, 0x00CCCC, 0xCCCCCC, 0x555555,
0xFF5555, 0x55FF55, 0xFFFF55, 0x5555FF, 0xFF55FF, 0x55FFFF, 0xFFFFFF,
];
PALETTE[(index as usize).min(15)]
}
fn cube_color(index: u8) -> u32 {
let i = (index - 16) as u32;
let r = (i / 36) % 6;
let g = (i / 6) % 6;
let b = i % 6;
let to_byte = |v: u32| -> u8 { (v * 51) as u8 }; (to_byte(r) as u32) << 16 | (to_byte(g) as u32) << 8 | to_byte(b) as u32
}
fn gray_color(index: u8) -> u32 {
let level = ((index - 232) as u32) * 10 + 8;
level | (level << 8) | (level << 16)
}
pub fn resolve_color(c: CellColor) -> u32 {
match color_type(c) {
1 => ansi_color(color_value(c) as u8),
2 => {
let idx = color_value(c) as u8;
match idx {
0..=15 => ansi_color(idx),
16..=231 => cube_color(idx),
_ => gray_color(idx),
}
}
3 => color_value(c), _ => 0xCCCCCC, }
}
pub fn resolve_bg(c: CellColor) -> u32 {
if c == DEFAULT_BG { 0 } else { resolve_color(c) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cell_default() {
let c = Cell::new('x');
assert_eq!(c.c, 'x');
assert_eq!(c.fg, DEFAULT_FG);
assert_eq!(c.bg, DEFAULT_BG);
}
#[test]
fn test_color_roundtrip() {
let c = color_rgb(0x12, 0x34, 0x56);
assert_eq!(color_type(c), 3);
assert_eq!(color_value(c), 0x123456);
}
#[test]
fn test_resolve_ansi() {
let r = resolve_color(color_ansi(1)); assert_eq!(r, 0xCC0000);
}
#[test]
fn test_resolve_truecolor() {
let r = resolve_color(color_rgb(0xAB, 0xCD, 0xEF));
assert_eq!(r, 0xABCDEF);
}
}