macOS: Add support for quick terminal sizing configuration

Added C bindings for the already existing quick-terminal-size
configuration. Created a new QuickTerminalSize struct to hold these
values in Swift. Updated the QuickTerminal implementation to use the
user's configuration if supplied. Retains defaults. Also adds support to
customize the width of the quick terminal (height if quick terminal is
set to right or left).
This commit is contained in:
Friedrich Stoltzfus
2025-06-11 14:41:35 -04:00
committed by Mitchell Hashimoto
parent 5c464e855d
commit 63cd424678
7 changed files with 167 additions and 48 deletions

View File

@@ -450,6 +450,14 @@ typedef struct {
ghostty_config_color_s colors[256];
} ghostty_config_palette_s;
// config.QuickTerminalSize
typedef struct {
uint8_t primary_type; // 0 = none, 1 = percentage, 2 = pixels
float primary_value;
uint8_t secondary_type; // 0 = none, 1 = percentage, 2 = pixels
float secondary_value;
} ghostty_config_quick_terminal_size_s;
// apprt.Target.Key
typedef enum {
GHOSTTY_TARGET_APP,

View File

@@ -104,6 +104,7 @@
A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */; };
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
A5B4EA852DFE691B0022C3A2 /* NSMenuItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */; };
A5BB78B92DF9D8CE009AC3FA /* QuickTerminalSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */; };
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; };
A5CA378E2D31D6C300931030 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378D2D31D6C100931030 /* Weak.swift */; };
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; };
@@ -252,6 +253,7 @@
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+Extension.swift"; sourceTree = "<group>"; };
A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalSize.swift; sourceTree = "<group>"; };
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = "<group>"; };
A5CA378D2D31D6C100931030 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = "<group>"; };
@@ -638,6 +640,7 @@
A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */,
A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */,
A5CBD05F2CA0C9080017A1AE /* QuickTerminalWindow.swift */,
A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */,
);
path = QuickTerminal;
sourceTree = "<group>";
@@ -939,6 +942,8 @@
A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */,
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */,
A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */,
A5BB78B92DF9D8CE009AC3FA /* QuickTerminalSize.swift in Sources */,
A57D79272C9C879B001D522E /* SecureInput.swift in Sources */,
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */,
A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */,

View File

