mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
Merge branch 'main' into vi_VN
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
354
po/kk.po
Normal 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 әзірлеушілері"
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
15
src/lib/string.zig
Normal 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="));
|
||||
}
|
||||
@@ -55,4 +55,5 @@ pub const locales = [_][:0]const u8{
|
||||
"lt",
|
||||
"lv",
|
||||
"vi",
|
||||
"kk",
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user