@@ -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.
5446const RunInfo = struct {
5547 start_char : usize ,
5648 end_char : usize ,
57- style : CellStyle ,
49+ style : ? CellStyle ,
5850};
5951
6052fn 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).
132124fn 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