Skip to content

Commit b42321f

Browse files
committed
Add ANSI 16-color palette customization
The ghostel-color-palette defcustom lets users configure the 16 ANSI colors as a list of hex strings. The palette is applied to the terminal at creation time via ghostel--set-palette, which parses the hex colors in Zig and merges them with the default 256-color palette. Rendered text now uses custom palette colors for SGR 30-37/90-97.
1 parent f384746 commit b42321f

5 files changed

Lines changed: 174 additions & 1 deletion

File tree

ghostel.el

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
(declare-function ghostel--encode-key "ghostel-module")
3737
(declare-function ghostel--mouse-event "ghostel-module")
3838
(declare-function ghostel--focus-event "ghostel-module")
39+
(declare-function ghostel--set-palette "ghostel-module")
3940

4041
;;; Customization
4142

@@ -71,6 +72,27 @@ These keys pass through to Emacs instead."
7172
:type '(repeat string)
7273
:group 'ghostel)
7374

75+
(defcustom ghostel-color-palette
76+
'("#000000" ; 0 black
77+
"#aa0000" ; 1 red
78+
"#00aa00" ; 2 green
79+
"#aa5500" ; 3 yellow
80+
"#0000aa" ; 4 blue
81+
"#aa00aa" ; 5 magenta
82+
"#00aaaa" ; 6 cyan
83+
"#aaaaaa" ; 7 white
84+
"#555555" ; 8 bright black
85+
"#ff5555" ; 9 bright red
86+
"#55ff55" ; 10 bright green
87+
"#ffff55" ; 11 bright yellow
88+
"#5555ff" ; 12 bright blue
89+
"#ff55ff" ; 13 bright magenta
90+
"#55ffff" ; 14 bright cyan
91+
"#ffffff") ; 15 bright white
92+
"ANSI 16-color palette for the terminal.
93+
Each entry is a hex color string. Changes take effect on new terminals."
94+
:type '(repeat color)
95+
:group 'ghostel)
7496

7597
;;; Internal variables
7698

@@ -542,6 +564,16 @@ DIR may be a file:// URL or a plain path."
542564
(when (and path (file-directory-p path))
543565
(setq default-directory (file-name-as-directory path))))))
544566

567+
;;; Palette
568+
569+
(defun ghostel--apply-palette (term)
570+
"Apply `ghostel-color-palette' to TERM."
571+
(when (and term ghostel-color-palette)
572+
(let ((colors (mapconcat #'identity
573+
(seq-take ghostel-color-palette 16)
574+
"")))
575+
(ghostel--set-palette term colors))))
576+
545577
;;; Focus events
546578

547579
(defun ghostel--focus-in ()
@@ -696,7 +728,8 @@ PROCESS is the shell process, WINDOWS is the list of windows."
696728
(let* ((height (window-body-height))
697729
(width (window-max-chars-per-line)))
698730
(setq ghostel--term
699-
(ghostel--new height width ghostel-max-scrollback)))
731+
(ghostel--new height width ghostel-max-scrollback))
732+
(ghostel--apply-palette ghostel--term))
700733
(ghostel--start-process))
701734
(switch-to-buffer buffer)))
702735

src/ghostty.zig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ pub const OPT_DEVICE_ATTRIBUTES = c.GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES;
4848
pub const OPT_XTVERSION = c.GHOSTTY_TERMINAL_OPT_XTVERSION;
4949
pub const OPT_COLOR_FOREGROUND = c.GHOSTTY_TERMINAL_OPT_COLOR_FOREGROUND;
5050
pub const OPT_COLOR_BACKGROUND = c.GHOSTTY_TERMINAL_OPT_COLOR_BACKGROUND;
51+
pub const OPT_COLOR_PALETTE = c.GHOSTTY_TERMINAL_OPT_COLOR_PALETTE;
52+
pub const DATA_COLOR_PALETTE = c.GHOSTTY_TERMINAL_DATA_COLOR_PALETTE;
5153

5254
// Terminal data constants
5355
pub const DATA_COLS = c.GHOSTTY_TERMINAL_DATA_COLS;

