Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import SwiftUI

@MainActor
enum CLIWindowBridge {
private static var openedFileRequests: [String] = []
private static var filesObserver: NSObjectProtocol?
private static var revealObserver: NSObjectProtocol?
private static var snapshotObserver: NSObjectProtocol?

Expand All @@ -19,10 +21,36 @@ enum CLIWindowBridge {
AppRuntimeCenter().perform(.snapshot) { snapshot(info) }
}
}
filesObserver = IPC.observe(IPC.Name.requestFinderOpen) { info in
MainActor.assumeIsolated { openFiles(info) }
}
QuinjetSessionBridge.shared.install()
MachineTerminalBroadcastBridge.install()
}

private static func openFiles(_ info: [AnyHashable: Any]) {
let model = MachinesModel.shared
let requestID = info["requestID"] as? String ?? ""
if openedFileRequests.contains(requestID) {
IPC.post(IPC.Name.finderOpenResult, userInfo: ["opened": true, "requestID": requestID])
return
}
guard !requestID.isEmpty, let raw = info["machine"] as? String,
let id = UUID(uuidString: raw), model.knows(id)
else {
IPC.post(
IPC.Name.finderOpenResult,
userInfo: [
"opened": false, "reason": "Machine is unavailable.", "requestID": requestID,
])
return
}
FinderWindow.open(session: model.session(for: id), path: info["path"] as? String)
openedFileRequests.append(requestID)
openedFileRequests = Array(openedFileRequests.suffix(16))
IPC.post(IPC.Name.finderOpenResult, userInfo: ["opened": true, "requestID": requestID])
}

private static func fail(_ message: String) {
IPC.post(IPC.Name.revealResult, userInfo: ["ok": false, "error": message])
}
Expand Down
2 changes: 2 additions & 0 deletions Packages/Edith/Sources/EdithCLI/CommandTree.swift
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,8 @@ public enum CommandTree {
"ed machines files get": Spec(
options: ["--json", "--help", "--dry-run", "--replace", "--yes"],
arguments: [.machine, .remotePath, .localPath], destructivePolicy: .previewThenYes),
"ed machines files open": Spec(
options: ["--json"], arguments: [.machineOrLocal, .remotePath]),
"ed machines files preview": Spec(
options: ["--json", "-h", "--help", "--version"], arguments: [.machine, .remotePath]),
"ed machines files launch": Spec(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ struct MachinesFilesCommand: AsyncParsableCommand {
MachinesFilesMakeDirectoryCommand.self, MachinesFilesRemoveCommand.self,
MachinesFilesSearchCommand.self, MachinesFilesInfoCommand.self,
MachinesFilesDuplicateCommand.self, MachinesFilesUndoCommand.self,
MachineFilesPreviewCommand.self,
MachinesFilesOpenCommand.self, MachineFilesPreviewCommand.self,
MachineFilesLaunchCommand.self, MachineFilesRevealCommand.self,
MachineFilesGetManyCommand.self, MachineFilesTransferCommand.self,
],
Expand Down Expand Up @@ -349,6 +349,81 @@ struct MachineFilesPutCommand: AsyncParsableCommand {
}
}

struct MachinesFilesOpenCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "open",
abstract: "Open Edith's Files window on a machine directory.",
discussion: """
Opens a Files window in the main Edith application, starting it if needed.
With no path, browsing continues in this terminal's remembered directory.
""")

@Flag(name: .long, help: "Emit JSON on stdout.")
var json = false

@Argument(help: "Machine name, ssh alias or id.")
var machine: String

@Argument(help: "Remote directory to show. Defaults to this terminal's directory.")
var path: String?

func run() async throws {
try await execute {
let target =
machine.lowercased() == "local"
? Machine.local : try MachineResolver.machine(machine)
let directory =
path ?? MachineWorkingDirectory.load(machineID: target.id)
let progress = CLIProgress.forCommand(json: json)
if !AppBridge.mainAppIsRunning {
guard let bundle = CLIEnvironment.installedAppURL() else {
throw CLIFailure.unavailable(
"Edith is not installed", hint: "install Edith and retry")
}
try await EdithProcesses.launch(bundle)
}
let requestID = UUID().uuidString
var answer: [AnyHashable: Any]?
for _ in 0..<4 {
answer = await AppBridge.awaitReply(
IPC.Name.finderOpenResult, timeout: 3,
matching: { $0["requestID"] as? String == requestID }
) {
var info: [String: Any] = [
"machine": target.id.uuidString, "requestID": requestID,
]
if let directory { info["path"] = directory }
AppBridge.post(IPC.Name.requestFinderOpen, userInfo: info)
}
if answer != nil { break }
}
progress.end()
guard let reply = answer else {
throw AppBridge.silence("opening the Files window")
}
guard reply["opened"] as? Bool == true else {
throw CLIFailure.unavailable(
"Edith would not open a window for \(target.name)",
hint: reply["reason"] as? String)
}
report(machine: target, directory: directory)
}
}

