mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #1155 from mitchellh/macos-restore
macOS: restore windows, tabs, splits, working directories, etc. on restart
This commit is contained in:
@ -460,6 +460,7 @@ void ghostty_surface_split_resize(ghostty_surface_t, ghostty_split_resize_direct
|
|||||||
void ghostty_surface_split_equalize(ghostty_surface_t);
|
void ghostty_surface_split_equalize(ghostty_surface_t);
|
||||||
bool ghostty_surface_binding_action(ghostty_surface_t, const char *, uintptr_t);
|
bool ghostty_surface_binding_action(ghostty_surface_t, const char *, uintptr_t);
|
||||||
void ghostty_surface_complete_clipboard_request(ghostty_surface_t, const char *, void *, bool);
|
void ghostty_surface_complete_clipboard_request(ghostty_surface_t, const char *, void *, bool);
|
||||||
|
uintptr_t ghostty_surface_pwd(ghostty_surface_t, char *, uintptr_t);
|
||||||
|
|
||||||
ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
|
ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
|
||||||
void ghostty_inspector_free(ghostty_surface_t);
|
void ghostty_inspector_free(ghostty_surface_t);
|
||||||
|
@ -46,6 +46,8 @@
|
|||||||
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDB29B8009000646FDA /* SplitView.swift */; };
|
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDB29B8009000646FDA /* SplitView.swift */; };
|
||||||
A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */; };
|
A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */; };
|
||||||
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
|
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
|
||||||
|
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */; };
|
||||||
|
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3C2B37804400D21823 /* CodableBridge.swift */; };
|
||||||
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; };
|
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; };
|
||||||
A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; };
|
A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; };
|
||||||
A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; };
|
A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; };
|
||||||
@ -95,6 +97,8 @@
|
|||||||
A5CEAFDB29B8009000646FDA /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = "<group>"; };
|
A5CEAFDB29B8009000646FDA /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = "<group>"; };
|
||||||
A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Divider.swift; sourceTree = "<group>"; };
|
A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Divider.swift; sourceTree = "<group>"; };
|
||||||
A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
||||||
|
A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalRestorable.swift; sourceTree = "<group>"; };
|
||||||
|
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableBridge.swift; sourceTree = "<group>"; };
|
||||||
A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = "<group>"; };
|
A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = "<group>"; };
|
||||||
A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ClipboardConfirmation.xib; sourceTree = "<group>"; };
|
A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ClipboardConfirmation.xib; sourceTree = "<group>"; };
|
||||||
A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationController.swift; sourceTree = "<group>"; };
|
A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationController.swift; sourceTree = "<group>"; };
|
||||||
@ -151,6 +155,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A5CEAFFE29C2410700646FDA /* Backport.swift */,
|
A5CEAFFE29C2410700646FDA /* Backport.swift */,
|
||||||
|
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */,
|
||||||
8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */,
|
8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */,
|
||||||
A59630962AEE163600D64628 /* HostingWindow.swift */,
|
A59630962AEE163600D64628 /* HostingWindow.swift */,
|
||||||
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */,
|
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */,
|
||||||
@ -213,6 +218,7 @@
|
|||||||
A59630992AEE1C6400D64628 /* Terminal.xib */,
|
A59630992AEE1C6400D64628 /* Terminal.xib */,
|
||||||
A596309F2AEF6AEB00D64628 /* TerminalManager.swift */,
|
A596309F2AEF6AEB00D64628 /* TerminalManager.swift */,
|
||||||
A596309B2AEE1C9E00D64628 /* TerminalController.swift */,
|
A596309B2AEE1C9E00D64628 /* TerminalController.swift */,
|
||||||
|
A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */,
|
||||||
A596309D2AEE1D6C00D64628 /* TerminalView.swift */,
|
A596309D2AEE1D6C00D64628 /* TerminalView.swift */,
|
||||||
A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */,
|
A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */,
|
||||||
A535B9D9299C569B0017E2E4 /* ErrorView.swift */,
|
A535B9D9299C569B0017E2E4 /* ErrorView.swift */,
|
||||||
@ -366,6 +372,8 @@
|
|||||||
files = (
|
files = (
|
||||||
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
|
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
|
||||||
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
|
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
|
||||||
|
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
|
||||||
|
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
|
||||||
A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */,
|
A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */,
|
||||||
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */,
|
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */,
|
||||||
A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */,
|
A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */,
|
||||||
|
@ -58,7 +58,7 @@ class AppDelegate: NSObject,
|
|||||||
private var dockMenu: NSMenu = NSMenu()
|
private var dockMenu: NSMenu = NSMenu()
|
||||||
|
|
||||||
/// The ghostty global state. Only one per process.
|
/// The ghostty global state. Only one per process.
|
||||||
private let ghostty: Ghostty.AppState = Ghostty.AppState()
|
let ghostty: Ghostty.AppState = Ghostty.AppState()
|
||||||
|
|
||||||
/// Manages our terminal windows.
|
/// Manages our terminal windows.
|
||||||
let terminalManager: TerminalManager
|
let terminalManager: TerminalManager
|
||||||
@ -101,6 +101,9 @@ class AppDelegate: NSObject,
|
|||||||
menuCheckForUpdates?.target = updaterController
|
menuCheckForUpdates?.target = updaterController
|
||||||
menuCheckForUpdates?.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:))
|
menuCheckForUpdates?.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:))
|
||||||
|
|
||||||
|
// Initial config loading
|
||||||
|
configDidReload(ghostty)
|
||||||
|
|
||||||
// Let's launch our first window. We only do this if we have no other windows. It
|
// Let's launch our first window. We only do this if we have no other windows. It
|
||||||
// is possible to have other windows if we're opening a URL since `application(_:openFile:)`
|
// is possible to have other windows if we're opening a URL since `application(_:openFile:)`
|
||||||
// is called before this.
|
// is called before this.
|
||||||
@ -108,9 +111,6 @@ class AppDelegate: NSObject,
|
|||||||
terminalManager.newWindow()
|
terminalManager.newWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial config loading
|
|
||||||
configDidReload(ghostty)
|
|
||||||
|
|
||||||
// Register our service provider. This must happen after everything
|
// Register our service provider. This must happen after everything
|
||||||
// else is initialized.
|
// else is initialized.
|
||||||
NSApp.servicesProvider = ServiceProvider()
|
NSApp.servicesProvider = ServiceProvider()
|
||||||
@ -294,6 +294,21 @@ class AppDelegate: NSObject,
|
|||||||
return terminalManager.focusedSurface?.surface
|
return terminalManager.focusedSurface?.surface
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//MARK: - Restorable State
|
||||||
|
|
||||||
|
/// We support NSSecureCoding for restorable state. Required as of macOS Sonoma (14) but a good idea anyways.
|
||||||
|
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ app: NSApplication, willEncodeRestorableState coder: NSCoder) {
|
||||||
|
Self.logger.debug("application will save window state")
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ app: NSApplication, didDecodeRestorableState coder: NSCoder) {
|
||||||
|
Self.logger.debug("application will restore window state")
|
||||||
|
}
|
||||||
|
|
||||||
//MARK: - UNUserNotificationCenterDelegate
|
//MARK: - UNUserNotificationCenterDelegate
|
||||||
|
|
||||||
func userNotificationCenter(
|
func userNotificationCenter(
|
||||||
@ -318,6 +333,16 @@ class AppDelegate: NSObject,
|
|||||||
//MARK: - GhosttyAppStateDelegate
|
//MARK: - GhosttyAppStateDelegate
|
||||||
|
|
||||||
func configDidReload(_ state: Ghostty.AppState) {
|
func configDidReload(_ state: Ghostty.AppState) {
|
||||||
|
// Depending on the "window-save-state" setting we have to set the NSQuitAlwaysKeepsWindows
|
||||||
|
// configuration. This is the only way to carefully control whether macOS invokes the
|
||||||
|
// state restoration system.
|
||||||
|
switch (ghostty.windowSaveState) {
|
||||||
|
case "never": UserDefaults.standard.setValue(false, forKey: "NSQuitAlwaysKeepsWindows")
|
||||||
|
case "always": UserDefaults.standard.setValue(true, forKey: "NSQuitAlwaysKeepsWindows")
|
||||||
|
case "default": fallthrough
|
||||||
|
default: UserDefaults.standard.removeObject(forKey: "NSQuitAlwaysKeepsWindows")
|
||||||
|
}
|
||||||
|
|
||||||
// Config could change keybindings, so update everything that depends on that
|
// Config could change keybindings, so update everything that depends on that
|
||||||
syncMenuShortcuts()
|
syncMenuShortcuts()
|
||||||
terminalManager.relabelAllTabs()
|
terminalManager.relabelAllTabs()
|
||||||
|
@ -148,6 +148,11 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
override func windowDidLoad() {
|
override func windowDidLoad() {
|
||||||
guard let window = window else { return }
|
guard let window = window else { return }
|
||||||
|
|
||||||
|
// Setting all three of these is required for restoration to work.
|
||||||
|
window.isRestorable = true
|
||||||
|
window.restorationClass = TerminalWindowRestoration.self
|
||||||
|
window.identifier = .init(String(describing: TerminalWindowRestoration.self))
|
||||||
|
|
||||||
// If window decorations are disabled, remove our title
|
// If window decorations are disabled, remove our title
|
||||||
if (!ghostty.windowDecorations) { window.styleMask.remove(.titled) }
|
if (!ghostty.windowDecorations) { window.styleMask.remove(.titled) }
|
||||||
|
|
||||||
@ -250,6 +255,13 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
self.relabelTabs()
|
self.relabelTabs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called when the window will be encoded. We handle the data encoding here in the
|
||||||
|
// window controller.
|
||||||
|
func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) {
|
||||||
|
let data = TerminalRestorableState(from: self)
|
||||||
|
data.encode(with: state)
|
||||||
|
}
|
||||||
|
|
||||||
//MARK: - First Responder
|
//MARK: - First Responder
|
||||||
|
|
||||||
@IBAction func newWindow(_ sender: Any?) {
|
@IBAction func newWindow(_ sender: Any?) {
|
||||||
|
@ -139,7 +139,7 @@ class TerminalManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a window controller, adds it to our managed list, and returns it.
|
/// Creates a window controller, adds it to our managed list, and returns it.
|
||||||
private func createWindow(withBaseConfig base: Ghostty.SurfaceConfiguration?) -> TerminalController {
|
func createWindow(withBaseConfig base: Ghostty.SurfaceConfiguration?) -> TerminalController {
|
||||||
// Initialize our controller to load the window
|
// Initialize our controller to load the window
|
||||||
let c = TerminalController(ghostty, withBaseConfig: base)
|
let c = TerminalController(ghostty, withBaseConfig: base)
|
||||||
|
|
||||||
@ -162,7 +162,7 @@ class TerminalManager {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeWindow(_ controller: TerminalController) {
|
func removeWindow(_ controller: TerminalController) {
|
||||||
// Remove it from our managed set
|
// Remove it from our managed set
|
||||||
guard let idx = self.windows.firstIndex(where: { $0.controller == controller }) else { return }
|
guard let idx = self.windows.firstIndex(where: { $0.controller == controller }) else { return }
|
||||||
let w = self.windows[idx]
|
let w = self.windows[idx]
|
||||||
|
92
macos/Sources/Features/Terminal/TerminalRestorable.swift
Normal file
92
macos/Sources/Features/Terminal/TerminalRestorable.swift
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import Cocoa
|
||||||
|
|
||||||
|
/// The state stored for terminal window restoration.
|
||||||
|
class TerminalRestorableState: Codable {
|
||||||
|
static let selfKey = "state"
|
||||||
|
static let versionKey = "version"
|
||||||
|
static let version: Int = 1
|
||||||
|
|
||||||
|
let surfaceTree: Ghostty.SplitNode?
|
||||||
|
|
||||||
|
init(from controller: TerminalController) {
|
||||||
|
self.surfaceTree = controller.surfaceTree
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(coder aDecoder: NSCoder) {
|
||||||
|
// If the version doesn't match then we can't decode. In the future we can perform
|
||||||
|
// version upgrading or something but for now we only have one version so we
|
||||||
|
// don't bother.
|
||||||
|
guard aDecoder.decodeInteger(forKey: Self.versionKey) == Self.version else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let v = aDecoder.decodeObject(of: CodableBridge<Self>.self, forKey: Self.selfKey) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.surfaceTree = v.value.surfaceTree
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(with coder: NSCoder) {
|
||||||
|
coder.encode(Self.version, forKey: Self.versionKey)
|
||||||
|
coder.encode(CodableBridge(self), forKey: Self.selfKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TerminalRestoreError: Error {
|
||||||
|
case delegateInvalid
|
||||||
|
case identifierUnknown
|
||||||
|
case stateDecodeFailed
|
||||||
|
case windowDidNotLoad
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The NSWindowRestoration implementation that is called when a terminal window needs to be restored.
|
||||||
|
/// The encoding of a terminal window is handled elsewhere (usually NSWindowDelegate).
|
||||||
|
class TerminalWindowRestoration: NSObject, NSWindowRestoration {
|
||||||
|
static func restoreWindow(
|
||||||
|
withIdentifier identifier: NSUserInterfaceItemIdentifier,
|
||||||
|
state: NSCoder,
|
||||||
|
completionHandler: @escaping (NSWindow?, Error?) -> Void
|
||||||
|
) {
|
||||||
|
// Verify the identifier is what we expect
|
||||||
|
guard identifier == .init(String(describing: Self.self)) else {
|
||||||
|
completionHandler(nil, TerminalRestoreError.identifierUnknown)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The app delegate is definitely setup by now. If it isn't our AppDelegate
|
||||||
|
// then something is royally fucked up but protect against it anyhow.
|
||||||
|
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else {
|
||||||
|
completionHandler(nil, TerminalRestoreError.delegateInvalid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If our configuration is "never" then we never restore the state
|
||||||
|
// no matter what.
|
||||||
|
if (appDelegate.terminalManager.ghostty.windowSaveState == "never") {
|
||||||
|
completionHandler(nil, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the state. If we can't decode the state, then we can't restore.
|
||||||
|
guard let state = TerminalRestorableState(coder: state) else {
|
||||||
|
completionHandler(nil, TerminalRestoreError.stateDecodeFailed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// The window creation has to go through our terminalManager so that it
|
||||||
|
// can be found for events from libghostty. This uses the low-level
|
||||||
|
// createWindow so that AppKit can place the window wherever it should
|
||||||
|
// be.
|
||||||
|
let c = appDelegate.terminalManager.createWindow(withBaseConfig: nil)
|
||||||
|
guard let window = c.window else {
|
||||||
|
completionHandler(nil, TerminalRestoreError.windowDidNotLoad)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup our restored state on the controller
|
||||||
|
c.surfaceTree = state.surfaceTree
|
||||||
|
|
||||||
|
completionHandler(window, nil)
|
||||||
|
}
|
||||||
|
}
|
@ -58,7 +58,17 @@ extension Ghostty {
|
|||||||
var v = false;
|
var v = false;
|
||||||
let key = "quit-after-last-window-closed"
|
let key = "quit-after-last-window-closed"
|
||||||
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
||||||
return v;
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
/// window-save-state
|
||||||
|
var windowSaveState: String {
|
||||||
|
guard let config = self.config else { return "" }
|
||||||
|
var v: UnsafePointer<Int8>? = nil
|
||||||
|
let key = "window-save-state"
|
||||||
|
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
|
||||||
|
guard let ptr = v else { return "" }
|
||||||
|
return String(cString: ptr)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True if we need to confirm before quitting.
|
/// True if we need to confirm before quitting.
|
||||||
|
@ -11,10 +11,33 @@ extension Ghostty {
|
|||||||
/// "container" which has a recursive top/left SplitNode and bottom/right SplitNode. These
|
/// "container" which has a recursive top/left SplitNode and bottom/right SplitNode. These
|
||||||
/// values can further be split infinitely.
|
/// values can further be split infinitely.
|
||||||
///
|
///
|
||||||
enum SplitNode: Equatable, Hashable {
|
enum SplitNode: Equatable, Hashable, Codable {
|
||||||
case leaf(Leaf)
|
case leaf(Leaf)
|
||||||
case split(Container)
|
case split(Container)
|
||||||
|
|
||||||
|
/// The parent of this node.
|
||||||
|
var parent: Container? {
|
||||||
|
get {
|
||||||
|
switch (self) {
|
||||||
|
case .leaf(let leaf):
|
||||||
|
return leaf.parent
|
||||||
|
|
||||||
|
case .split(let container):
|
||||||
|
return container.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
switch (self) {
|
||||||
|
case .leaf(let leaf):
|
||||||
|
leaf.parent = newValue
|
||||||
|
|
||||||
|
case .split(let container):
|
||||||
|
container.parent = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the view that would prefer receiving focus in this tree. This is always the
|
/// Returns the view that would prefer receiving focus in this tree. This is always the
|
||||||
/// top-left-most view. This is used when creating a split or closing a split to find the
|
/// top-left-most view. This is used when creating a split or closing a split to find the
|
||||||
/// next view to send focus to.
|
/// next view to send focus to.
|
||||||
@ -91,7 +114,7 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Leaf: ObservableObject, Equatable, Hashable {
|
class Leaf: ObservableObject, Equatable, Hashable, Codable {
|
||||||
let app: ghostty_app_t
|
let app: ghostty_app_t
|
||||||
@Published var surface: SurfaceView
|
@Published var surface: SurfaceView
|
||||||
|
|
||||||
@ -115,9 +138,35 @@ extension Ghostty {
|
|||||||
static func == (lhs: Leaf, rhs: Leaf) -> Bool {
|
static func == (lhs: Leaf, rhs: Leaf) -> Bool {
|
||||||
return lhs.app == rhs.app && lhs.surface === rhs.surface
|
return lhs.app == rhs.app && lhs.surface === rhs.surface
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Codable
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case pwd
|
||||||
|
}
|
||||||
|
|
||||||
|
required convenience init(from decoder: Decoder) throws {
|
||||||
|
// Decoding uses the global Ghostty app
|
||||||
|
guard let del = NSApplication.shared.delegate,
|
||||||
|
let appDel = del as? AppDelegate,
|
||||||
|
let app = appDel.ghostty.app else {
|
||||||
|
throw TerminalRestoreError.delegateInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
var config = SurfaceConfiguration()
|
||||||
|
config.workingDirectory = try container.decode(String?.self, forKey: .pwd)
|
||||||
|
|
||||||
|
self.init(app, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(surface.pwd, forKey: .pwd)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Container: ObservableObject, Equatable, Hashable {
|
class Container: ObservableObject, Equatable, Hashable, Codable {
|
||||||
let app: ghostty_app_t
|
let app: ghostty_app_t
|
||||||
let direction: SplitViewDirection
|
let direction: SplitViewDirection
|
||||||
|
|
||||||
@ -213,6 +262,43 @@ extension Ghostty {
|
|||||||
lhs.topLeft == rhs.topLeft &&
|
lhs.topLeft == rhs.topLeft &&
|
||||||
lhs.bottomRight == rhs.bottomRight
|
lhs.bottomRight == rhs.bottomRight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Codable
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case direction
|
||||||
|
case split
|
||||||
|
case topLeft
|
||||||
|
case bottomRight
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws {
|
||||||
|
// Decoding uses the global Ghostty app
|
||||||
|
guard let del = NSApplication.shared.delegate,
|
||||||
|
let appDel = del as? AppDelegate,
|
||||||
|
let app = appDel.ghostty.app else {
|
||||||
|
throw TerminalRestoreError.delegateInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.app = app
|
||||||
|
self.direction = try container.decode(SplitViewDirection.self, forKey: .direction)
|
||||||
|
self.split = try container.decode(CGFloat.self, forKey: .split)
|
||||||
|
self.topLeft = try container.decode(SplitNode.self, forKey: .topLeft)
|
||||||
|
self.bottomRight = try container.decode(SplitNode.self, forKey: .bottomRight)
|
||||||
|
|
||||||
|
// Fix up the parent references
|
||||||
|
self.topLeft.parent = self
|
||||||
|
self.bottomRight.parent = self
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(direction, forKey: .direction)
|
||||||
|
try container.encode(split, forKey: .split)
|
||||||
|
try container.encode(topLeft, forKey: .topLeft)
|
||||||
|
try container.encode(bottomRight, forKey: .bottomRight)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This keeps track of the "neighbors" of a split: the immediately above/below/left/right
|
/// This keeps track of the "neighbors" of a split: the immediately above/below/left/right
|
||||||
|
@ -283,6 +283,17 @@ extension Ghostty {
|
|||||||
return ghostty_surface_needs_confirm_quit(surface)
|
return ghostty_surface_needs_confirm_quit(surface)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the pwd of the surface if it has one.
|
||||||
|
var pwd: String? {
|
||||||
|
guard let surface = self.surface else { return nil }
|
||||||
|
let v = String(unsafeUninitializedCapacity: 1024) {
|
||||||
|
Int(ghostty_surface_pwd(surface, $0.baseAddress, UInt($0.count)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v.count == 0) { return nil }
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
// Returns the inspector instance for this surface, or nil if the
|
// Returns the inspector instance for this surface, or nil if the
|
||||||
// surface has been closed.
|
// surface has been closed.
|
||||||
var inspector: ghostty_inspector_t? {
|
var inspector: ghostty_inspector_t? {
|
||||||
|
22
macos/Sources/Helpers/CodableBridge.swift
Normal file
22
macos/Sources/Helpers/CodableBridge.swift
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import Cocoa
|
||||||
|
|
||||||
|
/// A wrapper that allows a Swift Codable to implement NSSecureCoding.
|
||||||
|
class CodableBridge<Wrapped: Codable>: NSObject, NSSecureCoding {
|
||||||
|
let value: Wrapped
|
||||||
|
init(_ value: Wrapped) { self.value = value }
|
||||||
|
|
||||||
|
static var supportsSecureCoding: Bool { return true }
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
guard let data = aDecoder.decodeObject(of: NSData.self, forKey: "data") as? Data else { return nil }
|
||||||
|
guard let archiver = try? NSKeyedUnarchiver(forReadingFrom: data) else { return nil }
|
||||||
|
guard let value = archiver.decodeDecodable(Wrapped.self, forKey: "value") else { return nil }
|
||||||
|
self.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(with aCoder: NSCoder) {
|
||||||
|
let archiver = NSKeyedArchiver(requiringSecureCoding: true)
|
||||||
|
try? archiver.encodeEncodable(value, forKey: "value")
|
||||||
|
aCoder.encode(archiver.encodedData, forKey: "data")
|
||||||
|
}
|
||||||
|
}
|
@ -163,6 +163,6 @@ struct SplitView<L: View, R: View>: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SplitViewDirection {
|
enum SplitViewDirection: Codable {
|
||||||
case horizontal, vertical
|
case horizontal, vertical
|
||||||
}
|
}
|
||||||
|
@ -1355,6 +1355,25 @@ pub const CAPI = struct {
|
|||||||
return surface.core_surface.needsConfirmQuit();
|
return surface.core_surface.needsConfirmQuit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Copies the surface working directory into the provided buffer and
|
||||||
|
/// returns the copied size. If the buffer is too small, there is no pwd,
|
||||||
|
/// or there is an error, then 0 is returned.
|
||||||
|
export fn ghostty_surface_pwd(surface: *Surface, buf: [*]u8, cap: usize) usize {
|
||||||
|
const pwd_ = surface.core_surface.pwd(global.alloc) catch |err| {
|
||||||
|
log.warn("error getting pwd err={}", .{err});
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
const pwd = pwd_ orelse return 0;
|
||||||
|
defer global.alloc.free(pwd);
|
||||||
|
|
||||||
|
// If the buffer is too small, return no pwd.
|
||||||
|
if (pwd.len > cap) return 0;
|
||||||
|
|
||||||
|
// Copy into the buffer and return the length
|
||||||
|
@memcpy(buf[0..pwd.len], pwd);
|
||||||
|
return pwd.len;
|
||||||
|
}
|
||||||
|
|
||||||
/// Tell the surface that it needs to schedule a render
|
/// Tell the surface that it needs to schedule a render
|
||||||
export fn ghostty_surface_refresh(surface: *Surface) void {
|
export fn ghostty_surface_refresh(surface: *Surface) void {
|
||||||
surface.refresh();
|
surface.refresh();
|
||||||
|
@ -563,6 +563,36 @@ keybind: Keybinds = .{},
|
|||||||
@"window-height": u32 = 0,
|
@"window-height": u32 = 0,
|
||||||
@"window-width": u32 = 0,
|
@"window-width": u32 = 0,
|
||||||
|
|
||||||
|
/// Whether to enable saving and restoring window state. Window state
|
||||||
|
/// includes their position, size, tabs, splits, etc. Some window state
|
||||||
|
/// requires shell integration, such as preserving working directories.
|
||||||
|
/// See shell-integration for more information.
|
||||||
|
///
|
||||||
|
/// There are three valid values for this configuration:
|
||||||
|
/// - "default" will use the default system behavior. On macOS, this
|
||||||
|
/// will only save state if the application is forcibly terminated
|
||||||
|
/// or if it is configured systemwide via Settings.app.
|
||||||
|
/// - "never" will never save window state.
|
||||||
|
/// - "always" will always save window state whenever Ghostty is exited.
|
||||||
|
///
|
||||||
|
/// If you change this value to "never" while Ghostty is not running,
|
||||||
|
/// the next Ghostty launch will NOT restore the window state.
|
||||||
|
///
|
||||||
|
/// If you change this value to "default" while Ghostty is not running
|
||||||
|
/// and the previous exit saved state, the next Ghostty launch will
|
||||||
|
/// still restore the window state. This is because Ghostty cannot know
|
||||||
|
/// if the previous exit was due to a forced save or not (macOS doesn't
|
||||||
|
/// provide this information).
|
||||||
|
///
|
||||||
|
/// If you change this value so that window state is saved while Ghostty
|
||||||
|
/// is not running, the previous window state will not be restored because
|
||||||
|
/// Ghostty only saves state on exit if this is enabled.
|
||||||
|
///
|
||||||
|
/// The default value is "default".
|
||||||
|
///
|
||||||
|
/// This is currently only supported on macOS. This has no effect on Linux.
|
||||||
|
@"window-save-state": WindowSaveState = .default,
|
||||||
|
|
||||||
/// Resize the window in discrete increments of the focused surface's
|
/// Resize the window in discrete increments of the focused surface's
|
||||||
/// cell size. If this is disabled, surfaces are resized in pixel increments.
|
/// cell size. If this is disabled, surfaces are resized in pixel increments.
|
||||||
/// Currently only supported on macOS.
|
/// Currently only supported on macOS.
|
||||||
@ -2744,3 +2774,10 @@ pub const ClipboardAccess = enum {
|
|||||||
deny,
|
deny,
|
||||||
ask,
|
ask,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// See window-save-state
|
||||||
|
pub const WindowSaveState = enum {
|
||||||
|
default,
|
||||||
|
never,
|
||||||
|
always,
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user