Merge remote-tracking branch 'origin/ms_MY' into ms_MY

This commit is contained in:
Ruzaini Ourtech
2025-04-19 13:51:02 +08:00
7 changed files with 270 additions and 428 deletions

View File

@ -254,8 +254,10 @@ typedef enum {
typedef struct {
ghostty_input_action_e action;
ghostty_input_mods_e mods;
ghostty_input_mods_e consumed_mods;
uint32_t keycode;
const char* text;
uint32_t unshifted_codepoint;
bool composing;
} ghostty_input_key_s;
@ -725,6 +727,7 @@ ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t,
bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s);
bool ghostty_surface_key_is_binding(ghostty_surface_t, ghostty_input_key_s);
void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t);
void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t);
bool ghostty_surface_mouse_captured(ghostty_surface_t);
bool ghostty_surface_mouse_button(ghostty_surface_t,
ghostty_input_mouse_state_e,

View File

@ -3,13 +3,66 @@ import GhosttyKit
extension NSEvent {
/// Create a Ghostty key event for a given keyboard action.
func ghosttyKeyEvent(_ action: ghostty_input_action_e) -> ghostty_input_key_s {
var key_ev = ghostty_input_key_s()
///
/// This will not set the "text" or "composing" fields since these can't safely be set
/// with the information or lifetimes given.
///
/// The translationMods should be set to the modifiers used for actual character
/// translation if available.
func ghosttyKeyEvent(
_ action: ghostty_input_action_e,
translationMods: NSEvent.ModifierFlags? = nil
) -> ghostty_input_key_s {
var key_ev: ghostty_input_key_s = .init()
key_ev.action = action
key_ev.mods = Ghostty.ghosttyMods(modifierFlags)
key_ev.keycode = UInt32(keyCode)
// We can't infer or set these safely from this method. Since text is
// a cString, we can't use self.characters because of garbage collection.
// We have to let the caller handle this.
key_ev.text = nil
key_ev.composing = false
// macOS provides no easy way to determine the consumed modifiers for
// producing text. We apply a simple heuristic here that has worked for years
// so far: control and command never contribute to the translation of text,
// assume everything else did.
key_ev.mods = Ghostty.ghosttyMods(modifierFlags)
key_ev.consumed_mods = Ghostty.ghosttyMods(
(translationMods ?? modifierFlags)
.subtracting([.control, .command]))
// Our unshifted codepoint is the codepoint with no modifiers. We
// ignore multi-codepoint values.
key_ev.unshifted_codepoint = 0
if type == .keyDown || type == .keyUp {
if let charactersIgnoringModifiers,
let codepoint = charactersIgnoringModifiers.unicodeScalars.first
{
key_ev.unshifted_codepoint = codepoint.value
}
}
return key_ev
}
/// Returns the text to set for a key event for Ghostty.
///
/// This namely contains logic to avoid control characters, since we handle control character
/// mapping manually within Ghostty.
var ghosttyCharacters: String? {
// If we have no characters associated with this event we do nothing.
guard let characters else { return nil }
// If we have a single control character, then we return the characters
// without control pressed. We do this because we handle control character
// encoding directly within Ghostty's KeyEncoder.
if characters.count == 1,
let scalar = characters.unicodeScalars.first,
scalar.value < 0x20 {
return self.characters(byApplyingModifiers: modifierFlags.subtracting(.control))
}
return nil
}
}

View File

@ -951,29 +951,32 @@ extension Ghostty {
return
}
// If we have text, then we've composed a character, send that down. We do this
// first because if we completed a preedit, the text will be available here
// AND we'll have a preedit.
var handled: Bool = false
// If we have marked text, we're in a preedit state. The order we
// do this and the key event callbacks below doesn't matter since
// we control the preedit state only through the preedit API.
syncPreedit(clearIfNeeded: markedTextBefore)
if let list = keyTextAccumulator, list.count > 0 {
handled = true
// If we have text, then we've composed a character, send that down.
// These never have "composing" set to true because these are the
// result of a composition.
for text in list {
_ = keyAction(action, event: event, text: text)
_ = keyAction(
action,
event: event,
translationEvent: translationEvent,
text: text
)
}
}
// If we have marked text, we're in a preedit state. Send that down.
// If we don't have marked text but we had marked text before, then the preedit
// was cleared so we want to send down an empty string to ensure we've cleared
// the preedit.
if (markedText.length > 0 || markedTextBefore) {
handled = true
_ = keyAction(action, event: event, preedit: markedText.string)
}
if (!handled) {
// No text or anything, we want to handle this manually.
_ = keyAction(action, event: event)
} else {
// We have no accumulated text so this is a normal key event.
_ = keyAction(
action,
event: event,
translationEvent: translationEvent,
text: translationEvent.ghosttyCharacters,
composing: markedText.length > 0
)
}
}
@ -1043,16 +1046,6 @@ extension Ghostty {
let equivalent: String
switch (event.charactersIgnoringModifiers) {
case "/":
// Treat C-/ as C-_. We do this because C-/ makes macOS make a beep
// sound and we don't like the beep sound.
if (!event.modifierFlags.contains(.control) ||
!event.modifierFlags.isDisjoint(with: [.shift, .command, .option])) {
return false
}
equivalent = "_"
case "\r":
// Pass C-<return> through verbatim
// (prevent the default context menu equivalent)
@ -1165,34 +1158,23 @@ extension Ghostty {
_ = keyAction(action, event: event)
}
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) -> Bool {
guard let surface = self.surface else { return false }
return ghostty_surface_key(surface, event.ghosttyKeyEvent(action))
}
private func keyAction(
_ action: ghostty_input_action_e,
event: NSEvent, preedit: String
event: NSEvent,
translationEvent: NSEvent? = nil,
text: String? = nil,
composing: Bool = false
) -> Bool {
guard let surface = self.surface else { return false }
return preedit.withCString { ptr in
var key_ev = event.ghosttyKeyEvent(action)
key_ev.text = ptr
key_ev.composing = true
return ghostty_surface_key(surface, key_ev)
}
}
private func keyAction(
_ action: ghostty_input_action_e,
event: NSEvent, text: String
) -> Bool {
guard let surface = self.surface else { return false }
return text.withCString { ptr in
var key_ev = event.ghosttyKeyEvent(action)
key_ev.text = ptr
var key_ev = event.ghosttyKeyEvent(action, translationMods: translationEvent?.modifierFlags)
key_ev.composing = composing
if let text {
return text.withCString { ptr in
key_ev.text = ptr
return ghostty_surface_key(surface, key_ev)
}
} else {
return ghostty_surface_key(surface, key_ev)
}
}
@ -1468,10 +1450,21 @@ extension Ghostty.SurfaceView: NSTextInputClient {
default:
print("unknown marked text: \(string)")
}
// If we're not in a keyDown event, then we want to update our preedit
// text immediately. This can happen due to external events, for example
// changing keyboard layouts while composing: (1) set US intl (2) type '
// to enter dead key state (3)
if keyTextAccumulator == nil {
syncPreedit()
}
}
func unmarkText() {
self.markedText.mutableString.setString("")
if self.markedText.length > 0 {
self.markedText.mutableString.setString("")
syncPreedit()
}
}
func validAttributesForMarkedText() -> [NSAttributedString.Key] {
@ -1610,6 +1603,26 @@ extension Ghostty.SurfaceView: NSTextInputClient {
print("SEL: \(selector)")
}
/// Sync the preedit state based on the markedText value to libghostty
private func syncPreedit(clearIfNeeded: Bool = true) {
guard let surface else { return }
if markedText.length > 0 {
let str = markedText.string
let len = str.utf8CString.count
if len > 0 {
markedText.string.withCString { ptr in
// Subtract 1 for the null terminator
ghostty_surface_preedit(surface, ptr, UInt(len - 1))
}
}
} 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)
}
}
}
// MARK: Services

