mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
feat: customize quick terminal size
This commit introduce `quick-terminal-size` option which allows to define the size of the quick terminal. It also fixes an issue where the quick terminal position was not properly updated when reloading the configuration. Resolves #2384
This commit is contained in:
@ -348,6 +348,22 @@ typedef struct {
|
|||||||
size_t len;
|
size_t len;
|
||||||
} ghostty_config_color_list_s;
|
} ghostty_config_color_list_s;
|
||||||
|
|
||||||
|
// config.QuickTerminalSize
|
||||||
|
typedef enum {
|
||||||
|
GHOSTTY_QUICK_TERMINAL_PIXEL_UNIT,
|
||||||
|
GHOSTTY_QUICK_TERMINAL_PERCENTAGE_UNIT,
|
||||||
|
} ghostty_config_quick_terminal_unit_e;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint16_t value;
|
||||||
|
ghostty_config_quick_terminal_unit_e unit;
|
||||||
|
} ghostty_config_quick_terminal_dimension_s;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
ghostty_config_quick_terminal_dimension_s* dimensions;
|
||||||
|
size_t len;
|
||||||
|
} ghostty_config_quick_terminal_size_s;
|
||||||
|
|
||||||
// apprt.Target.Key
|
// apprt.Target.Key
|
||||||
typedef enum {
|
typedef enum {
|
||||||
GHOSTTY_TARGET_APP,
|
GHOSTTY_TARGET_APP,
|
||||||
|
@ -92,6 +92,7 @@
|
|||||||
A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; };
|
A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; };
|
||||||
A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; };
|
A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; };
|
||||||
A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; };
|
A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; };
|
||||||
|
AB315D402D1DCC6B0012D326 /* QuickTerminalSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB315D3F2D1DCC630012D326 /* QuickTerminalSize.swift */; };
|
||||||
AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */; };
|
AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */; };
|
||||||
AEF9CE242B6AD07A0017E195 /* TerminalToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */; };
|
AEF9CE242B6AD07A0017E195 /* TerminalToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */; };
|
||||||
C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; };
|
C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; };
|
||||||
@ -183,6 +184,7 @@
|
|||||||
A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationController.swift; sourceTree = "<group>"; };
|
A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationController.swift; sourceTree = "<group>"; };
|
||||||
A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationView.swift; sourceTree = "<group>"; };
|
A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationView.swift; sourceTree = "<group>"; };
|
||||||
A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||||
|
AB315D3F2D1DCC630012D326 /* QuickTerminalSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalSize.swift; sourceTree = "<group>"; };
|
||||||
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = "<group>"; };
|
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = "<group>"; };
|
||||||
AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalToolbar.swift; sourceTree = "<group>"; };
|
AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalToolbar.swift; sourceTree = "<group>"; };
|
||||||
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = "<group>"; };
|
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = "<group>"; };
|
||||||
@ -434,6 +436,7 @@
|
|||||||
A5CBD05A2CA0C5910017A1AE /* QuickTerminal */ = {
|
A5CBD05A2CA0C5910017A1AE /* QuickTerminal */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
AB315D3F2D1DCC630012D326 /* QuickTerminalSize.swift */,
|
||||||
A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */,
|
A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */,
|
||||||
A5CBD05D2CA0C5E70017A1AE /* QuickTerminalController.swift */,
|
A5CBD05D2CA0C5E70017A1AE /* QuickTerminalController.swift */,
|
||||||
A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */,
|
A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */,
|
||||||
@ -633,6 +636,7 @@
|
|||||||
A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
|
A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
|
||||||
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
|
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
|
||||||
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */,
|
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */,
|
||||||
|
AB315D402D1DCC6B0012D326 /* QuickTerminalSize.swift in Sources */,
|
||||||
A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */,
|
A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */,
|
||||||
A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */,
|
A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */,
|
||||||
A5FEB3002ABB69450068369E /* main.swift in Sources */,
|
A5FEB3002ABB69450068369E /* main.swift in Sources */,
|
||||||
|
@ -8,7 +8,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
override var windowNibName: NSNib.Name? { "QuickTerminal" }
|
override var windowNibName: NSNib.Name? { "QuickTerminal" }
|
||||||
|
|
||||||
/// The position for the quick terminal.
|
/// The position for the quick terminal.
|
||||||
let position: QuickTerminalPosition
|
private var position: QuickTerminalPosition
|
||||||
|
|
||||||
/// The current state of the quick terminal
|
/// The current state of the quick terminal
|
||||||
private(set) var visible: Bool = false
|
private(set) var visible: Bool = false
|
||||||
@ -72,7 +72,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
syncAppearance(ghostty.config)
|
syncAppearance(ghostty.config)
|
||||||
|
|
||||||
// Setup our initial size based on our configured position
|
// Setup our initial size based on our configured position
|
||||||
position.setLoaded(window)
|
derivedConfig.quickTerminalSize.apply(window, position)
|
||||||
|
|
||||||
// Setup our content
|
// Setup our content
|
||||||
window.contentView = NSHostingView(rootView: TerminalView(
|
window.contentView = NSHostingView(rootView: TerminalView(
|
||||||
@ -307,6 +307,9 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
private func syncAppearance(_ config: Ghostty.Config) {
|
private func syncAppearance(_ config: Ghostty.Config) {
|
||||||
guard let window else { return }
|
guard let window else { return }
|
||||||
|
|
||||||
|
// Update the quick terminal size right away
|
||||||
|
config.quickTerminalSize.apply(window, config.quickTerminalPosition)
|
||||||
|
|
||||||
// If our window is not visible, then delay this. This is possible specifically
|
// If our window is not visible, then delay this. This is possible specifically
|
||||||
// during state restoration but probably in other scenarios as well. To delay,
|
// during state restoration but probably in other scenarios as well. To delay,
|
||||||
// we just loop directly on the dispatch queue. We have to delay because some
|
// we just loop directly on the dispatch queue. We have to delay because some
|
||||||
@ -390,6 +393,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
|
|
||||||
// Update our derived config
|
// Update our derived config
|
||||||
self.derivedConfig = DerivedConfig(config)
|
self.derivedConfig = DerivedConfig(config)
|
||||||
|
self.position = self.derivedConfig.quickTerminalPosition
|
||||||
|
|
||||||
syncAppearance(config)
|
syncAppearance(config)
|
||||||
}
|
}
|
||||||
@ -398,17 +402,23 @@ class QuickTerminalController: BaseTerminalController {
|
|||||||
let quickTerminalScreen: QuickTerminalScreen
|
let quickTerminalScreen: QuickTerminalScreen
|
||||||
let quickTerminalAnimationDuration: Double
|
let quickTerminalAnimationDuration: Double
|
||||||
let quickTerminalAutoHide: Bool
|
let quickTerminalAutoHide: Bool
|
||||||
|
let quickTerminalPosition: QuickTerminalPosition
|
||||||
|
let quickTerminalSize: QuickTerminalSize
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.quickTerminalScreen = .main
|
self.quickTerminalScreen = .main
|
||||||
self.quickTerminalAnimationDuration = 0.2
|
self.quickTerminalAnimationDuration = 0.2
|
||||||
self.quickTerminalAutoHide = true
|
self.quickTerminalAutoHide = true
|
||||||
|
self.quickTerminalPosition = .top
|
||||||
|
self.quickTerminalSize = .init()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ config: Ghostty.Config) {
|
init(_ config: Ghostty.Config) {
|
||||||
self.quickTerminalScreen = config.quickTerminalScreen
|
self.quickTerminalScreen = config.quickTerminalScreen
|
||||||
self.quickTerminalAnimationDuration = config.quickTerminalAnimationDuration
|
self.quickTerminalAnimationDuration = config.quickTerminalAnimationDuration
|
||||||
self.quickTerminalAutoHide = config.quickTerminalAutoHide
|
self.quickTerminalAutoHide = config.quickTerminalAutoHide
|
||||||
|
self.quickTerminalPosition = config.quickTerminalPosition
|
||||||
|
self.quickTerminalSize = config.quickTerminalSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,36 +7,6 @@ enum QuickTerminalPosition : String {
|
|||||||
case right
|
case right
|
||||||
case center
|
case center
|
||||||
|
|
||||||
/// Set the loaded state for a window.
|
|
||||||
func setLoaded(_ window: NSWindow) {
|
|
||||||
guard let screen = window.screen ?? NSScreen.main else { return }
|
|
||||||
switch (self) {
|
|
||||||
case .top, .bottom:
|
|
||||||
window.setFrame(.init(
|
|
||||||
origin: window.frame.origin,
|
|
||||||
size: .init(
|
|
||||||
width: screen.frame.width,
|
|
||||||
height: screen.frame.height / 4)
|
|
||||||
), 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.
|
/// 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) {
|
||||||
// We always start invisible
|
// We always start invisible
|
||||||
@ -72,8 +42,7 @@ enum QuickTerminalPosition : String {
|
|||||||
finalSize.height = screen.frame.height
|
finalSize.height = screen.frame.height
|
||||||
|
|
||||||
case .center:
|
case .center:
|
||||||
finalSize.width = screen.frame.width / 2
|
break
|
||||||
finalSize.height = screen.frame.height / 3
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return finalSize
|
return finalSize
|
||||||
|
81
macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift
Normal file
81
macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import Cocoa
|
||||||
|
import GhosttyKit
|
||||||
|
|
||||||
|
class QuickTerminalSize {
|
||||||
|
enum Size {
|
||||||
|
case percent(value: Double)
|
||||||
|
case pixel(value: UInt)
|
||||||
|
|
||||||
|
init?(c_dimension: ghostty_config_quick_terminal_dimension_s) {
|
||||||
|
switch(c_dimension.unit) {
|
||||||
|
case GHOSTTY_QUICK_TERMINAL_PIXEL_UNIT:
|
||||||
|
self = .pixel(value: UInt(c_dimension.value))
|
||||||
|
case GHOSTTY_QUICK_TERMINAL_PERCENTAGE_UNIT:
|
||||||
|
self = .percent(value: Double(c_dimension.value) / 100.0)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func apply(value: CGFloat) -> CGFloat {
|
||||||
|
switch(self) {
|
||||||
|
case .pixel(let fixed_size):
|
||||||
|
return CGFloat(fixed_size);
|
||||||
|
case .percent(let pct):
|
||||||
|
return value * pct;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mainDimension: Size;
|
||||||
|
var secondDimension: Size;
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.mainDimension = Size.percent(value: 0.25)
|
||||||
|
self.secondDimension = Size.percent(value: 0.25)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(config: ghostty_config_quick_terminal_size_s) {
|
||||||
|
switch (config.len) {
|
||||||
|
case 1:
|
||||||
|
self.mainDimension = Size(c_dimension: config.dimensions[0]) ?? Size.percent(value: 0.25)
|
||||||
|
self.secondDimension = Size.percent(value: 0.25)
|
||||||
|
case 2:
|
||||||
|
self.mainDimension = Size(c_dimension: config.dimensions[0]) ?? Size.percent(value: 0.25)
|
||||||
|
self.secondDimension = Size(c_dimension: config.dimensions[1]) ?? Size.percent(value: 0.25)
|
||||||
|
default:
|
||||||
|
self.mainDimension = Size.percent(value: 0.25)
|
||||||
|
self.secondDimension = Size.percent(value: 0.25)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the window size.
|
||||||
|
func apply(_ window: NSWindow, _ position: QuickTerminalPosition) {
|
||||||
|
guard let screen = window.screen ?? NSScreen.main else { return }
|
||||||
|
switch (position) {
|
||||||
|
case .top, .bottom:
|
||||||
|
window.setFrame(.init(
|
||||||
|
origin: window.frame.origin,
|
||||||
|
size: .init(
|
||||||
|
width: screen.frame.width,
|
||||||
|
height: self.mainDimension.apply(value: screen.frame.height))
|
||||||
|
), display: false)
|
||||||
|
|
||||||
|
case .left, .right:
|
||||||
|
window.setFrame(.init(
|
||||||
|
origin: window.frame.origin,
|
||||||
|
size: .init(
|
||||||
|
width: self.mainDimension.apply(value: screen.frame.width),
|
||||||
|
height: screen.frame.height)
|
||||||
|
), display: false)
|
||||||
|
|
||||||
|
case .center:
|
||||||
|
window.setFrame(.init(
|
||||||
|
origin: window.frame.origin,
|
||||||
|
size: .init(
|
||||||
|
width: self.mainDimension.apply(value: screen.frame.width),
|
||||||
|
height: self.secondDimension.apply(value: screen.frame.height))
|
||||||
|
), display: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -406,6 +406,14 @@ extension Ghostty {
|
|||||||
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var quickTerminalSize: QuickTerminalSize {
|
||||||
|
guard let config = self.config else { return .init() }
|
||||||
|
var v: ghostty_config_quick_terminal_size_s = .init()
|
||||||
|
let key = "quick-terminal-size"
|
||||||
|
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
||||||
|
return .init(config: v);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var resizeOverlay: ResizeOverlay {
|
var resizeOverlay: ResizeOverlay {
|
||||||
|
@ -1411,6 +1411,31 @@ keybind: Keybinds = .{},
|
|||||||
/// Set it to false for the quick terminal to remain open even when it loses focus.
|
/// Set it to false for the quick terminal to remain open even when it loses focus.
|
||||||
@"quick-terminal-autohide": bool = true,
|
@"quick-terminal-autohide": bool = true,
|
||||||
|
|
||||||
|
/// Control the size of the quick terminal.
|
||||||
|
///
|
||||||
|
/// The size can expressed in two units:
|
||||||
|
/// * A value ending in `%` specifies a percentage of the screen size.
|
||||||
|
/// * A value ending in `px` specifies a fixed size in pixel, which is clamped by the
|
||||||
|
/// screen's maximum width or height.
|
||||||
|
///
|
||||||
|
/// The configuration accept one or two dimensions:
|
||||||
|
/// * A single value specifies the dimension that is growable based on the quick terminal
|
||||||
|
/// position:
|
||||||
|
/// * For `top` and `bottom` positions, the value applies to the height.
|
||||||
|
/// * For `right` and `left` positions, the value applies to the width.
|
||||||
|
/// * For `center`, the size applied to both the width and height.
|
||||||
|
/// * Two comma separated value specifies the width and height.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// quick-terminal-size = 25% // 25% of the maximum size for the growable dimension
|
||||||
|
/// quick-terminal-size = 42px // 42 pixels for the growable dimension
|
||||||
|
/// quick-terminal-size = 25%,75% // 25% for the primary dimension, 75% for the secondary
|
||||||
|
/// quick-terminal-size = 300px,80% // 300px for the primary dimension, 80% for the secondary
|
||||||
|
/// ```
|
||||||
|
@"quick-terminal-size": ?QuickTerminalSize = null,
|
||||||
|
|
||||||
/// Whether to enable shell integration auto-injection or not. Shell integration
|
/// Whether to enable shell integration auto-injection or not. Shell integration
|
||||||
/// greatly enhances the terminal experience by enabling a number of features:
|
/// greatly enhances the terminal experience by enabling a number of features:
|
||||||
///
|
///
|
||||||
@ -5294,6 +5319,240 @@ pub const QuickTerminalScreen = enum {
|
|||||||
@"macos-menu-bar",
|
@"macos-menu-bar",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// See quick-terminal-size
|
||||||
|
pub const QuickTerminalSize = struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub const Size = union(UnitKey) {
|
||||||
|
// Absolute value in pixel.
|
||||||
|
pixel: u16,
|
||||||
|
|
||||||
|
// Percentage value relative to the screen size. Allowed value [0-100].
|
||||||
|
percent: u8,
|
||||||
|
|
||||||
|
// Sync with `ghostty_config_quick_terminal_unit_e`.
|
||||||
|
pub const UnitKey = enum(c_int) {
|
||||||
|
pixel,
|
||||||
|
percent,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const C = extern struct {
|
||||||
|
value: u16,
|
||||||
|
tag: UnitKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn cval(self: Size) Size.C {
|
||||||
|
return .{
|
||||||
|
.tag = @as(UnitKey, self),
|
||||||
|
.value = res: {
|
||||||
|
switch (self) {
|
||||||
|
.percent => |v| {
|
||||||
|
break :res v;
|
||||||
|
},
|
||||||
|
.pixel => |v| {
|
||||||
|
break :res @intCast(v);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseValue(input: []const u8) !Size {
|
||||||
|
if (input[input.len - 1] == '%') {
|
||||||
|
const v = std.fmt.parseInt(
|
||||||
|
u8,
|
||||||
|
input[0 .. input.len - 1],
|
||||||
|
10,
|
||||||
|
) catch return error.InvalidFormat;
|
||||||
|
|
||||||
|
// Percentage value must be between 0-100.
|
||||||
|
if (v > 100) return error.InvalidFormat;
|
||||||
|
|
||||||
|
return .{ .percent = v };
|
||||||
|
} else if (input[input.len - 2] == 'p' and input[input.len - 1] == 'x') {
|
||||||
|
const v = std.fmt.parseInt(
|
||||||
|
u16,
|
||||||
|
input[0 .. input.len - 2],
|
||||||
|
10,
|
||||||
|
) catch return error.InvalidFormat;
|
||||||
|
|
||||||
|
return .{ .pixel = v };
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.InvalidFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn formatBuf(self: Size, buf: []u8) Allocator.Error![]const u8 {
|
||||||
|
return res: {
|
||||||
|
switch (self) {
|
||||||
|
.percent => |v| {
|
||||||
|
break :res std.fmt.bufPrint(buf, "{d}%", .{v});
|
||||||
|
},
|
||||||
|
.pixel => |v| {
|
||||||
|
break :res std.fmt.bufPrint(buf, "{d}px", .{v});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} catch error.OutOfMemory;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn equal(self: Size, other: Size) bool {
|
||||||
|
return std.meta.eql(self, other);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dimensions: std.ArrayListUnmanaged(Size) = .{},
|
||||||
|
dimensions_c: std.ArrayListUnmanaged(Size.C) = .{},
|
||||||
|
|
||||||
|
/// Sync with `ghostty_config_quick_terminal_size_s`
|
||||||
|
pub const C = extern struct {
|
||||||
|
dimensions: [*]Size.C,
|
||||||
|
len: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Required by Config, for C bindings.
|
||||||
|
pub fn cval(self: *const Self) C {
|
||||||
|
return .{
|
||||||
|
.dimensions = self.dimensions_c.items.ptr,
|
||||||
|
.len = self.dimensions_c.items.len,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Required by Config.
|
||||||
|
pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self {
|
||||||
|
return .{
|
||||||
|
.dimensions = try self.dimensions.clone(alloc),
|
||||||
|
.dimensions_c = try self.dimensions_c.clone(alloc),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Required by Config.
|
||||||
|
pub fn equal(self: Self, other: Self) bool {
|
||||||
|
const itemsA = self.dimensions.items;
|
||||||
|
const itemsB = other.dimensions.items;
|
||||||
|
if (itemsA.len != itemsB.len) return false;
|
||||||
|
for (itemsA, itemsB) |a, b| {
|
||||||
|
if (!a.equal(b)) return false;
|
||||||
|
} else return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Required by Config.
|
||||||
|
pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void {
|
||||||
|
const value = input orelse return error.ValueRequired;
|
||||||
|
if (value.len == 0) return error.InvalidFormat;
|
||||||
|
|
||||||
|
self.* = .{};
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
var it = std.mem.tokenizeScalar(u8, value, ',');
|
||||||
|
while (it.next()) |part| {
|
||||||
|
i += 1;
|
||||||
|
if (i > 2) return error.InvalidValue;
|
||||||
|
|
||||||
|
const parsed_value = try Size.parseValue(part);
|
||||||
|
try self.dimensions.append(alloc, parsed_value);
|
||||||
|
try self.dimensions_c.append(alloc, parsed_value.cval());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.dimensions.items.len == 0) return error.InvalidValue;
|
||||||
|
assert(self.dimensions.items.len == self.dimensions_c.items.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Required by Config, use for config formatted.
|
||||||
|
pub fn formatEntry(self: Self, formatter: anytype) !void {
|
||||||
|
var buf: [1024]u8 = undefined;
|
||||||
|
var fbs = std.io.fixedBufferStream(&buf);
|
||||||
|
var writer = fbs.writer();
|
||||||
|
|
||||||
|
for (self.dimensions.items, 0..) |dim, i| {
|
||||||
|
var dim_buf: [128]u8 = undefined;
|
||||||
|
const dim_str = try dim.formatBuf(&dim_buf);
|
||||||
|
if (i != 0) writer.writeByte(',') catch return error.OutOfMemory;
|
||||||
|
writer.writeAll(dim_str) catch return error.OutOfMemory;
|
||||||
|
}
|
||||||
|
|
||||||
|
try formatter.formatEntry([]const u8, fbs.getWritten());
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseCLI" {
|
||||||
|
const testing = std.testing;
|
||||||
|
var arena = ArenaAllocator.init(testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
|
||||||
|
const alloc = arena.allocator();
|
||||||
|
|
||||||
|
var p: Self = .{};
|
||||||
|
|
||||||
|
{
|
||||||
|
var expected_dimensions: [1]Size = .{.{ .pixel = 42 }};
|
||||||
|
|
||||||
|
try p.parseCLI(alloc, "42px");
|
||||||
|
const parsed_dimensions = try p.dimensions.toOwnedSlice(alloc);
|
||||||
|
try testing.expectEqualSlices(Size, &expected_dimensions, parsed_dimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var expected_dimensions = [1]Size{.{ .percent = 15 }};
|
||||||
|
|
||||||
|
try p.parseCLI(alloc, "15%");
|
||||||
|
const parsed_dimensions = try p.dimensions.toOwnedSlice(alloc);
|
||||||
|
try testing.expectEqualSlices(Size, &expected_dimensions, parsed_dimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var expected_dimensions = [_]Size{ .{ .pixel = 4096 }, .{ .percent = 23 } };
|
||||||
|
|
||||||
|
try p.parseCLI(alloc, "4096px,23%");
|
||||||
|
const parsed_dimensions = try p.dimensions.toOwnedSlice(alloc);
|
||||||
|
try testing.expectEqualSlices(Size, &expected_dimensions, parsed_dimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var expected_dimensions = [2]Size{ .{ .percent = 78 }, .{ .pixel = 75 } };
|
||||||
|
|
||||||
|
try p.parseCLI(alloc, "78%,75px");
|
||||||
|
const parsed_dimensions = try p.dimensions.toOwnedSlice(alloc);
|
||||||
|
try testing.expectEqualSlices(Size, &expected_dimensions, parsed_dimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
try testing.expectError(error.InvalidFormat, p.parseCLI(alloc, ""));
|
||||||
|
try testing.expectError(error.InvalidFormat, p.parseCLI(alloc, "29"));
|
||||||
|
try testing.expectError(error.InvalidFormat, p.parseCLI(alloc, "120%"));
|
||||||
|
try testing.expectError(error.InvalidFormat, p.parseCLI(alloc, "65px,12"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatEntry on one-dimension size" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var buf = std.ArrayList(u8).init(testing.allocator);
|
||||||
|
defer buf.deinit();
|
||||||
|
|
||||||
|
var arena = ArenaAllocator.init(testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const alloc = arena.allocator();
|
||||||
|
|
||||||
|
var p: Self = .{};
|
||||||
|
try p.parseCLI(alloc, "1024px");
|
||||||
|
try p.formatEntry(formatterpkg.entryFormatter("v", buf.writer()));
|
||||||
|
try testing.expectEqualSlices(u8, "v = 1024px\n", buf.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "formatEntry on two-dimension size" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var buf = std.ArrayList(u8).init(testing.allocator);
|
||||||
|
defer buf.deinit();
|
||||||
|
|
||||||
|
var arena = ArenaAllocator.init(testing.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const alloc = arena.allocator();
|
||||||
|
|
||||||
|
var p: Self = .{};
|
||||||
|
try p.parseCLI(alloc, "1024px,80%");
|
||||||
|
try p.formatEntry(formatterpkg.entryFormatter("v", buf.writer()));
|
||||||
|
try testing.expectEqualSlices(u8, "v = 1024px,80%\n", buf.items);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/// See grapheme-width-method
|
/// See grapheme-width-method
|
||||||
pub const GraphemeWidthMethod = enum {
|
pub const GraphemeWidthMethod = enum {
|
||||||
legacy,
|
legacy,
|
||||||
|
Reference in New Issue
Block a user