Merge remote-tracking branch 'upstream/main' into jacob/uucode

pull/8757/head
Jacob Sandlund 2025-08-04 10:00:33 -04:00
commit e5bbc9e83d
41 changed files with 2599 additions and 288 deletions

View File

@ -258,7 +258,7 @@ recreated the next time you run the VM.
We welcome the contribution of new VM definitions, as long as they meet the following criteria: We welcome the contribution of new VM definitions, as long as they meet the following criteria:
1. The should be different enough from existing VM definitions that they represent a distinct 1. They should be different enough from existing VM definitions that they represent a distinct
user (and developer) experience. user (and developer) experience.
2. There's a significant Ghostty user population that uses a similar environment. 2. There's a significant Ghostty user population that uses a similar environment.
3. The VMs can be built using only packages from the current stable NixOS release. 3. The VMs can be built using only packages from the current stable NixOS release.

View File

@ -116,8 +116,8 @@
// Other // Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" }, .apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{ .iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/92f20650771384b82f981fb0f249e5fbdcb69e9f.tar.gz", .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b2742b8baf86f556d6be4d9c6515bfd9d9c7a140.tar.gz",
.hash = "N-V-__8AAGHcWgSXHA9Fw-E0Hbe5EZWyYyI1AvW9O_HBbkRH", .hash = "N-V-__8AAN83XASXgcKp4RDTj_WcQ19E5X24C3FjQoffeMFv",
.lazy = true, .lazy = true,
}, },
}, },

6
build.zig.zon.json generated
View File

@ -49,10 +49,10 @@
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
}, },
"N-V-__8AAGHcWgSXHA9Fw-E0Hbe5EZWyYyI1AvW9O_HBbkRH": { "N-V-__8AAN83XASXgcKp4RDTj_WcQ19E5X24C3FjQoffeMFv": {
"name": "iterm2_themes", "name": "iterm2_themes",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/92f20650771384b82f981fb0f249e5fbdcb69e9f.tar.gz", "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b2742b8baf86f556d6be4d9c6515bfd9d9c7a140.tar.gz",
"hash": "sha256-sQ5IWKQdEU3MOHzxovjA4DO6f/ryvtW18aITN4Bkog0=" "hash": "sha256-w/biUQZ+AJv0atXypwQxJlKkHRUaFS0AlE/VlBJXlVU="
}, },
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
"name": "jetbrains_mono", "name": "jetbrains_mono",

6
build.zig.zon.nix generated
View File

@ -162,11 +162,11 @@ in
}; };
} }
{ {
name = "N-V-__8AAGHcWgSXHA9Fw-E0Hbe5EZWyYyI1AvW9O_HBbkRH"; name = "N-V-__8AAN83XASXgcKp4RDTj_WcQ19E5X24C3FjQoffeMFv";
path = fetchZigArtifact { path = fetchZigArtifact {
name = "iterm2_themes"; name = "iterm2_themes";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/92f20650771384b82f981fb0f249e5fbdcb69e9f.tar.gz"; url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b2742b8baf86f556d6be4d9c6515bfd9d9c7a140.tar.gz";
hash = "sha256-sQ5IWKQdEU3MOHzxovjA4DO6f/ryvtW18aITN4Bkog0="; hash = "sha256-w/biUQZ+AJv0atXypwQxJlKkHRUaFS0AlE/VlBJXlVU=";
}; };
} }
{ {

2
build.zig.zon.txt generated
View File

@ -28,7 +28,7 @@ https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21a
https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-07-23-28-1/ghostty-gobject-0.14.1-2025-07-23-28-1.tar.zst https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-07-23-28-1/ghostty-gobject-0.14.1-2025-07-23-28-1.tar.zst
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/92f20650771384b82f981fb0f249e5fbdcb69e9f.tar.gz https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b2742b8baf86f556d6be4d9c6515bfd9d9c7a140.tar.gz
https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz
https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz

View File

@ -61,9 +61,9 @@
}, },
{ {
"type": "archive", "type": "archive",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/92f20650771384b82f981fb0f249e5fbdcb69e9f.tar.gz", "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b2742b8baf86f556d6be4d9c6515bfd9d9c7a140.tar.gz",
"dest": "vendor/p/N-V-__8AAGHcWgSXHA9Fw-E0Hbe5EZWyYyI1AvW9O_HBbkRH", "dest": "vendor/p/N-V-__8AAN83XASXgcKp4RDTj_WcQ19E5X24C3FjQoffeMFv",
"sha256": "b10e4858a41d114dcc387cf1a2f8c0e033ba7ffaf2bed5b5f1a213378064a20d" "sha256": "c3f6e251067e009bf46ad5f2a704312652a41d151a152d00944fd59412579555"
}, },
{ {
"type": "archive", "type": "archive",

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
images/gnome/nightly-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
images/gnome/nightly-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

BIN
images/gnome/nightly-64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -493,7 +493,7 @@ class AppDelegate: NSObject,
self.menuMoveSplitDividerDown?.setImageIfDesired(systemSymbolName: "arrow.down.to.line") self.menuMoveSplitDividerDown?.setImageIfDesired(systemSymbolName: "arrow.down.to.line")
self.menuMoveSplitDividerLeft?.setImageIfDesired(systemSymbolName: "arrow.left.to.line") self.menuMoveSplitDividerLeft?.setImageIfDesired(systemSymbolName: "arrow.left.to.line")
self.menuMoveSplitDividerRight?.setImageIfDesired(systemSymbolName: "arrow.right.to.line") self.menuMoveSplitDividerRight?.setImageIfDesired(systemSymbolName: "arrow.right.to.line")
self.menuFloatOnTop?.setImageIfDesired(systemSymbolName: "square.3.layers.3d.top.filled") self.menuFloatOnTop?.setImageIfDesired(systemSymbolName: "square.filled.on.square")
} }
/// Sync all of our menu item keyboard shortcuts with the Ghostty configuration. /// Sync all of our menu item keyboard shortcuts with the Ghostty configuration.

View File

@ -2,14 +2,15 @@
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
# This file is distributed under the same license as the com.mitchellh.ghostty package. # This file is distributed under the same license as the com.mitchellh.ghostty package.
# Satrio Bayu Aji <halosatrio@gmail.com>, 2025. # Satrio Bayu Aji <halosatrio@gmail.com>, 2025.
# Mikail Muzakki <mikailmmuzakki@gmail.com>, 2025.
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n" "Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-03-20 15:19+0700\n" "PO-Revision-Date: 2025-08-01 10:15+0700\n"
"Last-Translator: Satrio Bayu Aji <halosatrio@gmail.com>\n" "Last-Translator: Mikail Muzakki <mikailmmuzakki@gmail.com>\n"
"Language-Team: Indonesian <translation-team-id@lists.sourceforge.net>\n" "Language-Team: Indonesian <translation-team-id@lists.sourceforge.net>\n"
"Language: id\n" "Language: id\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -86,7 +87,7 @@ msgstr "Belah kanan"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16 #: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…" msgid "Execute a command…"
msgstr "" msgstr "Eksekusi perintah…"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
@ -102,7 +103,7 @@ msgstr "Tempel"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73
msgid "Clear" msgid "Clear"
msgstr "Hapus" msgstr "Bersihkan"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78
@ -159,7 +160,7 @@ msgstr "Buka konfigurasi"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette" msgid "Command Palette"
msgstr "" msgstr "Palet perintah"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector" msgid "Terminal Inspector"
@ -195,7 +196,7 @@ msgstr ""
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10
msgid "Deny" msgid "Deny"
msgstr "Menyangkal" msgstr "Tolak"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11
@ -207,12 +208,12 @@ msgstr "Izinkan"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split" msgid "Remember choice for this split"
msgstr "" msgstr "Ingat pilihan untuk belahan ini"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again" msgid "Reload configuration to show this prompt again"
msgstr "" msgstr "Muat ulang konfigurasi untuk menampilkan pesan ini lagi"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@ -225,7 +226,7 @@ msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 #: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6
msgid "Warning: Potentially Unsafe Paste" msgid "Warning: Potentially Unsafe Paste"
msgstr "Peringatan: Tempelan yang berpotensi tidak aman" msgstr "Peringatan: Tempelan berpotensi tidak aman"
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 #: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7
msgid "" msgid ""
@ -277,15 +278,15 @@ msgstr "Disalin ke papan klip"
#: src/apprt/gtk/Surface.zig:1268 #: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard" msgid "Cleared clipboard"
msgstr "" msgstr "Papan klip dibersihkan"
#: src/apprt/gtk/Surface.zig:2525 #: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded" msgid "Command succeeded"
msgstr "" msgstr "Perintah berhasil"
#: src/apprt/gtk/Surface.zig:2527 #: src/apprt/gtk/Surface.zig:2527
msgid "Command failed" msgid "Command failed"
msgstr "" msgstr "Perintah gagal"
#: src/apprt/gtk/Window.zig:216 #: src/apprt/gtk/Window.zig:216
msgid "Main Menu" msgid "Main Menu"
@ -297,7 +298,7 @@ msgstr "Lihat tab terbuka"
#: src/apprt/gtk/Window.zig:266 #: src/apprt/gtk/Window.zig:266
msgid "New Split" msgid "New Split"
msgstr "" msgstr "Belahan baru"
#: src/apprt/gtk/Window.zig:329 #: src/apprt/gtk/Window.zig:329
msgid "" msgid ""

View File

@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n" "Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-07-09 16:11-0400\n" "PO-Revision-Date: 2025-08-03 20:42+0900\n"
"Last-Translator: Hojin You <dev.hojin@gmail.com>\n" "Last-Translator: Jinhyeok Lee <zenyr@zenyr.com>\n"
"Language-Team: Korean <translation-team-ko@googlegroups.com>\n" "Language-Team: Korean <translation-team-ko@googlegroups.com>\n"
"Language: ko\n" "Language: ko\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -46,7 +46,7 @@ msgid ""
"One or more configuration errors were found. Please review the errors below, " "One or more configuration errors were found. Please review the errors below, "
"and either reload your configuration or ignore these errors." "and either reload your configuration or ignore these errors."
msgstr "" msgstr ""
"설정에 하나 이상의 문제가 발견되었습니다. 아래 오류()들을 확인한 후 설정을 " "설정에 하나 이상의 문제가 발견되었습니다. 아래 오류를 확인한 후 설정을 "
"다시 불러오거나 무시하세요." "다시 불러오거나 무시하세요."
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9
@ -232,9 +232,7 @@ msgstr "경고: 잠재적으로 안전하지 않은 붙여넣기"
msgid "" msgid ""
"Pasting this text into the terminal may be dangerous as it looks like some " "Pasting this text into the terminal may be dangerous as it looks like some "
"commands may be executed." "commands may be executed."
msgstr "" msgstr "이 텍스트를 터미널에 붙여넣으면 일부 명령이 실행될 수 있어 위험할 수 있습니다."
"이 텍스트를 터미널에 붙여넣는 것은 위험할 수 있습니다. 일부 명령이 실행될 수 "
"있는 것으로 보입니다."
#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2531 #: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2531
msgid "Close" msgid "Close"
@ -278,15 +276,15 @@ msgstr "클립보드에 복사됨"
#: src/apprt/gtk/Surface.zig:1268 #: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard" msgid "Cleared clipboard"
msgstr "" msgstr "클립보드 지워짐"
#: src/apprt/gtk/Surface.zig:2525 #: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded" msgid "Command succeeded"
msgstr "" msgstr "명령 성공"
#: src/apprt/gtk/Surface.zig:2527 #: src/apprt/gtk/Surface.zig:2527
msgid "Command failed" msgid "Command failed"
msgstr "" msgstr "명령 실패"
#: src/apprt/gtk/Window.zig:216 #: src/apprt/gtk/Window.zig:216
msgid "Main Menu" msgid "Main Menu"

