Skip to content

Commit fcb8d3b

Browse files
committed
Fix cursor position for chars where Emacs char-width disagrees with terminal
Emacs' move-to-column uses char-width for column counting, which can disagree with libghostty's wcwidth-based columns for certain characters (e.g. box-drawing glyphs like ┃ on CJK/pgtk systems where char-width returns 2 but the terminal treats them as single-width). This caused the cursor to land on the wrong character. Replace move-to-column with a new positionCursorByCell helper that iterates the cursor row's terminal cells to compute the exact Emacs character offset for the target column, then uses goto-char. This is both correct (independent of char-width) and faster (avoids Elisp display-width scanning). Fixes #86
1 parent 17264f7 commit fcb8d3b

1 file changed

Lines changed: 67 additions & 1 deletion

File tree

src/render.zig

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,70 @@ fn insertScrollbackRange(
750750
}
751751

752752

753+
/// Convert a terminal column to an Emacs character offset by iterating
754+
/// the row's cells. Returns `true` and positions point on success;
755+
/// `false` if the cell data is unavailable (caller should fall back to
756+
/// `move-to-column`).
757+
///
758+
/// This avoids relying on Emacs' `char-width`, which can disagree with
759+
/// the terminal's column width for certain characters (e.g. box-drawing
760+
/// glyphs on CJK/pgtk systems where `char-width` returns 2 but the
761+
/// terminal treats them as single-width).
762+
fn positionCursorByCell(env: emacs.Env, term: *Terminal, cx: u16, cy: u16) bool {
763+
if (cx == 0) return true; // already at column 0
764+
765+
if (gt.c.ghostty_render_state_get(term.render_state, gt.RS_DATA_ROW_ITERATOR, @ptrCast(&term.row_iterator)) != gt.SUCCESS) {
766+
return false;
767+
}
768+
769+
// Advance iterator to cursor row cy.
770+
{
771+
var ri: u16 = 0;
772+
while (ri <= cy) : (ri += 1) {
773+
if (!gt.c.ghostty_render_state_row_iterator_next(term.row_iterator)) {
774+
return false;
775+
}
776+
}
777+
}
778+
779+
if (gt.c.ghostty_render_state_row_get(term.row_iterator, gt.RS_ROW_DATA_CELLS, @ptrCast(&term.row_cells)) != gt.SUCCESS) {
780+
return false;
781+
}
782+
783+
// Walk cells 0..cx-1, counting Emacs characters.
784+
var col: u16 = 0;
785+
var char_count: i64 = 0;
786+
while (col < cx) : (col += 1) {
787+
if (!gt.c.ghostty_render_state_row_cells_next(term.row_cells)) break;
788+
789+
var graphemes_len: u32 = 0;
790+
_ = gt.c.ghostty_render_state_row_cells_get(term.row_cells, gt.RS_CELLS_DATA_GRAPHEMES_LEN, @ptrCast(&graphemes_len));
791+
792+
if (graphemes_len == 0) {
793+
// Spacer tails produce no Emacs character.
794+
var raw_cell: gt.c.GhosttyCell = undefined;
795+
var wide: c_int = gt.c.GHOSTTY_CELL_WIDE_NARROW;
796+
if (gt.c.ghostty_render_state_row_cells_get(term.row_cells, gt.c.GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW, @ptrCast(&raw_cell)) == gt.SUCCESS) {
797+
_ = gt.c.ghostty_cell_get(raw_cell, gt.c.GHOSTTY_CELL_DATA_WIDE, @ptrCast(&wide));
798+
}
799+
if (wide == gt.c.GHOSTTY_CELL_WIDE_SPACER_TAIL) {
800+
continue;
801+
}
802+
char_count += 1; // empty cell → space
803+
} else {
804+
char_count += @intCast(@min(graphemes_len, 16));
805+
}
806+
}
807+
808+
// Cap at end of line so we never jump past it into the next row
809+
// (can happen when cursor is on a trimmed trailing blank).
810+
const pt = env.extractInteger(env.point());
811+
const eol = env.extractInteger(env.lineEndPosition());
812+
const max_chars = eol - pt;
813+
env.gotoCharN(pt + @min(char_count, max_chars));
814+
return true;
815+
}
816+
753817
/// Redraw the terminal into the current Emacs buffer.
754818
///
755819
/// Maintains a "growing buffer" model where the Emacs buffer contains
@@ -1129,7 +1193,9 @@ pub fn redraw(env: emacs.Env, term: *Terminal, force_full_arg: bool) void {
11291193

11301194
env.gotoCharN(viewport_start_int);
11311195
_ = env.forwardLine(@as(i64, cy));
1132-
env.moveToColumn(@as(i64, cx));
1196+
if (!positionCursorByCell(env, term, cx, cy)) {
1197+
env.moveToColumn(@as(i64, cx));
1198+
}
11331199
}
11341200

11351201
// Update cursor style

0 commit comments

Comments
 (0)