View File

@ -36,14 +36,14 @@ msgstr "确认"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5
msgid "Configuration Errors"
msgstr "置错误"
msgstr "置错误"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6
msgid ""
"One or more configuration errors were found. Please review the errors below, "
"and either reload your configuration or ignore these errors."
msgstr ""
"加载设置时发现了以下错误。请仔细阅读错误信息,并选择忽略或重新加载设置文件。"
"加载配置时发现了以下错误。请仔细阅读错误信息,并选择忽略或重新加载配置文件。"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9
msgid "Ignore"
@ -53,7 +53,7 @@ msgstr "忽略"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95
msgid "Reload Configuration"
msgstr "重新加载置"
msgstr "重新加载置"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
@ -69,7 +69,7 @@ msgstr "粘贴"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73
msgid "Clear"
msgstr "清除界面"
msgstr "清除屏幕"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78
@ -137,16 +137,16 @@ msgstr "关闭窗口"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89
msgid "Config"
msgstr "置"
msgstr "置"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Open Configuration"
msgstr "打开置文件"
msgstr "打开置文件"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Terminal Inspector"
msgstr "终端检视器"
msgstr "终端调试器"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102
#: src/apprt/gtk/Window.zig:960
@ -160,13 +160,13 @@ msgstr "退出"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6
msgid "Authorize Clipboard Access"
msgstr "剪板访问授权"
msgstr "剪板访问授权"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7
msgid ""
"An application is attempting to read from the clipboard. The current "
"clipboard contents are shown below."
msgstr "一个应用正在试图从剪切板读取内容。剪切板目前的内容如下:"
msgstr "一个应用正在试图从剪贴板读取内容。剪贴板目前的内容如下:"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10
@ -182,7 +182,7 @@ msgstr "允许"
msgid ""
"An application is attempting to write to the clipboard. The current "
"clipboard contents are shown below."
msgstr "一个应用正在试图向剪切板写入内容。剪切板目前的内容如下:"
msgstr "一个应用正在试图向剪贴板写入内容。剪贴板目前的内容如下:"
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6
msgid "Warning: Potentially Unsafe Paste"
@ -196,11 +196,11 @@ msgstr "将以下内容粘贴至终端内将可能执行有害命令。"
#: src/apprt/gtk/inspector.zig:144
msgid "Ghostty: Terminal Inspector"
msgstr "Ghostty 终端检视器"
msgstr "Ghostty 终端调试器"
#: src/apprt/gtk/Surface.zig:1243
msgid "Copied to clipboard"
msgstr "已复制至剪板"
msgstr "已复制至剪板"
#: src/apprt/gtk/CloseDialog.zig:47
msgid "Close"
@ -253,7 +253,7 @@ msgstr "⚠️ Ghostty 正在以调试模式运行!性能将大打折扣。"
#: src/apprt/gtk/Window.zig:725
msgid "Reloaded the configuration"
msgstr "已重新加载置"
msgstr "已重新加载置"
#: src/apprt/gtk/Window.zig:941
msgid "Ghostty Developers"

