mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-25 00:21:46 +00:00
macos: ignore -e arguments as open files
Fixes #13319 AppKit treats existing positional arguments as documents, causing paths passed to a child command after -e to open an extra terminal surface. We now process args ourselves during openFile callbacks to ignore file paths after `-e`. There isn't a way to avoid this I can find because AppKit processes argc/argv from the main entrypoint and that can't be overridden.
This commit is contained in:
@@ -118,6 +118,7 @@
|
||||
membershipExceptions = (
|
||||
App/macOS/AppDelegate.swift,
|
||||
"App/macOS/AppDelegate+Ghostty.swift",
|
||||
App/macOS/CommandLineOpenFileFilter.swift,
|
||||
App/macOS/main.swift,
|
||||
App/macOS/MainMenu.xib,
|
||||
Features/About/About.xib,
|
||||
|
||||
@@ -92,6 +92,15 @@ class AppDelegate: NSObject,
|
||||
/// seconds since the process was launched.
|
||||
private var applicationLaunchTime: TimeInterval = 0
|
||||
|
||||
/// AppKit treats positional command-line arguments as documents to open. This
|
||||
/// filter consumes the corresponding open-file events for arguments following
|
||||
/// `-e`. It is initialized lazily because most launches never open a file.
|
||||
private lazy var commandLineOpenFileFilter = CommandLineOpenFileFilter(
|
||||
arguments: CommandLine.arguments,
|
||||
workingDirectory: FileManager.default.currentDirectoryPath,
|
||||
fileExists: { FileManager.default.fileExists(atPath: $0) }
|
||||
)
|
||||
|
||||
/// This is the current configuration from the Ghostty configuration that we need.
|
||||
private var derivedConfig: DerivedConfig = DerivedConfig()
|
||||
|
||||
@@ -435,6 +444,14 @@ class AppDelegate: NSObject,
|
||||
}
|
||||
|
||||
func application(_ sender: NSApplication, openFile filename: String) -> Bool {
|
||||
// `-e` makes existing path arguments part of the child command, but
|
||||
// AppKit also reports those paths as documents to open. Only consume
|
||||
// matching command arguments so unrelated Finder or Dock requests work.
|
||||
if commandLineOpenFileFilter.shouldIgnore(filename) {
|
||||
Self.logger.debug("ignoring command argument open-file event path=\(filename, privacy: .public)")
|
||||
return true
|
||||
}
|
||||
|
||||
// Ghostty will validate as well but we can avoid creating an entirely new
|
||||
// surface by doing our own validation here. We can also show a useful error
|
||||
// this way.
|
||||
|
||||
56
macos/Sources/App/macOS/CommandLineOpenFileFilter.swift
Normal file
56
macos/Sources/App/macOS/CommandLineOpenFileFilter.swift
Normal file
@@ -0,0 +1,56 @@
|
||||
import Foundation
|
||||
|
||||
/// Filters the open-file events AppKit creates from command arguments following
|
||||
/// `-e`. Each matching event is consumed once so later requests to open the same
|
||||
/// file are handled normally.
|
||||
final class CommandLineOpenFileFilter {
|
||||
private let workingDirectory: String
|
||||
private var filesToIgnore: Set<String>
|
||||
|
||||
init(
|
||||
arguments: [String],
|
||||
workingDirectory: String,
|
||||
fileExists: (String) -> Bool
|
||||
) {
|
||||
self.workingDirectory = workingDirectory
|
||||
|
||||
guard let commandIndex = arguments.firstIndex(of: "-e") else {
|
||||
self.filesToIgnore = []
|
||||
return
|
||||
}
|
||||
|
||||
// Ghostty treats every argument following `-e` as part of the child
|
||||
// command. Existing paths in that suffix are the arguments AppKit can
|
||||
// independently turn into open-file events.
|
||||
self.filesToIgnore = Set(arguments[arguments.index(after: commandIndex)...]
|
||||
.compactMap { argument in
|
||||
// Command arguments can be relative, while AppKit normally
|
||||
// reports absolute paths for the corresponding open event.
|
||||
let path = Self.absolutePath(argument, relativeTo: workingDirectory)
|
||||
|
||||
// Ignore only paths that exist during launch. A non-path
|
||||
// argument cannot produce the duplicate event and retaining it
|
||||
// could suppress a legitimate open if that path appears later.
|
||||
return fileExists(path) ? path : nil
|
||||
})
|
||||
}
|
||||
|
||||
func shouldIgnore(_ filename: String) -> Bool {
|
||||
let path = Self.absolutePath(filename, relativeTo: workingDirectory)
|
||||
|
||||
// Consume each match once. Later requests to open the same file may
|
||||
// come from Finder, the Dock, or another invocation and must proceed.
|
||||
return filesToIgnore.remove(path) != nil
|
||||
}
|
||||
|
||||
private static func absolutePath(_ path: String, relativeTo workingDirectory: String) -> String {
|
||||
let expanded = (path as NSString).expandingTildeInPath
|
||||
let absolute = if (expanded as NSString).isAbsolutePath {
|
||||
expanded
|
||||
} else {
|
||||
(workingDirectory as NSString).appendingPathComponent(expanded)
|
||||
}
|
||||
|
||||
return (absolute as NSString).standardizingPath
|
||||
}
|
||||
}
|
||||
64
macos/Tests/CommandLineOpenFileFilterTests.swift
Normal file
64
macos/Tests/CommandLineOpenFileFilterTests.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
import Testing
|
||||
@testable import Ghostty
|
||||
|
||||
@Suite
|
||||
struct CommandLineOpenFileFilterTests {
|
||||
@Test func requiresExecuteFlag() {
|
||||
let filter = CommandLineOpenFileFilter(
|
||||
arguments: ["ghostty", "/tmp/file.txt"],
|
||||
workingDirectory: "/tmp",
|
||||
fileExists: { _ in true }
|
||||
)
|
||||
|
||||
#expect(!filter.shouldIgnore("/tmp/file.txt"))
|
||||
}
|
||||
|
||||
@Test func ignoresExistingPathsAfterExecuteFlag() {
|
||||
let existing: Set<String> = [
|
||||
"/usr/bin/vim",
|
||||
"/tmp/project/file.txt",
|
||||
"/tmp/other.txt",
|
||||
]
|
||||
let filter = CommandLineOpenFileFilter(
|
||||
arguments: [
|
||||
"ghostty",
|
||||
"/tmp/before.txt",
|
||||
"-e",
|
||||
"/usr/bin/vim",
|
||||
"./file.txt",
|
||||
"../other.txt",
|
||||
"missing.txt",
|
||||
],
|
||||
workingDirectory: "/tmp/project",
|
||||
fileExists: { existing.contains($0) }
|
||||
)
|
||||
|
||||
#expect(!filter.shouldIgnore("/tmp/before.txt"))
|
||||
#expect(filter.shouldIgnore("/usr/bin/vim"))
|
||||
#expect(filter.shouldIgnore("/tmp/project/file.txt"))
|
||||
#expect(filter.shouldIgnore("/tmp/other.txt"))
|
||||
#expect(!filter.shouldIgnore("/tmp/project/missing.txt"))
|
||||
}
|
||||
|
||||
@Test func ignoresEachPathOnce() {
|
||||
let filter = CommandLineOpenFileFilter(
|
||||
arguments: ["ghostty", "-e", "./file.txt"],
|
||||
workingDirectory: "/tmp/project",
|
||||
fileExists: { $0 == "/tmp/project/file.txt" }
|
||||
)
|
||||
|
||||
#expect(filter.shouldIgnore("./file.txt"))
|
||||
#expect(!filter.shouldIgnore("/tmp/project/file.txt"))
|
||||
}
|
||||
|
||||
@Test func preservesUnrelatedOpenFileRequests() {
|
||||
let filter = CommandLineOpenFileFilter(
|
||||
arguments: ["ghostty", "-e", "vim", "/tmp/command-file.txt"],
|
||||
workingDirectory: "/tmp",
|
||||
fileExists: { $0 == "/tmp/command-file.txt" }
|
||||
)
|
||||
|
||||
#expect(!filter.shouldIgnore("/tmp/finder-file.txt"))
|
||||
#expect(filter.shouldIgnore("/tmp/command-file.txt"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user