Compare commits
1 Commits
main
...
refactor/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afabfc82a6 |
@ -33,7 +33,7 @@ struct CommandRouter {
|
||||
func run(argv: [String]) async -> Int32 {
|
||||
let argv = normalizeArguments(argv)
|
||||
if argv.contains("--version") || argv.contains("-V") {
|
||||
Swift.print(version)
|
||||
StdoutWriter.writeLine(version)
|
||||
return 0
|
||||
}
|
||||
if argv.count <= 1 || argv.contains("--help") || argv.contains("-h") {
|
||||
@ -46,7 +46,7 @@ struct CommandRouter {
|
||||
guard let commandName = invocation.path.last,
|
||||
let spec = specs.first(where: { $0.name == commandName })
|
||||
else {
|
||||
Swift.print("Unknown command")
|
||||
StdoutWriter.writeLine("Unknown command")
|
||||
HelpPrinter.printRoot(version: version, rootName: rootName, commands: specs)
|
||||
return 1
|
||||
}
|
||||
@ -55,17 +55,17 @@ struct CommandRouter {
|
||||
try await spec.run(invocation.parsedValues, runtime)
|
||||
return 0
|
||||
} catch {
|
||||
Swift.print(error)
|
||||
StdoutWriter.writeLine(String(describing: error))
|
||||
return 1
|
||||
}
|
||||
} catch let error as CommanderProgramError {
|
||||
Swift.print(error.description)
|
||||
StdoutWriter.writeLine(error.description)
|
||||
if case .missingSubcommand = error {
|
||||
HelpPrinter.printRoot(version: version, rootName: rootName, commands: specs)
|
||||
}
|
||||
return 1
|
||||
} catch {
|
||||
Swift.print(error)
|
||||
StdoutWriter.writeLine(String(describing: error))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,14 +26,14 @@ enum ChatsCommand {
|
||||
|
||||
if runtime.jsonOutput {
|
||||
for chat in chats {
|
||||
try JSONLines.print(ChatPayload(chat: chat))
|
||||
try StdoutWriter.writeJSONLine(ChatPayload(chat: chat))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for chat in chats {
|
||||
let last = CLIISO8601.format(chat.lastMessageAt)
|
||||
Swift.print("[\(chat.id)] \(chat.name) (\(chat.identifier)) last=\(last)")
|
||||
StdoutWriter.writeLine("[\(chat.id)] \(chat.name) (\(chat.identifier)) last=\(last)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,7 +57,7 @@ enum HistoryCommand {
|
||||
attachments: attachments,
|
||||
reactions: reactions
|
||||
)
|
||||
try JSONLines.print(payload)
|
||||
try StdoutWriter.writeJSONLine(payload)
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -65,18 +65,18 @@ enum HistoryCommand {
|
||||
for message in filtered {
|
||||
let direction = message.isFromMe ? "sent" : "recv"
|
||||
let timestamp = CLIISO8601.format(message.date)
|
||||
Swift.print("\(timestamp) [\(direction)] \(message.sender): \(message.text)")
|
||||
StdoutWriter.writeLine("\(timestamp) [\(direction)] \(message.sender): \(message.text)")
|
||||
if message.attachmentsCount > 0 {
|
||||
if showAttachments {
|
||||
let metas = try store.attachments(for: message.rowID)
|
||||
for meta in metas {
|
||||
let name = displayName(for: meta)
|
||||
Swift.print(
|
||||
StdoutWriter.writeLine(
|
||||
" attachment: name=\(name) mime=\(meta.mimeType) missing=\(meta.missing) path=\(meta.originalPath)"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Swift.print(
|
||||
StdoutWriter.writeLine(
|
||||
" (\(message.attachmentsCount) attachment\(pluralSuffix(for: message.attachmentsCount)))"
|
||||
)
|
||||
}
|
||||
|
||||
@ -91,9 +91,9 @@ enum SendCommand {
|
||||
))
|
||||
|
||||
if runtime.jsonOutput {
|
||||
try JSONLines.print(["status": "sent"])
|
||||
try StdoutWriter.writeJSONLine(["status": "sent"])
|
||||
} else {
|
||||
Swift.print("sent")
|
||||
StdoutWriter.writeLine("sent")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import Commander
|
||||
import Darwin
|
||||
import Foundation
|
||||
import IMsgCore
|
||||
|
||||
@ -90,29 +89,27 @@ enum WatchCommand {
|
||||
attachments: attachments,
|
||||
reactions: reactions
|
||||
)
|
||||
try JSONLines.print(payload)
|
||||
fflush(stdout)
|
||||
try StdoutWriter.writeJSONLine(payload)
|
||||
continue
|
||||
}
|
||||
let direction = message.isFromMe ? "sent" : "recv"
|
||||
let timestamp = CLIISO8601.format(message.date)
|
||||
Swift.print("\(timestamp) [\(direction)] \(message.sender): \(message.text)")
|
||||
StdoutWriter.writeLine("\(timestamp) [\(direction)] \(message.sender): \(message.text)")
|
||||
if message.attachmentsCount > 0 {
|
||||
if showAttachments {
|
||||
let metas = try store.attachments(for: message.rowID)
|
||||
for meta in metas {
|
||||
let name = displayName(for: meta)
|
||||
Swift.print(
|
||||
StdoutWriter.writeLine(
|
||||
" attachment: name=\(name) mime=\(meta.mimeType) missing=\(meta.missing) path=\(meta.originalPath)"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Swift.print(
|
||||
StdoutWriter.writeLine(
|
||||
" (\(message.attachmentsCount) attachment\(pluralSuffix(for: message.attachmentsCount)))"
|
||||
)
|
||||
}
|
||||
}
|
||||
fflush(stdout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,13 +4,13 @@ import Foundation
|
||||
struct HelpPrinter {
|
||||
static func printRoot(version: String, rootName: String, commands: [CommandSpec]) {
|
||||
for line in renderRoot(version: version, rootName: rootName, commands: commands) {
|
||||
Swift.print(line)
|
||||
StdoutWriter.writeLine(line)
|
||||
}
|
||||
}
|
||||
|
||||
static func printCommand(rootName: String, spec: CommandSpec) {
|
||||
for line in renderCommand(rootName: rootName, spec: spec) {
|
||||
Swift.print(line)
|
||||
StdoutWriter.writeLine(line)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ enum JSONLines {
|
||||
static func print<T: Encodable>(_ value: T) throws {
|
||||
let line = try encode(value)
|
||||
if !line.isEmpty {
|
||||
Swift.print(line)
|
||||
StdoutWriter.writeLine(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -286,8 +286,6 @@ private func buildMessagePayload(
|
||||
}
|
||||
|
||||
private final class RPCWriter: RPCOutput, @unchecked Sendable {
|
||||
private let queue = DispatchQueue(label: "imsg.rpc.writer")
|
||||
|
||||
func sendResponse(id: Any, result: Any) {
|
||||
send(["jsonrpc": "2.0", "id": id, "result": result])
|
||||
}
|
||||
@ -306,21 +304,15 @@ private final class RPCWriter: RPCOutput, @unchecked Sendable {
|
||||
}
|
||||
|
||||
private func send(_ object: Any) {
|
||||
queue.sync {
|
||||
do {
|
||||
let data = try JSONSerialization.data(withJSONObject: object, options: [])
|
||||
if let output = String(data: data, encoding: .utf8) {
|
||||
FileHandle.standardOutput.write(Data(output.utf8))
|
||||
FileHandle.standardOutput.write(Data("\n".utf8))
|
||||
}
|
||||
} catch {
|
||||
if let fallback =
|
||||
"{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32603,\"message\":\"write failed\"}}\n"
|
||||
.data(using: .utf8)
|
||||
{
|
||||
FileHandle.standardOutput.write(fallback)
|
||||
}
|
||||
do {
|
||||
let data = try JSONSerialization.data(withJSONObject: object, options: [])
|
||||
if let output = String(data: data, encoding: .utf8) {
|
||||
StdoutWriter.writeLine(output)
|
||||
}
|
||||
} catch {
|
||||
StdoutWriter.writeLine(
|
||||
"{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32603,\"message\":\"write failed\"}}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
Sources/imsg/StdoutWriter.swift
Normal file
24
Sources/imsg/StdoutWriter.swift
Normal file
@ -0,0 +1,24 @@
|
||||
import Dispatch
|
||||
import Foundation
|
||||
|
||||
enum StdoutWriter {
|
||||
private static let queue = DispatchQueue(label: "imsg.stdout.writer")
|
||||
|
||||
private static let jsonEncoder: JSONEncoder = {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.withoutEscapingSlashes]
|
||||
return encoder
|
||||
}()
|
||||
|
||||
static func writeLine(_ line: String) {
|
||||
queue.sync {
|
||||
FileHandle.standardOutput.write(Data((line + "\n").utf8))
|
||||
}
|
||||
}
|
||||
|
||||
static func writeJSONLine<T: Encodable>(_ value: T) throws {
|
||||
let data = try jsonEncoder.encode(value)
|
||||
guard let line = String(data: data, encoding: .utf8), !line.isEmpty else { return }
|
||||
writeLine(line)
|
||||
}
|
||||
}
|
||||
@ -4,25 +4,31 @@ import Testing
|
||||
@testable import imsg
|
||||
|
||||
@Test
|
||||
func commandRouterPrintsVersionFromEnv() async throws {
|
||||
func commandRouterPrintsVersionFromEnv() async {
|
||||
setenv("IMSG_VERSION", "9.9.9-test", 1)
|
||||
defer { unsetenv("IMSG_VERSION") }
|
||||
let router = CommandRouter()
|
||||
#expect(router.version == "9.9.9-test")
|
||||
let status = await router.run(argv: ["imsg", "--version"])
|
||||
let (_, status) = await StdoutCapture.capture {
|
||||
await router.run(argv: ["imsg", "--version"])
|
||||
}
|
||||
#expect(status == 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
func commandRouterPrintsHelp() async {
|
||||
let router = CommandRouter()
|
||||
let status = await router.run(argv: ["imsg", "--help"])
|
||||
let (_, status) = await StdoutCapture.capture {
|
||||
await router.run(argv: ["imsg", "--help"])
|
||||
}
|
||||
#expect(status == 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
func commandRouterUnknownCommand() async {
|
||||
let router = CommandRouter()
|
||||
let status = await router.run(argv: ["imsg", "nope"])
|
||||
let (_, status) = await StdoutCapture.capture {
|
||||
await router.run(argv: ["imsg", "nope"])
|
||||
}
|
||||
#expect(status == 1)
|
||||
}
|
||||
|
||||
@ -100,7 +100,9 @@ func chatsCommandRunsWithJsonOutput() async throws {
|
||||
flags: ["jsonOutput"]
|
||||
)
|
||||
let runtime = RuntimeOptions(parsedValues: values)
|
||||
try await ChatsCommand.spec.run(values, runtime)
|
||||
_ = try await StdoutCapture.capture {
|
||||
try await ChatsCommand.spec.run(values, runtime)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -112,7 +114,9 @@ func historyCommandRunsWithChatID() async throws {
|
||||
flags: ["jsonOutput"]
|
||||
)
|
||||
let runtime = RuntimeOptions(parsedValues: values)
|
||||
try await HistoryCommand.spec.run(values, runtime)
|
||||
_ = try await StdoutCapture.capture {
|
||||
try await HistoryCommand.spec.run(values, runtime)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -124,7 +128,9 @@ func historyCommandRunsWithAttachmentsNonJson() async throws {
|
||||
flags: ["attachments"]
|
||||
)
|
||||
let runtime = RuntimeOptions(parsedValues: values)
|
||||
try await HistoryCommand.spec.run(values, runtime)
|
||||
_ = try await StdoutCapture.capture {
|
||||
try await HistoryCommand.spec.run(values, runtime)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -136,7 +142,9 @@ func chatsCommandRunsWithPlainOutput() async throws {
|
||||
flags: []
|
||||
)
|
||||
let runtime = RuntimeOptions(parsedValues: values)
|
||||
try await ChatsCommand.spec.run(values, runtime)
|
||||
_ = try await StdoutCapture.capture {
|
||||
try await ChatsCommand.spec.run(values, runtime)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -204,7 +212,9 @@ func watchCommandRejectsInvalidDebounce() async {
|
||||
)
|
||||
let runtime = RuntimeOptions(parsedValues: values)
|
||||
do {
|
||||
try await WatchCommand.spec.run(values, runtime)
|
||||
_ = try await StdoutCapture.capture {
|
||||
try await WatchCommand.spec.run(values, runtime)
|
||||
}
|
||||
#expect(Bool(false))
|
||||
} catch let error as ParsedValuesError {
|
||||
#expect(error.description.contains("Invalid value"))
|
||||
@ -251,12 +261,14 @@ func watchCommandRunsWithStubStream() async throws {
|
||||
continuation.finish()
|
||||
}
|
||||
}
|
||||
try await WatchCommand.run(
|
||||
values: values,
|
||||
runtime: runtime,
|
||||
storeFactory: { _ in store },
|
||||
streamProvider: streamProvider
|
||||
)
|
||||
_ = try await StdoutCapture.capture {
|
||||
try await WatchCommand.run(
|
||||
values: values,
|
||||
runtime: runtime,
|
||||
storeFactory: { _ in store },
|
||||
streamProvider: streamProvider
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -320,12 +332,14 @@ func watchCommandRunsWithJsonOutput() async throws {
|
||||
continuation.finish()
|
||||
}
|
||||
}
|
||||
try await WatchCommand.run(
|
||||
values: values,
|
||||
runtime: runtime,
|
||||
storeFactory: { _ in store },
|
||||
streamProvider: streamProvider
|
||||
)
|
||||
_ = try await StdoutCapture.capture {
|
||||
try await WatchCommand.run(
|
||||
values: values,
|
||||
runtime: runtime,
|
||||
storeFactory: { _ in store },
|
||||
streamProvider: streamProvider
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Loading…
Reference in New Issue
Block a user