mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
macos: take over menu bar, separate close and close window
This commit is contained in:
@ -252,6 +252,7 @@ void ghostty_surface_mouse_button(ghostty_surface_t, ghostty_input_mouse_state_e
|
|||||||
void ghostty_surface_mouse_pos(ghostty_surface_t, double, double);
|
void ghostty_surface_mouse_pos(ghostty_surface_t, double, double);
|
||||||
void ghostty_surface_mouse_scroll(ghostty_surface_t, double, double);
|
void ghostty_surface_mouse_scroll(ghostty_surface_t, double, double);
|
||||||
void ghostty_surface_ime_point(ghostty_surface_t, double *, double *);
|
void ghostty_surface_ime_point(ghostty_surface_t, double *, double *);
|
||||||
|
void ghostty_surface_request_close(ghostty_surface_t);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
@ -82,6 +82,12 @@ extension Ghostty {
|
|||||||
ghostty_app_tick(app)
|
ghostty_app_tick(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Request that the given surface is closed. This will trigger the full normal surface close event
|
||||||
|
/// cycle which will call our close surface callback.
|
||||||
|
func requestClose(surface: ghostty_surface_t) {
|
||||||
|
ghostty_surface_request_close(surface)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Ghostty Callbacks
|
// MARK: Ghostty Callbacks
|
||||||
|
|
||||||
static func newSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_direction_e) {
|
static func newSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_direction_e) {
|
||||||
|
@ -55,6 +55,7 @@ extension Ghostty {
|
|||||||
Surface(view: surfaceView, hasFocus: hasFocus, size: geo.size)
|
Surface(view: surfaceView, hasFocus: hasFocus, size: geo.size)
|
||||||
.focused($surfaceFocus)
|
.focused($surfaceFocus)
|
||||||
.focusedValue(\.ghosttySurfaceTitle, surfaceView.title)
|
.focusedValue(\.ghosttySurfaceTitle, surfaceView.title)
|
||||||
|
.focusedValue(\.ghosttySurfaceView, surfaceView)
|
||||||
}
|
}
|
||||||
.ghosttySurfaceView(surfaceView)
|
.ghosttySurfaceView(surfaceView)
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,9 @@ struct GhosttyApp: App {
|
|||||||
@StateObject private var ghostty = Ghostty.AppState()
|
@StateObject private var ghostty = Ghostty.AppState()
|
||||||
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
||||||
|
|
||||||
|
/// The current focused Ghostty surface in this app
|
||||||
|
@FocusedValue(\.ghosttySurfaceView) private var focusedSurface
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
switch ghostty.readiness {
|
switch ghostty.readiness {
|
||||||
@ -27,6 +30,9 @@ struct GhosttyApp: App {
|
|||||||
}.commands {
|
}.commands {
|
||||||
CommandGroup(after: .newItem) {
|
CommandGroup(after: .newItem) {
|
||||||
Button("New Tab", action: newTab).keyboardShortcut("t", modifiers: [.command])
|
Button("New Tab", action: newTab).keyboardShortcut("t", modifiers: [.command])
|
||||||
|
Divider()
|
||||||
|
Button("Close", action: close).keyboardShortcut("w", modifiers: [.command])
|
||||||
|
Button("Close Window", action: closeWindow).keyboardShortcut("w", modifiers: [.command, .shift])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,13 +50,72 @@ struct GhosttyApp: App {
|
|||||||
currentWindow.addTabbedWindow(newWindow, ordered: .above)
|
currentWindow.addTabbedWindow(newWindow, ordered: .above)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func close() {
|
||||||
|
guard let surfaceView = focusedSurface else { return }
|
||||||
|
guard let surface = surfaceView.surface else { return }
|
||||||
|
ghostty.requestClose(surface: surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeWindow() {
|
||||||
|
guard let currentWindow = NSApp.keyWindow else { return }
|
||||||
|
currentWindow.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
// See CursedMenuManager for more information.
|
||||||
|
private var menuManager: CursedMenuManager?
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
UserDefaults.standard.register(defaults: [
|
UserDefaults.standard.register(defaults: [
|
||||||
// Disable this so that repeated key events make it through to our terminal views.
|
// Disable this so that repeated key events make it through to our terminal views.
|
||||||
"ApplePressAndHoldEnabled": false,
|
"ApplePressAndHoldEnabled": false,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// Create our menu manager to create some custom menu items that
|
||||||
|
// we can't create from SwiftUI.
|
||||||
|
menuManager = CursedMenuManager()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// SwiftUI as of macOS 13.x provides no way to manage the default menu items that are created
|
||||||
|
/// as part of a WindowGroup. This class is prefixed with "Cursed" because this is a truly cursed
|
||||||
|
/// solution to the problem and I think its quite brittle. As soon as SwiftUI supports a better option
|
||||||
|
/// we should conditionally compile for that when supported.
|
||||||
|
///
|
||||||
|
/// The way this works is by setting up KVO on various menu objects and reacting to it. For example,
|
||||||
|
/// when SwiftUI tries to add a "Close" menu, we intercept it and delete it. Nice try!
|
||||||
|
private class CursedMenuManager {
|
||||||
|
var mainToken: NSKeyValueObservation?
|
||||||
|
var fileToken: NSKeyValueObservation?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// If the whole menu changed we want to setup our new KVO
|
||||||
|
self.mainToken = NSApp.observe(\.mainMenu, options: .new) { app, change in
|
||||||
|
self.onNewMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial setup
|
||||||
|
onNewMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func onNewMenu() {
|
||||||
|
guard let menu = NSApp.mainMenu else { return }
|
||||||
|
guard let file = menu.item(withTitle: "File") else { return }
|
||||||
|
guard let submenu = file.submenu else { return }
|
||||||
|
fileToken = submenu.observe(\.items) { (_, _) in
|
||||||
|
let remove = ["Close", "Close All"]
|
||||||
|
|
||||||
|
// We look for the items in reverse since we're removing only the
|
||||||
|
// ones SwiftUI inserts which are at the end. We make replacements
|
||||||
|
// which we DON'T want deleted.
|
||||||
|
let items = submenu.items.reversed()
|
||||||
|
remove.forEach { title in
|
||||||
|
if let item = items.first(where: { $0.title.caseInsensitiveCompare(title) == .orderedSame }) {
|
||||||
|
submenu.removeItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -470,4 +470,10 @@ pub const CAPI = struct {
|
|||||||
x.* = pos.x;
|
x.* = pos.x;
|
||||||
y.* = pos.y;
|
y.* = pos.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Request that the surface become closed. This will go through the
|
||||||
|
/// normal trigger process that a close surface input binding would.
|
||||||
|
export fn ghostty_surface_request_close(ptr: *Surface) void {
|
||||||
|
ptr.closeSurface() catch {};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user