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:
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.
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.

View File

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

6
build.zig.zon.json generated
View File

@ -49,10 +49,10 @@
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
},
"N-V-__8AAGHcWgSXHA9Fw-E0Hbe5EZWyYyI1AvW9O_HBbkRH": {
"N-V-__8AAN83XASXgcKp4RDTj_WcQ19E5X24C3FjQoffeMFv": {
"name": "iterm2_themes",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/92f20650771384b82f981fb0f249e5fbdcb69e9f.tar.gz",
"hash": "sha256-sQ5IWKQdEU3MOHzxovjA4DO6f/ryvtW18aITN4Bkog0="
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b2742b8baf86f556d6be4d9c6515bfd9d9c7a140.tar.gz",
"hash": "sha256-w/biUQZ+AJv0atXypwQxJlKkHRUaFS0AlE/VlBJXlVU="
},
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
"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 {
name = "iterm2_themes";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/92f20650771384b82f981fb0f249e5fbdcb69e9f.tar.gz";
hash = "sha256-sQ5IWKQdEU3MOHzxovjA4DO6f/ryvtW18aITN4Bkog0=";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b2742b8baf86f556d6be4d9c6515bfd9d9c7a140.tar.gz";
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/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/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/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz

View File

@ -61,9 +61,9 @@
},
{
"type": "archive",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/92f20650771384b82f981fb0f249e5fbdcb69e9f.tar.gz",
"dest": "vendor/p/N-V-__8AAGHcWgSXHA9Fw-E0Hbe5EZWyYyI1AvW9O_HBbkRH",
"sha256": "b10e4858a41d114dcc387cf1a2f8c0e033ba7ffaf2bed5b5f1a213378064a20d"
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b2742b8baf86f556d6be4d9c6515bfd9d9c7a140.tar.gz",
"dest": "vendor/p/N-V-__8AAN83XASXgcKp4RDTj_WcQ19E5X24C3FjQoffeMFv",
"sha256": "c3f6e251067e009bf46ad5f2a704312652a41d151a152d00944fd59412579555"
},
{
"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.menuMoveSplitDividerLeft?.setImageIfDesired(systemSymbolName: "arrow.left.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.

View File

@ -2,14 +2,15 @@
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Satrio Bayu Aji <halosatrio@gmail.com>, 2025.
# Mikail Muzakki <mikailmmuzakki@gmail.com>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-03-20 15:19+0700\n"
"Last-Translator: Satrio Bayu Aji <halosatrio@gmail.com>\n"
"PO-Revision-Date: 2025-08-01 10:15+0700\n"
"Last-Translator: Mikail Muzakki <mikailmmuzakki@gmail.com>\n"
"Language-Team: Indonesian <translation-team-id@lists.sourceforge.net>\n"
"Language: id\n"
"MIME-Version: 1.0\n"
@ -86,7 +87,7 @@ msgstr "Belah kanan"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
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-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-window-titlebar_menu.blp:73
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-window-titlebar_menu.blp:78
@ -159,7 +160,7 @@ msgstr "Buka konfigurasi"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr ""
msgstr "Palet perintah"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
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-write.blp:10
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-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-write.blp:77
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-write.blp:78
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.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
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
msgid ""
@ -277,15 +278,15 @@ msgstr "Disalin ke papan klip"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr ""
msgstr "Papan klip dibersihkan"
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr ""
msgstr "Perintah berhasil"
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr ""
msgstr "Perintah gagal"
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
@ -297,7 +298,7 @@ msgstr "Lihat tab terbuka"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr ""
msgstr "Belahan baru"
#: src/apprt/gtk/Window.zig:329
msgid ""

View File

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

View File

@ -1,4 +1,5 @@
const std = @import("std");
const build_config = @import("../build_config.zig");
const assert = std.debug.assert;
const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig");
@ -533,6 +534,16 @@ pub const SizeLimit = extern struct {
pub const InitialSize = extern struct {
width: 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 {

View File

@ -37,20 +37,21 @@ pub const blueprints: []const Blueprint = &.{
.{ .major = 1, .minor = 4, .name = "clipboard-confirmation-dialog" },
.{ .major = 1, .minor = 2, .name = "close-confirmation-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 = "surface" },
.{ .major = 1, .minor = 3, .name = "surface-child-exited" },
.{ .major = 1, .minor = 5, .name = "tab" },
.{ .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
pub const css = [_][]const u8{
"style.css",
// "style-dark.css",
// "style-hc.css",
// "style-hc-dark.css",
"style-dark.css",
"style-hc.css",
"style-hc-dark.css",
};
pub const Blueprint = struct {

View File

@ -22,6 +22,7 @@ const xev = @import("../../../global.zig").xev;
const CoreConfig = configpkg.Config;
const CoreSurface = @import("../../../Surface.zig");
const ext = @import("../ext.zig");
const adw_version = @import("../adw_version.zig");
const gtk_version = @import("../gtk_version.zig");
const winprotopkg = @import("../winproto.zig");
@ -128,6 +129,15 @@ pub const Application = extern struct {
/// outside of our own lifecycle and that's okay.
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;
};
@ -263,6 +273,16 @@ pub const Application = extern struct {
const config_obj: *Config = try .new(alloc, &config);
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.
const self = gobject.ext.newInstance(Self, .{
.application_id = app_id.ptr,
@ -283,8 +303,22 @@ pub const Application = extern struct {
.core_app = core_app,
.config = config_obj,
.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;
}
@ -299,6 +333,22 @@ pub const Application = extern struct {
priv.config.unref();
priv.winproto.deinit(alloc);
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
@ -493,10 +543,20 @@ pub const Application = extern struct {
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_shape => Action.mouseShape(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(
self,
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),
.present_terminal => return Action.presentTerminal(target),
.progress_report => return Action.progressReport(target, value),
.quit => self.quit(),
.quit_timer => try Action.quitTimer(self, value),
.progress_report => return Action.progressReport(target, value),
.reload_config => try Action.reloadConfig(self, target, value),
.render => Action.render(target),
@ -525,30 +591,31 @@ pub const Application = extern struct {
.show_gtk_inspector => Action.showGtkInspector(),
.size_limit => return Action.sizeLimit(target, value),
.toggle_maximize => Action.toggleMaximize(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
.new_tab,
.goto_tab,
.move_tab,
.prompt_title,
.toggle_command_palette,
.inspector,
// TODO: splits
.new_split,
.resize_split,
.equalize_splits,
.goto_split,
.open_config,
.inspector,
.desktop_notification,
.present_terminal,
.initial_size,
.size_limit,
.toggle_tab_overview,
.toggle_split_zoom,
.toggle_window_decorations,
.prompt_title,
.toggle_quick_terminal,
.toggle_command_palette,
.open_url,
=> {
log.warn("unimplemented action={}", .{action});
return false;
},
// Unimplemented
.secure_input,
.close_all_windows,
.float_window,
.toggle_visibility,
@ -565,13 +632,6 @@ pub const Application = extern struct {
log.warn("unimplemented action={}", .{action});
return false;
},
// Unimplemented
.secure_input,
=> {
log.warn("unimplemented action={}", .{action});
return false;
},
}
// 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
@ -670,6 +879,28 @@ pub const Application = extern struct {
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
@ -698,6 +929,9 @@ pub const Application = extern struct {
// Setup our style manager (light/dark mode)
self.startupStyleManager();
// Setup some signal handlers
self.startupSignals();
// Setup our action map
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.
fn startupActionMap(self: *Self) void {
const t_variant_type = glib.ext.VariantType.newFor(u64);
@ -804,6 +1049,8 @@ pub const Application = extern struct {
const actions = .{
.{ "new-window", actionNewWindow, null },
.{ "new-window-command", actionNewWindow, as_variant_type },
.{ "open-config", actionOpenConfig, null },
.{ "present-surface", actionPresentSurface, t_variant_type },
.{ "quit", actionQuit, null },
.{ "reload-config", actionReloadConfig, null },
};
@ -914,6 +1161,12 @@ pub const Application = extern struct {
diag.close();
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(
Class.parent,
@ -932,6 +1185,26 @@ pub const Application = extern struct {
//---------------------------------------------------------------
// 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(
_: *CloseConfirmationDialog,
self: *Self,
@ -1098,6 +1371,58 @@ pub const Application = extern struct {
}, .{ .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
@ -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(
target: apprt.Target,
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(
self: *Application,
parent: ?*CoreSurface,
) !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
// window. There's never a time when the window config should be out
// of sync with the application config.
@ -1246,10 +1712,44 @@ const Action = struct {
.{},
);
// Create a new tab
win.newTab(parent);
// Show the 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(
target: apprt.Target,
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(
target: apprt.Target,
value: terminal.osc.Command.ProgressReport,
@ -1378,6 +1890,26 @@ const Action = struct {
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 {
switch (target) {
.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 {
switch (target) {
.app => {},
.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
@ -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.
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,
);
};
pub const cancel = struct {
pub const name = "cancel";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
};
const Private = struct {
@ -72,14 +83,15 @@ pub const CloseConfirmationDialog = extern struct {
fn init(self: *Self, _: *Class) callconv(.C) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
}
pub fn present(self: *Self, parent: ?*gtk.Widget) void {
// Setup our title/body text.
const priv = self.private();
self.as(Dialog.Parent).setHeading(priv.target.title());
self.as(Dialog.Parent).setBody(priv.target.body());
}
pub fn present(self: *Self, parent: ?*gtk.Widget) void {
// Show it
self.as(Dialog).present(parent);
}
@ -91,13 +103,21 @@ pub const CloseConfirmationDialog = extern struct {
self: *Self,
response_id: [*:0]const u8,
) 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(
self,
null,
.{},
null,
);
} else {
signals.cancel.impl.emit(
self,
null,
.{},
null,
);
}
}
fn dispose(self: *Self) callconv(.C) void {
@ -141,6 +161,7 @@ pub const CloseConfirmationDialog = extern struct {
// Signals
signals.@"close-request".impl.register(.{});
signals.cancel.impl.register(.{});
// Virtual methods
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.
pub const Target = enum(c_int) {
app,
tab,
window,
pub fn title(self: Target) [*:0]const u8 {
return switch (self) {
.app => i18n._("Quit Ghostty?"),
.tab => i18n._("Close Tab?"),
.window => i18n._("Close Window?"),
};
}
@ -170,6 +193,7 @@ pub const Target = enum(c_int) {
pub fn body(self: Target) [*:0]const u8 {
return switch (self) {
.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."),
};
}

View File

@ -16,6 +16,7 @@ const renderer = @import("../../../renderer.zig");
const terminal = @import("../../../terminal/main.zig");
const CoreSurface = @import("../../../Surface.zig");
const gresource = @import("../build/gresource.zig");
const ext = @import("../ext.zig");
const adw_version = @import("../adw_version.zig");
const gtk_key = @import("../key.zig");
const ApprtSurface = @import("../Surface.zig");
@ -25,6 +26,7 @@ const Config = @import("config.zig").Config;
const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay;
const ChildExited = @import("surface_child_exited.zig").SurfaceChildExited;
const ClipboardConfirmationDialog = @import("clipboard_confirmation_dialog.zig").ClipboardConfirmationDialog;
const Window = @import("window.zig").Window;
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 name = "font-size-request";
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 name = "mouse-hidden";
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
/// fullscreen state.
pub const @"toggle-fullscreen" = struct {
@ -320,6 +375,13 @@ pub const Surface = extern struct {
/// if `Application.transient_cgroup_base` is set.
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
/// and has no effect later.
font_size_request: ?*font.face.DesiredSize = null,
@ -578,6 +640,17 @@ pub const Surface = extern struct {
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).
///
/// 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 {
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 {
@ -983,8 +1062,6 @@ pub const Surface = extern struct {
}
pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
_ = self;
const alloc = Application.default().allocator();
var env = try internal_os.getEnvMap(alloc);
errdefer env.deinit();
@ -1021,6 +1098,14 @@ pub const Surface = extern struct {
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;
}
@ -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
@ -1065,12 +1157,7 @@ pub const Surface = extern struct {
priv.mouse_shape = .text;
priv.mouse_hidden = false;
priv.focused = true;
priv.size = .{
// Funky numbers on purpose so they stand out if for some reason
// our size doesn't get properly set.
.width = 111,
.height = 111,
};
priv.size = .{ .width = 0, .height = 0 };
// If our configuration is null then we get the configuration
// from the application.
@ -1150,10 +1237,18 @@ pub const Surface = extern struct {
glib.free(@constCast(@ptrCast(v)));
priv.mouse_hover_url = null;
}
if (priv.default_size) |v| {
ext.boxedFree(Size, v);
priv.default_size = null;
}
if (priv.font_size_request) |v| {
glib.ext.destroy(v);
priv.font_size_request = null;
}
if (priv.min_size) |v| {
ext.boxedFree(Size, v);
priv.min_size = null;
}
if (priv.pwd) |v| {
glib.free(@constCast(@ptrCast(v)));
priv.pwd = null;
@ -1191,6 +1286,50 @@ pub const Surface = extern struct {
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(
self: *Self,
_: *gobject.ParamSpec,
@ -1832,15 +1971,32 @@ pub const Surface = extern struct {
) callconv(.c) void {
log.debug("realize", .{});
// Setup our core surface
self.realizeSurface() catch |err| {
log.warn("surface failed to realize err={}", .{err});
// If we already have an initialized surface then we notify it.
// If we don't, we'll initialize it on the first resize so we have
// 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
// create a strong reference back to ourself and we want to be
// able to release that in unrealize.
const priv = self.private();
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
self.resizeOverlaySchedule();
}
return;
}
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 "";
// If we don't have a surface, then we initialize it.
self.initSurface() catch |err| {
log.warn("surface failed to initialize err={}", .{err});
};
});
priv.resize_overlay.schedule();
}
const RealizeError = Allocator.Error || error{
const InitError = Allocator.Error || error{
GLAreaError,
RendererError,
SurfaceError,
};
fn realizeSurface(self: *Self) RealizeError!void {
fn initSurface(self: *Self) InitError!void {
const priv = self.private();
assert(priv.core_surface == null);
const gl_area = priv.gl_area;
// 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;
}
// 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 alloc = app.allocator();
@ -2048,6 +2169,46 @@ pub const Surface = extern struct {
// Store it!
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(
@ -2136,8 +2297,10 @@ pub const Surface = extern struct {
gobject.ext.registerProperties(class, &.{
properties.config.impl,
properties.@"child-exited".impl,
properties.@"default-size".impl,
properties.@"font-size-request".impl,
properties.focused.impl,
properties.@"min-size".impl,
properties.@"mouse-shape".impl,
properties.@"mouse-hidden".impl,
properties.@"mouse-hover-url".impl,
@ -2151,6 +2314,8 @@ pub const Surface = extern struct {
signals.bell.impl.register(.{});
signals.@"clipboard-read".impl.register(.{});
signals.@"clipboard-write".impl.register(.{});
signals.init.impl.register(.{});
signals.@"present-request".impl.register(.{});
signals.@"toggle-fullscreen".impl.register(.{});
signals.@"toggle-maximize".impl.register(.{});
@ -2182,6 +2347,17 @@ pub const Surface = extern struct {
.{ .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.

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
*/
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
*/

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,29 +7,61 @@ template $GhosttyWindow: Adw.ApplicationWindow {
]
close-request => $close_request();
realize => $realize();
notify::background-opaque => $notify_background_opaque();
notify::config => $notify_config();
notify::fullscreened => $notify_fullscreened();
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-height: 600;
// 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)
handle-menubar-accel: false;
title: bind (template.active-surface as <$GhosttySurface>).title;
content: Box {
orientation: vertical;
content: Adw.TabOverview tab_overview {
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.ToolbarView toolbar {
top-bar-style: bind template.toolbar-style;
bottom-bar-style: bind template.toolbar-style;
[top]
Adw.HeaderBar {
visible: bind template.headerbar-visible;
title-widget: Adw.WindowTitle {
title: bind (template.active-surface as <$GhosttySurface>).title;
title: bind template.title;
};
[start]
Adw.SplitButton {
clicked => $new_tab();
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";
@ -40,16 +72,32 @@ template $GhosttyWindow: Adw.ApplicationWindow {
}
}
[top]
Adw.TabBar tab_bar {
autohide: bind template.tabs-autohide;
expand-tabs: bind template.tabs-wide;
view: tab_view;
visible: bind template.tabs-visible;
}
Box {
orientation: vertical;
$GhosttyDebugWarning {
visible: bind template.debug;
}
Adw.ToastOverlay toast_overlay {
$GhosttySurface surface {
close-request => $surface_close_request();
clipboard-write => $surface_clipboard_write();
toggle-fullscreen => $surface_toggle_fullscreen();
toggle-maximize => $surface_toggle_maximize();
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 input = @import("../../input.zig");
const key = @import("key.zig");
// TODO: As we get to these APIs the compiler should tell us
const ApprtWindow = void;
const ApprtWindow = @import("class/window.zig").Window;
pub const noop = @import("winproto/noop.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 input = @import("../../../input.zig");
const ApprtWindow = void; // TODO: fix
const ApprtWindow = @import("../class/window.zig").Window;
const log = std.log.scoped(.winproto_noop);

View File

@ -12,7 +12,7 @@ const wayland = @import("wayland");
const Config = @import("../../../config.zig").Config;
const input = @import("../../../input.zig");
const ApprtWindow = void; // TODO: fix
const ApprtWindow = @import("../class/window.zig").Window;
const wl = wayland.client.wl;
const org = wayland.client.org;
@ -127,7 +127,7 @@ pub const App = struct {
}
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.setLayer(window, .top);
@ -157,11 +157,19 @@ pub const App = struct {
const ctx_fields = @typeInfo(Context).@"struct".fields;
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
// 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;
return;
}
inline for (ctx_fields) |field| {
const T = getInterfaceType(field) orelse continue;
@ -170,7 +178,8 @@ pub const App = struct {
u8,
v.interface,
T.interface.name,
) != .eq) break :global;
) == .eq) {
log.debug("matched {}", .{T});
@field(context, field.name) = registry.bind(
v.name,
@ -184,6 +193,7 @@ pub const App = struct {
return;
};
}
}
},
// This should be a rare occurrence, but in case a global
@ -247,7 +257,7 @@ pub const Window = struct {
) !Window {
_ = 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;
// 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.
fn syncBlur(self: *Window) !void {
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| {
// Only release token when transitioning from blurred -> not blurred
@ -382,7 +396,7 @@ pub const Window = struct {
}
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,
.client => .Client,
.server => .Server,
@ -391,12 +405,15 @@ pub const Window = struct {
}
fn syncQuickTerminal(self: *Window) !void {
const window = self.apprt_window.window.as(gtk.Window);
const config = &self.apprt_window.config;
const window = self.apprt_window.as(gtk.Window);
const config = if (self.apprt_window.getConfig()) |v|
v.get()
else
return;
layer_shell.setKeyboardMode(
window,
switch (config.quick_terminal_keyboard_interactivity) {
switch (config.@"quick-terminal-keyboard-interactivity") {
.none => .none,
.@"on-demand" => on_demand: {
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,
.right => .right,
.top => .top,
@ -460,14 +477,14 @@ pub const Window = struct {
monitor: *gdk.Monitor,
apprt_window: *ApprtWindow,
) callconv(.c) void {
const window = apprt_window.window.as(gtk.Window);
const config = &apprt_window.config;
const window = apprt_window.as(gtk.Window);
const config = if (apprt_window.getConfig()) |v| v.get() else return;
var monitor_size: gdk.Rectangle = undefined;
monitor.getGeometry(&monitor_size);
const dims = config.quick_terminal_size.calculate(
config.quick_terminal_position,
const dims = config.@"quick-terminal-size".calculate(
config.@"quick-terminal-position",
.{
.width = @intCast(monitor_size.f_width),
.height = @intCast(monitor_size.f_height),

View File

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

View File

@ -157,11 +157,19 @@ pub const App = struct {
const ctx_fields = @typeInfo(Context).@"struct".fields;
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
// 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;
return;
}
inline for (ctx_fields) |field| {
const T = getInterfaceType(field) orelse continue;
@ -170,7 +178,8 @@ pub const App = struct {
u8,
v.interface,
T.interface.name,
) != .eq) break :global;
) == .eq) {
log.debug("matched {}", .{T});
@field(context, field.name) = registry.bind(
v.name,
@ -184,6 +193,7 @@ pub const App = struct {
return;
};
}
}
},
// This should be a rare occurrence, but in case a global

View File

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

View File

@ -7434,12 +7434,22 @@ pub const BackgroundBlur = union(enum) {
};
/// See window-decoration
pub const WindowDecoration = enum {
pub const WindowDecoration = enum(c_int) {
auto,
client,
server,
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 {
const input = input_ orelse return .auto;

View File

@ -9,7 +9,7 @@
# Author: Ryan Caloras (ryan@bashhub.com)
# Forked from Original Author: Glyph Lefkowitz
#
# V0.5.0
# V0.6.0
#
# General Usage:
@ -38,7 +38,7 @@
# Make sure this is bash that's running and return otherwise.
# Use POSIX syntax for this line:
if [ -z "${BASH_VERSION-}" ]; then
return 1;
return 1
fi
# We only support Bash 3.1+.
@ -95,7 +95,7 @@ __bp_adjust_histcontrol() {
# Replace ignoreboth with ignoredups
if [[ "$histcontrol" == *"ignoreboth"* ]]; then
histcontrol="ignoredups:${histcontrol//ignoreboth}"
fi;
fi
export HISTCONTROL="$histcontrol"
}
@ -136,7 +136,7 @@ __bp_sanitize_string() {
# 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.
__bp_interactive_mode() {
__bp_preexec_interactive_mode="on";
__bp_preexec_interactive_mode="on"
}
@ -289,7 +289,7 @@ __bp_preexec_invoke_exec() {
__bp_install() {
# Exit if we already have this installed.
if [[ "${PROMPT_COMMAND[*]:-}" == *"__bp_precmd_invoke_cmd"* ]]; then
return 1;
return 1
fi
trap '__bp_preexec_invoke_exec "$_"' DEBUG
@ -323,7 +323,7 @@ __bp_install() {
# Set so debug trap will work be invoked in subshells.
set -o functrace > /dev/null 2>&1
shopt -s extdebug > /dev/null 2>&1
fi;
fi
local existing_prompt_command
# 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
# shellcheck disable=SC2178 # PROMPT_COMMAND is not an array in bash <= 5.0
PROMPT_COMMAND=${sanitized_prompt_command}$'\n'
fi;
fi
# shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0
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.
if [[ -z "${__bp_delay_install:-}" ]]; then
__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
# 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

View File

@ -13,6 +13,21 @@
# You must gracefully exit Ghostty (do not SIGINT) by closing all windows
# 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
Memcheck:Leak
@ -135,10 +150,31 @@
...
fun:gsk_gpu_node_processor_process
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
...
}
@ -351,6 +387,7 @@
Memcheck:Leak
match-leak-kinds: possible
fun:*alloc
...
fun:FcFontSet*
...
fun:fc_thread_func