Skip to content

Commit 909e4a9

Browse files
committed
Implemented 'reveal in finder' environment action
This action is optional to implement and is just 'nil' on platforms that don't support such an action
1 parent 684b714 commit 909e4a9

File tree

8 files changed

+127
-0
lines changed

8 files changed

+127
-0
lines changed

Sources/AppKitBackend/AppKitBackend.swift

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public final class AppKitBackend: AppBackend {
2222
public let defaultToggleStyle = ToggleStyle.button
2323
public let requiresImageUpdateOnScaleFactorChange = false
2424
public let menuImplementationStyle = MenuImplementationStyle.dynamicPopover
25+
public let canRevealFiles = true
2526

2627
public var scrollBarWidth: Int {
2728
// We assume that all scrollers have their controlSize set to `.regular` by default.
@@ -123,6 +124,10 @@ public final class AppKitBackend: AppBackend {
123124
NSWorkspace.shared.open(url)
124125
}
125126

127+
public func revealFile(_ url: URL) throws {
128+
NSWorkspace.shared.activateFileViewerSelecting([url])
129+
}
130+
126131
private static func renderMenuItems(_ items: [ResolvedMenu.Item]) -> [NSMenuItem] {
127132
items.map { item in
128133
switch item {

Sources/Gtk3Backend/Gtk3Backend.swift

+37
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public final class Gtk3Backend: AppBackend {
3131
public let defaultToggleStyle = ToggleStyle.button
3232
public let requiresImageUpdateOnScaleFactorChange = true
3333
public let menuImplementationStyle = MenuImplementationStyle.dynamicPopover
34+
public let canRevealFiles = true
3435

3536
var gtkApp: Application
3637

@@ -261,6 +262,42 @@ public final class Gtk3Backend: AppBackend {
261262
}
262263
}
263264

265+
public func revealFile(_ url: URL) throws {
266+
var success = false
267+
268+
#if !os(Windows)
269+
let fileURI = url.absoluteString.replacingOccurrences(
270+
of: ",",
271+
with: "\\,"
272+
)
273+
let process = Process()
274+
process.arguments = [
275+
"dbus-send", "--print-reply",
276+
"--dest=org.freedesktop.FileManager1",
277+
"/org/freedesktop/FileManager1",
278+
"org.freedesktop.FileManager1.ShowItems",
279+
"array:string:\(fileURI)",
280+
"string:"
281+
]
282+
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
283+
284+
do {
285+
try process.run()
286+
process.waitUntilExit()
287+
288+
success = process.terminationStatus == 0
289+
} catch {
290+
// Fall through to fallback
291+
}
292+
#endif
293+
294+
if !success {
295+
// Fall back to opening the parent directory without highlighting
296+
// the file.
297+
try openExternalURL(url.deletingLastPathComponent())
298+
}
299+
}
300+
264301
class ThreadActionContext {
265302
var action: () -> Void
266303

Sources/GtkBackend/GtkBackend.swift

+34
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public final class GtkBackend: AppBackend {
3131
public let defaultToggleStyle = ToggleStyle.button
3232
public let requiresImageUpdateOnScaleFactorChange = false
3333
public let menuImplementationStyle = MenuImplementationStyle.dynamicPopover
34+
public let canRevealFiles = true
3435

3536
var gtkApp: Application
3637

@@ -164,6 +165,39 @@ public final class GtkBackend: AppBackend {
164165
gtk_show_uri(nil, url.absoluteString, guint(GDK_CURRENT_TIME))
165166
}
166167

168+
public func revealFile(_ url: URL) throws {
169+
var success = false
170+
171+
#if !os(Windows)
172+
let fileURI = url.absoluteString.replacingOccurrences(
173+
of: ",",
174+
with: "\\,"
175+
)
176+
let process = Process()
177+
process.arguments = [
178+
"dbus-send", "--print-reply",
179+
"--dest=org.freedesktop.FileManager1",
180+
"/org/freedesktop/FileManager1",
181+
"org.freedesktop.FileManager1.ShowItems",
182+
"array:string:\(fileURI)",
183+
"string:"
184+
]
185+
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
186+
187+
do {
188+
try process.run()
189+
process.waitUntilExit()
190+
191+
success = process.terminationStatus == 0
192+
} catch {
193+
break
194+
}
195+
#endif
196+
197+
// Fall back to opening the parent directory without highlighting the file.
198+
try openExternalURL(url.deletingLastPathComponent())
199+
}
200+
167201
private func renderMenu(
168202
_ menu: ResolvedMenu,
169203
actionMap: any GActionMap,

Sources/SwiftCrossUI/Backend/AppBackend.swift

+12
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ public protocol AppBackend {
8888
/// are called.
8989
var menuImplementationStyle: MenuImplementationStyle { get }
9090

91+
/// Whether the backend can reveal files in the system file manager or not.
92+
/// Mobile backends generally can't.
93+
var canRevealFiles: Bool { get }
94+
9195
/// Often in UI frameworks (such as Gtk), code is run in a callback
9296
/// after starting the app, and hence this generic root window creation
9397
/// API must reflect that. This is always the first method to be called
@@ -201,6 +205,10 @@ public protocol AppBackend {
201205
/// URL's protocol.
202206
func openExternalURL(_ url: URL) throws
203207

208+
/// Reveals a file in the system's file manager. This opens
209+
/// the file's enclosing directory and highlighting the file.
210+
func revealFile(_ url: URL) throws
211+
204212
/// Shows a widget after it has been created or updated (may be unnecessary
205213
/// for some backends). Predominantly used by ``ViewGraphNode`` after
206214
/// propagating updates.
@@ -549,6 +557,10 @@ extension AppBackend {
549557
todo()
550558
}
551559

560+
public func revealFile(_ url: URL) throws {
561+
todo()
562+
}
563+
552564
// MARK: Application
553565

554566
public func setApplicationMenu(_ submenus: [ResolvedMenu.Submenu]) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Foundation
2+
3+
/// Reveals a file in the system's file manager. This opens
4+
/// the file's enclosing directory and highlighting the file.
5+
public struct RevealFileAction {
6+
let action: (URL) -> Void
7+
8+
init?<Backend: AppBackend>(backend: Backend) {
9+
guard backend.canRevealFiles else {
10+
return nil
11+
}
12+
13+
action = { file in
14+
do {
15+
try backend.revealFile(file)
16+
} catch {
17+
print("warning: Failed to reveal file: \(error)")
18+
}
19+
}
20+
}
21+
22+
public func callAsFunction(_ file: URL) {
23+
action(file)
24+
}
25+
}

Sources/SwiftCrossUI/Environment/EnvironmentValues.swift

+11
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,17 @@ public struct EnvironmentValues {
119119
)
120120
}
121121

122+
/// Reveals a file in the system's file manager. This opens
123+
/// the file's enclosing directory and highlighting the file.
124+
///
125+
/// `nil` on platforms that don't support revealing files, e.g.
126+
/// iOS.
127+
public var revealFile: RevealFileAction? {
128+
return RevealFileAction(
129+
backend: backend
130+
)
131+
}
132+
122133
/// Creates the default environment.
123134
init<Backend: AppBackend>(backend: Backend) {
124135
self.backend = backend

Sources/UIKitBackend/UIKitBackend.swift

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public final class UIKitBackend: AppBackend {
2222

2323
public let requiresImageUpdateOnScaleFactorChange = false
2424

25+
public let canRevealFiles = false
26+
2527
var onTraitCollectionChange: (() -> Void)?
2628

2729
private let appDelegateClass: ApplicationDelegate.Type

Sources/WinUIBackend/WinUIBackend.swift

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public final class WinUIBackend: AppBackend {
4141
public let defaultToggleStyle = ToggleStyle.button
4242
public let requiresImageUpdateOnScaleFactorChange = false
4343
public let menuImplementationStyle = MenuImplementationStyle.dynamicPopover
44+
public let canRevealFiles = false
4445

4546
public var scrollBarWidth: Int {
4647
12

0 commit comments

Comments
 (0)