diff options
| author | Kazunari Kamata <14287197+kazunari-kamata@users.noreply.github.com> | 2026-08-05 06:48:15 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-05 00:48:15 +0300 |
| commit | be1891ec5265e901b897846a6113c46cf330a7dc (patch) | |
| tree | ef895364bd183fec707dc382628c4ef0c427346f /src/protocol | |
| parent | 09cdd88d0aca35617eb05468c2421b0467656e4f (diff) | |
fix(terminal): render halfwidth katakana voiced marks (#2257)
Co-authored-by: oyoguhito <oyoguhito@kamatanoMacBook-Pro.local>
Co-authored-by: Can Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/protocol')
| -rw-r--r-- | src/protocol/render_ansi.rs | 85 |
1 files changed, 85 insertions, 0 deletions
diff --git a/src/protocol/render_ansi.rs b/src/protocol/render_ansi.rs index 6cee4fde..1e6b9aa4 100644 --- a/src/protocol/render_ansi.rs +++ b/src/protocol/render_ansi.rs @@ -522,9 +522,25 @@ fn repeat_ime_anchor_after_sync() -> bool { /// Writes all cells in the frame (full redraw). fn cell_width(cell: &CellData) -> usize { + if is_halfwidth_katakana_voiced_grapheme(&cell.symbol) { + return 2; + } cell.symbol.width() } +fn is_halfwidth_katakana_voiced_grapheme(symbol: &str) -> bool { + let mut chars = symbol.chars(); + let Some(base) = chars.next() else { + return false; + }; + let Some(mark) = chars.next() else { + return false; + }; + chars.next().is_none() + && ('\u{ff66}'..='\u{ff9d}').contains(&base) + && matches!(mark, '\u{ff9e}' | '\u{ff9f}') +} + #[derive(Clone, Copy)] struct HostCursorState { position: (u16, u16), @@ -819,6 +835,7 @@ mod tests { use crate::protocol::{CellData, CursorState}; const WIDE_GRAPHEME: &str = "💡"; + const HALFWIDTH_VOICED_KANA: &str = "カ\u{ff9e}"; fn make_cell(symbol: &str, fg: u32, bg: u32, modifier: u16) -> CellData { CellData { @@ -831,6 +848,12 @@ mod tests { } } + fn make_skip_cell(symbol: &str, fg: u32, bg: u32, modifier: u16) -> CellData { + let mut cell = make_cell(symbol, fg, bg, modifier); + cell.skip = true; + cell + } + fn make_frame(width: u16, height: u16, cells: Vec<CellData>) -> FrameData { FrameData { cells, @@ -1829,6 +1852,30 @@ mod tests { } #[test] + fn full_redraw_skips_trailing_cells_covered_by_halfwidth_voiced_kana() { + let frame = FrameData { + cells: vec![ + make_cell(HALFWIDTH_VOICED_KANA, 0, 0, 0), + make_skip_cell(" ", 0, 0, 0), + make_cell("Z", 0, 0, 0), + ], + width: 3, + height: 1, + cursor: None, + hyperlinks: Vec::new(), + graphics: Vec::new(), + }; + + let mut output = Vec::new(); + blit_frame_to(&mut output, &frame, None); + let output_str = String::from_utf8(output).unwrap(); + + assert!(output_str.contains("\x1b[1;1H")); + assert!(!output_str.contains("\x1b[1;2H")); + assert!(output_str.contains("\x1b[1;3H")); + } + + #[test] fn diff_redraw_reveals_cells_hidden_by_previous_wide_graphemes() { let prev = FrameData { cells: vec![ @@ -1900,4 +1947,42 @@ mod tests { assert!(output_str.contains("\x1b[1;1H")); assert!(!output_str.contains("\x1b[1;2H")); } + + #[test] + fn diff_redraw_reveals_cells_hidden_by_previous_halfwidth_voiced_kana() { + let prev = FrameData { + cells: vec![ + make_cell(HALFWIDTH_VOICED_KANA, 0, 0, 0), + make_skip_cell(" ", 0, 0, 0), + make_cell("Z", 0, 0, 0), + ], + width: 3, + height: 1, + cursor: None, + hyperlinks: Vec::new(), + graphics: Vec::new(), + }; + let curr = FrameData { + cells: vec![ + make_cell("A", 0, 0, 0), + make_cell(" ", 0, 0, 0), + make_cell("Z", 0, 0, 0), + ], + width: 3, + height: 1, + cursor: None, + hyperlinks: Vec::new(), + graphics: Vec::new(), + }; + + let mut output = Vec::new(); + blit_frame_to(&mut output, &curr, Some(&prev)); + let output_str = String::from_utf8(output).unwrap(); + + assert!(output_str.contains("\x1b[1;1H")); + assert!( + output_str.contains("\x1b[1;2H"), + "cells hidden by a previous halfwidth voiced kana must be redrawn when visible" + ); + } } |
