mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge branch 'main' into SetX11Atoms
This commit is contained in:
25
.github/workflows/nix.yml
vendored
25
.github/workflows/nix.yml
vendored
@ -1,6 +1,31 @@
|
||||
on: [push, pull_request]
|
||||
name: Nix
|
||||
jobs:
|
||||
required:
|
||||
name: "Required Checks: Nix"
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs:
|
||||
- check-zig-cache-hash
|
||||
steps:
|
||||
- id: status
|
||||
name: Determine status
|
||||
run: |
|
||||
results=$(tr -d '\n' <<< '${{ toJSON(needs.*.result) }}')
|
||||
if ! grep -q -v -E '(failure|cancelled)' <<< "$results"; then
|
||||
result="failed"
|
||||
else
|
||||
result="success"
|
||||
fi
|
||||
{
|
||||
echo "result=${result}"
|
||||
echo "results=${results}"
|
||||
} | tee -a "$GITHUB_OUTPUT"
|
||||
- if: always() && steps.status.outputs.result != 'success'
|
||||
name: Check for failed status
|
||||
run: |
|
||||
echo "One or more required build workflows failed: ${{ steps.status.outputs.results }}"
|
||||
exit 1
|
||||
|
||||
check-zig-cache-hash:
|
||||
if: github.repository == 'ghostty-org/ghostty'
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
|
45
.github/workflows/test.yml
vendored
45
.github/workflows/test.yml
vendored
@ -6,6 +6,45 @@ on:
|
||||
name: Test
|
||||
|
||||
jobs:
|
||||
required:
|
||||
name: "Required Checks: Test"
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs:
|
||||
- build
|
||||
- build-bench
|
||||
- build-linux-libghostty
|
||||
- build-nix
|
||||
- build-macos
|
||||
- build-macos-matrix
|
||||
- build-windows
|
||||
- test
|
||||
- test-gtk
|
||||
- test-sentry-linux
|
||||
- test-macos
|
||||
- prettier
|
||||
- alejandra
|
||||
- typos
|
||||
- test-pkg-linux
|
||||
steps:
|
||||
- id: status
|
||||
name: Determine status
|
||||
run: |
|
||||
results=$(tr -d '\n' <<< '${{ toJSON(needs.*.result) }}')
|
||||
if ! grep -q -v -E '(failure|cancelled)' <<< "$results"; then
|
||||
result="failed"
|
||||
else
|
||||
result="success"
|
||||
fi
|
||||
{
|
||||
echo "result=${result}"
|
||||
echo "results=${results}"
|
||||
} | tee -a "$GITHUB_OUTPUT"
|
||||
- if: always() && steps.status.outputs.result != 'success'
|
||||
name: Check for failed status
|
||||
run: |
|
||||
echo "One or more required build workflows failed: ${{ steps.status.outputs.results }}"
|
||||
exit 1
|
||||
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@ -342,7 +381,8 @@ jobs:
|
||||
matrix:
|
||||
adwaita: ["true", "false"]
|
||||
x11: ["true", "false"]
|
||||
name: GTK adwaita=${{ matrix.adwaita }} x11=${{ matrix.x11 }}
|
||||
wayland: ["true", "false"]
|
||||
name: GTK adwaita=${{ matrix.adwaita }} x11=${{ matrix.x11 }} wayland=${{ matrix.wayland }}
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
env:
|
||||
@ -374,7 +414,8 @@ jobs:
|
||||
zig build \
|
||||
-Dapp-runtime=gtk \
|
||||
-Dgtk-adwaita=${{ matrix.adwaita }} \
|
||||
-Dgtk-x11=${{ matrix.x11 }}
|
||||
-Dgtk-x11=${{ matrix.x11 }} \
|
||||
-Dgtk-wayland=${{ matrix.wayland }}
|
||||
|
||||
test-sentry-linux:
|
||||
strategy:
|
||||
|
@ -34,8 +34,8 @@
|
||||
.hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25",
|
||||
},
|
||||
.zig_wayland = .{
|
||||
.url = "https://codeberg.org/ifreund/zig-wayland/archive/0823d9116b80d65ecfad48a2efbca166c7b03497.tar.gz",
|
||||
.hash = "12205e05d4db71ef30aeb3517727382c12d294968e541090a762689acbb9038826a1",
|
||||
.url = "https://codeberg.org/ifreund/zig-wayland/archive/fbfe3b4ac0b472a27b1f1a67405436c58cbee12d.tar.gz",
|
||||
.hash = "12209ca054cb1919fa276e328967f10b253f7537c4136eb48f3332b0f7cf661cad38",
|
||||
},
|
||||
.zf = .{
|
||||
.url = "git+https://github.com/natecraddock/zf/?ref=main#ed99ca18b02dda052e20ba467e90b623c04690dd",
|
||||
|
@ -72,6 +72,7 @@
|
||||
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; };
|
||||
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; };
|
||||
A5CA378E2D31D6C300931030 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378D2D31D6C100931030 /* Weak.swift */; };
|
||||
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; };
|
||||
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0572C9F30860017A1AE /* Cursor.swift */; };
|
||||
A5CBD0592C9F37B10017A1AE /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
|
||||
@ -167,6 +168,7 @@
|
||||
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
|
||||
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = "<group>"; };
|
||||
A5CA378D2D31D6C100931030 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
|
||||
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = "<group>"; };
|
||||
A5CBD0572C9F30860017A1AE /* Cursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cursor.swift; sourceTree = "<group>"; };
|
||||
A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QuickTerminal.xib; sourceTree = "<group>"; };
|
||||
@ -282,6 +284,7 @@
|
||||
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
|
||||
A5985CD62C320C4500C57AD3 /* String+Extension.swift */,
|
||||
A5CC36142C9CDA03004D6760 /* View+Extension.swift */,
|
||||
A5CA378D2D31D6C100931030 /* Weak.swift */,
|
||||
C1F26EE72B76CBFC00404083 /* VibrantLayer.h */,
|
||||
C1F26EE82B76CBFC00404083 /* VibrantLayer.m */,
|
||||
A5CEAFDA29B8005900646FDA /* SplitView */,
|
||||
@ -647,6 +650,7 @@
|
||||
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */,
|
||||
A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */,
|
||||
A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */,
|
||||
A5CA378E2D31D6C300931030 /* Weak.swift in Sources */,
|
||||
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */,
|
||||
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
|
||||
A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
|
||||
|
@ -92,10 +92,8 @@ class AppDelegate: NSObject,
|
||||
return ProcessInfo.processInfo.systemUptime - applicationLaunchTime
|
||||
}
|
||||
|
||||
/// Tracks whether the application is currently visible. This can be gamed, i.e. if a user manually
|
||||
/// brings each window one by one to the front. But at worst its off by one set of toggles and this
|
||||
/// makes our logic very easy.
|
||||
private var isVisible: Bool = true
|
||||
/// Tracks the windows that we hid for toggleVisibility.
|
||||
private var hiddenWindows: [Weak<NSWindow>] = []
|
||||
|
||||
/// The observer for the app appearance.
|
||||
private var appearanceObserver: NSKeyValueObservation? = nil
|
||||
@ -219,15 +217,20 @@ class AppDelegate: NSObject,
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ notification: Notification) {
|
||||
guard !applicationHasBecomeActive else { return }
|
||||
applicationHasBecomeActive = true
|
||||
// If we're back then clear the hidden windows
|
||||
self.hiddenWindows = []
|
||||
|
||||
// Let's launch our first window. We only do this if we have no other windows. It
|
||||
// is possible to have other windows in a few scenarios:
|
||||
// - if we're opening a URL since `application(_:openFile:)` is called before this.
|
||||
// - if we're restoring from persisted state
|
||||
if terminalManager.windows.count == 0 && derivedConfig.initialWindow {
|
||||
terminalManager.newWindow()
|
||||
// First launch stuff
|
||||
if (!applicationHasBecomeActive) {
|
||||
applicationHasBecomeActive = true
|
||||
|
||||
// Let's launch our first window. We only do this if we have no other windows. It
|
||||
// is possible to have other windows in a few scenarios:
|
||||
// - if we're opening a URL since `application(_:openFile:)` is called before this.
|
||||
// - if we're restoring from persisted state
|
||||
if terminalManager.windows.count == 0 && derivedConfig.initialWindow {
|
||||
terminalManager.newWindow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -706,21 +709,23 @@ class AppDelegate: NSObject,
|
||||
|
||||
/// Toggles visibility of all Ghosty Terminal windows. When hidden, activates Ghostty as the frontmost application
|
||||
@IBAction func toggleVisibility(_ sender: Any) {
|
||||
// We only care about terminal windows.
|
||||
for window in NSApp.windows.filter({ $0.windowController is BaseTerminalController }) {
|
||||
if isVisible {
|
||||
window.orderOut(nil)
|
||||
} else {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
// If we have focus, then we hide all windows.
|
||||
if NSApp.isActive {
|
||||
// We need to keep track of the windows that were visible because we only
|
||||
// want to bring back these windows if we remove the toggle.
|
||||
self.hiddenWindows = NSApp.windows.filter { $0.isVisible }.map { Weak($0) }
|
||||
NSApp.hide(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// After bringing them all to front we make sure our app is active too.
|
||||
if !isVisible {
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
// If we're not active, we want to become active
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
|
||||
isVisible.toggle()
|
||||
// Bring all windows to the front. Note: we don't use NSApp.unhide because
|
||||
// that will unhide ALL hidden windows. We want to only bring forward the
|
||||
// ones that we hid.
|
||||
self.hiddenWindows.forEach { $0.value?.orderFrontRegardless() }
|
||||
self.hiddenWindows = []
|
||||
}
|
||||
|
||||
private struct DerivedConfig {
|
||||
|
@ -10,7 +10,7 @@ protocol TerminalViewDelegate: AnyObject {
|
||||
|
||||
/// The title of the terminal should change.
|
||||
func titleDidChange(to: String)
|
||||
|
||||
|
||||
/// The URL of the pwd should change.
|
||||
func pwdDidChange(to: URL?)
|
||||
|
||||
@ -56,15 +56,10 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
||||
|
||||
// The title for our window
|
||||
private var title: String {
|
||||
var title = "👻"
|
||||
|
||||
if let surfaceTitle = surfaceTitle {
|
||||
if (surfaceTitle.count > 0) {
|
||||
title = surfaceTitle
|
||||
}
|
||||
if let surfaceTitle, !surfaceTitle.isEmpty {
|
||||
return surfaceTitle
|
||||
}
|
||||
|
||||
return title
|
||||
return "👻"
|
||||
}
|
||||
|
||||
// The pwd of the focused surface as a URL
|
||||
@ -72,7 +67,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
||||
guard let surfacePwd, surfacePwd != "" else { return nil }
|
||||
return URL(fileURLWithPath: surfacePwd)
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
switch ghostty.readiness {
|
||||
case .loading:
|
||||
|
@ -12,7 +12,14 @@ extension Ghostty {
|
||||
// The current title of the surface as defined by the pty. This can be
|
||||
// changed with escape codes. This is public because the callbacks go
|
||||
// to the app level and it is set from there.
|
||||
@Published private(set) var title: String = "👻"
|
||||
@Published private(set) var title: String = "" {
|
||||
didSet {
|
||||
if !title.isEmpty {
|
||||
titleFallbackTimer?.invalidate()
|
||||
titleFallbackTimer = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The current pwd of the surface as defined by the pty. This can be
|
||||
// changed with escape codes.
|
||||
@ -113,6 +120,9 @@ extension Ghostty {
|
||||
// A small delay that is introduced before a title change to avoid flickers
|
||||
private var titleChangeTimer: Timer?
|
||||
|
||||
// A timer to fallback to ghost emoji if no title is set within the grace period
|
||||
private var titleFallbackTimer: Timer?
|
||||
|
||||
/// Event monitor (see individual events for why)
|
||||
private var eventMonitor: Any? = nil
|
||||
|
||||
@ -139,6 +149,13 @@ extension Ghostty {
|
||||
// can do SOMETHING.
|
||||
super.init(frame: NSMakeRect(0, 0, 800, 600))
|
||||
|
||||
// Set a timer to show the ghost emoji after 500ms if no title is set
|
||||
titleFallbackTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
|
||||
if let self = self, self.title.isEmpty {
|
||||
self.title = "👻"
|
||||
}
|
||||
}
|
||||
|
||||
// Before we initialize the surface we want to register our notifications
|
||||
// so there is no window where we can't receive them.
|
||||
let center = NotificationCenter.default
|
||||
|
9
macos/Sources/Helpers/Weak.swift
Normal file
9
macos/Sources/Helpers/Weak.swift
Normal file
@ -0,0 +1,9 @@
|
||||
/// A wrapper that holds a weak reference to an object. This lets us create native containers
|
||||
/// of weak references.
|
||||
class Weak<T: AnyObject> {
|
||||
weak var value: T?
|
||||
|
||||
init(_ value: T) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
|
||||
# more details.
|
||||
"sha256-MeSJiiSDDWZ7vUgY56t9aPSLPTgIKb4jexoHmDhJOGM="
|
||||
"sha256-Nx1tOhDnEZ7LVi/pKxYS3sg/Sf8TAUXDmST6EtBgDoQ="
|
||||
|
@ -36,8 +36,7 @@ const c = @import("c.zig").c;
|
||||
const version = @import("version.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
const key = @import("key.zig");
|
||||
const x11 = @import("x11.zig");
|
||||
const wayland = @import("wayland.zig");
|
||||
const winproto = @import("winproto.zig");
|
||||
const testing = std.testing;
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
@ -50,6 +49,9 @@ config: Config,
|
||||
app: *c.GtkApplication,
|
||||
ctx: *c.GMainContext,
|
||||
|
||||
/// State and logic for the underlying windowing protocol.
|
||||
winproto: winproto.App,
|
||||
|
||||
/// True if the app was launched with single instance mode.
|
||||
single_instance: bool,
|
||||
|
||||
@ -71,12 +73,6 @@ clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null,
|
||||
/// This is set to false when the main loop should exit.
|
||||
running: bool = true,
|
||||
|
||||
/// Xkb state (X11 only). Will be null on Wayland.
|
||||
x11_xkb: ?x11.Xkb = null,
|
||||
|
||||
/// Wayland app state. Will be null on X11.
|
||||
wayland: ?wayland.AppState = null,
|
||||
|
||||
/// The base path of the transient cgroup used to put all surfaces
|
||||
/// into their own cgroup. This is only set if cgroups are enabled
|
||||
/// and initialization was successful.
|
||||
@ -108,42 +104,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
c.gtk_get_micro_version(),
|
||||
});
|
||||
|
||||
// Disabling Vulkan can improve startup times by hundreds of
|
||||
// milliseconds on some systems. We don't use Vulkan so we can just
|
||||
// disable it.
|
||||
if (version.runtimeAtLeast(4, 16, 0)) {
|
||||
// From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE.
|
||||
// For the remainder of "why" see the 4.14 comment below.
|
||||
_ = internal_os.setenv("GDK_DISABLE", "gles-api,vulkan");
|
||||
_ = internal_os.setenv("GDK_DEBUG", "opengl,gl-no-fractional");
|
||||
} else if (version.runtimeAtLeast(4, 14, 0)) {
|
||||
// We need to export GDK_DEBUG to run on Wayland after GTK 4.14.
|
||||
// Older versions of GTK do not support these values so it is safe
|
||||
// to always set this. Forwards versions are uncertain so we'll have to
|
||||
// reassess...
|
||||
//
|
||||
// Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589
|
||||
//
|
||||
// Specific details about values:
|
||||
// - "opengl" - output OpenGL debug information
|
||||
// - "gl-disable-gles" - disable GLES, Ghostty can't use GLES
|
||||
// - "vulkan-disable" - disable Vulkan, Ghostty can't use Vulkan
|
||||
// and initializing a Vulkan context was causing a longer delay
|
||||
// on some systems.
|
||||
_ = internal_os.setenv("GDK_DEBUG", "opengl,gl-disable-gles,vulkan-disable,gl-no-fractional");
|
||||
} else {
|
||||
// Versions prior to 4.14 are a bit of an unknown for Ghostty. It
|
||||
// is an environment that isn't tested well and we don't have a
|
||||
// good understanding of what we may need to do.
|
||||
_ = internal_os.setenv("GDK_DEBUG", "vulkan-disable");
|
||||
}
|
||||
|
||||
if (version.runtimeAtLeast(4, 14, 0)) {
|
||||
// We need to export GSK_RENDERER to opengl because GTK uses ngl by
|
||||
// default after 4.14
|
||||
_ = internal_os.setenv("GSK_RENDERER", "opengl");
|
||||
}
|
||||
|
||||
// Load our configuration
|
||||
var config = try Config.load(core_app.alloc);
|
||||
errdefer config.deinit();
|
||||
@ -165,8 +125,111 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
}
|
||||
}
|
||||
|
||||
var gdk_debug: struct {
|
||||
/// output OpenGL debug information
|
||||
opengl: bool = false,
|
||||
/// disable GLES, Ghostty can't use GLES
|
||||
@"gl-disable-gles": bool = false,
|
||||
@"gl-no-fractional": bool = false,
|
||||
/// Disabling Vulkan can improve startup times by hundreds of
|
||||
/// milliseconds on some systems. We don't use Vulkan so we can just
|
||||
/// disable it.
|
||||
@"vulkan-disable": bool = false,
|
||||
} = .{
|
||||
.opengl = config.@"gtk-opengl-debug",
|
||||
};
|
||||
|
||||
var gdk_disable: struct {
|
||||
@"gles-api": bool = false,
|
||||
/// Disabling Vulkan can improve startup times by hundreds of
|
||||
/// milliseconds on some systems. We don't use Vulkan so we can just
|
||||
/// disable it.
|
||||
vulkan: bool = false,
|
||||
} = .{};
|
||||
|
||||
environment: {
|
||||
if (version.runtimeAtLeast(4, 16, 0)) {
|
||||
// From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE.
|
||||
// For the remainder of "why" see the 4.14 comment below.
|
||||
gdk_disable.@"gles-api" = true;
|
||||
gdk_disable.vulkan = true;
|
||||
gdk_debug.@"gl-no-fractional" = true;
|
||||
break :environment;
|
||||
}
|
||||
if (version.runtimeAtLeast(4, 14, 0)) {
|
||||
// We need to export GDK_DEBUG to run on Wayland after GTK 4.14.
|
||||
// Older versions of GTK do not support these values so it is safe
|
||||
// to always set this. Forwards versions are uncertain so we'll have
|
||||
// to reassess...
|
||||
//
|
||||
// Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589
|
||||
gdk_debug.@"gl-disable-gles" = true;
|
||||
gdk_debug.@"gl-no-fractional" = true;
|
||||
gdk_debug.@"vulkan-disable" = true;
|
||||
break :environment;
|
||||
}
|
||||
// Versions prior to 4.14 are a bit of an unknown for Ghostty. It
|
||||
// is an environment that isn't tested well and we don't have a
|
||||
// good understanding of what we may need to do.
|
||||
gdk_debug.@"vulkan-disable" = true;
|
||||
}
|
||||
|
||||
{
|
||||
var buf: [128]u8 = undefined;
|
||||
var fmt = std.io.fixedBufferStream(&buf);
|
||||
const writer = fmt.writer();
|
||||
var first: bool = true;
|
||||
inline for (@typeInfo(@TypeOf(gdk_debug)).Struct.fields) |field| {
|
||||
if (@field(gdk_debug, field.name)) {
|
||||
if (!first) try writer.writeAll(",");
|
||||
try writer.writeAll(field.name);
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
try writer.writeByte(0);
|
||||
const value = fmt.getWritten();
|
||||
log.warn("setting GDK_DEBUG={s}", .{value[0 .. value.len - 1]});
|
||||
_ = internal_os.setenv("GDK_DEBUG", value[0 .. value.len - 1 :0]);
|
||||
}
|
||||
|
||||
{
|
||||
var buf: [128]u8 = undefined;
|
||||
var fmt = std.io.fixedBufferStream(&buf);
|
||||
const writer = fmt.writer();
|
||||
var first: bool = true;
|
||||
inline for (@typeInfo(@TypeOf(gdk_disable)).Struct.fields) |field| {
|
||||
if (@field(gdk_disable, field.name)) {
|
||||
if (!first) try writer.writeAll(",");
|
||||
try writer.writeAll(field.name);
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
try writer.writeByte(0);
|
||||
const value = fmt.getWritten();
|
||||
log.warn("setting GDK_DISABLE={s}", .{value[0 .. value.len - 1]});
|
||||
_ = internal_os.setenv("GDK_DISABLE", value[0 .. value.len - 1 :0]);
|
||||
}
|
||||
|
||||
if (version.runtimeAtLeast(4, 14, 0)) {
|
||||
switch (config.@"gtk-gsk-renderer") {
|
||||
.default => {},
|
||||
else => |renderer| {
|
||||
// Force the GSK renderer to a specific value. After GTK 4.14 the
|
||||
// `ngl` renderer is used by default which causes artifacts when
|
||||
// used with Ghostty so it should be avoided.
|
||||
log.warn("setting GSK_RENDERER={s}", .{@tagName(renderer)});
|
||||
_ = internal_os.setenv("GSK_RENDERER", @tagName(renderer));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
c.gtk_init();
|
||||
const display = c.gdk_display_get_default();
|
||||
const display: *c.GdkDisplay = c.gdk_display_get_default() orelse {
|
||||
// I'm unsure of any scenario where this happens. Because we don't
|
||||
// want to litter null checks everywhere, we just exit here.
|
||||
log.warn("gdk display is null, exiting", .{});
|
||||
std.posix.exit(1);
|
||||
};
|
||||
|
||||
// If we're using libadwaita, log the version
|
||||
if (adwaita.enabled(&config)) {
|
||||
@ -364,46 +427,15 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
return error.GtkApplicationRegisterFailed;
|
||||
}
|
||||
|
||||
// Perform all X11 initialization. This ultimately returns the X11
|
||||
// keyboard state but the block does more than that (i.e. setting up
|
||||
// WM_CLASS).
|
||||
const x11_xkb: ?x11.Xkb = x11_xkb: {
|
||||
if (comptime !build_options.x11) break :x11_xkb null;
|
||||
if (!x11.is_display(display)) break :x11_xkb null;
|
||||
|
||||
// Set the X11 window class property (WM_CLASS) if are are on an X11
|
||||
// display.
|
||||
//
|
||||
// Note that we also set the program name here using g_set_prgname.
|
||||
// This is how the instance name field for WM_CLASS is derived when
|
||||
// calling gdk_x11_display_set_program_class; there does not seem to be
|
||||
// a way to set it directly. It does not look like this is being set by
|
||||
// our other app initialization routines currently, but since we're
|
||||
// currently deriving its value from x11-instance-name effectively, I
|
||||
// feel like gating it behind an X11 check is better intent.
|
||||
//
|
||||
// This makes the property show up like so when using xprop:
|
||||
//
|
||||
// WM_CLASS(STRING) = "ghostty", "com.mitchellh.ghostty"
|
||||
//
|
||||
// Append "-debug" on both when using the debug build.
|
||||
//
|
||||
const prgname = if (config.@"x11-instance-name") |pn|
|
||||
pn
|
||||
else if (builtin.mode == .Debug)
|
||||
"ghostty-debug"
|
||||
else
|
||||
"ghostty";
|
||||
c.g_set_prgname(prgname);
|
||||
c.gdk_x11_display_set_program_class(display, app_id);
|
||||
|
||||
// Set up Xkb
|
||||
break :x11_xkb try x11.Xkb.init(display);
|
||||
};
|
||||
|
||||
// Initialize Wayland state
|
||||
var wl = wayland.AppState.init(display);
|
||||
if (wl) |*w| try w.register();
|
||||
// Setup our windowing protocol logic
|
||||
var winproto_app = try winproto.App.init(
|
||||
core_app.alloc,
|
||||
display,
|
||||
app_id,
|
||||
&config,
|
||||
);
|
||||
errdefer winproto_app.deinit(core_app.alloc);
|
||||
log.debug("windowing protocol={s}", .{@tagName(winproto_app)});
|
||||
|
||||
// This just calls the `activate` signal but its part of the normal startup
|
||||
// routine so we just call it, but only if the config allows it (this allows
|
||||
@ -429,8 +461,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
.config = config,
|
||||
.ctx = ctx,
|
||||
.cursor_none = cursor_none,
|
||||
.x11_xkb = x11_xkb,
|
||||
.wayland = wl,
|
||||
.winproto = winproto_app,
|
||||
.single_instance = single_instance,
|
||||
// If we are NOT the primary instance, then we never want to run.
|
||||
// This means that another instance of the GTK app is running and
|
||||
@ -458,6 +489,8 @@ pub fn terminate(self: *App) void {
|
||||
}
|
||||
self.custom_css_providers.deinit(self.core_app.alloc);
|
||||
|
||||
self.winproto.deinit(self.core_app.alloc);
|
||||
|
||||
self.config.deinit();
|
||||
}
|
||||
|
||||
@ -882,9 +915,10 @@ fn configChange(
|
||||
new_config: *const Config,
|
||||
) void {
|
||||
switch (target) {
|
||||
.surface => |surface| {
|
||||
if (surface.rt_surface.container.window()) |window| window.syncAppearance(new_config) catch |err| {
|
||||
log.warn("error syncing appearance changes to window err={}", .{err});
|
||||
.surface => |surface| surface: {
|
||||
const window = surface.rt_surface.container.window() orelse break :surface;
|
||||
window.updateConfig(new_config) catch |err| {
|
||||
log.warn("error updating config for window err={}", .{err});
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -25,7 +25,6 @@ const ResizeOverlay = @import("ResizeOverlay.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
const gtk_key = @import("key.zig");
|
||||
const c = @import("c.zig").c;
|
||||
const x11 = @import("x11.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_surface);
|
||||
|
||||
@ -1412,6 +1411,14 @@ fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque)
|
||||
return;
|
||||
};
|
||||
|
||||
if (self.container.window()) |window| {
|
||||
if (window.winproto) |*winproto| {
|
||||
winproto.resizeEvent() catch |err| {
|
||||
log.warn("failed to notify window protocol of resize={}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
self.resize_overlay.maybeShow();
|
||||
}
|
||||
}
|
||||
@ -1727,11 +1734,10 @@ pub fn keyEvent(
|
||||
|
||||
// Get our modifier for the event
|
||||
const mods: input.Mods = gtk_key.eventMods(
|
||||
@ptrCast(self.gl_area),
|
||||
event,
|
||||
physical_key,
|
||||
gtk_mods,
|
||||
if (self.app.x11_xkb) |*xkb| xkb else null,
|
||||
&self.app.winproto,
|
||||
);
|
||||
|
||||
// Get our consumed modifiers
|
||||
|
@ -121,10 +121,63 @@ pub fn remove(self: *Tab) void {
|
||||
self.window.closeTab(self);
|
||||
}
|
||||
|
||||
pub fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
||||
/// Helper function to check if any surface in the split hierarchy needs close confirmation
|
||||
fn needsConfirm(elem: Surface.Container.Elem) bool {
|
||||
return switch (elem) {
|
||||
.surface => |s| s.core_surface.needsConfirmQuit(),
|
||||
.split => |s| needsConfirm(s.top_left) or needsConfirm(s.bottom_right),
|
||||
};
|
||||
}
|
||||
|
||||
/// Close the tab, asking for confirmation if any surface requests it.
|
||||
pub fn closeWithConfirmation(tab: *Tab) void {
|
||||
switch (tab.elem) {
|
||||
.surface => |s| s.close(s.core_surface.needsConfirmQuit()),
|
||||
.split => |s| {
|
||||
if (needsConfirm(s.top_left) or needsConfirm(s.bottom_right)) {
|
||||
const alert = c.gtk_message_dialog_new(
|
||||
tab.window.window,
|
||||
c.GTK_DIALOG_MODAL,
|
||||
c.GTK_MESSAGE_QUESTION,
|
||||
c.GTK_BUTTONS_YES_NO,
|
||||
"Close this tab?",
|
||||
);
|
||||
c.gtk_message_dialog_format_secondary_text(
|
||||
@ptrCast(alert),
|
||||
"All terminal sessions in this tab will be terminated.",
|
||||
);
|
||||
|
||||
// We want the "yes" to appear destructive.
|
||||
const yes_widget = c.gtk_dialog_get_widget_for_response(
|
||||
@ptrCast(alert),
|
||||
c.GTK_RESPONSE_YES,
|
||||
);
|
||||
c.gtk_widget_add_css_class(yes_widget, "destructive-action");
|
||||
|
||||
// We want the "no" to be the default action
|
||||
c.gtk_dialog_set_default_response(
|
||||
@ptrCast(alert),
|
||||
c.GTK_RESPONSE_NO,
|
||||
);
|
||||
|
||||
_ = c.g_signal_connect_data(alert, "response", c.G_CALLBACK(>kTabCloseConfirmation), tab, null, c.G_CONNECT_DEFAULT);
|
||||
c.gtk_widget_show(alert);
|
||||
return;
|
||||
}
|
||||
tab.remove();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn gtkTabCloseConfirmation(
|
||||
alert: *c.GtkMessageDialog,
|
||||
response: c.gint,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const tab: *Tab = @ptrCast(@alignCast(ud));
|
||||
const window = tab.window;
|
||||
window.closeTab(tab);
|
||||
c.gtk_window_destroy(@ptrCast(alert));
|
||||
if (response != c.GTK_RESPONSE_YES) return;
|
||||
tab.remove();
|
||||
}
|
||||
|
||||
fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||
@ -135,17 +188,3 @@ fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||
const tab: *Tab = @ptrCast(@alignCast(ud));
|
||||
tab.destroy(tab.window.app.core_app.alloc);
|
||||
}
|
||||
|
||||
pub fn gtkTabClick(
|
||||
gesture: *c.GtkGestureClick,
|
||||
_: c.gint,
|
||||
_: c.gdouble,
|
||||
_: c.gdouble,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *Tab = @ptrCast(@alignCast(ud));
|
||||
const gtk_button = c.gtk_gesture_single_get_current_button(@ptrCast(gesture));
|
||||
if (gtk_button == c.GDK_BUTTON_MIDDLE) {
|
||||
self.remove();
|
||||
}
|
||||
}
|
||||
|
@ -25,8 +25,7 @@ const gtk_key = @import("key.zig");
|
||||
const Notebook = @import("notebook.zig").Notebook;
|
||||
const HeaderBar = @import("headerbar.zig").HeaderBar;
|
||||
const version = @import("version.zig");
|
||||
const wayland = @import("wayland.zig");
|
||||
const x11 = @import("x11.zig");
|
||||
const winproto = @import("winproto.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
@ -38,7 +37,7 @@ window: *c.GtkWindow,
|
||||
/// The header bar for the window. This is possibly null since it can be
|
||||
/// disabled using gtk-titlebar. This is either an AdwHeaderBar or
|
||||
/// GtkHeaderBar depending on if adw is enabled and linked.
|
||||
header: ?HeaderBar,
|
||||
headerbar: HeaderBar,
|
||||
|
||||
/// The tab overview for the window. This is possibly null since there is no
|
||||
/// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0).
|
||||
@ -57,7 +56,8 @@ toast_overlay: ?*c.GtkWidget,
|
||||
/// See adwTabOverviewOpen for why we have this.
|
||||
adw_tab_overview_focus_timer: ?c.guint = null,
|
||||
|
||||
wayland: ?wayland.SurfaceState,
|
||||
/// State and logic for windowing protocol for a window.
|
||||
winproto: ?winproto.Window,
|
||||
|
||||
pub fn create(alloc: Allocator, app: *App) !*Window {
|
||||
// Allocate a fixed pointer for our window. We try to minimize
|
||||
@ -78,12 +78,12 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
self.* = .{
|
||||
.app = app,
|
||||
.window = undefined,
|
||||
.header = null,
|
||||
.headerbar = undefined,
|
||||
.tab_overview = null,
|
||||
.notebook = undefined,
|
||||
.context_menu = undefined,
|
||||
.toast_overlay = undefined,
|
||||
.wayland = null,
|
||||
.winproto = null,
|
||||
};
|
||||
|
||||
// Create the window
|
||||
@ -151,64 +151,56 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
break :overview tab_overview;
|
||||
} else null;
|
||||
|
||||
// gtk-titlebar can be used to disable the header bar (but keep
|
||||
// the window manager's decorations). We create this no matter if we
|
||||
// are decorated or not because we can have a keybind to toggle the
|
||||
// decorations.
|
||||
if (app.config.@"gtk-titlebar") {
|
||||
const header = HeaderBar.init(self);
|
||||
// gtk-titlebar can be used to disable the header bar (but keep the window
|
||||
// manager's decorations). We create this no matter if we are decorated or
|
||||
// not because we can have a keybind to toggle the decorations.
|
||||
self.headerbar.init();
|
||||
|
||||
// If we are not decorated then we hide the titlebar.
|
||||
header.setVisible(app.config.@"window-decoration");
|
||||
{
|
||||
const btn = c.gtk_menu_button_new();
|
||||
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
|
||||
c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic");
|
||||
c.gtk_menu_button_set_menu_model(@ptrCast(btn), @ptrCast(@alignCast(app.menu)));
|
||||
self.headerbar.packEnd(btn);
|
||||
}
|
||||
|
||||
{
|
||||
const btn = c.gtk_menu_button_new();
|
||||
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
|
||||
c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic");
|
||||
c.gtk_menu_button_set_menu_model(@ptrCast(btn), @ptrCast(@alignCast(app.menu)));
|
||||
header.packEnd(btn);
|
||||
}
|
||||
// If we're using an AdwWindow then we can support the tab overview.
|
||||
if (self.tab_overview) |tab_overview| {
|
||||
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||
assert(self.app.config.@"gtk-adwaita" and adwaita.versionAtLeast(1, 4, 0));
|
||||
const btn = switch (app.config.@"gtk-tabs-location") {
|
||||
.top, .bottom, .left, .right => btn: {
|
||||
const btn = c.gtk_toggle_button_new();
|
||||
c.gtk_widget_set_tooltip_text(btn, "View Open Tabs");
|
||||
c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic");
|
||||
_ = c.g_object_bind_property(
|
||||
btn,
|
||||
"active",
|
||||
tab_overview,
|
||||
"open",
|
||||
c.G_BINDING_BIDIRECTIONAL | c.G_BINDING_SYNC_CREATE,
|
||||
);
|
||||
|
||||
// If we're using an AdwWindow then we can support the tab overview.
|
||||
if (self.tab_overview) |tab_overview| {
|
||||
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||
assert(self.app.config.@"gtk-adwaita" and adwaita.versionAtLeast(1, 4, 0));
|
||||
const btn = switch (app.config.@"gtk-tabs-location") {
|
||||
.top, .bottom, .left, .right => btn: {
|
||||
const btn = c.gtk_toggle_button_new();
|
||||
c.gtk_widget_set_tooltip_text(btn, "View Open Tabs");
|
||||
c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic");
|
||||
_ = c.g_object_bind_property(
|
||||
btn,
|
||||
"active",
|
||||
tab_overview,
|
||||
"open",
|
||||
c.G_BINDING_BIDIRECTIONAL | c.G_BINDING_SYNC_CREATE,
|
||||
);
|
||||
break :btn btn;
|
||||
},
|
||||
|
||||
break :btn btn;
|
||||
},
|
||||
.hidden => btn: {
|
||||
const btn = c.adw_tab_button_new();
|
||||
c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.adw.tab_view);
|
||||
c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open");
|
||||
break :btn btn;
|
||||
},
|
||||
};
|
||||
|
||||
.hidden => btn: {
|
||||
const btn = c.adw_tab_button_new();
|
||||
c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.adw.tab_view);
|
||||
c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open");
|
||||
break :btn btn;
|
||||
},
|
||||
};
|
||||
c.gtk_widget_set_focus_on_click(btn, c.FALSE);
|
||||
self.headerbar.packEnd(btn);
|
||||
}
|
||||
|
||||
c.gtk_widget_set_focus_on_click(btn, c.FALSE);
|
||||
header.packEnd(btn);
|
||||
}
|
||||
|
||||
{
|
||||
const btn = c.gtk_button_new_from_icon_name("tab-new-symbolic");
|
||||
c.gtk_widget_set_tooltip_text(btn, "New Tab");
|
||||
_ = c.g_signal_connect_data(btn, "clicked", c.G_CALLBACK(>kTabNewClick), self, null, c.G_CONNECT_DEFAULT);
|
||||
header.packStart(btn);
|
||||
}
|
||||
|
||||
self.header = header;
|
||||
{
|
||||
const btn = c.gtk_button_new_from_icon_name("tab-new-symbolic");
|
||||
c.gtk_widget_set_tooltip_text(btn, "New Tab");
|
||||
_ = c.g_signal_connect_data(btn, "clicked", c.G_CALLBACK(>kTabNewClick), self, null, c.G_CONNECT_DEFAULT);
|
||||
self.headerbar.packStart(btn);
|
||||
}
|
||||
|
||||
_ = c.g_signal_connect_data(gtk_window, "notify::decorated", c.G_CALLBACK(>kWindowNotifyDecorated), self, null, c.G_CONNECT_DEFAULT);
|
||||
@ -222,9 +214,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
// If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we
|
||||
// need to stick the headerbar into the content box.
|
||||
if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
|
||||
if (self.header) |h| {
|
||||
c.gtk_box_append(@ptrCast(box), h.asWidget());
|
||||
}
|
||||
c.gtk_box_append(@ptrCast(box), self.headerbar.asWidget());
|
||||
}
|
||||
|
||||
// In debug we show a warning and apply the 'devel' class to the window.
|
||||
@ -299,10 +289,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
|
||||
const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new());
|
||||
|
||||
if (self.header) |header| {
|
||||
const header_widget = header.asWidget();
|
||||
c.adw_toolbar_view_add_top_bar(toolbar_view, header_widget);
|
||||
}
|
||||
c.adw_toolbar_view_add_top_bar(toolbar_view, self.headerbar.asWidget());
|
||||
|
||||
if (self.app.config.@"gtk-tabs-location" != .hidden) {
|
||||
const tab_bar = c.adw_tab_bar_new();
|
||||
@ -375,10 +362,8 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
box,
|
||||
);
|
||||
} else {
|
||||
c.gtk_window_set_titlebar(gtk_window, self.headerbar.asWidget());
|
||||
c.gtk_window_set_child(gtk_window, box);
|
||||
if (self.header) |h| {
|
||||
c.gtk_window_set_titlebar(gtk_window, h.asWidget());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -386,6 +371,16 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
c.gtk_widget_show(window);
|
||||
}
|
||||
|
||||
pub fn updateConfig(
|
||||
self: *Window,
|
||||
config: *const configpkg.Config,
|
||||
) !void {
|
||||
if (self.winproto) |*v| try v.updateConfigEvent(config);
|
||||
|
||||
// We always resync our appearance whenever the config changes.
|
||||
try self.syncAppearance(config);
|
||||
}
|
||||
|
||||
/// Updates appearance based on config settings. Will be called once upon window
|
||||
/// realization, and every time the config is reloaded.
|
||||
///
|
||||
@ -398,14 +393,10 @@ pub fn syncAppearance(self: *Window, config: *const configpkg.Config) !void {
|
||||
c.gtk_widget_add_css_class(@ptrCast(self.window), "background");
|
||||
}
|
||||
|
||||
if (self.wayland) |*wl| {
|
||||
const blurred = switch (config.@"background-blur-radius") {
|
||||
.false => false,
|
||||
.true => true,
|
||||
.radius => |v| v > 0,
|
||||
};
|
||||
try wl.setBlur(blurred);
|
||||
}
|
||||
// Window protocol specific appearance updates
|
||||
if (self.winproto) |*v| v.syncAppearance() catch |err| {
|
||||
log.warn("failed to sync window protocol appearance error={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// Sets up the GTK actions for the window scope. Actions are how GTK handles
|
||||
@ -445,7 +436,7 @@ fn initActions(self: *Window) void {
|
||||
pub fn deinit(self: *Window) void {
|
||||
c.gtk_widget_unparent(@ptrCast(self.context_menu));
|
||||
|
||||
if (self.wayland) |*wl| wl.deinit();
|
||||
if (self.winproto) |*v| v.deinit(self.app.core_app.alloc);
|
||||
|
||||
if (self.adw_tab_overview_focus_timer) |timer| {
|
||||
_ = c.g_source_remove(timer);
|
||||
@ -454,18 +445,12 @@ pub fn deinit(self: *Window) void {
|
||||
|
||||
/// Set the title of the window.
|
||||
pub fn setTitle(self: *Window, title: [:0]const u8) void {
|
||||
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config) and self.app.config.@"gtk-titlebar") {
|
||||
if (self.header) |header| header.setTitle(title);
|
||||
} else {
|
||||
c.gtk_window_set_title(self.window, title);
|
||||
}
|
||||
self.headerbar.setTitle(title);
|
||||
}
|
||||
|
||||
/// Set the subtitle of the window if it has one.
|
||||
pub fn setSubtitle(self: *Window, subtitle: [:0]const u8) void {
|
||||
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config) and self.app.config.@"gtk-titlebar") {
|
||||
if (self.header) |header| header.setSubtitle(subtitle);
|
||||
}
|
||||
self.headerbar.setSubtitle(subtitle);
|
||||
}
|
||||
|
||||
/// Add a new tab to this window.
|
||||
@ -558,9 +543,7 @@ pub fn toggleWindowDecorations(self: *Window) void {
|
||||
// decorated state. GTK tends to consider the titlebar part of the frame
|
||||
// and hides it with decorations, but libadwaita doesn't. This makes it
|
||||
// explicit.
|
||||
if (self.header) |headerbar| {
|
||||
headerbar.setVisible(new_decorated);
|
||||
}
|
||||
self.headerbar.setVisible(new_decorated);
|
||||
}
|
||||
|
||||
/// Grabs focus on the currently selected tab.
|
||||
@ -586,10 +569,19 @@ pub fn sendToast(self: *Window, title: [:0]const u8) void {
|
||||
fn gtkRealize(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
||||
const self = userdataSelf(ud.?);
|
||||
|
||||
if (self.app.wayland) |*wl| {
|
||||
self.wayland = wayland.SurfaceState.init(v, wl);
|
||||
// Initialize our window protocol logic
|
||||
if (winproto.Window.init(
|
||||
self.app.core_app.alloc,
|
||||
&self.app.winproto,
|
||||
v,
|
||||
&self.app.config,
|
||||
)) |winproto_win| {
|
||||
self.winproto = winproto_win;
|
||||
} else |err| {
|
||||
log.warn("failed to initialize window protocol error={}", .{err});
|
||||
}
|
||||
|
||||
// When we are realized we always setup our appearance
|
||||
self.syncAppearance(&self.app.config) catch |err| {
|
||||
log.err("failed to initialize appearance={}", .{err});
|
||||
};
|
||||
@ -624,7 +616,7 @@ fn gtkWindowNotifyMaximized(
|
||||
_: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const gdk_surface = c.gtk_native_get_surface(@ptrCast(window));
|
||||
if (!x11.should_use_net_wm_state(gdk_surface)) {
|
||||
if (!winproto.x11.should_use_net_wm_state(gdk_surface)) {
|
||||
log.warn("current WM does not support _NET_WM_STATE", .{});
|
||||
return;
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ pub const c = @cImport({
|
||||
// Add in X11-specific GDK backend which we use for specific things
|
||||
// (e.g. X11 window class).
|
||||
@cInclude("gdk/x11/gdkx.h");
|
||||
@cInclude("X11/Xlib.h");
|
||||
@cInclude("X11/Xatom.h");
|
||||
// Xkb for X11 state handling
|
||||
@cInclude("X11/XKBlib.h");
|
||||
}
|
||||
|
@ -4,93 +4,58 @@ const c = @import("c.zig").c;
|
||||
const Window = @import("Window.zig");
|
||||
const adwaita = @import("adwaita.zig");
|
||||
|
||||
const AdwHeaderBar = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwHeaderBar else void;
|
||||
const HeaderBarAdw = @import("headerbar_adw.zig");
|
||||
const HeaderBarGtk = @import("headerbar_gtk.zig");
|
||||
|
||||
pub const HeaderBar = union(enum) {
|
||||
adw: *AdwHeaderBar,
|
||||
gtk: *c.GtkHeaderBar,
|
||||
adw: HeaderBarAdw,
|
||||
gtk: HeaderBarGtk,
|
||||
|
||||
pub fn init(window: *Window) HeaderBar {
|
||||
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and
|
||||
adwaita.enabled(&window.app.config))
|
||||
{
|
||||
return initAdw(window);
|
||||
pub fn init(self: *HeaderBar) void {
|
||||
const window: *Window = @fieldParentPtr("headerbar", self);
|
||||
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.enabled(&window.app.config)) {
|
||||
HeaderBarAdw.init(self);
|
||||
} else {
|
||||
HeaderBarGtk.init(self);
|
||||
}
|
||||
|
||||
return initGtk();
|
||||
}
|
||||
|
||||
fn initAdw(window: *Window) HeaderBar {
|
||||
const headerbar = c.adw_header_bar_new();
|
||||
c.adw_header_bar_set_title_widget(@ptrCast(headerbar), @ptrCast(c.adw_window_title_new(c.gtk_window_get_title(window.window) orelse "Ghostty", null)));
|
||||
return .{ .adw = @ptrCast(headerbar) };
|
||||
}
|
||||
|
||||
fn initGtk() HeaderBar {
|
||||
const headerbar = c.gtk_header_bar_new();
|
||||
return .{ .gtk = @ptrCast(headerbar) };
|
||||
if (!window.app.config.@"gtk-titlebar" or !window.app.config.@"window-decoration")
|
||||
self.setVisible(false);
|
||||
}
|
||||
|
||||
pub fn setVisible(self: HeaderBar, visible: bool) void {
|
||||
c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
|
||||
switch (self) {
|
||||
inline else => |v| v.setVisible(visible),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn asWidget(self: HeaderBar) *c.GtkWidget {
|
||||
return switch (self) {
|
||||
.adw => |headerbar| @ptrCast(@alignCast(headerbar)),
|
||||
.gtk => |headerbar| @ptrCast(@alignCast(headerbar)),
|
||||
inline else => |v| v.asWidget(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn packEnd(self: HeaderBar, widget: *c.GtkWidget) void {
|
||||
switch (self) {
|
||||
.adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||
c.adw_header_bar_pack_end(
|
||||
@ptrCast(@alignCast(headerbar)),
|
||||
widget,
|
||||
);
|
||||
},
|
||||
.gtk => |headerbar| c.gtk_header_bar_pack_end(
|
||||
@ptrCast(@alignCast(headerbar)),
|
||||
widget,
|
||||
),
|
||||
inline else => |v| v.packEnd(widget),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn packStart(self: HeaderBar, widget: *c.GtkWidget) void {
|
||||
switch (self) {
|
||||
.adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||
c.adw_header_bar_pack_start(
|
||||
@ptrCast(@alignCast(headerbar)),
|
||||
widget,
|
||||
);
|
||||
},
|
||||
.gtk => |headerbar| c.gtk_header_bar_pack_start(
|
||||
@ptrCast(@alignCast(headerbar)),
|
||||
widget,
|
||||
),
|
||||
inline else => |v| v.packStart(widget),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setTitle(self: HeaderBar, title: [:0]const u8) void {
|
||||
switch (self) {
|
||||
.adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||
const window_title: *c.AdwWindowTitle = @ptrCast(c.adw_header_bar_get_title_widget(@ptrCast(headerbar)));
|
||||
c.adw_window_title_set_title(window_title, title);
|
||||
},
|
||||
// The title is owned by the window when not using Adwaita
|
||||
.gtk => unreachable,
|
||||
inline else => |v| v.setTitle(title),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setSubtitle(self: HeaderBar, subtitle: [:0]const u8) void {
|
||||
switch (self) {
|
||||
.adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||
const window_title: *c.AdwWindowTitle = @ptrCast(c.adw_header_bar_get_title_widget(@ptrCast(headerbar)));
|
||||
c.adw_window_title_set_subtitle(window_title, subtitle);
|
||||
},
|
||||
// There is no subtitle unless Adwaita is used
|
||||
.gtk => unreachable,
|
||||
inline else => |v| v.setSubtitle(subtitle),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
77
src/apprt/gtk/headerbar_adw.zig
Normal file
77
src/apprt/gtk/headerbar_adw.zig
Normal file
@ -0,0 +1,77 @@
|
||||
const HeaderBarAdw = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig").c;
|
||||
|
||||
const Window = @import("Window.zig");
|
||||
const adwaita = @import("adwaita.zig");
|
||||
|
||||
const HeaderBar = @import("headerbar.zig").HeaderBar;
|
||||
|
||||
const AdwHeaderBar = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwHeaderBar else anyopaque;
|
||||
const AdwWindowTitle = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwWindowTitle else anyopaque;
|
||||
|
||||
/// the window that this headerbar is attached to
|
||||
window: *Window,
|
||||
/// the Adwaita headerbar widget
|
||||
headerbar: *AdwHeaderBar,
|
||||
/// the Adwaita window title widget
|
||||
title: *AdwWindowTitle,
|
||||
|
||||
pub fn init(headerbar: *HeaderBar) void {
|
||||
if (!adwaita.versionAtLeast(0, 0, 0)) return;
|
||||
|
||||
const window: *Window = @fieldParentPtr("headerbar", headerbar);
|
||||
headerbar.* = .{
|
||||
.adw = .{
|
||||
.window = window,
|
||||
.headerbar = @ptrCast(@alignCast(c.adw_header_bar_new())),
|
||||
.title = @ptrCast(@alignCast(c.adw_window_title_new(
|
||||
c.gtk_window_get_title(window.window) orelse "Ghostty",
|
||||
null,
|
||||
))),
|
||||
},
|
||||
};
|
||||
c.adw_header_bar_set_title_widget(
|
||||
headerbar.adw.headerbar,
|
||||
@ptrCast(@alignCast(headerbar.adw.title)),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setVisible(self: HeaderBarAdw, visible: bool) void {
|
||||
c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
|
||||
}
|
||||
|
||||
pub fn asWidget(self: HeaderBarAdw) *c.GtkWidget {
|
||||
return @ptrCast(@alignCast(self.headerbar));
|
||||
}
|
||||
|
||||
pub fn packEnd(self: HeaderBarAdw, widget: *c.GtkWidget) void {
|
||||
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||
c.adw_header_bar_pack_end(
|
||||
@ptrCast(@alignCast(self.headerbar)),
|
||||
widget,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn packStart(self: HeaderBarAdw, widget: *c.GtkWidget) void {
|
||||
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||
c.adw_header_bar_pack_start(
|
||||
@ptrCast(@alignCast(self.headerbar)),
|
||||
widget,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setTitle(self: HeaderBarAdw, title: [:0]const u8) void {
|
||||
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||
c.adw_window_title_set_title(self.title, title);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setSubtitle(self: HeaderBarAdw, subtitle: [:0]const u8) void {
|
||||
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||
c.adw_window_title_set_subtitle(self.title, subtitle);
|
||||
}
|
||||
}
|
52
src/apprt/gtk/headerbar_gtk.zig
Normal file
52
src/apprt/gtk/headerbar_gtk.zig
Normal file
@ -0,0 +1,52 @@
|
||||
const HeaderBarGtk = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig").c;
|
||||
|
||||
const Window = @import("Window.zig");
|
||||
const adwaita = @import("adwaita.zig");
|
||||
|
||||
const HeaderBar = @import("headerbar.zig").HeaderBar;
|
||||
|
||||
/// the window that this headarbar is attached to
|
||||
window: *Window,
|
||||
/// the GTK headerbar widget
|
||||
headerbar: *c.GtkHeaderBar,
|
||||
|
||||
pub fn init(headerbar: *HeaderBar) void {
|
||||
const window: *Window = @fieldParentPtr("headerbar", headerbar);
|
||||
headerbar.* = .{
|
||||
.gtk = .{
|
||||
.window = window,
|
||||
.headerbar = @ptrCast(c.gtk_header_bar_new()),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setVisible(self: HeaderBarGtk, visible: bool) void {
|
||||
c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
|
||||
}
|
||||
|
||||
pub fn asWidget(self: HeaderBarGtk) *c.GtkWidget {
|
||||
return @ptrCast(@alignCast(self.headerbar));
|
||||
}
|
||||
|
||||
pub fn packEnd(self: HeaderBarGtk, widget: *c.GtkWidget) void {
|
||||
c.gtk_header_bar_pack_end(
|
||||
@ptrCast(@alignCast(self.headerbar)),
|
||||
widget,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn packStart(self: HeaderBarGtk, widget: *c.GtkWidget) void {
|
||||
c.gtk_header_bar_pack_start(
|
||||
@ptrCast(@alignCast(self.headerbar)),
|
||||
widget,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setTitle(self: HeaderBarGtk, title: [:0]const u8) void {
|
||||
c.gtk_window_set_title(self.window.window, title);
|
||||
}
|
||||
|
||||
pub fn setSubtitle(_: HeaderBarGtk, _: [:0]const u8) void {}
|
@ -2,7 +2,7 @@ const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
const input = @import("../../input.zig");
|
||||
const c = @import("c.zig").c;
|
||||
const x11 = @import("x11.zig");
|
||||
const winproto = @import("winproto.zig");
|
||||
|
||||
/// Returns a GTK accelerator string from a trigger.
|
||||
pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 {
|
||||
@ -105,34 +105,14 @@ pub fn keyvalUnicodeUnshifted(
|
||||
/// This requires a lot of context because the GdkEvent
|
||||
/// doesn't contain enough on its own.
|
||||
pub fn eventMods(
|
||||
widget: *c.GtkWidget,
|
||||
event: *c.GdkEvent,
|
||||
physical_key: input.Key,
|
||||
gtk_mods: c.GdkModifierType,
|
||||
x11_xkb: ?*x11.Xkb,
|
||||
app_winproto: *winproto.App,
|
||||
) input.Mods {
|
||||
const device = c.gdk_event_get_device(event);
|
||||
|
||||
var mods = mods: {
|
||||
// Add any modifier state events from Xkb if we have them (X11
|
||||
// only). Null back from the Xkb call means there was no modifier
|
||||
// event to read. This likely means that the key event did not
|
||||
// result in a modifier change and we can safely rely on the GDK
|
||||
// state.
|
||||
if (comptime build_options.x11) {
|
||||
const display = c.gtk_widget_get_display(widget);
|
||||
if (x11_xkb) |xkb| {
|
||||
if (xkb.modifier_state_from_notify(display)) |x11_mods| break :mods x11_mods;
|
||||
break :mods translateMods(gtk_mods);
|
||||
}
|
||||
}
|
||||
|
||||
// On Wayland, we have to use the GDK device because the mods sent
|
||||
// to this event do not have the modifier key applied if it was
|
||||
// pressed (i.e. left control).
|
||||
break :mods translateMods(c.gdk_device_get_modifier_state(device));
|
||||
};
|
||||
|
||||
var mods = app_winproto.eventMods(device, gtk_mods);
|
||||
mods.num_lock = c.gdk_device_get_num_lock_state(device) == 1;
|
||||
|
||||
switch (physical_key) {
|
||||
|
@ -17,6 +17,14 @@ pub const NotebookAdw = struct {
|
||||
/// the tab view
|
||||
tab_view: *AdwTabView,
|
||||
|
||||
/// Set to true so that the adw close-page handler knows we're forcing
|
||||
/// and to allow a close to happen with no confirm. This is a bit of a hack
|
||||
/// because we currently use GTK alerts to confirm tab close and they
|
||||
/// don't carry with them the ADW state that we are confirming or not.
|
||||
/// Long term we should move to ADW alerts so we can know if we are
|
||||
/// confirming or not.
|
||||
forcing_close: bool = false,
|
||||
|
||||
pub fn init(notebook: *Notebook) void {
|
||||
const window: *Window = @fieldParentPtr("notebook", notebook);
|
||||
const app = window.app;
|
||||
@ -38,6 +46,7 @@ pub const NotebookAdw = struct {
|
||||
};
|
||||
|
||||
_ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(tab_view, "close-page", c.G_CALLBACK(&adwClosePage), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT);
|
||||
}
|
||||
@ -112,6 +121,12 @@ pub const NotebookAdw = struct {
|
||||
pub fn closeTab(self: *NotebookAdw, tab: *Tab) void {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
|
||||
// closeTab always expects to close unconditionally so we mark this
|
||||
// as true so that the close_page call below doesn't request
|
||||
// confirmation.
|
||||
self.forcing_close = true;
|
||||
defer self.forcing_close = false;
|
||||
|
||||
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return;
|
||||
c.adw_tab_view_close_page(self.tab_view, page);
|
||||
|
||||
@ -143,6 +158,28 @@ fn adwPageAttached(_: *AdwTabView, page: *c.AdwTabPage, _: c_int, ud: ?*anyopaqu
|
||||
window.focusCurrentTab();
|
||||
}
|
||||
|
||||
fn adwClosePage(
|
||||
_: *AdwTabView,
|
||||
page: *c.AdwTabPage,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) c.gboolean {
|
||||
const child = c.adw_tab_page_get_child(page);
|
||||
const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(
|
||||
@ptrCast(child),
|
||||
Tab.GHOSTTY_TAB,
|
||||
) orelse return 0));
|
||||
|
||||
const window: *Window = @ptrCast(@alignCast(ud.?));
|
||||
const notebook = window.notebook.adw;
|
||||
c.adw_tab_view_close_page_finish(
|
||||
notebook.tab_view,
|
||||
page,
|
||||
@intFromBool(notebook.forcing_close),
|
||||
);
|
||||
if (!notebook.forcing_close) tab.closeWithConfirmation();
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn adwTabViewCreateWindow(
|
||||
_: *AdwTabView,
|
||||
ud: ?*anyopaque,
|
||||
|
@ -157,8 +157,8 @@ pub const NotebookGtk = struct {
|
||||
c.gtk_gesture_single_set_button(@ptrCast(gesture_tab_click), 0);
|
||||
c.gtk_widget_add_controller(label_box_widget, @ptrCast(gesture_tab_click));
|
||||
|
||||
_ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(&Tab.gtkTabCloseClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(&Tab.gtkTabClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(>kTabCloseClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(>kTabClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// Tab settings
|
||||
c.gtk_notebook_set_tab_reorderable(self.notebook, box_widget, 1);
|
||||
@ -283,3 +283,22 @@ fn gtkNotebookCreateWindow(
|
||||
|
||||
return newWindow.notebook.gtk.notebook;
|
||||
}
|
||||
|
||||
fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
||||
const tab: *Tab = @ptrCast(@alignCast(ud));
|
||||
tab.closeWithConfirmation();
|
||||
}
|
||||
|
||||
fn gtkTabClick(
|
||||
gesture: *c.GtkGestureClick,
|
||||
_: c.gint,
|
||||
_: c.gdouble,
|
||||
_: c.gdouble,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *Tab = @ptrCast(@alignCast(ud));
|
||||
const gtk_button = c.gtk_gesture_single_get_current_button(@ptrCast(gesture));
|
||||
if (gtk_button == c.GDK_BUTTON_MIDDLE) {
|
||||
self.closeWithConfirmation();
|
||||
}
|
||||
}
|
||||
|
@ -1,125 +0,0 @@
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig").c;
|
||||
const wayland = @import("wayland");
|
||||
const wl = wayland.client.wl;
|
||||
const org = wayland.client.org;
|
||||
const build_options = @import("build_options");
|
||||
|
||||
const log = std.log.scoped(.gtk_wayland);
|
||||
|
||||
/// Wayland state that contains application-wide Wayland objects (e.g. wl_display).
|
||||
pub const AppState = struct {
|
||||
display: *wl.Display,
|
||||
blur_manager: ?*org.KdeKwinBlurManager = null,
|
||||
|
||||
pub fn init(display: ?*c.GdkDisplay) ?AppState {
|
||||
if (comptime !build_options.wayland) return null;
|
||||
|
||||
// It should really never be null
|
||||
const display_ = display orelse return null;
|
||||
|
||||
// Check if we're actually on Wayland
|
||||
if (c.g_type_check_instance_is_a(
|
||||
@ptrCast(@alignCast(display_)),
|
||||
c.gdk_wayland_display_get_type(),
|
||||
) == 0)
|
||||
return null;
|
||||
|
||||
const wl_display: *wl.Display = @ptrCast(c.gdk_wayland_display_get_wl_display(display_) orelse return null);
|
||||
|
||||
return .{
|
||||
.display = wl_display,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn register(self: *AppState) !void {
|
||||
const registry = try self.display.getRegistry();
|
||||
|
||||
registry.setListener(*AppState, registryListener, self);
|
||||
if (self.display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
|
||||
log.debug("app wayland init={}", .{self});
|
||||
}
|
||||
};
|
||||
|
||||
/// Wayland state that contains Wayland objects associated with a window (e.g. wl_surface).
|
||||
pub const SurfaceState = struct {
|
||||
app_state: *AppState,
|
||||
surface: *wl.Surface,
|
||||
|
||||
/// A token that, when present, indicates that the window is blurred.
|
||||
blur_token: ?*org.KdeKwinBlur = null,
|
||||
|
||||
pub fn init(window: *c.GtkWindow, app_state: *AppState) ?SurfaceState {
|
||||
if (comptime !build_options.wayland) return null;
|
||||
|
||||
const surface = c.gtk_native_get_surface(@ptrCast(window)) orelse return null;
|
||||
|
||||
// Check if we're actually on Wayland
|
||||
if (c.g_type_check_instance_is_a(
|
||||
@ptrCast(@alignCast(surface)),
|
||||
c.gdk_wayland_surface_get_type(),
|
||||
) == 0)
|
||||
return null;
|
||||
|
||||
const wl_surface: *wl.Surface = @ptrCast(c.gdk_wayland_surface_get_wl_surface(surface) orelse return null);
|
||||
|
||||
return .{
|
||||
.app_state = app_state,
|
||||
.surface = wl_surface,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *SurfaceState) void {
|
||||
if (self.blur_token) |blur| blur.release();
|
||||
}
|
||||
|
||||
pub fn setBlur(self: *SurfaceState, blurred: bool) !void {
|
||||
log.debug("setting blur={}", .{blurred});
|
||||
|
||||
const mgr = self.app_state.blur_manager orelse {
|
||||
log.warn("can't set blur: org_kde_kwin_blur_manager protocol unavailable", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
if (self.blur_token) |blur| {
|
||||
// Only release token when transitioning from blurred -> not blurred
|
||||
if (!blurred) {
|
||||
mgr.unset(self.surface);
|
||||
blur.release();
|
||||
self.blur_token = null;
|
||||
}
|
||||
} else {
|
||||
// Only acquire token when transitioning from not blurred -> blurred
|
||||
if (blurred) {
|
||||
const blur_token = try mgr.create(self.surface);
|
||||
blur_token.commit();
|
||||
self.blur_token = blur_token;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, state: *AppState) void {
|
||||
switch (event) {
|
||||
.global => |global| {
|
||||
log.debug("got global interface={s}", .{global.interface});
|
||||
if (bindInterface(org.KdeKwinBlurManager, registry, global, 1)) |iface| {
|
||||
state.blur_manager = iface;
|
||||
return;
|
||||
}
|
||||
},
|
||||
.global_remove => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn bindInterface(comptime T: type, registry: *wl.Registry, global: anytype, version: u32) ?*T {
|
||||
if (std.mem.orderZ(u8, global.interface, T.interface.name) == .eq) {
|
||||
return registry.bind(global.name, T, version) catch |err| {
|
||||
log.warn("encountered error={} while binding interface {s}", .{ err, global.interface });
|
||||
return null;
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
128
src/apprt/gtk/winproto.zig
Normal file
128
src/apprt/gtk/winproto.zig
Normal file
@ -0,0 +1,128 @@
|
||||
const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const c = @import("c.zig").c;
|
||||
const Config = @import("../../config.zig").Config;
|
||||
const input = @import("../../input.zig");
|
||||
const key = @import("key.zig");
|
||||
|
||||
pub const noop = @import("winproto/noop.zig");
|
||||
pub const x11 = @import("winproto/x11.zig");
|
||||
pub const wayland = @import("winproto/wayland.zig");
|
||||
|
||||
pub const Protocol = enum {
|
||||
none,
|
||||
wayland,
|
||||
x11,
|
||||
};
|
||||
|
||||
/// App-state for the underlying windowing protocol. There should be one
|
||||
/// instance of this struct per application.
|
||||
pub const App = union(Protocol) {
|
||||
none: noop.App,
|
||||
wayland: if (build_options.wayland) wayland.App else noop.App,
|
||||
x11: if (build_options.x11) x11.App else noop.App,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
gdk_display: *c.GdkDisplay,
|
||||
app_id: [:0]const u8,
|
||||
config: *const Config,
|
||||
) !App {
|
||||
inline for (@typeInfo(App).Union.fields) |field| {
|
||||
if (try field.type.init(
|
||||
alloc,
|
||||
gdk_display,
|
||||
app_id,
|
||||
config,
|
||||
)) |v| {
|
||||
return @unionInit(App, field.name, v);
|
||||
}
|
||||
}
|
||||
|
||||
return .{ .none = .{} };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| v.deinit(alloc),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eventMods(
|
||||
self: *App,
|
||||
device: ?*c.GdkDevice,
|
||||
gtk_mods: c.GdkModifierType,
|
||||
) input.Mods {
|
||||
return switch (self.*) {
|
||||
inline else => |*v| v.eventMods(device, gtk_mods),
|
||||
} orelse key.translateMods(gtk_mods);
|
||||
}
|
||||
};
|
||||
|
||||
/// Per-Window state for the underlying windowing protocol.
|
||||
///
|
||||
/// In both X and Wayland, the terminology used is "Surface" and this is
|
||||
/// really "Surface"-specific state. But Ghostty uses the term "Surface"
|
||||
/// heavily to mean something completely different, so we use "Window" here
|
||||
/// to better match what it generally maps to in the Ghostty codebase.
|
||||
pub const Window = union(Protocol) {
|
||||
none: noop.Window,
|
||||
wayland: if (build_options.wayland) wayland.Window else noop.Window,
|
||||
x11: if (build_options.x11) x11.Window else noop.Window,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
app: *App,
|
||||
window: *c.GtkWindow,
|
||||
config: *const Config,
|
||||
) !Window {
|
||||
return switch (app.*) {
|
||||
inline else => |*v, tag| {
|
||||
inline for (@typeInfo(Window).Union.fields) |field| {
|
||||
if (comptime std.mem.eql(
|
||||
u8,
|
||||
field.name,
|
||||
@tagName(tag),
|
||||
)) return @unionInit(
|
||||
Window,
|
||||
field.name,
|
||||
try field.type.init(
|
||||
alloc,
|
||||
v,
|
||||
window,
|
||||
config,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Window, alloc: Allocator) void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| v.deinit(alloc),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resizeEvent(self: *Window) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.resizeEvent(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn updateConfigEvent(
|
||||
self: *Window,
|
||||
config: *const Config,
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.updateConfigEvent(config),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.syncAppearance(),
|
||||
}
|
||||
}
|
||||
};
|
56
src/apprt/gtk/winproto/noop.zig
Normal file
56
src/apprt/gtk/winproto/noop.zig
Normal file
@ -0,0 +1,56 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const c = @import("../c.zig").c;
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const input = @import("../../../input.zig");
|
||||
|
||||
const log = std.log.scoped(.winproto_noop);
|
||||
|
||||
pub const App = struct {
|
||||
pub fn init(
|
||||
_: Allocator,
|
||||
_: *c.GdkDisplay,
|
||||
_: [:0]const u8,
|
||||
_: *const Config,
|
||||
) !?App {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn eventMods(
|
||||
_: *App,
|
||||
_: ?*c.GdkDevice,
|
||||
_: c.GdkModifierType,
|
||||
) ?input.Mods {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Window = struct {
|
||||
pub fn init(
|
||||
_: Allocator,
|
||||
_: *App,
|
||||
_: *c.GtkWindow,
|
||||
_: *const Config,
|
||||
) !Window {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn updateConfigEvent(
|
||||
_: *Window,
|
||||
_: *const Config,
|
||||
) !void {}
|
||||
|
||||
pub fn resizeEvent(_: *Window) !void {}
|
||||
|
||||
pub fn syncAppearance(_: *Window) !void {}
|
||||
};
|
211
src/apprt/gtk/winproto/wayland.zig
Normal file
211
src/apprt/gtk/winproto/wayland.zig
Normal file
@ -0,0 +1,211 @@
|
||||
//! Wayland protocol implementation for the Ghostty GTK apprt.
|
||||
const std = @import("std");
|
||||
const wayland = @import("wayland");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const c = @import("../c.zig").c;
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const input = @import("../../../input.zig");
|
||||
|
||||
const wl = wayland.client.wl;
|
||||
const org = wayland.client.org;
|
||||
|
||||
const log = std.log.scoped(.winproto_wayland);
|
||||
|
||||
/// Wayland state that contains application-wide Wayland objects (e.g. wl_display).
|
||||
pub const App = struct {
|
||||
display: *wl.Display,
|
||||
context: *Context,
|
||||
|
||||
const Context = struct {
|
||||
kde_blur_manager: ?*org.KdeKwinBlurManager = null,
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
gdk_display: *c.GdkDisplay,
|
||||
app_id: [:0]const u8,
|
||||
config: *const Config,
|
||||
) !?App {
|
||||
_ = config;
|
||||
_ = app_id;
|
||||
|
||||
// Check if we're actually on Wayland
|
||||
if (c.g_type_check_instance_is_a(
|
||||
@ptrCast(@alignCast(gdk_display)),
|
||||
c.gdk_wayland_display_get_type(),
|
||||
) == 0) return null;
|
||||
|
||||
const display: *wl.Display = @ptrCast(c.gdk_wayland_display_get_wl_display(
|
||||
gdk_display,
|
||||
) orelse return error.NoWaylandDisplay);
|
||||
|
||||
// Create our context for our callbacks so we have a stable pointer.
|
||||
// Note: at the time of writing this comment, we don't really need
|
||||
// a stable pointer, but it's too scary that we'd need one in the future
|
||||
// and not have it and corrupt memory or something so let's just do it.
|
||||
const context = try alloc.create(Context);
|
||||
errdefer alloc.destroy(context);
|
||||
context.* = .{};
|
||||
|
||||
// Get our display registry so we can get all the available interfaces
|
||||
// and bind to what we need.
|
||||
const registry = try display.getRegistry();
|
||||
registry.setListener(*Context, registryListener, context);
|
||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
|
||||
return .{
|
||||
.display = display,
|
||||
.context = context,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
alloc.destroy(self.context);
|
||||
}
|
||||
|
||||
pub fn eventMods(
|
||||
_: *App,
|
||||
_: ?*c.GdkDevice,
|
||||
_: c.GdkModifierType,
|
||||
) ?input.Mods {
|
||||
return null;
|
||||
}
|
||||
|
||||
fn registryListener(
|
||||
registry: *wl.Registry,
|
||||
event: wl.Registry.Event,
|
||||
context: *Context,
|
||||
) void {
|
||||
switch (event) {
|
||||
// https://wayland.app/protocols/wayland#wl_registry:event:global
|
||||
.global => |global| global: {
|
||||
log.debug("wl_registry.global: interface={s}", .{global.interface});
|
||||
|
||||
if (registryBind(
|
||||
org.KdeKwinBlurManager,
|
||||
registry,
|
||||
global,
|
||||
1,
|
||||
)) |blur_manager| {
|
||||
context.kde_blur_manager = blur_manager;
|
||||
break :global;
|
||||
}
|
||||
},
|
||||
|
||||
// We don't handle removal events
|
||||
.global_remove => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn registryBind(
|
||||
comptime T: type,
|
||||
registry: *wl.Registry,
|
||||
global: anytype,
|
||||
version: u32,
|
||||
) ?*T {
|
||||
if (std.mem.orderZ(
|
||||
u8,
|
||||
global.interface,
|
||||
T.interface.name,
|
||||
) != .eq) return null;
|
||||
|
||||
return registry.bind(global.name, T, version) catch |err| {
|
||||
log.warn("error binding interface {s} error={}", .{
|
||||
global.interface,
|
||||
err,
|
||||
});
|
||||
return null;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Per-window (wl_surface) state for the Wayland protocol.
|
||||
pub const Window = struct {
|
||||
config: DerivedConfig,
|
||||
|
||||
/// The Wayland surface for this window.
|
||||
surface: *wl.Surface,
|
||||
|
||||
/// The context from the app where we can load our Wayland interfaces.
|
||||
app_context: *App.Context,
|
||||
|
||||
/// A token that, when present, indicates that the window is blurred.
|
||||
blur_token: ?*org.KdeKwinBlur = null,
|
||||
|
||||
const DerivedConfig = struct {
|
||||
blur: bool,
|
||||
|
||||
pub fn init(config: *const Config) DerivedConfig {
|
||||
return .{
|
||||
.blur = config.@"background-blur-radius".enabled(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
app: *App,
|
||||
gtk_window: *c.GtkWindow,
|
||||
config: *const Config,
|
||||
) !Window {
|
||||
_ = alloc;
|
||||
|
||||
const gdk_surface = c.gtk_native_get_surface(
|
||||
@ptrCast(gtk_window),
|
||||
) orelse return error.NotWaylandSurface;
|
||||
|
||||
// This should never fail, because if we're being called at this point
|
||||
// then we've already asserted that our app state is Wayland.
|
||||
if (c.g_type_check_instance_is_a(
|
||||
@ptrCast(@alignCast(gdk_surface)),
|
||||
c.gdk_wayland_surface_get_type(),
|
||||
) == 0) return error.NotWaylandSurface;
|
||||
|
||||
const wl_surface: *wl.Surface = @ptrCast(c.gdk_wayland_surface_get_wl_surface(
|
||||
gdk_surface,
|
||||
) orelse return error.NoWaylandSurface);
|
||||
|
||||
return .{
|
||||
.config = DerivedConfig.init(config),
|
||||
.surface = wl_surface,
|
||||
.app_context = app.context,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = alloc;
|
||||
if (self.blur_token) |blur| blur.release();
|
||||
}
|
||||
|
||||
pub fn updateConfigEvent(self: *Window, config: *const Config) !void {
|
||||
self.config = DerivedConfig.init(config);
|
||||
}
|
||||
|
||||
pub fn resizeEvent(_: *Window) !void {}
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
try self.syncBlur();
|
||||
}
|
||||
|
||||
/// 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.config.blur;
|
||||
|
||||
if (self.blur_token) |tok| {
|
||||
// Only release token when transitioning from blurred -> not blurred
|
||||
if (!blur) {
|
||||
manager.unset(self.surface);
|
||||
tok.release();
|
||||
self.blur_token = null;
|
||||
}
|
||||
} else {
|
||||
// Only acquire token when transitioning from not blurred -> blurred
|
||||
if (blur) {
|
||||
const tok = try manager.create(self.surface);
|
||||
tok.commit();
|
||||
self.blur_token = tok;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
293
src/apprt/gtk/winproto/x11.zig
Normal file
293
src/apprt/gtk/winproto/x11.zig
Normal file
@ -0,0 +1,293 @@
|
||||
//! X11 window protocol implementation for the Ghostty GTK apprt.
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const build_options = @import("build_options");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const c = @import("../c.zig").c;
|
||||
const input = @import("../../../input.zig");
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const adwaita = @import("../adwaita.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_x11);
|
||||
|
||||
pub const App = struct {
|
||||
display: *c.Display,
|
||||
base_event_code: c_int,
|
||||
kde_blur_atom: c.Atom,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
gdk_display: *c.GdkDisplay,
|
||||
app_id: [:0]const u8,
|
||||
config: *const Config,
|
||||
) !?App {
|
||||
_ = alloc;
|
||||
|
||||
// If the display isn't X11, then we don't need to do anything.
|
||||
if (c.g_type_check_instance_is_a(
|
||||
@ptrCast(@alignCast(gdk_display)),
|
||||
c.gdk_x11_display_get_type(),
|
||||
) == 0) return null;
|
||||
|
||||
// Get our X11 display
|
||||
const display: *c.Display = c.gdk_x11_display_get_xdisplay(
|
||||
gdk_display,
|
||||
) orelse return error.NoX11Display;
|
||||
|
||||
const x11_program_name: [:0]const u8 = if (config.@"x11-instance-name") |pn|
|
||||
pn
|
||||
else if (builtin.mode == .Debug)
|
||||
"ghostty-debug"
|
||||
else
|
||||
"ghostty";
|
||||
|
||||
// Set the X11 window class property (WM_CLASS) if are are on an X11
|
||||
// display.
|
||||
//
|
||||
// Note that we also set the program name here using g_set_prgname.
|
||||
// This is how the instance name field for WM_CLASS is derived when
|
||||
// calling gdk_x11_display_set_program_class; there does not seem to be
|
||||
// a way to set it directly. It does not look like this is being set by
|
||||
// our other app initialization routines currently, but since we're
|
||||
// currently deriving its value from x11-instance-name effectively, I
|
||||
// feel like gating it behind an X11 check is better intent.
|
||||
//
|
||||
// This makes the property show up like so when using xprop:
|
||||
//
|
||||
// WM_CLASS(STRING) = "ghostty", "com.mitchellh.ghostty"
|
||||
//
|
||||
// Append "-debug" on both when using the debug build.
|
||||
c.g_set_prgname(x11_program_name);
|
||||
c.gdk_x11_display_set_program_class(gdk_display, app_id);
|
||||
|
||||
// XKB
|
||||
log.debug("Xkb.init: initializing Xkb", .{});
|
||||
log.debug("Xkb.init: running XkbQueryExtension", .{});
|
||||
var opcode: c_int = 0;
|
||||
var base_event_code: c_int = 0;
|
||||
var base_error_code: c_int = 0;
|
||||
var major = c.XkbMajorVersion;
|
||||
var minor = c.XkbMinorVersion;
|
||||
if (c.XkbQueryExtension(
|
||||
display,
|
||||
&opcode,
|
||||
&base_event_code,
|
||||
&base_error_code,
|
||||
&major,
|
||||
&minor,
|
||||
) == 0) {
|
||||
log.err("Fatal: error initializing Xkb extension: error executing XkbQueryExtension", .{});
|
||||
return error.XkbInitializationError;
|
||||
}
|
||||
|
||||
log.debug("Xkb.init: running XkbSelectEventDetails", .{});
|
||||
if (c.XkbSelectEventDetails(
|
||||
display,
|
||||
c.XkbUseCoreKbd,
|
||||
c.XkbStateNotify,
|
||||
c.XkbModifierStateMask,
|
||||
c.XkbModifierStateMask,
|
||||
) == 0) {
|
||||
log.err("Fatal: error initializing Xkb extension: error executing XkbSelectEventDetails", .{});
|
||||
return error.XkbInitializationError;
|
||||
}
|
||||
|
||||
return .{
|
||||
.display = display,
|
||||
.base_event_code = base_event_code,
|
||||
.kde_blur_atom = c.gdk_x11_get_xatom_by_name_for_display(
|
||||
gdk_display,
|
||||
"_KDE_NET_WM_BLUR_BEHIND_REGION",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
/// Checks for an immediate pending XKB state update event, and returns the
|
||||
/// keyboard state based on if it finds any. This is necessary as the
|
||||
/// standard GTK X11 API (and X11 in general) does not include the current
|
||||
/// key pressed in any modifier state snapshot for that event (e.g. if the
|
||||
/// pressed key is a modifier, that is not necessarily reflected in the
|
||||
/// modifiers).
|
||||
///
|
||||
/// Returns null if there is no event. In this case, the caller should fall
|
||||
/// back to the standard GDK modifier state (this likely means the key
|
||||
/// event did not result in a modifier change).
|
||||
pub fn eventMods(
|
||||
self: App,
|
||||
device: ?*c.GdkDevice,
|
||||
gtk_mods: c.GdkModifierType,
|
||||
) ?input.Mods {
|
||||
_ = device;
|
||||
_ = gtk_mods;
|
||||
|
||||
// Shoutout to Mozilla for figuring out a clean way to do this, this is
|
||||
// paraphrased from Firefox/Gecko in widget/gtk/nsGtkKeyUtils.cpp.
|
||||
if (c.XEventsQueued(self.display, c.QueuedAfterReading) == 0) return null;
|
||||
|
||||
var nextEvent: c.XEvent = undefined;
|
||||
_ = c.XPeekEvent(self.display, &nextEvent);
|
||||
if (nextEvent.type != self.base_event_code) return null;
|
||||
|
||||
const xkb_event: *c.XkbEvent = @ptrCast(&nextEvent);
|
||||
if (xkb_event.any.xkb_type != c.XkbStateNotify) return null;
|
||||
|
||||
const xkb_state_notify_event: *c.XkbStateNotifyEvent = @ptrCast(xkb_event);
|
||||
// Check the state according to XKB masks.
|
||||
const lookup_mods = xkb_state_notify_event.lookup_mods;
|
||||
var mods: input.Mods = .{};
|
||||
|
||||
log.debug("X11: found extra XkbStateNotify event w/lookup_mods: {b}", .{lookup_mods});
|
||||
if (lookup_mods & c.ShiftMask != 0) mods.shift = true;
|
||||
if (lookup_mods & c.ControlMask != 0) mods.ctrl = true;
|
||||
if (lookup_mods & c.Mod1Mask != 0) mods.alt = true;
|
||||
if (lookup_mods & c.Mod4Mask != 0) mods.super = true;
|
||||
if (lookup_mods & c.LockMask != 0) mods.caps_lock = true;
|
||||
|
||||
return mods;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Window = struct {
|
||||
app: *App,
|
||||
config: DerivedConfig,
|
||||
window: c.Window,
|
||||
gtk_window: *c.GtkWindow,
|
||||
blur_region: Region,
|
||||
|
||||
const DerivedConfig = struct {
|
||||
blur: bool,
|
||||
|
||||
pub fn init(config: *const Config) DerivedConfig {
|
||||
return .{
|
||||
.blur = config.@"background-blur-radius".enabled(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
_: Allocator,
|
||||
app: *App,
|
||||
gtk_window: *c.GtkWindow,
|
||||
config: *const Config,
|
||||
) !Window {
|
||||
const surface = c.gtk_native_get_surface(
|
||||
@ptrCast(gtk_window),
|
||||
) orelse return error.NotX11Surface;
|
||||
|
||||
// Check if we're actually on X11
|
||||
if (c.g_type_check_instance_is_a(
|
||||
@ptrCast(@alignCast(surface)),
|
||||
c.gdk_x11_surface_get_type(),
|
||||
) == 0) return error.NotX11Surface;
|
||||
|
||||
const blur_region: Region = blur: {
|
||||
if ((comptime !adwaita.versionAtLeast(0, 0, 0)) or
|
||||
!adwaita.enabled(config)) break :blur .{};
|
||||
|
||||
// NOTE(pluiedev): CSDs are a f--king mistake.
|
||||
// Please, GNOME, stop this nonsense of making a window ~30% bigger
|
||||
// internally than how they really are just for your shadows and
|
||||
// rounded corners and all that fluff. Please. I beg of you.
|
||||
var x: f64 = 0;
|
||||
var y: f64 = 0;
|
||||
c.gtk_native_get_surface_transform(
|
||||
@ptrCast(gtk_window),
|
||||
&x,
|
||||
&y,
|
||||
);
|
||||
|
||||
break :blur .{
|
||||
.x = @intFromFloat(x),
|
||||
.y = @intFromFloat(y),
|
||||
};
|
||||
};
|
||||
|
||||
return .{
|
||||
.app = app,
|
||||
.config = DerivedConfig.init(config),
|
||||
.window = c.gdk_x11_surface_get_xid(surface),
|
||||
.gtk_window = gtk_window,
|
||||
.blur_region = blur_region,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn updateConfigEvent(
|
||||
self: *Window,
|
||||
config: *const Config,
|
||||
) !void {
|
||||
self.config = DerivedConfig.init(config);
|
||||
}
|
||||
|
||||
pub fn resizeEvent(self: *Window) !void {
|
||||
// The blur region must update with window resizes
|
||||
self.blur_region.width = c.gtk_widget_get_width(@ptrCast(self.gtk_window));
|
||||
self.blur_region.height = c.gtk_widget_get_height(@ptrCast(self.gtk_window));
|
||||
try self.syncBlur();
|
||||
}
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
try self.syncBlur();
|
||||
}
|
||||
|
||||
fn syncBlur(self: *Window) !void {
|
||||
// FIXME: This doesn't currently factor in rounded corners on Adwaita,
|
||||
// which means that the blur region will grow slightly outside of the
|
||||
// window borders. Unfortunately, actually calculating the rounded
|
||||
// region can be quite complex without having access to existing APIs
|
||||
// (cf. https://github.com/cutefishos/fishui/blob/41d4ba194063a3c7fff4675619b57e6ac0504f06/src/platforms/linux/blurhelper/windowblur.cpp#L134)
|
||||
// and I think it's not really noticeable enough to justify the effort.
|
||||
// (Wayland also has this visual artifact anyway...)
|
||||
|
||||
const blur = self.config.blur;
|
||||
log.debug("set blur={}, window xid={}, region={}", .{
|
||||
blur,
|
||||
self.window,
|
||||
self.blur_region,
|
||||
});
|
||||
|
||||
if (blur) {
|
||||
_ = c.XChangeProperty(
|
||||
self.app.display,
|
||||
self.window,
|
||||
self.app.kde_blur_atom,
|
||||
c.XA_CARDINAL,
|
||||
// Despite what you might think, the "32" here does NOT mean
|
||||
// that the data should be in u32s. Instead, they should be
|
||||
// c_longs, which on any 64-bit architecture would be obviously
|
||||
// 64 bits. WTF?!
|
||||
32,
|
||||
c.PropModeReplace,
|
||||
// SAFETY: Region is an extern struct that has the same
|
||||
// representation of 4 c_longs put next to each other.
|
||||
// Therefore, reinterpretation should be safe.
|
||||
// We don't have to care about endianness either since
|
||||
// Xlib converts it to network byte order for us.
|
||||
@ptrCast(std.mem.asBytes(&self.blur_region)),
|
||||
4,
|
||||
);
|
||||
} else {
|
||||
_ = c.XDeleteProperty(
|
||||
self.app.display,
|
||||
self.window,
|
||||
self.app.kde_blur_atom,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Region = extern struct {
|
||||
x: c_long = 0,
|
||||
y: c_long = 0,
|
||||
width: c_long = 0,
|
||||
height: c_long = 0,
|
||||
};
|
@ -604,7 +604,7 @@ palette: Palette = .{},
|
||||
///
|
||||
/// Supported on macOS and on some Linux desktop environments, including:
|
||||
///
|
||||
/// * KDE Plasma (Wayland only)
|
||||
/// * KDE Plasma (Wayland and X11)
|
||||
///
|
||||
/// Warning: the exact blur intensity is _ignored_ under KDE Plasma, and setting
|
||||
/// this setting to either `true` or any positive blur intensity value would
|
||||
@ -1387,16 +1387,14 @@ keybind: Keybinds = .{},
|
||||
@"image-storage-limit": u32 = 320 * 1000 * 1000,
|
||||
|
||||
/// Whether to automatically copy selected text to the clipboard. `true`
|
||||
/// will prefer to copy to the selection clipboard if supported by the
|
||||
/// OS, otherwise it will copy to the system clipboard.
|
||||
/// will prefer to copy to the selection clipboard, otherwise it will copy to
|
||||
/// the system clipboard.
|
||||
///
|
||||
/// The value `clipboard` will always copy text to the selection clipboard
|
||||
/// (for supported systems) as well as the system clipboard. This is sometimes
|
||||
/// a preferred behavior on Linux.
|
||||
/// as well as the system clipboard.
|
||||
///
|
||||
/// Middle-click paste will always use the selection clipboard on Linux
|
||||
/// and the system clipboard on macOS. Middle-click paste is always enabled
|
||||
/// even if this is `false`.
|
||||
/// Middle-click paste will always use the selection clipboard. Middle-click
|
||||
/// paste is always enabled even if this is `false`.
|
||||
///
|
||||
/// The default value is true on Linux and macOS.
|
||||
@"copy-on-select": CopyOnSelect = switch (builtin.os.tag) {
|
||||
@ -1977,6 +1975,18 @@ keybind: Keybinds = .{},
|
||||
/// must always be able to move themselves into an isolated cgroup.
|
||||
@"linux-cgroup-hard-fail": bool = false,
|
||||
|
||||
/// Enable or disable GTK's OpenGL debugging logs. The default is `true` for
|
||||
/// debug builds, `false` for all others.
|
||||
@"gtk-opengl-debug": bool = builtin.mode == .Debug,
|
||||
|
||||
/// After GTK 4.14.0, we need to force the GSK renderer to OpenGL as the default
|
||||
/// GSK renderer is broken on some systems. If you would like to override
|
||||
/// that bekavior, set `gtk-gsk-renderer=default` and either use your system's
|
||||
/// default GSK renderer, or set the GSK_RENDERER environment variable to your
|
||||
/// renderer of choice before launching Ghostty. This setting has no effect when
|
||||
/// using versions of GTK earlier than 4.14.0.
|
||||
@"gtk-gsk-renderer": GtkGskRenderer = .opengl,
|
||||
|
||||
/// If `true`, the Ghostty GTK application will run in single-instance mode:
|
||||
/// each new `ghostty` process launched will result in a new window if there is
|
||||
/// already a running process.
|
||||
@ -5782,6 +5792,14 @@ pub const BackgroundBlur = union(enum) {
|
||||
) catch return error.InvalidValue };
|
||||
}
|
||||
|
||||
pub fn enabled(self: BackgroundBlur) bool {
|
||||
return switch (self) {
|
||||
.false => false,
|
||||
.true => true,
|
||||
.radius => |v| v > 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cval(self: BackgroundBlur) u8 {
|
||||
return switch (self) {
|
||||
.false => 0,
|
||||
@ -6152,6 +6170,12 @@ pub const WindowPadding = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// See the `gtk-gsk-renderer` config.
|
||||
pub const GtkGskRenderer = enum {
|
||||
default,
|
||||
opengl,
|
||||
};
|
||||
|
||||
test "parse duration" {
|
||||
inline for (Duration.units) |unit| {
|
||||
var buf: [16]u8 = undefined;
|
||||
|
@ -3,7 +3,16 @@ const Config = @import("Config.zig");
|
||||
|
||||
/// This is the associated Vim file as named by the variable.
|
||||
pub const syntax = comptimeGenSyntax();
|
||||
pub const ftdetect = "au BufRead,BufNewFile */ghostty/config set ft=ghostty\n";
|
||||
pub const ftdetect =
|
||||
\\" Vim filetype detect file
|
||||
\\" Language: Ghostty config file
|
||||
\\" Maintainer: Ghostty <https://github.com/ghostty-org/ghostty>
|
||||
\\"
|
||||
\\" THIS FILE IS AUTO-GENERATED
|
||||
\\
|
||||
\\au BufRead,BufNewFile */ghostty/config,*/ghostty/themes/* set ft=ghostty
|
||||
\\
|
||||
;
|
||||
pub const ftplugin =
|
||||
\\" Vim filetype plugin file
|
||||
\\" Language: Ghostty config file
|
||||
@ -31,13 +40,19 @@ pub const ftplugin =
|
||||
\\
|
||||
;
|
||||
pub const compiler =
|
||||
\\" Vim compiler file
|
||||
\\" Language: Ghostty config file
|
||||
\\" Maintainer: Ghostty <https://github.com/ghostty-org/ghostty>
|
||||
\\"
|
||||
\\" THIS FILE IS AUTO-GENERATED
|
||||
\\
|
||||
\\if exists("current_compiler")
|
||||
\\ finish
|
||||
\\endif
|
||||
\\let current_compiler = "ghostty"
|
||||
\\
|
||||
\\CompilerSet makeprg=ghostty\ +validate-config
|
||||
\\CompilerSet errorformat=%f:%l:%m
|
||||
\\CompilerSet makeprg=ghostty\ +validate-config\ --config-file=%:S
|
||||
\\CompilerSet errorformat=%f:%l:%m,%m
|
||||
\\
|
||||
;
|
||||
|
||||
|
@ -441,10 +441,10 @@ pub const Action = union(enum) {
|
||||
toggle_quick_terminal: void,
|
||||
|
||||
/// Show/hide all windows. If all windows become shown, we also ensure
|
||||
/// Ghostty is focused.
|
||||
/// Ghostty becomes focused. When hiding all windows, focus is yielded
|
||||
/// to the next application as determined by the OS.
|
||||
///
|
||||
/// This currently only works on macOS. When hiding all windows, we do
|
||||
/// not yield focus to the previous application.
|
||||
/// This currently only works on macOS.
|
||||
toggle_visibility: void,
|
||||
|
||||
/// Quit ghostty.
|
||||
|
Reference in New Issue
Block a user