View File

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const build_config = @import("../build_config.zig");
const assert = std.debug.assert; const assert = std.debug.assert;
const apprt = @import("../apprt.zig"); const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig"); const configpkg = @import("../config.zig");
@ -533,6 +534,16 @@ pub const SizeLimit = extern struct {
pub const InitialSize = extern struct { pub const InitialSize = extern struct {
width: u32, width: u32,
height: u32, height: u32,
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
InitialSize,
.{ .name = "GhosttyApprtInitialSize" },
),
.none => void,
};
}; };
pub const CellSize = extern struct { pub const CellSize = extern struct {

View File

@ -37,20 +37,21 @@ pub const blueprints: []const Blueprint = &.{
.{ .major = 1, .minor = 4, .name = "clipboard-confirmation-dialog" }, .{ .major = 1, .minor = 4, .name = "clipboard-confirmation-dialog" },
.{ .major = 1, .minor = 2, .name = "close-confirmation-dialog" }, .{ .major = 1, .minor = 2, .name = "close-confirmation-dialog" },
.{ .major = 1, .minor = 2, .name = "config-errors-dialog" }, .{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
.{ .major = 1, .minor = 2, .name = "debug-warning" },
.{ .major = 1, .minor = 3, .name = "debug-warning" },
.{ .major = 1, .minor = 2, .name = "resize-overlay" }, .{ .major = 1, .minor = 2, .name = "resize-overlay" },
.{ .major = 1, .minor = 2, .name = "surface" }, .{ .major = 1, .minor = 2, .name = "surface" },
.{ .major = 1, .minor = 3, .name = "surface-child-exited" }, .{ .major = 1, .minor = 3, .name = "surface-child-exited" },
.{ .major = 1, .minor = 5, .name = "tab" },
.{ .major = 1, .minor = 5, .name = "window" }, .{ .major = 1, .minor = 5, .name = "window" },
.{ .major = 1, .minor = 2, .name = "debug-warning" },
.{ .major = 1, .minor = 3, .name = "debug-warning" },
}; };
/// CSS files in css_path /// CSS files in css_path
pub const css = [_][]const u8{ pub const css = [_][]const u8{
"style.css", "style.css",
// "style-dark.css", "style-dark.css",
// "style-hc.css", "style-hc.css",
// "style-hc-dark.css", "style-hc-dark.css",
}; };
pub const Blueprint = struct { pub const Blueprint = struct {

View File

@ -22,6 +22,7 @@ const xev = @import("../../../global.zig").xev;
const CoreConfig = configpkg.Config; const CoreConfig = configpkg.Config;
const CoreSurface = @import("../../../Surface.zig"); const CoreSurface = @import("../../../Surface.zig");
const ext = @import("../ext.zig");
const adw_version = @import("../adw_version.zig"); const adw_version = @import("../adw_version.zig");
const gtk_version = @import("../gtk_version.zig"); const gtk_version = @import("../gtk_version.zig");
const winprotopkg = @import("../winproto.zig"); const winprotopkg = @import("../winproto.zig");
@ -128,6 +129,15 @@ pub const Application = extern struct {
/// outside of our own lifecycle and that's okay. /// outside of our own lifecycle and that's okay.
config_errors_dialog: WeakRef(ConfigErrorsDialog) = .{}, config_errors_dialog: WeakRef(ConfigErrorsDialog) = .{},
/// glib source for our signal handler.
signal_source: ?c_uint = null,
/// CSS Provider for any styles based on Ghostty configuration values.
css_provider: *gtk.CssProvider,
/// Providers for loading custom stylesheets defined by user
custom_css_providers: std.ArrayListUnmanaged(*gtk.CssProvider) = .empty,
pub var offset: c_int = 0; pub var offset: c_int = 0;
}; };
@ -263,6 +273,16 @@ pub const Application = extern struct {
const config_obj: *Config = try .new(alloc, &config); const config_obj: *Config = try .new(alloc, &config);
errdefer config_obj.unref(); errdefer config_obj.unref();
// Internally, GTK ensures that only one instance of this provider
// exists in the provider list for the display.
const css_provider = gtk.CssProvider.new();
gtk.StyleContext.addProviderForDisplay(
display,
css_provider.as(gtk.StyleProvider),
gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 3,
);
errdefer css_provider.unref();
// Initialize the app. // Initialize the app.
const self = gobject.ext.newInstance(Self, .{ const self = gobject.ext.newInstance(Self, .{
.application_id = app_id.ptr, .application_id = app_id.ptr,
@ -283,8 +303,22 @@ pub const Application = extern struct {
.core_app = core_app, .core_app = core_app,
.config = config_obj, .config = config_obj,
.winproto = wp, .winproto = wp,
.css_provider = css_provider,
.custom_css_providers = .empty,
}; };
// Signals
_ = gobject.Object.signals.notify.connect(
self,
*Self,
propConfig,
self,
.{ .detail = "config" },
);
// Trigger initial config changes
self.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec);
return self; return self;
} }
@ -299,6 +333,22 @@ pub const Application = extern struct {
priv.config.unref(); priv.config.unref();
priv.winproto.deinit(alloc); priv.winproto.deinit(alloc);
if (priv.transient_cgroup_base) |base| alloc.free(base); if (priv.transient_cgroup_base) |base| alloc.free(base);
if (gdk.Display.getDefault()) |display| {
gtk.StyleContext.removeProviderForDisplay(
display,
priv.css_provider.as(gtk.StyleProvider),
);
for (priv.custom_css_providers.items) |provider| {
gtk.StyleContext.removeProviderForDisplay(
display,
provider.as(gtk.StyleProvider),
);
}
}
priv.css_provider.unref();
for (priv.custom_css_providers.items) |provider| provider.unref();
priv.custom_css_providers.deinit(alloc);
} }
/// The global allocator that all other classes should use by /// The global allocator that all other classes should use by
@ -493,10 +543,20 @@ pub const Application = extern struct {
value.config, value.config,
), ),
.desktop_notification => Action.desktopNotification(self, target, value),
.goto_tab => return Action.gotoTab(target, value),
.initial_size => return Action.initialSize(target, value),
.mouse_over_link => Action.mouseOverLink(target, value), .mouse_over_link => Action.mouseOverLink(target, value),
.mouse_shape => Action.mouseShape(target, value), .mouse_shape => Action.mouseShape(target, value),
.mouse_visibility => Action.mouseVisibility(target, value), .mouse_visibility => Action.mouseVisibility(target, value),
.move_tab => return Action.moveTab(target, value),
.new_tab => return Action.newTab(target),
.new_window => try Action.newWindow( .new_window => try Action.newWindow(
self, self,
switch (target) { switch (target) {
@ -505,14 +565,20 @@ pub const Application = extern struct {
}, },
), ),
.open_config => return Action.openConfig(self),
.open_url => Action.openUrl(self, value),
.pwd => Action.pwd(target, value), .pwd => Action.pwd(target, value),
.present_terminal => return Action.presentTerminal(target),
.progress_report => return Action.progressReport(target, value),
.quit => self.quit(), .quit => self.quit(),
.quit_timer => try Action.quitTimer(self, value), .quit_timer => try Action.quitTimer(self, value),
.progress_report => return Action.progressReport(target, value),
.reload_config => try Action.reloadConfig(self, target, value), .reload_config => try Action.reloadConfig(self, target, value),
.render => Action.render(target), .render => Action.render(target),
@ -525,30 +591,31 @@ pub const Application = extern struct {
.show_gtk_inspector => Action.showGtkInspector(), .show_gtk_inspector => Action.showGtkInspector(),
.size_limit => return Action.sizeLimit(target, value),
.toggle_maximize => Action.toggleMaximize(target), .toggle_maximize => Action.toggleMaximize(target),
.toggle_fullscreen => Action.toggleFullscreen(target), .toggle_fullscreen => Action.toggleFullscreen(target),
.toggle_quick_terminal => return Action.toggleQuickTerminal(self),
.toggle_tab_overview => return Action.toggleTabOverview(target),
.toggle_window_decorations => return Action.toggleWindowDecorations(target),
// Unimplemented but todo on gtk-ng branch // Unimplemented but todo on gtk-ng branch
.new_tab, .prompt_title,
.goto_tab, .toggle_command_palette,
.move_tab, .inspector,
// TODO: splits
.new_split, .new_split,
.resize_split, .resize_split,
.equalize_splits, .equalize_splits,
.goto_split, .goto_split,
.open_config,
.inspector,
.desktop_notification,
.present_terminal,
.initial_size,
.size_limit,
.toggle_tab_overview,
.toggle_split_zoom, .toggle_split_zoom,
.toggle_window_decorations, => {
.prompt_title, log.warn("unimplemented action={}", .{action});
.toggle_quick_terminal, return false;
.toggle_command_palette, },
.open_url,
// Unimplemented
.secure_input,
.close_all_windows, .close_all_windows,
.float_window, .float_window,
.toggle_visibility, .toggle_visibility,
@ -565,13 +632,6 @@ pub const Application = extern struct {
log.warn("unimplemented action={}", .{action}); log.warn("unimplemented action={}", .{action});
return false; return false;
}, },
// Unimplemented
.secure_input,
=> {
log.warn("unimplemented action={}", .{action});
return false;
},
} }
// Assume it was handled. The unhandled case must be explicit // Assume it was handled. The unhandled case must be explicit
@ -645,6 +705,155 @@ pub const Application = extern struct {
} }
} }
fn loadRuntimeCss(
self: *Self,
) Allocator.Error!void {
const alloc = self.allocator();
var buf: std.ArrayListUnmanaged(u8) = .empty;
defer buf.deinit(alloc);
const writer = buf.writer(alloc);
const config = self.private().config.get();
const window_theme = config.@"window-theme";
const unfocused_fill: CoreConfig.Color = config.@"unfocused-split-fill" orelse config.background;
const headerbar_background = config.@"window-titlebar-background" orelse config.background;
const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
try writer.print(
\\widget.unfocused-split {{
\\ opacity: {d:.2};
\\ background-color: rgb({d},{d},{d});
\\}}
, .{
1.0 - config.@"unfocused-split-opacity",
unfocused_fill.r,
unfocused_fill.g,
unfocused_fill.b,
});
if (config.@"split-divider-color") |color| {
try writer.print(
\\.terminal-window .notebook separator {{
\\ color: rgb({[r]d},{[g]d},{[b]d});
\\ background: rgb({[r]d},{[g]d},{[b]d});
\\}}
, .{
.r = color.r,
.g = color.g,
.b = color.b,
});
}
if (config.@"window-title-font-family") |font_family| {
try writer.print(
\\.window headerbar {{
\\ font-family: "{[font_family]s}";
\\}}
, .{ .font_family = font_family });
}
switch (window_theme) {
.ghostty => try writer.print(
\\:root {{
\\ --ghostty-fg: rgb({d},{d},{d});
\\ --ghostty-bg: rgb({d},{d},{d});
\\ --headerbar-fg-color: var(--ghostty-fg);
\\ --headerbar-bg-color: var(--ghostty-bg);
\\ --headerbar-backdrop-color: oklab(from var(--headerbar-bg-color) calc(l * 0.9) a b / alpha);
\\ --overview-fg-color: var(--ghostty-fg);
\\ --overview-bg-color: var(--ghostty-bg);
\\ --popover-fg-color: var(--ghostty-fg);
\\ --popover-bg-color: var(--ghostty-bg);
\\ --window-fg-color: var(--ghostty-fg);
\\ --window-bg-color: var(--ghostty-bg);
\\}}
\\windowhandle {{
\\ background-color: var(--headerbar-bg-color);
\\ color: var(--headerbar-fg-color);
\\}}
\\windowhandle:backdrop {{
\\ background-color: var(--headerbar-backdrop-color);
\\}}
, .{
headerbar_foreground.r,
headerbar_foreground.g,
headerbar_foreground.b,
headerbar_background.r,
headerbar_background.g,
headerbar_background.b,
}),
else => {},
}
const data = try alloc.dupeZ(u8, buf.items);
defer alloc.free(data);
// Clears any previously loaded CSS from this provider
loadCssProviderFromData(
self.private().css_provider,
data,
);
}
fn loadCustomCss(self: *Self) !void {
const priv = self.private();
const alloc = self.allocator();
const display = gdk.Display.getDefault() orelse {
log.warn("unable to get display", .{});
return;
};
// unload the previously loaded style providers
for (priv.custom_css_providers.items) |provider| {
gtk.StyleContext.removeProviderForDisplay(
display,
provider.as(gtk.StyleProvider),
);
provider.unref();
}
priv.custom_css_providers.clearRetainingCapacity();
const config = priv.config.getMut();
for (config.@"gtk-custom-css".value.items) |p| {
const path, const optional = switch (p) {
.optional => |path| .{ path, true },
.required => |path| .{ path, false },
};
const file = std.fs.openFileAbsolute(path, .{}) catch |err| {
if (err != error.FileNotFound or !optional) {
log.warn(
"error opening gtk-custom-css file {s}: {}",
.{ path, err },
);
}
continue;
};
defer file.close();
log.info("loading gtk-custom-css path={s}", .{path});
const contents = try file.reader().readAllAlloc(
alloc,
5 * 1024 * 1024, // 5MB,
);
defer alloc.free(contents);
const data = try alloc.dupeZ(u8, contents);
defer alloc.free(data);
const provider = gtk.CssProvider.new();
errdefer provider.unref();
try priv.custom_css_providers.append(alloc, provider);
loadCssProviderFromData(provider, data);
gtk.StyleContext.addProviderForDisplay(
display,
provider.as(gtk.StyleProvider),
gtk.STYLE_PROVIDER_PRIORITY_USER,
);
}
}
//--------------------------------------------------------------- //---------------------------------------------------------------
// Properties // Properties
@ -670,6 +879,28 @@ pub const Application = extern struct {
self.showConfigErrorsDialog(); self.showConfigErrorsDialog();
} }
fn propConfig(
_: *Application,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
// Load our runtime and custom CSS. If this fails then our window is
// just stuck with the old CSS but we don't want to fail the entire
// config change operation.
self.loadRuntimeCss() catch |err| switch (err) {
error.OutOfMemory => log.warn(
"out of memory loading runtime CSS, no runtime CSS applied",
.{},
),
};
self.loadCustomCss() catch |err| {
log.warn(
"failed to load custom CSS, no custom CSS applied, err={}",
.{err},
);
};
}
//--------------------------------------------------------------- //---------------------------------------------------------------
// Libghostty Callbacks // Libghostty Callbacks
@ -698,6 +929,9 @@ pub const Application = extern struct {
// Setup our style manager (light/dark mode) // Setup our style manager (light/dark mode)
self.startupStyleManager(); self.startupStyleManager();
// Setup some signal handlers
self.startupSignals();
// Setup our action map // Setup our action map
self.startupActionMap(); self.startupActionMap();
@ -786,6 +1020,17 @@ pub const Application = extern struct {
); );
} }
/// Setup signal handlers
fn startupSignals(self: *Self) void {
const priv = self.private();
assert(priv.signal_source == null);
priv.signal_source = glib.unixSignalAdd(
std.posix.SIG.USR2,
handleSigusr2,
self,
);
}
/// Setup our action map. /// Setup our action map.
fn startupActionMap(self: *Self) void { fn startupActionMap(self: *Self) void {
const t_variant_type = glib.ext.VariantType.newFor(u64); const t_variant_type = glib.ext.VariantType.newFor(u64);
@ -804,6 +1049,8 @@ pub const Application = extern struct {
const actions = .{ const actions = .{
.{ "new-window", actionNewWindow, null }, .{ "new-window", actionNewWindow, null },
.{ "new-window-command", actionNewWindow, as_variant_type }, .{ "new-window-command", actionNewWindow, as_variant_type },
.{ "open-config", actionOpenConfig, null },
.{ "present-surface", actionPresentSurface, t_variant_type },
.{ "quit", actionQuit, null }, .{ "quit", actionQuit, null },
.{ "reload-config", actionReloadConfig, null }, .{ "reload-config", actionReloadConfig, null },
}; };
@ -914,6 +1161,12 @@ pub const Application = extern struct {
diag.close(); diag.close();
diag.unref(); // strong ref from get() diag.unref(); // strong ref from get()
} }
if (priv.signal_source) |v| {
if (glib.Source.remove(v) == 0) {
log.warn("unable to remove signal source", .{});
}
priv.signal_source = null;
}
gobject.Object.virtual_methods.dispose.call( gobject.Object.virtual_methods.dispose.call(
Class.parent, Class.parent,
@ -932,6 +1185,26 @@ pub const Application = extern struct {
//--------------------------------------------------------------- //---------------------------------------------------------------
// Signal Handlers // Signal Handlers
/// SIGUSR2 signal handler via g_unix_signal_add
fn handleSigusr2(ud: ?*anyopaque) callconv(.c) c_int {
const self: *Self = @ptrCast(@alignCast(ud orelse
return @intFromBool(glib.SOURCE_CONTINUE)));
log.info("received SIGUSR2, reloading configuration", .{});
Action.reloadConfig(
self,
.app,
.{},
) catch |err| {
// If we fail to reload the configuration, then we want the
// user to know it. For now we log but we should show another
// GUI.
log.warn("error reloading config: {}", .{err});
};
return @intFromBool(glib.SOURCE_CONTINUE);
}
fn handleCloseConfirmation( fn handleCloseConfirmation(
_: *CloseConfirmationDialog, _: *CloseConfirmationDialog,
self: *Self, self: *Self,
@ -1098,6 +1371,58 @@ pub const Application = extern struct {
}, .{ .forever = {} }); }, .{ .forever = {} });
} }
pub fn actionOpenConfig(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
_ = self.core().mailbox.push(.open_config, .forever);
}
fn actionPresentSurface(
_: *gio.SimpleAction,
parameter_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
const parameter = parameter_ orelse return;
const t = glib.ext.VariantType.newFor(u64);
defer glib.VariantType.free(t);
// Make sure that we've receiived a u64 from the system.
if (glib.Variant.isOfType(parameter, t) == 0) {
return;
}
// Convert that u64 to pointer to a core surface. A value of zero
// means that there was no target surface for the notification so
// we don't focus any surface.
//
// This is admittedly SUPER SUS and we should instead do what we
// do on macOS which is generate a UUID per surface and then pass
// that around. But, we do validate the pointer below so at worst
// this may result in focusing the wrong surface if the pointer was
// reused for a surface.
const ptr_int = parameter.getUint64();
if (ptr_int == 0) return;
const surface: *CoreSurface = @ptrFromInt(ptr_int);
// Send a message through the core app mailbox rather than presenting the
// surface directly so that it can validate that the surface pointer is
// valid. We could get an invalid pointer if a desktop notification outlives
// a Ghostty instance and a new one starts up, or there are multiple Ghostty
// instances running.
_ = self.core().mailbox.push(
.{
.surface_message = .{
.surface = surface,
.message = .present_surface,
},
},
.forever,
);
}
//---------------------------------------------------------------- //----------------------------------------------------------------
// Boilerplate/Noise // Boilerplate/Noise
@ -1171,6 +1496,91 @@ const Action = struct {
} }
} }
pub fn desktopNotification(
self: *Application,
target: apprt.Target,
n: apprt.action.DesktopNotification,
) void {
// TODO: We should move the surface target to a function call
// on Surface and emit a signal that embedders can connect to. This
// will let us handle notifications differently depending on where
// a surface is presented. At the time of writing this, we always
// want to show the notification AND the logic below was directly
// ported from "legacy" GTK so this is fine, but I want to leave this
// note so we can do it one day.
// Set a default title if we don't already have one
const t = switch (n.title.len) {
0 => "Ghostty",
else => n.title,
};
const notification = gio.Notification.new(t);
defer notification.unref();
notification.setBody(n.body);
const icon = gio.ThemedIcon.new("com.mitchellh.ghostty");
defer icon.unref();
notification.setIcon(icon.as(gio.Icon));
const pointer = glib.Variant.newUint64(switch (target) {
.app => 0,
.surface => |v| @intFromPtr(v),
});
notification.setDefaultActionAndTargetValue(
"app.present-surface",
pointer,
);
// We set the notification ID to the body content. If the content is the
// same, this notification may replace a previous notification
const gio_app = self.as(gio.Application);
gio_app.sendNotification(n.body, notification);
}
pub fn gotoTab(
target: apprt.Target,
tab: apprt.action.GotoTab,
) bool {
switch (target) {
.app => return false,
.surface => |core| {
const surface = core.rt_surface.surface;
const window = ext.getAncestor(
Window,
surface.as(gtk.Widget),
) orelse {
log.warn("surface is not in a window, ignoring new_tab", .{});
return false;
};
return window.selectTab(switch (tab) {
.previous => .previous,
.next => .next,
.last => .last,
else => .{ .n = @intCast(@intFromEnum(tab)) },
});
},
}
}
pub fn initialSize(
target: apprt.Target,
value: apprt.action.InitialSize,
) bool {
switch (target) {
.app => return false,
.surface => |core| {
const surface = core.rt_surface.surface;
surface.setDefaultSize(.{
.width = value.width,
.height = value.height,
});
return true;
},
}
}
pub fn mouseOverLink( pub fn mouseOverLink(
target: apprt.Target, target: apprt.Target,
value: apprt.action.MouseOverLink, value: apprt.action.MouseOverLink,
@ -1229,12 +1639,68 @@ const Action = struct {
} }
} }
pub fn moveTab(
target: apprt.Target,
value: apprt.action.MoveTab,
) bool {
switch (target) {
.app => return false,
.surface => |core| {
const surface = core.rt_surface.surface;
const window = ext.getAncestor(
Window,
surface.as(gtk.Widget),
) orelse {
log.warn("surface is not in a window, ignoring new_tab", .{});
return false;
};
return window.moveTab(
surface,
@intCast(value.amount),
);
},
}
}
pub fn newTab(target: apprt.Target) bool {
switch (target) {
.app => {
log.warn("new tab to app is unexpected", .{});
return false;
},
.surface => |core| {
// Get the window ancestor of the surface. Surfaces shouldn't
// be aware they might be in windows but at the app level we
// can do this.
const surface = core.rt_surface.surface;
const window = ext.getAncestor(
Window,
surface.as(gtk.Widget),
) orelse {
log.warn("surface is not in a window, ignoring new_tab", .{});
return false;
};
window.newTab(core);
return true;
},
}
}
pub fn newWindow( pub fn newWindow(
self: *Application, self: *Application,
parent: ?*CoreSurface, parent: ?*CoreSurface,
) !void { ) !void {
const win = Window.new(self, parent); const win = Window.new(self);
initAndShowWindow(self, win, parent);
}
fn initAndShowWindow(
self: *Application,
win: *Window,
parent: ?*CoreSurface,
) void {
// Setup a binding so that whenever our config changes so does the // 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 // window. There's never a time when the window config should be out
// of sync with the application config. // of sync with the application config.
@ -1246,10 +1712,44 @@ const Action = struct {
.{}, .{},
); );
// Create a new tab
win.newTab(parent);
// Show the window // Show the window
gtk.Window.present(win.as(gtk.Window)); gtk.Window.present(win.as(gtk.Window));
} }
pub fn openConfig(self: *Application) bool {
// Get the config file path
const alloc = self.allocator();
const path = configpkg.edit.openPath(alloc) catch |err| {
log.warn("error getting config file path: {}", .{err});
return false;
};
defer alloc.free(path);
// Open it using openURL. "path" isn't actually a URL but
// at the time of writing that works just fine for GTK.
openUrl(self, .{ .kind = .text, .url = path });
return true;
}
pub fn openUrl(
self: *Application,
value: apprt.action.OpenUrl,
) void {
// TODO: use https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html
// Fallback to the minimal cross-platform way of opening a URL.
// This is always a safe fallback and enables for example Windows
// to open URLs (GTK on Windows via WSL is a thing).
internal_os.open(
self.allocator(),
value.kind,
value.url,
) catch |err| log.warn("unable to open url: {}", .{err});
}
pub fn pwd( pub fn pwd(
target: apprt.Target, target: apprt.Target,
value: apprt.action.Pwd, value: apprt.action.Pwd,
@ -1278,6 +1778,18 @@ const Action = struct {
} }
} }
pub fn presentTerminal(
target: apprt.Target,
) bool {
return switch (target) {
.app => false,
.surface => |v| surface: {
v.rt_surface.surface.present();
break :surface true;
},
};
}
pub fn progressReport( pub fn progressReport(
target: apprt.Target, target: apprt.Target,
value: terminal.osc.Command.ProgressReport, value: terminal.osc.Command.ProgressReport,
@ -1378,6 +1890,26 @@ const Action = struct {
gtk.Window.setInteractiveDebugging(@intFromBool(true)); gtk.Window.setInteractiveDebugging(@intFromBool(true));
} }
pub fn sizeLimit(
target: apprt.Target,
value: apprt.action.SizeLimit,
) bool {
switch (target) {
.app => return false,
.surface => |core| {
// Note: we ignore the max size currently because we have
// no mechanism to enforce it.
const surface = core.rt_surface.surface;
surface.setMinSize(.{
.width = value.min_width,
.height = value.min_height,
});
return true;
},
}
}
pub fn toggleFullscreen(target: apprt.Target) void { pub fn toggleFullscreen(target: apprt.Target) void {
switch (target) { switch (target) {
.app => {}, .app => {},
@ -1385,12 +1917,92 @@ const Action = struct {
} }
} }
pub fn toggleQuickTerminal(self: *Application) bool {
// If we already have a quick terminal window, we just toggle the
// visibility of it.
if (getQuickTerminalWindow()) |win| {
win.toggleVisibility();
return true;
}
// If we don't support quick terminals then we do nothing.
const priv = self.private();
if (!priv.winproto.supportsQuickTerminal()) return false;
// Create our new window as a quick terminal
const win = gobject.ext.newInstance(Window, .{
.application = self,
.@"quick-terminal" = true,
});
assert(win.isQuickTerminal());
initAndShowWindow(self, win, null);
return true;
}
fn getQuickTerminalWindow() ?*Window {
// Find a quick terminal window.
const list = gtk.Window.listToplevels();
defer list.free();
if (ext.listFind(gtk.Window, list, struct {
fn find(gtk_win: *gtk.Window) bool {
const win = gobject.ext.cast(
Window,
gtk_win,
) orelse return false;
return win.isQuickTerminal();
}
}.find)) |w| return gobject.ext.cast(
Window,
w,
).?;
return null;
}
pub fn toggleMaximize(target: apprt.Target) void { pub fn toggleMaximize(target: apprt.Target) void {
switch (target) { switch (target) {
.app => {}, .app => {},
.surface => |v| v.rt_surface.surface.toggleMaximize(), .surface => |v| v.rt_surface.surface.toggleMaximize(),
} }
} }
pub fn toggleTabOverview(target: apprt.Target) bool {
switch (target) {
.app => return false,
.surface => |core| {
const surface = core.rt_surface.surface;
const window = ext.getAncestor(
Window,
surface.as(gtk.Widget),
) orelse {
log.warn("surface is not in a window, ignoring new_tab", .{});
return false;
};
window.toggleTabOverview();
return true;
},
}
}
pub fn toggleWindowDecorations(target: apprt.Target) bool {
switch (target) {
.app => return false,
.surface => |core| {
const surface = core.rt_surface.surface;
const window = ext.getAncestor(
Window,
surface.as(gtk.Widget),
) orelse {
log.warn("surface is not in a window, ignoring new_tab", .{});
return false;
};
window.toggleWindowDecorations();
return true;
},
}
}
}; };
/// This sets various GTK-related environment variables as necessary /// This sets various GTK-related environment variables as necessary
@ -1507,3 +2119,8 @@ fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c)
// Abusing integers to be enums and booleans is a terrible idea, C. // Abusing integers to be enums and booleans is a terrible idea, C.
return if (window.isActive() != 0) 0 else -1; return if (window.isActive() != 0) 0 else -1;
} }
fn loadCssProviderFromData(provider: *gtk.CssProvider, data: [:0]const u8) void {
assert(gtk_version.runtimeAtLeast(4, 12, 0));
provider.loadFromString(data);
}

