diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml
index 35ed2dc33..270bb86f5 100644
--- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml
+++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml
@@ -1,4 +1,4 @@
-labels: ["needs confirmation"]
+labels: ["needs-confirmation"]
body:
- type: markdown
attributes:
@@ -14,22 +14,37 @@ body:
description: |
Provide a detailed description of the issue. Include relevant information, such as:
- The feature or configuration option you encounter the issue with.
- - The expected behavior.
- - The actual behavior (and how it deviates from the expected behavior, if it is not immediately obvious).
- - Relevant Ghostty logs or other stacktraces.
- - Relevant screenshots, screen recordings, or other supporting media (as needed).
+ - Screenshots, screen recordings, or other supporting media (as needed).
- If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR, commit) in your description.
- >[!TIP]
+ > [!TIP]
> **Not sure what information to include?**
> Here are some recommendations:
- > - **Input issues:** include your keyboard layout, a screenshot of the terminal inspector's logged keystrokes (Linux: ctrl+shift+i; MacOS: cmd+alt+i), input method, Linux input method engine (IBus, Fcitx 5, or none) and its version.
+ > - **Input issues:** include your keyboard layout, a screenshot of logged keystrokes from the terminal inspector's "Keyboard" tab (Linux: ctrl+shift+i; MacOS: cmd+alt+i), input method, Linux input method engine (IBus, Fcitx 5, or none) and its version.
> - **Font issues:** include the problematic character(s), the output of `ghostty +show-face` for these character(s), and if they work in other applications.
- > - **VT issues (including image rendering issues):** attach an [asciinema](https://docs.asciinema.org/getting-started/) cast file, shell script, or text file for reproduction.
+ > - **Terminal emulation issues (including image rendering issues):** attach an [asciinema](https://docs.asciinema.org/getting-started/) cast file, shell script, or text file for reproduction.
> - **Renderer issues:** (Linux) include your OpenGL version, graphics card, driver version.
-
+ > - **Crashes:** (macOS) include the [Sentry UUID](https://github.com/ghostty-org/ghostty?tab=readme-ov-file#crash-reports); (Linux) try to reproduce using a debug build and provide the stack trace.
placeholder: |
- Example: When using SSH to connect to my remote Linux machine from my local macOS device in Ghostty, I try to run `clear`, and the screen does not clear. Instead, I see the following error message printed to the terminal: `Error opening terminal: xterm-ghostty.`
+ When using SSH to connect to my remote Linux machine from my local macOS device in Ghostty, I try to run `clear`, and the screen does not clear. Instead, I see the following error message printed to the terminal: `Error opening terminal: xterm-ghostty.`
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Expected Behavior
+ description: |
+ Describe how you expect Ghostty to behave in this situation. Include any relevant documentation links.
+ placeholder: |
+ The screen is cleared and the prompt is redrawn at the top of the window.
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Actual Behavior
+ description: |
+ Describe how Ghostty actually behaves in this situation. If it is not immediately obvious how the actual behavior differs from the expected behavior described above, please be sure to mention the deviation specifically.
+ placeholder: |
+ The screen is not cleared, and an error is printed: `Error opening terminal: xterm-ghostty`.
validations:
required: true
- type: textarea
@@ -44,6 +59,12 @@ body:
4. Observe `xterm-ghostty` error message above.
validations:
required: true
+ - type: textarea
+ attributes:
+ label: Ghostty Logs
+ description: |
+ Provide any captured Ghostty logs or stacktraces during your issue reproduction in this field. On Linux, logs can be found by running `ghostty` from the command-line; on macOS, logs can be viewed with `sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'` from another terminal emulator.
+ render: text
- type: textarea
attributes:
label: Ghostty Version
@@ -93,9 +114,9 @@ body:
required: false
- type: textarea
attributes:
- label: Ghostty Configuration
+ label: Minimal Ghostty Configuration
description: |
- Please provide the minimum configuration needed to reproduce this issue. If you cannot determine this, paste the output of `ghostty +show-config` here.
+ Please provide the **minimum** configuration needed to reproduce this issue. If you can still reproduce the issue with one of the lines removed, do not include that line. If and **only** if you are not able to determine this, paste the contents of your Ghostty configuration file here.
placeholder: |
font-family = CommitMono Nerd Font
font-family-bold = CommitMono Nerd Font
@@ -112,15 +133,15 @@ body:
attributes:
label: Additional Relevant Configuration
description: |
- If your issue involves other programs, tools, or applications in addition to Ghostty (e.g. Neovim, tmux, Zellij, etc.), please provide the minimum configuration needed for all relevant programs to reproduce the issue here. If you use custom CSS or shaders for Ghostty, also include them here, if applicable to your issue.
+ If your issue involves other programs, tools, or applications in addition to Ghostty (e.g. Neovim, tmux, Zellij, etc.), please provide the minimum configuration and versions needed for all relevant programs to reproduce the issue here. If you use custom CSS or shaders for Ghostty, also include them here, if applicable to your issue.
placeholder: |
- `tmux.conf`
- ---
+ #### `tmux.conf` (tmux 3.5a)
+ ```
set -g default-terminal "tmux-256color"
set-option -sa terminal-overrides ",xterm*:Tc"
set -g base-index 1
setw -g pane-base-index 1
- render: text
+ ```
validations:
required: false
- type: markdown
@@ -137,3 +158,5 @@ body:
required: true
- label: I have searched the Ghostty repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion.
required: true
+ - label: I have checked the "Preview" tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (` ``` `) on separate lines.
+ required: true
diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml
index e8e3fe99a..fed6d2db7 100644
--- a/.github/workflows/update-colorschemes.yml
+++ b/.github/workflows/update-colorschemes.yml
@@ -71,6 +71,7 @@ jobs:
build.zig.zon.nix
build.zig.zon.txt
build.zig.zon.json
+ flatpak/zig-packages.json
body: |
Upstream revision: https://github.com/mbadolato/iTerm2-Color-Schemes/tree/${{ steps.zig_fetch.outputs.upstream_rev }}
labels: dependencies
diff --git a/build.zig.zon b/build.zig.zon
index 180746436..7c5ff8ffc 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -103,8 +103,8 @@
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{
- .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5233095e442645995e8af1fcb7b011478ee86f32.tar.gz",
- .hash = "N-V-__8AAA38OASk6VOHVXwuyGVAeYu0nghqa1RSIliXV5ym",
+ .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/1e4957e65005908993250f8f07be3f70e805195e.tar.gz",
+ .hash = "N-V-__8AAHOASAQuLADcCSHLHEJiKFVLZiCD9Aq2rh5GT01A",
.lazy = true,
},
},
diff --git a/build.zig.zon.json b/build.zig.zon.json
index 640167615..513ee0dcd 100644
--- a/build.zig.zon.json
+++ b/build.zig.zon.json
@@ -54,10 +54,10 @@
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
},
- "N-V-__8AAA38OASk6VOHVXwuyGVAeYu0nghqa1RSIliXV5ym": {
+ "N-V-__8AAHOASAQuLADcCSHLHEJiKFVLZiCD9Aq2rh5GT01A": {
"name": "iterm2_themes",
- "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5233095e442645995e8af1fcb7b011478ee86f32.tar.gz",
- "hash": "sha256-Vy5muiJ3hJXcOvmFHLhqc+Dvdh74GG6+u/L+EsavDb0="
+ "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/1e4957e65005908993250f8f07be3f70e805195e.tar.gz",
+ "hash": "sha256-xpDitXpZrdU/EcgLyG4G0cEiT4r42viy+DJALmy2sQE="
},
"N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": {
"name": "libpng",
diff --git a/build.zig.zon.nix b/build.zig.zon.nix
index becb2ae87..46cf07cc9 100644
--- a/build.zig.zon.nix
+++ b/build.zig.zon.nix
@@ -170,11 +170,11 @@ in
};
}
{
- name = "N-V-__8AAA38OASk6VOHVXwuyGVAeYu0nghqa1RSIliXV5ym";
+ name = "N-V-__8AAHOASAQuLADcCSHLHEJiKFVLZiCD9Aq2rh5GT01A";
path = fetchZigArtifact {
name = "iterm2_themes";
- url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5233095e442645995e8af1fcb7b011478ee86f32.tar.gz";
- hash = "sha256-Vy5muiJ3hJXcOvmFHLhqc+Dvdh74GG6+u/L+EsavDb0=";
+ url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/1e4957e65005908993250f8f07be3f70e805195e.tar.gz";
+ hash = "sha256-xpDitXpZrdU/EcgLyG4G0cEiT4r42viy+DJALmy2sQE=";
};
}
{
diff --git a/build.zig.zon.txt b/build.zig.zon.txt
index a8cffe2b2..5f06418a7 100644
--- a/build.zig.zon.txt
+++ b/build.zig.zon.txt
@@ -27,7 +27,7 @@ https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz
https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst
-https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5233095e442645995e8af1fcb7b011478ee86f32.tar.gz
+https://github.com/mbadolato/iTerm2-Color-Schemes/archive/1e4957e65005908993250f8f07be3f70e805195e.tar.gz
https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz
https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz
diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json
index 0c36a600a..bc3b6cd0c 100644
--- a/flatpak/zig-packages.json
+++ b/flatpak/zig-packages.json
@@ -67,9 +67,9 @@
},
{
"type": "archive",
- "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5233095e442645995e8af1fcb7b011478ee86f32.tar.gz",
- "dest": "vendor/p/N-V-__8AAA38OASk6VOHVXwuyGVAeYu0nghqa1RSIliXV5ym",
- "sha256": "572e66ba22778495dc3af9851cb86a73e0ef761ef8186ebebbf2fe12c6af0dbd"
+ "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/1e4957e65005908993250f8f07be3f70e805195e.tar.gz",
+ "dest": "vendor/p/N-V-__8AAHOASAQuLADcCSHLHEJiKFVLZiCD9Aq2rh5GT01A",
+ "sha256": "c690e2b57a59add53f11c80bc86e06d1c1224f8af8daf8b2f832402e6cb6b101"
},
{
"type": "archive",
diff --git a/include/ghostty.h b/include/ghostty.h
index 18c547910..9409fa7c6 100644
--- a/include/ghostty.h
+++ b/include/ghostty.h
@@ -240,6 +240,9 @@ typedef enum {
GHOSTTY_KEY_KP_DELETE,
GHOSTTY_KEY_KP_BEGIN,
+ // special keys
+ GHOSTTY_KEY_CONTEXT_MENU,
+
// modifiers
GHOSTTY_KEY_LEFT_SHIFT,
GHOSTTY_KEY_LEFT_CONTROL,
diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift
index affbc4ddc..57a76dd43 100644
--- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift
+++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift
@@ -29,7 +29,6 @@ struct TerminalCommandPaletteView: View {
let key = String(cString: c.action_key)
switch (key) {
case "toggle_tab_overview",
- "toggle_maximize",
"toggle_window_decorations":
return false
default:
diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift
index b502e56e0..62384586a 100644
--- a/macos/Sources/Features/Terminal/BaseTerminalController.swift
+++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift
@@ -1,5 +1,6 @@
import Cocoa
import SwiftUI
+import Combine
import GhosttyKit
/// A base class for windows that can contain Ghostty windows. This base class implements
@@ -71,6 +72,9 @@ class BaseTerminalController: NSWindowController,
/// The configuration derived from the Ghostty config so we don't need to rely on references.
private var derivedConfig: DerivedConfig
+ /// The cancellables related to our focused surface.
+ private var focusedSurfaceCancellables: Set = []
+
struct SavedFrame {
let window: NSRect
let screen: NSRect
@@ -115,6 +119,11 @@ class BaseTerminalController: NSWindowController,
selector: #selector(ghosttyCommandPaletteDidToggle(_:)),
name: .ghosttyCommandPaletteDidToggle,
object: nil)
+ center.addObserver(
+ self,
+ selector: #selector(ghosttyMaximizeDidToggle(_:)),
+ name: .ghosttyMaximizeDidToggle,
+ object: nil)
// Listen for local events that we need to know of outside of
// single surface handlers.
@@ -234,6 +243,13 @@ class BaseTerminalController: NSWindowController,
toggleCommandPalette(nil)
}
+ @objc private func ghosttyMaximizeDidToggle(_ notification: Notification) {
+ guard let window else { return }
+ guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return }
+ guard surfaceTree?.contains(view: surfaceView) ?? false else { return }
+ window.zoom(nil)
+ }
+
// MARK: Local Events
private func localEventHandler(_ event: NSEvent) -> NSEvent? {
@@ -274,7 +290,26 @@ class BaseTerminalController: NSWindowController,
func surfaceTreeDidChange() {}
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {
+ let lastFocusedSurface = focusedSurface
focusedSurface = to
+
+ // Important to cancel any prior subscriptions
+ focusedSurfaceCancellables = []
+
+ // Setup our title listener. If we have a focused surface we always use that.
+ // Otherwise, we try to use our last focused surface. In either case, we only
+ // want to care if the surface is in the tree so we don't listen to titles of
+ // closed surfaces.
+ if let titleSurface = focusedSurface ?? lastFocusedSurface,
+ surfaceTree?.contains(view: titleSurface) ?? false {
+ // If we have a surface, we want to listen for title changes.
+ titleSurface.$title
+ .sink { [weak self] in self?.titleDidChange(to: $0) }
+ .store(in: &focusedSurfaceCancellables)
+ } else {
+ // There is no surface to listen to titles for.
+ titleDidChange(to: "👻")
+ }
}
func titleDidChange(to: String) {
@@ -361,14 +396,6 @@ class BaseTerminalController: NSWindowController,
}
}
- func fullscreenDidChange() {
- // For some reason focus can get lost when we change fullscreen. Regardless of
- // mode above we just move it back.
- if let focusedSurface {
- Ghostty.moveFocus(to: focusedSurface)
- }
- }
-
// MARK: Clipboard Confirmation
@objc private func onConfirmClipboardRequest(notification: SwiftUI.Notification) {
diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift
index f384b97ed..cf2dd3348 100644
--- a/macos/Sources/Features/Terminal/TerminalController.swift
+++ b/macos/Sources/Features/Terminal/TerminalController.swift
@@ -121,9 +121,7 @@ class TerminalController: BaseTerminalController {
}
- override func fullscreenDidChange() {
- super.fullscreenDidChange()
-
+ func fullscreenDidChange() {
// When our fullscreen state changes, we resync our appearance because some
// properties change when fullscreen or not.
guard let focusedSurface else { return }
diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift
index 1178c75a5..7caceb071 100644
--- a/macos/Sources/Features/Terminal/TerminalView.swift
+++ b/macos/Sources/Features/Terminal/TerminalView.swift
@@ -8,9 +8,6 @@ protocol TerminalViewDelegate: AnyObject {
/// Called when the currently focused surface changed. This can be nil.
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?)
- /// The title of the terminal should change.
- func titleDidChange(to: String)
-
/// The URL of the pwd should change.
func pwdDidChange(to: URL?)
@@ -59,19 +56,10 @@ struct TerminalView: View {
// Various state values sent back up from the currently focused terminals.
@FocusedValue(\.ghosttySurfaceView) private var focusedSurface
- @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle
@FocusedValue(\.ghosttySurfacePwd) private var surfacePwd
@FocusedValue(\.ghosttySurfaceZoomed) private var zoomedSplit
@FocusedValue(\.ghosttySurfaceCellSize) private var cellSize
- // The title for our window
- private var title: String {
- if let surfaceTitle, !surfaceTitle.isEmpty {
- return surfaceTitle
- }
- return "👻"
- }
-
// The pwd of the focused surface as a URL
private var pwdURL: URL? {
guard let surfacePwd, surfacePwd != "" else { return nil }
@@ -105,9 +93,6 @@ struct TerminalView: View {
self.delegate?.focusedSurfaceDidChange(to: newValue)
}
}
- .onChange(of: title) { newValue in
- self.delegate?.titleDidChange(to: newValue)
- }
.onChange(of: pwdURL) { newValue in
self.delegate?.pwdDidChange(to: newValue)
}
diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift
index c06287087..65e91ce83 100644
--- a/macos/Sources/Ghostty/Ghostty.App.swift
+++ b/macos/Sources/Ghostty/Ghostty.App.swift
@@ -526,6 +526,9 @@ extension Ghostty {
case GHOSTTY_ACTION_TOGGLE_COMMAND_PALETTE:
toggleCommandPalette(app, target: target)
+ case GHOSTTY_ACTION_TOGGLE_MAXIMIZE:
+ toggleMaximize(app, target: target)
+
case GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL:
toggleQuickTerminal(app, target: target)
@@ -770,6 +773,29 @@ extension Ghostty {
}
}
+ private static func toggleMaximize(
+ _ app: ghostty_app_t,
+ target: ghostty_target_s
+ ) {
+ switch (target.tag) {
+ case GHOSTTY_TARGET_APP:
+ Ghostty.logger.warning("toggle maximize does nothing with an app target")
+ return
+
+ case GHOSTTY_TARGET_SURFACE:
+ guard let surface = target.target.surface else { return }
+ guard let surfaceView = self.surfaceView(from: surface) else { return }
+ NotificationCenter.default.post(
+ name: .ghosttyMaximizeDidToggle,
+ object: surfaceView
+ )
+
+
+ default:
+ assertionFailure()
+ }
+ }
+
private static func toggleVisibility(
_ app: ghostty_app_t,
target: ghostty_target_s
diff --git a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift
index 127c925e1..3e942d774 100644
--- a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift
+++ b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift
@@ -45,8 +45,6 @@ extension Ghostty {
/// this one.
@Binding var zoomedSurface: SurfaceView?
- @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle: String?
-
var body: some View {
let center = NotificationCenter.default
let pubZoom = center.publisher(for: Notification.didToggleSplitZoom)
@@ -77,7 +75,6 @@ extension Ghostty {
.onReceive(pubZoom) { onZoom(notification: $0) }
}
}
- .navigationTitle(surfaceTitle ?? "Ghostty")
.id(node) // Needed for change detection on node
} else {
// On these events we want to reset the split state and call it.
diff --git a/macos/Sources/Ghostty/InspectorView.swift b/macos/Sources/Ghostty/InspectorView.swift
index b6147647e..a6e80bd47 100644
--- a/macos/Sources/Ghostty/InspectorView.swift
+++ b/macos/Sources/Ghostty/InspectorView.swift
@@ -31,7 +31,6 @@ extension Ghostty {
}, right: {
InspectorViewRepresentable(surfaceView: surfaceView)
.focused($inspectorFocus)
- .focusedValue(\.ghosttySurfaceTitle, surfaceView.title)
.focusedValue(\.ghosttySurfaceView, surfaceView)
})
}
diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift
index 366bb8113..30d5573df 100644
--- a/macos/Sources/Ghostty/Package.swift
+++ b/macos/Sources/Ghostty/Package.swift
@@ -279,6 +279,9 @@ extension Notification.Name {
/// Ring the bell
static let ghosttyBellDidRing = Notification.Name("com.mitchellh.ghostty.ghosttyBellDidRing")
static let ghosttyCommandPaletteDidToggle = Notification.Name("com.mitchellh.ghostty.commandPaletteDidToggle")
+
+ /// Toggle maximize of current window
+ static let ghosttyMaximizeDidToggle = Notification.Name("com.mitchellh.ghostty.maximizeDidToggle")
}
// NOTE: I am moving all of these to Notification.Name extensions over time. This
diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift
index 3b9c10067..1e9a4cfef 100644
--- a/macos/Sources/Ghostty/SurfaceView.swift
+++ b/macos/Sources/Ghostty/SurfaceView.swift
@@ -6,14 +6,12 @@ extension Ghostty {
/// Render a terminal for the active app in the environment.
struct Terminal: View {
@EnvironmentObject private var ghostty: Ghostty.App
- @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle: String?
var body: some View {
if let app = self.ghostty.app {
SurfaceForApp(app) { surfaceView in
SurfaceWrapper(surfaceView: surfaceView)
}
- .navigationTitle(surfaceTitle ?? "Ghostty")
}
}
}
@@ -83,7 +81,6 @@ extension Ghostty {
Surface(view: surfaceView, size: geo.size)
.focused($surfaceFocus)
- .focusedValue(\.ghosttySurfaceTitle, title)
.focusedValue(\.ghosttySurfacePwd, surfaceView.pwd)
.focusedValue(\.ghosttySurfaceView, surfaceView)
.focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize)
@@ -496,15 +493,6 @@ extension FocusedValues {
typealias Value = Ghostty.SurfaceView
}
- var ghosttySurfaceTitle: String? {
- get { self[FocusedGhosttySurfaceTitle.self] }
- set { self[FocusedGhosttySurfaceTitle.self] = newValue }
- }
-
- struct FocusedGhosttySurfaceTitle: FocusedValueKey {
- typealias Value = String
- }
-
var ghosttySurfacePwd: String? {
get { self[FocusedGhosttySurfacePwd.self] }
set { self[FocusedGhosttySurfacePwd.self] = newValue }
diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift
index b6fb08271..6094bf844 100644
--- a/macos/Sources/Helpers/Fullscreen.swift
+++ b/macos/Sources/Helpers/Fullscreen.swift
@@ -171,6 +171,13 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
guard let savedState = SavedState(window) else { return }
self.savedState = savedState
+ // Get our current first responder on this window. For non-native fullscreen
+ // we have to restore this because for some reason the operations below
+ // lose it (see: https://github.com/ghostty-org/ghostty/issues/6999).
+ // I don't know the root cause here so if we can figure that out there may
+ // be a nicer way than this.
+ let firstResponder = window.firstResponder
+
// We hide the dock if the window is on a screen with the dock.
// We must hide the dock FIRST then hide the menu:
// If you specify autoHideMenuBar, it must be accompanied by either hideDock or autoHideDock.
@@ -207,6 +214,10 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
// https://github.com/ghostty-org/ghostty/issues/1996
DispatchQueue.main.async {
self.window.setFrame(self.fullscreenFrame(screen), display: true)
+ if let firstResponder {
+ self.window.makeFirstResponder(firstResponder)
+ }
+
self.delegate?.fullscreenDidChange()
}
}
@@ -220,6 +231,9 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
let center = NotificationCenter.default
center.removeObserver(self, name: NSWindow.didChangeScreenNotification, object: window)
+ // See enter where we do the same thing to understand why.
+ let firstResponder = window.firstResponder
+
// Unhide our elements
if savedState.dock {
unhideDock()
@@ -258,6 +272,10 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
}
}
+ if let firstResponder {
+ window.makeFirstResponder(firstResponder)
+ }
+
// Unset our saved state, we're restored!
self.savedState = nil
@@ -355,16 +373,23 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
self.styleMask = window.styleMask
self.dock = window.screen?.hasDock ?? false
- // We hide the menu only if this window is not on any fullscreen
- // spaces. We do this because fullscreen spaces already hide the
- // menu and if we insert/remove this presentation option we get
- // issues (see #7075)
- let activeSpace = CGSSpace.active()
- let spaces = CGSSpace.list(for: window.cgWindowId)
- if spaces.contains(activeSpace) {
- self.menu = activeSpace.type != .fullscreen
+ if let cgWindowId = window.cgWindowId {
+ // We hide the menu only if this window is not on any fullscreen
+ // spaces. We do this because fullscreen spaces already hide the
+ // menu and if we insert/remove this presentation option we get
+ // issues (see #7075)
+ let activeSpace = CGSSpace.active()
+ let spaces = CGSSpace.list(for: cgWindowId)
+ if spaces.contains(activeSpace) {
+ self.menu = activeSpace.type != .fullscreen
+ } else {
+ self.menu = spaces.allSatisfy { $0.type != .fullscreen }
+ }
} else {
- self.menu = spaces.allSatisfy { $0.type != .fullscreen }
+ // Window doesn't have a window device, its not visible or something.
+ // In this case, we assume we can hide the menu. We may want to do
+ // something more sophisticated but this works for now.
+ self.menu = true
}
}
}
diff --git a/macos/Sources/Helpers/NSWindow+Extension.swift b/macos/Sources/Helpers/NSWindow+Extension.swift
index c7523bdb7..06a9fa4e0 100644
--- a/macos/Sources/Helpers/NSWindow+Extension.swift
+++ b/macos/Sources/Helpers/NSWindow+Extension.swift
@@ -2,7 +2,11 @@ import AppKit
extension NSWindow {
/// Get the CGWindowID type for the window (used for low level CoreGraphics APIs).
- var cgWindowId: CGWindowID {
- CGWindowID(windowNumber)
+ var cgWindowId: CGWindowID? {
+ // "If the window doesn’t have a window device, the value of this
+ // property is equal to or less than 0." - Docs. In practice I've
+ // found this is true if a window is not visible.
+ guard windowNumber > 0 else { return nil }
+ return CGWindowID(windowNumber)
}
}
diff --git a/pkg/cimgui/main.zig b/pkg/cimgui/main.zig
index e6e54c357..b890a49ee 100644
--- a/pkg/cimgui/main.zig
+++ b/pkg/cimgui/main.zig
@@ -1,20 +1,20 @@
pub const c = @import("c.zig").c;
// OpenGL
-pub extern fn ImGui_ImplOpenGL3_Init(?[*:0]const u8) callconv(.C) bool;
-pub extern fn ImGui_ImplOpenGL3_Shutdown() callconv(.C) void;
-pub extern fn ImGui_ImplOpenGL3_NewFrame() callconv(.C) void;
-pub extern fn ImGui_ImplOpenGL3_RenderDrawData(*c.ImDrawData) callconv(.C) void;
+pub extern fn ImGui_ImplOpenGL3_Init(?[*:0]const u8) callconv(.c) bool;
+pub extern fn ImGui_ImplOpenGL3_Shutdown() callconv(.c) void;
+pub extern fn ImGui_ImplOpenGL3_NewFrame() callconv(.c) void;
+pub extern fn ImGui_ImplOpenGL3_RenderDrawData(*c.ImDrawData) callconv(.c) void;
// Metal
-pub extern fn ImGui_ImplMetal_Init(*anyopaque) callconv(.C) bool;
-pub extern fn ImGui_ImplMetal_Shutdown() callconv(.C) void;
-pub extern fn ImGui_ImplMetal_NewFrame(*anyopaque) callconv(.C) void;
-pub extern fn ImGui_ImplMetal_RenderDrawData(*c.ImDrawData, *anyopaque, *anyopaque) callconv(.C) void;
+pub extern fn ImGui_ImplMetal_Init(*anyopaque) callconv(.c) bool;
+pub extern fn ImGui_ImplMetal_Shutdown() callconv(.c) void;
+pub extern fn ImGui_ImplMetal_NewFrame(*anyopaque) callconv(.c) void;
+pub extern fn ImGui_ImplMetal_RenderDrawData(*c.ImDrawData, *anyopaque, *anyopaque) callconv(.c) void;
// OSX
-pub extern fn ImGui_ImplOSX_Init(*anyopaque) callconv(.C) bool;
-pub extern fn ImGui_ImplOSX_Shutdown() callconv(.C) void;
-pub extern fn ImGui_ImplOSX_NewFrame(*anyopaque) callconv(.C) void;
+pub extern fn ImGui_ImplOSX_Init(*anyopaque) callconv(.c) bool;
+pub extern fn ImGui_ImplOSX_Shutdown() callconv(.c) void;
+pub extern fn ImGui_ImplOSX_NewFrame(*anyopaque) callconv(.c) void;
test {}
diff --git a/pkg/glfw/Joystick.zig b/pkg/glfw/Joystick.zig
index dd55c731d..a8152513e 100644
--- a/pkg/glfw/Joystick.zig
+++ b/pkg/glfw/Joystick.zig
@@ -333,7 +333,7 @@ pub inline fn setCallback(comptime callback: ?fn (joystick: Joystick, event: Eve
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn joystickCallbackWrapper(jid: c_int, event: c_int) callconv(.C) void {
+ pub fn joystickCallbackWrapper(jid: c_int, event: c_int) callconv(.c) void {
@call(.always_inline, user_callback, .{
Joystick{ .jid = @as(Joystick.Id, @enumFromInt(jid)) },
@as(Event, @enumFromInt(event)),
diff --git a/pkg/glfw/Monitor.zig b/pkg/glfw/Monitor.zig
index 868872e19..4accb23cd 100644
--- a/pkg/glfw/Monitor.zig
+++ b/pkg/glfw/Monitor.zig
@@ -389,7 +389,7 @@ pub inline fn setCallback(comptime callback: ?fn (monitor: Monitor, event: Event
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn monitorCallbackWrapper(monitor: ?*c.GLFWmonitor, event: c_int) callconv(.C) void {
+ pub fn monitorCallbackWrapper(monitor: ?*c.GLFWmonitor, event: c_int) callconv(.c) void {
@call(.always_inline, user_callback, .{
Monitor{ .handle = monitor.? },
@as(Event, @enumFromInt(event)),
diff --git a/pkg/glfw/Window.zig b/pkg/glfw/Window.zig
index 29dcac23e..804184f0e 100644
--- a/pkg/glfw/Window.zig
+++ b/pkg/glfw/Window.zig
@@ -1230,7 +1230,7 @@ pub inline fn setPosCallback(self: Window, comptime callback: ?fn (window: Windo
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn posCallbackWrapper(handle: ?*c.GLFWwindow, xpos: c_int, ypos: c_int) callconv(.C) void {
+ pub fn posCallbackWrapper(handle: ?*c.GLFWwindow, xpos: c_int, ypos: c_int) callconv(.c) void {
@call(.always_inline, user_callback, .{
from(handle.?),
@as(i32, @intCast(xpos)),
@@ -1263,7 +1263,7 @@ pub inline fn setSizeCallback(self: Window, comptime callback: ?fn (window: Wind
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn sizeCallbackWrapper(handle: ?*c.GLFWwindow, width: c_int, height: c_int) callconv(.C) void {
+ pub fn sizeCallbackWrapper(handle: ?*c.GLFWwindow, width: c_int, height: c_int) callconv(.c) void {
@call(.always_inline, user_callback, .{
from(handle.?),
@as(i32, @intCast(width)),
@@ -1304,7 +1304,7 @@ pub inline fn setCloseCallback(self: Window, comptime callback: ?fn (window: Win
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn closeCallbackWrapper(handle: ?*c.GLFWwindow) callconv(.C) void {
+ pub fn closeCallbackWrapper(handle: ?*c.GLFWwindow) callconv(.c) void {
@call(.always_inline, user_callback, .{
from(handle.?),
});
@@ -1341,7 +1341,7 @@ pub inline fn setRefreshCallback(self: Window, comptime callback: ?fn (window: W
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn refreshCallbackWrapper(handle: ?*c.GLFWwindow) callconv(.C) void {
+ pub fn refreshCallbackWrapper(handle: ?*c.GLFWwindow) callconv(.c) void {
@call(.always_inline, user_callback, .{
from(handle.?),
});
@@ -1379,7 +1379,7 @@ pub inline fn setFocusCallback(self: Window, comptime callback: ?fn (window: Win
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn focusCallbackWrapper(handle: ?*c.GLFWwindow, focused: c_int) callconv(.C) void {
+ pub fn focusCallbackWrapper(handle: ?*c.GLFWwindow, focused: c_int) callconv(.c) void {
@call(.always_inline, user_callback, .{
from(handle.?),
focused == c.GLFW_TRUE,
@@ -1413,7 +1413,7 @@ pub inline fn setIconifyCallback(self: Window, comptime callback: ?fn (window: W
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn iconifyCallbackWrapper(handle: ?*c.GLFWwindow, iconified: c_int) callconv(.C) void {
+ pub fn iconifyCallbackWrapper(handle: ?*c.GLFWwindow, iconified: c_int) callconv(.c) void {
@call(.always_inline, user_callback, .{
from(handle.?),
iconified == c.GLFW_TRUE,
@@ -1448,7 +1448,7 @@ pub inline fn setMaximizeCallback(self: Window, comptime callback: ?fn (window:
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn maximizeCallbackWrapper(handle: ?*c.GLFWwindow, maximized: c_int) callconv(.C) void {
+ pub fn maximizeCallbackWrapper(handle: ?*c.GLFWwindow, maximized: c_int) callconv(.c) void {
@call(.always_inline, user_callback, .{
from(handle.?),
maximized == c.GLFW_TRUE,
@@ -1483,7 +1483,7 @@ pub inline fn setFramebufferSizeCallback(self: Window, comptime callback: ?fn (w
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn framebufferSizeCallbackWrapper(handle: ?*c.GLFWwindow, width: c_int, height: c_int) callconv(.C) void {
+ pub fn framebufferSizeCallbackWrapper(handle: ?*c.GLFWwindow, width: c_int, height: c_int) callconv(.c) void {
@call(.always_inline, user_callback, .{
from(handle.?),
@as(u32, @intCast(width)),
@@ -1519,7 +1519,7 @@ pub inline fn setContentScaleCallback(self: Window, comptime callback: ?fn (wind
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn windowScaleCallbackWrapper(handle: ?*c.GLFWwindow, xscale: f32, yscale: f32) callconv(.C) void {
+ pub fn windowScaleCallbackWrapper(handle: ?*c.GLFWwindow, xscale: f32, yscale: f32) callconv(.c) void {
@call(.always_inline, user_callback, .{
from(handle.?),
xscale,
@@ -1871,7 +1871,7 @@ pub inline fn setKeyCallback(self: Window, comptime callback: ?fn (window: Windo
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn keyCallbackWrapper(handle: ?*c.GLFWwindow, key: c_int, scancode: c_int, action: c_int, mods: c_int) callconv(.C) void {
+ pub fn keyCallbackWrapper(handle: ?*c.GLFWwindow, key: c_int, scancode: c_int, action: c_int, mods: c_int) callconv(.c) void {
@call(.always_inline, user_callback, .{
from(handle.?),
@as(Key, @enumFromInt(key)),
@@ -1917,7 +1917,7 @@ pub inline fn setCharCallback(self: Window, comptime callback: ?fn (window: Wind
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn charCallbackWrapper(handle: ?*c.GLFWwindow, codepoint: c_uint) callconv(.C) void {
+ pub fn charCallbackWrapper(handle: ?*c.GLFWwindow, codepoint: c_uint) callconv(.c) void {
@call(.always_inline, user_callback, .{
from(handle.?),
@as(u21, @intCast(codepoint)),
@@ -1958,7 +1958,7 @@ pub inline fn setMouseButtonCallback(self: Window, comptime callback: ?fn (windo
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn mouseButtonCallbackWrapper(handle: ?*c.GLFWwindow, button: c_int, action: c_int, mods: c_int) callconv(.C) void {
+ pub fn mouseButtonCallbackWrapper(handle: ?*c.GLFWwindow, button: c_int, action: c_int, mods: c_int) callconv(.c) void {
@call(.always_inline, user_callback, .{
from(handle.?),
@as(MouseButton, @enumFromInt(button)),
@@ -1996,7 +1996,7 @@ pub inline fn setCursorPosCallback(self: Window, comptime callback: ?fn (window:
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn cursorPosCallbackWrapper(handle: ?*c.GLFWwindow, xpos: f64, ypos: f64) callconv(.C) void {
+ pub fn cursorPosCallbackWrapper(handle: ?*c.GLFWwindow, xpos: f64, ypos: f64) callconv(.c) void {
@call(.always_inline, user_callback, .{
from(handle.?),
xpos,
@@ -2030,7 +2030,7 @@ pub inline fn setCursorEnterCallback(self: Window, comptime callback: ?fn (windo
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn cursorEnterCallbackWrapper(handle: ?*c.GLFWwindow, entered: c_int) callconv(.C) void {
+ pub fn cursorEnterCallbackWrapper(handle: ?*c.GLFWwindow, entered: c_int) callconv(.c) void {
@call(.always_inline, user_callback, .{
from(handle.?),
entered == c.GLFW_TRUE,
@@ -2067,7 +2067,7 @@ pub inline fn setScrollCallback(self: Window, comptime callback: ?fn (window: Wi
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn scrollCallbackWrapper(handle: ?*c.GLFWwindow, xoffset: f64, yoffset: f64) callconv(.C) void {
+ pub fn scrollCallbackWrapper(handle: ?*c.GLFWwindow, xoffset: f64, yoffset: f64) callconv(.c) void {
@call(.always_inline, user_callback, .{
from(handle.?),
xoffset,
@@ -2110,7 +2110,7 @@ pub inline fn setDropCallback(self: Window, comptime callback: ?fn (window: Wind
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn dropCallbackWrapper(handle: ?*c.GLFWwindow, path_count: c_int, paths: [*c][*c]const u8) callconv(.C) void {
+ pub fn dropCallbackWrapper(handle: ?*c.GLFWwindow, path_count: c_int, paths: [*c][*c]const u8) callconv(.c) void {
@call(.always_inline, user_callback, .{
from(handle.?),
@as([*][*:0]const u8, @ptrCast(paths))[0..@as(u32, @intCast(path_count))],
diff --git a/pkg/glfw/errors.zig b/pkg/glfw/errors.zig
index ce98ec5cd..b9721fd05 100644
--- a/pkg/glfw/errors.zig
+++ b/pkg/glfw/errors.zig
@@ -300,7 +300,7 @@ pub inline fn mustGetErrorString() [:0]const u8 {
pub fn setErrorCallback(comptime callback: ?fn (error_code: ErrorCode, description: [:0]const u8) void) void {
if (callback) |user_callback| {
const CWrapper = struct {
- pub fn errorCallbackWrapper(err_int: c_int, c_description: [*c]const u8) callconv(.C) void {
+ pub fn errorCallbackWrapper(err_int: c_int, c_description: [*c]const u8) callconv(.c) void {
convertError(err_int) catch |error_code| {
user_callback(error_code, mem.sliceTo(c_description, 0));
};
diff --git a/pkg/glfw/opengl.zig b/pkg/glfw/opengl.zig
index de99582c2..04bc3a65c 100644
--- a/pkg/glfw/opengl.zig
+++ b/pkg/glfw/opengl.zig
@@ -161,7 +161,7 @@ pub const GLProc = *const fn () callconv(if (builtin.os.tag == .windows and buil
/// @thread_safety This function may be called from any thread.
///
/// see also: context_glext, glfwExtensionSupported
-pub fn getProcAddress(proc_name: [*:0]const u8) callconv(.C) ?GLProc {
+pub fn getProcAddress(proc_name: [*:0]const u8) callconv(.c) ?GLProc {
internal_debug.assertInitialized();
if (c.glfwGetProcAddress(proc_name)) |proc_address| return @ptrCast(proc_address);
return null;
diff --git a/pkg/glfw/vulkan.zig b/pkg/glfw/vulkan.zig
index 6c6021d02..1b84145d5 100644
--- a/pkg/glfw/vulkan.zig
+++ b/pkg/glfw/vulkan.zig
@@ -33,7 +33,7 @@ pub fn initVulkanLoader(loader_function: ?VKGetInstanceProcAddr) void {
c.glfwInitVulkanLoader(loader_function orelse null);
}
-pub const VKGetInstanceProcAddr = *const fn (vk_instance: c.VkInstance, name: [*c]const u8) callconv(.C) ?VKProc;
+pub const VKGetInstanceProcAddr = *const fn (vk_instance: c.VkInstance, name: [*c]const u8) callconv(.c) ?VKProc;
/// Returns whether the Vulkan loader and an ICD have been found.
///
@@ -127,7 +127,7 @@ pub const VKProc = *const fn () callconv(if (builtin.os.tag == .windows and buil
/// @pointer_lifetime The returned function pointer is valid until the library is terminated.
///
/// @thread_safety This function may be called from any thread.
-pub fn getInstanceProcAddress(vk_instance: ?*anyopaque, proc_name: [*:0]const u8) callconv(.C) ?VKProc {
+pub fn getInstanceProcAddress(vk_instance: ?*anyopaque, proc_name: [*:0]const u8) callconv(.c) ?VKProc {
internal_debug.assertInitialized();
if (c.glfwGetInstanceProcAddress(if (vk_instance) |v| @as(c.VkInstance, @ptrCast(v)) else null, proc_name)) |proc_address| return proc_address;
return null;
diff --git a/pkg/harfbuzz/blob.zig b/pkg/harfbuzz/blob.zig
index d25df6974..9472e4c75 100644
--- a/pkg/harfbuzz/blob.zig
+++ b/pkg/harfbuzz/blob.zig
@@ -77,11 +77,11 @@ pub const Blob = struct {
comptime T: type,
key: ?*anyopaque,
ptr: ?*T,
- comptime destroycb: ?*const fn (?*T) callconv(.C) void,
+ comptime destroycb: ?*const fn (?*T) callconv(.c) void,
replace: bool,
) bool {
const Callback = struct {
- pub fn callback(data: ?*anyopaque) callconv(.C) void {
+ pub fn callback(data: ?*anyopaque) callconv(.c) void {
@call(.{ .modifier = .always_inline }, destroycb, .{
@as(?*T, @ptrCast(@alignCast(data))),
});
diff --git a/pkg/macos/foundation/array.zig b/pkg/macos/foundation/array.zig
index 37fa2b985..d3a977539 100644
--- a/pkg/macos/foundation/array.zig
+++ b/pkg/macos/foundation/array.zig
@@ -84,7 +84,7 @@ pub const MutableArray = opaque {
a: *const Elem,
b: *const Elem,
context: ?*Context,
- ) callconv(.C) ComparisonResult,
+ ) callconv(.c) ComparisonResult,
) void {
CFArraySortValues(
self,
@@ -155,7 +155,7 @@ test "array sorting" {
void,
null,
struct {
- fn compare(a: *const u8, b: *const u8, _: ?*void) callconv(.C) ComparisonResult {
+ fn compare(a: *const u8, b: *const u8, _: ?*void) callconv(.c) ComparisonResult {
if (a.* > b.*) return .greater;
if (a.* == b.*) return .equal;
return .less;
diff --git a/pkg/macos/video/display_link.zig b/pkg/macos/video/display_link.zig
index ca0c80d0b..4bbf58a0c 100644
--- a/pkg/macos/video/display_link.zig
+++ b/pkg/macos/video/display_link.zig
@@ -66,7 +66,7 @@ pub const DisplayLink = opaque {
flagsIn: c.CVOptionFlags,
flagsOut: *c.CVOptionFlags,
inner_userinfo: ?*anyopaque,
- ) callconv(.C) c.CVReturn {
+ ) callconv(.c) c.CVReturn {
_ = inNow;
_ = inOutputTime;
_ = flagsIn;
diff --git a/pkg/opengl/glad.zig b/pkg/opengl/glad.zig
index 79a2e4d6b..663e75e12 100644
--- a/pkg/opengl/glad.zig
+++ b/pkg/opengl/glad.zig
@@ -13,8 +13,8 @@ pub threadlocal var context: Context = undefined;
/// The getProcAddress param is an anytype so that we can accept multiple
/// forms of the function depending on what we're interfacing with.
pub fn load(getProcAddress: anytype) !c_int {
- const GlProc = *const fn () callconv(.C) void;
- const GlfwFn = *const fn ([*:0]const u8) callconv(.C) ?GlProc;
+ const GlProc = *const fn () callconv(.c) void;
+ const GlfwFn = *const fn ([*:0]const u8) callconv(.c) ?GlProc;
const res = switch (@TypeOf(getProcAddress)) {
// glfw
diff --git a/pkg/sentry/transport.zig b/pkg/sentry/transport.zig
index 835b87cd3..747187211 100644
--- a/pkg/sentry/transport.zig
+++ b/pkg/sentry/transport.zig
@@ -5,8 +5,8 @@ const Envelope = @import("envelope.zig").Envelope;
/// sentry_transport_t
pub const Transport = opaque {
- pub const SendFunc = *const fn (envelope: *Envelope, state: ?*anyopaque) callconv(.C) void;
- pub const FreeFunc = *const fn (state: ?*anyopaque) callconv(.C) void;
+ pub const SendFunc = *const fn (envelope: *Envelope, state: ?*anyopaque) callconv(.c) void;
+ pub const FreeFunc = *const fn (state: ?*anyopaque) callconv(.c) void;
pub fn init(f: SendFunc) *Transport {
return @ptrCast(c.sentry_transport_new(@ptrCast(f)).?);
diff --git a/src/Surface.zig b/src/Surface.zig
index 6e62f6639..0d4c9d984 100644
--- a/src/Surface.zig
+++ b/src/Surface.zig
@@ -4119,6 +4119,14 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
}, .unlocked);
},
+ .scroll_to_selection => {
+ self.renderer_state.mutex.lock();
+ defer self.renderer_state.mutex.unlock();
+ const sel = self.io.terminal.screen.selection orelse return false;
+ const tl = sel.topLeft(&self.io.terminal.screen);
+ self.io.terminal.screen.scroll(.{ .pin = tl });
+ },
+
.scroll_page_up => {
const rows: isize = @intCast(self.size.grid().rows);
self.io.queueMessage(.{
diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig
index 22ae6e488..c953300cd 100644
--- a/src/apprt/embedded.zig
+++ b/src/apprt/embedded.zig
@@ -43,15 +43,15 @@ pub const App = struct {
/// Callback called to wakeup the event loop. This should trigger
/// a full tick of the app loop.
- wakeup: *const fn (AppUD) callconv(.C) void,
+ wakeup: *const fn (AppUD) callconv(.c) void,
/// Callback called to handle an action.
- action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.C) bool,
+ action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.c) bool,
/// Read the clipboard value. The return value must be preserved
/// by the host until the next call. If there is no valid clipboard
/// value then this should return null.
- read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.C) void,
+ read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.c) void,
/// This may be called after a read clipboard call to request
/// confirmation that the clipboard value is safe to read. The embedder
@@ -61,13 +61,13 @@ pub const App = struct {
[*:0]const u8,
*apprt.ClipboardRequest,
apprt.ClipboardRequestType,
- ) callconv(.C) void,
+ ) callconv(.c) void,
/// Write the clipboard value.
- write_clipboard: *const fn (SurfaceUD, [*:0]const u8, c_int, bool) callconv(.C) void,
+ write_clipboard: *const fn (SurfaceUD, [*:0]const u8, c_int, bool) callconv(.c) void,
/// Close the current surface given by this function.
- close_surface: ?*const fn (SurfaceUD, bool) callconv(.C) void = null,
+ close_surface: ?*const fn (SurfaceUD, bool) callconv(.c) void = null,
};
/// This is the key event sent for ghostty_surface_key and
diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig
index 2de22d8c2..06cc41b9d 100644
--- a/src/apprt/gtk/App.zig
+++ b/src/apprt/gtk/App.zig
@@ -74,6 +74,9 @@ cursor_none: ?*gdk.Cursor,
/// The clipboard confirmation window, if it is currently open.
clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null,
+/// The config errors dialog, if it is currently open.
+config_errors_dialog: ?ConfigErrorsDialog = null,
+
/// The window containing the quick terminal.
/// Null when never initialized.
quick_terminal: ?*Window = null,
@@ -159,6 +162,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
opengl: bool = false,
/// disable GLES, Ghostty can't use GLES
@"gl-disable-gles": bool = false,
+ // GTK's new renderer can cause blurry font when using fractional scaling.
@"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
@@ -190,7 +194,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
// 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 (gtk_version.runtimeAtLeast(4, 14, 0)) {
@@ -201,8 +204,12 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
//
// 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;
+
+ if (gtk_version.runtimeUntil(4, 17, 5)) {
+ // Removed at GTK v4.17.5
+ gdk_debug.@"gl-no-fractional" = true;
+ }
break :environment;
}
// Versions prior to 4.14 are a bit of an unknown for Ghostty. It
@@ -1523,7 +1530,7 @@ fn adwNotifyDark(
style_manager: *adw.StyleManager,
_: *gobject.ParamSpec,
self: *App,
-) callconv(.C) void {
+) callconv(.c) void {
const color_scheme: apprt.ColorScheme = if (style_manager.getDark() == 0)
.light
else
diff --git a/src/apprt/gtk/Builder.zig b/src/apprt/gtk/Builder.zig
index 028629200..dbd765ba3 100644
--- a/src/apprt/gtk/Builder.zig
+++ b/src/apprt/gtk/Builder.zig
@@ -18,88 +18,37 @@ pub fn init(
/// The minor version of the minimum Adwaita version that is required to use
/// this resource.
comptime minor: u16,
- /// `blp` signifies that the resource is a Blueprint that has been compiled
- /// to GTK Builder XML at compile time. `ui` signifies that the resource is
- /// a GTK Builder XML file that is included in the Ghostty source (perhaps
- /// because the Blueprint compiler on some target platforms cannot compile a
- /// Blueprint that generates the necessary resources).
- comptime kind: enum { blp, ui },
) Builder {
const resource_path = comptime resource_path: {
const gresource = @import("gresource.zig");
- switch (kind) {
- .blp => {
- // Check to make sure that our file is listed as a
- // `blueprint_file` in `gresource.zig`. If it isn't Ghostty
- // could crash at runtime when we try and load a nonexistent
- // GResource.
- for (gresource.blueprint_files) |file| {
- if (major != file.major or minor != file.minor or !std.mem.eql(u8, file.name, name)) continue;
- // Use @embedFile to make sure that the `.blp` file exists
- // at compile time. Zig _should_ discard the data so that
- // it doesn't end up in the final executable. At runtime we
- // will load the data from a GResource.
- const blp_filename = std.fmt.comptimePrint(
- "ui/{d}.{d}/{s}.blp",
- .{
- file.major,
- file.minor,
- file.name,
- },
- );
- _ = @embedFile(blp_filename);
- break :resource_path std.fmt.comptimePrint(
- "/com/mitchellh/ghostty/ui/{d}.{d}/{s}.ui",
- .{
- file.major,
- file.minor,
- file.name,
- },
- );
- } else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig");
- },
- .ui => {
- // Check to make sure that our file is listed as a `ui_file` in
- // `gresource.zig`. If it isn't Ghostty could crash at runtime
- // when we try and load a nonexistent GResource.
- for (gresource.ui_files) |file| {
- if (major != file.major or minor != file.minor or !std.mem.eql(u8, file.name, name)) continue;
- // Use @embedFile to make sure that the `.ui` file exists
- // at compile time. Zig _should_ discard the data so that
- // it doesn't end up in the final executable. At runtime we
- // will load the data from a GResource.
- const ui_filename = std.fmt.comptimePrint(
- "ui/{d}.{d}/{s}.ui",
- .{
- file.major,
- file.minor,
- file.name,
- },
- );
- _ = @embedFile(ui_filename);
- // Also use @embedFile to make sure that a matching `.blp`
- // file exists at compile time. Zig _should_ discard the
- // data so that it doesn't end up in the final executable.
- const blp_filename = std.fmt.comptimePrint(
- "ui/{d}.{d}/{s}.blp",
- .{
- file.major,
- file.minor,
- file.name,
- },
- );
- _ = @embedFile(blp_filename);
- break :resource_path std.fmt.comptimePrint(
- "/com/mitchellh/ghostty/ui/{d}.{d}/{s}.ui",
- .{
- file.major,
- file.minor,
- file.name,
- },
- );
- } else @compileError("missing ui file '" ++ name ++ "' in gresource.zig");
- },
- }
+ // Check to make sure that our file is listed as a
+ // `blueprint_file` in `gresource.zig`. If it isn't Ghostty
+ // could crash at runtime when we try and load a nonexistent
+ // GResource.
+ for (gresource.blueprint_files) |file| {
+ if (major != file.major or minor != file.minor or !std.mem.eql(u8, file.name, name)) continue;
+ // Use @embedFile to make sure that the `.blp` file exists
+ // at compile time. Zig _should_ discard the data so that
+ // it doesn't end up in the final executable. At runtime we
+ // will load the data from a GResource.
+ const blp_filename = std.fmt.comptimePrint(
+ "ui/{d}.{d}/{s}.blp",
+ .{
+ file.major,
+ file.minor,
+ file.name,
+ },
+ );
+ _ = @embedFile(blp_filename);
+ break :resource_path std.fmt.comptimePrint(
+ "/com/mitchellh/ghostty/ui/{d}.{d}/{s}.ui",
+ .{
+ file.major,
+ file.minor,
+ file.name,
+ },
+ );
+ } else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig");
};
return .{
diff --git a/src/apprt/gtk/ClipboardConfirmationWindow.zig b/src/apprt/gtk/ClipboardConfirmationWindow.zig
index a28b7ddd4..f10fc79ac 100644
--- a/src/apprt/gtk/ClipboardConfirmationWindow.zig
+++ b/src/apprt/gtk/ClipboardConfirmationWindow.zig
@@ -71,14 +71,14 @@ fn init(
) !void {
var builder = switch (DialogType) {
adw.AlertDialog => switch (request) {
- .osc_52_read => Builder.init("ccw-osc-52-read", 1, 5, .blp),
- .osc_52_write => Builder.init("ccw-osc-52-write", 1, 5, .blp),
- .paste => Builder.init("ccw-paste", 1, 5, .blp),
+ .osc_52_read => Builder.init("ccw-osc-52-read", 1, 5),
+ .osc_52_write => Builder.init("ccw-osc-52-write", 1, 5),
+ .paste => Builder.init("ccw-paste", 1, 5),
},
adw.MessageDialog => switch (request) {
- .osc_52_read => Builder.init("ccw-osc-52-read", 1, 2, .ui),
- .osc_52_write => Builder.init("ccw-osc-52-write", 1, 2, .ui),
- .paste => Builder.init("ccw-paste", 1, 2, .ui),
+ .osc_52_read => Builder.init("ccw-osc-52-read", 1, 2),
+ .osc_52_write => Builder.init("ccw-osc-52-write", 1, 2),
+ .paste => Builder.init("ccw-paste", 1, 2),
},
else => unreachable,
};
@@ -152,7 +152,7 @@ fn init(
}
}
-fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.C) void {
+fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.c) void {
if (std.mem.orderZ(u8, response, "ok") == .eq) {
self.core_surface.completeClipboardRequest(
self.pending_req,
@@ -165,7 +165,7 @@ fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation)
self.destroy();
}
-fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.C) void {
+fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.c) void {
self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true));
self.text_view.as(gtk.Widget).removeCssClass("blurred");
@@ -173,7 +173,7 @@ fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv
self.reveal_button.as(gtk.Widget).setVisible(@intFromBool(false));
}
-fn gtkHideButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.C) void {
+fn gtkHideButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.c) void {
self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(false));
self.text_view.as(gtk.Widget).addCssClass("blurred");
diff --git a/src/apprt/gtk/CloseDialog.zig b/src/apprt/gtk/CloseDialog.zig
index ea683c477..559737cf4 100644
--- a/src/apprt/gtk/CloseDialog.zig
+++ b/src/apprt/gtk/CloseDialog.zig
@@ -64,7 +64,7 @@ fn responseCallback(
_: *DialogType,
response: [*:0]const u8,
target: *Target,
-) callconv(.C) void {
+) callconv(.c) void {
const alloc = target.allocator();
defer alloc.destroy(target);
@@ -141,7 +141,7 @@ pub const Target = union(enum) {
}
};
-fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.C) c_int {
+fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) c_int {
const window: *gtk.Window = @ptrCast(@alignCast(@constCast(data orelse return -1)));
// Confusingly, `isActive` returns 1 when active,
diff --git a/src/apprt/gtk/ConfigErrorsDialog.zig b/src/apprt/gtk/ConfigErrorsDialog.zig
index c10f8e679..ccc5599ad 100644
--- a/src/apprt/gtk/ConfigErrorsDialog.zig
+++ b/src/apprt/gtk/ConfigErrorsDialog.zig
@@ -29,15 +29,38 @@ error_message: *gtk.TextBuffer,
pub fn maybePresent(app: *App, window: ?*Window) void {
if (app.config._diagnostics.empty()) return;
- var builder = switch (DialogType) {
- adw.AlertDialog => Builder.init("config-errors-dialog", 1, 5, .blp),
- adw.MessageDialog => Builder.init("config-errors-dialog", 1, 2, .ui),
- else => unreachable,
- };
- defer builder.deinit();
+ const config_errors_dialog = config_errors_dialog: {
+ if (app.config_errors_dialog) |config_errors_dialog| break :config_errors_dialog config_errors_dialog;
- const dialog = builder.getObject(DialogType, "config_errors_dialog").?;
- const error_message = builder.getObject(gtk.TextBuffer, "error_message").?;
+ var builder = switch (DialogType) {
+ adw.AlertDialog => Builder.init("config-errors-dialog", 1, 5),
+ adw.MessageDialog => Builder.init("config-errors-dialog", 1, 2),
+ else => unreachable,
+ };
+
+ const dialog = builder.getObject(DialogType, "config_errors_dialog").?;
+ const error_message = builder.getObject(gtk.TextBuffer, "error_message").?;
+
+ _ = DialogType.signals.response.connect(dialog, *App, onResponse, app, .{});
+
+ app.config_errors_dialog = .{
+ .builder = builder,
+ .dialog = dialog,
+ .error_message = error_message,
+ };
+
+ break :config_errors_dialog app.config_errors_dialog.?;
+ };
+
+ {
+ var start = std.mem.zeroes(gtk.TextIter);
+ config_errors_dialog.error_message.getStartIter(&start);
+
+ var end = std.mem.zeroes(gtk.TextIter);
+ config_errors_dialog.error_message.getEndIter(&end);
+
+ config_errors_dialog.error_message.delete(&start, &end);
+ }
var msg_buf: [4095:0]u8 = undefined;
var fbs = std.io.fixedBufferStream(&msg_buf);
@@ -52,22 +75,24 @@ pub fn maybePresent(app: *App, window: ?*Window) void {
continue;
};
- error_message.insertAtCursor(&msg_buf, @intCast(fbs.pos));
- error_message.insertAtCursor("\n", 1);
+ config_errors_dialog.error_message.insertAtCursor(&msg_buf, @intCast(fbs.pos));
+ config_errors_dialog.error_message.insertAtCursor("\n", 1);
}
- _ = DialogType.signals.response.connect(dialog, *App, onResponse, app, .{});
-
- const parent = if (window) |w| w.window.as(gtk.Widget) else null;
-
switch (DialogType) {
- adw.AlertDialog => dialog.as(adw.Dialog).present(parent),
- adw.MessageDialog => dialog.as(gtk.Window).present(),
+ adw.AlertDialog => {
+ const parent = if (window) |w| w.window.as(gtk.Widget) else null;
+ config_errors_dialog.dialog.as(adw.Dialog).present(parent);
+ },
+ adw.MessageDialog => config_errors_dialog.dialog.as(gtk.Window).present(),
else => unreachable,
}
}
-fn onResponse(_: *DialogType, response: [*:0]const u8, app: *App) callconv(.C) void {
+fn onResponse(_: *DialogType, response: [*:0]const u8, app: *App) callconv(.c) void {
+ if (app.config_errors_dialog) |config_errors_dialog| config_errors_dialog.builder.deinit();
+ app.config_errors_dialog = null;
+
if (std.mem.orderZ(u8, response, "reload") == .eq) {
app.reloadConfig(.app, .{}) catch |err| {
log.warn("error reloading config error={}", .{err});
diff --git a/src/apprt/gtk/ImguiWidget.zig b/src/apprt/gtk/ImguiWidget.zig
index f1f0c8f6b..338fd7982 100644
--- a/src/apprt/gtk/ImguiWidget.zig
+++ b/src/apprt/gtk/ImguiWidget.zig
@@ -221,12 +221,12 @@ fn translateMouseButton(button: c_uint) ?c_int {
};
}
-fn gtkDestroy(_: *gtk.GLArea, self: *ImguiWidget) callconv(.C) void {
+fn gtkDestroy(_: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
log.debug("imgui widget destroy", .{});
self.deinit();
}
-fn gtkRealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.C) void {
+fn gtkRealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
log.debug("gl surface realized", .{});
// We need to make the context current so we can call GL functions.
@@ -242,7 +242,7 @@ fn gtkRealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.C) void {
_ = cimgui.ImGui_ImplOpenGL3_Init(null);
}
-fn gtkUnrealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.C) void {
+fn gtkUnrealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
_ = area;
log.debug("gl surface unrealized", .{});
@@ -250,7 +250,7 @@ fn gtkUnrealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.C) void {
cimgui.ImGui_ImplOpenGL3_Shutdown();
}
-fn gtkResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *ImguiWidget) callconv(.C) void {
+fn gtkResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *ImguiWidget) callconv(.c) void {
cimgui.c.igSetCurrentContext(self.ig_ctx);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const scale_factor = area.as(gtk.Widget).getScaleFactor();
@@ -273,7 +273,7 @@ fn gtkResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *ImguiWidget)
active_style.* = style.*;
}
-fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *ImguiWidget) callconv(.C) c_int {
+fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *ImguiWidget) callconv(.c) c_int {
cimgui.c.igSetCurrentContext(self.ig_ctx);
// Setup our frame. We render twice because some ImGui behaviors
@@ -307,7 +307,7 @@ fn gtkMouseMotion(
x: f64,
y: f64,
self: *ImguiWidget,
-) callconv(.C) void {
+) callconv(.c) void {
cimgui.c.igSetCurrentContext(self.ig_ctx);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const scale_factor: f64 = @floatFromInt(self.gl_area.as(gtk.Widget).getScaleFactor());
@@ -325,7 +325,7 @@ fn gtkMouseDown(
_: f64,
_: f64,
self: *ImguiWidget,
-) callconv(.C) void {
+) callconv(.c) void {
self.queueRender();
cimgui.c.igSetCurrentContext(self.ig_ctx);
@@ -343,7 +343,7 @@ fn gtkMouseUp(
_: f64,
_: f64,
self: *ImguiWidget,
-) callconv(.C) void {
+) callconv(.c) void {
self.queueRender();
cimgui.c.igSetCurrentContext(self.ig_ctx);
@@ -359,7 +359,7 @@ fn gtkMouseScroll(
x: f64,
y: f64,
self: *ImguiWidget,
-) callconv(.C) c_int {
+) callconv(.c) c_int {
self.queueRender();
cimgui.c.igSetCurrentContext(self.ig_ctx);
@@ -373,7 +373,7 @@ fn gtkMouseScroll(
return @intFromBool(true);
}
-fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.C) void {
+fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.c) void {
self.queueRender();
cimgui.c.igSetCurrentContext(self.ig_ctx);
@@ -381,7 +381,7 @@ fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.C)
cimgui.c.ImGuiIO_AddFocusEvent(io, true);
}
-fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.C) void {
+fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.c) void {
self.queueRender();
cimgui.c.igSetCurrentContext(self.ig_ctx);
@@ -393,7 +393,7 @@ fn gtkInputCommit(
_: *gtk.IMMulticontext,
bytes: [*:0]u8,
self: *ImguiWidget,
-) callconv(.C) void {
+) callconv(.c) void {
self.queueRender();
cimgui.c.igSetCurrentContext(self.ig_ctx);
@@ -407,7 +407,7 @@ fn gtkKeyPressed(
keycode: c_uint,
gtk_mods: gdk.ModifierType,
self: *ImguiWidget,
-) callconv(.C) c_int {
+) callconv(.c) c_int {
return @intFromBool(self.keyEvent(
.press,
ec_key,
@@ -423,7 +423,7 @@ fn gtkKeyReleased(
keycode: c_uint,
gtk_mods: gdk.ModifierType,
self: *ImguiWidget,
-) callconv(.C) void {
+) callconv(.c) void {
_ = self.keyEvent(
.release,
ec_key,
diff --git a/src/apprt/gtk/ResizeOverlay.zig b/src/apprt/gtk/ResizeOverlay.zig
index 47f2aea1a..767cf097d 100644
--- a/src/apprt/gtk/ResizeOverlay.zig
+++ b/src/apprt/gtk/ResizeOverlay.zig
@@ -104,7 +104,7 @@ pub fn maybeShow(self: *ResizeOverlay) void {
/// Actually update the overlay widget. This should only be called from a GTK
/// idle handler.
-fn gtkUpdate(ud: ?*anyopaque) callconv(.C) c_int {
+fn gtkUpdate(ud: ?*anyopaque) callconv(.c) c_int {
const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0));
// No matter what our idler is complete with this callback
@@ -198,7 +198,7 @@ fn setPosition(label: *gtk.Label, config: *DerivedConfig) void {
/// If this fires, it means that the delay period has expired and the resize
/// overlay widget should be hidden.
-fn gtkTimerExpired(ud: ?*anyopaque) callconv(.C) c_int {
+fn gtkTimerExpired(ud: ?*anyopaque) callconv(.c) c_int {
const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0));
self.timer = null;
if (self.label) |label| hide(label);
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
index e99fe29ce..7ff96480e 100644
--- a/src/apprt/gtk/Surface.zig
+++ b/src/apprt/gtk/Surface.zig
@@ -1025,7 +1025,7 @@ pub fn setTitle(self: *Surface, slice: [:0]const u8, source: SetTitleSource) !vo
self.update_title_timer = glib.timeoutAdd(75, updateTitleTimerExpired, self);
}
-fn updateTitleTimerExpired(ud: ?*anyopaque) callconv(.C) c_int {
+fn updateTitleTimerExpired(ud: ?*anyopaque) callconv(.c) c_int {
const self: *Surface = @ptrCast(@alignCast(ud.?));
self.updateTitleLabels();
@@ -1061,7 +1061,7 @@ pub fn promptTitle(self: *Surface) !void {
if (!adw_version.atLeast(1, 5, 0)) return;
const window = self.container.window() orelse return;
- var builder = Builder.init("prompt-title-dialog", 1, 5, .blp);
+ var builder = Builder.init("prompt-title-dialog", 1, 5);
defer builder.deinit();
const entry = builder.getObject(gtk.Entry, "title_entry").?;
@@ -1265,7 +1265,7 @@ fn gtkClipboardRead(
source: ?*gobject.Object,
res: *gio.AsyncResult,
ud: ?*anyopaque,
-) callconv(.C) void {
+) callconv(.c) void {
const clipboard = gobject.ext.cast(gdk.Clipboard, source orelse return) orelse return;
const req: *ClipboardRequest = @ptrCast(@alignCast(ud orelse return));
const self = req.self;
@@ -1349,7 +1349,7 @@ pub fn showDesktopNotification(
app.sendNotification(body.ptr, notification);
}
-fn gtkRealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.C) void {
+fn gtkRealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.c) void {
log.debug("gl surface realized", .{});
// We need to make the context current so we can call GL functions.
@@ -1377,7 +1377,7 @@ fn gtkRealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.C) void {
/// This is called when the underlying OpenGL resources must be released.
/// This is usually due to the OpenGL area changing GDK surfaces.
-fn gtkUnrealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.C) void {
+fn gtkUnrealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.c) void {
log.debug("gl surface unrealized", .{});
// See gtkRealize for why we do this here.
@@ -1405,7 +1405,7 @@ fn gtkUnrealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.C) void {
}
/// render signal
-fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *Surface) callconv(.C) c_int {
+fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *Surface) callconv(.c) c_int {
self.render() catch |err| {
log.err("surface failed to render: {}", .{err});
return 0;
@@ -1415,7 +1415,7 @@ fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *Surface) callconv(.C) c_i
}
/// resize signal
-fn gtkResize(gl_area: *gtk.GLArea, width: c_int, height: c_int, self: *Surface) callconv(.C) void {
+fn gtkResize(gl_area: *gtk.GLArea, width: c_int, height: c_int, self: *Surface) callconv(.c) void {
// Some debug output to help understand what GTK is telling us.
{
const scale_factor = scale: {
@@ -1471,7 +1471,7 @@ fn gtkResize(gl_area: *gtk.GLArea, width: c_int, height: c_int, self: *Surface)
}
/// "destroy" signal for surface
-fn gtkDestroy(_: *gtk.GLArea, self: *Surface) callconv(.C) void {
+fn gtkDestroy(_: *gtk.GLArea, self: *Surface) callconv(.c) void {
log.debug("gl destroy", .{});
const alloc = self.app.core_app.alloc;
@@ -1505,7 +1505,7 @@ fn gtkMouseDown(
x: f64,
y: f64,
self: *Surface,
-) callconv(.C) void {
+) callconv(.c) void {
const event = gesture.as(gtk.EventController).getCurrentEvent() orelse return;
const gtk_mods = event.getModifierState();
@@ -1538,7 +1538,7 @@ fn gtkMouseUp(
_: f64,
_: f64,
self: *Surface,
-) callconv(.C) void {
+) callconv(.c) void {
const event = gesture.as(gtk.EventController).getCurrentEvent() orelse return;
const gtk_mods = event.getModifierState();
@@ -1557,7 +1557,7 @@ fn gtkMouseMotion(
x: f64,
y: f64,
self: *Surface,
-) callconv(.C) void {
+) callconv(.c) void {
const event = ec.as(gtk.EventController).getCurrentEvent() orelse return;
const scaled = self.scaledCoordinates(x, y);
@@ -1603,7 +1603,7 @@ fn gtkMouseMotion(
fn gtkMouseLeave(
ec_motion: *gtk.EventControllerMotion,
self: *Surface,
-) callconv(.C) void {
+) callconv(.c) void {
const event = ec_motion.as(gtk.EventController).getCurrentEvent() orelse return;
// Get our modifiers
@@ -1618,14 +1618,14 @@ fn gtkMouseLeave(
fn gtkMouseScrollPrecisionBegin(
_: *gtk.EventControllerScroll,
self: *Surface,
-) callconv(.C) void {
+) callconv(.c) void {
self.precision_scroll = true;
}
fn gtkMouseScrollPrecisionEnd(
_: *gtk.EventControllerScroll,
self: *Surface,
-) callconv(.C) void {
+) callconv(.c) void {
self.precision_scroll = false;
}
@@ -1634,7 +1634,7 @@ fn gtkMouseScroll(
x: f64,
y: f64,
self: *Surface,
-) callconv(.C) c_int {
+) callconv(.c) c_int {
const scaled = self.scaledCoordinates(x, y);
// GTK doesn't support any of the scroll mods.
@@ -1664,7 +1664,7 @@ fn gtkKeyPressed(
keycode: c_uint,
gtk_mods: gdk.ModifierType,
self: *Surface,
-) callconv(.C) c_int {
+) callconv(.c) c_int {
return @intFromBool(self.keyEvent(
.press,
ec_key,
@@ -1680,7 +1680,7 @@ fn gtkKeyReleased(
keycode: c_uint,
state: gdk.ModifierType,
self: *Surface,
-) callconv(.C) void {
+) callconv(.c) void {
_ = self.keyEvent(
.release,
ec_key,
@@ -1971,7 +1971,7 @@ pub fn keyEvent(
fn gtkInputPreeditStart(
_: *gtk.IMMulticontext,
self: *Surface,
-) callconv(.C) void {
+) callconv(.c) void {
// log.warn("GTKIM: preedit start", .{});
// Start our composing state for the input method and reset our
@@ -1983,7 +1983,7 @@ fn gtkInputPreeditStart(
fn gtkInputPreeditChanged(
ctx: *gtk.IMMulticontext,
self: *Surface,
-) callconv(.C) void {
+) callconv(.c) void {
// Any preedit change should mark that we're composing. Its possible this
// is false using fcitx5-hangul and typing "dkssud" ("안녕"). The
// second "s" results in a "commit" for "안" which sets composing to false,
@@ -2009,7 +2009,7 @@ fn gtkInputPreeditChanged(
fn gtkInputPreeditEnd(
_: *gtk.IMMulticontext,
self: *Surface,
-) callconv(.C) void {
+) callconv(.c) void {
// log.warn("GTKIM: preedit end", .{});
// End our composing state for GTK, allowing us to commit the text.
@@ -2025,7 +2025,7 @@ fn gtkInputCommit(
_: *gtk.IMMulticontext,
bytes: [*:0]u8,
self: *Surface,
-) callconv(.C) void {
+) callconv(.c) void {
const str = std.mem.sliceTo(bytes, 0);
// log.debug("GTKIM: input commit composing={} keyevent={} str={s}", .{
@@ -2100,7 +2100,7 @@ fn gtkInputCommit(
};
}
-fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *Surface) callconv(.C) void {
+fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *Surface) callconv(.c) void {
if (!self.realized) return;
// Notify our IM context
@@ -2125,7 +2125,7 @@ fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *Surface) callconv(.C) void
};
}
-fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *Surface) callconv(.C) void {
+fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *Surface) callconv(.c) void {
if (!self.realized) return;
// Notify our IM context
@@ -2243,7 +2243,7 @@ fn gtkDrop(
_: f64,
_: f64,
self: *Surface,
-) callconv(.C) c_int {
+) callconv(.c) c_int {
const alloc = self.app.core_app.alloc;
if (g_value_holds(value, gdk.FileList.getGObjectType())) {
@@ -2395,7 +2395,7 @@ fn g_value_holds(value_: ?*gobject.Value, g_type: gobject.Type) bool {
return false;
}
-fn gtkPromptTitleResponse(source_object: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.C) void {
+fn gtkPromptTitleResponse(source_object: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.c) void {
if (!adw_version.supportsDialogs()) return;
const dialog = gobject.ext.cast(adw.AlertDialog, source_object.?).?;
const self: *Surface = @ptrCast(@alignCast(ud));
diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig
index 57a9644d9..c32fa19fc 100644
--- a/src/apprt/gtk/Tab.zig
+++ b/src/apprt/gtk/Tab.zig
@@ -161,7 +161,7 @@ pub fn closeWithConfirmation(tab: *Tab) void {
}
}
-fn gtkDestroy(_: *gtk.Box, self: *Tab) callconv(.C) void {
+fn gtkDestroy(_: *gtk.Box, self: *Tab) callconv(.c) void {
log.debug("tab box destroy", .{});
const alloc = self.window.app.core_app.alloc;
diff --git a/src/apprt/gtk/TabView.zig b/src/apprt/gtk/TabView.zig
index ddd0951d2..29a069a6d 100644
--- a/src/apprt/gtk/TabView.zig
+++ b/src/apprt/gtk/TabView.zig
@@ -227,7 +227,7 @@ pub fn createWindow(window: *Window) !*Window {
return new_window;
}
-fn adwPageAttached(_: *adw.TabView, page: *adw.TabPage, _: c_int, self: *TabView) callconv(.C) void {
+fn adwPageAttached(_: *adw.TabView, page: *adw.TabPage, _: c_int, self: *TabView) callconv(.c) void {
const child = page.getChild().as(gobject.Object);
const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return));
tab.window = self.window;
@@ -239,7 +239,7 @@ fn adwClosePage(
_: *adw.TabView,
page: *adw.TabPage,
self: *TabView,
-) callconv(.C) c_int {
+) callconv(.c) c_int {
const child = page.getChild().as(gobject.Object);
const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return 0));
self.tab_view.closePageFinish(page, @intFromBool(self.forcing_close));
@@ -251,7 +251,7 @@ fn adwClosePage(
fn adwTabViewCreateWindow(
_: *adw.TabView,
self: *TabView,
-) callconv(.C) ?*adw.TabView {
+) callconv(.c) ?*adw.TabView {
const window = createWindow(self.window) catch |err| {
log.warn("error creating new window error={}", .{err});
return null;
@@ -259,7 +259,7 @@ fn adwTabViewCreateWindow(
return window.notebook.tab_view;
}
-fn adwSelectPage(_: *adw.TabView, _: *gobject.ParamSpec, self: *TabView) callconv(.C) void {
+fn adwSelectPage(_: *adw.TabView, _: *gobject.ParamSpec, self: *TabView) callconv(.c) void {
const page = self.tab_view.getSelectedPage() orelse return;
// If the tab was previously marked as needing attention
diff --git a/src/apprt/gtk/URLWidget.zig b/src/apprt/gtk/URLWidget.zig
index d1628aa6e..e59827aaf 100644
--- a/src/apprt/gtk/URLWidget.zig
+++ b/src/apprt/gtk/URLWidget.zig
@@ -101,7 +101,7 @@ fn gtkLeftEnter(
_: f64,
_: f64,
right: *gtk.Label,
-) callconv(.C) void {
+) callconv(.c) void {
right.as(gtk.Widget).removeCssClass("hidden");
}
@@ -110,6 +110,6 @@ fn gtkLeftEnter(
fn gtkLeftLeave(
_: *gtk.EventControllerMotion,
right: *gtk.Label,
-) callconv(.C) void {
+) callconv(.c) void {
right.as(gtk.Widget).addCssClass("hidden");
}
diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig
index 20ac3d955..d82087ff0 100644
--- a/src/apprt/gtk/Window.zig
+++ b/src/apprt/gtk/Window.zig
@@ -248,7 +248,7 @@ pub fn init(self: *Window, app: *App) !void {
btn.as(gtk.Widget).setTooltipText(i18n._("New Tab"));
btn.setDropdownTooltip(i18n._("New Split"));
- var builder = Builder.init("menu-headerbar-split_menu", 1, 0, .blp);
+ var builder = Builder.init("menu-headerbar-split_menu", 1, 0);
defer builder.deinit();
btn.setMenuModel(builder.getObject(gio.MenuModel, "menu"));
@@ -792,7 +792,7 @@ fn gtkWindowNotifyIsActive(
_: *adw.ApplicationWindow,
_: *gobject.ParamSpec,
self: *Window,
-) callconv(.C) void {
+) callconv(.c) void {
if (!self.isQuickTerminal()) return;
// Hide when we're unfocused
@@ -883,7 +883,7 @@ fn adwTabOverviewOpen(
fn adwTabOverviewFocusTimer(
ud: ?*anyopaque,
-) callconv(.C) c_int {
+) callconv(.c) c_int {
if (!adw_version.supportsTabOverview()) unreachable;
const self: *Window = @ptrCast(@alignCast(ud orelse return 0));
self.adw_tab_overview_focus_timer = null;
@@ -970,7 +970,7 @@ fn gtkActionAbout(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
-) callconv(.C) void {
+) callconv(.c) void {
const name = "Ghostty";
const icon = "com.mitchellh.ghostty";
const website = "https://ghostty.org";
@@ -1014,7 +1014,7 @@ fn gtkActionClose(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
-) callconv(.C) void {
+) callconv(.c) void {
self.closeWithConfirmation();
}
@@ -1022,7 +1022,7 @@ fn gtkActionNewWindow(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
-) callconv(.C) void {
+) callconv(.c) void {
self.performBindingAction(.{ .new_window = {} });
}
@@ -1030,7 +1030,7 @@ fn gtkActionNewTab(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
-) callconv(.C) void {
+) callconv(.c) void {
self.performBindingAction(.{ .new_tab = {} });
}
@@ -1038,7 +1038,7 @@ fn gtkActionCloseTab(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
-) callconv(.C) void {
+) callconv(.c) void {
self.performBindingAction(.{ .close_tab = {} });
}
@@ -1046,7 +1046,7 @@ fn gtkActionSplitRight(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
-) callconv(.C) void {
+) callconv(.c) void {
self.performBindingAction(.{ .new_split = .right });
}
@@ -1054,7 +1054,7 @@ fn gtkActionSplitDown(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
-) callconv(.C) void {
+) callconv(.c) void {
self.performBindingAction(.{ .new_split = .down });
}
@@ -1062,7 +1062,7 @@ fn gtkActionSplitLeft(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
-) callconv(.C) void {
+) callconv(.c) void {
self.performBindingAction(.{ .new_split = .left });
}
@@ -1070,7 +1070,7 @@ fn gtkActionSplitUp(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
-) callconv(.C) void {
+) callconv(.c) void {
self.performBindingAction(.{ .new_split = .up });
}
@@ -1078,7 +1078,7 @@ fn gtkActionToggleInspector(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
-) callconv(.C) void {
+) callconv(.c) void {
self.performBindingAction(.{ .inspector = .toggle });
}
@@ -1086,7 +1086,7 @@ fn gtkActionCopy(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
-) callconv(.C) void {
+) callconv(.c) void {
self.performBindingAction(.{ .copy_to_clipboard = {} });
}
@@ -1094,7 +1094,7 @@ fn gtkActionPaste(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
-) callconv(.C) void {
+) callconv(.c) void {
self.performBindingAction(.{ .paste_from_clipboard = {} });
}
@@ -1102,7 +1102,7 @@ fn gtkActionReset(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
-) callconv(.C) void {
+) callconv(.c) void {
self.performBindingAction(.{ .reset = {} });
}
@@ -1110,7 +1110,7 @@ fn gtkActionClear(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
-) callconv(.C) void {
+) callconv(.c) void {
self.performBindingAction(.{ .clear_screen = {} });
}
@@ -1118,7 +1118,7 @@ fn gtkActionPromptTitle(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
-) callconv(.C) void {
+) callconv(.c) void {
self.performBindingAction(.{ .prompt_surface_title = {} });
}
@@ -1133,7 +1133,7 @@ fn gtkTitlebarMenuActivate(
btn: *gtk.MenuButton,
_: *gobject.ParamSpec,
self: *Window,
-) callconv(.C) void {
+) callconv(.c) void {
// debian 12 is stuck on GTK 4.8
if (!gtk_version.atLeast(4, 10, 0)) return;
const active = btn.getActive() != 0;
diff --git a/src/apprt/gtk/blueprint_compiler.zig b/src/apprt/gtk/blueprint_compiler.zig
index 7a0442e92..9bc515655 100644
--- a/src/apprt/gtk/blueprint_compiler.zig
+++ b/src/apprt/gtk/blueprint_compiler.zig
@@ -4,62 +4,157 @@ pub const c = @cImport({
@cInclude("adwaita.h");
});
+const adwaita_version = std.SemanticVersion{
+ .major = c.ADW_MAJOR_VERSION,
+ .minor = c.ADW_MINOR_VERSION,
+ .patch = c.ADW_MICRO_VERSION,
+};
+const required_blueprint_version = std.SemanticVersion{
+ .major = 0,
+ .minor = 16,
+ .patch = 0,
+};
+
pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- const alloc = gpa.allocator();
+ var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
+ defer _ = debug_allocator.deinit();
+ const alloc = debug_allocator.allocator();
var it = try std.process.argsWithAllocator(alloc);
defer it.deinit();
_ = it.next();
- const major = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMajorVersion, 10);
- const minor = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMinorVersion, 10);
+ const required_adwaita_version = std.SemanticVersion{
+ .major = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMajorVersion, 10),
+ .minor = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMinorVersion, 10),
+ .patch = 0,
+ };
const output = it.next() orelse return error.NoOutput;
const input = it.next() orelse return error.NoInput;
- if (c.ADW_MAJOR_VERSION < major or (c.ADW_MAJOR_VERSION == major and c.ADW_MINOR_VERSION < minor)) {
- // If the Adwaita version is too old, generate an "empty" file.
- const file = try std.fs.createFileAbsolute(output, .{
- .truncate = true,
- });
- try file.writeAll(
- \\
- \\
- );
- defer file.close();
-
- return;
+ if (adwaita_version.order(required_adwaita_version) == .lt) {
+ std.debug.print(
+ \\`libadwaita` is too old.
+ \\
+ \\Ghostty requires a version {} or newer of `libadwaita` to
+ \\compile this blueprint. Please install it, ensure that it is
+ \\available on your PATH, and then retry building Ghostty.
+ , .{required_adwaita_version});
+ std.posix.exit(1);
}
- var compiler = std.process.Child.init(
- &.{
- "blueprint-compiler",
- "compile",
- "--output",
- output,
- input,
- },
- alloc,
- );
+ {
+ var stdout: std.ArrayListUnmanaged(u8) = .empty;
+ defer stdout.deinit(alloc);
+ var stderr: std.ArrayListUnmanaged(u8) = .empty;
+ defer stderr.deinit(alloc);
- const term = compiler.spawnAndWait() catch |err| switch (err) {
- error.FileNotFound => {
- std.log.err(
- \\`blueprint-compiler` not found.
+ var blueprint_compiler = std.process.Child.init(
+ &.{
+ "blueprint-compiler",
+ "--version",
+ },
+ alloc,
+ );
+ blueprint_compiler.stdout_behavior = .Pipe;
+ blueprint_compiler.stderr_behavior = .Pipe;
+ try blueprint_compiler.spawn();
+ try blueprint_compiler.collectOutput(
+ alloc,
+ &stdout,
+ &stderr,
+ std.math.maxInt(u16),
+ );
+ const term = blueprint_compiler.wait() catch |err| switch (err) {
+ error.FileNotFound => {
+ std.debug.print(
+ \\`blueprint-compiler` not found.
+ \\
+ \\Ghostty requires version {} or newer of
+ \\`blueprint-compiler` as a build-time dependency starting
+ \\from version 1.2. Please install it, ensure that it is
+ \\available on your PATH, and then retry building Ghostty.
+ \\
+ , .{required_blueprint_version});
+ std.posix.exit(1);
+ },
+ else => return err,
+ };
+ switch (term) {
+ .Exited => |rc| {
+ if (rc != 0) std.process.exit(1);
+ },
+ else => std.process.exit(1),
+ }
+
+ const version = try std.SemanticVersion.parse(std.mem.trim(u8, stdout.items, &std.ascii.whitespace));
+ if (version.order(required_blueprint_version) == .lt) {
+ std.debug.print(
+ \\`blueprint-compiler` is the wrong version.
\\
- \\Ghostty requires `blueprint-compiler` as a build-time dependency starting from version 1.2.
- \\Please install it, ensure that it is available on your PATH, and then retry building Ghostty.
- , .{});
+ \\Ghostty requires version {} or newer of
+ \\`blueprint-compiler` as a build-time dependency starting
+ \\from version 1.2. Please install it, ensure that it is
+ \\available on your PATH, and then retry building Ghostty.
+ \\
+ , .{required_blueprint_version});
std.posix.exit(1);
- },
- else => return err,
- };
+ }
+ }
- switch (term) {
- .Exited => |rc| {
- if (rc != 0) std.process.exit(1);
- },
- else => std.process.exit(1),
+ {
+ var stdout: std.ArrayListUnmanaged(u8) = .empty;
+ defer stdout.deinit(alloc);
+ var stderr: std.ArrayListUnmanaged(u8) = .empty;
+ defer stderr.deinit(alloc);
+
+ var blueprint_compiler = std.process.Child.init(
+ &.{
+ "blueprint-compiler",
+ "compile",
+ "--output",
+ output,
+ input,
+ },
+ alloc,
+ );
+ blueprint_compiler.stdout_behavior = .Pipe;
+ blueprint_compiler.stderr_behavior = .Pipe;
+ try blueprint_compiler.spawn();
+ try blueprint_compiler.collectOutput(
+ alloc,
+ &stdout,
+ &stderr,
+ std.math.maxInt(u16),
+ );
+ const term = blueprint_compiler.wait() catch |err| switch (err) {
+ error.FileNotFound => {
+ std.debug.print(
+ \\`blueprint-compiler` not found.
+ \\
+ \\Ghostty requires version {} or newer of
+ \\`blueprint-compiler` as a build-time dependency starting
+ \\from version 1.2. Please install it, ensure that it is
+ \\available on your PATH, and then retry building Ghostty.
+ \\
+ , .{required_blueprint_version});
+ std.posix.exit(1);
+ },
+ else => return err,
+ };
+
+ switch (term) {
+ .Exited => |rc| {
+ if (rc != 0) {
+ std.debug.print("{s}", .{stderr.items});
+ std.process.exit(1);
+ }
+ },
+ else => {
+ std.debug.print("{s}", .{stderr.items});
+ std.process.exit(1);
+ },
+ }
}
}
diff --git a/src/apprt/gtk/builder_check.zig b/src/apprt/gtk/builder_check.zig
deleted file mode 100644
index 015c6310d..000000000
--- a/src/apprt/gtk/builder_check.zig
+++ /dev/null
@@ -1,32 +0,0 @@
-const std = @import("std");
-const build_options = @import("build_options");
-
-const gtk = @import("gtk");
-const adw = @import("adw");
-
-pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- const alloc = gpa.allocator();
-
- const filename = filename: {
- var it = try std.process.argsWithAllocator(alloc);
- defer it.deinit();
-
- _ = it.next() orelse return error.NoFilename;
- break :filename try alloc.dupeZ(u8, it.next() orelse return error.NoFilename);
- };
- defer alloc.free(filename);
-
- const data = try std.fs.cwd().readFileAllocOptions(alloc, filename, std.math.maxInt(u16), null, 1, 0);
- defer alloc.free(data);
-
- if (gtk.initCheck() == 0) {
- std.debug.print("{s}: skipping builder check because we can't connect to display!\n", .{filename});
- return;
- }
-
- adw.init();
-
- const builder = gtk.Builder.newFromString(data.ptr, @intCast(data.len));
- defer builder.unref();
-}
diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig
index 7ced9fc45..a1db8ac62 100644
--- a/src/apprt/gtk/gresource.zig
+++ b/src/apprt/gtk/gresource.zig
@@ -53,19 +53,6 @@ const icons = [_]struct {
},
};
-pub const VersionedBuilderXML = struct {
- major: u16,
- minor: u16,
- name: []const u8,
-};
-
-pub const ui_files = [_]VersionedBuilderXML{
- .{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
- .{ .major = 1, .minor = 2, .name = "ccw-osc-52-read" },
- .{ .major = 1, .minor = 2, .name = "ccw-osc-52-write" },
- .{ .major = 1, .minor = 2, .name = "ccw-paste" },
-};
-
pub const VersionedBlueprint = struct {
major: u16,
minor: u16,
@@ -81,16 +68,21 @@ pub const blueprint_files = [_]VersionedBlueprint{
.{ .major = 1, .minor = 5, .name = "ccw-osc-52-read" },
.{ .major = 1, .minor = 5, .name = "ccw-osc-52-write" },
.{ .major = 1, .minor = 5, .name = "ccw-paste" },
+ .{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
+ .{ .major = 1, .minor = 2, .name = "ccw-osc-52-read" },
+ .{ .major = 1, .minor = 2, .name = "ccw-osc-52-write" },
+ .{ .major = 1, .minor = 2, .name = "ccw-paste" },
};
pub fn main() !void {
- var gpa = std.heap.GeneralPurposeAllocator(.{}){};
- const alloc = gpa.allocator();
+ var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
+ defer _ = debug_allocator.deinit();
+ const alloc = debug_allocator.allocator();
- var extra_ui_files = std.ArrayList([]const u8).init(alloc);
+ var extra_ui_files: std.ArrayListUnmanaged([]const u8) = .empty;
defer {
for (extra_ui_files.items) |item| alloc.free(item);
- extra_ui_files.deinit();
+ extra_ui_files.deinit(alloc);
}
var it = try std.process.argsWithAllocator(alloc);
@@ -98,7 +90,7 @@ pub fn main() !void {
while (it.next()) |argument| {
if (std.mem.eql(u8, std.fs.path.extension(argument), ".ui")) {
- try extra_ui_files.append(try alloc.dupe(u8, argument));
+ try extra_ui_files.append(alloc, try alloc.dupe(u8, argument));
}
}
@@ -132,16 +124,11 @@ pub fn main() !void {
\\
\\
);
- for (ui_files) |ui_file| {
- try writer.print(
- " src/apprt/gtk/ui/{0d}.{1d}/{2s}.ui\n",
- .{ ui_file.major, ui_file.minor, ui_file.name },
- );
- }
for (extra_ui_files.items) |ui_file| {
- const stem = std.fs.path.stem(ui_file);
for (blueprint_files) |file| {
- if (!std.mem.eql(u8, file.name, stem)) continue;
+ const expected = try std.fmt.allocPrint(alloc, "/{d}.{d}/{s}.ui", .{ file.major, file.minor, file.name });
+ defer alloc.free(expected);
+ if (!std.mem.endsWith(u8, ui_file, expected)) continue;
try writer.print(
" {s}\n",
.{ file.major, file.minor, file.name, ui_file },
@@ -157,7 +144,7 @@ pub fn main() !void {
}
pub const dependencies = deps: {
- const total = css_files.len + icons.len + ui_files.len + blueprint_files.len;
+ const total = css_files.len + icons.len + blueprint_files.len;
var deps: [total][]const u8 = undefined;
var index: usize = 0;
for (css_files) |css_file| {
@@ -168,14 +155,6 @@ pub const dependencies = deps: {
deps[index] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
index += 1;
}
- for (ui_files) |ui_file| {
- deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{d}.{d}/{s}.ui", .{
- ui_file.major,
- ui_file.minor,
- ui_file.name,
- });
- index += 1;
- }
for (blueprint_files) |blueprint_file| {
deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{d}.{d}/{s}.blp", .{
blueprint_file.major,
diff --git a/src/apprt/gtk/gtk_version.zig b/src/apprt/gtk/gtk_version.zig
index 59d7a5782..5d75fb4fe 100644
--- a/src/apprt/gtk/gtk_version.zig
+++ b/src/apprt/gtk/gtk_version.zig
@@ -87,10 +87,23 @@ pub inline fn runtimeAtLeast(
}) != .lt;
}
+pub inline fn runtimeUntil(
+ comptime major: u16,
+ comptime minor: u16,
+ comptime micro: u16,
+) bool {
+ const runtime_version = getRuntimeVersion();
+ return runtime_version.order(.{
+ .major = major,
+ .minor = minor,
+ .patch = micro,
+ }) == .lt;
+}
+
test "atLeast" {
const testing = std.testing;
- const funs = &.{ atLeast, runtimeAtLeast };
+ const funs = &.{ atLeast, runtimeAtLeast, runtimeUntil };
inline for (funs) |fun| {
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
diff --git a/src/apprt/gtk/inspector.zig b/src/apprt/gtk/inspector.zig
index aa4f6e435..e3e61e258 100644
--- a/src/apprt/gtk/inspector.zig
+++ b/src/apprt/gtk/inspector.zig
@@ -177,7 +177,7 @@ const Window = struct {
}
/// "destroy" signal for the window
- fn gtkDestroy(_: *gtk.ApplicationWindow, self: *Window) callconv(.C) void {
+ fn gtkDestroy(_: *gtk.ApplicationWindow, self: *Window) callconv(.c) void {
log.debug("window destroy", .{});
self.deinit();
}
diff --git a/src/apprt/gtk/menu.zig b/src/apprt/gtk/menu.zig
index d0a93b80d..d9d0083d0 100644
--- a/src/apprt/gtk/menu.zig
+++ b/src/apprt/gtk/menu.zig
@@ -41,7 +41,7 @@ pub fn Menu(
else => unreachable,
};
- var builder = Builder.init("menu-" ++ object_type ++ "-" ++ menu_name, 1, 0, .blp);
+ var builder = Builder.init("menu-" ++ object_type ++ "-" ++ menu_name, 1, 0);
defer builder.deinit();
const menu_model = builder.getObject(gio.MenuModel, "menu").?;
@@ -130,7 +130,7 @@ pub fn Menu(
}
/// Refocus tab that lost focus because of the popover menu
- fn gtkRefocusTerm(_: *gtk.PopoverMenu, self: *Self) callconv(.C) void {
+ fn gtkRefocusTerm(_: *gtk.PopoverMenu, self: *Self) callconv(.c) void {
const window: *Window = switch (T) {
Window => self.parent,
Surface => self.parent.container.window() orelse return,
diff --git a/src/apprt/gtk/ui/1.2/ccw-osc-52-read.ui b/src/apprt/gtk/ui/1.2/ccw-osc-52-read.ui
deleted file mode 100644
index 82512e3a2..000000000
--- a/src/apprt/gtk/ui/1.2/ccw-osc-52-read.ui
+++ /dev/null
@@ -1,77 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/apprt/gtk/ui/1.2/ccw-osc-52-write.ui b/src/apprt/gtk/ui/1.2/ccw-osc-52-write.ui
deleted file mode 100644
index 195fb1de1..000000000
--- a/src/apprt/gtk/ui/1.2/ccw-osc-52-write.ui
+++ /dev/null
@@ -1,77 +0,0 @@
-
-
-
-
-
- Authorize Clipboard Access
- An application is attempting to write to the clipboard. The current clipboard contents are shown below.
-
- Deny
- Allow
-
- cancel
- cancel
-
-
-
-
-
- 500
- 250
-
-
- false
- false
- true
- 8
- 8
- 8
- 8
-
-
-
-
-
-
-
- false
- 2
- 1
- 12
- 12
-
-
- view-reveal-symbolic
-
-
-
-
-
-
- false
- 2
- 1
- 12
- 12
-
-
-
- view-conceal-symbolic
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/apprt/gtk/ui/1.2/ccw-paste.ui b/src/apprt/gtk/ui/1.2/ccw-paste.ui
deleted file mode 100644
index 342c767e6..000000000
--- a/src/apprt/gtk/ui/1.2/ccw-paste.ui
+++ /dev/null
@@ -1,77 +0,0 @@
-
-
-
-
-
- Warning: Potentially Unsafe Paste
- Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.
-
- Cancel
- Paste
-
- cancel
- cancel
-
-
-
-
-
- 500
- 250
-
-
- false
- false
- true
- 8
- 8
- 8
- 8
-
-
-
-
-
-
-
- false
- 2
- 1
- 12
- 12
-
-
- view-reveal-symbolic
-
-
-
-
-
-
- false
- 2
- 1
- 12
- 12
-
-
-
- view-conceal-symbolic
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/apprt/gtk/ui/1.2/config-errors-dialog.ui b/src/apprt/gtk/ui/1.2/config-errors-dialog.ui
deleted file mode 100644
index 1d7517f7a..000000000
--- a/src/apprt/gtk/ui/1.2/config-errors-dialog.ui
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
- Configuration Errors
- One or more configuration errors were found. Please review the errors below, and either reload your configuration or ignore these errors.
-
- Ignore
- Reload Configuration
-
-
-
- 500
- 100
-
-
- false
- false
- 8
- 8
- 8
- 8
-
-
-
-
-
-
-
-
-
diff --git a/src/apprt/gtk/ui/README.md b/src/apprt/gtk/ui/README.md
index 08f3f367c..b9dc732b6 100644
--- a/src/apprt/gtk/ui/README.md
+++ b/src/apprt/gtk/ui/README.md
@@ -1,21 +1,15 @@
# GTK UI files
-This directory is for storing GTK resource definitions. With one exception, the
-files should be be in the Blueprint markup language.
+This directory is for storing GTK blueprints. GTK blueprints are compiled into
+GTK resource builder `.ui` files by `blueprint-compiler` at build time and then
+converted into an embeddable resource by `glib-compile-resources`.
-Resource files should be stored in directories that represent the minimum
-Adwaita version needed to use that resource. Resource files should also be
-formatted using `blueprint-compiler format` as well to ensure consistency.
+Blueprint files should be stored in directories that represent the minimum
+Adwaita version needed to use that resource. Blueprint files should also be
+formatted using `blueprint-compiler format` as well to ensure consistency
+(formatting will be checked in CI).
-The one exception to files being in Blueprint markup language is when Adwaita
-features are used that the `blueprint-compiler` on a supported platform does not
-compile. For example, Debian 12 includes Adwaita 1.2 and `blueprint-compiler`
-0.6.0. Adwaita 1.2 includes support for `MessageDialog` but `blueprint-compiler`
-0.6.0 does not. In cases like that the Blueprint markup should be compiled on a
-platform that provides a new enough `blueprint-compiler` and the resulting `.ui`
-file should be committed to the Ghostty source code. Care should be taken that
-the `.blp` file and the `.ui` file remain in sync.
-
-In all other cases only the `.blp` should be committed to the Ghostty source
-code. The build process will use `blueprint-compiler` to generate the `.ui`
-files necessary at runtime.
+`blueprint-compiler` version 0.16.0 or newer is required to compile Blueprint
+files. If your system does not have `blueprint-compiler` or does not have a
+new enough version you can use the generated source tarballs, which contain
+precompiled versions of the blueprints.
diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig
index 6737e98e2..5f5feca6e 100644
--- a/src/apprt/gtk/winproto/wayland.zig
+++ b/src/apprt/gtk/winproto/wayland.zig
@@ -410,7 +410,7 @@ pub const Window = struct {
_: *gdk.Surface,
monitor: *gdk.Monitor,
apprt_window: *ApprtWindow,
- ) callconv(.C) void {
+ ) callconv(.c) void {
const window = apprt_window.window.as(gtk.Window);
const size = apprt_window.config.quick_terminal_size;
const position = apprt_window.config.quick_terminal_position;
diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig
index 4f9373adb..4b97298f7 100644
--- a/src/build/SharedDeps.zig
+++ b/src/build/SharedDeps.zig
@@ -662,34 +662,6 @@ fn addGTK(
}
{
- // For our actual build, we validate our GTK builder files if we can.
- {
- const gtk_builder_check = b.addExecutable(.{
- .name = "gtk_builder_check",
- .root_source_file = b.path("src/apprt/gtk/builder_check.zig"),
- .target = b.graph.host,
- });
- gtk_builder_check.root_module.addOptions("build_options", self.options);
- if (gobject_) |gobject| {
- gtk_builder_check.root_module.addImport(
- "gtk",
- gobject.module("gtk4"),
- );
- gtk_builder_check.root_module.addImport(
- "adw",
- gobject.module("adw1"),
- );
- }
-
- for (gresource.dependencies) |pathname| {
- const extension = std.fs.path.extension(pathname);
- if (!std.mem.eql(u8, extension, ".ui")) continue;
- const check = b.addRunArtifact(gtk_builder_check);
- check.addFileArg(b.path(pathname));
- step.step.dependOn(&check.step);
- }
- }
-
// Get our gresource c/h files and add them to our build.
const dist = gtkDistResources(b);
step.addCSourceFile(.{ .file = dist.resources_c.path(b), .flags = &.{} });
diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig
index 8ebac4487..54f4c0969 100644
--- a/src/cli/list_themes.zig
+++ b/src/cli/list_themes.zig
@@ -24,6 +24,9 @@ pub const Options = struct {
/// If true, force a plain list of themes.
plain: bool = false,
+ /// Specifies the color scheme of the themes to include in the list.
+ color: enum { all, dark, light } = .all,
+
pub fn deinit(self: Options) void {
_ = self;
}
@@ -93,6 +96,9 @@ const ThemeListElement = struct {
/// * `--path`: Show the full path to the theme.
///
/// * `--plain`: Force a plain listing of themes.
+///
+/// * `--color`: Specify the color scheme of the themes included in the list.
+/// This can be `dark`, `light`, or `all`. The default is `all`.
pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
var opts: Options = .{};
defer opts.deinit();
@@ -137,11 +143,30 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
if (std.mem.eql(u8, entry.name, ".DS_Store"))
continue;
count += 1;
- try themes.append(.{
- .location = loc.location,
- .path = try std.fs.path.join(alloc, &.{ loc.dir, entry.name }),
- .theme = try alloc.dupe(u8, entry.name),
- });
+
+ const path = try std.fs.path.join(alloc, &.{ loc.dir, entry.name });
+ // if there is no need to filter just append the theme to the list
+ if (opts.color == .all) {
+ try themes.append(.{
+ .path = path,
+ .location = loc.location,
+ .theme = try alloc.dupe(u8, entry.name),
+ });
+ continue;
+ }
+
+ // otherwise check if the theme should be included based on the provided options
+ var config = try Config.default(alloc);
+ defer config.deinit();
+ try config.loadFile(config._arena.?.allocator(), path);
+
+ if (shouldIncludeTheme(opts, config)) {
+ try themes.append(.{
+ .path = path,
+ .location = loc.location,
+ .theme = try alloc.dupe(u8, entry.name),
+ });
+ }
},
else => {},
}
@@ -1594,3 +1619,13 @@ fn preview(allocator: std.mem.Allocator, themes: []ThemeListElement) !void {
defer app.deinit();
try app.run();
}
+
+fn shouldIncludeTheme(opts: Options, theme_config: Config) bool {
+ const rf = @as(f32, @floatFromInt(theme_config.background.r)) / 255.0;
+ const gf = @as(f32, @floatFromInt(theme_config.background.g)) / 255.0;
+ const bf = @as(f32, @floatFromInt(theme_config.background.b)) / 255.0;
+ const luminance = 0.2126 * rf + 0.7152 * gf + 0.0722 * bf;
+ const is_dark = luminance < 0.5;
+
+ return (opts.color == .dark and is_dark) or (opts.color == .light and !is_dark);
+}
diff --git a/src/config/Config.zig b/src/config/Config.zig
index bfaaff554..05a4a73bb 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -2008,7 +2008,7 @@ keybind: Keybinds = .{},
/// macOS doesn't have a distinct "alt" key and instead has the "option"
/// key which behaves slightly differently. On macOS by default, the
-/// option key plus a character will sometimes produces a Unicode character.
+/// option key plus a character will sometimes produce a Unicode character.
/// For example, on US standard layouts option-b produces "∫". This may be
/// undesirable if you want to use "option" as an "alt" key for keybindings
/// in terminal programs or shells.
diff --git a/src/crash/sentry.zig b/src/crash/sentry.zig
index e9c49048c..c29184020 100644
--- a/src/crash/sentry.zig
+++ b/src/crash/sentry.zig
@@ -166,7 +166,7 @@ fn beforeSend(
event_val: sentry.c.sentry_value_t,
_: ?*anyopaque,
_: ?*anyopaque,
-) callconv(.C) sentry.c.sentry_value_t {
+) callconv(.c) sentry.c.sentry_value_t {
// The native SDK at the time of writing doesn't support thread-local
// scopes. The full SDK has one global scope. So we use the beforeSend
// handler to set thread-specific data such as window size, grid size,
@@ -237,7 +237,7 @@ fn beforeSend(
}
pub const Transport = struct {
- pub fn send(envelope: *sentry.Envelope, ud: ?*anyopaque) callconv(.C) void {
+ pub fn send(envelope: *sentry.Envelope, ud: ?*anyopaque) callconv(.c) void {
_ = ud;
defer envelope.deinit();
diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig
index 326ca0186..37093b59a 100644
--- a/src/font/CodepointResolver.zig
+++ b/src/font/CodepointResolver.zig
@@ -380,7 +380,7 @@ test getIndex {
const testEmoji = font.embedded.emoji;
const testEmojiText = font.embedded.emoji_text;
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
var c = Collection.init();
@@ -461,7 +461,7 @@ test "getIndex disabled font style" {
var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale);
defer atlas_grayscale.deinit(alloc);
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
var c = Collection.init();
@@ -513,7 +513,7 @@ test "getIndex box glyph" {
const testing = std.testing;
const alloc = testing.allocator;
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
const c = Collection.init();
diff --git a/src/font/Collection.zig b/src/font/Collection.zig
index cfc633b04..59f89d402 100644
--- a/src/font/Collection.zig
+++ b/src/font/Collection.zig
@@ -78,8 +78,8 @@ pub const AddError = Allocator.Error || error{
/// next in priority if others exist already, i.e. it'll be the _last_ to be
/// searched for a glyph in that list.
///
-/// The collection takes ownership of the face. The face will be deallocated
-/// when the collection is deallocated.
+/// If no error is encountered then the collection takes ownership of the face,
+/// in which case face will be deallocated when the collection is deallocated.
///
/// If a loaded face is added to the collection, it should be the same
/// size as all the other faces in the collection. This function will not
@@ -700,7 +700,7 @@ test "add full" {
const alloc = testing.allocator;
const testFont = font.embedded.regular;
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
var c = init();
@@ -714,15 +714,18 @@ test "add full" {
) });
}
- try testing.expectError(error.CollectionFull, c.add(
- alloc,
- .regular,
- .{ .loaded = try Face.init(
- lib,
- testFont,
- .{ .size = .{ .points = 12 } },
- ) },
- ));
+ var face = try Face.init(
+ lib,
+ testFont,
+ .{ .size = .{ .points = 12 } },
+ );
+ // We have to deinit it manually since the
+ // collection doesn't do it if adding fails.
+ defer face.deinit();
+ try testing.expectError(
+ error.CollectionFull,
+ c.add(alloc, .regular, .{ .loaded = face }),
+ );
}
test "add deferred without loading options" {
@@ -746,7 +749,7 @@ test getFace {
const alloc = testing.allocator;
const testFont = font.embedded.regular;
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
var c = init();
@@ -770,7 +773,7 @@ test getIndex {
const alloc = testing.allocator;
const testFont = font.embedded.regular;
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
var c = init();
@@ -801,7 +804,7 @@ test completeStyles {
const alloc = testing.allocator;
const testFont = font.embedded.regular;
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
var c = init();
@@ -828,7 +831,7 @@ test setSize {
const alloc = testing.allocator;
const testFont = font.embedded.regular;
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
var c = init();
@@ -851,7 +854,7 @@ test hasCodepoint {
const alloc = testing.allocator;
const testFont = font.embedded.regular;
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
var c = init();
@@ -875,7 +878,7 @@ test "hasCodepoint emoji default graphical" {
const alloc = testing.allocator;
const testEmoji = font.embedded.emoji;
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
var c = init();
@@ -898,7 +901,7 @@ test "metrics" {
const alloc = testing.allocator;
const testFont = font.embedded.inconsolata;
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
var c = init();
diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig
index 3ee104386..8794ccea9 100644
--- a/src/font/DeferredFace.zig
+++ b/src/font/DeferredFace.zig
@@ -407,7 +407,7 @@ test "fontconfig" {
const alloc = testing.allocator;
// Load freetype
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
// Get a deferred face from fontconfig
@@ -425,7 +425,8 @@ test "fontconfig" {
try testing.expect(n.len > 0);
// Load it and verify it works
- const face = try def.load(lib, .{ .size = .{ .points = 12 } });
+ var face = try def.load(lib, .{ .size = .{ .points = 12 } });
+ defer face.deinit();
try testing.expect(face.glyphIndex(' ') != null);
}
@@ -437,7 +438,7 @@ test "coretext" {
const alloc = testing.allocator;
// Load freetype
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
// Get a deferred face from fontconfig
@@ -456,6 +457,7 @@ test "coretext" {
try testing.expect(n.len > 0);
// Load it and verify it works
- const face = try def.load(lib, .{ .size = .{ .points = 12 } });
+ var face = try def.load(lib, .{ .size = .{ .points = 12 } });
+ defer face.deinit();
try testing.expect(face.glyphIndex(' ') != null);
}
diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig
index 65c7ecd87..72e97fad8 100644
--- a/src/font/SharedGrid.zig
+++ b/src/font/SharedGrid.zig
@@ -338,7 +338,7 @@ test getIndex {
const alloc = testing.allocator;
// const testEmoji = @import("test.zig").fontEmoji;
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
var grid = try testGrid(.normal, alloc, lib);
diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig
index ca535eaf8..8ad30629e 100644
--- a/src/font/SharedGridSet.zig
+++ b/src/font/SharedGridSet.zig
@@ -50,7 +50,7 @@ pub const InitError = Library.InitError;
/// Initialize a new SharedGridSet.
pub fn init(alloc: Allocator) InitError!SharedGridSet {
- var font_lib = try Library.init();
+ var font_lib = try Library.init(alloc);
errdefer font_lib.deinit();
return .{
diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig
index 3749b4824..639eae43c 100644
--- a/src/font/face/coretext.zig
+++ b/src/font/face/coretext.zig
@@ -46,7 +46,11 @@ pub const Face = struct {
};
/// Initialize a CoreText-based font from a TTF/TTC in memory.
- pub fn init(lib: font.Library, source: [:0]const u8, opts: font.face.Options) !Face {
+ pub fn init(
+ lib: font.Library,
+ source: [:0]const u8,
+ opts: font.face.Options,
+ ) !Face {
_ = lib;
const data = try macos.foundation.Data.createWithBytesNoCopy(source);
@@ -914,7 +918,7 @@ test "in-memory" {
var atlas = try font.Atlas.init(alloc, 512, .grayscale);
defer atlas.deinit(alloc);
- var lib = try font.Library.init();
+ var lib = try font.Library.init(alloc);
defer lib.deinit();
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
@@ -941,7 +945,7 @@ test "variable" {
var atlas = try font.Atlas.init(alloc, 512, .grayscale);
defer atlas.deinit(alloc);
- var lib = try font.Library.init();
+ var lib = try font.Library.init(alloc);
defer lib.deinit();
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
@@ -968,7 +972,7 @@ test "variable set variation" {
var atlas = try font.Atlas.init(alloc, 512, .grayscale);
defer atlas.deinit(alloc);
- var lib = try font.Library.init();
+ var lib = try font.Library.init(alloc);
defer lib.deinit();
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
@@ -996,7 +1000,7 @@ test "svg font table" {
const alloc = testing.allocator;
const testFont = font.embedded.julia_mono;
- var lib = try font.Library.init();
+ var lib = try font.Library.init(alloc);
defer lib.deinit();
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
@@ -1010,9 +1014,10 @@ test "svg font table" {
test "glyphIndex colored vs text" {
const testing = std.testing;
+ const alloc = testing.allocator;
const testFont = font.embedded.julia_mono;
- var lib = try font.Library.init();
+ var lib = try font.Library.init(alloc);
defer lib.deinit();
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig
index c2eab4599..bf86b88de 100644
--- a/src/font/face/freetype.zig
+++ b/src/font/face/freetype.zig
@@ -29,12 +29,20 @@ pub const Face = struct {
assert(font.face.FreetypeLoadFlags != void);
}
- /// Our freetype library
- lib: freetype.Library,
+ /// Our Library
+ lib: Library,
/// Our font face.
face: freetype.Face,
+ /// This mutex MUST be held while doing anything with the
+ /// glyph slot on the freetype face, because this struct
+ /// may be shared across multiple surfaces.
+ ///
+ /// This means that anywhere where `self.face.loadGlyph`
+ /// is called, this mutex must be held.
+ ft_mutex: *std.Thread.Mutex,
+
/// Harfbuzz font corresponding to this face.
hb_font: harfbuzz.Font,
@@ -59,30 +67,52 @@ pub const Face = struct {
};
/// Initialize a new font face with the given source in-memory.
- pub fn initFile(lib: Library, path: [:0]const u8, index: i32, opts: font.face.Options) !Face {
+ pub fn initFile(
+ lib: Library,
+ path: [:0]const u8,
+ index: i32,
+ opts: font.face.Options,
+ ) !Face {
+ lib.mutex.lock();
+ defer lib.mutex.unlock();
const face = try lib.lib.initFace(path, index);
errdefer face.deinit();
return try initFace(lib, face, opts);
}
/// Initialize a new font face with the given source in-memory.
- pub fn init(lib: Library, source: [:0]const u8, opts: font.face.Options) !Face {
+ pub fn init(
+ lib: Library,
+ source: [:0]const u8,
+ opts: font.face.Options,
+ ) !Face {
+ lib.mutex.lock();
+ defer lib.mutex.unlock();
const face = try lib.lib.initMemoryFace(source, 0);
errdefer face.deinit();
return try initFace(lib, face, opts);
}
- fn initFace(lib: Library, face: freetype.Face, opts: font.face.Options) !Face {
+ fn initFace(
+ lib: Library,
+ face: freetype.Face,
+ opts: font.face.Options,
+ ) !Face {
try face.selectCharmap(.unicode);
try setSize_(face, opts.size);
var hb_font = try harfbuzz.freetype.createFont(face.handle);
errdefer hb_font.destroy();
+ const ft_mutex = try lib.alloc.create(std.Thread.Mutex);
+ errdefer lib.alloc.destroy(ft_mutex);
+ ft_mutex.* = .{};
+
var result: Face = .{
- .lib = lib.lib,
+ .lib = lib,
.face = face,
.hb_font = hb_font,
+ .ft_mutex = ft_mutex,
.load_flags = opts.freetype_load_flags,
};
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
@@ -114,7 +144,13 @@ pub const Face = struct {
}
pub fn deinit(self: *Face) void {
- self.face.deinit();
+ self.lib.alloc.destroy(self.ft_mutex);
+ {
+ self.lib.mutex.lock();
+ defer self.lib.mutex.unlock();
+
+ self.face.deinit();
+ }
self.hb_font.destroy();
self.* = undefined;
}
@@ -147,11 +183,7 @@ pub const Face = struct {
self.face.ref();
errdefer self.face.deinit();
- var f = try initFace(
- .{ .lib = self.lib },
- self.face,
- opts,
- );
+ var f = try initFace(self.lib, self.face, opts);
errdefer f.deinit();
f.synthetic = self.synthetic;
f.synthetic.bold = true;
@@ -166,11 +198,7 @@ pub const Face = struct {
self.face.ref();
errdefer self.face.deinit();
- var f = try initFace(
- .{ .lib = self.lib },
- self.face,
- opts,
- );
+ var f = try initFace(self.lib, self.face, opts);
errdefer f.deinit();
f.synthetic = self.synthetic;
f.synthetic.italic = true;
@@ -228,7 +256,7 @@ pub const Face = struct {
// first thing we have to do is get all the vars and put them into
// an array.
const mm = try self.face.getMMVar();
- defer self.lib.doneMMVar(mm);
+ defer self.lib.lib.doneMMVar(mm);
// To avoid allocations, we cap the number of variation axes we can
// support. This is arbitrary but Firefox caps this at 16 so I
@@ -270,6 +298,9 @@ pub const Face = struct {
/// Returns true if the given glyph ID is colorized.
pub fn isColorGlyph(self: *const Face, glyph_id: u32) bool {
+ self.ft_mutex.lock();
+ defer self.ft_mutex.unlock();
+
// Load the glyph and see what pixel mode it renders with.
// All modes other than BGRA are non-color.
// If the glyph fails to load, just return false.
@@ -296,6 +327,9 @@ pub const Face = struct {
glyph_index: u32,
opts: font.face.RenderOptions,
) !Glyph {
+ self.ft_mutex.lock();
+ defer self.ft_mutex.unlock();
+
const metrics = opts.grid_metrics;
// If we have synthetic italic, then we apply a transformation matrix.
@@ -741,6 +775,9 @@ pub const Face = struct {
// If we fail to load any visible ASCII we just use max_advance from
// the metrics provided by FreeType.
const cell_width: f64 = cell_width: {
+ self.ft_mutex.lock();
+ defer self.ft_mutex.unlock();
+
var max: f64 = 0.0;
var c: u8 = ' ';
while (c < 127) : (c += 1) {
@@ -780,6 +817,8 @@ pub const Face = struct {
break :heights .{
cap: {
+ self.ft_mutex.lock();
+ defer self.ft_mutex.unlock();
if (face.getCharIndex('H')) |glyph_index| {
if (face.loadGlyph(glyph_index, .{
.render = true,
@@ -791,6 +830,8 @@ pub const Face = struct {
break :cap null;
},
ex: {
+ self.ft_mutex.lock();
+ defer self.ft_mutex.unlock();
if (face.getCharIndex('x')) |glyph_index| {
if (face.loadGlyph(glyph_index, .{
.render = true,
@@ -832,7 +873,7 @@ test {
const testFont = font.embedded.inconsolata;
const alloc = testing.allocator;
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
var atlas = try font.Atlas.init(alloc, 512, .grayscale);
@@ -881,7 +922,7 @@ test "color emoji" {
const alloc = testing.allocator;
const testFont = font.embedded.emoji;
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
var atlas = try font.Atlas.init(alloc, 512, .rgba);
@@ -936,7 +977,7 @@ test "mono to rgba" {
const alloc = testing.allocator;
const testFont = font.embedded.emoji;
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
var atlas = try font.Atlas.init(alloc, 512, .rgba);
@@ -958,7 +999,7 @@ test "svg font table" {
const alloc = testing.allocator;
const testFont = font.embedded.julia_mono;
- var lib = try font.Library.init();
+ var lib = try font.Library.init(alloc);
defer lib.deinit();
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12, .xdpi = 72, .ydpi = 72 } });
@@ -995,7 +1036,7 @@ test "bitmap glyph" {
const alloc = testing.allocator;
const testFont = font.embedded.terminus_ttf;
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
defer lib.deinit();
var atlas = try font.Atlas.init(alloc, 512, .grayscale);
diff --git a/src/font/library.zig b/src/font/library.zig
index b00bbfce0..43aa101b7 100644
--- a/src/font/library.zig
+++ b/src/font/library.zig
@@ -1,5 +1,7 @@
//! A library represents the shared state that the underlying font
//! library implementation(s) require per-process.
+const std = @import("std");
+const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const options = @import("main.zig").options;
const freetype = @import("freetype");
@@ -24,13 +26,26 @@ pub const Library = switch (options.backend) {
pub const FreetypeLibrary = struct {
lib: freetype.Library,
- pub const InitError = freetype.Error;
+ alloc: Allocator,
- pub fn init() InitError!Library {
- return Library{ .lib = try freetype.Library.init() };
+ /// Mutex to be held any time the library is
+ /// being used to create or destroy a face.
+ mutex: *std.Thread.Mutex,
+
+ pub const InitError = freetype.Error || Allocator.Error;
+
+ pub fn init(alloc: Allocator) InitError!Library {
+ const lib = try freetype.Library.init();
+ errdefer lib.deinit();
+
+ const mutex = try alloc.create(std.Thread.Mutex);
+ mutex.* = .{};
+
+ return Library{ .lib = lib, .alloc = alloc, .mutex = mutex };
}
pub fn deinit(self: *Library) void {
+ self.alloc.destroy(self.mutex);
self.lib.deinit();
}
};
@@ -38,7 +53,8 @@ pub const FreetypeLibrary = struct {
pub const NoopLibrary = struct {
pub const InitError = error{};
- pub fn init() InitError!Library {
+ pub fn init(alloc: Allocator) InitError!Library {
+ _ = alloc;
return Library{};
}
diff --git a/src/font/opentype/svg.zig b/src/font/opentype/svg.zig
index 01d172d17..ff8eeed49 100644
--- a/src/font/opentype/svg.zig
+++ b/src/font/opentype/svg.zig
@@ -99,7 +99,7 @@ test "SVG" {
const alloc = testing.allocator;
const testFont = font.embedded.julia_mono;
- var lib = try font.Library.init();
+ var lib = try font.Library.init(alloc);
defer lib.deinit();
var face = try font.Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig
index ec64fe6eb..f2ac5b85d 100644
--- a/src/font/shaper/coretext.zig
+++ b/src/font/shaper/coretext.zig
@@ -1761,7 +1761,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
.nerd_font => font.embedded.nerd_font,
};
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
errdefer lib.deinit();
var c = Collection.init();
diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig
index b284dc140..eb8130f79 100644
--- a/src/font/shaper/harfbuzz.zig
+++ b/src/font/shaper/harfbuzz.zig
@@ -1220,7 +1220,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
.arabic => font.embedded.arabic,
};
- var lib = try Library.init();
+ var lib = try Library.init(alloc);
errdefer lib.deinit();
var c = Collection.init();
diff --git a/src/input/Binding.zig b/src/input/Binding.zig
index 10e16f1fe..6583e1462 100644
--- a/src/input/Binding.zig
+++ b/src/input/Binding.zig
@@ -279,6 +279,7 @@ pub const Action = union(enum) {
/// Scroll the screen varying amounts.
scroll_to_top,
scroll_to_bottom,
+ scroll_to_selection,
scroll_page_up,
scroll_page_down,
scroll_page_fractional: f32,
@@ -345,7 +346,7 @@ pub const Action = union(enum) {
move_tab: isize,
/// Toggle the tab overview.
- /// This only works with libadwaita enabled currently.
+ /// This only works with libadwaita version 1.4.0 or newer.
toggle_tab_overview,
/// Change the title of the current focused surface via a prompt.
@@ -567,6 +568,8 @@ pub const Action = union(enum) {
left,
up,
auto, // splits along the larger direction
+
+ pub const default: SplitDirection = .auto;
};
pub const SplitFocusDirection = enum {
@@ -728,7 +731,28 @@ pub const Action = union(enum) {
Action.CursorKey => return Error.InvalidAction,
else => {
- const idx = colonIdx orelse return Error.InvalidFormat;
+ // Get the parameter after the colon. The parameter
+ // can be optional for action types that can have a
+ // "default" decl.
+ const idx = colonIdx orelse {
+ switch (@typeInfo(field.type)) {
+ .@"struct",
+ .@"union",
+ .@"enum",
+ => if (@hasDecl(field.type, "default")) {
+ return @unionInit(
+ Action,
+ field.name,
+ @field(field.type, "default"),
+ );
+ },
+
+ else => {},
+ }
+
+ return Error.InvalidFormat;
+ };
+
const param = input[idx + 1 ..];
return @unionInit(
Action,
@@ -789,6 +813,7 @@ pub const Action = union(enum) {
.select_all,
.scroll_to_top,
.scroll_to_bottom,
+ .scroll_to_selection,
.scroll_page_up,
.scroll_page_down,
.scroll_page_fractional,
@@ -2013,6 +2038,17 @@ test "parse: action with enum" {
}
}
+test "parse: action with enum with default" {
+ const testing = std.testing;
+
+ // parameter
+ {
+ const binding = try parseSingle("a=new_split");
+ try testing.expect(binding.action == .new_split);
+ try testing.expectEqual(Action.SplitDirection.auto, binding.action.new_split);
+ }
+}
+
test "parse: action with int" {
const testing = std.testing;
diff --git a/src/input/command.zig b/src/input/command.zig
index 701d537a1..1f685269b 100644
--- a/src/input/command.zig
+++ b/src/input/command.zig
@@ -170,6 +170,12 @@ fn actionCommands(action: Action.Key) []const Command {
.description = "Scroll to the bottom of the screen.",
}},
+ .scroll_to_selection => comptime &.{.{
+ .action = .scroll_to_selection,
+ .title = "Scroll to Selection",
+ .description = "Scroll to the selected text.",
+ }},
+
.scroll_page_up => comptime &.{.{
.action = .scroll_page_up,
.title = "Scroll Page Up",
diff --git a/src/input/key.zig b/src/input/key.zig
index ec65170f2..c0f80e294 100644
--- a/src/input/key.zig
+++ b/src/input/key.zig
@@ -401,7 +401,8 @@ pub const Key = enum(c_int) {
kp_delete,
kp_begin,
- // TODO: media keys
+ // special keys
+ context_menu,
// modifiers
left_shift,
@@ -579,6 +580,7 @@ pub const Key = enum(c_int) {
.backspace => cimgui.c.ImGuiKey_Backspace,
.print_screen => cimgui.c.ImGuiKey_PrintScreen,
.pause => cimgui.c.ImGuiKey_Pause,
+ .context_menu => cimgui.c.ImGuiKey_Menu,
.f1 => cimgui.c.ImGuiKey_F1,
.f2 => cimgui.c.ImGuiKey_F2,
diff --git a/src/input/keycodes.zig b/src/input/keycodes.zig
index 67ce46daf..e9adbc156 100644
--- a/src/input/keycodes.zig
+++ b/src/input/keycodes.zig
@@ -153,6 +153,7 @@ const code_to_key = code_to_key: {
.{ "Numpad0", .kp_0 },
.{ "NumpadDecimal", .kp_decimal },
.{ "NumpadEqual", .kp_equal },
+ .{ "ContextMenu", .context_menu },
.{ "ControlLeft", .left_control },
.{ "ShiftLeft", .left_shift },
.{ "AltLeft", .left_alt },
diff --git a/src/os/flatpak.zig b/src/os/flatpak.zig
index 61a217929..7b92a8ba9 100644
--- a/src/os/flatpak.zig
+++ b/src/os/flatpak.zig
@@ -444,7 +444,7 @@ pub const FlatpakHostCommand = struct {
_: [*c]const u8,
params: ?*c.GVariant,
ud: ?*anyopaque,
- ) callconv(.C) void {
+ ) callconv(.c) void {
const self = @as(*FlatpakHostCommand, @ptrCast(@alignCast(ud)));
const state = state: {
self.state_mutex.lock();
diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig
index 160c68a5b..5aa2ba59a 100644
--- a/src/renderer/Metal.zig
+++ b/src/renderer/Metal.zig
@@ -1524,7 +1524,7 @@ const CompletionBlock = objc.Block(struct { self: *Metal }, .{
fn bufferCompleted(
block: *const CompletionBlock.Context,
buffer_id: objc.c.id,
-) callconv(.C) void {
+) callconv(.c) void {
const self = block.self;
const buffer = objc.Object.fromId(buffer_id);
diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig
index 8c9b68447..45d86cbfe 100644
--- a/src/renderer/shadertoy.zig
+++ b/src/renderer/shadertoy.zig
@@ -250,7 +250,7 @@ fn spvCross(
// It would be better to get this out into an output parameter to
// show users but for now we can just log it.
c.spvc_context_set_error_callback(ctx, @ptrCast(&(struct {
- fn callback(_: ?*anyopaque, msg_ptr: [*c]const u8) callconv(.C) void {
+ fn callback(_: ?*anyopaque, msg_ptr: [*c]const u8) callconv(.c) void {
const msg = std.mem.sliceTo(msg_ptr, 0);
std.log.warn("spirv-cross error message={s}", .{msg});
}
diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig
index abe49a47b..23c626879 100644
--- a/src/termio/Exec.zig
+++ b/src/termio/Exec.zig
@@ -745,7 +745,7 @@ const Subprocess = struct {
});
arena: std.heap.ArenaAllocator,
- cwd: ?[]const u8,
+ cwd: ?[:0]const u8,
env: ?EnvMap,
args: []const [:0]const u8,
grid_size: renderer.GridSize,
@@ -985,8 +985,8 @@ const Subprocess = struct {
// We have to copy the cwd because there is no guarantee that
// pointers in full_config remain valid.
- const cwd: ?[]u8 = if (cfg.working_directory) |cwd|
- try alloc.dupe(u8, cwd)
+ const cwd: ?[:0]u8 = if (cfg.working_directory) |cwd|
+ try alloc.dupeZ(u8, cwd)
else
null;
@@ -1048,6 +1048,47 @@ const Subprocess = struct {
log.debug("starting command command={s}", .{self.args});
+ // If we can't access the cwd, then don't set any cwd and inherit.
+ // This is important because our cwd can be set by the shell (OSC 7)
+ // and we don't want to break new windows.
+ const cwd: ?[:0]const u8 = if (self.cwd) |proposed| cwd: {
+ if ((comptime build_config.flatpak) and internal_os.isFlatpak()) {
+ // Flatpak sandboxing prevents access to certain reserved paths
+ // regardless of configured permissions. Perform a test spawn
+ // to get around this problem
+ //
+ // https://docs.flatpak.org/en/latest/sandbox-permissions.html#reserved-paths
+ log.info("flatpak detected, will use host command to verify cwd access", .{});
+ const dev_null = try std.fs.cwd().openFile("/dev/null", .{ .mode = .read_write });
+ defer dev_null.close();
+ var cmd: internal_os.FlatpakHostCommand = .{
+ .argv = &[_][]const u8{
+ "/bin/sh",
+ "-c",
+ ":",
+ },
+ .cwd = proposed,
+ .stdin = dev_null.handle,
+ .stdout = dev_null.handle,
+ .stderr = dev_null.handle,
+ };
+ _ = cmd.spawn(alloc) catch |err| {
+ log.warn("cannot spawn command at cwd, ignoring: {}", .{err});
+ break :cwd null;
+ };
+ _ = try cmd.wait();
+
+ break :cwd proposed;
+ }
+
+ if (std.fs.cwd().access(proposed, .{})) {
+ break :cwd proposed;
+ } else |err| {
+ log.warn("cannot access cwd, ignoring: {}", .{err});
+ break :cwd null;
+ }
+ } else null;
+
// In flatpak, we use the HostCommand to execute our shell.
if (internal_os.isFlatpak()) flatpak: {
if (comptime !build_config.flatpak) {
@@ -1058,6 +1099,7 @@ const Subprocess = struct {
// Flatpak command must have a stable pointer.
self.flatpak_command = .{
.argv = self.args,
+ .cwd = cwd,
.env = if (self.env) |*env| env else null,
.stdin = pty.slave,
.stdout = pty.slave,
@@ -1083,18 +1125,6 @@ const Subprocess = struct {
};
}
- // If we can't access the cwd, then don't set any cwd and inherit.
- // This is important because our cwd can be set by the shell (OSC 7)
- // and we don't want to break new windows.
- const cwd: ?[]const u8 = if (self.cwd) |proposed| cwd: {
- if (std.fs.cwd().access(proposed, .{})) {
- break :cwd proposed;
- } else |err| {
- log.warn("cannot access cwd, ignoring: {}", .{err});
- break :cwd null;
- }
- } else null;
-
// Build our subcommand
var cmd: Command = .{
.path = self.args[0],
diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig
index 2cf809694..fb62327d3 100644
--- a/src/termio/shell_integration.zig
+++ b/src/termio/shell_integration.zig
@@ -239,7 +239,7 @@ fn setupBash(
resource_dir: []const u8,
env: *EnvMap,
) !?config.Command {
- var args = try std.ArrayList([:0]const u8).initCapacity(alloc, 2);
+ var args = try std.ArrayList([:0]const u8).initCapacity(alloc, 3);
defer args.deinit();
// Iterator that yields each argument in the original command line.
@@ -247,12 +247,17 @@ fn setupBash(
var iter = try command.argIterator(alloc);
defer iter.deinit();
- // Start accumulating arguments with the executable and `--posix` mode flag.
+ // Start accumulating arguments with the executable and initial flags.
if (iter.next()) |exe| {
try args.append(try alloc.dupeZ(u8, exe));
} else return null;
try args.append("--posix");
+ // On macOS, we request a login shell to match that platform's norms.
+ if (comptime builtin.target.os.tag.isDarwin()) {
+ try args.append("--login");
+ }
+
// Stores the list of intercepted command line flags that will be passed
// to our shell integration script: --norc --noprofile
// We always include at least "1" so the script can differentiate between
@@ -342,9 +347,12 @@ test "bash" {
const command = try setupBash(alloc, .{ .shell = "bash" }, ".", &env);
- try testing.expectEqual(2, command.?.direct.len);
+ try testing.expect(command.?.direct.len >= 2);
try testing.expectEqualStrings("bash", command.?.direct[0]);
try testing.expectEqualStrings("--posix", command.?.direct[1]);
+ if (comptime builtin.target.os.tag.isDarwin()) {
+ try testing.expectEqualStrings("--login", command.?.direct[2]);
+ }
try testing.expectEqualStrings("./shell-integration/bash/ghostty.bash", env.get("ENV").?);
try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_INJECT").?);
}
@@ -387,9 +395,12 @@ test "bash: inject flags" {
const command = try setupBash(alloc, .{ .shell = "bash --norc" }, ".", &env);
- try testing.expectEqual(2, command.?.direct.len);
+ try testing.expect(command.?.direct.len >= 2);
try testing.expectEqualStrings("bash", command.?.direct[0]);
try testing.expectEqualStrings("--posix", command.?.direct[1]);
+ if (comptime builtin.target.os.tag.isDarwin()) {
+ try testing.expectEqualStrings("--login", command.?.direct[2]);
+ }
try testing.expectEqualStrings("1 --norc", env.get("GHOSTTY_BASH_INJECT").?);
}
@@ -400,9 +411,12 @@ test "bash: inject flags" {
const command = try setupBash(alloc, .{ .shell = "bash --noprofile" }, ".", &env);
- try testing.expectEqual(2, command.?.direct.len);
+ try testing.expect(command.?.direct.len >= 2);
try testing.expectEqualStrings("bash", command.?.direct[0]);
try testing.expectEqualStrings("--posix", command.?.direct[1]);
+ if (comptime builtin.target.os.tag.isDarwin()) {
+ try testing.expectEqualStrings("--login", command.?.direct[2]);
+ }
try testing.expectEqualStrings("1 --noprofile", env.get("GHOSTTY_BASH_INJECT").?);
}
}
@@ -419,18 +433,24 @@ test "bash: rcfile" {
// bash --rcfile
{
const command = try setupBash(alloc, .{ .shell = "bash --rcfile profile.sh" }, ".", &env);
- try testing.expectEqual(2, command.?.direct.len);
+ try testing.expect(command.?.direct.len >= 2);
try testing.expectEqualStrings("bash", command.?.direct[0]);
try testing.expectEqualStrings("--posix", command.?.direct[1]);
+ if (comptime builtin.target.os.tag.isDarwin()) {
+ try testing.expectEqualStrings("--login", command.?.direct[2]);
+ }
try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?);
}
// bash --init-file
{
const command = try setupBash(alloc, .{ .shell = "bash --init-file profile.sh" }, ".", &env);
- try testing.expectEqual(2, command.?.direct.len);
+ try testing.expect(command.?.direct.len >= 2);
try testing.expectEqualStrings("bash", command.?.direct[0]);
try testing.expectEqualStrings("--posix", command.?.direct[1]);
+ if (comptime builtin.target.os.tag.isDarwin()) {
+ try testing.expectEqualStrings("--login", command.?.direct[2]);
+ }
try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?);
}
}
@@ -476,25 +496,35 @@ test "bash: additional arguments" {
// "-" argument separator
{
const command = try setupBash(alloc, .{ .shell = "bash - --arg file1 file2" }, ".", &env);
- try testing.expectEqual(6, command.?.direct.len);
+ try testing.expect(command.?.direct.len >= 6);
try testing.expectEqualStrings("bash", command.?.direct[0]);
try testing.expectEqualStrings("--posix", command.?.direct[1]);
- try testing.expectEqualStrings("-", command.?.direct[2]);
- try testing.expectEqualStrings("--arg", command.?.direct[3]);
- try testing.expectEqualStrings("file1", command.?.direct[4]);
- try testing.expectEqualStrings("file2", command.?.direct[5]);
+ if (comptime builtin.target.os.tag.isDarwin()) {
+ try testing.expectEqualStrings("--login", command.?.direct[2]);
+ }
+
+ const offset = if (comptime builtin.target.os.tag.isDarwin()) 3 else 2;
+ try testing.expectEqualStrings("-", command.?.direct[offset + 0]);
+ try testing.expectEqualStrings("--arg", command.?.direct[offset + 1]);
+ try testing.expectEqualStrings("file1", command.?.direct[offset + 2]);
+ try testing.expectEqualStrings("file2", command.?.direct[offset + 3]);
}
// "--" argument separator
{
const command = try setupBash(alloc, .{ .shell = "bash -- --arg file1 file2" }, ".", &env);
- try testing.expectEqual(6, command.?.direct.len);
+ try testing.expect(command.?.direct.len >= 6);
try testing.expectEqualStrings("bash", command.?.direct[0]);
try testing.expectEqualStrings("--posix", command.?.direct[1]);
- try testing.expectEqualStrings("--", command.?.direct[2]);
- try testing.expectEqualStrings("--arg", command.?.direct[3]);
- try testing.expectEqualStrings("file1", command.?.direct[4]);
- try testing.expectEqualStrings("file2", command.?.direct[5]);
+ if (comptime builtin.target.os.tag.isDarwin()) {
+ try testing.expectEqualStrings("--login", command.?.direct[2]);
+ }
+
+ const offset = if (comptime builtin.target.os.tag.isDarwin()) 3 else 2;
+ try testing.expectEqualStrings("--", command.?.direct[offset + 0]);
+ try testing.expectEqualStrings("--arg", command.?.direct[offset + 1]);
+ try testing.expectEqualStrings("file1", command.?.direct[offset + 2]);
+ try testing.expectEqualStrings("file2", command.?.direct[offset + 3]);
}
}