Skip to content
Open
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
81 changes: 46 additions & 35 deletions include/ghostty.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ typedef SSIZE_T ssize_t;
#include <sys/types.h>
#endif

// Homoglyph URL report layout for `ghostty_runtime_confirm_read_clipboard_cb` only (stddef; no VT paste API).
#include "ghostty/vt/paste_homoglyph_report.h"

//-------------------------------------------------------------------
// Macros

Expand Down Expand Up @@ -84,6 +87,11 @@ typedef enum {
GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE,
} ghostty_clipboard_request_e;

typedef enum {
GHOSTTY_CLIPBOARD_CONFIRM_REASON_NONE,
GHOSTTY_CLIPBOARD_CONFIRM_REASON_MIXED_SCRIPT_URL,
} ghostty_clipboard_confirm_reason_e;

typedef enum {
GHOSTTY_MOUSE_RELEASE,
GHOSTTY_MOUSE_PRESS,
Expand Down Expand Up @@ -1001,11 +1009,14 @@ typedef void (*ghostty_runtime_wakeup_cb)(void*);
typedef bool (*ghostty_runtime_read_clipboard_cb)(void*,
ghostty_clipboard_e,
void*);

typedef void (*ghostty_runtime_confirm_read_clipboard_cb)(
void*,
const char*,
void*,
ghostty_clipboard_request_e);
ghostty_clipboard_request_e,
ghostty_clipboard_confirm_reason_e,
const ghostty_paste_homoglyph_report_t*);
typedef void (*ghostty_runtime_write_clipboard_cb)(void*,
ghostty_clipboard_e,
const ghostty_clipboard_content_s*,
Expand Down Expand Up @@ -1076,14 +1087,14 @@ GHOSTTY_API void ghostty_config_load_recursive_files(ghostty_config_t);
GHOSTTY_API void ghostty_config_finalize(ghostty_config_t);
GHOSTTY_API bool ghostty_config_get(ghostty_config_t, void*, const char*, uintptr_t);
GHOSTTY_API ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t,
const char*,
uintptr_t);
const char*,
uintptr_t);
GHOSTTY_API uint32_t ghostty_config_diagnostics_count(ghostty_config_t);
GHOSTTY_API ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t);
GHOSTTY_API ghostty_string_s ghostty_config_open_path(void);

GHOSTTY_API ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
ghostty_config_t);
ghostty_config_t);
GHOSTTY_API void ghostty_app_free(ghostty_app_t);
GHOSTTY_API void ghostty_app_tick(ghostty_app_t);
GHOSTTY_API void* ghostty_app_userdata(ghostty_app_t);
Expand All @@ -1100,7 +1111,7 @@ GHOSTTY_API void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_schem
GHOSTTY_API ghostty_surface_config_s ghostty_surface_config_new();

GHOSTTY_API ghostty_surface_t ghostty_surface_new(ghostty_app_t,
const ghostty_surface_config_s*);
const ghostty_surface_config_s*);
GHOSTTY_API void ghostty_surface_free(ghostty_surface_t);
GHOSTTY_API void* ghostty_surface_userdata(ghostty_surface_t);
GHOSTTY_API ghostty_app_t ghostty_surface_app(ghostty_surface_t);
Expand All @@ -1116,48 +1127,48 @@ GHOSTTY_API void ghostty_surface_set_occlusion(ghostty_surface_t, bool);
GHOSTTY_API void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t);
GHOSTTY_API ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t);
GHOSTTY_API void ghostty_surface_set_color_scheme(ghostty_surface_t,
ghostty_color_scheme_e);
ghostty_color_scheme_e);
GHOSTTY_API ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t,
ghostty_input_mods_e);
ghostty_input_mods_e);
GHOSTTY_API bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s);
GHOSTTY_API bool ghostty_surface_key_is_binding(ghostty_surface_t,
ghostty_input_key_s,
ghostty_binding_flags_e*);
ghostty_input_key_s,
ghostty_binding_flags_e*);
GHOSTTY_API void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t);
GHOSTTY_API void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t);
GHOSTTY_API bool ghostty_surface_mouse_captured(ghostty_surface_t);
GHOSTTY_API bool ghostty_surface_mouse_button(ghostty_surface_t,
ghostty_input_mouse_state_e,
ghostty_input_mouse_button_e,
ghostty_input_mods_e);
ghostty_input_mouse_state_e,
ghostty_input_mouse_button_e,
ghostty_input_mods_e);
GHOSTTY_API void ghostty_surface_mouse_pos(ghostty_surface_t,
double,
double,
ghostty_input_mods_e);
double,
double,
ghostty_input_mods_e);
GHOSTTY_API void ghostty_surface_mouse_scroll(ghostty_surface_t,
double,
double,
ghostty_input_scroll_mods_t);
double,
double,
ghostty_input_scroll_mods_t);
GHOSTTY_API void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double);
GHOSTTY_API void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*);
GHOSTTY_API void ghostty_surface_request_close(ghostty_surface_t);
GHOSTTY_API void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e);
GHOSTTY_API void ghostty_surface_split_focus(ghostty_surface_t,
ghostty_action_goto_split_e);
ghostty_action_goto_split_e);
GHOSTTY_API void ghostty_surface_split_resize(ghostty_surface_t,
ghostty_action_resize_split_direction_e,
uint16_t);
ghostty_action_resize_split_direction_e,
uint16_t);
GHOSTTY_API void ghostty_surface_split_equalize(ghostty_surface_t);
GHOSTTY_API bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t);
GHOSTTY_API void ghostty_surface_complete_clipboard_request(ghostty_surface_t,
const char*,
void*,
bool);
const char*,
void*,
bool);
GHOSTTY_API bool ghostty_surface_has_selection(ghostty_surface_t);
GHOSTTY_API bool ghostty_surface_read_selection(ghostty_surface_t, ghostty_text_s*);
GHOSTTY_API bool ghostty_surface_read_text(ghostty_surface_t,
ghostty_selection_s,
ghostty_text_s*);
ghostty_selection_s,
ghostty_text_s*);
GHOSTTY_API void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*);

