Skip to content

Add support for Terminal.app on macOS #30

@lhecker

Description

@lhecker

We currently expect full RGB support from the hosting terminal. Terminal.app is likely the only popular terminal that doesn't support it, but it's very popular (relatively speaking), so we should add support for it. This requires downsampling the color here:

edit/src/framebuffer.rs

Lines 524 to 555 in b008570

fn format_color(&self, dst: &mut ArenaString, fg: bool, mut color: u32) {
let typ = if fg { '3' } else { '4' };
// Some terminals support transparent backgrounds which are used
// if the default background color is active (CSI 49 m).
//
// If [`Framebuffer::set_indexed_colors`] was never called, we assume
// that the terminal doesn't support transparency and initialize the
// background bitmap with the `DEFAULT_THEME` default background color.
// Otherwise, we assume that the terminal supports transparency
// and initialize it with 0x00000000 (transparent).
//
// We also apply this to the foreground color, because it compresses
// the output slightly and ensures that we keep "default foreground"
// and "color that happens to be default foreground" separate.
// (This also applies to the background color by the way.)
if color == 0 {
_ = write!(dst, "\x1b[{typ}9m");
return;
}
if (color & 0xff000000) != 0xff000000 {
let idx = if fg { IndexedColor::Foreground } else { IndexedColor::Background };
let dst = self.indexed(idx);
color = oklab_blend(dst, color);
}
let r = color & 0xff;
let g = (color >> 8) & 0xff;
let b = (color >> 16) & 0xff;
_ = write!(dst, "\x1b[{typ}8;2;{r};{g};{b}m");
}

And somehow detecting RGB support here:

edit/src/bin/edit/main.rs

Lines 499 to 595 in b008570

fn setup_terminal(tui: &mut Tui, vt_parser: &mut vt::Parser) -> RestoreModes {
sys::write_stdout(concat!(
// 1049: Alternative Screen Buffer
// I put the ASB switch in the beginning, just in case the terminal performs
// some additional state tracking beyond the modes we enable/disable.
// 1002: Cell Motion Mouse Tracking
// 1006: SGR Mouse Mode
// 2004: Bracketed Paste Mode
"\x1b[?1049h\x1b[?1002;1006;2004h",
// OSC 4 color table requests for indices 0 through 15 (base colors).
"\x1b]4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?\x07",
"\x1b]4;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?\x07",
// OSC 10 and 11 queries for the current foreground and background colors.
"\x1b]10;?\x07\x1b]11;?\x07",
// CSI c reports the terminal capabilities.
// It also helps us to detect the end of the responses, because not all
// terminals support the OSC queries, but all of them support CSI c.
"\x1b[c",
));
let mut done = false;
let mut osc_buffer = String::new();
let mut indexed_colors = framebuffer::DEFAULT_THEME;
let mut color_responses = 0;
while !done {
let scratch = scratch_arena(None);
let Some(input) = sys::read_stdin(&scratch, vt_parser.read_timeout()) else {
break;
};
let mut vt_stream = vt_parser.parse(&input);
while let Some(token) = vt_stream.next() {
match token {
Token::Csi(state) if state.final_byte == 'c' => done = true,
Token::Osc { mut data, partial } => {
if partial {
osc_buffer.push_str(data);
continue;
}
if !osc_buffer.is_empty() {
osc_buffer.push_str(data);
data = &osc_buffer;
}
let mut splits = data.split_terminator(';');
let color = match splits.next().unwrap_or("") {
// The response is `4;<color>;rgb:<r>/<g>/<b>`.
"4" => match splits.next().unwrap_or("").parse::<usize>() {
Ok(val) if val < 16 => &mut indexed_colors[val],
_ => continue,
},
// The response is `10;rgb:<r>/<g>/<b>`.
"10" => &mut indexed_colors[IndexedColor::Foreground as usize],
// The response is `11;rgb:<r>/<g>/<b>`.
"11" => &mut indexed_colors[IndexedColor::Background as usize],
_ => continue,
};
let color_param = splits.next().unwrap_or("");
if !color_param.starts_with("rgb:") {
continue;
}
let mut iter = color_param[4..].split_terminator('/');
let rgb_parts = [(); 3].map(|_| iter.next().unwrap_or("0"));
let mut rgb = 0;
for part in rgb_parts {
if part.len() == 2 || part.len() == 4 {
let Ok(mut val) = usize::from_str_radix(part, 16) else {
continue;
};
if part.len() == 4 {
// Round from 16 bits to 8 bits.
val = (val * 0xff + 0x7fff) / 0xffff;
}
rgb = (rgb >> 8) | ((val as u32) << 16);
}
}
*color = rgb | 0xff000000;
color_responses += 1;
osc_buffer.clear();
}
_ => {}
}
}
}
if color_responses == indexed_colors.len() {
tui.setup_indexed_colors(indexed_colors);
}
RestoreModes
}

Detecting RGB support should not use the COLORTERM environment variable, because a) I don't like it and b) I'm quite certain there's alternatives to do it. The simplest alternative is to just assume that Terminal.app is the only terminal that doesn't support it (...given that this project doesn't aim to support old terminals).

Metadata

Metadata

Assignees

No one assigned

    Labels

    I-taskSmaller tasks. Requires work in the 100s of LOC (usually).

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions