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
2 changes: 1 addition & 1 deletion include/ghostty.h
Original file line number Diff line number Diff line change
Expand Up @@ -1124,7 +1124,7 @@ GHOSTTY_API bool ghostty_surface_key_is_binding(ghostty_surface_t,
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 void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t, int32_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,
Expand Down
12 changes: 10 additions & 2 deletions macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ extension Ghostty {
var notificationIdentifiers: Set<String> = []

private var markedText: NSMutableAttributedString
private var markedTextSelectedLocation: Int = 0
private(set) var focused: Bool = true
private var prevPressureStage: Int = 0
private var appearanceObserver: NSKeyValueObservation?
Expand Down Expand Up @@ -1835,9 +1836,11 @@ extension Ghostty.SurfaceView: NSTextInputClient {
switch string {
case let v as NSAttributedString:
self.markedText = NSMutableAttributedString(attributedString: v)
self.markedTextSelectedLocation = selectedRange.location

case let v as String:
self.markedText = NSMutableAttributedString(string: v)
self.markedTextSelectedLocation = selectedRange.location

default:
print("unknown marked text: \(string)")
Expand Down Expand Up @@ -2026,13 +2029,18 @@ extension Ghostty.SurfaceView: NSTextInputClient {
if len > 0 {
markedText.string.withCString { ptr in
// Subtract 1 for the null terminator
ghostty_surface_preedit(surface, ptr, UInt(len - 1))
ghostty_surface_preedit(
surface,
ptr,
UInt(len - 1),
Int32(markedTextSelectedLocation)
)
}
}
} else if clearIfNeeded {
// If we had marked text before but don't now, we're no longer
// in a preedit state so we can clear it.
ghostty_surface_preedit(surface, nil, 0)
ghostty_surface_preedit(surface, nil, 0, 0)
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2499,7 +2499,7 @@ fn balancePaddingIfNeeded(self: *Surface) void {
/// the preedit state correctly.
///
/// The preedit input must be UTF-8 encoded.
pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void {
pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8, cursor_pos: i32) !void {
// log.debug("text preeditCallback value={any}", .{preedit_});

// Crash metadata in case we crash in here
Expand Down Expand Up @@ -2566,6 +2566,7 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void {

self.renderer_state.preedit = .{
.codepoints = try codepoints.toOwnedSlice(self.alloc),
.cursor_pos = cursor_pos
};
try self.queueRender();
}
Expand Down
7 changes: 4 additions & 3 deletions src/apprt/embedded.zig
Original file line number Diff line number Diff line change
Expand Up @@ -889,8 +889,8 @@ pub const Surface = struct {
};
}

pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) void {
_ = self.core_surface.preeditCallback(preedit_) catch |err| {
pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8, cursor_pos: i32) void {
_ = self.core_surface.preeditCallback(preedit_, cursor_pos) catch |err| {
log.err("error in preedit callback err={}", .{err});
return;
};
Expand Down Expand Up @@ -1810,8 +1810,9 @@ pub const CAPI = struct {
surface: *Surface,
ptr: [*]const u8,
len: usize,
cursor_pos: i32
) void {
surface.preeditCallback(if (len == 0) null else ptr[0..len]);
surface.preeditCallback(if (len == 0) null else ptr[0..len], cursor_pos);
}

/// Returns true if the surface currently has mouse capturing
Expand Down
23 changes: 18 additions & 5 deletions src/apprt/gtk/class/surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ pub const Surface = extern struct {
im_composing: bool = false,
im_buf: [128]u8 = undefined,
im_len: u7 = 0,
im_show_cursor: bool = false,

/// True when we have a precision scroll in progress
precision_scroll: bool = false,
Expand Down Expand Up @@ -1420,7 +1421,7 @@ pub const Surface = extern struct {
// such as quotation mark ordering for Chinese input.
if (priv.im_composing) {
priv.im_context.as(gtk.IMContext).reset();
surface.preeditCallback(null) catch {};
surface.preeditCallback(null, 0) catch {};
}

// Bell stops ringing when any key is pressed that is used by
Expand Down Expand Up @@ -3038,6 +3039,7 @@ pub const Surface = extern struct {
const priv = self.private();
priv.im_composing = true;
priv.im_len = 0;
priv.im_show_cursor = false;
}

fn imPreeditChanged(
Expand All @@ -3061,17 +3063,28 @@ pub const Surface = extern struct {

// Get our pre-edit string that we'll use to show the user.
var buf: [*:0]u8 = undefined;
var cursor_pos: i32 = 0;
ctx.as(gtk.IMContext).getPreeditString(
&buf,
null,
null,
&cursor_pos,
);
defer glib.free(buf);
const str = std.mem.sliceTo(buf, 0);

// some IME may hard code the cursor as a bar,
// in which case, cursor_pos would always be 0
// if cursor_pos has never been non-zero,
// set it to -1 to hide the cursor to avoid conflict
if (cursor_pos > 0) {
priv.im_show_cursor = true;
} else if (!priv.im_show_cursor) {
cursor_pos = -1;
}

// Update our preedit state in Ghostty core
// log.warn("GTKIM: preedit change str={s}", .{str});
surface.preeditCallback(str) catch |err| {
surface.preeditCallback(str, cursor_pos) catch |err| {
log.warn(
"error in preedit callback err={}",
.{err},
Expand All @@ -3091,7 +3104,7 @@ pub const Surface = extern struct {

// End our preedit state in Ghostty core
const surface = priv.core_surface orelse return;
surface.preeditCallback(null) catch |err| {
surface.preeditCallback(null, 0) catch |err| {
log.warn("error in preedit callback err={}", .{err});
};
}
Expand Down Expand Up @@ -3160,7 +3173,7 @@ pub const Surface = extern struct {
if (priv.core_surface) |surface| {
// End our preedit state. Well-behaved input methods do this for us
// by triggering a preedit-end event but some do not (ibus 1.5.29).
surface.preeditCallback(null) catch |err| {
surface.preeditCallback(null, 0) catch |err| {
log.warn("error in preedit callback err={}", .{err});
};

Expand Down
17 changes: 17 additions & 0 deletions src/config/Config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,23 @@ palette: Palette = .{},
/// behavior around edge cases is possible.
@"cursor-click-to-move": bool = true,

/// The style of the cursor when editing the preedit text using
/// an IME (Input Method Editor).
///
/// All other cursor configs are applicable to IME cursor as well,
/// with the exception of `cursor-click-to-move` and `cursor-style-blink`
///
/// Note: Some IME hardcodes the cursor as a bar as a part of the preedit text.
/// To avoid showing two cursors, the native Ghostty cursor would be hidden.
///
/// Valid values are:
///
/// * `block`
/// * `bar`
/// * `underline`
/// * `block_hollow`
@"ime-cursor-style": terminal.CursorStyle = .bar,

/// Hide the mouse immediately when typing. The mouse becomes visible again
/// when the mouse is used (button, movement, etc.). Platform-specific behavior
/// may dictate other scenarios where the mouse is shown. For example on macOS,
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/State.zig
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub const Mouse = struct {
pub const Preedit = struct {
/// The codepoints to render as preedit text.
codepoints: []const Codepoint = &.{},
cursor_pos: i32,

/// A single codepoint to render as preedit text.
pub const Codepoint = struct {
Expand All @@ -62,6 +63,7 @@ pub const Preedit = struct {
pub fn clone(self: *const Preedit, alloc: Allocator) !Preedit {
return .{
.codepoints = try alloc.dupe(Codepoint, self.codepoints),
.cursor_pos = self.cursor_pos
};
}

Expand Down
Loading