mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-19 18:26:13 +03:00
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:

committed by
Mitchell Hashimoto

parent
8a5d4cd196
commit
4c5800734b
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user