use std::fmt; use std::io; use std::io::Write; use crossterm::Command; use crossterm::queue; use crossterm::style::Attribute as CAttribute; use crossterm::style::Color as CColor; use crossterm::style::Colors; use crossterm::style::Print; use crossterm::style::SetAttribute; use crossterm::style::SetBackgroundColor; use crossterm::style::SetColors; use crossterm::style::SetForegroundColor; use ratatui::style::Color; use ratatui::style::Modifier; use ratatui::text::Span; #[derive(Debug, Clone, PartialEq, Eq)] pub struct SetScrollRegion(pub std::ops::Range); impl Command for SetScrollRegion { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { write!(f, "\x1b[{};{}r", self.0.start, self.0.end) } #[cfg(windows)] fn execute_winapi(&self) -> std::io::Result<()> { panic!("tried to execute SetScrollRegion command using WinAPI, use ANSI instead"); } #[cfg(windows)] fn is_ansi_code_supported(&self) -> bool { // TODO(nornagon): is this supported on Windows? true } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ResetScrollRegion; impl Command for ResetScrollRegion { fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { write!(f, "\x1b[r") } #[cfg(windows)] fn execute_winapi(&self) -> std::io::Result<()> { panic!("tried to execute ResetScrollRegion command using WinAPI, use ANSI instead"); } #[cfg(windows)] fn is_ansi_code_supported(&self) -> bool { // TODO(nornagon): is this supported on Windows? true } } struct ModifierDiff { pub from: Modifier, pub to: Modifier, } impl ModifierDiff { fn queue(self, mut w: W) -> io::Result<()> where W: io::Write, { use crossterm::style::Attribute as CAttribute; let removed = self.from - self.to; if removed.contains(Modifier::REVERSED) { queue!(w, SetAttribute(CAttribute::NoReverse))?; } if removed.contains(Modifier::BOLD) { queue!(w, SetAttribute(CAttribute::NormalIntensity))?; if self.to.contains(Modifier::DIM) { queue!(w, SetAttribute(CAttribute::Dim))?; } } if removed.contains(Modifier::ITALIC) { queue!(w, SetAttribute(CAttribute::NoItalic))?; } if removed.contains(Modifier::UNDERLINED) { queue!(w, SetAttribute(CAttribute::NoUnderline))?; } if removed.contains(Modifier::DIM) { queue!(w, SetAttribute(CAttribute::NormalIntensity))?; } if removed.contains(Modifier::CROSSED_OUT) { queue!(w, SetAttribute(CAttribute::NotCrossedOut))?; } if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) { queue!(w, SetAttribute(CAttribute::NoBlink))?; } let added = self.to - self.from; if added.contains(Modifier::REVERSED) { queue!(w, SetAttribute(CAttribute::Reverse))?; } if added.contains(Modifier::BOLD) { queue!(w, SetAttribute(CAttribute::Bold))?; } if added.contains(Modifier::ITALIC) { queue!(w, SetAttribute(CAttribute::Italic))?; } if added.contains(Modifier::UNDERLINED) { queue!(w, SetAttribute(CAttribute::Underlined))?; } if added.contains(Modifier::DIM) { queue!(w, SetAttribute(CAttribute::Dim))?; } if added.contains(Modifier::CROSSED_OUT) { queue!(w, SetAttribute(CAttribute::CrossedOut))?; } if added.contains(Modifier::SLOW_BLINK) { queue!(w, SetAttribute(CAttribute::SlowBlink))?; } if added.contains(Modifier::RAPID_BLINK) { queue!(w, SetAttribute(CAttribute::RapidBlink))?; } Ok(()) } } fn write_spans<'a, I>(mut writer: &mut impl Write, content: I) -> io::Result<()> where I: Iterator>, { let mut fg = Color::Reset; let mut bg = Color::Reset; let mut last_modifier = Modifier::empty(); for span in content { let mut modifier = Modifier::empty(); modifier.insert(span.style.add_modifier); modifier.remove(span.style.sub_modifier); if modifier != last_modifier { let diff = ModifierDiff { from: last_modifier, to: modifier, }; diff.queue(&mut writer)?; last_modifier = modifier; } let next_fg = span.style.fg.unwrap_or(Color::Reset); let next_bg = span.style.bg.unwrap_or(Color::Reset); if next_fg != fg || next_bg != bg { queue!( writer, SetColors(Colors::new(next_fg.into(), next_bg.into())) )?; fg = next_fg; bg = next_bg; } queue!(writer, Print(span.content.clone()))?; } queue!( writer, SetForegroundColor(CColor::Reset), SetBackgroundColor(CColor::Reset), SetAttribute(crossterm::style::Attribute::Reset), ) } #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] use super::*; #[test] fn writes_bold_then_regular_spans() { use ratatui::style::Stylize; let spans = ["A".bold(), "B".into()]; let mut actual: Vec = Vec::new(); write_spans(&mut actual, spans.iter()).unwrap(); let mut expected: Vec = Vec::new(); queue!( expected, SetAttribute(crossterm::style::Attribute::Bold), Print("A"), SetAttribute(crossterm::style::Attribute::NormalIntensity), Print("B"), SetForegroundColor(CColor::Reset), SetBackgroundColor(CColor::Reset), SetAttribute(crossterm::style::Attribute::Reset), ) .unwrap(); assert_eq!( String::from_utf8(actual).unwrap(), String::from_utf8(expected).unwrap() ); } }