Skip to content

Commit 58f92e3

Browse files
committed
Add OSC 8 hyperlink support with click-to-open
Use the ghostty HTML formatter API to detect hyperlink spans and their URIs, then apply text properties (help-echo, mouse-face, keymap) so users can click or press RET on terminal hyperlinks to open them in a browser.
1 parent 1d676df commit 58f92e3

6 files changed

Lines changed: 371 additions & 33 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ Soft-wrapped newlines are automatically stripped from copied text.
123123
### Shell Integration
124124
- Directory tracking via OSC 7
125125
- Title tracking (buffer renamed from OSC 2)
126+
- OSC 8 hyperlinks — clickable URLs in terminal output (click or `RET` to open)
126127
- OSC 52 clipboard support (opt-in, for remote sessions)
127128
- `INSIDE_EMACS` and `EMACS_GHOSTEL_PATH` environment variables
128129

ghostel.el

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,31 @@ stripped so the copied text matches the original terminal content."
777777
(message "Copied to kill ring")))
778778
(ghostel-copy-mode-exit))
779779

780+
;;; Hyperlinks (OSC 8)
781+
782+
(defvar ghostel-link-map
783+
(let ((map (make-sparse-keymap)))
784+
(define-key map [mouse-1] #'ghostel-open-link-at-click)
785+
(define-key map [mouse-2] #'ghostel-open-link-at-click)
786+
(define-key map (kbd "RET") #'ghostel-open-link-at-point)
787+
map)
788+
"Keymap for clickable hyperlinks in ghostel buffers.")
789+
790+
(defun ghostel-open-link-at-click (event)
791+
"Open the hyperlink at the mouse click position."
792+
(interactive "e")
793+
(let* ((pos (posn-point (event-start event)))
794+
(url (get-text-property pos 'help-echo)))
795+
(when (and url (stringp url) (string-match-p "\\`https?://" url))
796+
(browse-url url))))
797+
798+
(defun ghostel-open-link-at-point ()
799+
"Open the hyperlink at point."
800+
(interactive)
801+
(let ((url (get-text-property (point) 'help-echo)))
802+
(when (and url (stringp url) (string-match-p "\\`https?://" url))
803+
(browse-url url))))
804+
780805
;;; Callbacks from native module
781806

782807
(defun ghostel--osc52-handle (_selection base64-data)

src/emacs.zig

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,52 @@ pub const Env = struct {
184184
_ = self.call1(self.intern("provide"), self.intern(feature));
185185
}
186186

187+
// --- Buffer helpers ---
188+
189+
pub fn point(self: Env) Value {
190+
return self.call0(self.intern("point"));
191+
}
192+
193+
pub fn gotoChar(self: Env, pos: Value) void {
194+
_ = self.call1(self.intern("goto-char"), pos);
195+
}
196+
197+
pub fn gotoCharN(self: Env, pos: i64) void {
198+
_ = self.call1(self.intern("goto-char"), self.makeInteger(pos));
199+
}
200+
201+
pub fn insert(self: Env, text: []const u8) void {
202+
_ = self.call1(self.intern("insert"), self.makeString(text));
203+
}
204+
205+
pub fn forwardLine(self: Env, n: i64) i64 {
206+
return self.extractInteger(self.call1(self.intern("forward-line"), self.makeInteger(n)));
207+
}
208+
209+
pub fn moveToColumn(self: Env, col: i64) void {
210+
_ = self.call1(self.intern("move-to-column"), self.makeInteger(col));
211+
}
212+
213+
pub fn eraseBuffer(self: Env) void {
214+
_ = self.call0(self.intern("erase-buffer"));
215+
}
216+
217+
pub fn lineEndPosition(self: Env) Value {
218+
return self.call0(self.intern("line-end-position"));
219+
}
220+
221+
pub fn pointMax(self: Env) Value {
222+
return self.call0(self.intern("point-max"));
223+
}
224+
225+
pub fn deleteRegion(self: Env, start: Value, end: Value) void {
226+
_ = self.call2(self.intern("delete-region"), start, end);
227+
}
228+
229+
pub fn putTextProperty(self: Env, start: Value, end: Value, prop: Value, value: Value) void {
230+
_ = self.call4(self.intern("put-text-property"), start, end, prop, value);
231+
}
232+
187233
/// Signal an error with a message string.
188234
pub fn signalError(self: Env, msg: []const u8) void {
189235
self.nonLocalExitSignal(

src/ghostty.zig

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,10 @@ pub const CURSOR_BLOCK_HOLLOW: c_int = c.GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYL
106106
pub const SCROLL_TOP: c_int = c.GHOSTTY_SCROLL_VIEWPORT_TOP;
107107
pub const SCROLL_BOTTOM: c_int = c.GHOSTTY_SCROLL_VIEWPORT_BOTTOM;
108108
pub const SCROLL_DELTA: c_int = c.GHOSTTY_SCROLL_VIEWPORT_DELTA;
109+
110+
// Formatter types and constants
111+
pub const Formatter = c.GhosttyFormatter;
112+
pub const FormatterTerminalOptions = c.GhosttyFormatterTerminalOptions;
113+
pub const FormatterTerminalExtra = c.GhosttyFormatterTerminalExtra;
114+
pub const FormatterScreenExtra = c.GhosttyFormatterScreenExtra;
115+
pub const FORMATTER_FORMAT_HTML: c_int = c.GHOSTTY_FORMATTER_FORMAT_HTML;

0 commit comments

Comments
 (0)