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/server | |
| 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/server')
| -rw-r--r-- | src/server/headless.rs | 72 |
1 files changed, 71 insertions, 1 deletions
diff --git a/src/server/headless.rs b/src/server/headless.rs index b4f433f7..859e02af 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -4910,7 +4910,8 @@ mod tests { use super::*; use crate::app::AppState; - use crate::protocol::CursorState; + use crate::protocol::{CellData, CursorState}; + use unicode_width::UnicodeWidthStr; #[path = "pane_graphics.rs"] mod pane_graphics_tests; @@ -5350,6 +5351,16 @@ mod tests { for (idx, (actual_cell, expected_cell)) in actual.cells.iter().zip(expected.cells.iter()).enumerate() { + if cells_equivalent_for_frame_compare( + &actual.cells, + &expected.cells, + usize::from(actual.width), + idx, + actual_cell, + expected_cell, + ) { + continue; + } assert_eq!( actual_cell, expected_cell, @@ -5360,6 +5371,65 @@ mod tests { } } + fn cells_equivalent_for_frame_compare( + actual_cells: &[CellData], + expected_cells: &[CellData], + width: usize, + idx: usize, + actual: &CellData, + expected: &CellData, + ) -> bool { + if actual == expected { + return true; + } + if !cell_style_without_symbol_eq(actual, expected) { + return false; + } + if !matches!( + (actual.symbol.as_str(), expected.symbol.as_str()), + ("", " ") | (" ", "") + ) { + return false; + } + covered_by_previous_wide_cell(actual_cells, width, idx) + || covered_by_previous_wide_cell(expected_cells, width, idx) + } + + fn cell_style_without_symbol_eq(a: &CellData, b: &CellData) -> bool { + a.fg == b.fg + && a.bg == b.bg + && a.modifier == b.modifier + && a.skip == b.skip + && a.hyperlink == b.hyperlink + } + + fn covered_by_previous_wide_cell(cells: &[CellData], width: usize, idx: usize) -> bool { + if idx == 0 || idx.is_multiple_of(width) { + return false; + } + frame_cell_display_width(&cells[idx - 1]) > 1 + } + + fn frame_cell_display_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}') + } + #[test] fn foreground_client_applies_client_keybindings() { let mut server = test_headless_server(); |
