Merge branch 'main' into vi_VN

This commit is contained in:
Anh Thang
2026-03-05 08:56:57 +07:00
committed by GitHub
28 changed files with 1036 additions and 232 deletions

View File

@@ -177,6 +177,7 @@
/po/id.po @ghostty-org/id_ID
/po/it.po @ghostty-org/it_IT
/po/ja.po @ghostty-org/ja_JP
/po/kk.po @ghostty-org/kk_KZ
/po/ko_KR.po @ghostty-org/ko_KR
/po/lt.po @ghostty-org/lt_LT
/po/lv.po @ghostty-org/lv_LV

View File

@@ -38,15 +38,6 @@ here:
| `zig build dist` | Builds a source tarball |
| `zig build distcheck` | Builds and validates a source tarball |
## FontConfig and GTK
Because of the global shared state that FontConfig maintains, FontConfig must
be linked dynamically to the same system FontConfig shared library that GTK
uses. Ghostty's default has been changed to always link to the system FontConfig
library. If that is overridden (by specifying `-fno-sys=fontconfig` during the
build) Ghostty may crash when trying to locate glyphs that are not available in
the default font.
## Extra Dependencies
Building Ghostty from a Git checkout on Linux requires some additional

View File

@@ -463,6 +463,12 @@ typedef struct {
// Config types
// config.Path
typedef struct {
const char* path;
bool optional;
} ghostty_config_path_s;
// config.Color
typedef struct {
uint8_t r;

View File

@@ -777,6 +777,14 @@ class AppDelegate: NSObject,
NSSound.beep()
}
if ghostty.config.bellFeatures.contains(.audio) {
if let configPath = ghostty.config.bellAudioPath,
let sound = NSSound(contentsOfFile: configPath.path, byReference: false) {
sound.volume = ghostty.config.bellAudioVolume
sound.play()
}
}
if ghostty.config.bellFeatures.contains(.attention) {
// Bounce the dock icon if we're not focused.
NSApp.requestUserAttention(.informationalRequest)

View File

@@ -1239,9 +1239,11 @@ class BaseTerminalController: NSWindowController,
}
}
// Becoming/losing key means we have to notify our surface(s) that we have focus
// so things like cursors blink, pty events are sent, etc.
self.syncFocusToSurfaceTree()
// Becoming key can race with responder updates when activating a window.
// Sync on the next runloop so split focus has settled first.
DispatchQueue.main.async {
self.syncFocusToSurfaceTree()
}
}
func windowDidResignKey(_ notification: Notification) {

View File

@@ -134,6 +134,23 @@ extension Ghostty {
return .init(rawValue: v)
}
var bellAudioPath: ConfigPath? {
guard let config = self.config else { return nil }
var v = ghostty_config_path_s()
let key = "bell-audio-path"
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil }
let path = String(cString: v.path)
return path.isEmpty ? nil : ConfigPath(path: path, optional: v.optional)
}
var bellAudioVolume: Float {
guard let config = self.config else { return 0.5 }
var v: Double = 0.5
let key = "bell-audio-volume"
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return Float(v)
}
var notifyOnCommandFinish: NotifyOnCommandFinish {
guard let config = self.config else { return .never }
var v: UnsafePointer<Int8>?

View File

@@ -2,6 +2,12 @@
// can get typed information without depending on all the dependencies of GhosttyKit.
extension Ghostty {
/// A configuration path value that may be optional or required.
struct ConfigPath: Sendable {
let path: String
let optional: Bool
}
/// macos-icon
enum MacOSIcon: String, Sendable {
case official

View File

@@ -221,6 +221,10 @@ extension Ghostty {
// This is set to non-null during keyDown to accumulate insertText contents
private var keyTextAccumulator: [String]?
// True when we've consumed a left mouse-down only to move focus and
// should suppress the matching mouse-up from being reported.
private var suppressNextLeftMouseUp: Bool = false
// A small delay that is introduced before a title change to avoid flickers
private var titleChangeTimer: Timer?
@@ -644,12 +648,18 @@ extension Ghostty {
let location = convert(event.locationInWindow, from: nil)
guard hitTest(location) == self else { return event }
// We only want to grab focus if either our app or window was
// not focused.
guard !NSApp.isActive || !window.isKeyWindow else { return event }
// If we're already the first responder then no focus transfer is
// happening, so the click should continue as normal.
guard window.firstResponder !== self else { return event }
// If we're already focused we do nothing
guard !focused else { return event }
// If our window/app is already focused, then this click is only
// being used to transfer split focus. Consume it so it does not
// get forwarded to the terminal as a mouse click.
if NSApp.isActive && window.isKeyWindow {
window.makeFirstResponder(self)
suppressNextLeftMouseUp = true
return nil
}
// Make ourselves the first responder
window.makeFirstResponder(self)
@@ -854,6 +864,13 @@ extension Ghostty {
}
override func mouseUp(with event: NSEvent) {
// If this mouse-up corresponds to a focus-only click transfer,
// suppress it so we don't emit a release without a press.
if suppressNextLeftMouseUp {
suppressNextLeftMouseUp = false
return
}
// Always reset our pressure when the mouse goes up
prevPressureStage = 0

View File

@@ -2,6 +2,34 @@ import Testing
@testable import Ghostty
struct ShellTests {
@Test(arguments: [
("hello", "hello"),
("", ""),
("file name", "file\\ name"),
("a\\b", "a\\\\b"),
("(foo)", "\\(foo\\)"),
("[bar]", "\\[bar\\]"),
("{baz}", "\\{baz\\}"),
("<qux>", "\\<qux\\>"),
("say\"hi\"", "say\\\"hi\\\""),
("it's", "it\\'s"),
("`cmd`", "\\`cmd\\`"),
("wow!", "wow\\!"),
("#comment", "\\#comment"),
("$HOME", "\\$HOME"),
("a&b", "a\\&b"),
("a;b", "a\\;b"),
("a|b", "a\\|b"),
("*.txt", "\\*.txt"),
("file?.log", "file\\?.log"),
("col1\tcol2", "col1\\\tcol2"),
("$(echo 'hi')", "\\$\\(echo\\ \\'hi\\'\\)"),
("/tmp/my file (1).txt", "/tmp/my\\ file\\ \\(1\\).txt"),
])
func escape(input: String, expected: String) {
#expect(Ghostty.Shell.escape(input) == expected)
}
@Test(arguments: [
("", "''"),
("filename", "filename"),

354
po/kk.po Normal file
View File

@@ -0,0 +1,354 @@
# Kazakh translation for Ghostty.
# Copyright (C) 2026 "Mitchell Hashimoto, Ghostty contributors"
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Baurzhan Muftakhidinov <baurthefirst@gmail.com>, 2026.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2026-02-17 23:16+0100\n"
"PO-Revision-Date: 2026-03-04 21:16+0500\n"
"Last-Translator: Baurzhan Muftakhidinov <baurthefirst@gmail.com>\n"
"Language-Team: Kazakh <kk_KZ@googlegroups.com>\n"
"Language: kk\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: dist/linux/ghostty_nautilus.py:53
msgid "Open in Ghostty"
msgstr "Ghostty ішінде ашу"
#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12
#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12
#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:197
#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:201
msgid "Authorize Clipboard Access"
msgstr "Алмасу буферіне қол жеткізуді рұқсат ету"
#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:17
#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:17
msgid "Deny"
msgstr "Тыйым салу"
#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:18
#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:18
msgid "Allow"
msgstr "Рұқсат ету"
#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:92
msgid "Remember choice for this split"
msgstr "Осы бөлу үшін таңдауды есте сақтау"
#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:93
msgid "Reload configuration to show this prompt again"
msgstr "Бұл сұрауды қайта көрсету үшін конфигурацияны қайта жүктеңіз"
#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:7
#: src/apprt/gtk/ui/1.5/title-dialog.blp:8
msgid "Cancel"
msgstr "Бас тарту"
#: src/apprt/gtk/ui/1.2/close-confirmation-dialog.blp:8
#: src/apprt/gtk/ui/1.2/search-overlay.blp:85
#: src/apprt/gtk/ui/1.3/surface-child-exited.blp:17
msgid "Close"
msgstr "Жабу"
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6
msgid "Configuration Errors"
msgstr "Конфигурация қателері"
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:7
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.2/config-errors-dialog.blp:10
msgid "Ignore"
msgstr "Елемеу"
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11
#: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300
msgid "Reload Configuration"
msgstr "Конфигурацияны қайта жүктеу"
#: src/apprt/gtk/ui/1.2/debug-warning.blp:7
#: src/apprt/gtk/ui/1.3/debug-warning.blp:6
msgid "⚠️ You're running a debug build of Ghostty! Performance will be degraded."
msgstr ""
"⚠️ Сіз Ghostty жөндеу құрастырылымын іске қосып тұрсыз! Өнімділік төмен болуы "
"мүмкін."
#: src/apprt/gtk/ui/1.5/inspector-window.blp:5
msgid "Ghostty: Terminal Inspector"
msgstr "Ghostty: Терминал инспекторы"
#: src/apprt/gtk/ui/1.2/search-overlay.blp:29
msgid "Find…"
msgstr "Табу…"
#: src/apprt/gtk/ui/1.2/search-overlay.blp:64
msgid "Previous Match"
msgstr "Алдыңғы сәйкестік"
#: src/apprt/gtk/ui/1.2/search-overlay.blp:74
msgid "Next Match"
msgstr "Келесі сәйкестік"
#: src/apprt/gtk/ui/1.2/surface.blp:6
msgid "Oh, no."
msgstr "О, жоқ."
#: src/apprt/gtk/ui/1.2/surface.blp:7
msgid "Unable to acquire an OpenGL context for rendering."
msgstr "Рендеринг үшін OpenGL контекстін алу мүмкін емес."
#: src/apprt/gtk/ui/1.2/surface.blp:97
msgid ""
"This terminal is in read-only mode. You can still view, select, and scroll "
"through the content, but no input events will be sent to the running "
"application."
msgstr ""
"Бұл терминал тек оқу режимінде. Сіз әлі де мазмұнды көре, таңдай және жылжыта "
"аласыз, бірақ іске қосылған қолданбаға ешқандай енгізу оқиғалары жіберілмейді."
#: src/apprt/gtk/ui/1.2/surface.blp:107
msgid "Read-only"
msgstr "Тек оқу"
#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200
msgid "Copy"
msgstr "Көшіріп алу"
#: src/apprt/gtk/ui/1.2/surface.blp:265 src/apprt/gtk/ui/1.5/window.blp:205
msgid "Paste"
msgstr "Кірістіру"
#: src/apprt/gtk/ui/1.2/surface.blp:270
msgid "Notify on Next Command Finish"
msgstr "Келесі команда аяқталғанда хабарлау"
#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273
msgid "Clear"
msgstr "Тазарту"
#: src/apprt/gtk/ui/1.2/surface.blp:282 src/apprt/gtk/ui/1.5/window.blp:278
msgid "Reset"
msgstr "Тастау"
#: src/apprt/gtk/ui/1.2/surface.blp:289 src/apprt/gtk/ui/1.5/window.blp:242
msgid "Split"
msgstr "Бөлу"
#: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245
msgid "Change Title…"
msgstr "Тақырыпты өзгерту…"
#: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177
#: src/apprt/gtk/ui/1.5/window.blp:250
msgid "Split Up"
msgstr "Жоғарыға бөлу"
#: src/apprt/gtk/ui/1.2/surface.blp:303 src/apprt/gtk/ui/1.5/window.blp:182
#: src/apprt/gtk/ui/1.5/window.blp:255
msgid "Split Down"
msgstr "Төменге бөлу"
#: src/apprt/gtk/ui/1.2/surface.blp:309 src/apprt/gtk/ui/1.5/window.blp:187
#: src/apprt/gtk/ui/1.5/window.blp:260
msgid "Split Left"
msgstr "Сол жаққа бөлу"
#: src/apprt/gtk/ui/1.2/surface.blp:315 src/apprt/gtk/ui/1.5/window.blp:192
#: src/apprt/gtk/ui/1.5/window.blp:265
msgid "Split Right"
msgstr "Оң жаққа бөлу"
#: src/apprt/gtk/ui/1.2/surface.blp:322
msgid "Tab"
msgstr "Бет"
#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224
#: src/apprt/gtk/ui/1.5/window.blp:320
msgid "Change Tab Title…"
msgstr "Бет атауын өзгерту…"
#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57
#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229
msgid "New Tab"
msgstr "Жаңа бет"
#: src/apprt/gtk/ui/1.2/surface.blp:335 src/apprt/gtk/ui/1.5/window.blp:234
msgid "Close Tab"
msgstr "Бетті жабу"
#: src/apprt/gtk/ui/1.2/surface.blp:342
msgid "Window"
msgstr "Терезе"
#: src/apprt/gtk/ui/1.2/surface.blp:345 src/apprt/gtk/ui/1.5/window.blp:212
msgid "New Window"
msgstr "Жаңа терезе"
#: src/apprt/gtk/ui/1.2/surface.blp:350 src/apprt/gtk/ui/1.5/window.blp:217
msgid "Close Window"
msgstr "Терезені жабу"
#: src/apprt/gtk/ui/1.2/surface.blp:358
msgid "Config"
msgstr "Конфигурация"
#: src/apprt/gtk/ui/1.2/surface.blp:361 src/apprt/gtk/ui/1.5/window.blp:295
msgid "Open Configuration"
msgstr "Конфигурацияны ашу"
#: src/apprt/gtk/ui/1.5/title-dialog.blp:5
msgid "Leave blank to restore the default title."
msgstr "Бастапқы атауды қалпына келтіру үшін бос қалдырыңыз."
#: src/apprt/gtk/ui/1.5/title-dialog.blp:9
msgid "OK"
msgstr "ОК"
#: src/apprt/gtk/ui/1.5/window.blp:58 src/apprt/gtk/ui/1.5/window.blp:108
msgid "New Split"
msgstr "Жаңа бөлу"
#: src/apprt/gtk/ui/1.5/window.blp:68 src/apprt/gtk/ui/1.5/window.blp:126
msgid "View Open Tabs"
msgstr "Ашық беттерді көру"
#: src/apprt/gtk/ui/1.5/window.blp:78 src/apprt/gtk/ui/1.5/window.blp:140
msgid "Main Menu"
msgstr "Бас мәзір"
#: src/apprt/gtk/ui/1.5/window.blp:285
msgid "Command Palette"
msgstr "Командалар палитрасы"
#: src/apprt/gtk/ui/1.5/window.blp:290
msgid "Terminal Inspector"
msgstr "Терминал инспекторы"
#: src/apprt/gtk/ui/1.5/window.blp:307 src/apprt/gtk/class/window.zig:1727
msgid "About Ghostty"
msgstr "Ghostty туралы"
#: src/apprt/gtk/ui/1.5/window.blp:312
msgid "Quit"
msgstr "Шығу"
#: src/apprt/gtk/ui/1.5/command-palette.blp:17
msgid "Execute a command…"
msgstr "Команданы орындау…"
#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:198
msgid ""
"An application is attempting to write to the clipboard. The current clipboard "
"contents are shown below."
msgstr ""
"Қолданба алмасу буферіне жазуға әрекеттенуде. Ағымдағы алмасу буферінің "
"мазмұны төменде көрсетілген."
#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:202
msgid ""
"An application is attempting to read from the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
"Қолданба алмасу буферінен оқуға әрекеттенуде. Ағымдағы алмасу буферінің "
"мазмұны төменде көрсетілген."
#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205
msgid "Warning: Potentially Unsafe Paste"
msgstr "Ескерту: Қауіпсіз емес бола алатын кірістіру"
#: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:206
msgid ""
"Pasting this text into the terminal may be dangerous as it looks like some "
"commands may be executed."
msgstr ""
"Бұл мәтінді терминалға кірістіру қауіпті болуы мүмкін, себебі кейбір "
"командалар орындалуы мүмкін сияқты."
#: src/apprt/gtk/class/close_confirmation_dialog.zig:184
msgid "Quit Ghostty?"
msgstr "Ghostty-ден шығу керек пе?"
#: src/apprt/gtk/class/close_confirmation_dialog.zig:185
msgid "Close Tab?"
msgstr "Бетті жабу керек пе?"
#: src/apprt/gtk/class/close_confirmation_dialog.zig:186
msgid "Close Window?"
msgstr "Терезені жабу керек пе?"
#: src/apprt/gtk/class/close_confirmation_dialog.zig:187
msgid "Close Split?"
msgstr "Бөлуді жабу керек пе?"
#: src/apprt/gtk/class/close_confirmation_dialog.zig:193
msgid "All terminal sessions will be terminated."
msgstr "Барлық терминал сессиялары тоқтатылады."
#: src/apprt/gtk/class/close_confirmation_dialog.zig:194
msgid "All terminal sessions in this tab will be terminated."
msgstr "Осы беттегі барлық терминал сессиялары тоқтатылады."
#: src/apprt/gtk/class/close_confirmation_dialog.zig:195
msgid "All terminal sessions in this window will be terminated."
msgstr "Осы терезедегі барлық терминал сессиялары тоқтатылады."
#: src/apprt/gtk/class/close_confirmation_dialog.zig:196
msgid "The currently running process in this split will be terminated."
msgstr "Осы бөлудегі ағымдағы орындалып жатқан процесс тоқтатылады."
#: src/apprt/gtk/class/surface.zig:1108
msgid "Command Finished"
msgstr "Команда аяқталды"
#: src/apprt/gtk/class/surface.zig:1109
msgid "Command Succeeded"
msgstr "Команда сәтті орындалды"
#: src/apprt/gtk/class/surface.zig:1110
msgid "Command Failed"
msgstr "Команда сәтсіз аяқталды"
#: src/apprt/gtk/class/surface_child_exited.zig:109
msgid "Command succeeded"
msgstr "Команда сәтті орындалды"
#: src/apprt/gtk/class/surface_child_exited.zig:113
msgid "Command failed"
msgstr "Команда сәтсіз аяқталды"
#: src/apprt/gtk/class/title_dialog.zig:225
msgid "Change Terminal Title"
msgstr "Терминал атауын өзгерту"
#: src/apprt/gtk/class/title_dialog.zig:226
msgid "Change Tab Title"
msgstr "Бет атауын өзгерту"
#: src/apprt/gtk/class/window.zig:1007
msgid "Reloaded the configuration"
msgstr "Конфигурация қайта жүктелді"
#: src/apprt/gtk/class/window.zig:1566
msgid "Copied to clipboard"
msgstr "Алмасу буферіне көшірілді"
#: src/apprt/gtk/class/window.zig:1568
msgid "Cleared clipboard"
msgstr "Алмасу буфері тазартылды"
#: src/apprt/gtk/class/window.zig:1708
msgid "Ghostty Developers"
msgstr "Ghostty әзірлеушілері"

View File

@@ -607,10 +607,14 @@ pub fn init(
};
// The command we're going to execute
const command: ?configpkg.Command = if (app.first)
config.@"initial-command" orelse config.command
else
config.command;
const command: ?configpkg.Command = command: {
if (app.first) {
if (config.@"initial-command") |command| {
break :command command;
}
}
break :command config.command;
};
// Start our IO implementation
// This separate block ({}) is important because our errdefers must

View File

@@ -2,6 +2,7 @@ const Self = @This();
const std = @import("std");
const apprt = @import("../../apprt.zig");
const configpkg = @import("../../config.zig");
const CoreSurface = @import("../../Surface.zig");
const ApprtApp = @import("App.zig");
const Application = @import("class/application.zig").Application;

View File

@@ -22,6 +22,7 @@ const xev = @import("../../../global.zig").xev;
const Binding = @import("../../../input.zig").Binding;
const CoreConfig = configpkg.Config;
const CoreSurface = @import("../../../Surface.zig");
const lib = @import("../../../lib/main.zig");
const ext = @import("../ext.zig");
const key = @import("../key.zig");
@@ -709,6 +710,7 @@ pub const Application = extern struct {
.app => null,
.surface => |v| v,
},
.none,
),
.open_config => return Action.openConfig(self),
@@ -1669,17 +1671,30 @@ pub const Application = extern struct {
) callconv(.c) void {
log.debug("received new window action", .{});
parameter: {
var arena: std.heap.ArenaAllocator = .init(Application.default().allocator());
defer arena.deinit();
const alloc = arena.allocator();
var working_directory: ?[:0]const u8 = null;
var title: ?[:0]const u8 = null;
var command: ?configpkg.Command = null;
var args: std.ArrayList([:0]const u8) = .empty;
overrides: {
// were we given a parameter?
const parameter = parameter_ orelse break :parameter;
const parameter = parameter_ orelse break :overrides;
const as_variant_type = glib.VariantType.new("as");
defer as_variant_type.free();
// ensure that the supplied parameter is an array of strings
if (glib.Variant.isOfType(parameter, as_variant_type) == 0) {
log.warn("parameter is of type {s}", .{parameter.getTypeString()});
break :parameter;
log.warn("parameter is of type '{s}', not '{s}'", .{
parameter.getTypeString(),
as_variant_type.peekString()[0..as_variant_type.getStringLength()],
});
break :overrides;
}
const s_variant_type = glib.VariantType.new("s");
@@ -1688,7 +1703,10 @@ pub const Application = extern struct {
var it: glib.VariantIter = undefined;
_ = it.init(parameter);
while (it.nextValue()) |value| {
var e_seen: bool = false;
var i: usize = 0;
while (it.nextValue()) |value| : (i += 1) {
defer value.unref();
// just to be sure
@@ -1698,13 +1716,64 @@ pub const Application = extern struct {
const buf = value.getString(&len);
const str = buf[0..len];
log.debug("new-window command argument: {s}", .{str});
log.debug("new-window argument: {d} {s}", .{ i, str });
if (e_seen) {
const cpy = alloc.dupeZ(u8, str) catch |err| {
log.warn("unable to duplicate argument {d} {s}: {t}", .{ i, str, err });
break :overrides;
};
args.append(alloc, cpy) catch |err| {
log.warn("unable to append argument {d} {s}: {t}", .{ i, str, err });
break :overrides;
};
continue;
}
if (std.mem.eql(u8, str, "-e")) {
e_seen = true;
continue;
}
if (lib.cutPrefix(u8, str, "--command=")) |v| {
var cmd: configpkg.Command = undefined;
cmd.parseCLI(alloc, v) catch |err| {
log.warn("unable to parse command: {t}", .{err});
continue;
};
command = cmd;
continue;
}
if (lib.cutPrefix(u8, str, "--working-directory=")) |v| {
working_directory = alloc.dupeZ(u8, std.mem.trim(u8, v, &std.ascii.whitespace)) catch |err| wd: {
log.warn("unable to duplicate working directory: {t}", .{err});
break :wd null;
};
continue;
}
if (lib.cutPrefix(u8, str, "--title=")) |v| {
title = alloc.dupeZ(u8, std.mem.trim(u8, v, &std.ascii.whitespace)) catch |err| t: {
log.warn("unable to duplicate title: {t}", .{err});
break :t null;
};
continue;
}
}
}
_ = self.core().mailbox.push(.{
.new_window = .{},
}, .{ .forever = {} });
if (args.items.len > 0) {
command = .{
.direct = args.items,
};
}
Action.newWindow(self, null, .{
.command = command,
.working_directory = working_directory,
.title = title,
}) catch |err| {
log.warn("unable to create new window: {t}", .{err});
};
}
pub fn actionOpenConfig(
@@ -2151,6 +2220,13 @@ const Action = struct {
pub fn newWindow(
self: *Application,
parent: ?*CoreSurface,
overrides: struct {
command: ?configpkg.Command = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
pub const none: @This() = .{};
},
) !void {
// Note that we've requested a window at least once. This is used
// to trigger quit on no windows. Note I'm not sure if this is REALLY
@@ -2159,14 +2235,32 @@ const Action = struct {
// was a delay in the event loop before we created a Window.
self.private().requested_window = true;
const win = Window.new(self);
initAndShowWindow(self, win, parent);
const win = Window.new(self, .{
.title = overrides.title,
});
initAndShowWindow(
self,
win,
parent,
.{
.command = overrides.command,
.working_directory = overrides.working_directory,
.title = overrides.title,
},
);
}
fn initAndShowWindow(
self: *Application,
win: *Window,
parent: ?*CoreSurface,
overrides: struct {
command: ?configpkg.Command = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
pub const none: @This() = .{};
},
) void {
// Setup a binding so that whenever our config changes so does the
// window. There's never a time when the window config should be out
@@ -2180,7 +2274,11 @@ const Action = struct {
);
// Create a new tab with window context (first tab in new window)
win.newTabForWindow(parent);
win.newTabForWindow(parent, .{
.command = overrides.command,
.working_directory = overrides.working_directory,
.title = overrides.title,
});
// Estimate the initial window size before presenting so the window
// manager can position it correctly.
@@ -2506,7 +2604,7 @@ const Action = struct {
.@"quick-terminal" = true,
});
assert(win.isQuickTerminal());
initAndShowWindow(self, win, null);
initAndShowWindow(self, win, null, .none);
return true;
}

View File

@@ -7,6 +7,7 @@ const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const configpkg = @import("../../../config.zig");
const apprt = @import("../../../apprt.zig");
const ext = @import("../ext.zig");
const gresource = @import("../build/gresource.zig");
@@ -157,11 +158,6 @@ pub const SplitTree = extern struct {
/// used to debounce updates.
rebuild_source: ?c_uint = null,
/// Tracks whether we want a rebuild to happen at the next tick
/// that our surface tree has no surfaces with parents. See the
/// propTree function for a lot more details.
rebuild_pending: bool,
/// Used to store state about a pending surface close for the
/// close dialog.
pending_close: ?Surface.Tree.Node.Handle,
@@ -208,11 +204,22 @@ pub const SplitTree = extern struct {
self: *Self,
direction: Surface.Tree.Split.Direction,
parent_: ?*Surface,
overrides: struct {
command: ?configpkg.Command = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
pub const none: @This() = .{};
},
) Allocator.Error!void {
const alloc = Application.default().allocator();
// Create our new surface.
const surface: *Surface = .new();
const surface: *Surface = .new(.{
.command = overrides.command,
.working_directory = overrides.working_directory,
.title = overrides.title,
});
defer surface.unref();
_ = surface.refSink();
@@ -408,13 +415,6 @@ pub const SplitTree = extern struct {
self,
.{ .detail = "focused" },
);
_ = gobject.Object.signals.notify.connect(
surface.as(gtk.Widget),
*Self,
propSurfaceParent,
self,
.{ .detail = "parent" },
);
}
}
@@ -478,20 +478,6 @@ pub const SplitTree = extern struct {
return surface;
}
/// Returns whether any of the surfaces in the tree have a parent.
/// This is important because we can only rebuild the widget tree
/// when every surface has no parent.
fn getTreeHasParents(self: *Self) bool {
const tree: *const Surface.Tree = self.getTree() orelse &.empty;
var it = tree.iterator();
while (it.next()) |entry| {
const surface = entry.view;
if (surface.as(gtk.Widget).getParent() != null) return true;
}
return false;
}
pub fn getHasSurfaces(self: *Self) bool {
const tree: *const Surface.Tree = self.private().tree orelse &.empty;
return !tree.isEmpty();
@@ -638,6 +624,7 @@ pub const SplitTree = extern struct {
self.newSplit(
direction,
self.getActiveSurface(),
.none,
) catch |err| {
log.warn("new split failed error={}", .{err});
};
@@ -779,27 +766,6 @@ pub const SplitTree = extern struct {
self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
}
fn propSurfaceParent(
_: *gtk.Widget,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
const priv = self.private();
// If we're not waiting to rebuild then ignore this.
if (!priv.rebuild_pending) return;
// If any parents still exist in our tree then don't do anything.
if (self.getTreeHasParents()) return;
// Schedule the rebuild. Note, I tried to do this immediately (not
// on an idle tick) and it didn't work and had obvious rendering
// glitches. Something to look into in the future.
assert(priv.rebuild_source == null);
priv.rebuild_pending = false;
priv.rebuild_source = glib.idleAdd(onRebuild, self);
}
fn propTree(
self: *Self,
_: *gobject.ParamSpec,
@@ -807,6 +773,12 @@ pub const SplitTree = extern struct {
) callconv(.c) void {
const priv = self.private();
// No matter what we notify
self.as(gobject.Object).freezeNotify();
defer self.as(gobject.Object).thawNotify();
self.as(gobject.Object).notifyByPspec(properties.@"has-surfaces".impl.param_spec);
self.as(gobject.Object).notifyByPspec(properties.@"is-zoomed".impl.param_spec);
// If we were planning a rebuild, always remove that so we can
// start from a clean slate.
if (priv.rebuild_source) |v| {
@@ -816,38 +788,22 @@ pub const SplitTree = extern struct {
priv.rebuild_source = null;
}
// We need to wait for all our previous surfaces to lose their
// parent before adding them to a new one. I'm not sure if its a GTK
// bug, but manually forcing an unparent of all prior surfaces AND
// adding them to a new parent in the same tick causes the GLArea
// to break (it seems). I didn't investigate too deeply.
//
// Note, we also can't just defer to an idle tick (via idleAdd) because
// sometimes it takes more than one tick for all our surfaces to
// lose their parent.
//
// To work around this issue, if we have any surfaces that have
// a parent, we set the build pending flag and wait for the tree
// to be fully parent-free before building.
priv.rebuild_pending = self.getTreeHasParents();
// Reset our prior bin. This will force all prior surfaces to
// unparent... eventually.
priv.tree_bin.setChild(null);
// If none of the surfaces we plan on drawing require an unparent
// then we can setup our tree immediately. Otherwise, it'll happen
// via the `propSurfaceParent` callback.
if (!priv.rebuild_pending and priv.rebuild_source == null) {
priv.rebuild_source = glib.idleAdd(
onRebuild,
self,
);
// If we transitioned to an empty tree, clear immediately instead of
// waiting for an idle callback. Delaying teardown can keep the last
// surface alive during shutdown if the main loop exits first.
if (priv.tree == null) {
priv.tree_bin.setChild(null);
return;
}
// Dependent properties
self.as(gobject.Object).notifyByPspec(properties.@"has-surfaces".impl.param_spec);
self.as(gobject.Object).notifyByPspec(properties.@"is-zoomed".impl.param_spec);
// Build on an idle callback so rapid tree changes are debounced.
// We keep the existing tree attached until the rebuild runs,
// which avoids transient empty frames.
assert(priv.rebuild_source == null);
priv.rebuild_source = glib.idleAdd(
onRebuild,
self,
);
}
fn onRebuild(ud: ?*anyopaque) callconv(.c) c_int {
@@ -857,22 +813,21 @@ pub const SplitTree = extern struct {
const priv = self.private();
priv.rebuild_source = null;
// Prior to rebuilding the tree, our surface tree must be
// comprised of fully orphaned surfaces.
assert(!self.getTreeHasParents());
// Rebuild our tree
const tree: *const Surface.Tree = self.private().tree orelse &.empty;
if (!tree.isEmpty()) {
priv.tree_bin.setChild(self.buildTree(
if (tree.isEmpty()) {
priv.tree_bin.setChild(null);
} else {
const built = self.buildTree(
tree,
tree.zoomed orelse .root,
));
);
defer built.deinit();
priv.tree_bin.setChild(built.widget);
}
// If we have a last focused surface, we need to refocus it, because
// during the frame between setting the bin to null and rebuilding,
// GTK will reset our focus state (as it should!)
// Replacing our tree widget hierarchy can reset focus state.
// If we have a last-focused surface, restore focus to it.
if (priv.last_focused.get()) |v| {
defer v.unref();
v.grabFocus();
@@ -889,26 +844,120 @@ pub const SplitTree = extern struct {
/// Builds the widget tree associated with a surface split tree.
///
/// The final returned widget is expected to be a floating reference,
/// ready to be attached to a parent widget.
/// Returned widgets are expected to be attached to a parent by the caller.
///
/// If `release_ref` is true then `widget` has an extra temporary
/// reference that must be released once it is parented in the rebuilt
/// tree.
const BuildTreeResult = struct {
widget: *gtk.Widget,
release_ref: bool,
pub fn initNew(widget: *gtk.Widget) BuildTreeResult {
return .{ .widget = widget, .release_ref = false };
}
pub fn initReused(widget: *gtk.Widget) BuildTreeResult {
// We add a temporary ref to the widget to ensure it doesn't
// get destroyed while we're rebuilding the tree and detaching
// it from its old parent. The caller is expected to release
// this ref once the widget is attached to its new parent.
_ = widget.as(gobject.Object).ref();
// Detach after we ref it so that this doesn't mark the
// widget for destruction.
detachWidget(widget);
return .{ .widget = widget, .release_ref = true };
}
pub fn deinit(self: BuildTreeResult) void {
// If we have to release a ref, do it.
if (self.release_ref) self.widget.as(gobject.Object).unref();
}
};
fn buildTree(
self: *Self,
tree: *const Surface.Tree,
current: Surface.Tree.Node.Handle,
) *gtk.Widget {
) BuildTreeResult {
return switch (tree.nodes[current.idx()]) {
.leaf => |v| gobject.ext.newInstance(SurfaceScrolledWindow, .{
.surface = v,
}).as(gtk.Widget),
.split => |s| SplitTreeSplit.new(
current,
&s,
self.buildTree(tree, s.left),
self.buildTree(tree, s.right),
).as(gtk.Widget),
.leaf => |v| leaf: {
const window = ext.getAncestor(
SurfaceScrolledWindow,
v.as(gtk.Widget),
) orelse {
// The surface isn't in a window already so we don't
// have to worry about reuse.
break :leaf .initNew(gobject.ext.newInstance(
SurfaceScrolledWindow,
.{ .surface = v },
).as(gtk.Widget));
};
// Keep this widget alive while we detach it from the
// old tree and adopt it into the new one.
break :leaf .initReused(window.as(gtk.Widget));
},
.split => |s| split: {
const left = self.buildTree(tree, s.left);
defer left.deinit();
const right = self.buildTree(tree, s.right);
defer right.deinit();
break :split .initNew(SplitTreeSplit.new(
current,
&s,
left.widget,
right.widget,
).as(gtk.Widget));
},
};
}
/// Detach a split widget from its current parent.
///
/// We intentionally use parent-specific child APIs when possible
/// (`GtkPaned.setStartChild/setEndChild`, `AdwBin.setChild`) instead of
/// calling `gtk.Widget.unparent` directly. Container implementations track
/// child pointers/properties internally, and those setters are the path
/// that keeps container state and notifications in sync.
fn detachWidget(widget: *gtk.Widget) void {
const parent = widget.getParent() orelse return;
// Surface will be in a paned when it is split.
if (gobject.ext.cast(gtk.Paned, parent)) |paned| {
if (paned.getStartChild()) |child| {
if (child == widget) {
paned.setStartChild(null);
return;
}
}
if (paned.getEndChild()) |child| {
if (child == widget) {
paned.setEndChild(null);
return;
}
}
}
// Surface will be in a bin when it is not split.
if (gobject.ext.cast(adw.Bin, parent)) |bin| {
if (bin.getChild()) |child| {
if (child == widget) {
bin.setChild(null);
return;
}
}
}
// Fallback for unexpected parents where we don't have a typed
// container API available.
widget.unparent();
}
//---------------------------------------------------------------
// Class

View File

@@ -10,6 +10,7 @@ const gtk = @import("gtk");
const apprt = @import("../../../apprt.zig");
const build_config = @import("../../../build_config.zig");
const configpkg = @import("../../../config.zig");
const datastruct = @import("../../../datastruct/main.zig");
const font = @import("../../../font/main.zig");
const input = @import("../../../input.zig");
@@ -693,6 +694,10 @@ pub const Surface = extern struct {
/// Whether primary paste (middle-click paste) is enabled.
gtk_enable_primary_paste: bool = true,
/// True when a left mouse down was consumed purely for a focus change,
/// and the matching left mouse release should also be suppressed.
suppress_left_mouse_release: bool = false,
/// How much pending horizontal scroll do we have?
pending_horizontal_scroll: f64 = 0.0,
@@ -700,11 +705,33 @@ pub const Surface = extern struct {
/// stops scrolling.
pending_horizontal_scroll_reset: ?c_uint = null,
overrides: struct {
command: ?configpkg.Command = null,
working_directory: ?[:0]const u8 = null,
pub const none: @This() = .{};
} = .none,
pub var offset: c_int = 0;
};
pub fn new() *Self {
return gobject.ext.newInstance(Self, .{});
pub fn new(overrides: struct {
command: ?configpkg.Command = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
pub const none: @This() = .{};
}) *Self {
const self = gobject.ext.newInstance(Self, .{
.@"title-override" = overrides.title,
});
const alloc = Application.default().allocator();
const priv: *Private = self.private();
priv.overrides = .{
.command = if (overrides.command) |c| c.clone(alloc) catch null else null,
.working_directory = if (overrides.working_directory) |wd| alloc.dupeZ(u8, wd) catch null else null,
};
return self;
}
pub fn core(self: *Self) ?*CoreSurface {
@@ -1849,6 +1876,7 @@ pub const Surface = extern struct {
}
fn finalize(self: *Self) callconv(.c) void {
const alloc = Application.default().allocator();
const priv = self.private();
if (priv.core_surface) |v| {
// Remove ourselves from the list of known surfaces in the app.
@@ -1862,7 +1890,6 @@ pub const Surface = extern struct {
// Deinit the surface
v.deinit();
const alloc = Application.default().allocator();
alloc.destroy(v);
priv.core_surface = null;
@@ -1895,9 +1922,16 @@ pub const Surface = extern struct {
glib.free(@ptrCast(@constCast(v)));
priv.title_override = null;
}
if (priv.overrides.command) |c| {
c.deinit(alloc);
priv.overrides.command = null;
}
if (priv.overrides.working_directory) |wd| {
alloc.free(wd);
priv.overrides.working_directory = null;
}
// Clean up key sequence and key table state
const alloc = Application.default().allocator();
for (priv.key_sequence.items) |s| alloc.free(s);
priv.key_sequence.deinit(alloc);
for (priv.key_tables.items) |s| alloc.free(s);
@@ -2733,13 +2767,21 @@ pub const Surface = extern struct {
// If we don't have focus, grab it.
const gl_area_widget = priv.gl_area.as(gtk.Widget);
if (gl_area_widget.hasFocus() == 0) {
const had_focus = gl_area_widget.hasFocus() != 0;
if (!had_focus) {
_ = gl_area_widget.grabFocus();
}
// Report the event
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
// If this click is only transitioning split focus, suppress it so
// it doesn't get forwarded to the terminal as a mouse event.
if (!had_focus and button == .left) {
priv.suppress_left_mouse_release = true;
return;
}
if (button == .middle and !priv.gtk_enable_primary_paste) {
return;
}
@@ -2795,6 +2837,11 @@ pub const Surface = extern struct {
const gtk_mods = event.getModifierState();
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
if (button == .left and priv.suppress_left_mouse_release) {
priv.suppress_left_mouse_release = false;
return;
}
if (button == .middle and !priv.gtk_enable_primary_paste) {
return;
}
@@ -3296,7 +3343,7 @@ pub const Surface = extern struct {
};
fn initSurface(self: *Self) InitError!void {
const priv = self.private();
const priv: *Private = self.private();
assert(priv.core_surface == null);
const gl_area = priv.gl_area;
@@ -3329,6 +3376,13 @@ pub const Surface = extern struct {
);
defer config.deinit();
if (priv.overrides.command) |c| {
config.command = try c.clone(config._arena.?.allocator());
}
if (priv.overrides.working_directory) |wd| {
config.@"working-directory" = try config._arena.?.allocator().dupeZ(u8, wd);
}
// Properties that can impact surface init
if (priv.font_size_request) |size| config.@"font-size" = size.points;
if (priv.pwd) |pwd| config.@"working-directory" = pwd;

View File

@@ -5,6 +5,7 @@ const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const configpkg = @import("../../../config.zig");
const apprt = @import("../../../apprt.zig");
const CoreSurface = @import("../../../Surface.zig");
const ext = @import("../ext.zig");
@@ -186,22 +187,34 @@ pub const Tab = extern struct {
}
}
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
pub fn new(config: ?*Config, overrides: struct {
command: ?configpkg.Command = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
// Init our actions
self.initActionMap();
pub const none: @This() = .{};
}) *Self {
const tab = gobject.ext.newInstance(Tab, .{});
const priv: *Private = tab.private();
if (config) |c| priv.config = c.ref();
// If our configuration is null then we get the configuration
// from the application.
const priv = self.private();
if (priv.config == null) {
const app = Application.default();
priv.config = app.getConfig();
}
tab.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec);
// Create our initial surface in the split tree.
priv.split_tree.newSplit(.right, null) catch |err| switch (err) {
priv.split_tree.newSplit(.right, null, .{
.command = overrides.command,
.working_directory = overrides.working_directory,
.title = overrides.title,
}) catch |err| switch (err) {
error.OutOfMemory => {
// TODO: We should make our "no surfaces" state more aesthetically
// pleasing and show something like an "Oops, something went wrong"
@@ -209,6 +222,15 @@ pub const Tab = extern struct {
@panic("oom");
},
};
return tab;
}
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
// Init our actions
self.initActionMap();
}
fn initActionMap(self: *Self) void {

View File

@@ -266,10 +266,27 @@ pub const Window = extern struct {
pub var offset: c_int = 0;
};
pub fn new(app: *Application) *Self {
return gobject.ext.newInstance(Self, .{
pub fn new(
app: *Application,
overrides: struct {
title: ?[:0]const u8 = null,
pub const none: @This() = .{};
},
) *Self {
const win = gobject.ext.newInstance(Self, .{
.application = app,
});
if (overrides.title) |title| {
// If the overrides have a title set, we set that immediately
// so that any applications inspecting the window states see an
// immediate title set when the window appears, rather than waiting
// possibly a few event loop ticks for it to sync from the surface.
win.as(gtk.Window).setTitle(title);
}
return win;
}
fn init(self: *Self, _: *Class) callconv(.c) void {
@@ -278,10 +295,14 @@ pub const Window = extern struct {
// If our configuration is null then we get the configuration
// from the application.
const priv = self.private();
if (priv.config == null) {
const config = config: {
if (priv.config) |config| break :config config.get();
const app = Application.default();
priv.config = app.getConfig();
}
const config = app.getConfig();
priv.config = config;
break :config config.get();
};
// We initialize our windowing protocol to none because we can't
// actually initialize this until we get realized.
@@ -305,17 +326,16 @@ pub const Window = extern struct {
self.initActionMap();
// Start states based on config.
if (priv.config) |config_obj| {
const config = config_obj.get();
if (config.maximize) self.as(gtk.Window).maximize();
if (config.fullscreen != .false) self.as(gtk.Window).fullscreen();
if (config.maximize) self.as(gtk.Window).maximize();
if (config.fullscreen != .false) self.as(gtk.Window).fullscreen();
// If we have an explicit title set, we set that immediately
// so that any applications inspecting the window states see
// an immediate title set when the window appears, rather than
// waiting possibly a few event loop ticks for it to sync from
// the surface.
if (config.title) |v| self.as(gtk.Window).setTitle(v);
// If we have an explicit title set, we set that immediately
// so that any applications inspecting the window states see
// an immediate title set when the window appears, rather than
// waiting possibly a few event loop ticks for it to sync from
// the surface.
if (config.title) |title| {
self.as(gtk.Window).setTitle(title);
}
// We always sync our appearance at the end because loading our
@@ -368,21 +388,56 @@ pub const Window = extern struct {
/// at the position dictated by the `window-new-tab-position` config.
/// The new tab will be selected.
pub fn newTab(self: *Self, parent_: ?*CoreSurface) void {
_ = self.newTabPage(parent_, .tab);
_ = self.newTabPage(parent_, .tab, .none);
}
pub fn newTabForWindow(self: *Self, parent_: ?*CoreSurface) void {
_ = self.newTabPage(parent_, .window);
pub fn newTabForWindow(
self: *Self,
parent_: ?*CoreSurface,
overrides: struct {
command: ?configpkg.Command = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
pub const none: @This() = .{};
},
) void {
_ = self.newTabPage(
parent_,
.window,
.{
.command = overrides.command,
.working_directory = overrides.working_directory,
.title = overrides.title,
},
);
}
fn newTabPage(self: *Self, parent_: ?*CoreSurface, context: apprt.surface.NewSurfaceContext) *adw.TabPage {
const priv = self.private();
fn newTabPage(
self: *Self,
parent_: ?*CoreSurface,
context: apprt.surface.NewSurfaceContext,
overrides: struct {
command: ?configpkg.Command = null,
working_directory: ?[:0]const u8 = null,
title: ?[:0]const u8 = null,
pub const none: @This() = .{};
},
) *adw.TabPage {
const priv: *Private = self.private();
const tab_view = priv.tab_view;
// Create our new tab object
const tab = gobject.ext.newInstance(Tab, .{
.config = priv.config,
});
const tab = Tab.new(
priv.config,
.{
.command = overrides.command,
.working_directory = overrides.working_directory,
.title = overrides.title,
},
);
if (parent_) |p| {
// For a new window's first tab, inherit the parent's initial size hints.
if (context == .window) {
@@ -1253,7 +1308,7 @@ pub const Window = extern struct {
_: *adw.TabOverview,
self: *Self,
) callconv(.c) *adw.TabPage {
return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null, .tab);
return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null, .tab, .none);
}
fn tabOverviewOpen(

View File

@@ -18,7 +18,7 @@ const DBus = @import("DBus.zig");
// `ghostty +new-window -e echo hello` would be equivalent to the following command (on a release build):
//
// ```
// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["echo" "hello"]>]' []
// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["-e" "echo" "hello"]>]' []
// ```
pub fn newWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.Io.Writer.Error || apprt.ipc.Errors)!bool {
var dbus = try DBus.init(
@@ -32,10 +32,10 @@ pub fn newWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Ac
defer dbus.deinit(alloc);
if (value.arguments) |arguments| {
// If `-e` was specified on the command line, the first
// parameter is an array of strings that contain the arguments
// that came after `-e`, which will be interpreted as a command
// to run.
// If any arguments were specified on the command line, the first
// parameter is an array of strings that contain the arguments. They
// will be sent to the main Ghostty instance and interpreted as CLI
// arguments.
const as_variant_type = glib.VariantType.new("as");
defer as_variant_type.free();

View File

@@ -424,19 +424,6 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config {
// show up properly in `--help`.
{
// These should default to `true` except on macOS because linking them
// to Ghostty statically when GTK is dynamically linked to them can
// cause crashes.
for (&[_][]const u8{
"fontconfig",
}) |dep| {
_ = b.systemIntegrationOption(
dep,
.{
.default = if (target.result.os.tag.isDarwin()) false else true,
},
);
}
// These dependencies we want to default false if we're on macOS.
// On macOS we don't want to use system libraries because we
// generally want a fat binary. This can be overridden with the
@@ -444,6 +431,7 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config {
for (&[_][]const u8{
"freetype",
"harfbuzz",
"fontconfig",
"libpng",
"zlib",
"oniguruma",

View File

@@ -200,13 +200,6 @@ pub fn add(
if (b.systemIntegrationOption("fontconfig", .{})) {
step.linkSystemLibrary2("fontconfig", dynamic_link_opts);
} else {
if (self.config.app_runtime == .gtk)
std.debug.print(
\\WARNING: Statically linking FontConfig when using the GTK app runtime is known
\\to cause crashes! It is HIGHLY recommended that Ghostty be dynamically linked
\\to the system FontConfig library.
\\
, .{});
step.linkLibrary(fontconfig_dep.artifact("fontconfig"));
try static_libs.append(
b.allocator,

View File

@@ -5,6 +5,8 @@ const Action = @import("../cli.zig").ghostty.Action;
const apprt = @import("../apprt.zig");
const args = @import("args.zig");
const diagnostics = @import("diagnostics.zig");
const lib = @import("../lib/main.zig");
const homedir = @import("../os/homedir.zig");
pub const Options = struct {
/// This is set by the CLI parser for deinit.
@@ -13,35 +15,63 @@ pub const Options = struct {
/// If set, open up a new window in a custom instance of Ghostty.
class: ?[:0]const u8 = null,
/// If `-e` is found in the arguments, this will contain all of the
/// arguments to pass to Ghostty as the command.
_arguments: ?[][:0]const u8 = null,
/// Did the user specify a `--working-directory` argument on the command line?
_working_directory_seen: bool = false,
/// All of the arguments after `+new-window`. They will be sent to Ghosttty
/// for processing.
_arguments: std.ArrayList([:0]const u8) = .empty,
/// Enable arg parsing diagnostics so that we don't get an error if
/// there is a "normal" config setting on the cli.
_diagnostics: diagnostics.DiagnosticList = .{},
/// Manual parse hook, used to deal with `-e`
pub fn parseManuallyHook(self: *Options, alloc: Allocator, arg: []const u8, iter: anytype) Allocator.Error!bool {
// If it's not `-e` continue with the standard argument parsning.
if (!std.mem.eql(u8, arg, "-e")) return true;
/// Manual parse hook, collect all of the arguments after `+new-window`.
pub fn parseManuallyHook(self: *Options, alloc: Allocator, arg: []const u8, iter: anytype) (error{InvalidValue} || homedir.ExpandError || std.fs.Dir.RealPathAllocError || Allocator.Error)!bool {
var e_seen: bool = std.mem.eql(u8, arg, "-e");
var arguments: std.ArrayList([:0]const u8) = .empty;
errdefer {
for (arguments.items) |argument| alloc.free(argument);
arguments.deinit(alloc);
}
// Include the argument that triggered the manual parse hook.
if (try self.checkArg(alloc, arg)) |a| try self._arguments.append(alloc, a);
// Otherwise gather up the rest of the arguments to use as the command.
// Gather up the rest of the arguments to use as the command.
while (iter.next()) |param| {
try arguments.append(alloc, try alloc.dupeZ(u8, param));
if (e_seen) {
try self._arguments.append(alloc, try alloc.dupeZ(u8, param));
continue;
}
if (std.mem.eql(u8, param, "-e")) {
e_seen = true;
try self._arguments.append(alloc, try alloc.dupeZ(u8, param));
continue;
}
if (try self.checkArg(alloc, param)) |a| try self._arguments.append(alloc, a);
}
self._arguments = try arguments.toOwnedSlice(alloc);
return false;
}
fn checkArg(self: *Options, alloc: Allocator, arg: []const u8) (error{InvalidValue} || homedir.ExpandError || std.fs.Dir.RealPathAllocError || Allocator.Error)!?[:0]const u8 {
if (lib.cutPrefix(u8, arg, "--class=")) |rest| {
self.class = try alloc.dupeZ(u8, std.mem.trim(u8, rest, &std.ascii.whitespace));
return null;
}
if (lib.cutPrefix(u8, arg, "--working-directory=")) |rest| {
const stripped = std.mem.trim(u8, rest, &std.ascii.whitespace);
if (std.mem.eql(u8, stripped, "home")) return try alloc.dupeZ(u8, arg);
if (std.mem.eql(u8, stripped, "inherit")) return try alloc.dupeZ(u8, arg);
const cwd: std.fs.Dir = std.fs.cwd();
var expandhome_buf: [std.fs.max_path_bytes]u8 = undefined;
const expanded = try homedir.expandHome(stripped, &expandhome_buf);
var realpath_buf: [std.fs.max_path_bytes]u8 = undefined;
const realpath = try cwd.realpath(expanded, &realpath_buf);
self._working_directory_seen = true;
return try std.fmt.allocPrintSentinel(alloc, "--working-directory={s}", .{realpath}, 0);
}
return try alloc.dupeZ(u8, arg);
}
pub fn deinit(self: *Options) void {
if (self._arena) |arena| arena.deinit();
self.* = undefined;
@@ -63,11 +93,23 @@ pub const Options = struct {
/// and contact a running Ghostty instance that was configured with the same
/// `class` as was given on the command line.
///
/// If the `-e` flag is included on the command line, any arguments that follow
/// will be sent to the running Ghostty instance and used as the command to run
/// in the new window rather than the default. If `-e` is not specified, Ghostty
/// will use the default command (either specified with `command` in your config
/// or your default shell as configured on your system).
/// All of the arguments after the `+new-window` argument (except for the
/// `--class` flag) will be sent to the remote Ghostty instance and will be
/// parsed as command line flags. These flags will override certain settings
/// when creating the first surface in the new window. Currently, only
/// `--working-directory`, `--command`, and `--title` are supported. `-e` will
/// also work as an alias for `--command`, except that if `-e` is found on the
/// command line all following arguments will become part of the command and no
/// more arguments will be parsed for configuration settings.
///
/// If `--working-directory` is found on the command line and is a relative
/// path (i.e. doesn't start with `/`) it will be resolved to an absolute path
/// relative to the current working directory that the `ghostty +new-window`
/// command is run from. `~/` prefixes will also be expanded to the user's home
/// directory.
///
/// If `--working-directory` is _not_ found on the command line, the working
/// directory that `ghostty +new-window` is run from will be passed to Ghostty.
///
/// GTK uses an application ID to identify instances of applications. If Ghostty
/// is compiled with release optimizations, the default application ID will be
@@ -92,8 +134,16 @@ pub const Options = struct {
/// * `--class=<class>`: If set, open up a new window in a custom instance of
/// Ghostty. The class must be a valid GTK application ID.
///
/// * `--command`: The command to be executed in the first surface of the new window.
///
/// * `--working-directory=<directory>`: The working directory to pass to Ghostty.
///
/// * `--title`: A title that will override the title of the first surface in
/// the new window. The title override may be edited or removed later.
///
/// * `-e`: Any arguments after this will be interpreted as a command to
/// execute inside the new window instead of the default command.
/// execute inside the first surface of the new window instead of the
/// default command.
///
/// Available since: 1.2.0
pub fn run(alloc: Allocator) !u8 {
@@ -143,11 +193,12 @@ fn runArgs(
if (exit) return 1;
}
if (opts._arguments) |arguments| {
if (arguments.len == 0) {
try stderr.print("The -e flag was specified on the command line, but no other arguments were found.\n", .{});
return 1;
}
if (!opts._working_directory_seen) {
const alloc = opts._arena.?.allocator();
const cwd: std.fs.Dir = std.fs.cwd();
var buf: [std.fs.max_path_bytes]u8 = undefined;
const wd = try cwd.realpath(".", &buf);
try opts._arguments.append(alloc, try std.fmt.allocPrintSentinel(alloc, "--working-directory={s}", .{wd}, 0));
}
var arena = ArenaAllocator.init(alloc_gpa);
@@ -159,7 +210,7 @@ fn runArgs(
if (opts.class) |class| .{ .class = class } else .detect,
.new_window,
.{
.arguments = opts._arguments,
.arguments = if (opts._arguments.items.len == 0) null else opts._arguments.items,
},
) catch |err| switch (err) {
error.IPCFailed => {

View File

@@ -29,7 +29,7 @@ const file_load = @import("file_load.zig");
const formatterpkg = @import("formatter.zig");
const themepkg = @import("theme.zig");
const url = @import("url.zig");
const Key = @import("key.zig").Key;
pub const Key = @import("key.zig").Key;
const MetricModifier = fontpkg.Metrics.Modifier;
const help_strings = @import("help_strings");
pub const Command = @import("command.zig").Command;
@@ -3087,7 +3087,7 @@ keybind: Keybinds = .{},
/// the path is not absolute, it is considered relative to the directory of the
/// configuration file that it is referenced from, or from the current working
/// directory if this is used as a CLI flag. The path may be prefixed with `~/`
/// to reference the user's home directory. (GTK only)
/// to reference the user's home directory.
///
/// Available since: 1.2.0
@"bell-audio-path": ?Path = null,
@@ -3095,7 +3095,6 @@ keybind: Keybinds = .{},
/// If `audio` is an enabled bell feature, this is the volume to play the audio
/// file at (relative to the system volume). This is a floating point number
/// ranging from 0.0 (silence) to 1.0 (as loud as possible). The default is 0.5.
/// (GTK only)
///
/// Available since: 1.2.0
@"bell-audio-volume": f64 = 0.5,
@@ -4794,8 +4793,8 @@ fn compatBoldIsBright(
_ = alloc;
assert(std.mem.eql(u8, key, "bold-is-bright"));
const set = cli.args.parseBool(value_ orelse "t") catch return false;
if (set) {
const isset = cli.args.parseBool(value_ orelse "t") catch return false;
if (isset) {
self.@"bold-color" = .bright;
}

View File

@@ -165,6 +165,16 @@ pub const Command = union(enum) {
};
}
pub fn deinit(self: *const Self, alloc: Allocator) void {
switch (self.*) {
.shell => |v| alloc.free(v),
.direct => |l| {
for (l) |v| alloc.free(v);
alloc.free(l);
},
}
}
pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void {
switch (self) {
.shell => |v| try formatter.formatEntry([]const u8, v),

View File

@@ -32,6 +32,20 @@ pub const Path = union(enum) {
return std.meta.eql(self, other);
}
/// ghostty_config_path_s
pub const C = extern struct {
path: [*:0]const u8,
optional: bool,
};
/// Returns the path as a C-compatible struct.
pub fn cval(self: Path) C {
return switch (self) {
.optional => |path| .{ .path = path.ptr, .optional = true },
.required => |path| .{ .path = path.ptr, .optional = false },
};
}
/// Parse the input and return a Path. A leading `?` indicates that the path
/// is _optional_ and an error should not be logged or displayed to the user
/// if that path does not exist. Otherwise the path is required and an error

View File

@@ -10,6 +10,7 @@ pub const String = types.String;
pub const Struct = @import("struct.zig").Struct;
pub const Target = @import("target.zig").Target;
pub const TaggedUnion = unionpkg.TaggedUnion;
pub const cutPrefix = @import("string.zig").cutPrefix;
test {
std.testing.refAllDecls(@This());

15
src/lib/string.zig Normal file
View File

@@ -0,0 +1,15 @@
const std = @import("std");
// This is a copy of std.mem.cutPrefix from 0.16. Once Ghostty has been ported
// to 0.16 this can be removed.
/// If slice starts with prefix, returns the rest of slice starting at
/// prefix.len.
pub fn cutPrefix(comptime T: type, slice: []const T, prefix: []const T) ?[]const T {
return if (std.mem.startsWith(T, slice, prefix)) slice[prefix.len..] else null;
}
test cutPrefix {
try std.testing.expectEqualStrings("foo", cutPrefix(u8, "--example=foo", "--example=").?);
try std.testing.expectEqual(null, cutPrefix(u8, "--example=foo", "-example="));
}

View File

@@ -55,4 +55,5 @@ pub const locales = [_][:0]const u8{
"lt",
"lv",
"vi",
"kk",
};

View File

@@ -141,10 +141,16 @@ _ghostty_deferred_init() {
# - False negative (with prompt_subst): PS1='$mark1'
[[ $PS1 == *$mark1* ]] || PS1=${mark1}${PS1}
[[ $PS1 == *$markB* ]] || PS1=${PS1}${markB}
# Handle multiline prompts by marking continuation lines as
# secondary by replacing newlines with being prefixed
# with k=s
if [[ $PS1 == *$'\n'* ]]; then
# Handle multiline prompts by marking newline-separated
# continuation lines with k=s (mark2). We skip the newline
# immediately after mark1 to avoid introducing a double
# newline due to OSC 133;A's fresh-line behavior.
if [[ $PS1 == ${mark1}$'\n'* ]]; then
builtin local rest=${PS1#${mark1}$'\n'}
if [[ $rest == *$'\n'* ]]; then
PS1=${mark1}$'\n'${rest//$'\n'/$'\n'${mark2}}
fi
elif [[ $PS1 == *$'\n'* ]]; then
PS1=${PS1//$'\n'/$'\n'${mark2}}
fi
@@ -239,6 +245,19 @@ _ghostty_deferred_init() {
builtin print -rnu $_ghostty_fd \$'\\e[0 q'"
fi
# Emit semantic prompt markers at line-init if PS1 doesn't contain our
# marks. This ensures the terminal sees prompt markers even if another
# plugin (like zinit or oh-my-posh) regenerated PS1 after our precmd ran.
# We use 133;P instead of 133;A to avoid fresh-line behavior which would
# disrupt the display since the prompt has already been drawn. We also
# emit 133;B to mark the input area, which is needed for click-to-move.
(( $+functions[_ghostty_zle_line_init] )) || _ghostty_zle_line_init() { builtin true; }
functions[_ghostty_zle_line_init]="
if [[ \$PS1 != *$'%{\\e]133;A'* ]]; then
builtin print -nu \$_ghostty_fd '\\e]133;P;k=i\\a\\e]133;B\\a'
fi
"${functions[_ghostty_zle_line_init]}
# Add Ghostty binary to PATH if the path feature is enabled
if [[ "$GHOSTTY_SHELL_FEATURES" == *"path"* ]] && [[ -n "$GHOSTTY_BIN_DIR" ]]; then
if [[ ":$PATH:" != *":$GHOSTTY_BIN_DIR:"* ]]; then