View File

@ -57,6 +57,17 @@ pub const CloseConfirmationDialog = extern struct {
void, void,
); );
}; };
pub const cancel = struct {
pub const name = "cancel";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
}; };
const Private = struct { const Private = struct {
@ -72,14 +83,15 @@ pub const CloseConfirmationDialog = extern struct {
fn init(self: *Self, _: *Class) callconv(.C) void { fn init(self: *Self, _: *Class) callconv(.C) void {
gtk.Widget.initTemplate(self.as(gtk.Widget)); gtk.Widget.initTemplate(self.as(gtk.Widget));
}
pub fn present(self: *Self, parent: ?*gtk.Widget) void {
// Setup our title/body text. // Setup our title/body text.
const priv = self.private(); const priv = self.private();
self.as(Dialog.Parent).setHeading(priv.target.title()); self.as(Dialog.Parent).setHeading(priv.target.title());
self.as(Dialog.Parent).setBody(priv.target.body()); self.as(Dialog.Parent).setBody(priv.target.body());
}
pub fn present(self: *Self, parent: ?*gtk.Widget) void { // Show it
self.as(Dialog).present(parent); self.as(Dialog).present(parent);
} }
@ -91,13 +103,21 @@ pub const CloseConfirmationDialog = extern struct {
self: *Self, self: *Self,
response_id: [*:0]const u8, response_id: [*:0]const u8,
) callconv(.C) void { ) callconv(.C) void {
if (std.mem.orderZ(u8, response_id, "close") != .eq) return; if (std.mem.orderZ(u8, response_id, "close") == .eq) {
signals.@"close-request".impl.emit( signals.@"close-request".impl.emit(
self, self,
null, null,
.{}, .{},
null, null,
); );
} else {
signals.cancel.impl.emit(
self,
null,
.{},
null,
);
}
} }
fn dispose(self: *Self) callconv(.C) void { fn dispose(self: *Self) callconv(.C) void {
@ -141,6 +161,7 @@ pub const CloseConfirmationDialog = extern struct {
// Signals // Signals
signals.@"close-request".impl.register(.{}); signals.@"close-request".impl.register(.{});
signals.cancel.impl.register(.{});
// Virtual methods // Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose); gobject.Object.virtual_methods.dispose.implement(class, &dispose);
@ -158,11 +179,13 @@ pub const CloseConfirmationDialog = extern struct {
/// together into one struct that is the sole source of truth. /// together into one struct that is the sole source of truth.
pub const Target = enum(c_int) { pub const Target = enum(c_int) {
app, app,
tab,
window, window,
pub fn title(self: Target) [*:0]const u8 { pub fn title(self: Target) [*:0]const u8 {
return switch (self) { return switch (self) {
.app => i18n._("Quit Ghostty?"), .app => i18n._("Quit Ghostty?"),
.tab => i18n._("Close Tab?"),
.window => i18n._("Close Window?"), .window => i18n._("Close Window?"),
}; };
} }
@ -170,6 +193,7 @@ pub const Target = enum(c_int) {
pub fn body(self: Target) [*:0]const u8 { pub fn body(self: Target) [*:0]const u8 {
return switch (self) { return switch (self) {
.app => i18n._("All terminal sessions will be terminated."), .app => i18n._("All terminal sessions will be terminated."),
.tab => i18n._("All terminal sessions in this tab will be terminated."),
.window => i18n._("All terminal sessions in this window will be terminated."), .window => i18n._("All terminal sessions in this window will be terminated."),
}; };
} }

View File

@ -16,6 +16,7 @@ const renderer = @import("../../../renderer.zig");
const terminal = @import("../../../terminal/main.zig"); const terminal = @import("../../../terminal/main.zig");
const CoreSurface = @import("../../../Surface.zig"); const CoreSurface = @import("../../../Surface.zig");
const gresource = @import("../build/gresource.zig"); const gresource = @import("../build/gresource.zig");
const ext = @import("../ext.zig");
const adw_version = @import("../adw_version.zig"); const adw_version = @import("../adw_version.zig");
const gtk_key = @import("../key.zig"); const gtk_key = @import("../key.zig");
const ApprtSurface = @import("../Surface.zig"); const ApprtSurface = @import("../Surface.zig");
@ -25,6 +26,7 @@ const Config = @import("config.zig").Config;
const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay; const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay;
const ChildExited = @import("surface_child_exited.zig").SurfaceChildExited; const ChildExited = @import("surface_child_exited.zig").SurfaceChildExited;
const ClipboardConfirmationDialog = @import("clipboard_confirmation_dialog.zig").ClipboardConfirmationDialog; const ClipboardConfirmationDialog = @import("clipboard_confirmation_dialog.zig").ClipboardConfirmationDialog;
const Window = @import("window.zig").Window;
const log = std.log.scoped(.gtk_ghostty_surface); const log = std.log.scoped(.gtk_ghostty_surface);
@ -75,6 +77,20 @@ pub const Surface = extern struct {
); );
}; };
pub const @"default-size" = struct {
pub const name = "default-size";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Size,
.{
.nick = "Default Size",
.blurb = "The default size of the window for this surface.",
.accessor = C.privateBoxedFieldAccessor("default_size"),
},
);
};
pub const @"font-size-request" = struct { pub const @"font-size-request" = struct {
pub const name = "font-size-request"; pub const name = "font-size-request";
const impl = gobject.ext.defineProperty( const impl = gobject.ext.defineProperty(
@ -109,6 +125,20 @@ pub const Surface = extern struct {
); );
}; };
pub const @"min-size" = struct {
pub const name = "min-size";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Size,
.{
.nick = "Minimum Size",
.blurb = "The minimum size of the surface.",
.accessor = C.privateBoxedFieldAccessor("min_size"),
},
);
};
pub const @"mouse-hidden" = struct { pub const @"mouse-hidden" = struct {
pub const name = "mouse-hidden"; pub const name = "mouse-hidden";
const impl = gobject.ext.defineProperty( const impl = gobject.ext.defineProperty(
@ -285,6 +315,31 @@ pub const Surface = extern struct {
); );
}; };
/// Emitted after the surface is initialized.
pub const init = struct {
pub const name = "init";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
/// Emitted when the focus wants to be brought to the top and
/// focused.
pub const @"present-request" = struct {
pub const name = "present-request";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
/// Emitted when this surface requests its container to toggle its /// Emitted when this surface requests its container to toggle its
/// fullscreen state. /// fullscreen state.
pub const @"toggle-fullscreen" = struct { pub const @"toggle-fullscreen" = struct {
@ -320,6 +375,13 @@ pub const Surface = extern struct {
/// if `Application.transient_cgroup_base` is set. /// if `Application.transient_cgroup_base` is set.
cgroup_path: ?[]const u8 = null, cgroup_path: ?[]const u8 = null,
/// The default size for a window that embeds this surface.
default_size: ?*Size = null,
/// The minimum size for this surface. Embedders enforce this,
/// not the surface itself.
min_size: ?*Size = null,
/// The requested font size. This only applies to initialization /// The requested font size. This only applies to initialization
/// and has no effect later. /// and has no effect later.
font_size_request: ?*font.face.DesiredSize = null, font_size_request: ?*font.face.DesiredSize = null,
@ -578,6 +640,17 @@ pub const Surface = extern struct {
return @intFromBool(glib.SOURCE_REMOVE); return @intFromBool(glib.SOURCE_REMOVE);
} }
/// Request that this terminal come to the front and become focused.
/// It is up to the embedding widget to react to this.
pub fn present(self: *Self) void {
signals.@"present-request".impl.emit(
self,
null,
.{},
null,
);
}
/// Key press event (press or release). /// Key press event (press or release).
/// ///
/// At a high level, we want to construct an `input.KeyEvent` and /// At a high level, we want to construct an `input.KeyEvent` and
@ -975,7 +1048,13 @@ pub const Surface = extern struct {
} }
pub fn getSize(self: *Self) apprt.SurfaceSize { pub fn getSize(self: *Self) apprt.SurfaceSize {
return self.private().size; const priv = self.private();
// By the time this is called, we should be in a widget tree.
// This should not be called before that. We ensure this by initializing
// the surface in `glareaResize`. This is VERY important because it
// avoids the pty having an incorrect initial size.
assert(priv.size.width >= 0 and priv.size.height >= 0);
return priv.size;
} }
pub fn getCursorPos(self: *Self) apprt.CursorPos { pub fn getCursorPos(self: *Self) apprt.CursorPos {
@ -983,8 +1062,6 @@ pub const Surface = extern struct {
} }
pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap { pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
_ = self;
const alloc = Application.default().allocator(); const alloc = Application.default().allocator();
var env = try internal_os.getEnvMap(alloc); var env = try internal_os.getEnvMap(alloc);
errdefer env.deinit(); errdefer env.deinit();
@ -1021,6 +1098,14 @@ pub const Surface = extern struct {
env.remove("GTK_PATH"); env.remove("GTK_PATH");
} }
// This is a hack because it ties ourselves (optionally) to the
// Window class. The right solution we should do is emit a signal
// here where the handler can modify our EnvMap, but boxing the
// EnvMap is a bit annoying so I'm punting it.
if (ext.getAncestor(Window, self.as(gtk.Widget))) |window| {
try window.winproto().addSubprocessEnv(&env);
}
return env; return env;
} }
@ -1050,6 +1135,13 @@ pub const Surface = extern struct {
); );
} }
/// Focus this surface. This properly focuses the input part of
/// our surface.
pub fn grabFocus(self: *Self) void {
const priv = self.private();
_ = priv.gl_area.as(gtk.Widget).grabFocus();
}
//--------------------------------------------------------------- //---------------------------------------------------------------
// Virtual Methods // Virtual Methods
@ -1065,12 +1157,7 @@ pub const Surface = extern struct {
priv.mouse_shape = .text; priv.mouse_shape = .text;
priv.mouse_hidden = false; priv.mouse_hidden = false;
priv.focused = true; priv.focused = true;
priv.size = .{ priv.size = .{ .width = 0, .height = 0 };
// Funky numbers on purpose so they stand out if for some reason
// our size doesn't get properly set.
.width = 111,
.height = 111,
};
// If our configuration is null then we get the configuration // If our configuration is null then we get the configuration
// from the application. // from the application.
@ -1150,10 +1237,18 @@ pub const Surface = extern struct {
glib.free(@constCast(@ptrCast(v))); glib.free(@constCast(@ptrCast(v)));
priv.mouse_hover_url = null; priv.mouse_hover_url = null;
} }
if (priv.default_size) |v| {
ext.boxedFree(Size, v);
priv.default_size = null;
}
if (priv.font_size_request) |v| { if (priv.font_size_request) |v| {
glib.ext.destroy(v); glib.ext.destroy(v);
priv.font_size_request = null; priv.font_size_request = null;
} }
if (priv.min_size) |v| {
ext.boxedFree(Size, v);
priv.min_size = null;
}
if (priv.pwd) |v| { if (priv.pwd) |v| {
glib.free(@constCast(@ptrCast(v))); glib.free(@constCast(@ptrCast(v)));
priv.pwd = null; priv.pwd = null;
@ -1191,6 +1286,50 @@ pub const Surface = extern struct {
self.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec); self.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec);
} }
/// Return the default size, if set.
pub fn getDefaultSize(self: *Self) ?*Size {
const priv = self.private();
return priv.default_size;
}
/// Set the default size for a window that contains this surface.
/// This is up to the embedding widget to respect this. Generally, only
/// the first surface in a window respects this.
pub fn setDefaultSize(self: *Self, size: Size) void {
const priv = self.private();
if (priv.default_size) |v| ext.boxedFree(
Size,
v,
);
priv.default_size = ext.boxedCopy(
Size,
&size,
);
self.as(gobject.Object).notifyByPspec(properties.@"default-size".impl.param_spec);
}
/// Return the min size, if set.
pub fn getMinSize(self: *Self) ?*Size {
const priv = self.private();
return priv.min_size;
}
/// Set the min size for a window that contains this surface.
/// This is up to the embedding widget to respect this. Generally, only
/// the first surface in a window respects this.
pub fn setMinSize(self: *Self, size: Size) void {
const priv = self.private();
if (priv.min_size) |v| ext.boxedFree(
Size,
v,
);
priv.min_size = ext.boxedCopy(
Size,
&size,
);
self.as(gobject.Object).notifyByPspec(properties.@"min-size".impl.param_spec);
}
fn propConfig( fn propConfig(
self: *Self, self: *Self,
_: *gobject.ParamSpec, _: *gobject.ParamSpec,
@ -1832,15 +1971,32 @@ pub const Surface = extern struct {
) callconv(.c) void { ) callconv(.c) void {
log.debug("realize", .{}); log.debug("realize", .{});
// Setup our core surface // If we already have an initialized surface then we notify it.
self.realizeSurface() catch |err| { // If we don't, we'll initialize it on the first resize so we have
log.warn("surface failed to realize err={}", .{err}); // our proper initial dimensions.
}; const priv = self.private();
if (priv.core_surface) |v| realize: {
// We need to make the context current so we can call GL functions.
// This is required for all surface operations.
priv.gl_area.makeCurrent();
if (priv.gl_area.getError()) |err| {
log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"});
log.warn("this error is usually due to a driver or gtk bug", .{});
log.warn("this is a common cause of this issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/4950", .{});
break :realize;
}
v.renderer.displayRealized() catch |err| {
log.warn("core displayRealized failed err={}", .{err});
break :realize;
};
self.redraw();
}
// Setup our input method. We do this here because this will // Setup our input method. We do this here because this will
// create a strong reference back to ourself and we want to be // create a strong reference back to ourself and we want to be
// able to release that in unrealize. // able to release that in unrealize.
const priv = self.private();
priv.im_context.as(gtk.IMContext).setClientWidget(self.as(gtk.Widget)); priv.im_context.as(gtk.IMContext).setClientWidget(self.as(gtk.Widget));
} }
@ -1942,49 +2098,24 @@ pub const Surface = extern struct {
// Setup our resize overlay if configured // Setup our resize overlay if configured
self.resizeOverlaySchedule(); self.resizeOverlaySchedule();
}
}
fn resizeOverlaySchedule(self: *Self) void { return;
const priv = self.private();
const surface = priv.core_surface orelse return;
// Only show the resize overlay if its enabled
const config = if (priv.config) |c| c.get() else return;
switch (config.@"resize-overlay") {
.always, .@"after-first" => {},
.never => return,
} }
// If we have resize overlays enabled, setup an idler // If we don't have a surface, then we initialize it.
// to show that. We do this in an idle tick because doing it self.initSurface() catch |err| {
// during the resize results in flickering. log.warn("surface failed to initialize err={}", .{err});
var buf: [32]u8 = undefined; };
priv.resize_overlay.setLabel(text: {
const grid_size = surface.size.grid();
break :text std.fmt.bufPrintZ(
&buf,
"{d} x {d}",
.{
grid_size.columns,
grid_size.rows,
},
) catch |err| err: {
log.warn("unable to format text: {}", .{err});
break :err "";
};
});
priv.resize_overlay.schedule();
} }
const RealizeError = Allocator.Error || error{ const InitError = Allocator.Error || error{
GLAreaError, GLAreaError,
RendererError,
SurfaceError, SurfaceError,
}; };
fn realizeSurface(self: *Self) RealizeError!void { fn initSurface(self: *Self) InitError!void {
const priv = self.private(); const priv = self.private();
assert(priv.core_surface == null);
const gl_area = priv.gl_area; const gl_area = priv.gl_area;
// We need to make the context current so we can call GL functions. // We need to make the context current so we can call GL functions.
@ -1997,16 +2128,6 @@ pub const Surface = extern struct {
return error.GLAreaError; return error.GLAreaError;
} }
// If we already have an initialized surface then we just notify.
if (priv.core_surface) |v| {
v.renderer.displayRealized() catch |err| {
log.warn("core displayRealized failed err={}", .{err});
return error.RendererError;
};
self.redraw();
return;
}
const app = Application.default(); const app = Application.default();
const alloc = app.allocator(); const alloc = app.allocator();
@ -2048,6 +2169,46 @@ pub const Surface = extern struct {
// Store it! // Store it!
priv.core_surface = surface; priv.core_surface = surface;
// Emit the signal that we initialized the surface.
Surface.signals.init.impl.emit(
self,
null,
.{},
null,
);
}
fn resizeOverlaySchedule(self: *Self) void {
const priv = self.private();
const surface = priv.core_surface orelse return;
// Only show the resize overlay if its enabled
const config = if (priv.config) |c| c.get() else return;
switch (config.@"resize-overlay") {
.always, .@"after-first" => {},
.never => return,
}
// If we have resize overlays enabled, setup an idler
// to show that. We do this in an idle tick because doing it
// during the resize results in flickering.
var buf: [32]u8 = undefined;
priv.resize_overlay.setLabel(text: {
const grid_size = surface.size.grid();
break :text std.fmt.bufPrintZ(
&buf,
"{d} x {d}",
.{
grid_size.columns,
grid_size.rows,
},
) catch |err| err: {
log.warn("unable to format text: {}", .{err});
break :err "";
};
});
priv.resize_overlay.schedule();
} }
fn ecUrlMouseEnter( fn ecUrlMouseEnter(
@ -2136,8 +2297,10 @@ pub const Surface = extern struct {
gobject.ext.registerProperties(class, &.{ gobject.ext.registerProperties(class, &.{
properties.config.impl, properties.config.impl,
properties.@"child-exited".impl, properties.@"child-exited".impl,
properties.@"default-size".impl,
properties.@"font-size-request".impl, properties.@"font-size-request".impl,
properties.focused.impl, properties.focused.impl,
properties.@"min-size".impl,
properties.@"mouse-shape".impl, properties.@"mouse-shape".impl,
properties.@"mouse-hidden".impl, properties.@"mouse-hidden".impl,
properties.@"mouse-hover-url".impl, properties.@"mouse-hover-url".impl,
@ -2151,6 +2314,8 @@ pub const Surface = extern struct {
signals.bell.impl.register(.{}); signals.bell.impl.register(.{});
signals.@"clipboard-read".impl.register(.{}); signals.@"clipboard-read".impl.register(.{});
signals.@"clipboard-write".impl.register(.{}); signals.@"clipboard-write".impl.register(.{});
signals.init.impl.register(.{});
signals.@"present-request".impl.register(.{});
signals.@"toggle-fullscreen".impl.register(.{}); signals.@"toggle-fullscreen".impl.register(.{});
signals.@"toggle-maximize".impl.register(.{}); signals.@"toggle-maximize".impl.register(.{});
@ -2182,6 +2347,17 @@ pub const Surface = extern struct {
.{ .name = "GhosttySurfaceCloseScope" }, .{ .name = "GhosttySurfaceCloseScope" },
); );
}; };
/// Simple dimensions struct for the surface used by various properties.
pub const Size = extern struct {
width: u32,
height: u32,
pub const getGObjectType = gobject.ext.defineBoxed(
Size,
.{ .name = "GhosttySurfaceSize" },
);
};
}; };
/// The state of the key event while we're doing IM composition. /// The state of the key event while we're doing IM composition.

View File

@ -0,0 +1,289 @@
const std = @import("std");
const build_config = @import("../../../build_config.zig");
const assert = std.debug.assert;
const adw = @import("adw");
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const i18n = @import("../../../os/main.zig").i18n;
const apprt = @import("../../../apprt.zig");
const input = @import("../../../input.zig");
const CoreSurface = @import("../../../Surface.zig");
const gtk_version = @import("../gtk_version.zig");
const adw_version = @import("../adw_version.zig");
const gresource = @import("../build/gresource.zig");
const Common = @import("../class.zig").Common;
const Config = @import("config.zig").Config;
const Application = @import("application.zig").Application;
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
const Surface = @import("surface.zig").Surface;
const log = std.log.scoped(.gtk_ghostty_window);
pub const Tab = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = gtk.Box;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttyTab",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
pub const properties = struct {
/// The active surface is the focus that should be receiving all
/// surface-targeted actions. This is usually the focused surface,
/// but may also not be focused if the user has selected a non-surface
/// widget.
pub const @"active-surface" = struct {
pub const name = "active-surface";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Surface,
.{
.nick = "Active Surface",
.blurb = "The currently active surface.",
.accessor = gobject.ext.typedAccessor(
Self,
?*Surface,
.{
.getter = Self.getActiveSurface,
},
),
},
);
};
pub const config = struct {
pub const name = "config";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Config,
.{
.nick = "Config",
.blurb = "The configuration that this surface is using.",
.accessor = C.privateObjFieldAccessor("config"),
},
);
};
pub const title = struct {
pub const name = "title";
pub const get = impl.get;
pub const set = impl.set;
const impl = gobject.ext.defineProperty(
name,
Self,
?[:0]const u8,
.{
.nick = "Title",
.blurb = "The title of the active surface.",
.default = null,
.accessor = C.privateStringFieldAccessor("title"),
},
);
};
};
pub const signals = struct {
/// Emitted whenever the tab would like to be closed.
pub const @"close-request" = struct {
pub const name = "close-request";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
};
const Private = struct {
/// The configuration that this surface is using.
config: ?*Config = null,
/// The title to show for this tab. This is usually set to a binding
/// with the active surface but can be manually set to anything.
title: ?[:0]const u8 = null,
/// The binding groups for the current active surface.
surface_bindings: *gobject.BindingGroup,
// Template bindings
surface: *Surface,
pub var offset: c_int = 0;
};
/// Set the parent of this tab page. This only affects the first surface
/// ever created for a tab. If a surface was already created this does
/// nothing.
pub fn setParent(
self: *Self,
parent: *CoreSurface,
) void {
const priv = self.private();
priv.surface.setParent(parent);
}
fn init(self: *Self, _: *Class) callconv(.C) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
// 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();
}
// Setup binding groups for surface properties
priv.surface_bindings = gobject.BindingGroup.new();
priv.surface_bindings.bind(
"title",
self.as(gobject.Object),
"title",
.{},
);
// TODO: Eventually this should be set dynamically based on the
// current active surface.
priv.surface_bindings.setSource(priv.surface.as(gobject.Object));
// We need to do this so that the title initializes properly,
// I think because its a dynamic getter.
self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
}
//---------------------------------------------------------------
// Properties
/// Get the currently active surface. See the "active-surface" property.
/// This does not ref the value.
pub fn getActiveSurface(self: *Self) *Surface {
const priv = self.private();
return priv.surface;
}
/// Returns true if this tab needs confirmation before quitting based
/// on the various Ghostty configurations.
pub fn getNeedsConfirmQuit(self: *Self) bool {
const surface = self.getActiveSurface();
const core_surface = surface.core() orelse return false;
return core_surface.needsConfirmQuit();
}
//---------------------------------------------------------------
// Virtual methods
fn dispose(self: *Self) callconv(.C) void {
const priv = self.private();
if (priv.config) |v| {
v.unref();
priv.config = null;
}
priv.surface_bindings.setSource(null);
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
getGObjectType(),
);
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
fn finalize(self: *Self) callconv(.C) void {
const priv = self.private();
if (priv.title) |v| {
glib.free(@constCast(@ptrCast(v)));
priv.title = null;
}
priv.surface_bindings.unref();
gobject.Object.virtual_methods.finalize.call(
Class.parent,
self.as(Parent),
);
}
//---------------------------------------------------------------
// Signal handlers
fn surfaceCloseRequest(
_: *Surface,
scope: *const Surface.CloseScope,
self: *Self,
) callconv(.c) void {
switch (scope.*) {
// Handled upstream... we don't control our window close.
.window => return,
// Presently both the same, results in the tab closing.
.surface, .tab => {
signals.@"close-request".impl.emit(
self,
null,
.{},
null,
);
},
}
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const unref = C.unref;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.C) void {
gobject.ext.ensureType(Surface);
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
.major = 1,
.minor = 5,
.name = "tab",
}),
);
// Properties
gobject.ext.registerProperties(class, &.{
properties.@"active-surface".impl,
properties.config.impl,
properties.title.impl,
});
// Bindings
class.bindTemplateChildPrivate("surface", .{});
// Template Callbacks
class.bindTemplateCallback("surface_close_request", &surfaceCloseRequest);
// Signals
signals.@"close-request".impl.register(.{});
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
}
pub const as = C.Class.as;
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
};
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
.transparent {
background-color: transparent;
}