#ifdef __APPLE__
Expand All @@ -1172,18 +1183,18 @@ GHOSTTY_API void ghostty_inspector_set_focus(ghostty_inspector_t, bool);
GHOSTTY_API void ghostty_inspector_set_content_scale(ghostty_inspector_t, double, double);
GHOSTTY_API void ghostty_inspector_set_size(ghostty_inspector_t, uint32_t, uint32_t);
GHOSTTY_API void ghostty_inspector_mouse_button(ghostty_inspector_t,
ghostty_input_mouse_state_e,
ghostty_input_mouse_button_e,
ghostty_input_mods_e);
ghostty_input_mouse_state_e,
ghostty_input_mouse_button_e,
ghostty_input_mods_e);
GHOSTTY_API void ghostty_inspector_mouse_pos(ghostty_inspector_t, double, double);
GHOSTTY_API void ghostty_inspector_mouse_scroll(ghostty_inspector_t,
double,
double,
ghostty_input_scroll_mods_t);
double,
double,
ghostty_input_scroll_mods_t);
GHOSTTY_API void ghostty_inspector_key(ghostty_inspector_t,
ghostty_input_action_e,
ghostty_input_key_e,
ghostty_input_mods_e);
ghostty_input_action_e,
ghostty_input_key_e,
ghostty_input_mods_e);
GHOSTTY_API void ghostty_inspector_text(ghostty_inspector_t, const char*);

#ifdef __APPLE__
Expand Down
16 changes: 16 additions & 0 deletions include/ghostty/vt/paste.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

#include <stdbool.h>
#include <stddef.h>
#include "ghostty/vt/paste_homoglyph_report.h"
#include <ghostty/vt/types.h>

#ifdef __cplusplus
Expand Down Expand Up @@ -92,6 +93,21 @@ GHOSTTY_API GhosttyResult ghostty_paste_encode(
size_t buf_len,
size_t* out_written);

GHOSTTY_API size_t ghostty_paste_homoglyph_suspicious_spans(const char* data,
size_t len,
ghostty_paste_homoglyph_span_t* out,
size_t max_out);


GHOSTTY_API int ghostty_paste_homoglyph_first_url_range(const char* data,
size_t len,
size_t* out_start,
size_t* out_end);

GHOSTTY_API int ghostty_paste_homoglyph_first_url_report(const char* data,
size_t len,
ghostty_paste_homoglyph_report_t* out);

#ifdef __cplusplus
}
#endif
Expand Down
39 changes: 39 additions & 0 deletions include/ghostty/vt/paste_homoglyph_report.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @file paste_homoglyph_report.h
*
* Layout for mixed-script URL homoglyph data passed to embedders via
* `ghostty_runtime_confirm_read_clipboard_cb`. Shared with `paste.h` for the VT
* library; this header has no VT paste API and does not include `types.h`.
*/

#ifndef GHOSTTY_VT_PASTE_HOMOGLYPH_REPORT_H
#define GHOSTTY_VT_PASTE_HOMOGLYPH_REPORT_H

#include <stddef.h>

#ifdef __cplusplus
extern "C" {
#endif

/** UTF-8 byte range in paste data (`end` exclusive). */
typedef struct {
size_t start;
size_t end;
} ghostty_paste_homoglyph_span_t;

/** Max spans stored in `ghostty_paste_homoglyph_report_t::spans`. */
#define GHOSTTY_PASTE_HOMOGLYPH_REPORT_MAX_SPANS 128

typedef struct {
size_t url_start;
size_t url_end;
size_t span_total;
size_t span_written;
ghostty_paste_homoglyph_span_t spans[GHOSTTY_PASTE_HOMOGLYPH_REPORT_MAX_SPANS];
} ghostty_paste_homoglyph_report_t;

#ifdef __cplusplus
}
#endif

