From 8c9fd7aa79c4d6cc768293fa6e3726162d00c618 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 11 Aug 2026 09:15:05 -0700 Subject: [PATCH] macos: normalize command paths as file URLs #13319 #13748 Normalize command-line file arguments as file URLs internally while keeping the AppKit and FileManager string boundaries unchanged. This handles relative paths, URL-sensitive characters, and trailing directory separators consistently when matching duplicate open-file events. --- .../App/macOS/CommandLineOpenFileFilter.swift | 33 ++++++++++--------- .../CommandLineOpenFileFilterTests.swift | 21 ++++++++++++ 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/macos/Sources/App/macOS/CommandLineOpenFileFilter.swift b/macos/Sources/App/macOS/CommandLineOpenFileFilter.swift index b4a92b64e..89e7eb79d 100644 --- a/macos/Sources/App/macOS/CommandLineOpenFileFilter.swift +++ b/macos/Sources/App/macOS/CommandLineOpenFileFilter.swift @@ -4,14 +4,18 @@ import Foundation /// `-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 + private let workingDirectory: URL + private var filesToIgnore: Set init( arguments: [String], workingDirectory: String, fileExists: (String) -> Bool ) { + let workingDirectory = URL( + filePath: workingDirectory, + directoryHint: .isDirectory + ).absoluteURL.standardizedFileURL self.workingDirectory = workingDirectory guard let commandIndex = arguments.firstIndex(of: "-e") else { @@ -25,32 +29,31 @@ final class CommandLineOpenFileFilter { 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) + // reports absolute paths for the corresponding open event. Use + // file URLs internally so both forms have the same identity. + let url = Self.fileURL(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 + return fileExists(url.path) ? url : nil }) } func shouldIgnore(_ filename: String) -> Bool { - let path = Self.absolutePath(filename, relativeTo: workingDirectory) + let url = Self.fileURL(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 + return filesToIgnore.remove(url) != nil } - private static func absolutePath(_ path: String, relativeTo workingDirectory: String) -> String { + private static func fileURL(_ path: String, relativeTo workingDirectory: URL) -> URL { 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 + return URL( + filePath: expanded, + directoryHint: .notDirectory, + relativeTo: workingDirectory + ).absoluteURL.standardizedFileURL } } diff --git a/macos/Tests/CommandLineOpenFileFilterTests.swift b/macos/Tests/CommandLineOpenFileFilterTests.swift index 9a302a1c0..e1d7a820c 100644 --- a/macos/Tests/CommandLineOpenFileFilterTests.swift +++ b/macos/Tests/CommandLineOpenFileFilterTests.swift @@ -61,4 +61,25 @@ struct CommandLineOpenFileFilterTests { #expect(!filter.shouldIgnore("/tmp/finder-file.txt")) #expect(filter.shouldIgnore("/tmp/command-file.txt")) } + + @Test func normalizesFileURLs() { + let existing: Set = [ + "/tmp/project/file #100%.txt", + "/tmp/project/directory", + ] + let filter = CommandLineOpenFileFilter( + arguments: [ + "ghostty", + "-e", + "command", + "./file #100%.txt", + "./directory/", + ], + workingDirectory: "/tmp/project", + fileExists: { existing.contains($0) } + ) + + #expect(filter.shouldIgnore("/tmp/project/file #100%.txt")) + #expect(filter.shouldIgnore("/tmp/project/directory")) + } }