Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion lisp/ghostel.el
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,7 @@ Used when `cursor-in-non-selected-windows' resolves to box.")
(declare-function ghostel--mouse-event "ghostel-module")
(declare-function ghostel--new "ghostel-module")
(declare-function ghostel--redraw "ghostel-module" (term &optional full))
(declare-function ghostel--set-bold-config "ghostel-module")
(declare-function ghostel--set-default-colors "ghostel-module")
(declare-function ghostel--set-palette "ghostel-module")
(declare-function ghostel--set-size "ghostel-module" (term rows cols &optional cell-w cell-h))
Expand Down Expand Up @@ -1234,6 +1235,36 @@ Updated whenever the terminal is created or resized.")
"Column count of the native terminal.
Updated whenever the terminal is created or resized.")

(defcustom ghostel-bold-color nil
"Configure how bold text is colored.

If nil (default), bold text uses the same color as normal text.

If `bright', bold text uses the bright version of the current
foreground color (ANSI colors 0-7 map to 8-15).

If a string (hex color like \"#RRGGBB\"), bold text with the default
foreground color uses this specific color. Bold text with a palette
color (0-7) will use the bright version (8-15).

Matches Ghostty 1.2.0's `bold-color' configuration."
:type '(choice (const :tag "None" nil)
(const :tag "Bright" bright)
(string :tag "Fixed color (#RRGGBB)"
:match (lambda (_widget val)
(and (stringp val)
(string-match-p
"\\`#[0-9A-Fa-f]\\{6\\}\\'" val)))))
:group 'ghostel
:set (lambda (sym val)
(set-default sym val)
(dolist (buf (buffer-list))
(with-current-buffer buf
(when (and (derived-mode-p 'ghostel-mode) ghostel--term)
(ghostel--apply-bold-config ghostel--term)
(let ((inhibit-read-only t))
(ghostel--redraw ghostel--term t)))))))

(defvar-local ghostel--cursor-pos nil
"The position of the terminal cursor as (COL . ROW) in terminal screen coords.")

Expand Down Expand Up @@ -5023,6 +5054,15 @@ Falls back to \"#000000\" if the color cannot be resolved."
"")))
(ghostel--set-palette term colors)))))

(defun ghostel--apply-bold-config (term)
"Apply `ghostel-bold-color' to terminal handle TERM."
(when (user-ptrp term)
(ghostel--load-module)
(ghostel--set-bold-config
term (if (eq ghostel-bold-color 'bright)
'bright
ghostel-bold-color))))


;;; Theme synchronization

Expand All @@ -5034,6 +5074,7 @@ Call this after changing the Emacs theme so terminals match."
(with-current-buffer buf
(when (and (derived-mode-p 'ghostel-mode) ghostel--term)
(ghostel--apply-palette ghostel--term)
(ghostel--apply-bold-config ghostel--term)
(when (ghostel--terminal-live-p)
(setq ghostel--force-next-redraw t)
(ghostel--delayed-redraw buf))))))
Expand Down Expand Up @@ -6363,7 +6404,8 @@ buffer can be found again after title-tracking renames it."
;; and the terminal advances the cursor zero rows, leaving the
;; next prompt on top of the image.
(ghostel--set-size-with-cell-dims ghostel--term height width)
(ghostel--apply-palette ghostel--term))
(ghostel--apply-palette ghostel--term)
(ghostel--apply-bold-config ghostel--term))
(ghostel--start-process))))

(defun ghostel--find-buffer-by-identity (identity)
Expand Down Expand Up @@ -6441,6 +6483,7 @@ Signals `user-error' if BUFFER already has a live ghostel process."
;; see the matching call in `ghostel--ensure-buffer-state'.
(ghostel--set-size-with-cell-dims ghostel--term height width)
(ghostel--apply-palette ghostel--term)
(ghostel--apply-bold-config ghostel--term)
(ghostel--spawn-pty program args height width
ghostel--default-stty nil remote-p)))))

Expand Down
34 changes: 31 additions & 3 deletions src/Renderer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ row: RowContent = .{},

font_info: ?FontInfo = null,

/// Bold text coloring configuration.
bold_config: BoldConfig = .none,

pub const BoldConfig = union(enum) {
none,
bright,
fixed: gt.ColorRgb,
};

