返回 DeepSeek-TUI-2026
color_compat.rs
根目录 / crates / tui / src / tui / color_compat.rs
1 //! Terminal color compatibility shim.
2 //!
3 //! Ratatui's crossterm backend emits truecolor SGR for every `Color::Rgb`
4 //! cell. That is correct for truecolor terminals, but macOS Terminal.app often
5 //! advertises only `xterm-256color`; sending `38;2` / `48;2` there can render
6 //! as stray green/cyan backgrounds. This backend adapts every cell to the
7 //! detected color depth before handing it to crossterm.
8
9 use std::io::{self, Write};
10
11 use ratatui::{
12 backend::{Backend, ClearType, CrosstermBackend, WindowSize},
13 buffer::Cell,
14 layout::{Position, Size},
15 };
16
17 use crate::palette::{self, ColorDepth};
18
19 #[derive(Debug)]
20 pub(crate) struct ColorCompatBackend<W: Write> {
21 inner: CrosstermBackend<W>,
22 depth: ColorDepth,
23 }
24
25 impl<W: Write> ColorCompatBackend<W> {
26 pub(crate) fn new(writer: W, depth: ColorDepth) -> Self {
27 Self {
28 inner: CrosstermBackend::new(writer),
29 depth,
30 }
31 }
32 }
33
34 impl<W: Write> Write for ColorCompatBackend<W> {
35 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
36 self.inner.write(buf)
37 }
38
39 fn flush(&mut self) -> io::Result<()> {
40 Write::flush(&mut self.inner)
41 }
42 }
43
44 impl<W: Write> Backend for ColorCompatBackend<W> {
45 fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
46 where
47 I: Iterator<Item = (u16, u16, &'a Cell)>,
48 {
49 let adapted = content
50 .map(|(x, y, cell)| {
51 let mut cell = cell.clone();
52 adapt_cell_colors(&mut cell, self.depth);
53 (x, y, cell)
54 })
55 .collect::<Vec<_>>();
56 self.inner
57 .draw(adapted.iter().map(|(x, y, cell)| (*x, *y, cell)))
58 }
59
60 fn append_lines(&mut self, n: u16) -> io::Result<()> {
61 self.inner.append_lines(n)
62 }
63
64 fn hide_cursor(&mut self) -> io::Result<()> {
65 self.inner.hide_cursor()
66 }
67
68 fn show_cursor(&mut self) -> io::Result<()> {
69 self.inner.show_cursor()
70 }
71
72 fn get_cursor_position(&mut self) -> io::Result<Position> {
73 self.inner.get_cursor_position()
74 }
75
76 fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
77 self.inner.set_cursor_position(position)
78 }
79
80 fn clear(&mut self) -> io::Result<()> {
81 self.inner.clear()
82 }
83
84 fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
85 self.inner.clear_region(clear_type)
86 }
87
88 fn size(&self) -> io::Result<Size> {
89 self.inner.size()
90 }
91
92 fn window_size(&mut self) -> io::Result<WindowSize> {
93 self.inner.window_size()
94 }
95
96 fn flush(&mut self) -> io::Result<()> {
97 Backend::flush(&mut self.inner)
98 }
99 }
100
101 fn adapt_cell_colors(cell: &mut Cell, depth: ColorDepth) {
102 cell.fg = palette::adapt_color(cell.fg, depth);
103 cell.bg = palette::adapt_bg(cell.bg, depth);
104 }
105
106 #[cfg(test)]
107 mod tests {
108 use std::{cell::RefCell, io::Write, rc::Rc};
109
110 use ratatui::backend::Backend;
111 use ratatui::{buffer::Cell, style::Color};
112
113 use super::*;
114
115 #[derive(Clone, Default)]
116 struct SharedWriter(Rc<RefCell<Vec<u8>>>);
117
118 impl Write for SharedWriter {
119 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
120 self.0.borrow_mut().extend_from_slice(buf);
121 Ok(buf.len())
122 }
123
124 fn flush(&mut self) -> io::Result<()> {
125 Ok(())
126 }
127 }
128
129 #[test]
130 fn adapts_rgb_cells_to_indexed_on_ansi256() {
131 let mut cell = Cell::default();
132 cell.set_fg(Color::Rgb(53, 120, 229));
133 cell.set_bg(Color::Rgb(11, 21, 38));
134
135 adapt_cell_colors(&mut cell, ColorDepth::Ansi256);
136
137 assert!(matches!(cell.fg, Color::Indexed(_)));
138 assert!(matches!(cell.bg, Color::Indexed(_)));
139 }
140
141 #[test]
142 fn leaves_truecolor_cells_unchanged() {
143 let mut cell = Cell::default();
144 cell.set_fg(Color::Rgb(53, 120, 229));
145 cell.set_bg(Color::Rgb(11, 21, 38));
146
147 adapt_cell_colors(&mut cell, ColorDepth::TrueColor);
148
149 assert_eq!(cell.fg, Color::Rgb(53, 120, 229));
150 assert_eq!(cell.bg, Color::Rgb(11, 21, 38));
151 }
152
153 #[test]
154 fn ansi256_backend_output_does_not_emit_truecolor_sgr() {
155 let writer = SharedWriter::default();
156 let capture = writer.0.clone();
157 let mut backend = ColorCompatBackend::new(writer, ColorDepth::Ansi256);
158 let mut cell = Cell::default();
159 cell.set_symbol("x")
160 .set_fg(Color::Rgb(53, 120, 229))
161 .set_bg(Color::Rgb(11, 21, 38));
162
163 backend.draw(std::iter::once((0, 0, &cell))).unwrap();
164
165 let output = String::from_utf8_lossy(&capture.borrow()).to_string();
166 assert!(!output.contains("38;2;"), "{output:?}");
167 assert!(!output.contains("48;2;"), "{output:?}");
168 }
169 }
170
170 lines RUST