macOS: enable quick terminal manual resizing

You can now resize the quick terminal both vertically and horizontally. To incorporate adjusting the custom secondary size on the quick terminal we needed to have the ability to resize the width (if from top, bottom, or center), and height (if from right, left, or center). The quick terminal will retain the user's manually adjusted size while the app is open. A new feature with this is that when the secondary size is adjusted (or primary if the quick terminal is center), the size will increase or decrease on both sides of the terminal.
This commit is contained in:
Friedrich Stoltzfus
2025-06-21 10:46:30 -04:00
committed by Mitchell Hashimoto
parent 8a5d4cd196
commit 4c5800734b
2 changed files with 113 additions and 31 deletions

View File

@ -22,7 +22,7 @@ class QuickTerminalController: BaseTerminalController {
private var previousActiveSpace: CGSSpace? = nil
/// The window frame saved when the quick terminal's surface tree becomes empty.
///
///
/// This preserves the user's window size and position when all terminal surfaces
/// are closed (e.g., via the `exit` command). When a new surface is created,
/// the window will be restored to this frame, preventing SwiftUI from resetting
@ -34,6 +34,9 @@ class QuickTerminalController: BaseTerminalController {
/// The configuration derived from the Ghostty config so we don't need to rely on references.
private var derivedConfig: DerivedConfig
/// Tracks if we're currently handling a manual resize to prevent recursion
private var isHandlingResize: Bool = false
init(_ ghostty: Ghostty.App,
position: QuickTerminalPosition = .top,
@ -76,6 +79,11 @@ class QuickTerminalController: BaseTerminalController {
selector: #selector(onNewTab),
name: Ghostty.Notification.ghosttyNewTab,
object: nil)
center.addObserver(
self,
selector: #selector(windowDidResize(_:)),
name: NSWindow.didResizeNotification,
object: nil)
}
required init?(coder: NSCoder) {
@ -195,10 +203,45 @@ class QuickTerminalController: BaseTerminalController {
}
func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize {
// We use the actual screen the window is on for this, since it should
// be on the proper screen.
guard let screen = window?.screen ?? NSScreen.main else { return frameSize }
return position.restrictFrameSize(frameSize, on: screen, terminalSize: derivedConfig.quickTerminalSize)
// Allow unrestricted resizing - users have full control
return frameSize
}
override func windowDidResize(_ notification: Notification) {
guard let window = notification.object as? NSWindow,
window == self.window,
visible,
!isHandlingResize else { return }
// For centered positions (top, bottom, center), we need to recenter the window
// when it's manually resized to maintain proper positioning
switch position {
case .top, .bottom, .center:
recenterWindow(window)
case .left, .right:
// For side positions, we may need to adjust vertical centering
recenterWindowVertically(window)
}
}
private func recenterWindow(_ window: NSWindow) {
guard let screen = window.screen ?? NSScreen.main else { return }
isHandlingResize = true
defer { isHandlingResize = false }
let newOrigin = position.centeredOrigin(for: window, on: screen)
window.setFrameOrigin(newOrigin)
}
private func recenterWindowVertically(_ window: NSWindow) {
guard let screen = window.screen ?? NSScreen.main else { return }
isHandlingResize = true
defer { isHandlingResize = false }
let newOrigin = position.verticallyCenteredOrigin(for: window, on: screen)
window.setFrameOrigin(newOrigin)
}
// MARK: Base Controller Overrides
@ -320,13 +363,15 @@ class QuickTerminalController: BaseTerminalController {
guard let screen = derivedConfig.quickTerminalScreen.screen else { return }
// Restore our previous frame if we have one
var preserveSize: NSSize? = nil
if let lastClosedFrame {
window.setFrame(lastClosedFrame, display: false)
preserveSize = lastClosedFrame.size
self.lastClosedFrame = nil
}
// Move our window off screen to the top
position.setInitial(in: window, on: screen, terminalSize: derivedConfig.quickTerminalSize)
position.setInitial(in: window, on: screen, terminalSize: derivedConfig.quickTerminalSize, preserveSize: preserveSize)
// We need to set our window level to a high value. In testing, only
// popUpMenu and above do what we want. This gets it above the menu bar
@ -357,7 +402,7 @@ class QuickTerminalController: BaseTerminalController {
NSAnimationContext.runAnimationGroup({ context in
context.duration = derivedConfig.quickTerminalAnimationDuration
context.timingFunction = .init(name: .easeIn)
position.setFinal(in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize)
position.setFinal(in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize, preserveSize: preserveSize)
}, completionHandler: {
// There is a very minor delay here so waiting at least an event loop tick
// keeps us safe from the view not being on the window.
@ -481,7 +526,7 @@ class QuickTerminalController: BaseTerminalController {
NSAnimationContext.runAnimationGroup({ context in
context.duration = derivedConfig.quickTerminalAnimationDuration
context.timingFunction = .init(name: .easeIn)
position.setInitial(in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize)
position.setInitial(in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize, preserveSize: window.frame.size)
}, completionHandler: {
// This causes the window to be removed from the screen list and macOS
// handles what should be focused next.

View File

@ -20,49 +20,38 @@ enum QuickTerminalPosition : String {
}
/// Set the initial state for a window for animating out of this position.
func setInitial(in window: NSWindow, on screen: NSScreen, terminalSize: QuickTerminalSize) {
func setInitial(in window: NSWindow, on screen: NSScreen, terminalSize: QuickTerminalSize, preserveSize: NSSize? = nil) {
// We always start invisible
window.alphaValue = 0
// Position depends
window.setFrame(.init(
origin: initialOrigin(for: window, on: screen),
size: restrictFrameSize(window.frame.size, on: screen, terminalSize: terminalSize)
size: configuredFrameSize(on: screen, terminalSize: terminalSize, preserveExisting: preserveSize)
), display: false)
}
/// Set the final state for a window in this position.
func setFinal(in window: NSWindow, on screen: NSScreen, terminalSize: QuickTerminalSize) {
func setFinal(in window: NSWindow, on screen: NSScreen, terminalSize: QuickTerminalSize, preserveSize: NSSize? = nil) {
// We always end visible
window.alphaValue = 1
// Position depends
window.setFrame(.init(
origin: finalOrigin(for: window, on: screen),
size: restrictFrameSize(window.frame.size, on: screen, terminalSize: terminalSize)
size: configuredFrameSize(on: screen, terminalSize: terminalSize, preserveExisting: preserveSize)
), display: true)
}
/// Restrict the frame size during resizing.
func restrictFrameSize(_ size: NSSize, on screen: NSScreen, terminalSize: QuickTerminalSize) -> NSSize {
var finalSize = size
let dimensions = terminalSize.calculate(position: self, screenDimensions: screen.frame.size)
switch (self) {
case .top, .bottom:
finalSize.width = dimensions.width
finalSize.height = dimensions.height
case .left, .right:
finalSize.width = dimensions.width
finalSize.height = dimensions.height
case .center:
finalSize.width = dimensions.width
finalSize.height = dimensions.height
/// Get the configured frame size for initial positioning and animations.
func configuredFrameSize(on screen: NSScreen, terminalSize: QuickTerminalSize, preserveExisting: NSSize? = nil) -> NSSize {
// If we have existing dimensions from manual resizing, preserve them
if let existing = preserveExisting, existing.width > 0 && existing.height > 0 {
return existing
}
return finalSize
let dimensions = terminalSize.calculate(position: self, screenDimensions: screen.frame.size)
return NSSize(width: dimensions.width, height: dimensions.height)
}
/// The initial point origin for this position.
@ -122,4 +111,52 @@ enum QuickTerminalPosition : String {
case .right: self == .top || self == .bottom
}
}
/// Calculate the centered origin for a window, keeping it properly positioned after manual resizing
func centeredOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
switch self {
case .top:
return CGPoint(
x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2,
y: window.frame.origin.y // Keep the same Y position
)
case .bottom:
return CGPoint(
x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2,
y: window.frame.origin.y // Keep the same Y position
)
case .center:
return CGPoint(
x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2,
y: screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2
)
case .left, .right:
// For left/right positions, only adjust horizontal centering if needed
return window.frame.origin
}
}
/// Calculate the vertically centered origin for side-positioned windows
func verticallyCenteredOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
switch self {
case .left:
return CGPoint(
x: window.frame.origin.x, // Keep the same X position
y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2
)
case .right:
return CGPoint(
x: window.frame.origin.x, // Keep the same X position
y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2
)
case .top, .bottom, .center:
// These positions don't need vertical recentering during resize
return window.frame.origin
}
}
}