const FontInfo = struct {
width: i64,
height: i64,
Expand Down Expand Up @@ -285,7 +294,7 @@ fn formatColor(color: gt.ColorRgb, buf: *[7]u8) []const u8 {
}

/// Read the style for the current cell from the render state.
fn readCellProps(cells: gt.RenderStateRowCells, key: CellPropKey) !?CellProps {
fn readCellProps(self: *Self, term: *Terminal, cells: gt.RenderStateRowCells, key: CellPropKey) !?CellProps {
var props: CellProps = .{};

props.fg = gt.rs_row_cells.get(gt.ColorRgb, cells, gt.RS_CELLS_DATA_FG_COLOR) catch |err| switch (err) {
Expand All @@ -310,6 +319,22 @@ fn readCellProps(cells: gt.RenderStateRowCells, key: CellPropKey) !?CellProps {
if (gs.underline_color.tag == gt.c.GHOSTTY_STYLE_COLOR_RGB) {
props.underline_color = gs.underline_color.value.rgb;
}

// Bold color handling (matches Ghostty 1.2.0+)
if (props.bold and self.bold_config != .none) {
if (gs.fg_color.tag == gt.c.GHOSTTY_STYLE_COLOR_PALETTE) {
const index = gs.fg_color.value.palette;
if (index < 8) {
const palette = try term.getColorPalette();
props.fg = palette[index + 8];
}
} else if (gs.fg_color.tag == gt.c.GHOSTTY_STYLE_COLOR_NONE) {
switch (self.bold_config) {
.fixed => |fixed_color| props.fg = fixed_color,
else => {},
}
}
}
}

props.hyperlink = key.hyperlink;
Expand Down Expand Up @@ -500,6 +525,7 @@ pub const RowContent = struct {
/// styled blanks are preserved.
pub fn build(
self: *RowContent,
term: *Terminal,
row: gt.RenderStateRowIterator,
row_cells: *gt.RenderStateRowCells,
adjustment_threshold: u32,
Expand Down Expand Up @@ -568,7 +594,7 @@ pub const RowContent = struct {
try self.runs.append(RowContent.allocator, .{
.start_char = self.char_len,
.end_char = self.char_len,
.props = try readCellProps(row_cells.*, prop_key),
.props = try readCellProps(&term.renderer, term, row_cells.*, prop_key),
});
current_prop_key = prop_key;
}
Expand Down Expand Up @@ -716,9 +742,11 @@ fn adjustGlyphs(self: *Self, env: emacs.Env, row_start: i64) void {
fn insertRow(
self: *Self,
env: emacs.Env,
term: *Terminal,
default_colors: *const BgFg,
) !void {
try self.row.build(
term,
self.row_iterator,
&self.row_cells,
if (self.font_info) |f| f.coverage else std.math.maxInt(u32),
Expand Down Expand Up @@ -853,7 +881,7 @@ pub fn render(self: *Self, env: emacs.Env, term: *Terminal, skip: usize, force_f
const dirty_row = dirty_full or try gt.rs_row.get(bool, self.row_iterator, gt.RS_ROW_DATA_DIRTY);
if (dirty_row) {
env.deleteRegion(env.point(), env.lineBeginningPosition2());
try self.insertRow(env, &default_colors);
try self.insertRow(env, term, &default_colors);
} else {
_ = env.forwardLine(1);
}
Expand Down
1 change: 1 addition & 0 deletions src/emacs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ const interned_symbols = [_][:0]const u8{
":underline",
":weight",
"bold",
"bright",
"char-before",
"cons",
"dash",
Expand Down
37 changes: 37 additions & 0 deletions src/module.zig
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ export fn emacs_module_init(runtime: *c.struct_emacs_runtime) callconv(.c) c_int
\\
\\(ghostel--set-default-colors TERM FG-HEX BG-HEX)
);
env.bindFunction("ghostel--set-bold-config", 2, 2, &fnSetBoldConfig,
\\Configure bold text coloring.
\\
\\CONFIG can be nil (none), 'bright, or a hex color string.
\\
\\(ghostel--set-bold-config TERM CONFIG)
);
env.bindFunction("ghostel--mode-enabled", 2, 2, &fnModeEnabled,
\\Return t if terminal DEC private MODE is enabled.
\\
Expand Down Expand Up @@ -1088,6 +1095,36 @@ fn fnSetDefaultColors(raw_env: ?*c.emacs_env, _: isize, args: [*c]c.emacs_value,
return env.t();
}

/// (ghostel--set-bold-config TERM CONFIG)
///
/// CONFIG can be nil (none), 'bright, or a hex color string.
fn fnSetBoldConfig(raw_env: ?*c.emacs_env, _: isize, args: [*c]c.emacs_value, _: ?*anyopaque) callconv(.c) c.emacs_value {
const env = emacs.Env.init(raw_env.?);
const term = env.getUserPtr(Terminal, args[0]) orelse return env.nil();
const val = args[1];

if (env.isNil(val)) {
term.renderer.bold_config = .none;
} else if (env.eq(val, emacs.sym.bright)) {
term.renderer.bold_config = .bright;
} else {
var hex_buf: [16]u8 = undefined;
const hex = env.extractString(val, &hex_buf) orelse {
env.signalError("invalid bold config value", .{});
return env.nil();
};

if (parseHexColor(hex)) |color| {
term.renderer.bold_config = .{ .fixed = color };
} else {
env.signalError("invalid bold color: %s", .{hex});
return env.nil();
}
}

return env.t();
}

/// (ghostel--debug-state TERM)
/// Returns a string with render state debug info.
///
Expand Down
107 changes: 107 additions & 0 deletions test/ghostel-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -14720,6 +14720,113 @@ slip past the unit tests."
`(and "^ghostel-test-"
(not (member ,@ghostel-test--elisp-tests)))))

(defun ghostel-test--bold-color-palette ()
"Return a 256-entry hex palette string with index 1 red and 9 green.
Used by bold-color tests so palette mapping is observable."
(concat "#000000" ;; 0
"#ff0000" ;; 1 (red)
(apply #'concat (make-list 7 "#000000")) ;; 2..8
"#00ff00" ;; 9 (bright red, distinguishable)
(apply #'concat (make-list 246 "#000000"))))

(ert-deftest ghostel-test-bold-is-bright ()
"Test that bold text uses bright colors when ghostel-bold-color is 'bright."
(let ((buf (generate-new-buffer " *ghostel-test-bold*"))
(ghostel-bold-color 'bright))
(unwind-protect
(with-current-buffer buf
(let* ((term (ghostel--new 5 40 100))
(inhibit-read-only t))
(ghostel--set-palette term (ghostel-test--bold-color-palette))
(ghostel--apply-bold-config term)

;; Write bold red text
(ghostel--write-input term "\e[1;31mBOLD\e[0m")
(ghostel--redraw term)
(goto-char (point-min))
(let ((face (get-text-property (point) 'face)))
(should (equal "#00ff00" (plist-get face :foreground)))
(should (eq 'bold (plist-get face :weight))))))
(kill-buffer buf))))

(ert-deftest ghostel-test-bold-fixed-color ()
"Test that bold text uses a fixed color when ghostel-bold-color is a hex string."
(let ((buf (generate-new-buffer " *ghostel-test-bold-fixed*"))
(ghostel-bold-color "#abcdef"))
(unwind-protect
(with-current-buffer buf
(let* ((term (ghostel--new 5 40 100))
(inhibit-read-only t))
(ghostel--apply-bold-config term)

;; Write bold text without color
(ghostel--write-input term "\e[1mBOLD\e[0m")
(ghostel--redraw term)
(goto-char (point-min))
(let ((face (get-text-property (point) 'face)))
(should (equal "#abcdef" (plist-get face :foreground)))
(should (eq 'bold (plist-get face :weight))))))
(kill-buffer buf))))

(ert-deftest ghostel-test-bold-color-nil-leaves-fg-alone ()
"Test that bold text keeps its original color when `ghostel-bold-color' is nil."
(let ((buf (generate-new-buffer " *ghostel-test-bold-nil*"))
(ghostel-bold-color nil))
(unwind-protect
(with-current-buffer buf
(let* ((term (ghostel--new 5 40 100))
(inhibit-read-only t))
(ghostel--set-palette term (ghostel-test--bold-color-palette))
(ghostel--apply-bold-config term)
;; Bold red (palette 1) must stay red — no brightening to palette 9.
(ghostel--write-input term "\e[1;31mBOLD\e[0m")
(ghostel--redraw term)
(goto-char (point-min))
(let ((face (get-text-property (point) 'face)))
(should (equal "#ff0000" (plist-get face :foreground)))
(should (eq 'bold (plist-get face :weight))))))
(kill-buffer buf))))

(ert-deftest ghostel-test-bold-fixed-also-brightens-palette ()
"Test that fixed-color bold still maps palette 0-7 to 8-15.
The fixed color only applies to default-fg cells; palette colors take
the bright variant just like in `bright' mode."
(let ((buf (generate-new-buffer " *ghostel-test-bold-fixed-palette*"))
(ghostel-bold-color "#abcdef"))
(unwind-protect
(with-current-buffer buf
(let* ((term (ghostel--new 5 40 100))
(inhibit-read-only t))
(ghostel--set-palette term (ghostel-test--bold-color-palette))
(ghostel--apply-bold-config term)
;; Bold red (palette 1) → bright red (palette 9 = #00ff00),
;; NOT the fixed color #abcdef.
(ghostel--write-input term "\e[1;31mBOLD\e[0m")
(ghostel--redraw term)
(goto-char (point-min))
(let ((face (get-text-property (point) 'face)))
(should (equal "#00ff00" (plist-get face :foreground))))))
(kill-buffer buf))))

(ert-deftest ghostel-test-bold-leaves-bright-palette-alone ()
"Test that bold on palette 8-15 is not re-mapped (no overflow into 16-23)."
(let ((buf (generate-new-buffer " *ghostel-test-bold-bright-palette*"))
(ghostel-bold-color 'bright))
(unwind-protect
(with-current-buffer buf
(let* ((term (ghostel--new 5 40 100))
(inhibit-read-only t))
(ghostel--set-palette term (ghostel-test--bold-color-palette))
(ghostel--apply-bold-config term)
;; SGR 91 selects palette 9 directly; bold must not shift it further.
(ghostel--write-input term "\e[1;91mBOLD\e[0m")
(ghostel--redraw term)
(goto-char (point-min))
(let ((face (get-text-property (point) 'face)))
(should (equal "#00ff00" (plist-get face :foreground)))
(should (eq 'bold (plist-get face :weight))))))
(kill-buffer buf))))

(defun ghostel-test-run ()
"Run all ghostel tests."
(ert-run-tests-batch-and-exit "^ghostel-test-"))
Expand Down
Loading