mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge branch 'ghostty-org:main' into macos-window-deocrations-rework
This commit is contained in:
@ -291,7 +291,7 @@ typedef struct {
|
|||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char* message;
|
const char* message;
|
||||||
} ghostty_error_s;
|
} ghostty_diagnostic_s;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
double tl_px_x;
|
double tl_px_x;
|
||||||
@ -607,7 +607,6 @@ ghostty_info_s ghostty_info(void);
|
|||||||
ghostty_config_t ghostty_config_new();
|
ghostty_config_t ghostty_config_new();
|
||||||
void ghostty_config_free(ghostty_config_t);
|
void ghostty_config_free(ghostty_config_t);
|
||||||
void ghostty_config_load_cli_args(ghostty_config_t);
|
void ghostty_config_load_cli_args(ghostty_config_t);
|
||||||
void ghostty_config_load_string(ghostty_config_t, const char*, uintptr_t);
|
|
||||||
void ghostty_config_load_default_files(ghostty_config_t);
|
void ghostty_config_load_default_files(ghostty_config_t);
|
||||||
void ghostty_config_load_recursive_files(ghostty_config_t);
|
void ghostty_config_load_recursive_files(ghostty_config_t);
|
||||||
void ghostty_config_finalize(ghostty_config_t);
|
void ghostty_config_finalize(ghostty_config_t);
|
||||||
@ -615,8 +614,8 @@ bool ghostty_config_get(ghostty_config_t, void*, const char*, uintptr_t);
|
|||||||
ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t,
|
ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t,
|
||||||
const char*,
|
const char*,
|
||||||
uintptr_t);
|
uintptr_t);
|
||||||
uint32_t ghostty_config_errors_count(ghostty_config_t);
|
uint32_t ghostty_config_diagnostics_count(ghostty_config_t);
|
||||||
ghostty_error_s ghostty_config_get_error(ghostty_config_t, uint32_t);
|
ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t);
|
||||||
void ghostty_config_open();
|
void ghostty_config_open();
|
||||||
|
|
||||||
ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
|
ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
|
||||||
|
@ -61,6 +61,7 @@
|
|||||||
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */; };
|
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */; };
|
||||||
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; };
|
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; };
|
||||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
|
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
|
||||||
|
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; };
|
||||||
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||||
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; };
|
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; };
|
||||||
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0572C9F30860017A1AE /* Cursor.swift */; };
|
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0572C9F30860017A1AE /* Cursor.swift */; };
|
||||||
@ -139,6 +140,7 @@
|
|||||||
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = "<group>"; };
|
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = "<group>"; };
|
||||||
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = "<group>"; };
|
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = "<group>"; };
|
||||||
A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = "<group>"; };
|
A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = "<group>"; };
|
||||||
|
A5A6F7292CC41B8700B232A5 /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = "<group>"; };
|
||||||
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
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>"; };
|
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
|
||||||
@ -233,6 +235,7 @@
|
|||||||
A534263D2A7DCBB000EBB7A2 /* Helpers */ = {
|
A534263D2A7DCBB000EBB7A2 /* Helpers */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A5A6F7292CC41B8700B232A5 /* Xcode.swift */,
|
||||||
A5CEAFFE29C2410700646FDA /* Backport.swift */,
|
A5CEAFFE29C2410700646FDA /* Backport.swift */,
|
||||||
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
|
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
|
||||||
A5CBD0572C9F30860017A1AE /* Cursor.swift */,
|
A5CBD0572C9F30860017A1AE /* Cursor.swift */,
|
||||||
@ -582,6 +585,7 @@
|
|||||||
A52FFF5D2CAB4D08000C6A5B /* NSScreen+Extension.swift in Sources */,
|
A52FFF5D2CAB4D08000C6A5B /* NSScreen+Extension.swift in Sources */,
|
||||||
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */,
|
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */,
|
||||||
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */,
|
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */,
|
||||||
|
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */,
|
||||||
A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */,
|
A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */,
|
||||||
A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */,
|
A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */,
|
||||||
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */,
|
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */,
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||||
<window allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" titlebarAppearsTransparent="YES" id="QvC-M9-y7g">
|
<window allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" titlebarAppearsTransparent="YES" id="QvC-M9-y7g">
|
||||||
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
|
<windowStyleMask key="styleMask" titled="YES" closable="YES" fullSizeContentView="YES"/>
|
||||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||||
<rect key="contentRect" x="196" y="240" width="300" height="172"/>
|
<rect key="contentRect" x="196" y="240" width="300" height="172"/>
|
||||||
<rect key="screenRect" x="0.0" y="0.0" width="3008" height="1667"/>
|
<rect key="screenRect" x="0.0" y="0.0" width="3008" height="1667"/>
|
||||||
|
@ -10,6 +10,7 @@ class AboutController: NSWindowController, NSWindowDelegate {
|
|||||||
override func windowDidLoad() {
|
override func windowDidLoad() {
|
||||||
guard let window = window else { return }
|
guard let window = window else { return }
|
||||||
window.center()
|
window.center()
|
||||||
|
window.isMovableByWindowBackground = true
|
||||||
window.contentView = NSHostingView(rootView: AboutView())
|
window.contentView = NSHostingView(rootView: AboutView())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,35 +1,136 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct AboutView: View {
|
struct AboutView: View {
|
||||||
|
@Environment(\.openURL) var openURL
|
||||||
|
|
||||||
|
private let githubLink = URL(string: "https://github.com/ghostty-org/ghostty")
|
||||||
|
|
||||||
/// Read the commit from the bundle.
|
/// Read the commit from the bundle.
|
||||||
var build: String? { Bundle.main.infoDictionary?["CFBundleVersion"] as? String }
|
private var build: String? { Bundle.main.infoDictionary?["CFBundleVersion"] as? String }
|
||||||
var commit: String? { Bundle.main.infoDictionary?["GhosttyCommit"] as? String }
|
private var commit: String? { Bundle.main.infoDictionary?["GhosttyCommit"] as? String }
|
||||||
var version: String? { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String }
|
private var version: String? { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String }
|
||||||
|
private var copyright: String? { Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String }
|
||||||
|
|
||||||
|
private var properties: [KeyValue<String>] {
|
||||||
|
let list: [KeyValue<String?>] = [
|
||||||
|
.init(key: "Version", value: version),
|
||||||
|
.init(key: "Build", value: build),
|
||||||
|
.init(key: "Commit", value: commit == "" ? nil : commit)
|
||||||
|
]
|
||||||
|
|
||||||
|
return list.compactMap {
|
||||||
|
guard let value = $0.value else { return nil }
|
||||||
|
return .init(key: $0.key, value: value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct KeyValue<Value: Equatable>: Identifiable {
|
||||||
|
var id = UUID()
|
||||||
|
public let key: LocalizedStringResource
|
||||||
|
public let value: Value
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
// This creates a background style similar to the Apple "About My Mac" Window
|
||||||
|
private struct VisualEffectBackground: NSViewRepresentable {
|
||||||
|
let material: NSVisualEffectView.Material
|
||||||
|
let blendingMode: NSVisualEffectView.BlendingMode
|
||||||
|
let isEmphasized: Bool
|
||||||
|
|
||||||
|
init(material: NSVisualEffectView.Material,
|
||||||
|
blendingMode: NSVisualEffectView.BlendingMode = .behindWindow,
|
||||||
|
isEmphasized: Bool = false)
|
||||||
|
{
|
||||||
|
self.material = material
|
||||||
|
self.blendingMode = blendingMode
|
||||||
|
self.isEmphasized = isEmphasized
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
|
||||||
|
nsView.material = material
|
||||||
|
nsView.blendingMode = blendingMode
|
||||||
|
nsView.isEmphasized = isEmphasized
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeNSView(context: Context) -> NSVisualEffectView {
|
||||||
|
let visualEffect = NSVisualEffectView()
|
||||||
|
visualEffect.autoresizingMask = [.width, .height]
|
||||||
|
return visualEffect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
Image("AppIconImage")
|
Image("AppIconImage")
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(maxHeight: 96)
|
.frame(height: 128)
|
||||||
|
|
||||||
Text("Ghostty")
|
VStack(alignment: .center, spacing: 32) {
|
||||||
.font(.title3)
|
VStack(alignment: .center, spacing: 8) {
|
||||||
|
Text("Ghostty")
|
||||||
|
.bold()
|
||||||
|
.font(.title)
|
||||||
|
Text("Fast, native, feature-rich terminal \nemulator pushing modern features.")
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.font(.caption)
|
||||||
|
.tint(.secondary)
|
||||||
|
.opacity(0.8)
|
||||||
|
}
|
||||||
.textSelection(.enabled)
|
.textSelection(.enabled)
|
||||||
|
VStack(spacing: 2) {
|
||||||
|
ForEach(properties) { item in
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Text(item.key)
|
||||||
|
.frame(width: 126, alignment: .trailing)
|
||||||
|
.padding(.trailing, 2)
|
||||||
|
Text(item.value)
|
||||||
|
.frame(width: 125, alignment: .leading)
|
||||||
|
.padding(.leading, 2)
|
||||||
|
.tint(.secondary)
|
||||||
|
.opacity(0.8)
|
||||||
|
}
|
||||||
|
.font(.callout)
|
||||||
|
.textSelection(.enabled)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
if let version = self.version {
|
HStack(spacing: 8) {
|
||||||
Text("Version: \(version)")
|
if let url = githubLink {
|
||||||
.font(.body)
|
Button("GitHub") {
|
||||||
.textSelection(.enabled)
|
openURL(url)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let build = self.build {
|
}
|
||||||
Text("Build: \(build)")
|
|
||||||
.font(.body)
|
if let copy = self.copyright {
|
||||||
.textSelection(.enabled)
|
Text(copy)
|
||||||
|
.font(.caption)
|
||||||
|
.textSelection(.enabled)
|
||||||
|
.tint(.secondary)
|
||||||
|
.opacity(0.8)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
.frame(minWidth: 300)
|
.padding(.top, 8)
|
||||||
.padding()
|
.padding(32)
|
||||||
|
.frame(minWidth: 256)
|
||||||
|
#if os(macOS)
|
||||||
|
.background(VisualEffectBackground(material: .underWindowBackground).ignoresSafeArea())
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AboutView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
AboutView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,14 @@ class BaseTerminalController: NSWindowController,
|
|||||||
/// Event monitor (see individual events for why)
|
/// Event monitor (see individual events for why)
|
||||||
private var eventMonitor: Any? = nil
|
private var eventMonitor: Any? = nil
|
||||||
|
|
||||||
|
/// The previous frame information from the window
|
||||||
|
private var savedFrame: SavedFrame? = nil
|
||||||
|
|
||||||
|
struct SavedFrame {
|
||||||
|
let window: NSRect
|
||||||
|
let screen: NSRect
|
||||||
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) is not supported for this view")
|
fatalError("init(coder:) is not supported for this view")
|
||||||
}
|
}
|
||||||
@ -80,6 +88,11 @@ class BaseTerminalController: NSWindowController,
|
|||||||
selector: #selector(onConfirmClipboardRequest),
|
selector: #selector(onConfirmClipboardRequest),
|
||||||
name: Ghostty.Notification.confirmClipboard,
|
name: Ghostty.Notification.confirmClipboard,
|
||||||
object: nil)
|
object: nil)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(didChangeScreenParametersNotification),
|
||||||
|
name: NSApplication.didChangeScreenParametersNotification,
|
||||||
|
object: nil)
|
||||||
|
|
||||||
// Listen for local events that we need to know of outside of
|
// Listen for local events that we need to know of outside of
|
||||||
// single surface handlers.
|
// single surface handlers.
|
||||||
@ -89,6 +102,8 @@ class BaseTerminalController: NSWindowController,
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
|
||||||
if let eventMonitor {
|
if let eventMonitor {
|
||||||
NSEvent.removeMonitor(eventMonitor)
|
NSEvent.removeMonitor(eventMonitor)
|
||||||
}
|
}
|
||||||
@ -121,6 +136,57 @@ class BaseTerminalController: NSWindowController,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call this whenever the frame changes
|
||||||
|
private func windowFrameDidChange() {
|
||||||
|
// We need to update our saved frame information in case of monitor
|
||||||
|
// changes (see didChangeScreenParameters notification).
|
||||||
|
savedFrame = nil
|
||||||
|
guard let window, let screen = window.screen else { return }
|
||||||
|
savedFrame = .init(window: window.frame, screen: screen.visibleFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Notifications
|
||||||
|
|
||||||
|
@objc private func didChangeScreenParametersNotification(_ notification: Notification) {
|
||||||
|
// If we have a window that is visible and it is outside the bounds of the
|
||||||
|
// screen then we clamp it back to within the screen.
|
||||||
|
guard let window else { return }
|
||||||
|
guard window.isVisible else { return }
|
||||||
|
guard let screen = window.screen else { return }
|
||||||
|
|
||||||
|
let visibleFrame = screen.visibleFrame
|
||||||
|
var newFrame = window.frame
|
||||||
|
|
||||||
|
// Clamp width/height
|
||||||
|
if newFrame.size.width > visibleFrame.size.width {
|
||||||
|
newFrame.size.width = visibleFrame.size.width
|
||||||
|
}
|
||||||
|
if newFrame.size.height > visibleFrame.size.height {
|
||||||
|
newFrame.size.height = visibleFrame.size.height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the window is on-screen. We only do this if the previous frame
|
||||||
|
// was also on screen. If a user explicitly wanted their window off screen
|
||||||
|
// then we let it stay that way.
|
||||||
|
x: if newFrame.origin.x < visibleFrame.origin.x {
|
||||||
|
if let savedFrame, savedFrame.window.origin.x < savedFrame.screen.origin.x {
|
||||||
|
break x;
|
||||||
|
}
|
||||||
|
|
||||||
|
newFrame.origin.x = visibleFrame.origin.x
|
||||||
|
}
|
||||||
|
y: if newFrame.origin.y < visibleFrame.origin.y {
|
||||||
|
if let savedFrame, savedFrame.window.origin.y < savedFrame.screen.origin.y {
|
||||||
|
break y;
|
||||||
|
}
|
||||||
|
|
||||||
|
newFrame.origin.y = visibleFrame.origin.y
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the new window frame
|
||||||
|
window.setFrame(newFrame, display: true)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Local Events
|
// MARK: Local Events
|
||||||
|
|
||||||
private func localEventHandler(_ event: NSEvent) -> NSEvent? {
|
private func localEventHandler(_ event: NSEvent) -> NSEvent? {
|
||||||
@ -371,6 +437,14 @@ class BaseTerminalController: NSWindowController,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func windowDidResize(_ notification: Notification) {
|
||||||
|
windowFrameDidChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowDidMove(_ notification: Notification) {
|
||||||
|
windowFrameDidChange()
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: First Responder
|
// MARK: First Responder
|
||||||
|
|
||||||
@IBAction func close(_ sender: Any) {
|
@IBAction func close(_ sender: Any) {
|
||||||
|
@ -365,7 +365,8 @@ class TerminalController: BaseTerminalController {
|
|||||||
self.fixTabBar()
|
self.fixTabBar()
|
||||||
}
|
}
|
||||||
|
|
||||||
func windowDidMove(_ notification: Notification) {
|
override func windowDidMove(_ notification: Notification) {
|
||||||
|
super.windowDidMove(notification)
|
||||||
self.fixTabBar()
|
self.fixTabBar()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,15 +22,15 @@ extension Ghostty {
|
|||||||
var errors: [String] {
|
var errors: [String] {
|
||||||
guard let cfg = self.config else { return [] }
|
guard let cfg = self.config else { return [] }
|
||||||
|
|
||||||
var errors: [String] = [];
|
var diags: [String] = [];
|
||||||
let errCount = ghostty_config_errors_count(cfg)
|
let diagsCount = ghostty_config_diagnostics_count(cfg)
|
||||||
for i in 0..<errCount {
|
for i in 0..<diagsCount {
|
||||||
let err = ghostty_config_get_error(cfg, UInt32(i))
|
let diag = ghostty_config_get_diagnostic(cfg, UInt32(i))
|
||||||
let message = String(cString: err.message)
|
let message = String(cString: diag.message)
|
||||||
errors.append(message)
|
diags.append(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@ -56,7 +56,13 @@ extension Ghostty {
|
|||||||
// same filesystem concept.
|
// same filesystem concept.
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
ghostty_config_load_default_files(cfg);
|
ghostty_config_load_default_files(cfg);
|
||||||
ghostty_config_load_cli_args(cfg);
|
|
||||||
|
// We only load CLI args when not running in Xcode because in Xcode we
|
||||||
|
// pass some special parameters to control the debugger.
|
||||||
|
if !isRunningInXcode() {
|
||||||
|
ghostty_config_load_cli_args(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
ghostty_config_load_recursive_files(cfg);
|
ghostty_config_load_recursive_files(cfg);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -69,14 +75,14 @@ extension Ghostty {
|
|||||||
|
|
||||||
// Log any configuration errors. These will be automatically shown in a
|
// Log any configuration errors. These will be automatically shown in a
|
||||||
// pop-up window too.
|
// pop-up window too.
|
||||||
let errCount = ghostty_config_errors_count(cfg)
|
let diagsCount = ghostty_config_diagnostics_count(cfg)
|
||||||
if errCount > 0 {
|
if diagsCount > 0 {
|
||||||
logger.warning("config error: \(errCount) configuration errors on reload")
|
logger.warning("config error: \(diagsCount) configuration errors on reload")
|
||||||
var errors: [String] = [];
|
var diags: [String] = [];
|
||||||
for i in 0..<errCount {
|
for i in 0..<diagsCount {
|
||||||
let err = ghostty_config_get_error(cfg, UInt32(i))
|
let diag = ghostty_config_get_diagnostic(cfg, UInt32(i))
|
||||||
let message = String(cString: err.message)
|
let message = String(cString: diag.message)
|
||||||
errors.append(message)
|
diags.append(message)
|
||||||
logger.warning("config error: \(message)")
|
logger.warning("config error: \(message)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
macos/Sources/Helpers/Xcode.swift
Normal file
10
macos/Sources/Helpers/Xcode.swift
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// True if we appear to be running in Xcode.
|
||||||
|
func isRunningInXcode() -> Bool {
|
||||||
|
if let _ = ProcessInfo.processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
36
src/App.zig
36
src/App.zig
@ -66,6 +66,8 @@ font_grid_set: font.SharedGridSet,
|
|||||||
last_notification_time: ?std.time.Instant = null,
|
last_notification_time: ?std.time.Instant = null,
|
||||||
last_notification_digest: u64 = 0,
|
last_notification_digest: u64 = 0,
|
||||||
|
|
||||||
|
pub const CreateError = Allocator.Error || font.SharedGridSet.InitError;
|
||||||
|
|
||||||
/// Initialize the main app instance. This creates the main window, sets
|
/// Initialize the main app instance. This creates the main window, sets
|
||||||
/// up the renderer state, compiles the shaders, etc. This is the primary
|
/// up the renderer state, compiles the shaders, etc. This is the primary
|
||||||
/// "startup" logic.
|
/// "startup" logic.
|
||||||
@ -74,7 +76,7 @@ last_notification_digest: u64 = 0,
|
|||||||
/// `focusEvent` to set the initial focus state of the app.
|
/// `focusEvent` to set the initial focus state of the app.
|
||||||
pub fn create(
|
pub fn create(
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
) !*App {
|
) CreateError!*App {
|
||||||
var app = try alloc.create(App);
|
var app = try alloc.create(App);
|
||||||
errdefer alloc.destroy(app);
|
errdefer alloc.destroy(app);
|
||||||
|
|
||||||
@ -150,7 +152,10 @@ pub fn updateConfig(self: *App, config: *const Config) !void {
|
|||||||
/// Add an initialized surface. This is really only for the runtime
|
/// Add an initialized surface. This is really only for the runtime
|
||||||
/// implementations to call and should NOT be called by general app users.
|
/// implementations to call and should NOT be called by general app users.
|
||||||
/// The surface must be from the pool.
|
/// The surface must be from the pool.
|
||||||
pub fn addSurface(self: *App, rt_surface: *apprt.Surface) !void {
|
pub fn addSurface(
|
||||||
|
self: *App,
|
||||||
|
rt_surface: *apprt.Surface,
|
||||||
|
) Allocator.Error!void {
|
||||||
try self.surfaces.append(self.alloc, rt_surface);
|
try self.surfaces.append(self.alloc, rt_surface);
|
||||||
|
|
||||||
// Since we have non-zero surfaces, we can cancel the quit timer.
|
// Since we have non-zero surfaces, we can cancel the quit timer.
|
||||||
@ -225,11 +230,20 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
|||||||
.reload_config => try self.reloadConfig(rt_app),
|
.reload_config => try self.reloadConfig(rt_app),
|
||||||
.open_config => try self.performAction(rt_app, .open_config),
|
.open_config => try self.performAction(rt_app, .open_config),
|
||||||
.new_window => |msg| try self.newWindow(rt_app, msg),
|
.new_window => |msg| try self.newWindow(rt_app, msg),
|
||||||
.close => |surface| try self.closeSurface(surface),
|
.close => |surface| self.closeSurface(surface),
|
||||||
.quit => try self.setQuit(),
|
|
||||||
.surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message),
|
.surface_message => |msg| try self.surfaceMessage(msg.surface, msg.message),
|
||||||
.redraw_surface => |surface| try self.redrawSurface(rt_app, surface),
|
.redraw_surface => |surface| self.redrawSurface(rt_app, surface),
|
||||||
.redraw_inspector => |surface| try self.redrawInspector(rt_app, surface),
|
.redraw_inspector => |surface| self.redrawInspector(rt_app, surface),
|
||||||
|
|
||||||
|
// If we're quitting, then we set the quit flag and stop
|
||||||
|
// draining the mailbox immediately. This lets us defer
|
||||||
|
// mailbox processing to the next tick so that the apprt
|
||||||
|
// can try to quit as quickly as possible.
|
||||||
|
.quit => {
|
||||||
|
log.info("quit message received, short circuiting mailbox drain", .{});
|
||||||
|
self.setQuit();
|
||||||
|
return;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -242,7 +256,7 @@ pub fn reloadConfig(self: *App, rt_app: *apprt.App) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn closeSurface(self: *App, surface: *Surface) !void {
|
pub fn closeSurface(self: *App, surface: *Surface) void {
|
||||||
if (!self.hasSurface(surface)) return;
|
if (!self.hasSurface(surface)) return;
|
||||||
surface.close();
|
surface.close();
|
||||||
}
|
}
|
||||||
@ -252,12 +266,12 @@ pub fn focusSurface(self: *App, surface: *Surface) void {
|
|||||||
self.focused_surface = surface;
|
self.focused_surface = surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn redrawSurface(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) !void {
|
fn redrawSurface(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) void {
|
||||||
if (!self.hasSurface(&surface.core_surface)) return;
|
if (!self.hasSurface(&surface.core_surface)) return;
|
||||||
rt_app.redrawSurface(surface);
|
rt_app.redrawSurface(surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn redrawInspector(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) !void {
|
fn redrawInspector(self: *App, rt_app: *apprt.App, surface: *apprt.Surface) void {
|
||||||
if (!self.hasSurface(&surface.core_surface)) return;
|
if (!self.hasSurface(&surface.core_surface)) return;
|
||||||
rt_app.redrawInspector(surface);
|
rt_app.redrawInspector(surface);
|
||||||
}
|
}
|
||||||
@ -278,7 +292,7 @@ pub fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Start quitting
|
/// Start quitting
|
||||||
pub fn setQuit(self: *App) !void {
|
pub fn setQuit(self: *App) void {
|
||||||
if (self.quit) return;
|
if (self.quit) return;
|
||||||
self.quit = true;
|
self.quit = true;
|
||||||
}
|
}
|
||||||
@ -373,7 +387,7 @@ pub fn performAction(
|
|||||||
switch (action) {
|
switch (action) {
|
||||||
.unbind => unreachable,
|
.unbind => unreachable,
|
||||||
.ignore => {},
|
.ignore => {},
|
||||||
.quit => try self.setQuit(),
|
.quit => self.setQuit(),
|
||||||
.new_window => try self.newWindow(rt_app, .{ .parent = null }),
|
.new_window => try self.newWindow(rt_app, .{ .parent = null }),
|
||||||
.open_config => try rt_app.performAction(.app, .open_config, {}),
|
.open_config => try rt_app.performAction(.app, .open_config, {}),
|
||||||
.reload_config => try self.reloadConfig(rt_app),
|
.reload_config => try self.reloadConfig(rt_app),
|
||||||
|
@ -500,6 +500,7 @@ pub fn init(
|
|||||||
|
|
||||||
try termio.Termio.init(&self.io, alloc, .{
|
try termio.Termio.init(&self.io, alloc, .{
|
||||||
.grid_size = grid_size,
|
.grid_size = grid_size,
|
||||||
|
.cell_size = cell_size,
|
||||||
.screen_size = screen_size,
|
.screen_size = screen_size,
|
||||||
.padding = padding,
|
.padding = padding,
|
||||||
.full_config = config,
|
.full_config = config,
|
||||||
@ -1331,6 +1332,7 @@ fn setCellSize(self: *Surface, size: renderer.CellSize) !void {
|
|||||||
self.io.queueMessage(.{
|
self.io.queueMessage(.{
|
||||||
.resize = .{
|
.resize = .{
|
||||||
.grid_size = self.grid_size,
|
.grid_size = self.grid_size,
|
||||||
|
.cell_size = self.cell_size,
|
||||||
.screen_size = self.screen_size,
|
.screen_size = self.screen_size,
|
||||||
.padding = self.padding,
|
.padding = self.padding,
|
||||||
},
|
},
|
||||||
@ -1435,6 +1437,7 @@ fn resize(self: *Surface, size: renderer.ScreenSize) !void {
|
|||||||
self.io.queueMessage(.{
|
self.io.queueMessage(.{
|
||||||
.resize = .{
|
.resize = .{
|
||||||
.grid_size = self.grid_size,
|
.grid_size = self.grid_size,
|
||||||
|
.cell_size = self.cell_size,
|
||||||
.screen_size = self.screen_size,
|
.screen_size = self.screen_size,
|
||||||
.padding = self.padding,
|
.padding = self.padding,
|
||||||
},
|
},
|
||||||
@ -4011,7 +4014,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||||||
|
|
||||||
.close_surface => self.close(),
|
.close_surface => self.close(),
|
||||||
|
|
||||||
.close_window => try self.app.closeSurface(self),
|
.close_window => self.app.closeSurface(self),
|
||||||
|
|
||||||
.crash => |location| switch (location) {
|
.crash => |location| switch (location) {
|
||||||
.main => @panic("crash binding action, crashing intentionally"),
|
.main => @panic("crash binding action, crashing intentionally"),
|
||||||
|
@ -11,6 +11,7 @@ const Allocator = std.mem.Allocator;
|
|||||||
const glfw = @import("glfw");
|
const glfw = @import("glfw");
|
||||||
const macos = @import("macos");
|
const macos = @import("macos");
|
||||||
const objc = @import("objc");
|
const objc = @import("objc");
|
||||||
|
const cli = @import("../cli.zig");
|
||||||
const input = @import("../input.zig");
|
const input = @import("../input.zig");
|
||||||
const internal_os = @import("../os/main.zig");
|
const internal_os = @import("../os/main.zig");
|
||||||
const renderer = @import("../renderer.zig");
|
const renderer = @import("../renderer.zig");
|
||||||
@ -69,13 +70,26 @@ pub const App = struct {
|
|||||||
errdefer config.deinit();
|
errdefer config.deinit();
|
||||||
|
|
||||||
// If we had configuration errors, then log them.
|
// If we had configuration errors, then log them.
|
||||||
if (!config._errors.empty()) {
|
if (!config._diagnostics.empty()) {
|
||||||
for (config._errors.list.items) |err| {
|
var buf = std.ArrayList(u8).init(core_app.alloc);
|
||||||
log.warn("configuration error: {s}", .{err.message});
|
defer buf.deinit();
|
||||||
|
for (config._diagnostics.items()) |diag| {
|
||||||
|
try diag.write(buf.writer());
|
||||||
|
log.warn("configuration error: {s}", .{buf.items});
|
||||||
|
buf.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have any CLI errors, exit.
|
||||||
|
if (config._diagnostics.containsLocation(.cli)) {
|
||||||
|
log.warn("CLI errors detected, exiting", .{});
|
||||||
|
_ = core_app.mailbox.push(.{
|
||||||
|
.quit = {},
|
||||||
|
}, .{ .forever = {} });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue a single new window that starts on launch
|
// Queue a single new window that starts on launch
|
||||||
|
// Note: above we may send a quit so this may never happen
|
||||||
_ = core_app.mailbox.push(.{
|
_ = core_app.mailbox.push(.{
|
||||||
.new_window = .{},
|
.new_window = .{},
|
||||||
}, .{ .forever = {} });
|
}, .{ .forever = {} });
|
||||||
|
@ -123,9 +123,19 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|||||||
errdefer config.deinit();
|
errdefer config.deinit();
|
||||||
|
|
||||||
// If we had configuration errors, then log them.
|
// If we had configuration errors, then log them.
|
||||||
if (!config._errors.empty()) {
|
if (!config._diagnostics.empty()) {
|
||||||
for (config._errors.list.items) |err| {
|
var buf = std.ArrayList(u8).init(core_app.alloc);
|
||||||
log.warn("configuration error: {s}", .{err.message});
|
defer buf.deinit();
|
||||||
|
for (config._diagnostics.items()) |diag| {
|
||||||
|
try diag.write(buf.writer());
|
||||||
|
log.warn("configuration error: {s}", .{buf.items});
|
||||||
|
buf.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have any CLI errors, exit.
|
||||||
|
if (config._diagnostics.containsLocation(.cli)) {
|
||||||
|
log.warn("CLI errors detected, exiting", .{});
|
||||||
|
std.posix.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -815,7 +825,7 @@ fn syncConfigChanges(self: *App) !void {
|
|||||||
/// there are new configuration errors and hide the window if the errors
|
/// there are new configuration errors and hide the window if the errors
|
||||||
/// are resolved.
|
/// are resolved.
|
||||||
fn updateConfigErrors(self: *App) !void {
|
fn updateConfigErrors(self: *App) !void {
|
||||||
if (!self.config._errors.empty()) {
|
if (!self.config._diagnostics.empty()) {
|
||||||
if (self.config_errors_window == null) {
|
if (self.config_errors_window == null) {
|
||||||
try ConfigErrorsWindow.create(self);
|
try ConfigErrorsWindow.create(self);
|
||||||
assert(self.config_errors_window != null);
|
assert(self.config_errors_window != null);
|
||||||
@ -1364,10 +1374,7 @@ fn gtkActionQuit(
|
|||||||
ud: ?*anyopaque,
|
ud: ?*anyopaque,
|
||||||
) callconv(.C) void {
|
) callconv(.C) void {
|
||||||
const self: *App = @ptrCast(@alignCast(ud orelse return));
|
const self: *App = @ptrCast(@alignCast(ud orelse return));
|
||||||
self.core_app.setQuit() catch |err| {
|
self.core_app.setQuit();
|
||||||
log.warn("error setting quit err={}", .{err});
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Action sent by the window manager asking us to present a specific surface to
|
/// Action sent by the window manager asking us to present a specific surface to
|
||||||
|
@ -28,7 +28,7 @@ pub fn create(app: *App) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(self: *ConfigErrors) void {
|
pub fn update(self: *ConfigErrors) void {
|
||||||
if (self.app.config._errors.empty()) {
|
if (self.app.config._diagnostics.empty()) {
|
||||||
c.gtk_window_destroy(@ptrCast(self.window));
|
c.gtk_window_destroy(@ptrCast(self.window));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -130,8 +130,21 @@ const PrimaryView = struct {
|
|||||||
const buf = c.gtk_text_buffer_new(null);
|
const buf = c.gtk_text_buffer_new(null);
|
||||||
errdefer c.g_object_unref(buf);
|
errdefer c.g_object_unref(buf);
|
||||||
|
|
||||||
for (config._errors.list.items) |err| {
|
var msg_buf: [4096]u8 = undefined;
|
||||||
c.gtk_text_buffer_insert_at_cursor(buf, err.message, @intCast(err.message.len));
|
var fbs = std.io.fixedBufferStream(&msg_buf);
|
||||||
|
|
||||||
|
for (config._diagnostics.items()) |diag| {
|
||||||
|
fbs.reset();
|
||||||
|
diag.write(fbs.writer()) catch |err| {
|
||||||
|
log.warn(
|
||||||
|
"error writing diagnostic to buffer err={}",
|
||||||
|
.{err},
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const msg = fbs.getWritten();
|
||||||
|
c.gtk_text_buffer_insert_at_cursor(buf, msg.ptr, @intCast(msg.len));
|
||||||
c.gtk_text_buffer_insert_at_cursor(buf, "\n", -1);
|
c.gtk_text_buffer_insert_at_cursor(buf, "\n", -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
|
const diags = @import("cli/diagnostics.zig");
|
||||||
|
|
||||||
pub const args = @import("cli/args.zig");
|
pub const args = @import("cli/args.zig");
|
||||||
pub const Action = @import("cli/action.zig").Action;
|
pub const Action = @import("cli/action.zig").Action;
|
||||||
|
pub const DiagnosticList = diags.DiagnosticList;
|
||||||
|
pub const Diagnostic = diags.Diagnostic;
|
||||||
|
pub const Location = diags.Location;
|
||||||
|
|
||||||
test {
|
test {
|
||||||
@import("std").testing.refAllDecls(@This());
|
@import("std").testing.refAllDecls(@This());
|
||||||
|
367
src/cli/args.zig
367
src/cli/args.zig
@ -3,8 +3,10 @@ const mem = std.mem;
|
|||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Allocator = mem.Allocator;
|
const Allocator = mem.Allocator;
|
||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
const diags = @import("diagnostics.zig");
|
||||||
const ErrorList = @import("../config/ErrorList.zig");
|
const internal_os = @import("../os/main.zig");
|
||||||
|
const Diagnostic = diags.Diagnostic;
|
||||||
|
const DiagnosticList = diags.DiagnosticList;
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// - Only `--long=value` format is accepted. Do we want to allow
|
// - Only `--long=value` format is accepted. Do we want to allow
|
||||||
@ -32,13 +34,18 @@ pub const Error = error{
|
|||||||
/// an arena allocator will be created (or reused if set already) for any
|
/// an arena allocator will be created (or reused if set already) for any
|
||||||
/// allocations. Allocations are necessary for certain types, like `[]const u8`.
|
/// allocations. Allocations are necessary for certain types, like `[]const u8`.
|
||||||
///
|
///
|
||||||
/// If the destination type has a field "_errors" of type "ErrorList" then
|
/// If the destination type has a field "_diagnostics", it must be of type
|
||||||
/// errors will be added to that list. In this case, the only error returned by
|
/// "DiagnosticList" and any diagnostic messages will be added to that list.
|
||||||
/// parse are allocation errors.
|
/// When diagnostics are present, only allocation errors will be returned.
|
||||||
///
|
///
|
||||||
/// Note: If the arena is already non-null, then it will be used. In this
|
/// Note: If the arena is already non-null, then it will be used. In this
|
||||||
/// case, in the case of an error some memory might be leaked into the arena.
|
/// case, in the case of an error some memory might be leaked into the arena.
|
||||||
pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void {
|
pub fn parse(
|
||||||
|
comptime T: type,
|
||||||
|
alloc: Allocator,
|
||||||
|
dst: *T,
|
||||||
|
iter: anytype,
|
||||||
|
) !void {
|
||||||
const info = @typeInfo(T);
|
const info = @typeInfo(T);
|
||||||
assert(info == .Struct);
|
assert(info == .Struct);
|
||||||
|
|
||||||
@ -69,7 +76,11 @@ pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void {
|
|||||||
while (iter.next()) |arg| {
|
while (iter.next()) |arg| {
|
||||||
// Do manual parsing if we have a hook for it.
|
// Do manual parsing if we have a hook for it.
|
||||||
if (@hasDecl(T, "parseManuallyHook")) {
|
if (@hasDecl(T, "parseManuallyHook")) {
|
||||||
if (!try dst.parseManuallyHook(arena_alloc, arg, iter)) return;
|
if (!try dst.parseManuallyHook(
|
||||||
|
arena_alloc,
|
||||||
|
arg,
|
||||||
|
iter,
|
||||||
|
)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the destination supports help then we check for it, call
|
// If the destination supports help then we check for it, call
|
||||||
@ -83,69 +94,66 @@ pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mem.startsWith(u8, arg, "--")) {
|
// If this doesn't start with "--" then it isn't a config
|
||||||
var key: []const u8 = arg[2..];
|
// flag. We don't support positional arguments or configuration
|
||||||
const value: ?[]const u8 = value: {
|
// values set with spaces so this is an error.
|
||||||
// If the arg has "=" then the value is after the "=".
|
if (!mem.startsWith(u8, arg, "--")) {
|
||||||
if (mem.indexOf(u8, key, "=")) |idx| {
|
if (comptime !canTrackDiags(T)) return Error.InvalidField;
|
||||||
defer key = key[0..idx];
|
|
||||||
break :value key[idx + 1 ..];
|
|
||||||
}
|
|
||||||
|
|
||||||
break :value null;
|
// Add our diagnostic
|
||||||
};
|
try dst._diagnostics.append(arena_alloc, .{
|
||||||
|
.key = try arena_alloc.dupeZ(u8, arg),
|
||||||
|
.message = "invalid field",
|
||||||
|
.location = diags.Location.fromIter(iter),
|
||||||
|
});
|
||||||
|
|
||||||
parseIntoField(T, arena_alloc, dst, key, value) catch |err| {
|
continue;
|
||||||
if (comptime !canTrackErrors(T)) return err;
|
|
||||||
|
|
||||||
// The error set is dependent on comptime T, so we always add
|
|
||||||
// an extra error so we can have the "else" below.
|
|
||||||
const ErrSet = @TypeOf(err) || error{Unknown};
|
|
||||||
switch (@as(ErrSet, @errorCast(err))) {
|
|
||||||
// OOM is not recoverable since we need to allocate to
|
|
||||||
// track more error messages.
|
|
||||||
error.OutOfMemory => return err,
|
|
||||||
|
|
||||||
error.InvalidField => try dst._errors.add(arena_alloc, .{
|
|
||||||
.message = try std.fmt.allocPrintZ(
|
|
||||||
arena_alloc,
|
|
||||||
"{s}: unknown field",
|
|
||||||
.{key},
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
|
|
||||||
error.ValueRequired => try dst._errors.add(arena_alloc, .{
|
|
||||||
.message = try std.fmt.allocPrintZ(
|
|
||||||
arena_alloc,
|
|
||||||
"{s}: value required",
|
|
||||||
.{key},
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
|
|
||||||
error.InvalidValue => try dst._errors.add(arena_alloc, .{
|
|
||||||
.message = try std.fmt.allocPrintZ(
|
|
||||||
arena_alloc,
|
|
||||||
"{s}: invalid value",
|
|
||||||
.{key},
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
|
|
||||||
else => try dst._errors.add(arena_alloc, .{
|
|
||||||
.message = try std.fmt.allocPrintZ(
|
|
||||||
arena_alloc,
|
|
||||||
"{s}: unknown error {}",
|
|
||||||
.{ key, err },
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var key: []const u8 = arg[2..];
|
||||||
|
const value: ?[]const u8 = value: {
|
||||||
|
// If the arg has "=" then the value is after the "=".
|
||||||
|
if (mem.indexOf(u8, key, "=")) |idx| {
|
||||||
|
defer key = key[0..idx];
|
||||||
|
break :value key[idx + 1 ..];
|
||||||
|
}
|
||||||
|
|
||||||
|
break :value null;
|
||||||
|
};
|
||||||
|
|
||||||
|
parseIntoField(T, arena_alloc, dst, key, value) catch |err| {
|
||||||
|
if (comptime !canTrackDiags(T)) return err;
|
||||||
|
|
||||||
|
// The error set is dependent on comptime T, so we always add
|
||||||
|
// an extra error so we can have the "else" below.
|
||||||
|
const ErrSet = @TypeOf(err) || error{Unknown};
|
||||||
|
const message: [:0]const u8 = switch (@as(ErrSet, @errorCast(err))) {
|
||||||
|
// OOM is not recoverable since we need to allocate to
|
||||||
|
// track more error messages.
|
||||||
|
error.OutOfMemory => return err,
|
||||||
|
error.InvalidField => "unknown field",
|
||||||
|
error.ValueRequired => "value required",
|
||||||
|
error.InvalidValue => "invalid value",
|
||||||
|
else => try std.fmt.allocPrintZ(
|
||||||
|
arena_alloc,
|
||||||
|
"unknown error {}",
|
||||||
|
.{err},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add our diagnostic
|
||||||
|
try dst._diagnostics.append(arena_alloc, .{
|
||||||
|
.key = try arena_alloc.dupeZ(u8, key),
|
||||||
|
.message = message,
|
||||||
|
.location = diags.Location.fromIter(iter),
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if this type can track errors.
|
/// Returns true if this type can track diagnostics.
|
||||||
fn canTrackErrors(comptime T: type) bool {
|
fn canTrackDiags(comptime T: type) bool {
|
||||||
return @hasField(T, "_errors");
|
return @hasField(T, "_diagnostics");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a single key/value pair into the destination type T.
|
/// Parse a single key/value pair into the destination type T.
|
||||||
@ -199,15 +207,6 @@ fn parseIntoField(
|
|||||||
// 3 arg = (self, alloc, input) => void
|
// 3 arg = (self, alloc, input) => void
|
||||||
3 => try @field(dst, field.name).parseCLI(alloc, value),
|
3 => try @field(dst, field.name).parseCLI(alloc, value),
|
||||||
|
|
||||||
// 4 arg = (self, alloc, errors, input) => void
|
|
||||||
4 => if (comptime canTrackErrors(T)) {
|
|
||||||
try @field(dst, field.name).parseCLI(alloc, &dst._errors, value);
|
|
||||||
} else {
|
|
||||||
var list: ErrorList = .{};
|
|
||||||
try @field(dst, field.name).parseCLI(alloc, &list, value);
|
|
||||||
if (!list.empty()) return error.InvalidValue;
|
|
||||||
},
|
|
||||||
|
|
||||||
else => @compileError("parseCLI invalid argument count"),
|
else => @compileError("parseCLI invalid argument count"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -468,7 +467,28 @@ test "parse: empty value resets to default" {
|
|||||||
try testing.expect(!data.b);
|
try testing.expect(!data.b);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "parse: error tracking" {
|
test "parse: positional arguments are invalid" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var data: struct {
|
||||||
|
a: u8 = 42,
|
||||||
|
_arena: ?ArenaAllocator = null,
|
||||||
|
} = .{};
|
||||||
|
defer if (data._arena) |arena| arena.deinit();
|
||||||
|
|
||||||
|
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||||
|
testing.allocator,
|
||||||
|
"--a=84 what",
|
||||||
|
);
|
||||||
|
defer iter.deinit();
|
||||||
|
try testing.expectError(
|
||||||
|
error.InvalidField,
|
||||||
|
parse(@TypeOf(data), testing.allocator, &data, &iter),
|
||||||
|
);
|
||||||
|
try testing.expectEqual(@as(u8, 84), data.a);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse: diagnostic tracking" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
|
||||||
var data: struct {
|
var data: struct {
|
||||||
@ -476,7 +496,7 @@ test "parse: error tracking" {
|
|||||||
b: enum { one } = .one,
|
b: enum { one } = .one,
|
||||||
|
|
||||||
_arena: ?ArenaAllocator = null,
|
_arena: ?ArenaAllocator = null,
|
||||||
_errors: ErrorList = .{},
|
_diagnostics: DiagnosticList = .{},
|
||||||
} = .{};
|
} = .{};
|
||||||
defer if (data._arena) |arena| arena.deinit();
|
defer if (data._arena) |arena| arena.deinit();
|
||||||
|
|
||||||
@ -488,7 +508,48 @@ test "parse: error tracking" {
|
|||||||
try parse(@TypeOf(data), testing.allocator, &data, &iter);
|
try parse(@TypeOf(data), testing.allocator, &data, &iter);
|
||||||
try testing.expect(data._arena != null);
|
try testing.expect(data._arena != null);
|
||||||
try testing.expectEqualStrings("42", data.a);
|
try testing.expectEqualStrings("42", data.a);
|
||||||
try testing.expect(!data._errors.empty());
|
try testing.expect(data._diagnostics.items().len == 1);
|
||||||
|
{
|
||||||
|
const diag = data._diagnostics.items()[0];
|
||||||
|
try testing.expectEqual(diags.Location.none, diag.location);
|
||||||
|
try testing.expectEqualStrings("what", diag.key);
|
||||||
|
try testing.expectEqualStrings("unknown field", diag.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse: diagnostic location" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
var data: struct {
|
||||||
|
a: []const u8 = "",
|
||||||
|
b: enum { one, two } = .one,
|
||||||
|
|
||||||
|
_arena: ?ArenaAllocator = null,
|
||||||
|
_diagnostics: DiagnosticList = .{},
|
||||||
|
} = .{};
|
||||||
|
defer if (data._arena) |arena| arena.deinit();
|
||||||
|
|
||||||
|
var fbs = std.io.fixedBufferStream(
|
||||||
|
\\a=42
|
||||||
|
\\what
|
||||||
|
\\b=two
|
||||||
|
);
|
||||||
|
const r = fbs.reader();
|
||||||
|
|
||||||
|
const Iter = LineIterator(@TypeOf(r));
|
||||||
|
var iter: Iter = .{ .r = r, .filepath = "test" };
|
||||||
|
try parse(@TypeOf(data), testing.allocator, &data, &iter);
|
||||||
|
try testing.expect(data._arena != null);
|
||||||
|
try testing.expectEqualStrings("42", data.a);
|
||||||
|
try testing.expect(data.b == .two);
|
||||||
|
try testing.expect(data._diagnostics.items().len == 1);
|
||||||
|
{
|
||||||
|
const diag = data._diagnostics.items()[0];
|
||||||
|
try testing.expectEqualStrings("what", diag.key);
|
||||||
|
try testing.expectEqualStrings("unknown field", diag.message);
|
||||||
|
try testing.expectEqualStrings("test", diag.location.file.path);
|
||||||
|
try testing.expectEqual(2, diag.location.file.line);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test "parseIntoField: ignore underscore-prefixed fields" {
|
test "parseIntoField: ignore underscore-prefixed fields" {
|
||||||
@ -738,62 +799,6 @@ test "parseIntoField: struct with parse func" {
|
|||||||
try testing.expectEqual(@as([]const u8, "HELLO!"), data.a.v);
|
try testing.expectEqual(@as([]const u8, "HELLO!"), data.a.v);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "parseIntoField: struct with parse func with error tracking" {
|
|
||||||
const testing = std.testing;
|
|
||||||
var arena = ArenaAllocator.init(testing.allocator);
|
|
||||||
defer arena.deinit();
|
|
||||||
const alloc = arena.allocator();
|
|
||||||
|
|
||||||
var data: struct {
|
|
||||||
a: struct {
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
pub fn parseCLI(
|
|
||||||
_: Self,
|
|
||||||
parse_alloc: Allocator,
|
|
||||||
errors: *ErrorList,
|
|
||||||
value: ?[]const u8,
|
|
||||||
) !void {
|
|
||||||
_ = value;
|
|
||||||
try errors.add(parse_alloc, .{ .message = "OH NO!" });
|
|
||||||
}
|
|
||||||
} = .{},
|
|
||||||
|
|
||||||
_errors: ErrorList = .{},
|
|
||||||
} = .{};
|
|
||||||
|
|
||||||
try parseIntoField(@TypeOf(data), alloc, &data, "a", "42");
|
|
||||||
try testing.expect(!data._errors.empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
test "parseIntoField: struct with parse func with unsupported error tracking" {
|
|
||||||
const testing = std.testing;
|
|
||||||
var arena = ArenaAllocator.init(testing.allocator);
|
|
||||||
defer arena.deinit();
|
|
||||||
const alloc = arena.allocator();
|
|
||||||
|
|
||||||
var data: struct {
|
|
||||||
a: struct {
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
pub fn parseCLI(
|
|
||||||
_: Self,
|
|
||||||
parse_alloc: Allocator,
|
|
||||||
errors: *ErrorList,
|
|
||||||
value: ?[]const u8,
|
|
||||||
) !void {
|
|
||||||
_ = value;
|
|
||||||
try errors.add(parse_alloc, .{ .message = "OH NO!" });
|
|
||||||
}
|
|
||||||
} = .{},
|
|
||||||
} = .{};
|
|
||||||
|
|
||||||
try testing.expectError(
|
|
||||||
error.InvalidValue,
|
|
||||||
parseIntoField(@TypeOf(data), alloc, &data, "a", "42"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "parseIntoField: tagged union" {
|
test "parseIntoField: tagged union" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
var arena = ArenaAllocator.init(testing.allocator);
|
var arena = ArenaAllocator.init(testing.allocator);
|
||||||
@ -887,6 +892,74 @@ test "parseIntoField: tagged union missing tag" {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An iterator that considers its location to be CLI args. It
|
||||||
|
/// iterates through an underlying iterator and increments a counter
|
||||||
|
/// to track the current CLI arg index.
|
||||||
|
///
|
||||||
|
/// This also ignores any argument that starts with `+`. It assumes that
|
||||||
|
/// actions were parsed out before this iterator was created.
|
||||||
|
pub fn ArgsIterator(comptime Iterator: type) type {
|
||||||
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
/// The underlying args iterator.
|
||||||
|
iterator: Iterator,
|
||||||
|
|
||||||
|
/// Our current index into the iterator. This is 1-indexed.
|
||||||
|
/// The 0 value is used to indicate that we haven't read any
|
||||||
|
/// values yet.
|
||||||
|
index: usize = 0,
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
if (@hasDecl(Iterator, "deinit")) {
|
||||||
|
self.iterator.deinit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(self: *Self) ?[]const u8 {
|
||||||
|
const value = self.iterator.next() orelse return null;
|
||||||
|
self.index += 1;
|
||||||
|
|
||||||
|
// We ignore any argument that starts with "+". This is used
|
||||||
|
// to indicate actions and are expected to be parsed out before
|
||||||
|
// this iterator is created.
|
||||||
|
if (value.len > 0 and value[0] == '+') return self.next();
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a location for a diagnostic message.
|
||||||
|
pub fn location(self: *const Self) ?diags.Location {
|
||||||
|
return .{ .cli = self.index };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an args iterator for the process args. This will skip argv0.
|
||||||
|
pub fn argsIterator(alloc_gpa: Allocator) internal_os.args.ArgIterator.InitError!ArgsIterator(internal_os.args.ArgIterator) {
|
||||||
|
var iter = try internal_os.args.iterator(alloc_gpa);
|
||||||
|
errdefer iter.deinit();
|
||||||
|
_ = iter.next(); // skip argv0
|
||||||
|
return .{ .iterator = iter };
|
||||||
|
}
|
||||||
|
|
||||||
|
test "ArgsIterator" {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
const child = try std.process.ArgIteratorGeneral(.{}).init(
|
||||||
|
testing.allocator,
|
||||||
|
"--what +list-things --a=42",
|
||||||
|
);
|
||||||
|
const Iter = ArgsIterator(@TypeOf(child));
|
||||||
|
var iter: Iter = .{ .iterator = child };
|
||||||
|
defer iter.deinit();
|
||||||
|
|
||||||
|
try testing.expectEqualStrings("--what", iter.next().?);
|
||||||
|
try testing.expectEqualStrings("--a=42", iter.next().?);
|
||||||
|
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||||
|
try testing.expectEqual(@as(?[]const u8, null), iter.next());
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns an iterator (implements "next") that reads CLI args by line.
|
/// Returns an iterator (implements "next") that reads CLI args by line.
|
||||||
/// Each CLI arg is expected to be a single line. This is used to implement
|
/// Each CLI arg is expected to be a single line. This is used to implement
|
||||||
/// configuration files.
|
/// configuration files.
|
||||||
@ -899,7 +972,21 @@ pub fn LineIterator(comptime ReaderType: type) type {
|
|||||||
/// like 4 years and be wrong about this.
|
/// like 4 years and be wrong about this.
|
||||||
pub const MAX_LINE_SIZE = 4096;
|
pub const MAX_LINE_SIZE = 4096;
|
||||||
|
|
||||||
|
/// Our stateful reader.
|
||||||
r: ReaderType,
|
r: ReaderType,
|
||||||
|
|
||||||
|
/// Filepath that is used for diagnostics. This is only used for
|
||||||
|
/// diagnostic messages so it can be formatted however you want.
|
||||||
|
/// It is prefixed to the messages followed by the line number.
|
||||||
|
filepath: []const u8 = "",
|
||||||
|
|
||||||
|
/// The current line that we're on. This is 1-indexed because
|
||||||
|
/// lines are generally 1-indexed in the real world. The value
|
||||||
|
/// can be zero if we haven't read any lines yet.
|
||||||
|
line: usize = 0,
|
||||||
|
|
||||||
|
/// This is the buffer where we store the current entry that
|
||||||
|
/// is formatted to be compatible with the parse function.
|
||||||
entry: [MAX_LINE_SIZE]u8 = [_]u8{ '-', '-' } ++ ([_]u8{0} ** (MAX_LINE_SIZE - 2)),
|
entry: [MAX_LINE_SIZE]u8 = [_]u8{ '-', '-' } ++ ([_]u8{0} ** (MAX_LINE_SIZE - 2)),
|
||||||
|
|
||||||
pub fn next(self: *Self) ?[]const u8 {
|
pub fn next(self: *Self) ?[]const u8 {
|
||||||
@ -912,6 +999,9 @@ pub fn LineIterator(comptime ReaderType: type) type {
|
|||||||
unreachable;
|
unreachable;
|
||||||
} orelse return null;
|
} orelse return null;
|
||||||
|
|
||||||
|
// Increment our line counter
|
||||||
|
self.line += 1;
|
||||||
|
|
||||||
// Trim any whitespace (including CR) around it
|
// Trim any whitespace (including CR) around it
|
||||||
const trim = std.mem.trim(u8, entry, whitespace ++ "\r");
|
const trim = std.mem.trim(u8, entry, whitespace ++ "\r");
|
||||||
if (trim.len != entry.len) {
|
if (trim.len != entry.len) {
|
||||||
@ -959,11 +1049,22 @@ pub fn LineIterator(comptime ReaderType: type) type {
|
|||||||
// as CLI args.
|
// as CLI args.
|
||||||
return self.entry[0 .. buf.len + 2];
|
return self.entry[0 .. buf.len + 2];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a location for a diagnostic message.
|
||||||
|
pub fn location(self: *const Self) ?diags.Location {
|
||||||
|
// If we have no filepath then we have no location.
|
||||||
|
if (self.filepath.len == 0) return null;
|
||||||
|
|
||||||
|
return .{ .file = .{
|
||||||
|
.path = self.filepath,
|
||||||
|
.line = self.line,
|
||||||
|
} };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructs a LineIterator (see docs for that).
|
// Constructs a LineIterator (see docs for that).
|
||||||
pub fn lineIterator(reader: anytype) LineIterator(@TypeOf(reader)) {
|
fn lineIterator(reader: anytype) LineIterator(@TypeOf(reader)) {
|
||||||
return .{ .r = reader };
|
return .{ .r = reader };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,9 +33,9 @@ pub fn run(alloc_gpa: Allocator) !u8 {
|
|||||||
defer opts.deinit();
|
defer opts.deinit();
|
||||||
|
|
||||||
{
|
{
|
||||||
var iter = try std.process.argsWithAllocator(alloc);
|
var iter = try args.argsIterator(alloc_gpa);
|
||||||
defer iter.deinit();
|
defer iter.deinit();
|
||||||
try args.parse(Options, alloc, &opts, &iter);
|
try args.parse(Options, alloc_gpa, &opts, &iter);
|
||||||
}
|
}
|
||||||
|
|
||||||
const crash_dir = try crash.defaultDir(alloc);
|
const crash_dir = try crash.defaultDir(alloc);
|
||||||
|
139
src/cli/diagnostics.zig
Normal file
139
src/cli/diagnostics.zig
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const build_config = @import("../build_config.zig");
|
||||||
|
|
||||||
|
/// A diagnostic message from parsing. This is used to provide additional
|
||||||
|
/// human-friendly warnings and errors about the parsed data.
|
||||||
|
///
|
||||||
|
/// All of the memory for the diagnostic is allocated from the arena
|
||||||
|
/// associated with the config structure. If an arena isn't available
|
||||||
|
/// then diagnostics are not supported.
|
||||||
|
pub const Diagnostic = struct {
|
||||||
|
location: Location = .none,
|
||||||
|
key: [:0]const u8 = "",
|
||||||
|
message: [:0]const u8,
|
||||||
|
|
||||||
|
/// Write the full user-friendly diagnostic message to the writer.
|
||||||
|
pub fn write(self: *const Diagnostic, writer: anytype) !void {
|
||||||
|
switch (self.location) {
|
||||||
|
.none => {},
|
||||||
|
.cli => |index| try writer.print("cli:{}:", .{index}),
|
||||||
|
.file => |file| try writer.print(
|
||||||
|
"{s}:{}:",
|
||||||
|
.{ file.path, file.line },
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.key.len > 0) {
|
||||||
|
try writer.print("{s}: ", .{self.key});
|
||||||
|
} else if (self.location != .none) {
|
||||||
|
try writer.print(" ", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
try writer.print("{s}", .{self.message});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The possible locations for a diagnostic message. This is used
|
||||||
|
/// to provide context for the message.
|
||||||
|
pub const Location = union(enum) {
|
||||||
|
none,
|
||||||
|
cli: usize,
|
||||||
|
file: struct {
|
||||||
|
path: []const u8,
|
||||||
|
line: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
pub const Key = @typeInfo(Location).Union.tag_type.?;
|
||||||
|
|
||||||
|
pub fn fromIter(iter: anytype) Location {
|
||||||
|
const Iter = t: {
|
||||||
|
const T = @TypeOf(iter);
|
||||||
|
break :t switch (@typeInfo(T)) {
|
||||||
|
.Pointer => |v| v.child,
|
||||||
|
.Struct => T,
|
||||||
|
else => return .none,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!@hasDecl(Iter, "location")) return .none;
|
||||||
|
return iter.location() orelse .none;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A list of diagnostics. The "_diagnostics" field must be this type
|
||||||
|
/// for diagnostics to be supported. If this field is an incorrect type
|
||||||
|
/// a compile-time error will be raised.
|
||||||
|
///
|
||||||
|
/// This is implemented as a simple wrapper around an array list
|
||||||
|
/// so that we can inject some logic around adding diagnostics
|
||||||
|
/// and potentially in the future structure them differently.
|
||||||
|
pub const DiagnosticList = struct {
|
||||||
|
/// The list of diagnostics.
|
||||||
|
list: std.ArrayListUnmanaged(Diagnostic) = .{},
|
||||||
|
|
||||||
|
/// Precomputed data for diagnostics. This is used specifically
|
||||||
|
/// when we build libghostty so that we can precompute the messages
|
||||||
|
/// and return them via the C API without allocating memory at
|
||||||
|
/// call time.
|
||||||
|
precompute: Precompute = precompute_init,
|
||||||
|
|
||||||
|
const precompute_enabled = switch (build_config.artifact) {
|
||||||
|
// We enable precompute for tests so that the logic is
|
||||||
|
// semantically analyzed and run.
|
||||||
|
.exe, .wasm_module => builtin.is_test,
|
||||||
|
|
||||||
|
// We specifically want precompute for libghostty.
|
||||||
|
.lib => true,
|
||||||
|
};
|
||||||
|
const Precompute = if (precompute_enabled) struct {
|
||||||
|
messages: std.ArrayListUnmanaged([:0]const u8) = .{},
|
||||||
|
} else void;
|
||||||
|
const precompute_init: Precompute = if (precompute_enabled) .{} else {};
|
||||||
|
|
||||||
|
pub fn append(
|
||||||
|
self: *DiagnosticList,
|
||||||
|
alloc: Allocator,
|
||||||
|
diag: Diagnostic,
|
||||||
|
) Allocator.Error!void {
|
||||||
|
try self.list.append(alloc, diag);
|
||||||
|
errdefer _ = self.list.pop();
|
||||||
|
|
||||||
|
if (comptime precompute_enabled) {
|
||||||
|
var buf = std.ArrayList(u8).init(alloc);
|
||||||
|
defer buf.deinit();
|
||||||
|
try diag.write(buf.writer());
|
||||||
|
|
||||||
|
const owned: [:0]const u8 = try buf.toOwnedSliceSentinel(0);
|
||||||
|
errdefer alloc.free(owned);
|
||||||
|
|
||||||
|
try self.precompute.messages.append(alloc, owned);
|
||||||
|
errdefer _ = self.precompute.messages.pop();
|
||||||
|
|
||||||
|
assert(self.precompute.messages.items.len == self.list.items.len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn empty(self: *const DiagnosticList) bool {
|
||||||
|
return self.list.items.len == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn items(self: *const DiagnosticList) []const Diagnostic {
|
||||||
|
return self.list.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if there are any diagnostics for the given
|
||||||
|
/// location type.
|
||||||
|
pub fn containsLocation(
|
||||||
|
self: *const DiagnosticList,
|
||||||
|
location: Location.Key,
|
||||||
|
) bool {
|
||||||
|
for (self.list.items) |diag| {
|
||||||
|
if (diag.location == location) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
@ -23,7 +23,7 @@ pub fn run(alloc: Allocator) !u8 {
|
|||||||
defer opts.deinit();
|
defer opts.deinit();
|
||||||
|
|
||||||
{
|
{
|
||||||
var iter = try std.process.argsWithAllocator(alloc);
|
var iter = try args.argsIterator(alloc);
|
||||||
defer iter.deinit();
|
defer iter.deinit();
|
||||||
try args.parse(Options, alloc, &opts, &iter);
|
try args.parse(Options, alloc, &opts, &iter);
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ pub fn run(alloc: Allocator) !u8 {
|
|||||||
defer opts.deinit();
|
defer opts.deinit();
|
||||||
|
|
||||||
{
|
{
|
||||||
var iter = try std.process.argsWithAllocator(alloc);
|
var iter = try args.argsIterator(alloc);
|
||||||
defer iter.deinit();
|
defer iter.deinit();
|
||||||
try args.parse(Options, alloc, &opts, &iter);
|
try args.parse(Options, alloc, &opts, &iter);
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ pub fn run(alloc: std.mem.Allocator) !u8 {
|
|||||||
defer opts.deinit();
|
defer opts.deinit();
|
||||||
|
|
||||||
{
|
{
|
||||||
var iter = try std.process.argsWithAllocator(alloc);
|
var iter = try args.argsIterator(alloc);
|
||||||
defer iter.deinit();
|
defer iter.deinit();
|
||||||
try args.parse(Options, alloc, &opts, &iter);
|
try args.parse(Options, alloc, &opts, &iter);
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ pub const Config = struct {
|
|||||||
/// specific styles. It is not guaranteed that only those styles are returned,
|
/// specific styles. It is not guaranteed that only those styles are returned,
|
||||||
/// it will just prioritize fonts that match those styles.
|
/// it will just prioritize fonts that match those styles.
|
||||||
pub fn run(alloc: Allocator) !u8 {
|
pub fn run(alloc: Allocator) !u8 {
|
||||||
var iter = try std.process.argsWithAllocator(alloc);
|
var iter = try args.argsIterator(alloc);
|
||||||
defer iter.deinit();
|
defer iter.deinit();
|
||||||
return try runArgs(alloc, &iter);
|
return try runArgs(alloc, &iter);
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ pub fn run(alloc: Allocator) !u8 {
|
|||||||
defer opts.deinit();
|
defer opts.deinit();
|
||||||
|
|
||||||
{
|
{
|
||||||
var iter = try std.process.argsWithAllocator(alloc);
|
var iter = try args.argsIterator(alloc);
|
||||||
defer iter.deinit();
|
defer iter.deinit();
|
||||||
try args.parse(Options, alloc, &opts, &iter);
|
try args.parse(Options, alloc, &opts, &iter);
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,7 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
|
|||||||
defer opts.deinit();
|
defer opts.deinit();
|
||||||
|
|
||||||
{
|
{
|
||||||
var iter = try std.process.argsWithAllocator(gpa_alloc);
|
var iter = try args.argsIterator(gpa_alloc);
|
||||||
defer iter.deinit();
|
defer iter.deinit();
|
||||||
try args.parse(Options, gpa_alloc, &opts, &iter);
|
try args.parse(Options, gpa_alloc, &opts, &iter);
|
||||||
}
|
}
|
||||||
@ -882,7 +882,7 @@ const Preview = struct {
|
|||||||
next_start += child.height;
|
next_start += child.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config._errors.empty()) {
|
if (config._diagnostics.items().len > 0) {
|
||||||
const child = win.child(
|
const child = win.child(
|
||||||
.{
|
.{
|
||||||
.x_off = x_off,
|
.x_off = x_off,
|
||||||
@ -891,7 +891,7 @@ const Preview = struct {
|
|||||||
.limit = width,
|
.limit = width,
|
||||||
},
|
},
|
||||||
.height = .{
|
.height = .{
|
||||||
.limit = if (config._errors.empty()) 0 else 2 + config._errors.list.items.len,
|
.limit = if (config._diagnostics.items().len == 0) 0 else 2 + config._diagnostics.items().len,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -908,10 +908,14 @@ const Preview = struct {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (config._errors.list.items, 0..) |err, i| {
|
|
||||||
|
var buf = std.ArrayList(u8).init(alloc);
|
||||||
|
defer buf.deinit();
|
||||||
|
for (config._diagnostics.items(), 0..) |diag, i| {
|
||||||
|
try diag.write(buf.writer());
|
||||||
_ = try child.printSegment(
|
_ = try child.printSegment(
|
||||||
.{
|
.{
|
||||||
.text = err.message,
|
.text = buf.items,
|
||||||
.style = self.ui_err(),
|
.style = self.ui_err(),
|
||||||
},
|
},
|
||||||
.{
|
.{
|
||||||
@ -919,6 +923,7 @@ const Preview = struct {
|
|||||||
.col_offset = 2,
|
.col_offset = 2,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
buf.clearRetainingCapacity();
|
||||||
}
|
}
|
||||||
next_start += child.height;
|
next_start += child.height;
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ pub fn run(alloc: Allocator) !u8 {
|
|||||||
defer opts.deinit();
|
defer opts.deinit();
|
||||||
|
|
||||||
{
|
{
|
||||||
var iter = try std.process.argsWithAllocator(alloc);
|
var iter = try args.argsIterator(alloc);
|
||||||
defer iter.deinit();
|
defer iter.deinit();
|
||||||
try args.parse(Options, alloc, &opts, &iter);
|
try args.parse(Options, alloc, &opts, &iter);
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ pub fn run(alloc: std.mem.Allocator) !u8 {
|
|||||||
defer opts.deinit();
|
defer opts.deinit();
|
||||||
|
|
||||||
{
|
{
|
||||||
var iter = try std.process.argsWithAllocator(alloc);
|
var iter = try args.argsIterator(alloc);
|
||||||
defer iter.deinit();
|
defer iter.deinit();
|
||||||
try args.parse(Options, alloc, &opts, &iter);
|
try args.parse(Options, alloc, &opts, &iter);
|
||||||
}
|
}
|
||||||
@ -46,7 +46,6 @@ pub fn run(alloc: std.mem.Allocator) !u8 {
|
|||||||
if (opts.@"config-file") |config_path| {
|
if (opts.@"config-file") |config_path| {
|
||||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||||
const abs_path = try std.fs.cwd().realpath(config_path, &buf);
|
const abs_path = try std.fs.cwd().realpath(config_path, &buf);
|
||||||
|
|
||||||
try cfg.loadFile(alloc, abs_path);
|
try cfg.loadFile(alloc, abs_path);
|
||||||
try cfg.loadRecursiveFiles(alloc);
|
try cfg.loadRecursiveFiles(alloc);
|
||||||
} else {
|
} else {
|
||||||
@ -55,9 +54,14 @@ pub fn run(alloc: std.mem.Allocator) !u8 {
|
|||||||
|
|
||||||
try cfg.finalize();
|
try cfg.finalize();
|
||||||
|
|
||||||
if (!cfg._errors.empty()) {
|
if (cfg._diagnostics.items().len > 0) {
|
||||||
for (cfg._errors.list.items) |err| {
|
var buf = std.ArrayList(u8).init(alloc);
|
||||||
try stdout.print("{s}\n", .{err.message});
|
defer buf.deinit();
|
||||||
|
|
||||||
|
for (cfg._diagnostics.items()) |diag| {
|
||||||
|
try diag.write(buf.writer());
|
||||||
|
try stdout.print("{s}\n", .{buf.items});
|
||||||
|
buf.clearRetainingCapacity();
|
||||||
}
|
}
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
|
@ -39,24 +39,6 @@ export fn ghostty_config_load_cli_args(self: *Config) void {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the configuration from a string in the same format as
|
|
||||||
/// the file-based syntax for the desktop version of the terminal.
|
|
||||||
export fn ghostty_config_load_string(
|
|
||||||
self: *Config,
|
|
||||||
str: [*]const u8,
|
|
||||||
len: usize,
|
|
||||||
) void {
|
|
||||||
config_load_string_(self, str[0..len]) catch |err| {
|
|
||||||
log.err("error loading config err={}", .{err});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_load_string_(self: *Config, str: []const u8) !void {
|
|
||||||
var fbs = std.io.fixedBufferStream(str);
|
|
||||||
var iter = cli.args.lineIterator(fbs.reader());
|
|
||||||
try cli.args.parse(Config, global.alloc, self, &iter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load the configuration from the default file locations. This
|
/// Load the configuration from the default file locations. This
|
||||||
/// is usually done first. The default file locations are locations
|
/// is usually done first. The default file locations are locations
|
||||||
/// such as the home directory.
|
/// such as the home directory.
|
||||||
@ -112,14 +94,15 @@ fn config_trigger_(
|
|||||||
return trigger.cval();
|
return trigger.cval();
|
||||||
}
|
}
|
||||||
|
|
||||||
export fn ghostty_config_errors_count(self: *Config) u32 {
|
export fn ghostty_config_diagnostics_count(self: *Config) u32 {
|
||||||
return @intCast(self._errors.list.items.len);
|
return @intCast(self._diagnostics.items().len);
|
||||||
}
|
}
|
||||||
|
|
||||||
export fn ghostty_config_get_error(self: *Config, idx: u32) Error {
|
export fn ghostty_config_get_diagnostic(self: *Config, idx: u32) Diagnostic {
|
||||||
if (idx >= self._errors.list.items.len) return .{};
|
const items = self._diagnostics.items();
|
||||||
const err = self._errors.list.items[idx];
|
if (idx >= items.len) return .{};
|
||||||
return .{ .message = err.message.ptr };
|
const message = self._diagnostics.precompute.messages.items[idx];
|
||||||
|
return .{ .message = message.ptr };
|
||||||
}
|
}
|
||||||
|
|
||||||
export fn ghostty_config_open() void {
|
export fn ghostty_config_open() void {
|
||||||
@ -128,7 +111,7 @@ export fn ghostty_config_open() void {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync with ghostty_error_s
|
/// Sync with ghostty_diagnostic_s
|
||||||
const Error = extern struct {
|
const Diagnostic = extern struct {
|
||||||
message: [*:0]const u8 = "",
|
message: [*:0]const u8 = "",
|
||||||
};
|
};
|
||||||
|
@ -1456,8 +1456,13 @@ keybind: Keybinds = .{},
|
|||||||
/// Note that if an *Option*-sequence doesn't produce a printable character, it
|
/// Note that if an *Option*-sequence doesn't produce a printable character, it
|
||||||
/// will be treated as *Alt* regardless of this setting. (i.e. `alt+ctrl+a`).
|
/// will be treated as *Alt* regardless of this setting. (i.e. `alt+ctrl+a`).
|
||||||
///
|
///
|
||||||
|
/// The default value is `left`. This allows alt-based bindings to work
|
||||||
|
/// with the left *Option* key while still allowing the right *Option* key
|
||||||
|
/// to be used for Unicode input. This is a common setup for users of
|
||||||
|
/// certain keyboard layouts.
|
||||||
|
///
|
||||||
/// This does not work with GLFW builds.
|
/// This does not work with GLFW builds.
|
||||||
@"macos-option-as-alt": OptionAsAlt = .false,
|
@"macos-option-as-alt": OptionAsAlt = .left,
|
||||||
|
|
||||||
/// Whether to enable the macOS window shadow. The default value is true.
|
/// Whether to enable the macOS window shadow. The default value is true.
|
||||||
/// With some window managers and window transparency settings, you may
|
/// With some window managers and window transparency settings, you may
|
||||||
@ -1662,10 +1667,9 @@ term: []const u8 = "xterm-ghostty",
|
|||||||
/// This is set by the CLI parser for deinit.
|
/// This is set by the CLI parser for deinit.
|
||||||
_arena: ?ArenaAllocator = null,
|
_arena: ?ArenaAllocator = null,
|
||||||
|
|
||||||
/// List of errors that occurred while loading. This can be accessed directly
|
/// List of diagnostics that were generated during the loading of
|
||||||
/// by callers. It is only underscore-prefixed so it can't be set by the
|
/// the configuration.
|
||||||
/// configuration file.
|
_diagnostics: cli.DiagnosticList = .{},
|
||||||
_errors: ErrorList = .{},
|
|
||||||
|
|
||||||
/// The steps we can use to reload the configuration after it has been loaded
|
/// The steps we can use to reload the configuration after it has been loaded
|
||||||
/// without reopening the files. This is used in very specific cases such
|
/// without reopening the files. This is used in very specific cases such
|
||||||
@ -2261,7 +2265,9 @@ pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void {
|
|||||||
std.log.info("reading configuration file path={s}", .{path});
|
std.log.info("reading configuration file path={s}", .{path});
|
||||||
|
|
||||||
var buf_reader = std.io.bufferedReader(file.reader());
|
var buf_reader = std.io.bufferedReader(file.reader());
|
||||||
var iter = cli.args.lineIterator(buf_reader.reader());
|
const reader = buf_reader.reader();
|
||||||
|
const Iter = cli.args.LineIterator(@TypeOf(reader));
|
||||||
|
var iter: Iter = .{ .r = reader, .filepath = path };
|
||||||
try self.loadIter(alloc, &iter);
|
try self.loadIter(alloc, &iter);
|
||||||
try self.expandPaths(std.fs.path.dirname(path).?);
|
try self.expandPaths(std.fs.path.dirname(path).?);
|
||||||
}
|
}
|
||||||
@ -2364,13 +2370,9 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
|
|||||||
counter[i] = @field(self, field).list.items.len;
|
counter[i] = @field(self, field).list.items.len;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize our CLI iterator. The first argument is always assumed
|
// Initialize our CLI iterator.
|
||||||
// to be the program name so we skip over that.
|
var iter = try cli.args.argsIterator(alloc_gpa);
|
||||||
var iter = try internal_os.args.iterator(alloc_gpa);
|
|
||||||
defer iter.deinit();
|
defer iter.deinit();
|
||||||
if (iter.next()) |argv0| log.debug("skipping argv0 value={s}", .{argv0});
|
|
||||||
|
|
||||||
// Parse the config from the CLI args
|
|
||||||
try self.loadIter(alloc_gpa, &iter);
|
try self.loadIter(alloc_gpa, &iter);
|
||||||
|
|
||||||
// If we are not loading the default files, then we need to
|
// If we are not loading the default files, then we need to
|
||||||
@ -2446,7 +2448,7 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void {
|
|||||||
|
|
||||||
// We must only load a unique file once
|
// We must only load a unique file once
|
||||||
if (try loaded.fetchPut(path, {}) != null) {
|
if (try loaded.fetchPut(path, {}) != null) {
|
||||||
try self._errors.add(arena_alloc, .{
|
try self._diagnostics.append(arena_alloc, .{
|
||||||
.message = try std.fmt.allocPrintZ(
|
.message = try std.fmt.allocPrintZ(
|
||||||
arena_alloc,
|
arena_alloc,
|
||||||
"config-file {s}: cycle detected",
|
"config-file {s}: cycle detected",
|
||||||
@ -2458,7 +2460,7 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void {
|
|||||||
|
|
||||||
var file = cwd.openFile(path, .{}) catch |err| {
|
var file = cwd.openFile(path, .{}) catch |err| {
|
||||||
if (err != error.FileNotFound or !optional) {
|
if (err != error.FileNotFound or !optional) {
|
||||||
try self._errors.add(arena_alloc, .{
|
try self._diagnostics.append(arena_alloc, .{
|
||||||
.message = try std.fmt.allocPrintZ(
|
.message = try std.fmt.allocPrintZ(
|
||||||
arena_alloc,
|
arena_alloc,
|
||||||
"error opening config-file {s}: {}",
|
"error opening config-file {s}: {}",
|
||||||
@ -2472,7 +2474,9 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void {
|
|||||||
|
|
||||||
log.info("loading config-file path={s}", .{path});
|
log.info("loading config-file path={s}", .{path});
|
||||||
var buf_reader = std.io.bufferedReader(file.reader());
|
var buf_reader = std.io.bufferedReader(file.reader());
|
||||||
var iter = cli.args.lineIterator(buf_reader.reader());
|
const reader = buf_reader.reader();
|
||||||
|
const Iter = cli.args.LineIterator(@TypeOf(reader));
|
||||||
|
var iter: Iter = .{ .r = reader, .filepath = path };
|
||||||
try self.loadIter(alloc_gpa, &iter);
|
try self.loadIter(alloc_gpa, &iter);
|
||||||
try self.expandPaths(std.fs.path.dirname(path).?);
|
try self.expandPaths(std.fs.path.dirname(path).?);
|
||||||
}
|
}
|
||||||
@ -2495,7 +2499,7 @@ fn expandPaths(self: *Config, base: []const u8) !void {
|
|||||||
try @field(self, field.name).expand(
|
try @field(self, field.name).expand(
|
||||||
arena_alloc,
|
arena_alloc,
|
||||||
base,
|
base,
|
||||||
&self._errors,
|
&self._diagnostics,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2503,11 +2507,13 @@ fn expandPaths(self: *Config, base: []const u8) !void {
|
|||||||
|
|
||||||
fn loadTheme(self: *Config, theme: []const u8) !void {
|
fn loadTheme(self: *Config, theme: []const u8) !void {
|
||||||
// Find our theme file and open it. See the open function for details.
|
// Find our theme file and open it. See the open function for details.
|
||||||
const file: std.fs.File = (try themepkg.open(
|
const themefile = (try themepkg.open(
|
||||||
self._arena.?.allocator(),
|
self._arena.?.allocator(),
|
||||||
theme,
|
theme,
|
||||||
&self._errors,
|
&self._diagnostics,
|
||||||
)) orelse return;
|
)) orelse return;
|
||||||
|
const path = themefile.path;
|
||||||
|
const file = themefile.file;
|
||||||
defer file.close();
|
defer file.close();
|
||||||
|
|
||||||
// From this point onwards, we load the theme and do a bit of a dance
|
// From this point onwards, we load the theme and do a bit of a dance
|
||||||
@ -2533,7 +2539,9 @@ fn loadTheme(self: *Config, theme: []const u8) !void {
|
|||||||
|
|
||||||
// Load our theme
|
// Load our theme
|
||||||
var buf_reader = std.io.bufferedReader(file.reader());
|
var buf_reader = std.io.bufferedReader(file.reader());
|
||||||
var iter = cli.args.lineIterator(buf_reader.reader());
|
const reader = buf_reader.reader();
|
||||||
|
const Iter = cli.args.LineIterator(@TypeOf(reader));
|
||||||
|
var iter: Iter = .{ .r = reader, .filepath = path };
|
||||||
try new_config.loadIter(alloc_gpa, &iter);
|
try new_config.loadIter(alloc_gpa, &iter);
|
||||||
|
|
||||||
// Replay our previous inputs so that we can override values
|
// Replay our previous inputs so that we can override values
|
||||||
@ -2697,7 +2705,12 @@ pub fn finalize(self: *Config) !void {
|
|||||||
|
|
||||||
/// Callback for src/cli/args.zig to allow us to handle special cases
|
/// Callback for src/cli/args.zig to allow us to handle special cases
|
||||||
/// like `--help` or `-e`. Returns "false" if the CLI parsing should halt.
|
/// like `--help` or `-e`. Returns "false" if the CLI parsing should halt.
|
||||||
pub fn parseManuallyHook(self: *Config, alloc: Allocator, arg: []const u8, iter: anytype) !bool {
|
pub fn parseManuallyHook(
|
||||||
|
self: *Config,
|
||||||
|
alloc: Allocator,
|
||||||
|
arg: []const u8,
|
||||||
|
iter: anytype,
|
||||||
|
) !bool {
|
||||||
// Keep track of our input args no matter what..
|
// Keep track of our input args no matter what..
|
||||||
try self._replay_steps.append(alloc, .{ .arg = try alloc.dupe(u8, arg) });
|
try self._replay_steps.append(alloc, .{ .arg = try alloc.dupe(u8, arg) });
|
||||||
|
|
||||||
@ -2714,7 +2727,8 @@ pub fn parseManuallyHook(self: *Config, alloc: Allocator, arg: []const u8, iter:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (command.items.len == 0) {
|
if (command.items.len == 0) {
|
||||||
try self._errors.add(alloc, .{
|
try self._diagnostics.append(alloc, .{
|
||||||
|
.location = cli.Location.fromIter(iter),
|
||||||
.message = try std.fmt.allocPrintZ(
|
.message = try std.fmt.allocPrintZ(
|
||||||
alloc,
|
alloc,
|
||||||
"missing command after {s}",
|
"missing command after {s}",
|
||||||
@ -2758,7 +2772,10 @@ pub fn shallowClone(self: *const Config, alloc_gpa: Allocator) Config {
|
|||||||
/// Create a copy of this configuration. This is useful as a starting
|
/// Create a copy of this configuration. This is useful as a starting
|
||||||
/// point for modifying a configuration since a config can NOT be
|
/// point for modifying a configuration since a config can NOT be
|
||||||
/// modified once it is in use by an app or surface.
|
/// modified once it is in use by an app or surface.
|
||||||
pub fn clone(self: *const Config, alloc_gpa: Allocator) !Config {
|
pub fn clone(
|
||||||
|
self: *const Config,
|
||||||
|
alloc_gpa: Allocator,
|
||||||
|
) Allocator.Error!Config {
|
||||||
// Start with an empty config with a new arena we're going
|
// Start with an empty config with a new arena we're going
|
||||||
// to use for all our copies.
|
// to use for all our copies.
|
||||||
var result: Config = .{
|
var result: Config = .{
|
||||||
@ -2779,7 +2796,11 @@ pub fn clone(self: *const Config, alloc_gpa: Allocator) !Config {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cloneValue(alloc: Allocator, comptime T: type, src: T) !T {
|
fn cloneValue(
|
||||||
|
alloc: Allocator,
|
||||||
|
comptime T: type,
|
||||||
|
src: T,
|
||||||
|
) Allocator.Error!T {
|
||||||
// Do known named types first
|
// Do known named types first
|
||||||
switch (T) {
|
switch (T) {
|
||||||
[]const u8 => return try alloc.dupe(u8, src),
|
[]const u8 => return try alloc.dupe(u8, src),
|
||||||
@ -3129,7 +3150,7 @@ pub const Color = packed struct(u24) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Deep copy of the struct. Required by Config.
|
/// Deep copy of the struct. Required by Config.
|
||||||
pub fn clone(self: Color, _: Allocator) !Color {
|
pub fn clone(self: Color, _: Allocator) error{}!Color {
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3225,7 +3246,7 @@ pub const Palette = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Deep copy of the struct. Required by Config.
|
/// Deep copy of the struct. Required by Config.
|
||||||
pub fn clone(self: Self, _: Allocator) !Self {
|
pub fn clone(self: Self, _: Allocator) error{}!Self {
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3301,7 +3322,7 @@ pub const RepeatableString = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Deep copy of the struct. Required by Config.
|
/// Deep copy of the struct. Required by Config.
|
||||||
pub fn clone(self: *const Self, alloc: Allocator) !Self {
|
pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self {
|
||||||
// Copy the list and all the strings in the list.
|
// Copy the list and all the strings in the list.
|
||||||
const list = try self.list.clone(alloc);
|
const list = try self.list.clone(alloc);
|
||||||
for (list.items) |*item| {
|
for (list.items) |*item| {
|
||||||
@ -3445,7 +3466,7 @@ pub const RepeatablePath = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Deep copy of the struct. Required by Config.
|
/// Deep copy of the struct. Required by Config.
|
||||||
pub fn clone(self: *const Self, alloc: Allocator) !Self {
|
pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self {
|
||||||
const value = try self.value.clone(alloc);
|
const value = try self.value.clone(alloc);
|
||||||
for (value.items) |*item| {
|
for (value.items) |*item| {
|
||||||
switch (item.*) {
|
switch (item.*) {
|
||||||
@ -3499,7 +3520,7 @@ pub const RepeatablePath = struct {
|
|||||||
self: *Self,
|
self: *Self,
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
base: []const u8,
|
base: []const u8,
|
||||||
errors: *ErrorList,
|
diags: *cli.DiagnosticList,
|
||||||
) !void {
|
) !void {
|
||||||
assert(std.fs.path.isAbsolute(base));
|
assert(std.fs.path.isAbsolute(base));
|
||||||
var dir = try std.fs.cwd().openDir(base, .{});
|
var dir = try std.fs.cwd().openDir(base, .{});
|
||||||
@ -3526,7 +3547,7 @@ pub const RepeatablePath = struct {
|
|||||||
break :abs buf[0..resolved.len];
|
break :abs buf[0..resolved.len];
|
||||||
}
|
}
|
||||||
|
|
||||||
try errors.add(alloc, .{
|
try diags.append(alloc, .{
|
||||||
.message = try std.fmt.allocPrintZ(
|
.message = try std.fmt.allocPrintZ(
|
||||||
alloc,
|
alloc,
|
||||||
"error resolving file path {s}: {}",
|
"error resolving file path {s}: {}",
|
||||||
@ -3656,7 +3677,7 @@ pub const RepeatableFontVariation = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Deep copy of the struct. Required by Config.
|
/// Deep copy of the struct. Required by Config.
|
||||||
pub fn clone(self: *const Self, alloc: Allocator) !Self {
|
pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self {
|
||||||
return .{
|
return .{
|
||||||
.list = try self.list.clone(alloc),
|
.list = try self.list.clone(alloc),
|
||||||
};
|
};
|
||||||
@ -3789,7 +3810,7 @@ pub const Keybinds = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Deep copy of the struct. Required by Config.
|
/// Deep copy of the struct. Required by Config.
|
||||||
pub fn clone(self: *const Keybinds, alloc: Allocator) !Keybinds {
|
pub fn clone(self: *const Keybinds, alloc: Allocator) Allocator.Error!Keybinds {
|
||||||
return .{ .set = try self.set.clone(alloc) };
|
return .{ .set = try self.set.clone(alloc) };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3944,7 +3965,7 @@ pub const RepeatableCodepointMap = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Deep copy of the struct. Required by Config.
|
/// Deep copy of the struct. Required by Config.
|
||||||
pub fn clone(self: *const Self, alloc: Allocator) !Self {
|
pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self {
|
||||||
return .{ .map = try self.map.clone(alloc) };
|
return .{ .map = try self.map.clone(alloc) };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4227,7 +4248,7 @@ pub const FontStyle = union(enum) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Deep copy of the struct. Required by Config.
|
/// Deep copy of the struct. Required by Config.
|
||||||
pub fn clone(self: Self, alloc: Allocator) !Self {
|
pub fn clone(self: Self, alloc: Allocator) Allocator.Error!Self {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.default, .false => self,
|
.default, .false => self,
|
||||||
.name => |v| .{ .name = try alloc.dupeZ(u8, v) },
|
.name => |v| .{ .name = try alloc.dupeZ(u8, v) },
|
||||||
@ -4332,7 +4353,7 @@ pub const RepeatableLink = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Deep copy of the struct. Required by Config.
|
/// Deep copy of the struct. Required by Config.
|
||||||
pub fn clone(self: *const Self, alloc: Allocator) !Self {
|
pub fn clone(self: *const Self, alloc: Allocator) error{}!Self {
|
||||||
_ = self;
|
_ = self;
|
||||||
_ = alloc;
|
_ = alloc;
|
||||||
return .{};
|
return .{};
|
||||||
@ -4539,7 +4560,7 @@ pub const Duration = struct {
|
|||||||
.{ .name = "ns", .factor = 1 },
|
.{ .name = "ns", .factor = 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn clone(self: *const Duration, _: Allocator) !Duration {
|
pub fn clone(self: *const Duration, _: Allocator) error{}!Duration {
|
||||||
return .{ .duration = self.duration };
|
return .{ .duration = self.duration };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4661,7 +4682,7 @@ pub const WindowPadding = struct {
|
|||||||
top_left: u32 = 0,
|
top_left: u32 = 0,
|
||||||
bottom_right: u32 = 0,
|
bottom_right: u32 = 0,
|
||||||
|
|
||||||
pub fn clone(self: Self, _: Allocator) !Self {
|
pub fn clone(self: Self, _: Allocator) error{}!Self {
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ const assert = std.debug.assert;
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const global_state = &@import("../global.zig").state;
|
const global_state = &@import("../global.zig").state;
|
||||||
const internal_os = @import("../os/main.zig");
|
const internal_os = @import("../os/main.zig");
|
||||||
const ErrorList = @import("ErrorList.zig");
|
const cli = @import("../cli.zig");
|
||||||
|
|
||||||
/// Location of possible themes. The order of this enum matters because it
|
/// Location of possible themes. The order of this enum matters because it
|
||||||
/// defines the priority of theme search (from top to bottom).
|
/// defines the priority of theme search (from top to bottom).
|
||||||
@ -107,19 +107,25 @@ pub const LocationIterator = struct {
|
|||||||
pub fn open(
|
pub fn open(
|
||||||
arena_alloc: Allocator,
|
arena_alloc: Allocator,
|
||||||
theme: []const u8,
|
theme: []const u8,
|
||||||
errors: *ErrorList,
|
diags: *cli.DiagnosticList,
|
||||||
) error{OutOfMemory}!?std.fs.File {
|
) error{OutOfMemory}!?struct {
|
||||||
|
path: []const u8,
|
||||||
|
file: std.fs.File,
|
||||||
|
} {
|
||||||
|
|
||||||
// Absolute themes are loaded a different path.
|
// Absolute themes are loaded a different path.
|
||||||
if (std.fs.path.isAbsolute(theme)) return try openAbsolute(
|
if (std.fs.path.isAbsolute(theme)) {
|
||||||
arena_alloc,
|
const file: std.fs.File = try openAbsolute(
|
||||||
theme,
|
arena_alloc,
|
||||||
errors,
|
theme,
|
||||||
);
|
diags,
|
||||||
|
) orelse return null;
|
||||||
|
return .{ .path = theme, .file = file };
|
||||||
|
}
|
||||||
|
|
||||||
const basename = std.fs.path.basename(theme);
|
const basename = std.fs.path.basename(theme);
|
||||||
if (!std.mem.eql(u8, theme, basename)) {
|
if (!std.mem.eql(u8, theme, basename)) {
|
||||||
try errors.add(arena_alloc, .{
|
try diags.append(arena_alloc, .{
|
||||||
.message = try std.fmt.allocPrintZ(
|
.message = try std.fmt.allocPrintZ(
|
||||||
arena_alloc,
|
arena_alloc,
|
||||||
"theme \"{s}\" cannot include path separators unless it is an absolute path",
|
"theme \"{s}\" cannot include path separators unless it is an absolute path",
|
||||||
@ -135,15 +141,16 @@ pub fn open(
|
|||||||
const cwd = std.fs.cwd();
|
const cwd = std.fs.cwd();
|
||||||
while (try it.next()) |loc| {
|
while (try it.next()) |loc| {
|
||||||
const path = try std.fs.path.join(arena_alloc, &.{ loc.dir, theme });
|
const path = try std.fs.path.join(arena_alloc, &.{ loc.dir, theme });
|
||||||
if (cwd.openFile(path, .{})) |file| {
|
if (cwd.openFile(path, .{})) |file| return .{
|
||||||
return file;
|
.path = path,
|
||||||
|
.file = file,
|
||||||
} else |err| switch (err) {
|
} else |err| switch (err) {
|
||||||
// Not an error, just continue to the next location.
|
// Not an error, just continue to the next location.
|
||||||
error.FileNotFound => {},
|
error.FileNotFound => {},
|
||||||
|
|
||||||
// Anything else is an error we log and give up on.
|
// Anything else is an error we log and give up on.
|
||||||
else => {
|
else => {
|
||||||
try errors.add(arena_alloc, .{
|
try diags.append(arena_alloc, .{
|
||||||
.message = try std.fmt.allocPrintZ(
|
.message = try std.fmt.allocPrintZ(
|
||||||
arena_alloc,
|
arena_alloc,
|
||||||
"failed to load theme \"{s}\" from the file \"{s}\": {}",
|
"failed to load theme \"{s}\" from the file \"{s}\": {}",
|
||||||
@ -163,7 +170,7 @@ pub fn open(
|
|||||||
it.reset();
|
it.reset();
|
||||||
while (try it.next()) |loc| {
|
while (try it.next()) |loc| {
|
||||||
const path = try std.fs.path.join(arena_alloc, &.{ loc.dir, theme });
|
const path = try std.fs.path.join(arena_alloc, &.{ loc.dir, theme });
|
||||||
try errors.add(arena_alloc, .{
|
try diags.append(arena_alloc, .{
|
||||||
.message = try std.fmt.allocPrintZ(
|
.message = try std.fmt.allocPrintZ(
|
||||||
arena_alloc,
|
arena_alloc,
|
||||||
"theme \"{s}\" not found, tried path \"{s}\"",
|
"theme \"{s}\" not found, tried path \"{s}\"",
|
||||||
@ -186,18 +193,18 @@ pub fn open(
|
|||||||
pub fn openAbsolute(
|
pub fn openAbsolute(
|
||||||
arena_alloc: Allocator,
|
arena_alloc: Allocator,
|
||||||
theme: []const u8,
|
theme: []const u8,
|
||||||
errors: *ErrorList,
|
diags: *cli.DiagnosticList,
|
||||||
) error{OutOfMemory}!?std.fs.File {
|
) error{OutOfMemory}!?std.fs.File {
|
||||||
return std.fs.openFileAbsolute(theme, .{}) catch |err| {
|
return std.fs.openFileAbsolute(theme, .{}) catch |err| {
|
||||||
switch (err) {
|
switch (err) {
|
||||||
error.FileNotFound => try errors.add(arena_alloc, .{
|
error.FileNotFound => try diags.append(arena_alloc, .{
|
||||||
.message = try std.fmt.allocPrintZ(
|
.message = try std.fmt.allocPrintZ(
|
||||||
arena_alloc,
|
arena_alloc,
|
||||||
"failed to load theme from the path \"{s}\"",
|
"failed to load theme from the path \"{s}\"",
|
||||||
.{theme},
|
.{theme},
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
else => try errors.add(arena_alloc, .{
|
else => try diags.append(arena_alloc, .{
|
||||||
.message = try std.fmt.allocPrintZ(
|
.message = try std.fmt.allocPrintZ(
|
||||||
arena_alloc,
|
arena_alloc,
|
||||||
"failed to load theme from the path \"{s}\": {}",
|
"failed to load theme from the path \"{s}\": {}",
|
||||||
|
@ -54,8 +54,9 @@ pub fn add(self: *CodepointMap, alloc: Allocator, entry: Entry) !void {
|
|||||||
/// Get a descriptor for a codepoint.
|
/// Get a descriptor for a codepoint.
|
||||||
pub fn get(self: *const CodepointMap, cp: u21) ?discovery.Descriptor {
|
pub fn get(self: *const CodepointMap, cp: u21) ?discovery.Descriptor {
|
||||||
const items = self.list.items(.range);
|
const items = self.list.items(.range);
|
||||||
for (items, 0..) |range, forward_i| {
|
for (0..items.len) |forward_i| {
|
||||||
const i = items.len - forward_i - 1;
|
const i = items.len - forward_i - 1;
|
||||||
|
const range = items[i];
|
||||||
if (range[0] <= cp and cp <= range[1]) {
|
if (range[0] <= cp and cp <= range[1]) {
|
||||||
const descs = self.list.items(.descriptor);
|
const descs = self.list.items(.descriptor);
|
||||||
return descs[i];
|
return descs[i];
|
||||||
@ -110,4 +111,15 @@ test "codepointmap" {
|
|||||||
// Non-matching
|
// Non-matching
|
||||||
try testing.expect(m.get(0) == null);
|
try testing.expect(m.get(0) == null);
|
||||||
try testing.expect(m.get(3) == null);
|
try testing.expect(m.get(3) == null);
|
||||||
|
|
||||||
|
try m.add(alloc, .{ .range = .{ 3, 4 }, .descriptor = .{ .family = "C" } });
|
||||||
|
try m.add(alloc, .{ .range = .{ 5, 6 }, .descriptor = .{ .family = "D" } });
|
||||||
|
{
|
||||||
|
const d = m.get(3).?;
|
||||||
|
try testing.expectEqualStrings("C", d.family.?);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const d = m.get(1).?;
|
||||||
|
try testing.expectEqualStrings("B", d.family.?);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,7 +118,7 @@ pub fn getFace(self: *Collection, index: Index) !*Face {
|
|||||||
break :item item;
|
break :item item;
|
||||||
};
|
};
|
||||||
|
|
||||||
return self.getFaceFromEntry(item);
|
return try self.getFaceFromEntry(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the face from an entry.
|
/// Get the face from an entry.
|
||||||
|
@ -46,8 +46,10 @@ font_discover: ?Discover = null,
|
|||||||
/// Lock to protect multi-threaded access to the map.
|
/// Lock to protect multi-threaded access to the map.
|
||||||
lock: std.Thread.Mutex = .{},
|
lock: std.Thread.Mutex = .{},
|
||||||
|
|
||||||
|
pub const InitError = Library.InitError;
|
||||||
|
|
||||||
/// Initialize a new SharedGridSet.
|
/// Initialize a new SharedGridSet.
|
||||||
pub fn init(alloc: Allocator) !SharedGridSet {
|
pub fn init(alloc: Allocator) InitError!SharedGridSet {
|
||||||
var font_lib = try Library.init();
|
var font_lib = try Library.init();
|
||||||
errdefer font_lib.deinit();
|
errdefer font_lib.deinit();
|
||||||
|
|
||||||
@ -428,7 +430,10 @@ pub const DerivedConfig = struct {
|
|||||||
|
|
||||||
/// Initialize a DerivedConfig. The config should be either a
|
/// Initialize a DerivedConfig. The config should be either a
|
||||||
/// config.Config or another DerivedConfig to clone from.
|
/// config.Config or another DerivedConfig to clone from.
|
||||||
pub fn init(alloc_gpa: Allocator, config: anytype) !DerivedConfig {
|
pub fn init(
|
||||||
|
alloc_gpa: Allocator,
|
||||||
|
config: anytype,
|
||||||
|
) Allocator.Error!DerivedConfig {
|
||||||
var arena = ArenaAllocator.init(alloc_gpa);
|
var arena = ArenaAllocator.init(alloc_gpa);
|
||||||
errdefer arena.deinit();
|
errdefer arena.deinit();
|
||||||
const alloc = arena.allocator();
|
const alloc = arena.allocator();
|
||||||
@ -511,7 +516,7 @@ pub const Key = struct {
|
|||||||
alloc_gpa: Allocator,
|
alloc_gpa: Allocator,
|
||||||
config_src: *const DerivedConfig,
|
config_src: *const DerivedConfig,
|
||||||
font_size: DesiredSize,
|
font_size: DesiredSize,
|
||||||
) !Key {
|
) Allocator.Error!Key {
|
||||||
var arena = ArenaAllocator.init(alloc_gpa);
|
var arena = ArenaAllocator.init(alloc_gpa);
|
||||||
errdefer arena.deinit();
|
errdefer arena.deinit();
|
||||||
const alloc = arena.allocator();
|
const alloc = arena.allocator();
|
||||||
|
@ -24,7 +24,9 @@ pub const Library = switch (options.backend) {
|
|||||||
pub const FreetypeLibrary = struct {
|
pub const FreetypeLibrary = struct {
|
||||||
lib: freetype.Library,
|
lib: freetype.Library,
|
||||||
|
|
||||||
pub fn init() freetype.Error!Library {
|
pub const InitError = freetype.Error;
|
||||||
|
|
||||||
|
pub fn init() InitError!Library {
|
||||||
return Library{ .lib = try freetype.Library.init() };
|
return Library{ .lib = try freetype.Library.init() };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,7 +36,9 @@ pub const FreetypeLibrary = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const NoopLibrary = struct {
|
pub const NoopLibrary = struct {
|
||||||
pub fn init() !Library {
|
pub const InitError = error{};
|
||||||
|
|
||||||
|
pub fn init() InitError!Library {
|
||||||
return Library{};
|
return Library{};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,9 @@ const termio = @import("../termio.zig");
|
|||||||
/// The size of the terminal grid.
|
/// The size of the terminal grid.
|
||||||
grid_size: renderer.GridSize,
|
grid_size: renderer.GridSize,
|
||||||
|
|
||||||
|
/// The size of a single cell, in pixels.
|
||||||
|
cell_size: renderer.CellSize,
|
||||||
|
|
||||||
/// The size of the viewport in pixels.
|
/// The size of the viewport in pixels.
|
||||||
screen_size: renderer.ScreenSize,
|
screen_size: renderer.ScreenSize,
|
||||||
|
|
||||||
|
@ -60,6 +60,9 @@ surface_mailbox: apprt.surface.Mailbox,
|
|||||||
/// The cached grid size whenever a resize is called.
|
/// The cached grid size whenever a resize is called.
|
||||||
grid_size: renderer.GridSize,
|
grid_size: renderer.GridSize,
|
||||||
|
|
||||||
|
/// The size of a single cell. Used for size reports.
|
||||||
|
cell_size: renderer.CellSize,
|
||||||
|
|
||||||
/// The mailbox implementation to use.
|
/// The mailbox implementation to use.
|
||||||
mailbox: termio.Mailbox,
|
mailbox: termio.Mailbox,
|
||||||
|
|
||||||
@ -171,9 +174,8 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
|
|||||||
backend.initTerminal(&term);
|
backend.initTerminal(&term);
|
||||||
|
|
||||||
// Setup our terminal size in pixels for certain requests.
|
// Setup our terminal size in pixels for certain requests.
|
||||||
const screen_size = opts.screen_size.subPadding(opts.padding);
|
term.width_px = opts.grid_size.columns * opts.cell_size.width;
|
||||||
term.width_px = screen_size.width;
|
term.height_px = opts.grid_size.rows * opts.cell_size.height;
|
||||||
term.height_px = screen_size.height;
|
|
||||||
|
|
||||||
// Create our stream handler. This points to memory in self so it
|
// Create our stream handler. This points to memory in self so it
|
||||||
// isn't safe to use until self.* is set.
|
// isn't safe to use until self.* is set.
|
||||||
@ -214,6 +216,7 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
|
|||||||
.renderer_mailbox = opts.renderer_mailbox,
|
.renderer_mailbox = opts.renderer_mailbox,
|
||||||
.surface_mailbox = opts.surface_mailbox,
|
.surface_mailbox = opts.surface_mailbox,
|
||||||
.grid_size = opts.grid_size,
|
.grid_size = opts.grid_size,
|
||||||
|
.cell_size = opts.cell_size,
|
||||||
.backend = opts.backend,
|
.backend = opts.backend,
|
||||||
.mailbox = opts.mailbox,
|
.mailbox = opts.mailbox,
|
||||||
.terminal_stream = .{
|
.terminal_stream = .{
|
||||||
@ -348,6 +351,7 @@ pub fn resize(
|
|||||||
self: *Termio,
|
self: *Termio,
|
||||||
td: *ThreadData,
|
td: *ThreadData,
|
||||||
grid_size: renderer.GridSize,
|
grid_size: renderer.GridSize,
|
||||||
|
cell_size: renderer.CellSize,
|
||||||
screen_size: renderer.ScreenSize,
|
screen_size: renderer.ScreenSize,
|
||||||
padding: renderer.Padding,
|
padding: renderer.Padding,
|
||||||
) !void {
|
) !void {
|
||||||
@ -357,6 +361,7 @@ pub fn resize(
|
|||||||
|
|
||||||
// Update our cached grid size
|
// Update our cached grid size
|
||||||
self.grid_size = grid_size;
|
self.grid_size = grid_size;
|
||||||
|
self.cell_size = cell_size;
|
||||||
|
|
||||||
// Enter the critical area that we want to keep small
|
// Enter the critical area that we want to keep small
|
||||||
{
|
{
|
||||||
@ -371,8 +376,8 @@ pub fn resize(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Update our pixel sizes
|
// Update our pixel sizes
|
||||||
self.terminal.width_px = padded_size.width;
|
self.terminal.width_px = self.grid_size.columns * self.cell_size.width;
|
||||||
self.terminal.height_px = padded_size.height;
|
self.terminal.height_px = self.grid_size.rows * self.cell_size.height;
|
||||||
|
|
||||||
// Disable synchronized output mode so that we show changes
|
// Disable synchronized output mode so that we show changes
|
||||||
// immediately for a resize. This is allowed by the spec.
|
// immediately for a resize. This is allowed by the spec.
|
||||||
@ -412,24 +417,24 @@ fn sizeReportLocked(self: *Termio, td: *ThreadData, style: termio.Message.SizeRe
|
|||||||
.{
|
.{
|
||||||
self.grid_size.rows,
|
self.grid_size.rows,
|
||||||
self.grid_size.columns,
|
self.grid_size.columns,
|
||||||
self.terminal.height_px,
|
self.grid_size.rows * self.cell_size.height,
|
||||||
self.terminal.width_px,
|
self.grid_size.columns * self.cell_size.width,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
.csi_14_t => try std.fmt.bufPrint(
|
.csi_14_t => try std.fmt.bufPrint(
|
||||||
&buf,
|
&buf,
|
||||||
"\x1b[4;{};{}t",
|
"\x1b[4;{};{}t",
|
||||||
.{
|
.{
|
||||||
self.terminal.height_px,
|
self.grid_size.rows * self.cell_size.height,
|
||||||
self.terminal.width_px,
|
self.grid_size.columns * self.cell_size.width,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
.csi_16_t => try std.fmt.bufPrint(
|
.csi_16_t => try std.fmt.bufPrint(
|
||||||
&buf,
|
&buf,
|
||||||
"\x1b[6;{};{}t",
|
"\x1b[6;{};{}t",
|
||||||
.{
|
.{
|
||||||
self.terminal.height_px / self.grid_size.rows,
|
self.cell_size.height,
|
||||||
self.terminal.width_px / self.grid_size.columns,
|
self.cell_size.width,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
.csi_18_t => try std.fmt.bufPrint(
|
.csi_18_t => try std.fmt.bufPrint(
|
||||||
|
@ -383,6 +383,7 @@ fn coalesceCallback(
|
|||||||
cb.io.resize(
|
cb.io.resize(
|
||||||
&cb.data,
|
&cb.data,
|
||||||
v.grid_size,
|
v.grid_size,
|
||||||
|
v.cell_size,
|
||||||
v.screen_size,
|
v.screen_size,
|
||||||
v.padding,
|
v.padding,
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
|
@ -20,6 +20,9 @@ pub const Message = union(enum) {
|
|||||||
/// The grid size for the given screen size with padding applied.
|
/// The grid size for the given screen size with padding applied.
|
||||||
grid_size: renderer.GridSize,
|
grid_size: renderer.GridSize,
|
||||||
|
|
||||||
|
/// The updated cell size.
|
||||||
|
cell_size: renderer.CellSize,
|
||||||
|
|
||||||
/// The full screen (drawable) size. This does NOT include padding.
|
/// The full screen (drawable) size. This does NOT include padding.
|
||||||
/// This should be sent on to the renderer.
|
/// This should be sent on to the renderer.
|
||||||
screen_size: renderer.ScreenSize,
|
screen_size: renderer.ScreenSize,
|
||||||
|
Reference in New Issue
Block a user