View File

@ -73,16 +73,68 @@ pub const App = struct {
/// This is the key event sent for ghostty_surface_key and
/// ghostty_app_key.
pub const KeyEvent = struct {
/// The three below are absolutely required.
action: input.Action,
mods: input.Mods,
consumed_mods: input.Mods,
keycode: u32,
/// Optionally, the embedder can handle text translation and send
/// the text value here. If text is non-nil, it is assumed that the
/// embedder also handles dead key states and sets composing as necessary.
text: ?[:0]const u8,
unshifted_codepoint: u32,
composing: bool,
/// Convert a libghostty key event into a core key event.
fn core(self: KeyEvent) ?input.KeyEvent {
const text: []const u8 = if (self.text) |v| v else "";
const unshifted_codepoint: u21 = std.math.cast(
u21,
self.unshifted_codepoint,
) orelse 0;
// We want to get the physical unmapped key to process keybinds.
const physical_key = keycode: for (input.keycodes.entries) |entry| {
if (entry.native == self.keycode) break :keycode entry.key;
} else .invalid;
// If the resulting text has length 1 then we can take its key
// and attempt to translate it to a key enum and call the key callback.
// If the length is greater than 1 then we're going to call the
// charCallback.
//
// We also only do key translation if this is not a dead key.
const key = if (!self.composing) key: {
// If our physical key is a keypad key, we use that.
if (physical_key.keypad()) break :key physical_key;
// A completed key. If the length of the key is one then we can
// attempt to translate it to a key enum and call the key
// callback. First try plain ASCII.
if (text.len > 0) {
if (input.Key.fromASCII(text[0])) |key| {
break :key key;
}
}
// If the above doesn't work, we use the unmodified value.
if (std.math.cast(u8, unshifted_codepoint)) |ascii| {
if (input.Key.fromASCII(ascii)) |key| {
break :key key;
}
}
break :key physical_key;
} else .invalid;
// Build our final key event
return .{
.action = self.action,
.key = key,
.physical_key = physical_key,
.mods = self.mods,
.consumed_mods = self.consumed_mods,
.composing = self.composing,
.utf8 = text,
.unshifted_codepoint = unshifted_codepoint,
};
}
};
core_app: *CoreApp,
@ -92,10 +144,6 @@ pub const App = struct {
/// The configuration for the app. This is owned by this structure.
config: Config,
/// The keymap state is used for global keybinds only. Each surface
/// also has its own keymap state for focused keybinds.
keymap_state: input.Keymap.State,
pub fn init(
core_app: *CoreApp,
config: *const Config,
@ -114,7 +162,6 @@ pub const App = struct {
.config = config_clone,
.opts = opts,
.keymap = keymap,
.keymap_state = .{},
};
}
@ -148,219 +195,6 @@ pub const App = struct {
self.core_app.focusEvent(focused);
}
/// Convert a C key event into a Zig key event.
///
/// The buffer is needed for possibly storing translated UTF-8 text.
/// This buffer may (or may not) be referenced by the resulting KeyEvent
/// so it should be valid for the lifetime of the KeyEvent.
///
/// The size of the buffer doesn't need to be large, we always
/// used to hardcode 128 bytes and never ran into issues. If it isn't
/// large enough an error will be returned.
fn coreKeyEvent(
self: *App,
buf: []u8,
target: KeyTarget,
event: KeyEvent,
) !?input.KeyEvent {
const action = event.action;
const keycode = event.keycode;
const mods = event.mods;
// True if this is a key down event
const is_down = action == .press or action == .repeat;
// If we're on macOS and we have macos-option-as-alt enabled,
// then we strip the alt modifier from the mods for translation.
const translate_mods = translate_mods: {
var translate_mods = mods;
if ((comptime builtin.target.os.tag.isDarwin()) and translate_mods.alt) {
// Note: the keyboardLayout() function is not super cheap
// so we only want to run it if alt is already pressed hence
// the above condition.
const option_as_alt: configpkg.OptionAsAlt =
self.config.@"macos-option-as-alt" orelse
self.keyboardLayout().detectOptionAsAlt();
const strip = switch (option_as_alt) {
.false => false,
.true => mods.alt,
.left => mods.sides.alt == .left,
.right => mods.sides.alt == .right,
};
if (strip) translate_mods.alt = false;
}
// We strip super on macOS because its not used for translation
// it results in a bad translation.
if (comptime builtin.target.os.tag.isDarwin()) {
translate_mods.super = false;
}
break :translate_mods translate_mods;
};
const event_text: ?[]const u8 = event_text: {
// This logic only applies to macOS.
if (comptime builtin.os.tag != .macos) break :event_text event.text;
// If we're in a preedit state then we allow it through. This
// allows ctrl sequences that affect IME to work. For example,
// Ctrl+H deletes a character with Japanese input.
if (event.composing) break :event_text event.text;
// If the modifiers are ONLY "control" then we never process
// the event text because we want to do our own translation so
// we can handle ctrl+c, ctrl+z, etc.
//
// This is specifically because on macOS using the
// "Dvorak - QWERTY ⌘" keyboard layout, ctrl+z is translated as
// "/" (the physical key that is z on a qwerty keyboard). But on
// other layouts, ctrl+<char> is not translated by AppKit. So,
// we just avoid this by never allowing AppKit to translate
// ctrl+<char> and instead do it ourselves.
const ctrl_only = comptime (input.Mods{ .ctrl = true }).int();
break :event_text if (mods.binding().int() == ctrl_only) null else event.text;
};
// Translate our key using the keymap for our localized keyboard layout.
// We only translate for keydown events. Otherwise, we only care about
// the raw keycode.
const result: input.Keymap.Translation = if (is_down) translate: {
// If the event provided us with text, then we use this as a result
// and do not do manual translation.
const result: input.Keymap.Translation = if (event_text) |text| .{
.text = text,
.composing = event.composing,
.mods = translate_mods,
} else try self.keymap.translate(
buf,
switch (target) {
.app => &self.keymap_state,
.surface => |surface| &surface.keymap_state,
},
@intCast(keycode),
translate_mods,
);
// TODO(mitchellh): I think we can get rid of the above keymap
// translation code completely and defer to AppKit/Swift
// (for macOS) for handling all translations. The translation
// within libghostty is an artifact of an earlier design and
// it is buggy (see #5558). We should move closer to a GTK-style
// model of tracking composing states and preedit in the apprt
// and not in libghostty.
// If this is a dead key, then we're composing a character and
// we need to set our proper preedit state if we're targeting a
// surface.
if (result.composing) {
switch (target) {
.app => {},
.surface => |surface| surface.core_surface.preeditCallback(
result.text,
) catch |err| {
log.err("error in preedit callback err={}", .{err});
return null;
},
}
} else {
switch (target) {
.app => {},
.surface => |surface| surface.core_surface.preeditCallback(null) catch |err| {
log.err("error in preedit callback err={}", .{err});
return null;
},
}
// If the text is just a single non-printable ASCII character
// then we clear the text. We handle non-printables in the
// key encoder manual (such as tab, ctrl+c, etc.)
if (result.text.len == 1 and result.text[0] < 0x20) {
break :translate .{};
}
}
break :translate result;
} else .{};
// We need to always do a translation with no modifiers at all in
// order to get the "unshifted_codepoint" for the key event.
const unshifted_codepoint: u21 = unshifted: {
var nomod_buf: [128]u8 = undefined;
var nomod_state: input.Keymap.State = .{};
const nomod = try self.keymap.translate(
&nomod_buf,
&nomod_state,
@intCast(keycode),
.{},
);
const view = std.unicode.Utf8View.init(nomod.text) catch |err| {
log.warn("cannot build utf8 view over text: {}", .{err});
break :unshifted 0;
};
var it = view.iterator();
break :unshifted it.nextCodepoint() orelse 0;
};
// log.warn("TRANSLATE: action={} keycode={x} dead={} key_len={} key={any} key_str={s} mods={}", .{
// action,
// keycode,
// result.composing,
// result.text.len,
// result.text,
// result.text,
// mods,
// });
// We want to get the physical unmapped key to process keybinds.
const physical_key = keycode: for (input.keycodes.entries) |entry| {
if (entry.native == keycode) break :keycode entry.key;
} else .invalid;
// If the resulting text has length 1 then we can take its key
// and attempt to translate it to a key enum and call the key callback.
// If the length is greater than 1 then we're going to call the
// charCallback.
//
// We also only do key translation if this is not a dead key.
const key = if (!result.composing) key: {
// If our physical key is a keypad key, we use that.
if (physical_key.keypad()) break :key physical_key;
// A completed key. If the length of the key is one then we can
// attempt to translate it to a key enum and call the key
// callback. First try plain ASCII.
if (result.text.len > 0) {
if (input.Key.fromASCII(result.text[0])) |key| {
break :key key;
}
}
// If the above doesn't work, we use the unmodified value.
if (std.math.cast(u8, unshifted_codepoint)) |ascii| {
if (input.Key.fromASCII(ascii)) |key| {
break :key key;
}
}
break :key physical_key;
} else .invalid;
// Build our final key event
return .{
.action = action,
.key = key,
.physical_key = physical_key,
.mods = mods,
.consumed_mods = result.mods,
.composing = result.composing,
.utf8 = result.text,
.unshifted_codepoint = unshifted_codepoint,
};
}
/// See CoreApp.keyEvent.
pub fn keyEvent(
self: *App,
@ -368,12 +202,8 @@ pub const App = struct {
event: KeyEvent,
) !bool {
// Convert our C key event into a Zig one.
var buf: [128]u8 = undefined;
const input_event: input.KeyEvent = (try self.coreKeyEvent(
&buf,
target,
event,
)) orelse return false;
const input_event: input.KeyEvent = event.core() orelse
return false;
// Invoke the core Ghostty logic to handle this input.
const effect: CoreSurface.InputEffect = switch (target) {
@ -390,23 +220,7 @@ pub const App = struct {
return switch (effect) {
.closed => true,
.ignored => false,
.consumed => consumed: {
const is_down = input_event.action == .press or
input_event.action == .repeat;
if (is_down) {
// If we consume the key then we want to reset the dead
// key state.
self.keymap_state = .{};
switch (target) {
.app => {},
.surface => |surface| surface.core_surface.preeditCallback(null) catch {},
}
}
break :consumed true;
},
.consumed => true,
};
}
@ -414,13 +228,6 @@ pub const App = struct {
pub fn reloadKeymap(self: *App) !void {
// Reload the keymap
try self.keymap.reload();
// Clear the dead key state since we changed the keymap, any
// dead key state is just forgotten. i.e. if you type ' on us-intl
// and then switch to us and type a, you'll get a rather than á.
for (self.core_app.surfaces.items) |surface| {
surface.keymap_state = .{};
}
}
/// Loads the keyboard layout.
@ -607,7 +414,6 @@ pub const Surface = struct {
content_scale: apprt.ContentScale,
size: apprt.SurfaceSize,
cursor_pos: apprt.CursorPos,
keymap_state: input.Keymap.State,
inspector: ?*Inspector = null,
/// The current title of the surface. The embedded apprt saves this so
@ -656,7 +462,6 @@ pub const Surface = struct {
},
.size = .{ .width = 800, .height = 600 },
.cursor_pos = .{ .x = -1, .y = -1 },
.keymap_state = .{},
};
// Add ourselves to the list of surfaces on the app.
@ -992,6 +797,13 @@ pub const Surface = struct {
};
}
pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) void {
_ = self.core_surface.preeditCallback(preedit_) catch |err| {
log.err("error in preedit callback err={}", .{err});
return;
};
}
pub fn textCallback(self: *Surface, text: []const u8) void {
_ = self.core_surface.textCallback(text) catch |err| {
log.err("error in key callback err={}", .{err});
@ -1082,7 +894,6 @@ pub const Inspector = struct {
surface: *Surface,
ig_ctx: *cimgui.c.ImGuiContext,
backend: ?Backend = null,
keymap_state: input.Keymap.State = .{},
content_scale: f64 = 1,
/// Our previous instant used to calculate delta time for animations.
@ -1328,11 +1139,13 @@ pub const CAPI = struct {
const KeyEvent = extern struct {
action: input.Action,
mods: c_int,
consumed_mods: c_int,
keycode: u32,
text: ?[*:0]const u8,
unshifted_codepoint: u32,
composing: bool,
/// Convert to surface key event.
/// Convert to Zig key event.
fn keyEvent(self: KeyEvent) App.KeyEvent {
return .{
.action = self.action,
@ -1340,8 +1153,13 @@ pub const CAPI = struct {
input.Mods.Backing,
@truncate(@as(c_uint, @bitCast(self.mods))),
)),
.consumed_mods = @bitCast(@as(
input.Mods.Backing,
@truncate(@as(c_uint, @bitCast(self.consumed_mods))),
)),
.keycode = self.keycode,
.text = if (self.text) |ptr| std.mem.sliceTo(ptr, 0) else null,
.unshifted_codepoint = self.unshifted_codepoint,
.composing = self.composing,
};
}
@ -1447,15 +1265,7 @@ pub const CAPI = struct {
app: *App,
event: KeyEvent,
) bool {
var buf: [128]u8 = undefined;
const core_event = app.coreKeyEvent(
&buf,
.app,
event.keyEvent(),
) catch |err| {
log.warn("error processing key event err={}", .{err});
return false;
} orelse {
const core_event = event.keyEvent().core() orelse {
log.warn("error processing key event", .{});
return false;
};
@ -1701,20 +1511,7 @@ pub const CAPI = struct {
surface: *Surface,
event: KeyEvent,
) bool {
var buf: [128]u8 = undefined;
const core_event = surface.app.coreKeyEvent(
&buf,
// Note: this "app" target here looks like a bug, but it is
// intentional. coreKeyEvent uses the target only as a way to
// trigger preedit callbacks for keymap translation and we don't
// want to trigger that here. See the todo item in coreKeyEvent
// for a long term solution to this and removing target altogether.
.app,
event.keyEvent(),
) catch |err| {
log.warn("error processing key event err={}", .{err});
return false;
} orelse {
const core_event = event.keyEvent().core() orelse {
log.warn("error processing key event", .{});
return false;
};
@ -1733,6 +1530,16 @@ pub const CAPI = struct {
surface.textCallback(ptr[0..len]);
}
/// Set the preedit text for the surface. This is used for IME
/// composition. If the length is 0, then the preedit text is cleared.
export fn ghostty_surface_preedit(
surface: *Surface,
ptr: [*]const u8,
len: usize,
) void {
surface.preeditCallback(if (len == 0) null else ptr[0..len]);
}
/// Returns true if the surface currently has mouse capturing
/// enabled.
export fn ghostty_surface_mouse_captured(surface: *Surface) bool {

View File

@ -811,16 +811,19 @@ fn gtkWindowUpdateScaleFactor(
};
}
// Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab
// sends an undefined value.
fn gtkTabNewClick(_: *gtk.Button, self: *Window) callconv(.c) void {
/// Perform a binding action on the window's action surface.
fn performBindingAction(self: *Window, action: input.Binding.Action) void {
const surface = self.actionSurface() orelse return;
_ = surface.performBindingAction(.{ .new_tab = {} }) catch |err| {
_ = surface.performBindingAction(action) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
}
fn gtkTabNewClick(_: *gtk.Button, self: *Window) callconv(.c) void {
self.performBindingAction(.{ .new_tab = {} });
}
/// Create a new tab from the AdwTabOverview. We can't copy gtkTabNewClick
/// because we need to return an AdwTabPage from this function.
fn gtkNewTabFromOverview(_: *adw.TabOverview, self: *Window) callconv(.c) *adw.TabPage {
@ -1007,11 +1010,7 @@ fn gtkActionNewWindow(
_: ?*glib.Variant,
self: *Window,
) callconv(.C) void {
const surface = self.actionSurface() orelse return;
_ = surface.performBindingAction(.{ .new_window = {} }) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
self.performBindingAction(.{ .new_window = {} });
}
fn gtkActionNewTab(
@ -1019,8 +1018,7 @@ fn gtkActionNewTab(
_: ?*glib.Variant,
self: *Window,
) callconv(.C) void {
// We can use undefined because the button is not used.
gtkTabNewClick(undefined, self);
self.performBindingAction(.{ .new_tab = {} });
}
fn gtkActionCloseTab(
@ -1028,11 +1026,7 @@ fn gtkActionCloseTab(
_: ?*glib.Variant,
self: *Window,
) callconv(.C) void {
const surface = self.actionSurface() orelse return;
_ = surface.performBindingAction(.{ .close_tab = {} }) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
self.performBindingAction(.{ .close_tab = {} });
}
fn gtkActionSplitRight(
@ -1040,11 +1034,7 @@ fn gtkActionSplitRight(
_: ?*glib.Variant,
self: *Window,
) callconv(.C) void {
const surface = self.actionSurface() orelse return;
_ = surface.performBindingAction(.{ .new_split = .right }) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
self.performBindingAction(.{ .new_split = .right });
}
fn gtkActionSplitDown(
@ -1052,11 +1042,7 @@ fn gtkActionSplitDown(
_: ?*glib.Variant,
self: *Window,
) callconv(.C) void {
const surface = self.actionSurface() orelse return;
_ = surface.performBindingAction(.{ .new_split = .down }) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
self.performBindingAction(.{ .new_split = .down });
}
fn gtkActionSplitLeft(
@ -1064,11 +1050,7 @@ fn gtkActionSplitLeft(
_: ?*glib.Variant,
self: *Window,
) callconv(.C) void {
const surface = self.actionSurface() orelse return;
_ = surface.performBindingAction(.{ .new_split = .left }) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
self.performBindingAction(.{ .new_split = .left });
}
fn gtkActionSplitUp(
@ -1076,11 +1058,7 @@ fn gtkActionSplitUp(
_: ?*glib.Variant,
self: *Window,
) callconv(.C) void {
const surface = self.actionSurface() orelse return;
_ = surface.performBindingAction(.{ .new_split = .up }) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
self.performBindingAction(.{ .new_split = .right });
}
fn gtkActionToggleInspector(
@ -1088,11 +1066,7 @@ fn gtkActionToggleInspector(
_: ?*glib.Variant,
self: *Window,
) callconv(.C) void {
const surface = self.actionSurface() orelse return;
_ = surface.performBindingAction(.{ .inspector = .toggle }) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
self.performBindingAction(.{ .inspector = .toggle });
}
fn gtkActionCopy(
@ -1100,11 +1074,7 @@ fn gtkActionCopy(
_: ?*glib.Variant,
self: *Window,
) callconv(.C) void {
const surface = self.actionSurface() orelse return;
_ = surface.performBindingAction(.{ .copy_to_clipboard = {} }) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
self.performBindingAction(.{ .copy_to_clipboard = {} });
}
fn gtkActionPaste(
@ -1112,11 +1082,7 @@ fn gtkActionPaste(
_: ?*glib.Variant,
self: *Window,
) callconv(.C) void {
const surface = self.actionSurface() orelse return;
_ = surface.performBindingAction(.{ .paste_from_clipboard = {} }) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
self.performBindingAction(.{ .paste_from_clipboard = {} });
}
fn gtkActionReset(
@ -1124,11 +1090,7 @@ fn gtkActionReset(
_: ?*glib.Variant,
self: *Window,
) callconv(.C) void {
const surface = self.actionSurface() orelse return;
_ = surface.performBindingAction(.{ .reset = {} }) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
self.performBindingAction(.{ .reset = {} });
}
fn gtkActionClear(
@ -1136,11 +1098,7 @@ fn gtkActionClear(
_: ?*glib.Variant,
self: *Window,
) callconv(.C) void {
const surface = self.actionSurface() orelse return;
_ = surface.performBindingAction(.{ .clear_screen = {} }) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
self.performBindingAction(.{ .clear_screen = {} });
}
fn gtkActionPromptTitle(
@ -1148,11 +1106,7 @@ fn gtkActionPromptTitle(
_: ?*glib.Variant,
self: *Window,
) callconv(.C) void {
const surface = self.actionSurface() orelse return;
_ = surface.performBindingAction(.{ .prompt_surface_title = {} }) catch |err| {
log.warn("error performing binding action error={}", .{err});
return;
};
self.performBindingAction(.{ .prompt_surface_title = {} });
}
/// Returns the surface to use for an action.

View File

@ -150,21 +150,25 @@ pub const Mods = packed struct(Mods.Backing) {
/// like macos-option-as-alt. The translation mods should be used for
/// translation but never sent back in for the key callback.
pub fn translation(self: Mods, option_as_alt: config.OptionAsAlt) Mods {
// We currently only process macos-option-as-alt so other
// platforms don't need to do anything.
if (comptime !builtin.target.os.tag.isDarwin()) return self;
var result = self;
// Alt has to be set only on the correct side
switch (option_as_alt) {
.false => return self,
.true => {},
.left => if (self.sides.alt == .right) return self,
.right => if (self.sides.alt == .left) return self,
// Control is never used for translation.
result.ctrl = false;
// macos-option-as-alt for darwin
if (comptime builtin.target.os.tag.isDarwin()) alt: {
// Alt has to be set only on the correct side
switch (option_as_alt) {
.false => break :alt,
.true => {},
.left => if (self.sides.alt == .right) break :alt,
.right => if (self.sides.alt == .left) break :alt,
}
// Unset alt
result.alt = false;
}
// Unset alt
var result = self;
result.alt = false;
return result;
}
@ -186,6 +190,14 @@ pub const Mods = packed struct(Mods.Backing) {
);
}
test "translation removes control" {
const testing = std.testing;
const mods: Mods = .{ .ctrl = true };
const result = mods.translation(.true);
try testing.expectEqual(Mods{}, result);
}
test "translation macos-option-as-alt" {
if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest;