diff --git a/crates/jcode-tui/src/tui/ui.rs b/crates/jcode-tui/src/tui/ui.rs index e6ba2c74c..de304a9d3 100644 --- a/crates/jcode-tui/src/tui/ui.rs +++ b/crates/jcode-tui/src/tui/ui.rs @@ -60,6 +60,9 @@ use std::time::{Duration, Instant}; #[cfg(test)] use unicode_width::UnicodeWidthStr; +const PLAIN_FONT_STYLE_ENV: &str = "JCODE_TUI_PLAIN_FONT_STYLE"; +static PLAIN_FONT_STYLE_REQUESTED: OnceLock = OnceLock::new(); + #[path = "ui_animations.rs"] mod animations; #[path = "ui_box.rs"] @@ -1964,6 +1967,54 @@ pub fn draw(frame: &mut Frame, app: &dyn TuiState) { Ok(()) => {} Err(payload) => render_recovered_panic_frame(frame, &payload), } + strip_font_variant_modifiers_if_requested(frame); +} + +fn plain_font_style_requested() -> bool { + *PLAIN_FONT_STYLE_REQUESTED.get_or_init(|| { + std::env::var_os(PLAIN_FONT_STYLE_ENV) + .and_then(|value| value.into_string().ok()) + .as_deref() + .is_some_and(parse_plain_font_style_value) + }) +} + +fn parse_plain_font_style_value(value: &str) -> bool { + let value = value.trim(); + value == "1" + || value.eq_ignore_ascii_case("true") + || value.eq_ignore_ascii_case("yes") + || value.eq_ignore_ascii_case("on") +} + +fn font_variant_modifiers() -> Modifier { + Modifier::BOLD | Modifier::DIM | Modifier::ITALIC | Modifier::REVERSED +} + +fn strip_font_variant_modifiers_if_requested(frame: &mut Frame) { + let requested = plain_font_style_requested(); + let frame_area = frame.area(); + let buf = frame.buffer_mut(); + let area = frame_area.intersection(*buf.area()); + strip_font_variant_modifiers_if(buf, area, requested); +} + +fn strip_font_variant_modifiers_if(buf: &mut ratatui::buffer::Buffer, area: Rect, requested: bool) { + if requested { + strip_font_variant_modifiers(buf, area); + } +} + +fn strip_font_variant_modifiers(buf: &mut ratatui::buffer::Buffer, area: Rect) { + let modifiers = font_variant_modifiers(); + for y in area.y..area.y.saturating_add(area.height) { + for x in area.x..area.x.saturating_add(area.width) { + let cell = &mut buf[(x, y)]; + if cell.modifier.intersects(modifiers) { + cell.modifier.remove(modifiers); + } + } + } } fn draw_inner(frame: &mut Frame, app: &dyn TuiState) { @@ -2797,6 +2848,74 @@ pub(crate) fn render_native_scrollbar( frame.render_widget(Paragraph::new(lines), area); } +#[cfg(test)] +mod font_style_tests { + use super::*; + + fn styled_ascii_buffer() -> ratatui::buffer::Buffer { + let mut buffer = ratatui::buffer::Buffer::empty(Rect::new(0, 0, 32, 1)); + let text = "browser continue Done Built"; + for (x, ch) in text.chars().enumerate() { + let cell = &mut buffer[(x as u16, 0)]; + cell.set_symbol(ch.encode_utf8(&mut [0; 4])); + cell.modifier + .insert(Modifier::BOLD | Modifier::DIM | Modifier::ITALIC | Modifier::REVERSED); + } + buffer + } + + #[test] + fn parse_plain_font_style_value_accepts_truthy_values() { + for value in ["1", "true", "TRUE", "yes", "YES", "on", "ON", " true "] { + assert!( + parse_plain_font_style_value(value), + "{value:?} should enable plain font style" + ); + } + } + + #[test] + fn parse_plain_font_style_value_rejects_empty_and_falsey_values() { + for value in ["", "0", "false", "no", "off", "plain"] { + assert!( + !parse_plain_font_style_value(value), + "{value:?} should not enable plain font style" + ); + } + } + + #[test] + fn strip_font_variant_modifiers_if_is_noop_when_not_requested() { + let mut buffer = styled_ascii_buffer(); + + strip_font_variant_modifiers_if(&mut buffer, Rect::new(0, 0, 32, 1), false); + + assert!( + buffer[(0, 0)].modifier.intersects(font_variant_modifiers()), + "font modifiers should remain when plain font style is disabled" + ); + } + + #[test] + fn strip_font_variant_modifiers_preserves_ascii_symbols() { + let mut buffer = styled_ascii_buffer(); + let text = "browser continue Done Built"; + + strip_font_variant_modifiers(&mut buffer, Rect::new(0, 0, 32, 1)); + + let rendered: String = (0..text.len() as u16) + .map(|x| buffer[(x, 0)].symbol()) + .collect(); + assert_eq!(rendered, text); + for x in 0..text.len() as u16 { + assert!( + !buffer[(x, 0)].modifier.intersects(font_variant_modifiers()), + "cell {x} should not keep font-variant modifiers" + ); + } + } +} + #[cfg(test)] #[path = "ui_tests/mod.rs"] mod tests;