aboutsummaryrefslogtreecommitdiff
path: root/src/protocol
diff options
context:
space:
mode:
authorKazunari Kamata <14287197+kazunari-kamata@users.noreply.github.com>2026-08-05 06:48:15 +0900
committerGitHub <noreply@github.com>2026-08-05 00:48:15 +0300
commitbe1891ec5265e901b897846a6113c46cf330a7dc (patch)
treeef895364bd183fec707dc382628c4ef0c427346f /src/protocol
parent09cdd88d0aca35617eb05468c2421b0467656e4f (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.rs85
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"
+ );
+ }
}