View File

@ -0,0 +1,3 @@
.transparent {
background-color: transparent;
}

View File

@ -0,0 +1,3 @@
.transparent {
background-color: transparent;
}

View File

@ -4,6 +4,14 @@
* https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.3/styles-and-appearance.html#custom-styles * https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.3/styles-and-appearance.html#custom-styles
*/ */
window.ssd.no-border-radius {
/* Without clearing the border radius, at least on Mutter with
* gtk-titlebar=true and gtk-adwaita=false, there is some window artifacting
* that this will mitigate.
*/
border-radius: 0 0;
}
/* /*
* GhosttySurface URL overlay * GhosttySurface URL overlay
*/ */

52
src/apprt/gtk-ng/ext.zig Normal file
View File

@ -0,0 +1,52 @@
//! Extensions/helpers for GTK objects, following a similar naming
//! style to zig-gobject. These should, wherever possible, be Zig-friendly
//! wrappers around existing GTK functionality, rather than complex new
//! helpers.
const std = @import("std");
const assert = std.debug.assert;
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
/// Wrapper around `gobject.boxedCopy` to copy a boxed type `T`.
pub fn boxedCopy(comptime T: type, ptr: *const T) *T {
const copy = gobject.boxedCopy(T.getGObjectType(), ptr);
return @ptrCast(@alignCast(copy));
}
/// Wrapper around `gobject.boxedFree` to free a boxed type `T`.
pub fn boxedFree(comptime T: type, ptr: ?*T) void {
if (ptr) |p| gobject.boxedFree(
T.getGObjectType(),
p,
);
}
/// A wrapper around `glib.List.findCustom` to find an element in the list.
/// The type `T` must be the guaranteed type of every list element.
pub fn listFind(
comptime T: type,
list: *glib.List,
comptime func: *const fn (*T) bool,
) ?*T {
const elem_: ?*glib.List = list.findCustom(null, struct {
fn callback(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) c_int {
const ptr = data orelse return 1;
const v: *T = @ptrCast(@alignCast(@constCast(ptr)));
return if (func(v)) 0 else 1;
}
}.callback);
const elem = elem_ orelse return null;
return @ptrCast(@alignCast(elem.f_data));
}
/// Wrapper around `gtk.Widget.getAncestor` to get the widget ancestor
/// of the given type `T`, or null if it doesn't exist.
pub fn getAncestor(comptime T: type, widget: *gtk.Widget) ?*T {
const ancestor_ = widget.getAncestor(gobject.ext.typeFor(T));
const ancestor = ancestor_ orelse return null;
// We can assert the unwrap because getAncestor above
return gobject.ext.cast(T, ancestor).?;
}