src/module.zig

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export fn emacs_module_init(runtime: *c.struct_emacs_runtime) callconv(.c) c_int
3535
env.bindFunction("ghostel--encode-key", 3, 4, &fnEncodeKey, "Encode a key event using the terminal's key encoder.\n\n(ghostel--encode-key TERM KEY MODS &optional UTF8)");
3636
env.bindFunction("ghostel--mouse-event", 6, 6, &fnMouseEvent, "Send a mouse event to the terminal.\n\n(ghostel--mouse-event TERM ACTION BUTTON ROW COL MODS)");
3737
env.bindFunction("ghostel--focus-event", 2, 2, &fnFocusEvent, "Send a focus event to the terminal.\n\n(ghostel--focus-event TERM GAINED)");
38+
env.bindFunction("ghostel--set-palette", 2, 2, &fnSetPalette, "Set the ANSI color palette.\n\n(ghostel--set-palette TERM COLORS-STRING)");
3839
env.bindFunction("ghostel--debug-state", 1, 1, &fnDebugState, "Return debug info about terminal/render state.\n\n(ghostel--debug-state TERM)");
3940
env.bindFunction("ghostel--debug-feed", 2, 2, &fnDebugFeed, "Feed STR to terminal and return first row + cursor.\n\n(ghostel--debug-feed TERM STR)");
4041

@@ -271,6 +272,74 @@ fn fnFocusEvent(raw_env: ?*c.emacs_env, _: isize, args: [*c]c.emacs_value, _: ?*
271272
return env.t();
272273
}
273274

275+
/// (ghostel--set-palette TERM COLORS-STRING)
276+
/// Set the 16 ANSI colors from a concatenated hex string like "#000000#aa0000...".
277+
/// The remaining 240 palette entries are taken from the terminal's current palette.
278+
fn fnSetPalette(raw_env: ?*c.emacs_env, _: isize, args: [*c]c.emacs_value, _: ?*anyopaque) callconv(.c) c.emacs_value {
279+
const env = emacs.Env.init(raw_env.?);
280+
const term = env.getUserPtr(Terminal, args[0]) orelse {
281+
env.signalError("ghostel: invalid terminal handle");
282+
return env.nil();
283+
};
284+
285+
var str_buf: [2048]u8 = undefined;
286+
const colors_str = env.extractString(args[1], &str_buf) orelse {
287+
env.signalError("ghostel: invalid palette string");
288+
return env.nil();
289+
};
290+
291+
// Get current palette as base (keeps entries 16-255)
292+
var palette: [256]gt.ColorRgb = undefined;
293+
if (!term.getColorPalette(&palette)) {
294+
env.signalError("ghostel: failed to get current palette");
295+
return env.nil();
296+
}
297+
298+
// Parse "#RRGGBB" entries — 7 chars each
299+
var idx: usize = 0;
300+
var pos: usize = 0;
301+
while (idx < 16 and pos + 7 <= colors_str.len) {
302+
if (colors_str[pos] != '#') {
303+
pos += 1;
304+
continue;
305+
}
306+
const r = parseHexByte(colors_str[pos + 1], colors_str[pos + 2]) orelse {
307+
pos += 7;
308+
idx += 1;
309+
continue;
310+
};
311+
const g = parseHexByte(colors_str[pos + 3], colors_str[pos + 4]) orelse {
312+
pos += 7;
313+
idx += 1;
314+
continue;
315+
};
316+
const b = parseHexByte(colors_str[pos + 5], colors_str[pos + 6]) orelse {
317+
pos += 7;
318+
idx += 1;
319+
continue;
320+
};
321+
palette[idx] = .{ .r = r, .g = g, .b = b };
322+
idx += 1;
323+
pos += 7;
324+
}
325+
326+
term.setColorPalette(&palette);
327+
return env.t();
328+
}
329+
330+
fn parseHexByte(hi: u8, lo: u8) ?u8 {
331+
const h = hexDigit(hi) orelse return null;
332+
const l = hexDigit(lo) orelse return null;
333+
return (h << 4) | l;
334+
}
335+
336+
fn hexDigit(ch: u8) ?u8 {
337+
if (ch >= '0' and ch <= '9') return ch - '0';
338+
if (ch >= 'a' and ch <= 'f') return ch - 'a' + 10;
339+
if (ch >= 'A' and ch <= 'F') return ch - 'A' + 10;
340+
return null;
341+
}
342+
274343
/// (ghostel--debug-state TERM)
275344
/// Returns a string with render state debug info.
276345
fn fnDebugState(raw_env: ?*c.emacs_env, _: isize, args: [*c]c.emacs_value, _: ?*anyopaque) callconv(.c) c.emacs_value {

src/terminal.zig

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,24 @@ pub fn setColorBackground(self: *Self, color: *const gt.ColorRgb) void {
152152
);
153153
}
154154