private func report(machine target: Machine, directory: String?) {
guard !json else {
CLIOut.json(
.object([
"machine": .string(target.name),
"opened": .bool(true),
"path": .string(directory ?? ""),
]))
return
}
CLIOut.out("opened \(directory ?? "the home directory") on \(target.name)")
}
}

extension String {
func expandingTilde() -> String {
(self as NSString).expandingTildeInPath
Expand Down
2 changes: 2 additions & 0 deletions Packages/Edith/Sources/EdithKit/Core/IPC/IPC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ public enum IPC {
public static let requestCalendarEvents = IPC.scopedName(
"com.pulkit.edith.requestCalendarEvents")
public static let calendarEvents = IPC.scopedName("com.pulkit.edith.calendarEvents")
public static let requestFinderOpen = IPC.scopedName("com.pulkit.edith.requestFinderOpen")
public static let finderOpenResult = IPC.scopedName("com.pulkit.edith.finderOpenResult")
public static let requestReveal = IPC.scopedName("com.pulkit.edith.requestReveal")
public static let revealResult = IPC.scopedName("com.pulkit.edith.revealResult")
public static let requestWindowSnapshot = IPC.scopedName(
Expand Down
2 changes: 2 additions & 0 deletions Packages/Edith/Tests/EdithTests/CLIContractTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,8 @@ enum JSONContract {
JSONCase("ed machines workspace rm", ["machines", "workspace", "rm", "nope", "--json"]),
JSONCase(
"ed machines files undo", ["machines", "files", "undo", "nowhere-at-all", "--json"]),
JSONCase(
"ed machines files open", ["machines", "files", "open", "nowhere-at-all", "--json"]),
JSONCase("ed machines mounts", ["machines", "mounts", "--json"]),
JSONCase("ed machines mount", ["machines", "mount", "nowhere-at-all", "--json"]),
JSONCase("ed machines unmount", ["machines", "unmount", "nowhere-at-all", "--json"]),
Expand Down
38 changes: 38 additions & 0 deletions Packages/Edith/Tests/EdithTests/CLIFilesOpenTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Foundation
import Testing

@testable import EdithCLI
@testable import EdithKit

@Suite struct CLIFilesOpenTests {
@Test func localWindowUsesTheMainAppAndCorrelatedReply() async throws {
try await CLIProbe.inWorld { world in
CLIEnvironment.isMainAppRunning = { true }
world.answers { _ in
["opened": true, "requestID": world.posted.last?.info["requestID"] ?? ""]
}
let result = await CLIProbe.capture([
"machines", "files", "open", "local", "/tmp/demo-project", "--json",
])
#expect(result.code == 0)
#expect(result.object?["opened"] as? Bool == true)
#expect(result.object?["path"] as? String == "/tmp/demo-project")
#expect(world.posted.first?.info["machine"] as? String == Machine.localID.uuidString)
#expect(world.postedNames() == [IPC.Name.requestFinderOpen.rawValue])
if let directory = ProcessInfo.processInfo.environment["EDITH_RENDER_DUMP"] {
let url = URL(fileURLWithPath: directory).appendingPathComponent("files-open.txt")
try ("$ ed machines files open local /tmp/demo-project --json\n" + result.stdout)
.write(to: url, atomically: true, encoding: .utf8)
}
}
}

@Test func unrelatedWindowRepliesAreRejected() async {
await CLIProbe.inWorld { world in
CLIEnvironment.isMainAppRunning = { true }
world.answers { _ in ["opened": true, "requestID": "another-request"] }
let result = await CLIProbe.capture(["machines", "files", "open", "local", "--json"])
#expect(result.code == ExitCodes.unavailable)
}
}
}
2 changes: 2 additions & 0 deletions Packages/Edith/Tests/EdithTests/CLIParityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ enum UIParity {
"ed color clear": "the colour picker no longer offers a clear-history action",
"ed companion machines add":
"the companion setup no longer adds deployment machines from the app",
"ed machines files open":
"the Files window is presentation state rather than a domain operation",
"ed machines docker compose up":
"the Docker window groups by project but never runs compose",
"ed machines docker compose down":
Expand Down
2 changes: 2 additions & 0 deletions docs/cli/machines-files/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,5 @@ for that machine is already open.
- [Conventions and contracts](../conventions.md) has the full exit code and JSON
contract these pages assume.
- [All command groups](../README.md)

Use [`ed machines files open`](./open.md) to open a directory in the main application’s Files window.
16 changes: 16 additions & 0 deletions docs/cli/machines-files/open.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Open a Files window

`ed machines files open <machine> [path] [--json]` opens a directory in Edith's Files window.

The window belongs to the main application. The command starts Edith when needed and waits for the window to acknowledge the request. The background agent continues owning collection and shared state.

With no path, the command uses the directory remembered for this terminal and machine. If no directory was remembered, the window opens at the machine's home directory.

```sh
ed machines files open local ~/projects
ed machines files open workstation --json
```

A JSON success includes `machine`, `opened`, and `path`. Unknown machines fail before the application is launched. An unavailable application or an unanswered window request returns an error.

[Back to CLI index](../README.md)
Loading