mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-21 03:06:15 +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
@ -35,6 +35,9 @@ 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
|
||||||
|
|
||||||
|
/// Tracks if we're currently handling a manual resize to prevent recursion
|
||||||
|
private var isHandlingResize: Bool = false
|
||||||
|
|
||||||
init(_ ghostty: Ghostty.App,
|
init(_ ghostty: Ghostty.App,
|
||||||
position: QuickTerminalPosition = .top,
|
position: QuickTerminalPosition = .top,
|
||||||
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||||
@ -76,6 +79,11 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
selector: #selector(onNewTab),
|
selector: #selector(onNewTab),
|
||||||
name: Ghostty.Notification.ghosttyNewTab,
|
name: Ghostty.Notification.ghosttyNewTab,
|
||||||
object: nil)
|
object: nil)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(windowDidResize(_:)),
|
||||||
|
name: NSWindow.didResizeNotification,
|
||||||
|
object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -195,10 +203,45 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize {
|
func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize {
|
||||||
// We use the actual screen the window is on for this, since it should
|
// Allow unrestricted resizing - users have full control
|
||||||
// be on the proper screen.
|
return frameSize
|
||||||
guard let screen = window?.screen ?? NSScreen.main else { return frameSize }
|
}
|
||||||
return position.restrictFrameSize(frameSize, on: screen, terminalSize: derivedConfig.quickTerminalSize)
|
|
||||||
|
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
|
// MARK: Base Controller Overrides
|
||||||
@ -320,13 +363,15 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
guard let screen = derivedConfig.quickTerminalScreen.screen else { return }
|
guard let screen = derivedConfig.quickTerminalScreen.screen else { return }
|
||||||
|
|
||||||
// Restore our previous frame if we have one
|
// Restore our previous frame if we have one
|
||||||
|
var preserveSize: NSSize? = nil
|
||||||
if let lastClosedFrame {
|
if let lastClosedFrame {
|
||||||
window.setFrame(lastClosedFrame, display: false)
|
window.setFrame(lastClosedFrame, display: false)
|
||||||
|
preserveSize = lastClosedFrame.size
|
||||||
self.lastClosedFrame = nil
|
self.lastClosedFrame = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move our window off screen to the top
|
// 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
|
// 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
|
// 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
|
NSAnimationContext.runAnimationGroup({ context in
|
||||||
context.duration = derivedConfig.quickTerminalAnimationDuration
|
context.duration = derivedConfig.quickTerminalAnimationDuration
|
||||||
context.timingFunction = .init(name: .easeIn)
|
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: {
|
}, completionHandler: {
|
||||||
// There is a very minor delay here so waiting at least an event loop tick
|
// 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.
|
// keeps us safe from the view not being on the window.
|
||||||
@ -481,7 +526,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
NSAnimationContext.runAnimationGroup({ context in
|
NSAnimationContext.runAnimationGroup({ context in
|
||||||
context.duration = derivedConfig.quickTerminalAnimationDuration
|
context.duration = derivedConfig.quickTerminalAnimationDuration
|
||||||
context.timingFunction = .init(name: .easeIn)
|
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: {
|
}, completionHandler: {
|
||||||
// This causes the window to be removed from the screen list and macOS
|
// This causes the window to be removed from the screen list and macOS
|
||||||
// handles what should be focused next.
|
// 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.
|
/// 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
|
// We always start invisible
|
||||||
window.alphaValue = 0
|
window.alphaValue = 0
|
||||||
|
|
||||||
// Position depends
|
// Position depends
|
||||||
window.setFrame(.init(
|
window.setFrame(.init(
|
||||||
origin: initialOrigin(for: window, on: screen),
|
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)
|
), display: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the final state for a window in this position.
|
/// 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
|
// We always end visible
|
||||||
window.alphaValue = 1
|
window.alphaValue = 1
|
||||||
|
|
||||||
// Position depends
|
// Position depends
|
||||||
window.setFrame(.init(
|
window.setFrame(.init(
|
||||||
origin: finalOrigin(for: window, on: screen),
|
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)
|
), display: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restrict the frame size during resizing.
|
/// Get the configured frame size for initial positioning and animations.
|
||||||
func restrictFrameSize(_ size: NSSize, on screen: NSScreen, terminalSize: QuickTerminalSize) -> NSSize {
|
func configuredFrameSize(on screen: NSScreen, terminalSize: QuickTerminalSize, preserveExisting: NSSize? = nil) -> NSSize {
|
||||||
var finalSize = size
|
// If we have existing dimensions from manual resizing, preserve them
|
||||||
let dimensions = terminalSize.calculate(position: self, screenDimensions: screen.frame.size)
|
if let existing = preserveExisting, existing.width > 0 && existing.height > 0 {
|
||||||
|
return existing
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
/// The initial point origin for this position.
|
||||||
@ -122,4 +111,52 @@ enum QuickTerminalPosition : String {
|
|||||||
case .right: self == .top || self == .bottom
|
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