View File

@ -0,0 +1,15 @@
using Gtk 4.0;
template $GhosttyTab: Box {
styles [
"tab",
]
hexpand: true;
vexpand: true;
// A tab currently just contains a surface directly. When we introduce
// splits we probably want to replace this with the split widget type.
$GhosttySurface surface {
close-request => $surface_close_request();
}
}

View File

@ -7,49 +7,97 @@ template $GhosttyWindow: Adw.ApplicationWindow {
] ]
close-request => $close_request(); close-request => $close_request();
realize => $realize();
notify::background-opaque => $notify_background_opaque();
notify::config => $notify_config(); notify::config => $notify_config();
notify::fullscreened => $notify_fullscreened(); notify::fullscreened => $notify_fullscreened();
notify::maximized => $notify_maximized(); notify::maximized => $notify_maximized();
notify::background-opaque => $notify_background_opaque(); notify::quick-terminal => $notify_quick_terminal();
notify::scale-factor => $notify_scale_factor();
default-width: 800; default-width: 800;
default-height: 600; default-height: 600;
// GTK4 grabs F10 input by default to focus the menubar icon. We want // GTK4 grabs F10 input by default to focus the menubar icon. We want
// to disable this so that terminal programs can capture F10 (such as htop) // to disable this so that terminal programs can capture F10 (such as htop)
handle-menubar-accel: false; handle-menubar-accel: false;
title: bind (template.active-surface as <$GhosttySurface>).title;
content: Box { content: Adw.TabOverview tab_overview {
orientation: vertical; create-tab => $overview_create_tab();
notify::open => $overview_notify_open();
view: tab_view;
enable-new-tab: true;
// Disable the title buttons (close, maximize, minimize, ...)
// *inside* the tab overview if CSDs are disabled.
// We do spare the search button, though.
show-start-title-buttons: bind template.decorated;
show-end-title-buttons: bind template.decorated;
Adw.HeaderBar { Adw.ToolbarView toolbar {
visible: bind template.headerbar-visible; top-bar-style: bind template.toolbar-style;
bottom-bar-style: bind template.toolbar-style;
title-widget: Adw.WindowTitle { [top]
title: bind (template.active-surface as <$GhosttySurface>).title; Adw.HeaderBar {
}; visible: bind template.headerbar-visible;
[end] title-widget: Adw.WindowTitle {
Gtk.Box { title: bind template.title;
Gtk.MenuButton { };
notify::active => $notify_menu_active();
icon-name: "open-menu-symbolic"; [start]
menu-model: main_menu; Adw.SplitButton {
tooltip-text: _("Main Menu"); clicked => $new_tab();
can-focus: false; icon-name: "tab-new-symbolic";
tooltip-text: _("New Tab");
dropdown-tooltip: _("New Split");
menu-model: split_menu;
}
[end]
Gtk.Box {
Gtk.ToggleButton {
icon-name: "view-grid-symbolic";
tooltip-text: _("View Open Tabs");
active: bind tab_overview.open bidirectional;
can-focus: false;
focus-on-click: false;
}
Gtk.MenuButton {
notify::active => $notify_menu_active();
icon-name: "open-menu-symbolic";
menu-model: main_menu;
tooltip-text: _("Main Menu");
can-focus: false;
}
} }
} }
}
$GhosttyDebugWarning { [top]
visible: bind template.debug; Adw.TabBar tab_bar {
} autohide: bind template.tabs-autohide;
expand-tabs: bind template.tabs-wide;
view: tab_view;
visible: bind template.tabs-visible;
}
Adw.ToastOverlay toast_overlay { Box {
$GhosttySurface surface { orientation: vertical;
close-request => $surface_close_request();
clipboard-write => $surface_clipboard_write(); $GhosttyDebugWarning {
toggle-fullscreen => $surface_toggle_fullscreen(); visible: bind template.debug;
toggle-maximize => $surface_toggle_maximize(); }
Adw.ToastOverlay toast_overlay {
Adw.TabView tab_view {
notify::n-pages => $notify_n_pages();
notify::selected-page => $notify_selected_page();
close-page => $close_page();
page-attached => $page_attached();
page-detached => $page_detached();
create-window => $tab_create_window();
shortcuts: none;
}
}
} }
} }
}; };

