use crate::color::Rgb;
const DEVICE: &[u8] = b"\x1b[?62;22c";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Colours {
pub(crate) text: Rgb,
pub(crate) ground: Rgb,
pub(crate) cursor: Rgb,
}
#[derive(Debug, Default)]
pub(crate) struct Answers {
colours: Option<Colours>,
}
impl Answers {
pub(crate) fn set_colours(&mut self, colours: Colours) {
self.colours = Some(colours);
}
pub(crate) fn csi(
&self,
screen: &vt100::Screen,
focus_reports: bool,
(marker, second): (Option<u8>, Option<u8>),
params: &[&[u16]],
end: char,
) -> Option<Vec<u8>> {
let param = |index: usize| params.get(index).and_then(|param| param.first()).copied().unwrap_or(0);
match (marker, second, end) {
(None, None, 'n') => match param(0) {
5 => Some(b"\x1b[0n".to_vec()),
6 => {
let (row, col) = screen.cursor_position();
Some(format!("\x1b[{};{}R", u32::from(row) + 1, u32::from(col) + 1).into_bytes())
}
_ => None,
},
(None, None, 'c') if param(0) == 0 => Some(DEVICE.to_vec()),
(Some(b'?'), Some(b'$'), 'p') => {
let mode = param(0);
let state = mode_state(screen, focus_reports, mode);
Some(format!("\x1b[?{mode};{state}$y").into_bytes())
}
_ => None,
}
}
pub(crate) fn osc(&self, params: &[&[u8]]) -> Option<Vec<u8>> {
let [number, b"?"] = params else { return None };
let colours = self.colours?;
let colour = match *number {
b"10" => colours.text,
b"11" => colours.ground,
b"12" => colours.cursor,
_ => return None,
};
let number = std::str::from_utf8(number).ok()?;
let wide = |channel: u8| u16::from(channel) * 0x101;
Some(
format!("\x1b]{number};rgb:{:04x}/{:04x}/{:04x}\x1b\\", wide(colour.r), wide(colour.g), wide(colour.b))
.into_bytes(),
)
}
}
fn mode_state(screen: &vt100::Screen, focus_reports: bool, mode: u16) -> u8 {
let on = |set: bool| if set { 1 } else { 2 };
match mode {
1 => on(screen.application_cursor()),
25 => on(!screen.hide_cursor()),
47 | 1047 | 1049 => on(screen.alternate_screen()),
1000 => on(screen.mouse_protocol_mode() == vt100::MouseProtocolMode::PressRelease),
1002 => on(screen.mouse_protocol_mode() == vt100::MouseProtocolMode::ButtonMotion),
1003 => on(screen.mouse_protocol_mode() == vt100::MouseProtocolMode::AnyMotion),
1004 => on(focus_reports),
1006 => on(screen.mouse_protocol_encoding() == vt100::MouseProtocolEncoding::Sgr),
2004 => on(screen.bracketed_paste()),
_ => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn screen_after(bytes: &[u8]) -> vt100::Parser {
let mut parser = vt100::Parser::new(24, 80, 0);
parser.process(bytes);
parser
}
fn csi(
answers: &Answers,
screen: &vt100::Screen,
marker: (Option<u8>, Option<u8>),
params: &[u16],
end: char,
) -> Option<String> {
let params: Vec<&[u16]> = params.iter().map(std::slice::from_ref).collect();
answers.csi(screen, false, marker, ¶ms, end).map(|bytes| String::from_utf8(bytes).expect("ascii"))
}
#[test]
fn the_cursor_is_reported_where_the_screen_has_it() {
let parser = screen_after(b"\x1b[5;12Habc");
let answer = csi(&Answers::default(), parser.screen(), (None, None), &[6], 'n');
assert_eq!(answer.as_deref(), Some("\x1b[5;15R"), "row 5, column 12 plus three letters");
}
#[test]
fn a_status_and_a_device_question_are_answered() {
let parser = screen_after(b"");
let answers = Answers::default();
assert_eq!(csi(&answers, parser.screen(), (None, None), &[5], 'n').as_deref(), Some("\x1b[0n"));
assert_eq!(csi(&answers, parser.screen(), (None, None), &[], 'c').as_deref(), Some("\x1b[?62;22c"));
assert_eq!(csi(&answers, parser.screen(), (None, None), &[0], 'c').as_deref(), Some("\x1b[?62;22c"));
assert_eq!(csi(&answers, parser.screen(), (None, None), &[7], 'n'), None, "an unknown report asks nothing");
assert_eq!(
csi(&answers, parser.screen(), (Some(b'>'), None), &[], 'c'),
None,
"the second device question is not this one"
);
}
#[test]
fn a_mode_question_says_set_reset_or_unknown() {
let parser = screen_after(b"\x1b[?2004h\x1b[?1049h");
let answers = Answers::default();
let asked = |mode: u16| csi(&answers, parser.screen(), (Some(b'?'), Some(b'$')), &[mode], 'p');
assert_eq!(asked(2004).as_deref(), Some("\x1b[?2004;1$y"), "bracketed paste is on");
assert_eq!(asked(1049).as_deref(), Some("\x1b[?1049;1$y"), "the alternate screen is up");
assert_eq!(asked(1002).as_deref(), Some("\x1b[?1002;2$y"), "mouse drags are off");
assert_eq!(asked(2026).as_deref(), Some("\x1b[?2026;0$y"), "synchronized output is not drawn here");
let focus: &[&[u16]] = &[&[1004]];
let told = answers.csi(parser.screen(), true, (Some(b'?'), Some(b'$')), focus, 'p');
assert_eq!(told.as_deref(), Some(&b"\x1b[?1004;1$y"[..]), "the focus reports the session keeps");
}
#[test]
fn colours_are_told_once_the_widget_has_drawn_them() {
let mut answers = Answers::default();
assert_eq!(answers.osc(&[b"11", b"?"]), None, "no colour is made up before one is known");
answers.set_colours(Colours {
text: Rgb::new(0xee, 0xdd, 0xcc),
ground: Rgb::new(0x12, 0x34, 0x56),
cursor: Rgb::new(1, 2, 3),
});
let told = |number: &[u8]| answers.osc(&[number, b"?"]).map(|bytes| String::from_utf8(bytes).expect("ascii"));
assert_eq!(told(b"11").as_deref(), Some("\x1b]11;rgb:1212/3434/5656\x1b\\"));
assert_eq!(told(b"10").as_deref(), Some("\x1b]10;rgb:eeee/dddd/cccc\x1b\\"));
assert_eq!(told(b"12").as_deref(), Some("\x1b]12;rgb:0101/0202/0303\x1b\\"));
assert_eq!(answers.osc(&[b"11", b"#000000"]), None, "setting a colour is not a question");
assert_eq!(answers.osc(&[b"4", b"1", b"?"]), None, "the palette is not answered");
}
}