Skip to content

Commit 81f1258

Browse files
emil-edakra
authored andcommitted
Detect style run breaks via cheap CellStyleKey rather than full CellStyle comparison
Replaces per-cell CellStyle.eql() with a two-field CellStyleKey (style_id + hyperlink) that is cheap to read and compare in the tight inner loop. The full readCellStyle call is deferred to run boundaries only. Default styles are represented as ?CellStyle = null, removing the isDefault() guard from applyStyle and the explicit "close final run" block. Wide-character spacer tails are compensated with end_char -= 1 to prevent boundary overcounting.
1 parent 5baea2d commit 81f1258

5 files changed

Lines changed: 41 additions & 52 deletions

File tree

build.zig.zon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.{
22
.name = .ghostel,
3-
.version = "0.20.0",
3+
.version = "0.20.1",
44
.paths = .{""},
55
.fingerprint = 0x5a44bdd1198a0f4b,
66
.dependencies = .{

extensions/evil-ghostel/evil-ghostel.el

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
;; Author: Daniel Kraus <daniel@kraus.my>
66
;; URL: https://github.com/dakra/ghostel
7-
;; Version: 0.20.0
7+
;; Version: 0.20.1
88
;; Package-Requires: ((emacs "28.1") (evil "1.0") (ghostel "0.8.0"))
99
;; SPDX-License-Identifier: GPL-3.0-or-later
1010

lisp/ghostel.el

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
;; Author: Daniel Kraus <daniel@kraus.my>
66
;; URL: https://github.com/dakra/ghostel
7-
;; Version: 0.20.0
7+
;; Version: 0.20.1
88
;; Keywords: terminals
99
;; Package-Requires: ((emacs "28.1"))
1010
;; SPDX-License-Identifier: GPL-3.0-or-later
@@ -626,7 +626,7 @@ before sending the input."
626626
Customize this when downloading pre-built modules from a fork or mirror."
627627
:type 'string)
628628

629-
(defconst ghostel--minimum-module-version "0.20.0"
629+
(defconst ghostel--minimum-module-version "0.20.1"
630630
"Minimum native module version required by this Elisp version.
631631
Bump this only when the Elisp code requires a newer native module
632632
\(e.g. new Zig-exported function or changed calling convention).")

src/module.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const sys = @import("sys.zig");
1515
const c = emacs.c;
1616

1717
/// Module version — keep in sync with ghostel.el and build.zig.zon.
18-
const version = "0.20.0";
18+
const version = "0.20.1";
1919

2020
// ---------------------------------------------------------------------------
2121
// Module entry point

src/render.zig

Lines changed: 36 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,6 @@ const CellStyle = struct {
2222
inverse: bool = false,
2323
hyperlink: bool = false,
2424

25-
fn eql(a: CellStyle, b: CellStyle) bool {
26-
return colorEql(a.fg, b.fg) and
27-
colorEql(a.bg, b.bg) and
28-
a.bold == b.bold and
29-
a.italic == b.italic and
30-
a.faint == b.faint and
31-
a.underline == b.underline and
32-
colorEql(a.underline_color, b.underline_color) and
33-
a.strikethrough == b.strikethrough and
34-
a.inverse == b.inverse and
35-
a.hyperlink == b.hyperlink;
36-
}
37-
3825
fn isDefault(self: CellStyle) bool {
3926
return self.fg == null and
4027
self.bg == null and
@@ -48,13 +35,18 @@ const CellStyle = struct {
4835
}
4936
};
5037

38+
// Unique identifier for what we consider a style. Normally, you would only use
39+
// style_id for this but we also use Emacs text properties for hyperlinks so
40+
// we include "hyperlink-ness" here.
41+
const CellStyleKey = struct { style_id: gt.c.GhosttyStyleId, hyperlink: bool };
42+
5143
/// Track style runs for propertizing after insertion.
5244
/// Positions are in characters (codepoints), not bytes, because
5345
/// Emacs put-text-property works with character positions.
5446
const RunInfo = struct {
5547
start_char: usize,
5648
end_char: usize,
57-
style: CellStyle,
49+
style: ?CellStyle,
5850
};
5951

6052
fn colorEql(a: ?gt.ColorRgb, b: ?gt.ColorRgb) bool {
@@ -87,7 +79,7 @@ fn formatColor(color: gt.ColorRgb, buf: *[7]u8) []const u8 {
8779
}
8880

8981
/// Read the style for the current cell from the render state.
90-
fn readCellStyle(cells: gt.RenderStateRowCells, raw: gt.c.GhosttyCell) CellStyle {
82+
fn readCellStyle(cells: gt.RenderStateRowCells, raw: gt.c.GhosttyCell) ?CellStyle {
9183
var style: CellStyle = .{};
9284

9385
// Read resolved FG color
@@ -124,13 +116,12 @@ fn readCellStyle(cells: gt.RenderStateRowCells, raw: gt.c.GhosttyCell) CellStyle
124116
style.hyperlink = hl;
125117
}
126118

127-
return style;
119+
return if (style.isDefault()) null else style;
128120
}
129121

130122
/// Apply face properties to a region of the buffer.
131123
/// Uses (put-text-property START END 'face PLIST).
132124
fn applyStyle(env: emacs.Env, start: i64, end: i64, style: CellStyle, default_colors: *const BgFg) void {
133-
if (style.isDefault()) return;
134125
if (start >= end) return;
135126

136127
var face_props: [24]emacs.Value = undefined;
@@ -273,6 +264,13 @@ const RowContent = struct {
273264
has_wide: bool,
274265
};
275266

267+
fn getStyleKey(cell: gt.c.GhosttyCell) CellStyleKey {
268+
var key = CellStyleKey{ .style_id = 0, .hyperlink = false };
269+
_ = gt.c.ghostty_cell_get(cell, gt.c.GHOSTTY_CELL_DATA_STYLE_ID, @ptrCast(&key.style_id));
270+
_ = gt.c.ghostty_cell_get(cell, gt.c.GHOSTTY_CELL_DATA_HAS_HYPERLINK, @ptrCast(&key.hyperlink));
271+
return key;
272+
}
273+
276274
/// Build text content and style runs for the current row in the iterator.
277275
/// Style runs use character (codepoint) offsets for Emacs put-text-property.
278276
///
@@ -299,9 +297,8 @@ fn buildRowContent(
299297
var prompt_char_len: usize = 0; // chars that are semantic prompt
300298
var in_prompt: bool = true; // track contiguous leading prompt cells
301299
var has_wide: bool = false;
300+
var current_style_key: ?CellStyleKey = null;
302301
run_count.* = 0;
303-
var current_style: CellStyle = .{};
304-
var run_start_char: usize = 0;
305302

306303
while (gt.c.ghostty_render_state_row_cells_next(term.row_cells)) {
307304
var graphemes_len: u32 = 0;
@@ -322,22 +319,21 @@ fn buildRowContent(
322319
}
323320
}
324321

325-
const cell_style = readCellStyle(term.row_cells, raw_cell);
326-
327-
// Flush run on style change
328-
if (char_len > run_start_char and !cell_style.eql(current_style)) {
329-
if (run_count.* < runs.len) {
330-
runs[run_count.*] = .{
331-
.start_char = run_start_char,
332-
.end_char = char_len,
333-
.style = current_style,
334-
};
335-
run_count.* += 1;
336-
}
337-
run_start_char = char_len;
338-
current_style = cell_style;
339-
} else if (char_len == run_start_char) {
340-
current_style = cell_style;
322+
// We use a "key" that holds a minimum set of values that are cheap to
323+
// read and compare to detect style run breaks. Only when we detect a
324+
// break do we read the cell style, which is a more expensive operation
325+
// in such a tight loop.
326+
const style_key: CellStyleKey = getStyleKey(raw_cell);
327+
if (!std.meta.eql(@as(?CellStyleKey, style_key), current_style_key) and run_count.* < runs.len) {
328+
runs[run_count.*] = .{
329+
.start_char = char_len,
330+
.end_char = char_len + 1,
331+
.style = readCellStyle(term.row_cells, raw_cell),
332+
};
333+
run_count.* += 1;
334+
current_style_key = style_key;
335+
} else {
336+
runs[run_count.* - 1].end_char += 1;
341337
}
342338

343339
if (graphemes_len == 0) {
@@ -347,6 +343,7 @@ fn buildRowContent(
347343
var wide: c_int = gt.c.GHOSTTY_CELL_WIDE_NARROW;
348344
_ = gt.c.ghostty_cell_get(raw_cell, gt.c.GHOSTTY_CELL_DATA_WIDE, @ptrCast(&wide));
349345
if (wide == gt.c.GHOSTTY_CELL_WIDE_SPACER_TAIL) {
346+
runs[run_count.* - 1].end_char -= 1;
350347
has_wide = true;
351348
continue;
352349
}
@@ -358,7 +355,7 @@ fn buildRowContent(
358355
if (in_prompt) prompt_char_len = char_len;
359356
// Empty cells are blank for trim purposes unless their
360357
// style has a visible attribute (e.g. colored background).
361-
if (!cell_style.isDefault()) {
358+
if (runs[run_count.* - 1].style != null) {
362359
trim_text_len = text_len;
363360
trim_char_len = char_len;
364361
}
@@ -398,16 +395,6 @@ fn buildRowContent(
398395
char_len = trim_char_len;
399396
if (prompt_char_len > char_len) prompt_char_len = char_len;
400397

401-
// Close final run
402-
if (char_len > run_start_char and run_count.* < runs.len) {
403-
runs[run_count.*] = .{
404-
.start_char = run_start_char,
405-
.end_char = char_len,
406-
.style = current_style,
407-
};
408-
run_count.* += 1;
409-
}
410-
411398
return .{ .byte_len = text_len, .char_len = char_len, .prompt_char_len = prompt_char_len, .has_wide = has_wide };
412399
}
413400

@@ -453,7 +440,9 @@ fn insertAndStyle(
453440

454441
const prop_start = row_start + @as(i64, @intCast(run.start_char));
455442
const prop_end = row_start + @as(i64, @intCast(run_end));
456-
applyStyle(env, prop_start, prop_end, run.style, default_colors);
443+
if (run.style) |style| {
444+
applyStyle(env, prop_start, prop_end, style, default_colors);
445+
}
457446
}
458447

459448
if (!newline_in_buf) {

0 commit comments

Comments
 (0)