View File

@ -7,9 +7,7 @@ const gdk = @import("gdk");
const Config = @import("../../config.zig").Config; const Config = @import("../../config.zig").Config;
const input = @import("../../input.zig"); const input = @import("../../input.zig");
const key = @import("key.zig"); const key = @import("key.zig");
const ApprtWindow = @import("class/window.zig").Window;
// TODO: As we get to these APIs the compiler should tell us
const ApprtWindow = void;
pub const noop = @import("winproto/noop.zig"); pub const noop = @import("winproto/noop.zig");
pub const x11 = @import("winproto/x11.zig"); pub const x11 = @import("winproto/x11.zig");

View File

@ -5,7 +5,7 @@ const gdk = @import("gdk");
const Config = @import("../../../config.zig").Config; const Config = @import("../../../config.zig").Config;
const input = @import("../../../input.zig"); const input = @import("../../../input.zig");
const ApprtWindow = void; // TODO: fix const ApprtWindow = @import("../class/window.zig").Window;
const log = std.log.scoped(.winproto_noop); const log = std.log.scoped(.winproto_noop);

View File

@ -12,7 +12,7 @@ const wayland = @import("wayland");
const Config = @import("../../../config.zig").Config; const Config = @import("../../../config.zig").Config;
const input = @import("../../../input.zig"); const input = @import("../../../input.zig");
const ApprtWindow = void; // TODO: fix const ApprtWindow = @import("../class/window.zig").Window;
const wl = wayland.client.wl; const wl = wayland.client.wl;
const org = wayland.client.org; const org = wayland.client.org;
@ -127,7 +127,7 @@ pub const App = struct {
} }
pub fn initQuickTerminal(_: *App, apprt_window: *ApprtWindow) !void { pub fn initQuickTerminal(_: *App, apprt_window: *ApprtWindow) !void {
const window = apprt_window.window.as(gtk.Window); const window = apprt_window.as(gtk.Window);
layer_shell.initForWindow(window); layer_shell.initForWindow(window);
layer_shell.setLayer(window, .top); layer_shell.setLayer(window, .top);
@ -157,11 +157,19 @@ pub const App = struct {
const ctx_fields = @typeInfo(Context).@"struct".fields; const ctx_fields = @typeInfo(Context).@"struct".fields;
switch (event) { switch (event) {
.global => |v| global: { .global => |v| {
log.debug("found global {s}", .{v.interface});
// We don't actually do anything with this other than checking // We don't actually do anything with this other than checking
// for its existence, so we process this separately. // for its existence, so we process this separately.
if (std.mem.orderZ(u8, v.interface, "xdg_wm_dialog_v1") == .eq) if (std.mem.orderZ(
u8,
v.interface,
"xdg_wm_dialog_v1",
) == .eq) {
context.xdg_wm_dialog_present = true; context.xdg_wm_dialog_present = true;
return;
}
inline for (ctx_fields) |field| { inline for (ctx_fields) |field| {
const T = getInterfaceType(field) orelse continue; const T = getInterfaceType(field) orelse continue;
@ -170,19 +178,21 @@ pub const App = struct {
u8, u8,
v.interface, v.interface,
T.interface.name, T.interface.name,
) != .eq) break :global; ) == .eq) {
log.debug("matched {}", .{T});
@field(context, field.name) = registry.bind( @field(context, field.name) = registry.bind(
v.name, v.name,
T, T,
T.generated_version, T.generated_version,
) catch |err| { ) catch |err| {
log.warn( log.warn(
"error binding interface {s} error={}", "error binding interface {s} error={}",
.{ v.interface, err }, .{ v.interface, err },
); );
return; return;
}; };
}
} }
}, },
@ -247,7 +257,7 @@ pub const Window = struct {
) !Window { ) !Window {
_ = alloc; _ = alloc;
const gtk_native = apprt_window.window.as(gtk.Native); const gtk_native = apprt_window.as(gtk.Native);
const gdk_surface = gtk_native.getSurface() orelse return error.NotWaylandSurface; const gdk_surface = gtk_native.getSurface() orelse return error.NotWaylandSurface;
// This should never fail, because if we're being called at this point // This should never fail, because if we're being called at this point
@ -354,7 +364,11 @@ pub const Window = struct {
/// Update the blur state of the window. /// Update the blur state of the window.
fn syncBlur(self: *Window) !void { fn syncBlur(self: *Window) !void {
const manager = self.app_context.kde_blur_manager orelse return; const manager = self.app_context.kde_blur_manager orelse return;
const blur = self.apprt_window.config.background_blur; const config = if (self.apprt_window.getConfig()) |v|
v.get()
else
return;
const blur = config.@"background-blur";
if (self.blur_token) |tok| { if (self.blur_token) |tok| {
// Only release token when transitioning from blurred -> not blurred // Only release token when transitioning from blurred -> not blurred
@ -382,7 +396,7 @@ pub const Window = struct {
} }
fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode { fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode {
return switch (self.apprt_window.config.window_decoration) { return switch (self.apprt_window.getWindowDecoration()) {
.auto => self.app_context.default_deco_mode orelse .Client, .auto => self.app_context.default_deco_mode orelse .Client,
.client => .Client, .client => .Client,
.server => .Server, .server => .Server,
@ -391,12 +405,15 @@ pub const Window = struct {
} }
fn syncQuickTerminal(self: *Window) !void { fn syncQuickTerminal(self: *Window) !void {
const window = self.apprt_window.window.as(gtk.Window); const window = self.apprt_window.as(gtk.Window);
const config = &self.apprt_window.config; const config = if (self.apprt_window.getConfig()) |v|
v.get()
else
return;
layer_shell.setKeyboardMode( layer_shell.setKeyboardMode(
window, window,
switch (config.quick_terminal_keyboard_interactivity) { switch (config.@"quick-terminal-keyboard-interactivity") {
.none => .none, .none => .none,
.@"on-demand" => on_demand: { .@"on-demand" => on_demand: {
if (layer_shell.getProtocolVersion() < 4) { if (layer_shell.getProtocolVersion() < 4) {
@ -409,7 +426,7 @@ pub const Window = struct {
}, },
); );
const anchored_edge: ?layer_shell.ShellEdge = switch (config.quick_terminal_position) { const anchored_edge: ?layer_shell.ShellEdge = switch (config.@"quick-terminal-position") {
.left => .left, .left => .left,
.right => .right, .right => .right,
.top => .top, .top => .top,
@ -460,14 +477,14 @@ pub const Window = struct {
monitor: *gdk.Monitor, monitor: *gdk.Monitor,
apprt_window: *ApprtWindow, apprt_window: *ApprtWindow,
) callconv(.c) void { ) callconv(.c) void {
const window = apprt_window.window.as(gtk.Window); const window = apprt_window.as(gtk.Window);
const config = &apprt_window.config; const config = if (apprt_window.getConfig()) |v| v.get() else return;
var monitor_size: gdk.Rectangle = undefined; var monitor_size: gdk.Rectangle = undefined;
monitor.getGeometry(&monitor_size); monitor.getGeometry(&monitor_size);
const dims = config.quick_terminal_size.calculate( const dims = config.@"quick-terminal-size".calculate(
config.quick_terminal_position, config.@"quick-terminal-position",
.{ .{
.width = @intCast(monitor_size.f_width), .width = @intCast(monitor_size.f_width),
.height = @intCast(monitor_size.f_height), .height = @intCast(monitor_size.f_height),

View File

@ -20,7 +20,7 @@ pub const c = @cImport({
const input = @import("../../../input.zig"); const input = @import("../../../input.zig");
const Config = @import("../../../config.zig").Config; const Config = @import("../../../config.zig").Config;
const ApprtWindow = void; // TODO: fix const ApprtWindow = @import("../class/window.zig").Window;
const log = std.log.scoped(.gtk_x11); const log = std.log.scoped(.gtk_x11);
@ -170,8 +170,7 @@ pub const App = struct {
pub const Window = struct { pub const Window = struct {
app: *App, app: *App,
config: *const ApprtWindow.DerivedConfig, apprt_window: *ApprtWindow,
gtk_window: *adw.ApplicationWindow,
x11_surface: *gdk_x11.X11Surface, x11_surface: *gdk_x11.X11Surface,
blur_region: Region = .{}, blur_region: Region = .{},
@ -183,9 +182,8 @@ pub const Window = struct {
) !Window { ) !Window {
_ = alloc; _ = alloc;
const surface = apprt_window.window.as( const surface = apprt_window.as(gtk.Native).getSurface() orelse
gtk.Native, return error.NotX11Surface;
).getSurface() orelse return error.NotX11Surface;
const x11_surface = gobject.ext.cast( const x11_surface = gobject.ext.cast(
gdk_x11.X11Surface, gdk_x11.X11Surface,
@ -194,8 +192,7 @@ pub const Window = struct {
return .{ return .{
.app = app, .app = app,
.config = &apprt_window.config, .apprt_window = apprt_window,
.gtk_window = apprt_window.window,
.x11_surface = x11_surface, .x11_surface = x11_surface,
}; };
} }
@ -221,10 +218,10 @@ pub const Window = struct {
var x: f64 = 0; var x: f64 = 0;
var y: f64 = 0; var y: f64 = 0;
self.gtk_window.as(gtk.Native).getSurfaceTransform(&x, &y); self.apprt_window.as(gtk.Native).getSurfaceTransform(&x, &y);
// Transform surface coordinates to device coordinates. // Transform surface coordinates to device coordinates.
const scale: f64 = @floatFromInt(self.gtk_window.as(gtk.Widget).getScaleFactor()); const scale: f64 = @floatFromInt(self.apprt_window.as(gtk.Widget).getScaleFactor());
x *= scale; x *= scale;
y *= scale; y *= scale;
@ -242,7 +239,7 @@ pub const Window = struct {
} }
pub fn clientSideDecorationEnabled(self: Window) bool { pub fn clientSideDecorationEnabled(self: Window) bool {
return switch (self.config.window_decoration) { return switch (self.apprt_window.getWindowDecoration()) {
.auto, .client => true, .auto, .client => true,
.server, .none => false, .server, .none => false,
}; };
@ -257,14 +254,15 @@ pub const Window = struct {
// and I think it's not really noticeable enough to justify the effort. // and I think it's not really noticeable enough to justify the effort.
// (Wayland also has this visual artifact anyway...) // (Wayland also has this visual artifact anyway...)
const gtk_widget = self.gtk_window.as(gtk.Widget); const gtk_widget = self.apprt_window.as(gtk.Widget);
const config = if (self.apprt_window.getConfig()) |v| v.get() else return;
// Transform surface coordinates to device coordinates. // Transform surface coordinates to device coordinates.
const scale = self.gtk_window.as(gtk.Widget).getScaleFactor(); const scale = gtk_widget.getScaleFactor();
self.blur_region.width = gtk_widget.getWidth() * scale; self.blur_region.width = gtk_widget.getWidth() * scale;
self.blur_region.height = gtk_widget.getHeight() * scale; self.blur_region.height = gtk_widget.getHeight() * scale;
const blur = self.config.background_blur; const blur = config.@"background-blur";
log.debug("set blur={}, window xid={}, region={}", .{ log.debug("set blur={}, window xid={}, region={}", .{
blur, blur,
self.x11_surface.getXid(), self.x11_surface.getXid(),
@ -306,7 +304,7 @@ pub const Window = struct {
}; };
hints.flags.decorations = true; hints.flags.decorations = true;
hints.decorations.all = switch (self.config.window_decoration) { hints.decorations.all = switch (self.apprt_window.getWindowDecoration()) {
.server => true, .server => true,
.auto, .client, .none => false, .auto, .client, .none => false,
}; };

View File

@ -157,11 +157,19 @@ pub const App = struct {
const ctx_fields = @typeInfo(Context).@"struct".fields; const ctx_fields = @typeInfo(Context).@"struct".fields;
switch (event) { switch (event) {
.global => |v| global: { .global => |v| {
log.debug("found global {s}", .{v.interface});
// We don't actually do anything with this other than checking // We don't actually do anything with this other than checking
// for its existence, so we process this separately. // for its existence, so we process this separately.
if (std.mem.orderZ(u8, v.interface, "xdg_wm_dialog_v1") == .eq) if (std.mem.orderZ(
u8,
v.interface,
"xdg_wm_dialog_v1",
) == .eq) {
context.xdg_wm_dialog_present = true; context.xdg_wm_dialog_present = true;
return;
}
inline for (ctx_fields) |field| { inline for (ctx_fields) |field| {
const T = getInterfaceType(field) orelse continue; const T = getInterfaceType(field) orelse continue;
@ -170,19 +178,21 @@ pub const App = struct {
u8, u8,
v.interface, v.interface,
T.interface.name, T.interface.name,
) != .eq) break :global; ) == .eq) {
log.debug("matched {}", .{T});
@field(context, field.name) = registry.bind( @field(context, field.name) = registry.bind(
v.name, v.name,
T, T,
T.generated_version, T.generated_version,
) catch |err| { ) catch |err| {
log.warn( log.warn(
"error binding interface {s} error={}", "error binding interface {s} error={}",
.{ v.interface, err }, .{ v.interface, err },
); );
return; return;
}; };
}
} }
}, },

View File

@ -35,6 +35,7 @@ pub const RepeatableStringMap = @import("config/RepeatableStringMap.zig");
pub const RepeatablePath = Config.RepeatablePath; pub const RepeatablePath = Config.RepeatablePath;
pub const Path = Config.Path; pub const Path = Config.Path;
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures; pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
pub const WindowDecoration = Config.WindowDecoration;
pub const WindowPaddingColor = Config.WindowPaddingColor; pub const WindowPaddingColor = Config.WindowPaddingColor;
pub const BackgroundImagePosition = Config.BackgroundImagePosition; pub const BackgroundImagePosition = Config.BackgroundImagePosition;
pub const BackgroundImageFit = Config.BackgroundImageFit; pub const BackgroundImageFit = Config.BackgroundImageFit;

View File

@ -7434,12 +7434,22 @@ pub const BackgroundBlur = union(enum) {
}; };
/// See window-decoration /// See window-decoration
pub const WindowDecoration = enum { pub const WindowDecoration = enum(c_int) {
auto, auto,
client, client,
server, server,
none, none,
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk, .@"gtk-ng" => @import("gobject").ext.defineEnum(
WindowDecoration,
.{ .name = "GhosttyConfigWindowDecoration" },
),
.none => void,
};
pub fn parseCLI(input_: ?[]const u8) !WindowDecoration { pub fn parseCLI(input_: ?[]const u8) !WindowDecoration {
const input = input_ orelse return .auto; const input = input_ orelse return .auto;

View File

@ -9,7 +9,7 @@
# Author: Ryan Caloras (ryan@bashhub.com) # Author: Ryan Caloras (ryan@bashhub.com)
# Forked from Original Author: Glyph Lefkowitz # Forked from Original Author: Glyph Lefkowitz
# #
# V0.5.0 # V0.6.0
# #
# General Usage: # General Usage:
@ -38,7 +38,7 @@
# Make sure this is bash that's running and return otherwise. # Make sure this is bash that's running and return otherwise.
# Use POSIX syntax for this line: # Use POSIX syntax for this line:
if [ -z "${BASH_VERSION-}" ]; then if [ -z "${BASH_VERSION-}" ]; then
return 1; return 1
fi fi
# We only support Bash 3.1+. # We only support Bash 3.1+.
@ -76,13 +76,13 @@ __bp_install_string=$'__bp_trap_string="$(trap -p DEBUG)"\ntrap - DEBUG\n__bp_in
# Fails if any of the given variables are readonly # Fails if any of the given variables are readonly
# Reference https://stackoverflow.com/a/4441178 # Reference https://stackoverflow.com/a/4441178
__bp_require_not_readonly() { __bp_require_not_readonly() {
local var local var
for var; do for var; do
if ! ( unset "$var" 2> /dev/null ); then if ! ( unset "$var" 2> /dev/null ); then
echo "bash-preexec requires write access to ${var}" >&2 echo "bash-preexec requires write access to ${var}" >&2
return 1 return 1
fi fi
done done
} }
# Remove ignorespace and or replace ignoreboth from HISTCONTROL # Remove ignorespace and or replace ignoreboth from HISTCONTROL
@ -95,7 +95,7 @@ __bp_adjust_histcontrol() {
# Replace ignoreboth with ignoredups # Replace ignoreboth with ignoredups
if [[ "$histcontrol" == *"ignoreboth"* ]]; then if [[ "$histcontrol" == *"ignoreboth"* ]]; then
histcontrol="ignoredups:${histcontrol//ignoreboth}" histcontrol="ignoredups:${histcontrol//ignoreboth}"
fi; fi
export HISTCONTROL="$histcontrol" export HISTCONTROL="$histcontrol"
} }
@ -136,7 +136,7 @@ __bp_sanitize_string() {
# It sets a variable to indicate that the prompt was just displayed, # It sets a variable to indicate that the prompt was just displayed,
# to allow the DEBUG trap to know that the next command is likely interactive. # to allow the DEBUG trap to know that the next command is likely interactive.
__bp_interactive_mode() { __bp_interactive_mode() {
__bp_preexec_interactive_mode="on"; __bp_preexec_interactive_mode="on"
} }
@ -154,7 +154,7 @@ __bp_precmd_invoke_cmd() {
# prompt command" by another precmd execution loop. This avoids infinite # prompt command" by another precmd execution loop. This avoids infinite
# recursion. # recursion.
if (( __bp_inside_precmd > 0 )); then if (( __bp_inside_precmd > 0 )); then
return return
fi fi
local __bp_inside_precmd=1 local __bp_inside_precmd=1
@ -211,7 +211,7 @@ __bp_preexec_invoke_exec() {
__bp_last_argument_prev_command="${1:-}" __bp_last_argument_prev_command="${1:-}"
# Don't invoke preexecs if we are inside of another preexec. # Don't invoke preexecs if we are inside of another preexec.
if (( __bp_inside_preexec > 0 )); then if (( __bp_inside_preexec > 0 )); then
return return
fi fi
local __bp_inside_preexec=1 local __bp_inside_preexec=1
@ -289,7 +289,7 @@ __bp_preexec_invoke_exec() {
__bp_install() { __bp_install() {
# Exit if we already have this installed. # Exit if we already have this installed.
if [[ "${PROMPT_COMMAND[*]:-}" == *"__bp_precmd_invoke_cmd"* ]]; then if [[ "${PROMPT_COMMAND[*]:-}" == *"__bp_precmd_invoke_cmd"* ]]; then
return 1; return 1
fi fi
trap '__bp_preexec_invoke_exec "$_"' DEBUG trap '__bp_preexec_invoke_exec "$_"' DEBUG
@ -300,7 +300,7 @@ __bp_install() {
unset __bp_trap_string unset __bp_trap_string
if [[ -n "$prior_trap" ]]; then if [[ -n "$prior_trap" ]]; then
eval '__bp_original_debug_trap() { eval '__bp_original_debug_trap() {
'"$prior_trap"' '"$prior_trap"'
}' }'
preexec_functions+=(__bp_original_debug_trap) preexec_functions+=(__bp_original_debug_trap)
fi fi
@ -323,7 +323,7 @@ __bp_install() {
# Set so debug trap will work be invoked in subshells. # Set so debug trap will work be invoked in subshells.
set -o functrace > /dev/null 2>&1 set -o functrace > /dev/null 2>&1
shopt -s extdebug > /dev/null 2>&1 shopt -s extdebug > /dev/null 2>&1
fi; fi
local existing_prompt_command local existing_prompt_command
# Remove setting our trap install string and sanitize the existing prompt command string # Remove setting our trap install string and sanitize the existing prompt command string
@ -371,7 +371,7 @@ __bp_install_after_session_init() {
if [[ -n "$sanitized_prompt_command" ]]; then if [[ -n "$sanitized_prompt_command" ]]; then
# shellcheck disable=SC2178 # PROMPT_COMMAND is not an array in bash <= 5.0 # shellcheck disable=SC2178 # PROMPT_COMMAND is not an array in bash <= 5.0
PROMPT_COMMAND=${sanitized_prompt_command}$'\n' PROMPT_COMMAND=${sanitized_prompt_command}$'\n'
fi; fi
# shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0 # shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0
PROMPT_COMMAND+=${__bp_install_string} PROMPT_COMMAND+=${__bp_install_string}
} }
@ -379,4 +379,4 @@ __bp_install_after_session_init() {
# Run our install so long as we're not delaying it. # Run our install so long as we're not delaying it.
if [[ -z "${__bp_delay_install:-}" ]]; then if [[ -z "${__bp_delay_install:-}" ]]; then
__bp_install_after_session_init __bp_install_after_session_init
fi; fi

View File

@ -165,7 +165,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
end end
# Execute SSH with TERM environment variable # Execute SSH with TERM environment variable
env TERM="$ssh_term" command ssh $ssh_opts $argv TERM="$ssh_term" command ssh $ssh_opts $argv
end end
end end

View File

@ -13,6 +13,21 @@
# You must gracefully exit Ghostty (do not SIGINT) by closing all windows # You must gracefully exit Ghostty (do not SIGINT) by closing all windows
# and quitting. Otherwise, we leave a number of GTK resources around. # and quitting. Otherwise, we leave a number of GTK resources around.
{
GTK CSS Provider Leak
Memcheck:Leak
match-leak-kinds: definite
fun:calloc
fun:g_malloc0
fun:gtk_css_value_alloc
fun:_gtk_css_reference_value_new
fun:parse_ruleset
fun:gtk_css_provider_load_internal
fun:gtk_css_provider_load_from_bytes
fun:gtk_css_provider_load_from_string
...
}
{ {
GDK SVG Loading Leaks GDK SVG Loading Leaks
Memcheck:Leak Memcheck:Leak
@ -135,13 +150,34 @@
... ...
fun:gsk_gpu_node_processor_process fun:gsk_gpu_node_processor_process
fun:gsk_gpu_frame_render fun:gsk_gpu_frame_render
fun:gsk_gpu_renderer_render
fun:gsk_renderer_render
fun:gtk_widget_render
fun:surface_render
... ...
} }
{
GDK GLArea
Memcheck:Leak
match-leak-kinds: possible
fun:*alloc
...
fun:gdk_memory_texture_from_texture
fun:gdk_gl_texture_release
fun:delete_one_texture
fun:g_list_foreach
fun:g_list_free_full
fun:gtk_gl_area_unrealize
...
}
{
GDK GLArea Snapshot
Memcheck:Leak
match-leak-kinds: definite
fun:*alloc
...
fun:gtk_gl_area_snapshot
...
}
{ {
GSK GPU Rendering GSK GPU Rendering
Memcheck:Leak Memcheck:Leak
@ -351,6 +387,7 @@
Memcheck:Leak Memcheck:Leak
match-leak-kinds: possible match-leak-kinds: possible
fun:*alloc fun:*alloc
...
fun:FcFontSet* fun:FcFontSet*
... ...
fun:fc_thread_func fun:fc_thread_func