uncurses_ratatui/convert.rs
1//! Conversion helpers from widget-library buffer data to uncurses cells.
2//!
3//! ## Color and style mapping
4//!
5//! Public helpers convert individual colors and styles into uncurses values.
6//! `Reset` colors become `None`, named colors become the closest ANSI basic
7//! color, indexed colors keep their palette index, and RGB colors keep their
8//! three components. Style conversion copies supported additive modifiers into
9//! [`AttrFlags`](uncurses::style::AttrFlags), maps underline to
10//! [`UnderlineStyle::Single`], and ignores subtractive modifiers because a
11//! concrete buffer cell already carries its effective style.
12//!
13//! ## Cell conversion
14//!
15//! Backend drawing uses the private cell converter to combine symbol width and
16//! style conversion before writing into the screen buffer.
17//!
18//! ```text
19//! ratatui Cell
20//! ├─ symbol ───────────────► width check ───► narrow / wide uncurses Cell
21//! └─ fg/bg/underline/style ─► style mapper ─► uncurses Style
22//! └► styled Cell
23//! ```
24
25use ratatui::style::{Color as RtColor, Modifier, Style as RtStyle};
26use uncurses::cell::Cell as CzCell;
27use uncurses::color::Color as CzColor;
28use uncurses::style::{AttrFlags, Style as CzStyle, UnderlineStyle};
29
30/// Convert a widget-library color into an uncurses color.
31///
32/// ## Parameters
33///
34/// * `c` - the source color to encode.
35///
36/// ## Returns
37///
38/// `None` for [`RtColor::Reset`], because reset means "no explicit color" in
39/// the target style. All other variants return `Some`: named colors map to
40/// named [`CzColor`] values, [`RtColor::Indexed`] keeps the same palette index,
41/// and [`RtColor::Rgb`] keeps the same red, green, and blue channels.
42///
43/// ## Errors
44///
45/// This conversion is infallible.
46///
47/// ## Panics
48///
49/// Does not panic.
50///
51/// ## Usage note
52///
53/// This helper does not downsample colors for terminal capability profiles;
54/// that happens later in the uncurses renderer.
55pub fn to_uncurses_color(c: RtColor) -> Option<CzColor> {
56 Some(match c {
57 RtColor::Reset => return None,
58 RtColor::Black => CzColor::Black,
59 RtColor::Red => CzColor::Red,
60 RtColor::Green => CzColor::Green,
61 RtColor::Yellow => CzColor::Yellow,
62 RtColor::Blue => CzColor::Blue,
63 RtColor::Magenta => CzColor::Magenta,
64 RtColor::Cyan => CzColor::Cyan,
65 RtColor::Gray => CzColor::White,
66 RtColor::DarkGray => CzColor::BrightBlack,
67 RtColor::LightRed => CzColor::BrightRed,
68 RtColor::LightGreen => CzColor::BrightGreen,
69 RtColor::LightYellow => CzColor::BrightYellow,
70 RtColor::LightBlue => CzColor::BrightBlue,
71 RtColor::LightMagenta => CzColor::BrightMagenta,
72 RtColor::LightCyan => CzColor::BrightCyan,
73 RtColor::White => CzColor::BrightWhite,
74 RtColor::Rgb(r, g, b) => CzColor::Rgb(r, g, b),
75 RtColor::Indexed(i) => CzColor::Indexed(i),
76 })
77}
78
79fn to_attrs(m: Modifier) -> AttrFlags {
80 let mut a = AttrFlags::empty();
81 if m.contains(Modifier::BOLD) {
82 a |= AttrFlags::BOLD;
83 }
84 if m.contains(Modifier::DIM) {
85 a |= AttrFlags::FAINT;
86 }
87 if m.contains(Modifier::ITALIC) {
88 a |= AttrFlags::ITALIC;
89 }
90 if m.contains(Modifier::SLOW_BLINK) {
91 a |= AttrFlags::SLOW_BLINK;
92 }
93 if m.contains(Modifier::RAPID_BLINK) {
94 a |= AttrFlags::RAPID_BLINK;
95 }
96 if m.contains(Modifier::REVERSED) {
97 a |= AttrFlags::REVERSE;
98 }
99 if m.contains(Modifier::HIDDEN) {
100 a |= AttrFlags::CONCEAL;
101 }
102 if m.contains(Modifier::CROSSED_OUT) {
103 a |= AttrFlags::STRIKETHROUGH;
104 }
105 a
106}
107
108/// Convert a widget-library style into an uncurses style.
109///
110/// ## Parameters
111///
112/// * `s` - the source style. The conversion reads `fg`, `bg`,
113/// `underline_color`, and `add_modifier`.
114///
115/// ## Returns
116///
117/// A [`CzStyle`] with foreground, background, and underline colors converted by
118/// [`to_uncurses_color`]. Additive modifiers map as follows: bold, dim, italic,
119/// slow blink, rapid blink, reversed, hidden, and crossed-out become the
120/// corresponding uncurses attribute flags. Underline becomes
121/// [`UnderlineStyle::Single`] when [`Modifier::UNDERLINED`] is present, and
122/// [`UnderlineStyle::None`] otherwise.
123///
124/// ## Errors
125///
126/// This conversion is infallible.
127///
128/// ## Panics
129///
130/// Does not panic.
131///
132/// ## Usage note
133///
134/// `sub_modifier` is ignored. The backend converts concrete buffer cells, whose
135/// style has already been resolved by the widget library.
136pub fn to_uncurses_style(s: RtStyle) -> CzStyle {
137 let attrs = to_attrs(s.add_modifier);
138 let underline = if s.add_modifier.contains(Modifier::UNDERLINED) {
139 UnderlineStyle::Single
140 } else {
141 UnderlineStyle::None
142 };
143 let mut style = CzStyle::default().underline_style(underline);
144 if let Some(fg) = s.fg.and_then(to_uncurses_color) {
145 style = style.fg(fg);
146 }
147 if let Some(bg) = s.bg.and_then(to_uncurses_color) {
148 style = style.bg(bg);
149 }
150 if let Some(uc) = s.underline_color.and_then(to_uncurses_color) {
151 style = style.underline_color(uc);
152 }
153 style.attrs(attrs)
154}
155
156/// Halfwidth Katakana Voiced Sound Mark (dakuten).
157const HALFWIDTH_KATAKANA_VOICED_SOUND_MARK: char = '\u{FF9E}';
158/// Halfwidth Katakana Semi-Voiced Sound Mark (handakuten).
159const HALFWIDTH_KATAKANA_SEMI_VOICED_SOUND_MARK: char = '\u{FF9F}';
160
161/// Display width of `s` in terminal cells, matching the widget library's cell
162/// width for strings. Includes a fast path for single-byte ASCII and a `+1`
163/// compensation for each halfwidth dakuten/handakuten that `unicode-width`
164/// reports as zero.
165fn str_cell_width(s: &str) -> u16 {
166 use unicode_width::UnicodeWidthStr;
167 if s.len() == 1 {
168 1
169 } else {
170 let width = s.width() as u16;
171 let extra = s
172 .chars()
173 .filter(|c| {
174 matches!(
175 *c,
176 HALFWIDTH_KATAKANA_VOICED_SOUND_MARK
177 | HALFWIDTH_KATAKANA_SEMI_VOICED_SOUND_MARK
178 )
179 })
180 .count() as u16;
181 width.saturating_add(extra)
182 }
183}
184
185/// Convert a concrete buffer cell into the uncurses cell staged in the buffer.
186///
187/// The symbol is classified as wide when its terminal-cell width is at least
188/// two; otherwise it is stored as a narrow cell. The source cell's foreground,
189/// background, underline color, and modifiers are converted through
190/// [`to_uncurses_style`].
191pub(crate) fn cell_from_ratatui(rc: &ratatui::buffer::Cell) -> CzCell {
192 let style = RtStyle {
193 fg: Some(rc.fg),
194 bg: Some(rc.bg),
195 underline_color: Some(rc.underline_color),
196 add_modifier: rc.modifier,
197 sub_modifier: Modifier::empty(),
198 };
199 let style = to_uncurses_style(style);
200 let symbol = rc.symbol();
201 let cell = if str_cell_width(symbol) >= 2 {
202 CzCell::wide(symbol)
203 } else {
204 CzCell::narrow(symbol)
205 };
206 cell.style(style)
207}