mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge 27b59f7c2d1730e1ce4908ed76c0df282c78617b into ce8bfe45eddd7976da0bda2572484c8c72a9f162
This commit is contained in:
@ -107,6 +107,10 @@
|
|||||||
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F26EA62B738B9900404083 /* NSView+Extension.swift */; };
|
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F26EA62B738B9900404083 /* NSView+Extension.swift */; };
|
||||||
C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = C1F26EE82B76CBFC00404083 /* VibrantLayer.m */; };
|
C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = C1F26EE82B76CBFC00404083 /* VibrantLayer.m */; };
|
||||||
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */; };
|
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */; };
|
||||||
|
CF41FAD12D26AADB004A0BF7 /* QuickTerminalTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF41FAD02D26AADB004A0BF7 /* QuickTerminalTab.swift */; };
|
||||||
|
CF41FAD32D26AB35004A0BF7 /* QuickTerminalTabManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF41FAD22D26AB35004A0BF7 /* QuickTerminalTabManager.swift */; };
|
||||||
|
CF41FAD62D26ABC9004A0BF7 /* QuickTerminalTabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF41FAD52D26ABC9004A0BF7 /* QuickTerminalTabBarView.swift */; };
|
||||||
|
CF41FAD82D26ABF6004A0BF7 /* QuickTerminalTabItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF41FAD72D26ABF6004A0BF7 /* QuickTerminalTabItemView.swift */; };
|
||||||
FC5218FA2D10FFCE004C93E0 /* zsh in Resources */ = {isa = PBXBuildFile; fileRef = FC5218F92D10FFC7004C93E0 /* zsh */; };
|
FC5218FA2D10FFCE004C93E0 /* zsh in Resources */ = {isa = PBXBuildFile; fileRef = FC5218F92D10FFC7004C93E0 /* zsh */; };
|
||||||
FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */ = {isa = PBXBuildFile; fileRef = FC9ABA9B2D0F538D0020D4C8 /* bash-completion */; };
|
FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */ = {isa = PBXBuildFile; fileRef = FC9ABA9B2D0F538D0020D4C8 /* bash-completion */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
@ -208,6 +212,10 @@
|
|||||||
C1F26EE82B76CBFC00404083 /* VibrantLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VibrantLayer.m; sourceTree = "<group>"; };
|
C1F26EE82B76CBFC00404083 /* VibrantLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VibrantLayer.m; sourceTree = "<group>"; };
|
||||||
C1F26EEA2B76CC2400404083 /* ghostty-bridging-header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ghostty-bridging-header.h"; sourceTree = "<group>"; };
|
C1F26EEA2B76CC2400404083 /* ghostty-bridging-header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ghostty-bridging-header.h"; sourceTree = "<group>"; };
|
||||||
CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalSpaceBehavior.swift; sourceTree = "<group>"; };
|
CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalSpaceBehavior.swift; sourceTree = "<group>"; };
|
||||||
|
CF41FAD02D26AADB004A0BF7 /* QuickTerminalTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalTab.swift; sourceTree = "<group>"; };
|
||||||
|
CF41FAD22D26AB35004A0BF7 /* QuickTerminalTabManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalTabManager.swift; sourceTree = "<group>"; };
|
||||||
|
CF41FAD52D26ABC9004A0BF7 /* QuickTerminalTabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalTabBarView.swift; sourceTree = "<group>"; };
|
||||||
|
CF41FAD72D26ABF6004A0BF7 /* QuickTerminalTabItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalTabItemView.swift; sourceTree = "<group>"; };
|
||||||
FC5218F92D10FFC7004C93E0 /* zsh */ = {isa = PBXFileReference; lastKnownFileType = folder; name = zsh; path = "../zig-out/share/zsh"; sourceTree = "<group>"; };
|
FC5218F92D10FFC7004C93E0 /* zsh */ = {isa = PBXFileReference; lastKnownFileType = folder; name = zsh; path = "../zig-out/share/zsh"; sourceTree = "<group>"; };
|
||||||
FC9ABA9B2D0F538D0020D4C8 /* bash-completion */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "bash-completion"; path = "../zig-out/share/bash-completion"; sourceTree = "<group>"; };
|
FC9ABA9B2D0F538D0020D4C8 /* bash-completion */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "bash-completion"; path = "../zig-out/share/bash-completion"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
@ -460,6 +468,7 @@
|
|||||||
A5CBD05A2CA0C5910017A1AE /* QuickTerminal */ = {
|
A5CBD05A2CA0C5910017A1AE /* QuickTerminal */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
CF41FAD42D26AB7F004A0BF7 /* Tab */,
|
||||||
A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */,
|
A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */,
|
||||||
A5CBD05D2CA0C5E70017A1AE /* QuickTerminalController.swift */,
|
A5CBD05D2CA0C5E70017A1AE /* QuickTerminalController.swift */,
|
||||||
CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */,
|
CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */,
|
||||||
@ -506,6 +515,17 @@
|
|||||||
path = ClipboardConfirmation;
|
path = ClipboardConfirmation;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
CF41FAD42D26AB7F004A0BF7 /* Tab */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
CF41FAD02D26AADB004A0BF7 /* QuickTerminalTab.swift */,
|
||||||
|
CF41FAD52D26ABC9004A0BF7 /* QuickTerminalTabBarView.swift */,
|
||||||
|
CF41FAD72D26ABF6004A0BF7 /* QuickTerminalTabItemView.swift */,
|
||||||
|
CF41FAD22D26AB35004A0BF7 /* QuickTerminalTabManager.swift */,
|
||||||
|
);
|
||||||
|
path = Tab;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@ -630,6 +650,7 @@
|
|||||||
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
|
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
|
||||||
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
||||||
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
|
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
|
||||||
|
CF41FAD32D26AB35004A0BF7 /* QuickTerminalTabManager.swift in Sources */,
|
||||||
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
|
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
|
||||||
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
|
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
|
||||||
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */,
|
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */,
|
||||||
@ -650,13 +671,16 @@
|
|||||||
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
|
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
|
||||||
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */,
|
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */,
|
||||||
C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */,
|
C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */,
|
||||||
|
CF41FAD12D26AADB004A0BF7 /* QuickTerminalTab.swift in Sources */,
|
||||||
A59630972AEE163600D64628 /* HostingWindow.swift in Sources */,
|
A59630972AEE163600D64628 /* HostingWindow.swift in Sources */,
|
||||||
A59630A02AEF6AEB00D64628 /* TerminalManager.swift in Sources */,
|
A59630A02AEF6AEB00D64628 /* TerminalManager.swift in Sources */,
|
||||||
A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */,
|
A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */,
|
||||||
A5CBD06B2CA322430017A1AE /* GlobalEventTap.swift in Sources */,
|
A5CBD06B2CA322430017A1AE /* GlobalEventTap.swift in Sources */,
|
||||||
AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */,
|
AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */,
|
||||||
|
CF41FAD62D26ABC9004A0BF7 /* QuickTerminalTabBarView.swift in Sources */,
|
||||||
A52FFF5D2CAB4D08000C6A5B /* NSScreen+Extension.swift in Sources */,
|
A52FFF5D2CAB4D08000C6A5B /* NSScreen+Extension.swift in Sources */,
|
||||||
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */,
|
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */,
|
||||||
|
CF41FAD82D26ABF6004A0BF7 /* QuickTerminalTabItemView.swift in Sources */,
|
||||||
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */,
|
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */,
|
||||||
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */,
|
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */,
|
||||||
A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */,
|
A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */,
|
||||||
|
@ -33,10 +33,17 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||||
private var derivedConfig: DerivedConfig
|
private var derivedConfig: DerivedConfig
|
||||||
|
|
||||||
init(_ ghostty: Ghostty.App,
|
// The tab manager for the quick terminal
|
||||||
position: QuickTerminalPosition = .top,
|
private lazy var tabManager: QuickTerminalTabManager = {
|
||||||
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
let manager = QuickTerminalTabManager(controller: self)
|
||||||
surfaceTree tree: Ghostty.SplitNode? = nil
|
return manager
|
||||||
|
}()
|
||||||
|
|
||||||
|
init(
|
||||||
|
_ ghostty: Ghostty.App,
|
||||||
|
position: QuickTerminalPosition = .top,
|
||||||
|
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||||
|
surfaceTree tree: Ghostty.SplitNode? = nil
|
||||||
) {
|
) {
|
||||||
self.position = position
|
self.position = position
|
||||||
self.derivedConfig = DerivedConfig(ghostty.config)
|
self.derivedConfig = DerivedConfig(ghostty.config)
|
||||||
@ -59,6 +66,21 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
selector: #selector(ghosttyConfigDidChange(_:)),
|
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||||
name: .ghosttyConfigDidChange,
|
name: .ghosttyConfigDidChange,
|
||||||
object: nil)
|
object: nil)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(onNewTab),
|
||||||
|
name: Ghostty.Notification.ghosttyNewTab,
|
||||||
|
object: nil)
|
||||||
|
center.addObserver(
|
||||||
|
tabManager,
|
||||||
|
selector: #selector(tabManager.onMoveTab(_:)),
|
||||||
|
name: .ghosttyMoveTab,
|
||||||
|
object: nil)
|
||||||
|
center.addObserver(
|
||||||
|
tabManager,
|
||||||
|
selector: #selector(tabManager.onGoToTab(_:)),
|
||||||
|
name: Ghostty.Notification.ghosttyGotoTab,
|
||||||
|
object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -94,15 +116,34 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
// Setup our initial size based on our configured position
|
// Setup our initial size based on our configured position
|
||||||
position.setLoaded(window)
|
position.setLoaded(window)
|
||||||
|
|
||||||
// Setup our content
|
DispatchQueue.main.async {
|
||||||
window.contentView = NSHostingView(rootView: TerminalView(
|
self.setupMainView()
|
||||||
ghostty: self.ghostty,
|
self.animateIn()
|
||||||
viewModel: self,
|
}
|
||||||
delegate: self
|
}
|
||||||
))
|
|
||||||
|
|
||||||
// Animate the window in
|
private func setupMainView() {
|
||||||
animateIn()
|
guard let window = self.window else { return }
|
||||||
|
|
||||||
|
let leaf: Ghostty.SplitNode.Leaf = .init(ghostty.app!, baseConfig: nil)
|
||||||
|
let surface: Ghostty.SplitNode = .leaf(leaf)
|
||||||
|
let initialTab = QuickTerminalTab(surface: surface)
|
||||||
|
initialTab.isActive = true
|
||||||
|
tabManager.tabs.append(initialTab)
|
||||||
|
tabManager.currentTab = initialTab
|
||||||
|
surfaceTree = surface
|
||||||
|
focusedSurface = leaf.surface
|
||||||
|
|
||||||
|
let mainContent = VStack(spacing: 0) {
|
||||||
|
QuickTerminalTabBarView(tabManager: tabManager)
|
||||||
|
TerminalView(
|
||||||
|
ghostty: ghostty,
|
||||||
|
viewModel: self,
|
||||||
|
delegate: self
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.contentView = NSHostingView(rootView: mainContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: NSWindowDelegate
|
// MARK: NSWindowDelegate
|
||||||
@ -179,16 +220,22 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
override func surfaceTreeDidChange(from: Ghostty.SplitNode?, to: Ghostty.SplitNode?) {
|
override func surfaceTreeDidChange(from: Ghostty.SplitNode?, to: Ghostty.SplitNode?) {
|
||||||
super.surfaceTreeDidChange(from: from, to: to)
|
super.surfaceTreeDidChange(from: from, to: to)
|
||||||
|
|
||||||
// If our surface tree is nil then we animate the window out.
|
// If we have a tab with surfaces removed from surfaceTree, we need to remove them from the tab manager by calling closeTab
|
||||||
if (to == nil) {
|
if to == nil {
|
||||||
animateOut()
|
tabManager.tabs
|
||||||
|
.filter { tab in
|
||||||
|
tab.surface.contains { $0.surface.surface == nil }
|
||||||
|
}
|
||||||
|
.forEach { tab in
|
||||||
|
tabManager.closeTab(tab)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Methods
|
// MARK: Methods
|
||||||
|
|
||||||
func toggle() {
|
func toggle() {
|
||||||
if (visible) {
|
if visible {
|
||||||
animateOut()
|
animateOut()
|
||||||
} else {
|
} else {
|
||||||
animateIn()
|
animateIn()
|
||||||
@ -212,7 +259,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
// we want to store it so we can restore state later.
|
// we want to store it so we can restore state later.
|
||||||
if !NSApp.isActive {
|
if !NSApp.isActive {
|
||||||
if let previousApp = NSWorkspace.shared.frontmostApplication,
|
if let previousApp = NSWorkspace.shared.frontmostApplication,
|
||||||
previousApp.bundleIdentifier != Bundle.main.bundleIdentifier
|
previousApp.bundleIdentifier != Bundle.main.bundleIdentifier
|
||||||
{
|
{
|
||||||
self.previousApp = previousApp
|
self.previousApp = previousApp
|
||||||
}
|
}
|
||||||
@ -227,7 +274,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
// If our surface tree is nil then we initialize a new terminal. The surface
|
// If our surface tree is nil then we initialize a new terminal. The surface
|
||||||
// tree can be nil if for example we run "eixt" in the terminal and force
|
// tree can be nil if for example we run "eixt" in the terminal and force
|
||||||
// animate out.
|
// animate out.
|
||||||
if (surfaceTree == nil) {
|
if surfaceTree == nil {
|
||||||
let leaf: Ghostty.SplitNode.Leaf = .init(ghostty.app!, baseConfig: nil)
|
let leaf: Ghostty.SplitNode.Leaf = .init(ghostty.app!, baseConfig: nil)
|
||||||
surfaceTree = .leaf(leaf)
|
surfaceTree = .leaf(leaf)
|
||||||
focusedSurface = leaf.surface
|
focusedSurface = leaf.surface
|
||||||
@ -301,35 +348,35 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
// things like IME dropdowns to appear properly.
|
// things like IME dropdowns to appear properly.
|
||||||
window.level = .floating
|
window.level = .floating
|
||||||
|
|
||||||
// Now that the window is visible, sync our appearance. This function
|
// Now that the window is visible, sync our appearance. This function
|
||||||
// requires the window is visible.
|
// requires the window is visible.
|
||||||
self.syncAppearance()
|
self.syncAppearance()
|
||||||
|
|
||||||
// Once our animation is done, we must grab focus since we can't grab
|
// Once our animation is done, we must grab focus since we can't grab
|
||||||
// focus of a non-visible window.
|
// focus of a non-visible window.
|
||||||
self.makeWindowKey(window)
|
self.makeWindowKey(window)
|
||||||
|
|
||||||
// If our application is not active, then we grab focus. Its important
|
// If our application is not active, then we grab focus. Its important
|
||||||
// we do this AFTER our window is animated in and focused because
|
// we do this AFTER our window is animated in and focused because
|
||||||
// otherwise macOS will bring forward another window.
|
// otherwise macOS will bring forward another window.
|
||||||
if !NSApp.isActive {
|
if !NSApp.isActive {
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
|
||||||
// This works around a really funky bug where if the terminal is
|
// This works around a really funky bug where if the terminal is
|
||||||
// shown on a screen that has no other Ghostty windows, it takes
|
// shown on a screen that has no other Ghostty windows, it takes
|
||||||
// a few (variable) event loop ticks until we can actually focus it.
|
// a few (variable) event loop ticks until we can actually focus it.
|
||||||
// https://github.com/ghostty-org/ghostty/issues/2409
|
// https://github.com/ghostty-org/ghostty/issues/2409
|
||||||
//
|
//
|
||||||
// We wait one event loop tick to try it because under the happy
|
// We wait one event loop tick to try it because under the happy
|
||||||
// path (we have windows on this screen) it takes one event loop
|
// path (we have windows on this screen) it takes one event loop
|
||||||
// tick for window.isKeyWindow to return true.
|
// tick for window.isKeyWindow to return true.
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
guard !window.isKeyWindow else { return }
|
guard !window.isKeyWindow else { return }
|
||||||
self.makeWindowKey(window, retries: 10)
|
self.makeWindowKey(window, retries: 10)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt to make a window key, supporting retries if necessary. The retries will be attempted
|
/// Attempt to make a window key, supporting retries if necessary. The retries will be attempted
|
||||||
@ -422,7 +469,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
guard window.isVisible else { return }
|
guard window.isVisible else { return }
|
||||||
|
|
||||||
// If we have window transparency then set it transparent. Otherwise set it opaque.
|
// If we have window transparency then set it transparent. Otherwise set it opaque.
|
||||||
if (self.derivedConfig.backgroundOpacity < 1) {
|
if self.derivedConfig.backgroundOpacity < 1 {
|
||||||
window.isOpaque = false
|
window.isOpaque = false
|
||||||
|
|
||||||
// This is weird, but we don't use ".clear" because this creates a look that
|
// This is weird, but we don't use ".clear" because this creates a look that
|
||||||
@ -437,23 +484,21 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateSurfaceTree(to newTree: Ghostty.SplitNode) {
|
||||||
|
self.surfaceTree = newTree
|
||||||
|
if case let .leaf(leaf) = newTree {
|
||||||
|
self.focusedSurface = leaf.surface
|
||||||
|
guard let window = self.window, self.focusedSurface?.window == window else {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(25)) {
|
||||||
|
self.updateSurfaceTree(to: newTree)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
makeWindowKey(window, retries: 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: First Responder
|
// MARK: First Responder
|
||||||
|
|
||||||
@IBAction override func closeWindow(_ sender: Any) {
|
|
||||||
// Instead of closing the window, we animate it out.
|
|
||||||
animateOut()
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func newTab(_ sender: Any?) {
|
|
||||||
guard let window else { return }
|
|
||||||
let alert = NSAlert()
|
|
||||||
alert.messageText = "Cannot Create New Tab"
|
|
||||||
alert.informativeText = "Tabs aren't supported in the Quick Terminal."
|
|
||||||
alert.addButton(withTitle: "OK")
|
|
||||||
alert.alertStyle = .warning
|
|
||||||
alert.beginSheetModal(for: window)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func toggleGhosttyFullScreen(_ sender: Any) {
|
@IBAction func toggleGhosttyFullScreen(_ sender: Any) {
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
ghostty.toggleFullscreen(surface: surface)
|
ghostty.toggleFullscreen(surface: surface)
|
||||||
@ -482,9 +527,11 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
guard notification.object == nil else { return }
|
guard notification.object == nil else { return }
|
||||||
|
|
||||||
// Get our managed configuration object out
|
// Get our managed configuration object out
|
||||||
guard let config = notification.userInfo?[
|
guard
|
||||||
Notification.Name.GhosttyConfigChangeKey
|
let config = notification.userInfo?[
|
||||||
] as? Ghostty.Config else { return }
|
Notification.Name.GhosttyConfigChangeKey
|
||||||
|
] as? Ghostty.Config
|
||||||
|
else { return }
|
||||||
|
|
||||||
// Update our derived config
|
// Update our derived config
|
||||||
self.derivedConfig = DerivedConfig(config)
|
self.derivedConfig = DerivedConfig(config)
|
||||||
@ -492,6 +539,16 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
syncAppearance()
|
syncAppearance()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func onNewTab(notification: SwiftUI.Notification) {
|
||||||
|
guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return }
|
||||||
|
guard let window = surfaceView.window else { return }
|
||||||
|
|
||||||
|
// return if window is not in our managed windows
|
||||||
|
guard window == self.window else { return }
|
||||||
|
|
||||||
|
tabManager.newTab()
|
||||||
|
}
|
||||||
|
|
||||||
private struct DerivedConfig {
|
private struct DerivedConfig {
|
||||||
let quickTerminalScreen: QuickTerminalScreen
|
let quickTerminalScreen: QuickTerminalScreen
|
||||||
let quickTerminalAnimationDuration: Double
|
let quickTerminalAnimationDuration: Double
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
import Combine
|
||||||
|
|
||||||
|
class QuickTerminalTab: ObservableObject, Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
var surface: Ghostty.SplitNode
|
||||||
|
@Published var title: String
|
||||||
|
@Published var isActive: Bool = false
|
||||||
|
|
||||||
|
private var cancellable: AnyCancellable?
|
||||||
|
|
||||||
|
init(surface: Ghostty.SplitNode, title: String = "Terminal") {
|
||||||
|
self.surface = surface
|
||||||
|
self.title = surface.first { $0.surface.focused }?.surface.pwd ?? "Terminal"
|
||||||
|
|
||||||
|
let targetSurface = surface.first { $0.surface.focused }?.surface ?? surface.preferredFocus()
|
||||||
|
self.cancellable = targetSurface.$title
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] newTitle in
|
||||||
|
self?.title = newTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
cancellable?.cancel()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct QuickTerminalTabBarView: View {
|
||||||
|
@ObservedObject var tabManager: QuickTerminalTabManager
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
ForEach(tabManager.tabs) { tab in
|
||||||
|
QuickTerminalTabItemView(
|
||||||
|
tab: tab,
|
||||||
|
isHighlighted: tab.isActive,
|
||||||
|
onSelect: { tabManager.selectTab(tab) },
|
||||||
|
onClose: { tabManager.closeTab(tab) }
|
||||||
|
)
|
||||||
|
.contextMenu {
|
||||||
|
Button("Close Tab") {
|
||||||
|
tabManager.closeTab(tab)
|
||||||
|
}
|
||||||
|
Button("Close Other Tabs") {
|
||||||
|
tabManager.tabs.forEach { otherTab in
|
||||||
|
if otherTab.id != tab.id {
|
||||||
|
tabManager.closeTab(otherTab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDrag {
|
||||||
|
tabManager.draggedTab = tab
|
||||||
|
return NSItemProvider(object: tab.id.uuidString as NSString)
|
||||||
|
}
|
||||||
|
.onDrop(
|
||||||
|
of: [.text],
|
||||||
|
delegate: QuickTerminalTabDropDelegate(
|
||||||
|
item: tab,
|
||||||
|
tabManager: tabManager,
|
||||||
|
currentTab: tabManager.draggedTab
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Divider()
|
||||||
|
.background(Color(NSColor.separatorColor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(systemName: "plus")
|
||||||
|
.foregroundColor(Color(NSColor.secondaryLabelColor))
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.frame(width: 50)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
tabManager.newTab()
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.help("New Tab")
|
||||||
|
}
|
||||||
|
.frame(height: 32)
|
||||||
|
.background(Color(NSColor.controlBackgroundColor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct QuickTerminalTabDropDelegate: DropDelegate {
|
||||||
|
let item: QuickTerminalTab
|
||||||
|
let tabManager: QuickTerminalTabManager
|
||||||
|
let currentTab: QuickTerminalTab?
|
||||||
|
|
||||||
|
func performDrop(info: DropInfo) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func dropEntered(info: DropInfo) {
|
||||||
|
guard let currentTab = currentTab,
|
||||||
|
let from = tabManager.tabs.firstIndex(where: { $0.id == currentTab.id }),
|
||||||
|
let to = tabManager.tabs.firstIndex(where: { $0.id == item.id })
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
if tabManager.tabs[to].id != currentTab.id {
|
||||||
|
tabManager.tabs.move(
|
||||||
|
fromOffsets: IndexSet(integer: from),
|
||||||
|
toOffset: to > from ? to + 1 : to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct QuickTerminalTabItemView: View {
|
||||||
|
@ObservedObject var tab: QuickTerminalTab
|
||||||
|
let isHighlighted: Bool
|
||||||
|
let onSelect: () -> Void
|
||||||
|
let onClose: () -> Void
|
||||||
|
|
||||||
|
@State private var isHovered = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Button(action: onClose) {
|
||||||
|
Image(systemName: "xmark")
|
||||||
|
.font(.system(size: 11))
|
||||||
|
.foregroundColor(isHovered ? .primary : .secondary)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.opacity(isHovered ? 1 : 0)
|
||||||
|
.animation(.easeInOut, value: isHovered)
|
||||||
|
|
||||||
|
Text(tab.title)
|
||||||
|
.foregroundColor(isHighlighted ? .primary : .secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.frame(height: 32)
|
||||||
|
.background(
|
||||||
|
Rectangle()
|
||||||
|
.fill(
|
||||||
|
isHighlighted
|
||||||
|
? Color(NSColor.controlBackgroundColor)
|
||||||
|
: (isHovered ? Color(NSColor.underPageBackgroundColor) : Color(NSColor.windowBackgroundColor)))
|
||||||
|
)
|
||||||
|
.onHover { hovering in
|
||||||
|
isHovered = hovering
|
||||||
|
}
|
||||||
|
.onTapGesture(
|
||||||
|
perform: {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
onSelect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,128 @@
|
|||||||
|
import GhosttyKit
|
||||||
|
|
||||||
|
class QuickTerminalTabManager: ObservableObject {
|
||||||
|
@Published var tabs: [QuickTerminalTab] = []
|
||||||
|
@Published var currentTab: QuickTerminalTab?
|
||||||
|
@Published var draggedTab: QuickTerminalTab?
|
||||||
|
|
||||||
|
private weak var controller: QuickTerminalController?
|
||||||
|
|
||||||
|
init(controller: QuickTerminalController) {
|
||||||
|
self.controller = controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTab() {
|
||||||
|
guard let ghostty = controller?.ghostty else { return }
|
||||||
|
|
||||||
|
let leaf: Ghostty.SplitNode.Leaf = .init(ghostty.app!, baseConfig: nil)
|
||||||
|
let surface: Ghostty.SplitNode = .leaf(leaf)
|
||||||
|
let tabIndex = tabs.count + 1
|
||||||
|
|
||||||
|
let newTab = QuickTerminalTab(surface: surface, title: "Terminal \(tabIndex)")
|
||||||
|
tabs.append(newTab)
|
||||||
|
|
||||||
|
selectTab(newTab)
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectTab(_ tab: QuickTerminalTab) {
|
||||||
|
guard currentTab?.id != tab.id else { return } // Avoid unnecessary updates
|
||||||
|
|
||||||
|
currentTab?.isActive = false
|
||||||
|
tab.isActive = true
|
||||||
|
currentTab = tab
|
||||||
|
|
||||||
|
controller?.updateSurfaceTree(to: tab.surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeTab(_ tab: QuickTerminalTab) {
|
||||||
|
if let index = tabs.firstIndex(where: { $0.id == tab.id }) {
|
||||||
|
tabs.remove(at: index)
|
||||||
|
|
||||||
|
if currentTab?.id == tab.id {
|
||||||
|
if tabs.isEmpty {
|
||||||
|
newTab()
|
||||||
|
controller?.animateOut()
|
||||||
|
} else {
|
||||||
|
let newIndex = min(index, tabs.count - 1)
|
||||||
|
selectTab(tabs[newIndex])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func moveTab(from source: IndexSet, to destination: Int) {
|
||||||
|
tabs.move(fromOffsets: source, toOffset: destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectNextTab() {
|
||||||
|
guard let currentTab = currentTab,
|
||||||
|
let currentIndex = tabs.firstIndex(where: { $0.id == currentTab.id })
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
let nextIndex = (currentIndex + 1) % tabs.count
|
||||||
|
selectTab(tabs[nextIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectPreviousTab() {
|
||||||
|
guard let currentTab = currentTab,
|
||||||
|
let currentIndex = tabs.firstIndex(where: { $0.id == currentTab.id })
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
let previousIndex = (currentIndex - 1 + tabs.count) % tabs.count
|
||||||
|
selectTab(tabs[previousIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - Notifications
|
||||||
|
|
||||||
|
@objc func onMoveTab(_ notification: Notification) {
|
||||||
|
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||||
|
guard target == controller?.focusedSurface else { return }
|
||||||
|
|
||||||
|
// Get the move action
|
||||||
|
guard
|
||||||
|
let action = notification.userInfo?[Notification.Name.GhosttyMoveTabKey]
|
||||||
|
as? Ghostty.Action.MoveTab
|
||||||
|
else { return }
|
||||||
|
guard action.amount != 0 else { return }
|
||||||
|
|
||||||
|
guard let currentTabIndex = tabs.firstIndex(where: { $0.id == currentTab?.id }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the final index we want to insert our tab
|
||||||
|
let finalIndex: Int
|
||||||
|
if action.amount < 0 {
|
||||||
|
finalIndex = max(0, currentTabIndex - min(currentTabIndex, -action.amount))
|
||||||
|
} else {
|
||||||
|
let remaining: Int = tabs.count - 1 - currentTabIndex
|
||||||
|
finalIndex = currentTabIndex + min(remaining, action.amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If our index is the same we do nothing
|
||||||
|
guard finalIndex != currentTabIndex else { return }
|
||||||
|
|
||||||
|
// move the tab
|
||||||
|
moveTab(from: IndexSet(integer: currentTabIndex), to: finalIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func onGoToTab(_ notification: Notification) {
|
||||||
|
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||||
|
guard target == controller?.focusedSurface else { return }
|
||||||
|
|
||||||
|
guard let tabEnumAny = notification.userInfo?[Ghostty.Notification.GotoTabKey] else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let tabEnum = tabEnumAny as? ghostty_action_goto_tab_e else { return }
|
||||||
|
let tabIndex: Int32 = tabEnum.rawValue
|
||||||
|
|
||||||
|
if tabIndex == GHOSTTY_GOTO_TAB_PREVIOUS.rawValue {
|
||||||
|
selectPreviousTab()
|
||||||
|
} else if tabIndex == GHOSTTY_GOTO_TAB_NEXT.rawValue {
|
||||||
|
selectNextTab()
|
||||||
|
} else if tabIndex == GHOSTTY_GOTO_TAB_LAST.rawValue {
|
||||||
|
selectTab(tabs[tabs.count - 1])
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -326,6 +326,9 @@ class TerminalManager {
|
|||||||
guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return }
|
guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return }
|
||||||
guard let window = surfaceView.window else { return }
|
guard let window = surfaceView.window else { return }
|
||||||
|
|
||||||
|
// return if window is not in our managed windows
|
||||||
|
guard windows.contains(where: { $0.controller.window == window }) else { return }
|
||||||
|
|
||||||
let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey]
|
let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey]
|
||||||
let config = configAny as? Ghostty.SurfaceConfiguration
|
let config = configAny as? Ghostty.SurfaceConfiguration
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user