diff --git a/macos/GhosttyUITests/GhosttyCommandPaletteTests.swift b/macos/GhosttyUITests/GhosttyCommandPaletteTests.swift new file mode 100644 index 000000000..428682b4f --- /dev/null +++ b/macos/GhosttyUITests/GhosttyCommandPaletteTests.swift @@ -0,0 +1,81 @@ +// +// GhosttyCommandPaletteTests.swift +// Ghostty +// +// Created by Lukas on 19.03.2026. +// + +import XCTest + +final class GhosttyCommandPaletteTests: GhosttyCustomConfigCase { + override static var runsForEachTargetApplicationUIConfiguration: Bool { false } + @MainActor func testDismissingCommandPalette() async throws { + let app = try ghosttyApplication() + app.activate() + + XCTAssertTrue(app.windows.firstMatch.waitForExistence(timeout: 5), "New window should appear") + + app.menuItems["Command Palette"].firstMatch.click() + + let clearScreenButton = app.buttons + .containing(NSPredicate(format: "label CONTAINS[c] 'Clear Screen'")) + .firstMatch + + XCTAssertTrue(clearScreenButton.waitForExistence(timeout: 5), "Command Palette should appear") + + clearScreenButton.coordinate(withNormalizedOffset: .zero) + .withOffset(.init(dx: -30, dy: 0)) + .click() + + XCTAssertTrue(clearScreenButton.waitForNonExistence(timeout: 5), "Command Palette should disappear after clicking outside") + + app.typeKey("p", modifierFlags: [.command, .shift]) + + XCTAssertTrue(clearScreenButton.waitForExistence(timeout: 5), "Command Palette should appear") + + app.typeKey(.escape, modifierFlags: []) + + XCTAssertTrue(clearScreenButton.waitForNonExistence(timeout: 5), "Command Palette should disappear after typing escape") + + app.typeKey("p", modifierFlags: [.command, .shift]) + + XCTAssertTrue(clearScreenButton.waitForExistence(timeout: 5), "Command Palette should appear") + + app.typeKey(.enter, modifierFlags: []) + + XCTAssertTrue(clearScreenButton.waitForNonExistence(timeout: 5), "Command Palette should disappear after submitting query") + + app.typeKey("p", modifierFlags: [.command, .shift]) + + XCTAssertTrue(clearScreenButton.waitForExistence(timeout: 5), "Command Palette should appear") + + app.typeText("Clear Screen") + app.typeKey(.enter, modifierFlags: []) + + XCTAssertTrue(clearScreenButton.waitForNonExistence(timeout: 5), "Command Palette should disappear after selecting a command by keyboard") + + app.typeKey("p", modifierFlags: [.command, .shift]) + app.typeKey(.delete, modifierFlags: []) + + XCTAssertTrue(clearScreenButton.waitForExistence(timeout: 5), "Command Palette should appear") + clearScreenButton.click() + + XCTAssertTrue(clearScreenButton.waitForNonExistence(timeout: 5), "Command Palette should disappear after selecting a command by mouse") + } + + @MainActor func testSelectCommandWithMouse() async throws { + let app = try ghosttyApplication() + app.activate() + + XCTAssertTrue(app.windows.firstMatch.waitForExistence(timeout: 5), "New window should appear") + + app.menuItems["Command Palette"].firstMatch.click() + + app.buttons + .containing(NSPredicate(format: "label CONTAINS[c] 'Close All Windows'")) + .firstMatch.click() + + XCTAssertTrue(app.windows.firstMatch.waitForNonExistence(timeout: 2), "All windows should be closed") + } +} + diff --git a/macos/GhosttyUITests/GhosttyMouseStateTests.swift b/macos/GhosttyUITests/GhosttyMouseStateTests.swift new file mode 100644 index 000000000..9bb270b8e --- /dev/null +++ b/macos/GhosttyUITests/GhosttyMouseStateTests.swift @@ -0,0 +1,53 @@ +// +// GhosttyMouseStateTests.swift +// Ghostty +// +// Created by Lukas on 19.03.2026. +// + +import XCTest + +final class GhosttyMouseStateTests: GhosttyCustomConfigCase { + override static var runsForEachTargetApplicationUIConfiguration: Bool { false } + + // https://github.com/ghostty-org/ghostty/pull/11276 + @MainActor func testSelectionFocusChange() async throws { + let app = XCUIApplication() + app.activate() + // Write dummy text to a temp file, cat it into the terminal, then clean up + let lines = (1...200).map { "Line \($0): The quick brown fox jumps over the lazy dog. Lorem ipsum dolor sit amet, consectetur adipiscing elit." } + let text = lines.joined(separator: "\n") + "\n" + let tmpFile = NSTemporaryDirectory() + "ghostty_test_dummy.txt" + try text.write(toFile: tmpFile, atomically: true, encoding: .utf8) + defer { try? FileManager.default.removeItem(atPath: tmpFile) } + + app.typeText("cat \(tmpFile)\r") + app.menuItems["Command Palette"].firstMatch.click() + + let finder = XCUIApplication(bundleIdentifier: "com.apple.finder") + finder.activate() + + app.activate() + + app.buttons + .containing(NSPredicate(format: "label CONTAINS[c] 'Clear Screen'")) + .firstMatch + .click() + let surface = app.groups["Terminal pane"] + surface + .coordinate(withNormalizedOffset: .zero) + .withOffset(.init(dx: 20, dy: 10)) + .click() + + surface + .coordinate(withNormalizedOffset: .zero) + .withOffset(.init(dx: 20, dy: surface.frame.height * 0.5)) + .hover() + + NSPasteboard.general.clearContents() + app.typeKey("c", modifierFlags: .command) + + XCTAssertEqual(NSPasteboard.general.string(forType: .string), nil, "Moving mouse shouldn't select any texts") + } +} + diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index 3129acfba..2289a3bdd 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -652,6 +652,14 @@ extension Ghostty { } private func localEventLeftMouseDown(_ event: NSEvent) -> NSEvent? { + let isCommandPaletteVisible = (event.window?.windowController as? BaseTerminalController)? + .commandPaletteIsShowing == true + guard !isCommandPaletteVisible else { + // We don't want to process events that + // are supposed to be handled by CommandPaletteView + return event + } + // We only want to process events that are on this window. guard let window, event.window != nil,