155+
/// Set the color palette (256 entries).
156+
pub fn setColorPalette(self: *Self, palette: *const [256]gt.ColorRgb) void {
157+
_ = gt.c.ghostty_terminal_set(
158+
self.terminal,
159+
gt.OPT_COLOR_PALETTE,
160+
palette,
161+
);
162+
}
163+
164+
/// Get the current color palette (256 entries).
165+
pub fn getColorPalette(self: *Self, palette: *[256]gt.ColorRgb) bool {
166+
return gt.c.ghostty_terminal_get(
167+
self.terminal,
168+
gt.DATA_COLOR_PALETTE,
169+
@ptrCast(palette),
170+
) == gt.SUCCESS;
171+
}
172+
155173
/// Feed VT data from the PTY into the terminal.
156174
pub fn vtWrite(self: *Self, data: []const u8) void {
157175
gt.c.ghostty_terminal_vt_write(self.terminal, data.ptr, data.len);

test/ghostel-test.el

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,55 @@
483483
"aaabbb\nccc"
484484
(ghostel--filter-soft-wraps s))))
485485

486+
;; -----------------------------------------------------------------------
487+
;; Test: ANSI color palette customization
488+
;; -----------------------------------------------------------------------
489+
490+
(defun ghostel-test-color-palette ()
491+
"Test setting a custom ANSI color palette."
492+
(message "--- color palette ---")
493+
(let ((buf (generate-new-buffer " *ghostel-test-palette*")))
494+
(unwind-protect
495+
(with-current-buffer buf
496+
(let* ((term (ghostel--new 5 40 100))
497+
(inhibit-read-only t))
498+
;; Set a custom palette with bright red = #ff0000
499+
(let ((palette (make-list 16 "#000000")))
500+
(setcar (nthcdr 1 palette) "#ff0000") ; red
501+
(setcar (nthcdr 2 palette) "#00ff00") ; green
502+
(ghostel--set-palette term
503+
(mapconcat #'identity palette "")))
504+
;; Write red text (SGR 31 = ANSI red = palette index 1)
505+
(ghostel--write-input term "\e[31mRED\e[0m")
506+
(ghostel--redraw term)
507+
;; Check that the text appears
508+
(ghostel-test--assert-match "red text rendered"
509+
"RED"
510+
(buffer-substring-no-properties
511+
(point-min) (point-max)))
512+
;; Check that the face property uses our custom red
513+
(goto-char (point-min))
514+
(let ((face (get-text-property (point) 'face)))
515+
(ghostel-test--assert "face property exists" face)
516+
(when face
517+
(let ((fg (plist-get face :foreground)))
518+
(ghostel-test--assert "foreground is custom red"
519+
(and fg (string= fg "#ff0000"))))))))
520+
(kill-buffer buf))))
521+
522+
(defun ghostel-test-apply-palette ()
523+
"Test the Elisp apply-palette helper."
524+
(message "--- apply-palette ---")
525+
(let ((term (ghostel--new 5 40 100))
526+
(ghostel-color-palette
527+
'("#111111" "#ff0000" "#00ff00" "#ffff00"
528+
"#0000ff" "#ff00ff" "#00ffff" "#ffffff"
529+
"#333333" "#ff3333" "#33ff33" "#ffff33"
530+
"#3333ff" "#ff33ff" "#33ffff" "#ffffff")))
531+
;; Should not error
532+
(ghostel-test--assert "apply-palette succeeds"
533+
(ghostel--apply-palette term))))
534+
486535
;; -----------------------------------------------------------------------
487536
;; Runner
488537
;; -----------------------------------------------------------------------
@@ -514,6 +563,8 @@
514563
(ghostel-test-incremental-redraw)
515564
(ghostel-test-focus-events)
516565
(ghostel-test-soft-wrap-copy)
566+
(ghostel-test-color-palette)
567+
(ghostel-test-apply-palette)
517568

518569
;; Integration test (spawns a real shell)
519570
(ghostel-test-shell-integration)

0 commit comments

Comments
 (0)