From 31b6833ea1b0871eaa25512bcc82e3037d427ee1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 18 Oct 2024 14:51:33 -0700 Subject: [PATCH 01/10] config: change macos-option-as-alt default to left As suggested: https://github.com/ghostty-org/ghostty/discussions/2363#discussioncomment-10824847 This allows users of non-US keyboard layouts to continue to use the right option key for input methods, while still being able to use the the left option key as alt for keybindings. This is a bit of an experiment to see if this is a good default for everyone. This is in response to very common confusion of US keyboard layouts where "alt" doesn't work along with the very common use of non-US layouts where the right option key is used for input methods. I think this will strike the right balance for most users. --- src/config/Config.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 9097051ad..74933960d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1456,8 +1456,13 @@ keybind: Keybinds = .{}, /// 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`). /// +/// 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. -@"macos-option-as-alt": OptionAsAlt = .false, +@"macos-option-as-alt": OptionAsAlt = .left, /// Whether to enable the macOS window shadow. The default value is true. /// With some window managers and window transparency settings, you may From 4f1cee8eb904a7100ee0718b22b0ecb82b7e5c76 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Fri, 18 Oct 2024 22:29:52 -0500 Subject: [PATCH 02/10] fix: report correct screen pixel size Mode 2048 and CSI 14 t are size report control sequences which contain the text area size in pixels. The text area is defined to be the extents of the grid (rows and columns). Ghostty calculates the available size for the text area by setting the available padding, and then filling as much of the remaining space as possible. However, if there are remainder pixels these are still reported as part of the text area size. Pass the cell_size geometry through so that we can always report the correct value: columns * cell width and rows * cell height. --- src/Surface.zig | 3 +++ src/termio/Options.zig | 3 +++ src/termio/Termio.zig | 27 ++++++++++++++++----------- src/termio/Thread.zig | 1 + src/termio/message.zig | 3 +++ 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index c1b7afd7b..bd5073e3a 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -500,6 +500,7 @@ pub fn init( try termio.Termio.init(&self.io, alloc, .{ .grid_size = grid_size, + .cell_size = cell_size, .screen_size = screen_size, .padding = padding, .full_config = config, @@ -1331,6 +1332,7 @@ fn setCellSize(self: *Surface, size: renderer.CellSize) !void { self.io.queueMessage(.{ .resize = .{ .grid_size = self.grid_size, + .cell_size = self.cell_size, .screen_size = self.screen_size, .padding = self.padding, }, @@ -1435,6 +1437,7 @@ fn resize(self: *Surface, size: renderer.ScreenSize) !void { self.io.queueMessage(.{ .resize = .{ .grid_size = self.grid_size, + .cell_size = self.cell_size, .screen_size = self.screen_size, .padding = self.padding, }, diff --git a/src/termio/Options.zig b/src/termio/Options.zig index fe862a503..8014ed403 100644 --- a/src/termio/Options.zig +++ b/src/termio/Options.zig @@ -11,6 +11,9 @@ const termio = @import("../termio.zig"); /// The size of the terminal grid. grid_size: renderer.GridSize, +/// The size of a single cell, in pixels. +cell_size: renderer.CellSize, + /// The size of the viewport in pixels. screen_size: renderer.ScreenSize, diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index 865ca8d90..f28eb118e 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -60,6 +60,9 @@ surface_mailbox: apprt.surface.Mailbox, /// The cached grid size whenever a resize is called. grid_size: renderer.GridSize, +/// The size of a single cell. Used for size reports. +cell_size: renderer.CellSize, + /// The mailbox implementation to use. mailbox: termio.Mailbox, @@ -171,9 +174,8 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { backend.initTerminal(&term); // Setup our terminal size in pixels for certain requests. - const screen_size = opts.screen_size.subPadding(opts.padding); - term.width_px = screen_size.width; - term.height_px = screen_size.height; + term.width_px = opts.grid_size.columns * opts.cell_size.width; + term.height_px = opts.grid_size.rows * opts.cell_size.height; // Create our stream handler. This points to memory in self so it // 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, .surface_mailbox = opts.surface_mailbox, .grid_size = opts.grid_size, + .cell_size = opts.cell_size, .backend = opts.backend, .mailbox = opts.mailbox, .terminal_stream = .{ @@ -348,6 +351,7 @@ pub fn resize( self: *Termio, td: *ThreadData, grid_size: renderer.GridSize, + cell_size: renderer.CellSize, screen_size: renderer.ScreenSize, padding: renderer.Padding, ) !void { @@ -357,6 +361,7 @@ pub fn resize( // Update our cached grid size self.grid_size = grid_size; + self.cell_size = cell_size; // Enter the critical area that we want to keep small { @@ -371,8 +376,8 @@ pub fn resize( ); // Update our pixel sizes - self.terminal.width_px = padded_size.width; - self.terminal.height_px = padded_size.height; + self.terminal.width_px = self.grid_size.columns * self.cell_size.width; + self.terminal.height_px = self.grid_size.rows * self.cell_size.height; // Disable synchronized output mode so that we show changes // 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.columns, - self.terminal.height_px, - self.terminal.width_px, + self.grid_size.rows * self.cell_size.height, + self.grid_size.columns * self.cell_size.width, }, ), .csi_14_t => try std.fmt.bufPrint( &buf, "\x1b[4;{};{}t", .{ - self.terminal.height_px, - self.terminal.width_px, + self.grid_size.rows * self.cell_size.height, + self.grid_size.columns * self.cell_size.width, }, ), .csi_16_t => try std.fmt.bufPrint( &buf, "\x1b[6;{};{}t", .{ - self.terminal.height_px / self.grid_size.rows, - self.terminal.width_px / self.grid_size.columns, + self.cell_size.height, + self.cell_size.width, }, ), .csi_18_t => try std.fmt.bufPrint( diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index 4c75b3b9e..0f9cd782e 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -383,6 +383,7 @@ fn coalesceCallback( cb.io.resize( &cb.data, v.grid_size, + v.cell_size, v.screen_size, v.padding, ) catch |err| { diff --git a/src/termio/message.zig b/src/termio/message.zig index 79b920ad7..22b72235b 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -20,6 +20,9 @@ pub const Message = union(enum) { /// The grid size for the given screen size with padding applied. grid_size: renderer.GridSize, + /// The updated cell size. + cell_size: renderer.CellSize, + /// The full screen (drawable) size. This does NOT include padding. /// This should be sent on to the renderer. screen_size: renderer.ScreenSize, From 96b4ff39a6eb3cfd3d74d8d6dee640830d48c90b Mon Sep 17 00:00:00 2001 From: Charly Delay <0@0xcharly.com> Date: Sat, 19 Oct 2024 14:08:43 +0900 Subject: [PATCH 03/10] Tentative fix for unexpected `font-codepoint-map` behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In particular when configured to replace several ranges with multiple fonts. Given the following `font-codepoint-map` config: ``` font-codepoint-map=U+0030-U+0039=Monaco # 0-9 font-codepoint-map=U+0040=mononoki # @ font-codepoint-map=U+0041-U+005a=Pixel Code # A-Z font-codepoint-map=U+0061-U+007a=Victor Mono # a-z ``` I noticed a couple of unexpected behavior: 1. Codepoint ranges were assigned the wrong font 2. The declaration order had a direct impact on the font assignment (seemed to be rotating in some fashion) If my understanding of the current implementation is correct, for a given range index `n` in the `MultiArrayList` `CodepointMap.get(…)` returns the font descriptor at index `len - n - 1`. In other words, it returns the descriptor symmetrically opposite relative to the middle of the list. I've added a couple test cases that I would expect to pass if my understanding of the expected behavior is correct, verified that they were broken under the current behavior, and updated the implementation of `CodepointMap.get(…)` accordingly. My understanding of the original intent is to give priority to the latest range match in the list (which is a use case already tested by the `codepointmap` test, but which I believe happened to pass "by accident"), so I opted for a reverse traversal of the codepoint list. --- src/font/CodepointMap.zig | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/font/CodepointMap.zig b/src/font/CodepointMap.zig index 8c9ded402..5b174f129 100644 --- a/src/font/CodepointMap.zig +++ b/src/font/CodepointMap.zig @@ -54,8 +54,9 @@ pub fn add(self: *CodepointMap, alloc: Allocator, entry: Entry) !void { /// Get a descriptor for a codepoint. pub fn get(self: *const CodepointMap, cp: u21) ?discovery.Descriptor { 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 range = items[i]; if (range[0] <= cp and cp <= range[1]) { const descs = self.list.items(.descriptor); return descs[i]; @@ -110,4 +111,15 @@ test "codepointmap" { // Non-matching try testing.expect(m.get(0) == 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.?); + } } From 42bf37af32ae6f20b32b447e54e5e208d95a9a2f Mon Sep 17 00:00:00 2001 From: Josh <36625023+JoshuaBrest@users.noreply.github.com> Date: Sat, 19 Oct 2024 00:31:43 -0700 Subject: [PATCH 04/10] feat: Update about menu design to match macOS. --- macos/Sources/Features/About/About.xib | 2 +- .../Features/About/AboutController.swift | 1 + macos/Sources/Features/About/AboutView.swift | 139 ++++++++++++++++-- 3 files changed, 125 insertions(+), 17 deletions(-) diff --git a/macos/Sources/Features/About/About.xib b/macos/Sources/Features/About/About.xib index e884beff1..5803a32de 100644 --- a/macos/Sources/Features/About/About.xib +++ b/macos/Sources/Features/About/About.xib @@ -14,7 +14,7 @@ - + diff --git a/macos/Sources/Features/About/AboutController.swift b/macos/Sources/Features/About/AboutController.swift index d2ae68ea7..efd7a515a 100644 --- a/macos/Sources/Features/About/AboutController.swift +++ b/macos/Sources/Features/About/AboutController.swift @@ -10,6 +10,7 @@ class AboutController: NSWindowController, NSWindowDelegate { override func windowDidLoad() { guard let window = window else { return } window.center() + window.isMovableByWindowBackground = true window.contentView = NSHostingView(rootView: AboutView()) } diff --git a/macos/Sources/Features/About/AboutView.swift b/macos/Sources/Features/About/AboutView.swift index 02f899cc4..a7b0834fc 100644 --- a/macos/Sources/Features/About/AboutView.swift +++ b/macos/Sources/Features/About/AboutView.swift @@ -1,35 +1,142 @@ import SwiftUI struct AboutView: View { + @Environment(\.openURL) var openURL + + /// Eventually this should be a redirect like https://go.ghostty.dev/discord or https://go.ghostty.dev/github + @State var discordLink: String = "https://discord.gg/ghostty" + @State var githubLink: String = "https://github.com/ghostty-org/ghostty" + /// Read the commit from the bundle. var build: String? { Bundle.main.infoDictionary?["CFBundleVersion"] as? String } var commit: String? { Bundle.main.infoDictionary?["GhosttyCommit"] as? String } var version: String? { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String } + var copyright: String? { Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String } + + struct ValuePair: Identifiable { + var id = UUID() + public let key: LocalizedStringResource + public let value: Value + + } + + var computedStrings: [ValuePair] { + let list: [ValuePair] = [ + ValuePair(key: "Version", value: self.version), + ValuePair(key: "Build", value: self.build), + ValuePair(key: "Commit", value: self.commit == "" ? nil : self.commit) + ] + + let strings: [ValuePair] = list.compactMap { + guard let value = $0.value else { return nil } + + return ValuePair(key: $0.key, value: value) + } + + return strings + } + + #if os(macOS) + 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 { VStack(alignment: .center) { Image("AppIconImage") .resizable() .aspectRatio(contentMode: .fit) - .frame(maxHeight: 96) + .frame(height: 128) - Text("Ghostty") - .font(.title3) - .textSelection(.enabled) + VStack(alignment: .center, spacing: 32) { + VStack(alignment: .center, spacing: 8) { + Text("Ghostty") + .bold() + .font(.title) + Text("Fast, native, feature-rich terminal emulator pushing modern features.") + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .font(.caption) + .tint(.secondary) + .opacity(0.8) + } + VStack(spacing: 2) { + ForEach(computedStrings) { item in - if let version = self.version { - Text("Version: \(version)") - .font(.body) - .textSelection(.enabled) - } - - if let build = self.build { - Text("Build: \(build)") - .font(.body) - .textSelection(.enabled) + 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) + .frame(maxWidth: .infinity) + } + } + .frame(maxWidth: .infinity) + + HStack(spacing: 8) { + Button("Discord") { + guard let url = URL(string: discordLink) else { return + } + openURL(url) + } + Button("Github") { + guard let url = URL(string: githubLink) else { return + } + openURL(url) + } + } + + if let copy = self.copyright { + Text(copy) + .font(.caption) + .tint(.secondary) + .opacity(0.8) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + } } + .frame(maxWidth: .infinity) } - .frame(minWidth: 300) - .padding() + .padding(.top, 8) + .padding(32) + .frame(minWidth: 256) + #if os(macOS) + .background(VisualEffectBackground(material: .underWindowBackground).ignoresSafeArea()) + #endif + } +} + +struct AboutView_Previews: PreviewProvider { + static var previews: some View { + AboutView() } } From 70acb0d76e2670cf81272349f6297d88318ca1f4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 19 Oct 2024 10:01:28 -0700 Subject: [PATCH 05/10] macos: clamp window size to screen size on screen parameter changes Fixes #2462 This sets up a listener for screen parameter changes. This only triggers when a screen is added, removed, or a parameter such as its resolution changes. This doesn't trigger when a window is simply moved from one screen to another. On parameter change, we ensure that the window is within the bounds of the screen. As an exception, if the window was previously already outside the bounds of the screen, we don't move it back in. --- macos/Ghostty.xcodeproj/project.pbxproj | 4 + .../Terminal/BaseTerminalController.swift | 74 +++++++++++++++++++ .../Terminal/TerminalController.swift | 3 +- macos/Sources/Ghostty/Ghostty.Config.swift | 8 +- macos/Sources/Helpers/Xcode.swift | 10 +++ 5 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 macos/Sources/Helpers/Xcode.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index b60eb11f5..57070dc47 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */; }; A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; }; 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 */; }; A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.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 = ""; }; A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = ""; }; A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = ""; }; + A5A6F7292CC41B8700B232A5 /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = ""; }; 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 = ""; }; A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = ""; }; @@ -233,6 +235,7 @@ A534263D2A7DCBB000EBB7A2 /* Helpers */ = { isa = PBXGroup; children = ( + A5A6F7292CC41B8700B232A5 /* Xcode.swift */, A5CEAFFE29C2410700646FDA /* Backport.swift */, A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */, A5CBD0572C9F30860017A1AE /* Cursor.swift */, @@ -582,6 +585,7 @@ A52FFF5D2CAB4D08000C6A5B /* NSScreen+Extension.swift in Sources */, A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */, A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */, + A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */, A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */, A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */, A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 77d2d0033..ea35790fd 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -57,6 +57,14 @@ class BaseTerminalController: NSWindowController, /// Event monitor (see individual events for why) 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) { fatalError("init(coder:) is not supported for this view") } @@ -80,6 +88,11 @@ class BaseTerminalController: NSWindowController, selector: #selector(onConfirmClipboardRequest), name: Ghostty.Notification.confirmClipboard, 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 // single surface handlers. @@ -89,6 +102,8 @@ class BaseTerminalController: NSWindowController, } deinit { + NotificationCenter.default.removeObserver(self) + if let 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 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 @IBAction func close(_ sender: Any) { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 70df52b4b..f43454edd 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -358,7 +358,8 @@ class TerminalController: BaseTerminalController { self.fixTabBar() } - func windowDidMove(_ notification: Notification) { + override func windowDidMove(_ notification: Notification) { + super.windowDidMove(notification) self.fixTabBar() } diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 69c9f992b..29639c39e 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -56,7 +56,13 @@ extension Ghostty { // same filesystem concept. #if os(macOS) 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); #endif diff --git a/macos/Sources/Helpers/Xcode.swift b/macos/Sources/Helpers/Xcode.swift new file mode 100644 index 000000000..281bad18b --- /dev/null +++ b/macos/Sources/Helpers/Xcode.swift @@ -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 +} From fc6eda66232e381f514bd0097bd91ecee7863762 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 19 Oct 2024 10:42:58 -0700 Subject: [PATCH 06/10] cli: +help needs to use the proper args iterator --- src/cli/help.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/help.zig b/src/cli/help.zig index c0db37afe..e9e449550 100644 --- a/src/cli/help.zig +++ b/src/cli/help.zig @@ -23,7 +23,7 @@ pub fn run(alloc: Allocator) !u8 { defer opts.deinit(); { - var iter = try std.process.argsWithAllocator(alloc); + var iter = try args.argsIterator(alloc); defer iter.deinit(); try args.parse(Options, alloc, &opts, &iter); } From 4b08b3f8d8009a94c4c5fd5744e8a88c942ab764 Mon Sep 17 00:00:00 2001 From: Josh <36625023+JoshuaBrest@users.noreply.github.com> Date: Sat, 19 Oct 2024 11:23:04 -0700 Subject: [PATCH 07/10] refactor: hide buttons when URLs are invalid and change variable mutability and visibility. --- macos/Sources/Features/About/AboutView.swift | 31 ++++++++++---------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/macos/Sources/Features/About/AboutView.swift b/macos/Sources/Features/About/AboutView.swift index a7b0834fc..7c5c5de7f 100644 --- a/macos/Sources/Features/About/AboutView.swift +++ b/macos/Sources/Features/About/AboutView.swift @@ -4,23 +4,23 @@ struct AboutView: View { @Environment(\.openURL) var openURL /// Eventually this should be a redirect like https://go.ghostty.dev/discord or https://go.ghostty.dev/github - @State var discordLink: String = "https://discord.gg/ghostty" - @State var githubLink: String = "https://github.com/ghostty-org/ghostty" + private let discordLink = URL(string: "https://discord.gg/ghostty") + private let githubLink = URL(string: "https://github.com/ghostty-org/ghostty") /// Read the commit from the bundle. - var build: String? { Bundle.main.infoDictionary?["CFBundleVersion"] as? String } - var commit: String? { Bundle.main.infoDictionary?["GhosttyCommit"] as? String } - var version: String? { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String } - var copyright: String? { Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String } + private var build: String? { Bundle.main.infoDictionary?["CFBundleVersion"] as? String } + private var commit: String? { Bundle.main.infoDictionary?["GhosttyCommit"] as? String } + private var version: String? { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String } + private var copyright: String? { Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String } - struct ValuePair: Identifiable { + private struct ValuePair: Identifiable { var id = UUID() public let key: LocalizedStringResource public let value: Value } - var computedStrings: [ValuePair] { + private var computedStrings: [ValuePair] { let list: [ValuePair] = [ ValuePair(key: "Version", value: self.version), ValuePair(key: "Build", value: self.build), @@ -37,7 +37,7 @@ struct AboutView: View { } #if os(macOS) - struct VisualEffectBackground: NSViewRepresentable { + private struct VisualEffectBackground: NSViewRepresentable { let material: NSVisualEffectView.Material let blendingMode: NSVisualEffectView.BlendingMode let isEmphasized: Bool @@ -103,16 +103,17 @@ struct AboutView: View { .frame(maxWidth: .infinity) HStack(spacing: 8) { - Button("Discord") { - guard let url = URL(string: discordLink) else { return + if let url = discordLink { + Button("Discord") { + openURL(url) } - openURL(url) } - Button("Github") { - guard let url = URL(string: githubLink) else { return + if let url = githubLink { + Button("GitHub") { + openURL(url) } - openURL(url) } + } if let copy = self.copyright { From d291fcfd526e6c4622226b29a3f55ccc58ab400a Mon Sep 17 00:00:00 2001 From: Josh <36625023+JoshuaBrest@users.noreply.github.com> Date: Sat, 19 Oct 2024 11:45:24 -0700 Subject: [PATCH 08/10] style: add line break in the caption of the about dialog. --- macos/Sources/Features/About/AboutView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Features/About/AboutView.swift b/macos/Sources/Features/About/AboutView.swift index 7c5c5de7f..976207e9b 100644 --- a/macos/Sources/Features/About/AboutView.swift +++ b/macos/Sources/Features/About/AboutView.swift @@ -76,7 +76,7 @@ struct AboutView: View { Text("Ghostty") .bold() .font(.title) - Text("Fast, native, feature-rich terminal emulator pushing modern features.") + Text("Fast, native, feature-rich terminal \nemulator pushing modern features.") .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) .font(.caption) From cc42dc57b942a342bd82e7e94ba0411434d6b6da Mon Sep 17 00:00:00 2001 From: Josh <36625023+JoshuaBrest@users.noreply.github.com> Date: Sat, 19 Oct 2024 11:52:12 -0700 Subject: [PATCH 09/10] fix: add text-selection back in the about dialoge. --- macos/Sources/Features/About/AboutView.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/macos/Sources/Features/About/AboutView.swift b/macos/Sources/Features/About/AboutView.swift index 976207e9b..f56faaa1e 100644 --- a/macos/Sources/Features/About/AboutView.swift +++ b/macos/Sources/Features/About/AboutView.swift @@ -83,6 +83,7 @@ struct AboutView: View { .tint(.secondary) .opacity(0.8) } + .textSelection(.enabled) VStack(spacing: 2) { ForEach(computedStrings) { item in @@ -97,6 +98,7 @@ struct AboutView: View { .opacity(0.8) } .font(.callout) + .textSelection(.enabled) .frame(maxWidth: .infinity) } } @@ -119,6 +121,7 @@ struct AboutView: View { if let copy = self.copyright { Text(copy) .font(.caption) + .textSelection(.enabled) .tint(.secondary) .opacity(0.8) .multilineTextAlignment(.center) From 85db4d0277a39368df2e15d4d64017544ed77a8b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 19 Oct 2024 14:06:55 -0700 Subject: [PATCH 10/10] macos: personal nitpicks and improvements --- macos/Sources/Features/About/AboutView.swift | 50 ++++++++------------ 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/macos/Sources/Features/About/AboutView.swift b/macos/Sources/Features/About/AboutView.swift index f56faaa1e..71fe9c252 100644 --- a/macos/Sources/Features/About/AboutView.swift +++ b/macos/Sources/Features/About/AboutView.swift @@ -3,8 +3,6 @@ import SwiftUI struct AboutView: View { @Environment(\.openURL) var openURL - /// Eventually this should be a redirect like https://go.ghostty.dev/discord or https://go.ghostty.dev/github - private let discordLink = URL(string: "https://discord.gg/ghostty") private let githubLink = URL(string: "https://github.com/ghostty-org/ghostty") /// Read the commit from the bundle. @@ -13,36 +11,36 @@ struct AboutView: View { private var version: String? { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String } private var copyright: String? { Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String } - private struct ValuePair: Identifiable { + private var properties: [KeyValue] { + let list: [KeyValue] = [ + .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: Identifiable { var id = UUID() public let key: LocalizedStringResource public let value: Value - - } - - private var computedStrings: [ValuePair] { - let list: [ValuePair] = [ - ValuePair(key: "Version", value: self.version), - ValuePair(key: "Build", value: self.build), - ValuePair(key: "Commit", value: self.commit == "" ? nil : self.commit) - ] - - let strings: [ValuePair] = list.compactMap { - guard let value = $0.value else { return nil } - - return ValuePair(key: $0.key, value: value) - } - - return strings } #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) { + init(material: NSVisualEffectView.Material, + blendingMode: NSVisualEffectView.BlendingMode = .behindWindow, + isEmphasized: Bool = false) + { self.material = material self.blendingMode = blendingMode self.isEmphasized = isEmphasized @@ -56,9 +54,7 @@ struct AboutView: View { func makeNSView(context: Context) -> NSVisualEffectView { let visualEffect = NSVisualEffectView() - visualEffect.autoresizingMask = [.width, .height] - return visualEffect } } @@ -85,8 +81,7 @@ struct AboutView: View { } .textSelection(.enabled) VStack(spacing: 2) { - ForEach(computedStrings) { item in - + ForEach(properties) { item in HStack(spacing: 4) { Text(item.key) .frame(width: 126, alignment: .trailing) @@ -105,11 +100,6 @@ struct AboutView: View { .frame(maxWidth: .infinity) HStack(spacing: 8) { - if let url = discordLink { - Button("Discord") { - openURL(url) - } - } if let url = githubLink { Button("GitHub") { openURL(url)