#endif /* GHOSTTY_VT_PASTE_HOMOGLYPH_REPORT_H */
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,25 @@ class ClipboardConfirmationController: NSWindowController {
let surface: ghostty_surface_t
let contents: String
let request: Ghostty.ClipboardRequest
let confirmReason: Ghostty.ClipboardConfirmReason
let homoglyphHighlight: Ghostty.PasteHomoglyphURLHighlight?
let state: UnsafeMutableRawPointer?
weak private var delegate: ClipboardConfirmationViewDelegate?

init(surface: ghostty_surface_t, contents: String, request: Ghostty.ClipboardRequest, state: UnsafeMutableRawPointer?, delegate: ClipboardConfirmationViewDelegate) {
init(
surface: ghostty_surface_t,
contents: String,
request: Ghostty.ClipboardRequest,
confirmReason: Ghostty.ClipboardConfirmReason = .none,
homoglyphHighlight: Ghostty.PasteHomoglyphURLHighlight? = nil,
state: UnsafeMutableRawPointer?,
delegate: ClipboardConfirmationViewDelegate
) {
self.surface = surface
self.contents = contents
self.request = request
self.confirmReason = confirmReason
self.homoglyphHighlight = homoglyphHighlight
self.state = state
self.delegate = delegate
super.init(window: nil)
Expand All @@ -33,16 +45,13 @@ class ClipboardConfirmationController: NSWindowController {
override func windowDidLoad() {
guard let window = window else { return }

switch request {
case .paste:
window.title = "Warning: Potentially Unsafe Paste"
case .osc_52_read, .osc_52_write:
window.title = "Authorize Clipboard Access"
}
window.title = request.windowTitle(confirmReason: confirmReason)

window.contentView = NSHostingView(rootView: ClipboardConfirmationView(
contents: contents,
request: request,
confirmReason: confirmReason,
homoglyphHighlight: homoglyphHighlight,
delegate: delegate
))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,31 @@ struct ClipboardConfirmationView: View {
/// The type of the clipboard request
let request: Ghostty.ClipboardRequest

/// When request is paste, selects between default unsafe-paste copy and mixed-script URL copy.
let confirmReason: Ghostty.ClipboardConfirmReason

let homoglyphHighlight: Ghostty.PasteHomoglyphURLHighlight?

/// Optional delegate to get results. If this is nil, then this view will never close on its own.
weak var delegate: ClipboardConfirmationViewDelegate?

/// Used to track if we should rehide on disappear
@State private var cursorHiddenCount: UInt = 0

init(
contents: String,
request: Ghostty.ClipboardRequest,
confirmReason: Ghostty.ClipboardConfirmReason,
homoglyphHighlight: Ghostty.PasteHomoglyphURLHighlight? = nil,
delegate: ClipboardConfirmationViewDelegate?
) {
self.contents = contents
self.request = request
self.confirmReason = confirmReason
self.homoglyphHighlight = homoglyphHighlight
self.delegate = delegate
}

var body: some View {
VStack {
HStack {
Expand All @@ -46,14 +65,33 @@ struct ClipboardConfirmationView: View {
.padding()
.frame(alignment: .center)

Text(request.text())
Text(request.text(confirmReason: confirmReason))
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.multilineTextAlignment(.leading)
.lineLimit(6)
.fixedSize(horizontal: false, vertical: true)
}

if let highlight = homoglyphHighlight {
VStack(alignment: .leading, spacing: 8) {
Text("Suspicious URL (non-ascii characters are red and underlined)")
.font(.caption)
.foregroundStyle(.secondary)
Text(highlight.attributedLine())
.font(.system(.body, design: .monospaced))
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.horizontal)
.padding(.bottom, 4)
.frame(maxWidth: .infinity, alignment: .leading)
}

TextEditor(text: .constant(contents))
.focusable(false)
.font(.system(.body, design: .monospaced))
.padding(.top, homoglyphHighlight != nil ? 2 : 0)

HStack {
Spacer()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,10 @@ class BaseTerminalController: NSWindowController,
guard let str = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStrKey] as? String else { return }
guard let state = notification.userInfo?[Ghostty.Notification.ConfirmClipboardStateKey] as? UnsafeMutableRawPointer? else { return }
guard let request = notification.userInfo?[Ghostty.Notification.ConfirmClipboardRequestKey] as? Ghostty.ClipboardRequest else { return }
let rawReason = notification.userInfo?[Ghostty.Notification.ConfirmClipboardConfirmReasonKey] as? Int
let confirmReason = Ghostty.ClipboardConfirmReason(rawValue: rawReason ?? 0) ?? .none
let homoglyphHighlight =
notification.userInfo?[Ghostty.Notification.ConfirmClipboardHomoglyphPayloadKey] as? Ghostty.PasteHomoglyphURLHighlight

// If we already have a clipboard confirmation view up, we ignore this request.
// This shouldn't be possible...
Expand All @@ -1099,6 +1103,8 @@ class BaseTerminalController: NSWindowController,
surface: surface,
contents: str,
request: request,
confirmReason: confirmReason,
homoglyphHighlight: homoglyphHighlight,
state: state,
delegate: self
)
Expand Down
Loading
Loading