@@ -109,7 +109,7 @@ class QuickTerminalController: BaseTerminalController {
syncAppearance()
// Setup our initial size based on our configured position
position.setLoaded(window)
position.setLoaded(window, size: derivedConfig.quickTerminalSize)
// Upon first adding this Window to its host view, older SwiftUI
// seems to have a "hiccup" and corrupts the frameRect,
@@ -213,7 +213,7 @@ class QuickTerminalController: BaseTerminalController {
// 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)
return position.restrictFrameSize(frameSize, on: screen, terminalSize: derivedConfig.quickTerminalSize)
}
// MARK: Base Controller Overrides
@@ -341,7 +341,7 @@ class QuickTerminalController: BaseTerminalController {
}
// Move our window off screen to the top
position.setInitial(in: window, on: screen)
position.setInitial(in: window, on: screen, terminalSize: derivedConfig.quickTerminalSize)
// 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
@@ -372,7 +372,7 @@ class QuickTerminalController: BaseTerminalController {
NSAnimationContext.runAnimationGroup({ context in
context.duration = derivedConfig.quickTerminalAnimationDuration
context.timingFunction = .init(name: .easeIn)
position.setFinal(in: window.animator(), on: screen)
position.setFinal(in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize)
}, 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.
@@ -496,7 +496,7 @@ class QuickTerminalController: BaseTerminalController {
NSAnimationContext.runAnimationGroup({ context in
context.duration = derivedConfig.quickTerminalAnimationDuration
context.timingFunction = .init(name: .easeIn)
position.setInitial(in: window.animator(), on: screen)
position.setInitial(in: window.animator(), on: screen, terminalSize: derivedConfig.quickTerminalSize)
}, completionHandler: {
// This causes the window to be removed from the screen list and macOS
// handles what should be focused next.
@@ -627,6 +627,7 @@ class QuickTerminalController: BaseTerminalController {
let quickTerminalAnimationDuration: Double
let quickTerminalAutoHide: Bool
let quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior
let quickTerminalSize: QuickTerminalSize
let backgroundOpacity: Double
init() {
@@ -634,6 +635,7 @@ class QuickTerminalController: BaseTerminalController {
self.quickTerminalAnimationDuration = 0.2
self.quickTerminalAutoHide = true
self.quickTerminalSpaceBehavior = .move
self.quickTerminalSize = QuickTerminalSize()
self.backgroundOpacity = 1.0
}
@@ -642,6 +644,7 @@ class QuickTerminalController: BaseTerminalController {
self.quickTerminalAnimationDuration = config.quickTerminalAnimationDuration
self.quickTerminalAutoHide = config.quickTerminalAutoHide
self.quickTerminalSpaceBehavior = config.quickTerminalSpaceBehavior
self.quickTerminalSize = config.quickTerminalSize
self.backgroundOpacity = config.backgroundOpacity
}
}

View File

@@ -8,72 +8,58 @@ enum QuickTerminalPosition : String {
case center
/// Set the loaded state for a window.
func setLoaded(_ window: NSWindow) {
func setLoaded(_ window: NSWindow, size: QuickTerminalSize) {
guard let screen = window.screen ?? NSScreen.main else { return }
switch (self) {
case .top, .bottom:
let dimensions = size.calculate(position: self, screenDimensions: screen.frame.size)
window.setFrame(.init(
origin: window.frame.origin,
size: .init(
width: screen.frame.width,
height: screen.frame.height / 4)
width: dimensions.width,
height: dimensions.height)
), display: false)
case .left, .right:
window.setFrame(.init(
origin: window.frame.origin,
size: .init(
width: screen.frame.width / 4,
height: screen.frame.height)
), display: false)
case .center:
window.setFrame(.init(
origin: window.frame.origin,
size: .init(
width: screen.frame.width / 2,
height: screen.frame.height / 3)
), display: false)
}
}
/// Set the initial state for a window for animating out of this position.
func setInitial(in window: NSWindow, on screen: NSScreen) {
func setInitial(in window: NSWindow, on screen: NSScreen, terminalSize: QuickTerminalSize) {
// 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)
size: restrictFrameSize(window.frame.size, on: screen, terminalSize: terminalSize)
), display: false)
}
/// Set the final state for a window in this position.
func setFinal(in window: NSWindow, on screen: NSScreen) {
func setFinal(in window: NSWindow, on screen: NSScreen, terminalSize: QuickTerminalSize) {
// 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)
size: restrictFrameSize(window.frame.size, on: screen, terminalSize: terminalSize)
), display: true)
}
/// Restrict the frame size during resizing.
func restrictFrameSize(_ size: NSSize, on screen: NSScreen) -> NSSize {
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 = screen.frame.width
finalSize.width = dimensions.width
finalSize.height = dimensions.height
case .left, .right:
finalSize.height = screen.visibleFrame.height
finalSize.width = dimensions.width
finalSize.height = dimensions.height
case .center:
finalSize.width = screen.frame.width / 2
finalSize.height = screen.frame.height / 3
finalSize.width = dimensions.width
finalSize.height = dimensions.height
}
return finalSize
@@ -83,16 +69,16 @@ enum QuickTerminalPosition : String {
func initialOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
switch (self) {
case .top:
return .init(x: screen.frame.minX, y: screen.frame.maxY)
return .init(x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2, y: screen.frame.maxY)
case .bottom:
return .init(x: screen.frame.minX, y: -window.frame.height)
return .init(x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2, y: -window.frame.height)
case .left:
return .init(x: screen.frame.minX-window.frame.width, y: 0)
return .init(x: screen.frame.minX-window.frame.width, y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2)
case .right:
return .init(x: screen.frame.maxX, y: 0)
return .init(x: screen.frame.maxX, y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2)
case .center:
return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.height - window.frame.width)
@@ -103,16 +89,16 @@ enum QuickTerminalPosition : String {
func finalOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
switch (self) {
case .top:
return .init(x: screen.frame.minX, y: screen.visibleFrame.maxY - window.frame.height)
return .init(x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2, y: screen.visibleFrame.maxY - window.frame.height)
case .bottom:
return .init(x: screen.frame.minX, y: screen.frame.minY)
return .init(x: screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2, y: screen.frame.minY)
case .left:
return .init(x: screen.frame.minX, y: window.frame.origin.y)
return .init(x: screen.frame.minX, y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2)
case .right:
return .init(x: screen.visibleFrame.maxX - window.frame.width, y: window.frame.origin.y)
return .init(x: screen.visibleFrame.maxX - window.frame.width, y: screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2)
case .center:
return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)

View File

@@ -0,0 +1,80 @@
import Cocoa
import GhosttyKit
struct QuickTerminalSize {
let primary: Size?
let secondary: Size?
init(primary: Size? = nil, secondary: Size? = nil) {
self.primary = primary
self.secondary = secondary
}
init(from cStruct: ghostty_config_quick_terminal_size_s) {
self.primary = cStruct.primary_type == 0 ? nil : Size(type: cStruct.primary_type, value: cStruct.primary_value)
self.secondary = cStruct.secondary_type == 0 ? nil : Size(type: cStruct.secondary_type, value: cStruct.secondary_value)
}
enum Size {
case percentage(Float)
case pixels(UInt32)
init?(type: UInt8, value: Float) {
switch type {
case 1:
self = .percentage(value)
case 2:
self = .pixels(UInt32(value))
default:
return nil
}
}
func toPixels(parentDimension: CGFloat) -> CGFloat {
switch self {
case .percentage(let value):
return parentDimension * CGFloat(value) / 100.0
case .pixels(let value):
return CGFloat(value)
}
}
}
struct Dimensions {
let width: CGFloat
let height: CGFloat
}
func calculate(position: QuickTerminalPosition, screenDimensions: CGSize) -> Dimensions {
let dims = Dimensions(width: screenDimensions.width, height: screenDimensions.height)
switch position {
case .left, .right:
return Dimensions(
width: primary?.toPixels(parentDimension: dims.width) ?? 400,
height: secondary?.toPixels(parentDimension: dims.height) ?? dims.height
)
case .top, .bottom:
return Dimensions(
width: secondary?.toPixels(parentDimension: dims.width) ?? dims.width,
height: primary?.toPixels(parentDimension: dims.height) ?? 400
)
case .center:
if dims.width >= dims.height {
// Landscape
return Dimensions(
width: primary?.toPixels(parentDimension: dims.width) ?? 800,
height: secondary?.toPixels(parentDimension: dims.height) ?? 400
)
} else {
// Portrait
return Dimensions(
width: secondary?.toPixels(parentDimension: dims.width) ?? 400,
height: primary?.toPixels(parentDimension: dims.height) ?? 800
)
}
}
}
}

View File

@@ -504,6 +504,14 @@ extension Ghostty {
let str = String(cString: ptr)
return QuickTerminalSpaceBehavior(fromGhosttyConfig: str) ?? .move
}
var quickTerminalSize: QuickTerminalSize {
guard let config = self.config else { return QuickTerminalSize() }
var v = ghostty_config_quick_terminal_size_s()
let key = "quick-terminal-size"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return QuickTerminalSize() }
return QuickTerminalSize(from: v)
}
#endif
var resizeOverlay: ResizeOverlay {

View File

@@ -7198,6 +7198,35 @@ pub const QuickTerminalSize = struct {
height: u32,
};
/// C API structure for QuickTerminalSize
pub const C = extern struct {
primary_type: u8, // 0 = none, 1 = percentage, 2 = pixels
primary_value: f32,
secondary_type: u8, // 0 = none, 1 = percentage, 2 = pixels
secondary_value: f32,
};
pub fn cval(self: QuickTerminalSize) C {
return .{
.primary_type = if (self.primary) |p| switch (p) {
.percentage => 1,
.pixels => 2,
} else 0,
.primary_value = if (self.primary) |p| switch (p) {
.percentage => |v| v,
.pixels => |v| @floatFromInt(v),
} else 0,
.secondary_type = if (self.secondary) |s| switch (s) {
.percentage => 1,
.pixels => 2,
} else 0,
.secondary_value = if (self.secondary) |s| switch (s) {
.percentage => |v| v,
.pixels => |v| @floatFromInt(v),
} else 0,
};
}
pub fn calculate(
self: QuickTerminalSize,
position: QuickTerminalPosition,