Skip to content

Commit

Permalink
2/2 Add --json flag to list-* query commands
Browse files Browse the repository at this point in the history
  • Loading branch information
nikitabobko committed Oct 13, 2024
1 parent 0d71278 commit d914e20
Show file tree
Hide file tree
Showing 20 changed files with 233 additions and 64 deletions.
57 changes: 42 additions & 15 deletions Sources/AppBundle/command/format.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ extension [AeroObj] {
curCell += literal
case .value(let value):
switch value.expandFormatVar(obj: obj) {
case .success(let expanded): curCell += expanded
case .success(let expanded): curCell += expanded.toString()
case .failure(let error): errors.append(error)
}
}
Expand Down Expand Up @@ -75,21 +75,48 @@ private enum FormatVar: Equatable {
}
}

enum Primitive: Encodable {
case int(Int)
case int32(Int32)
case uint32(UInt32)
case string(String)

func toString() -> String {
switch self {
case .int(let x): x.description
case .int32(let x): x.description
case .uint32(let x): x.description
case .string(let x): x
}
}

func encode(to encoder: any Encoder) throws {
let value: Encodable = switch self {
case .int(let x): x
case .int32(let x): x
case .uint32(let x): x
case .string(let x): x
}
var container = encoder.singleValueContainer()
try container.encode(value)
}
}

private struct Cell<T> {
let value: T
let rightPadding: Bool
}

private extension String {
func expandFormatVar(obj: AeroObj) -> Result<String, String> {
extension String {
func expandFormatVar(obj: AeroObj) -> Result<Primitive, String> {
let formatVar = self.toFormatVar()
switch (obj, formatVar) {
case (_, .none): break

case (.window(let w), .workspace):
return w.workspace.flatMap(AeroObj.workspace).map(expandFormatVar) ?? .success("NULL-WOKRSPACE")
return w.workspace.flatMap(AeroObj.workspace).map(expandFormatVar) ?? .success(.string("NULL-WOKRSPACE"))
case (.window(let w), .monitor):
return w.nodeMonitor.flatMap(AeroObj.monitor).map(expandFormatVar) ?? .success("NULL-MONITOR")
return w.nodeMonitor.flatMap(AeroObj.monitor).map(expandFormatVar) ?? .success(.string("NULL-MONITOR"))
case (.window(let w), .app):
return expandFormatVar(obj: .app(w.app))
case (.window(_), .window): break
Expand All @@ -104,28 +131,28 @@ private extension String {
switch (obj, formatVar) {
case (.window(let w), .window(let f)):
return switch f {
case .windowId: .success(w.windowId.description)
case .windowTitle: .success(w.title)
case .windowId: .success(.uint32(w.windowId))
case .windowTitle: .success(.string(w.title))
}
case (.workspace(let w), .workspace(let f)):
return switch f {
case .workspaceName: .success(w.name)
case .workspaceName: .success(.string(w.name))
}
case (.monitor(let m), .monitor(let f)):
return switch f {
case .monitorId: .success(m.monitorId.map { $0 + 1 }?.description ?? "NULL-MONITOR-ID")
case .monitorName: .success(m.name)
case .monitorId: .success(m.monitorId.map { .int($0 + 1) } ?? .string("NULL-MONITOR-ID"))
case .monitorName: .success(.string(m.name))
}
case (.app(let a), .app(let f)):
return switch f {
case .appBundleId: .success(a.id ?? "NULL-APP-BUNDLE-ID")
case .appName: .success(a.name ?? "NULL-APP-NAME")
case .appPid: .success(a.pid.description)
case .appBundleId: .success(.string(a.id ?? "NULL-APP-BUNDLE-ID"))
case .appName: .success(.string(a.name ?? "NULL-APP-NAME"))
case .appPid: .success(.int32(a.pid))
}
default: break
}
if self == "newline" { return .success("\n") }
if self == "tab" { return .success("\t") }
if self == "newline" { return .success(.string("\n")) }
if self == "tab" { return .success(.string("\t")) }
return .failure("Unknown interpolation variable '\(self)'")
}

Expand Down
27 changes: 27 additions & 0 deletions Sources/AppBundle/command/formatToJson.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Common
import Foundation

extension [AeroObj] {
func formatToJson(_ format: [StringInterToken], ignoreRightPaddingVar: Bool) -> Result<String, String> {
var list: [[String: Primitive]] = []
for richObj in self {
var rawObj: [String: Primitive] = [:]
for token in format {
switch token {
case .value("right-padding") where ignoreRightPaddingVar:
break
case .literal:
break // should be spaces
case .value(let varName):
switch varName.expandFormatVar(obj: richObj) {
case .success(let expanded): rawObj[varName] = expanded
case .failure(let error): return .failure(error)
}
}
}
list.append(rawObj)
}
return JSONEncoder.aeroSpaceDefault.encodeToString(list).map(Result.success)
?? .failure("Can't encode '\(list)' to JSON")
}
}
12 changes: 4 additions & 8 deletions Sources/AppBundle/command/impl/ConfigCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,10 @@ private func getKey(_ io: CmdIo, args: ConfigCmdArgs, key: String) -> Bool {
}
}
if args.json {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes]
let _json = Result { try encoder.encode(configMap) }.flatMap {
String(data: $0, encoding: .utf8).flatMap(Result.success) ?? .failure("Can't convert json Data to String")
}
return switch _json {
case .success(let json): io.out(json)
case .failure(let error): io.err(error.localizedDescription)
if let json = JSONEncoder.aeroSpaceDefault.encodeToString(configMap) {
return io.out(json)
} else {
return io.err("Can't convert json Data to String")
}
} else {
switch configMap {
Expand Down
14 changes: 11 additions & 3 deletions Sources/AppBundle/command/impl/ListAppsCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@ struct ListAppsCommand: Command {
if args.outputOnlyCount {
return io.out("\(result.count)")
} else {
return switch result.map({ AeroObj.app($0) }).format(args.format) {
case .success(let lines): io.out(lines)
case .failure(let msg): io.err(msg)
let list = result.map { AeroObj.app($0) }
if args.json {
return switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
case .success(let json): io.out(json)
case .failure(let msg): io.err(msg)
}
} else {
return switch list.format(args.format) {
case .success(let lines): io.out(lines)
case .failure(let msg): io.err(msg)
}
}
}
}
Expand Down
14 changes: 11 additions & 3 deletions Sources/AppBundle/command/impl/ListMonitorsCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@ struct ListMonitorsCommand: Command {
if args.outputOnlyCount {
return io.out("\(result.count)")
} else {
return switch result.map({ AeroObj.monitor($0) }).format(args.format) {
case .success(let lines): io.out(lines)
case .failure(let msg): io.err(msg)
let list = result.map { AeroObj.monitor($0) }
if args.json {
return switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
case .success(let json): io.out(json)
case .failure(let msg): io.err(msg)
}
} else {
return switch list.format(args.format) {
case .success(let lines): io.out(lines)
case .failure(let msg): io.err(msg)
}
}
}
}
Expand Down
14 changes: 11 additions & 3 deletions Sources/AppBundle/command/impl/ListWindowsCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,17 @@ struct ListWindowsCommand: Command {
return io.out("\(windows.count)")
} else {
windows = windows.sorted(using: [SelectorComparator { $0.app.name ?? "" }, SelectorComparator(selector: \.title)])
return switch windows.map({ AeroObj.window($0) }).format(args.format) {
case .success(let lines): io.out(lines)
case .failure(let msg): io.err(msg)
let list = windows.map { AeroObj.window($0) }
if args.json {
return switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
case .success(let json): io.out(json)
case .failure(let msg): io.err(msg)
}
} else {
return switch list.format(args.format) {
case .success(let lines): io.out(lines)
case .failure(let msg): io.err(msg)
}
}
}
}
Expand Down
14 changes: 11 additions & 3 deletions Sources/AppBundle/command/impl/ListWorkspacesCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,17 @@ struct ListWorkspacesCommand: Command {
if args.outputOnlyCount {
return io.out("\(result.count)")
} else {
return switch result.map({ AeroObj.workspace($0) }).format(args.format) {
case .success(let lines): io.out(lines)
case .failure(let msg): io.err(msg)
let list = result.map { AeroObj.workspace($0) }
if args.json {
return switch list.formatToJson(args.format, ignoreRightPaddingVar: args._format.isEmpty) {
case .success(let json): io.out(json)
case .failure(let msg): io.err(msg)
}
} else {
return switch list.format(args.format) {
case .success(let lines): io.out(lines)
case .failure(let msg): io.err(msg)
}
}
}
}
Expand Down
14 changes: 14 additions & 0 deletions Sources/AppBundle/util/JsonEncoderEx.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation

extension JSONEncoder {
static var aeroSpaceDefault: JSONEncoder {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes]
return encoder
}

func encodeToString(_ value: Encodable) -> String? {
guard let data = Result(catching: { try encode(value) }).getOrNil() else { return nil }
return String(data: data, encoding: .utf8)
}
}
6 changes: 6 additions & 0 deletions Sources/AppBundleTests/command/ListWindowsTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,11 @@ final class ListWindowsTest: XCTestCase {
parseCommand("list-windows --all --workspace focused").errorOrNil,
"ERROR: Conflicting options: --all, --workspace")
assertNil(parseCommand("list-windows --monitor mouse").errorOrNil)

// --json
assertEquals(parseCommand("list-windows --all --count --json").errorOrNil, "ERROR: Conflicting options: --count, --json")
assertEquals(parseCommand("list-windows --all --format '%{right-padding}' --json").errorOrNil, "%{right-padding} interpolation variable is not allowed when --json is used")
assertEquals(parseCommand("list-windows --all --format '%{window-title} |' --json").errorOrNil, "Only interpolation variables and spaces are allowed in \'--format\' when \'--json\' is used")
assertNil(parseCommand("list-windows --all --format '%{window-title}' --json").errorOrNil)
}
}
6 changes: 3 additions & 3 deletions Sources/Common/cmdArgs/cmdArgsManifest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ func initSubcommands() -> [String: any SubCommandParserProtocol] {
case .layout:
result[kind.rawValue] = SubCommandParser(parseLayoutCmdArgs)
case .listApps:
result[kind.rawValue] = SubCommandParser(ListAppsCmdArgs.init)
result[kind.rawValue] = SubCommandParser(parseListAppsCmdArgs)
case .listExecEnvVars:
result[kind.rawValue] = SubCommandParser(ListExecEnvVarsCmdArgs.init)
case .listMonitors:
result[kind.rawValue] = SubCommandParser(ListMonitorsCmdArgs.init)
result[kind.rawValue] = SubCommandParser(parseListMonitorsCmdArgs)
case .listWindows:
result[kind.rawValue] = SubCommandParser(parseRawListWindowsCmdArgs)
result[kind.rawValue] = SubCommandParser(parseListWindowsCmdArgs)
case .listWorkspaces:
result[kind.rawValue] = SubCommandParser(parseListWorkspacesCmdArgs)
case .macosNativeFullscreen:
Expand Down
25 changes: 25 additions & 0 deletions Sources/Common/cmdArgs/impl/ListAppsCmdArgs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ public struct ListAppsCmdArgs: CmdArgs {
help: list_apps_help_generated,
options: [
"--macos-native-hidden": boolFlag(\.macosHidden),

// Formatting flags
"--format": ArgParser(\._format, parseFormat),
"--count": trueBoolFlag(\.outputOnlyCount),
"--json": trueBoolFlag(\.json),
],
arguments: [],
conflictingOptions: [
["--format", "--count"],
["--json", "--count"],
]
)

Expand All @@ -21,6 +25,7 @@ public struct ListAppsCmdArgs: CmdArgs {
public var macosHidden: Bool?
public var _format: [StringInterToken] = []
public var outputOnlyCount: Bool = false
public var json: Bool = false
}

public extension ListAppsCmdArgs {
Expand All @@ -34,3 +39,23 @@ public extension ListAppsCmdArgs {
: _format
}
}

public func parseListAppsCmdArgs(_ args: [String]) -> ParsedCmd<ListAppsCmdArgs> {
parseSpecificCmdArgs(ListAppsCmdArgs(rawArgs: args), args)
.flatMap { if $0.json, let msg = getErrorIfFormatIsIncompatibleWithJson($0._format) { .failure(msg) } else { .cmd($0) } }
}

func getErrorIfFormatIsIncompatibleWithJson(_ format: [StringInterToken]) -> String? {
for x in format {
switch x {
case .value("right-padding"):
return "%{right-padding} interpolation variable is not allowed when --json is used"
case .value: break // skip
case .literal(let literal):
if literal.contains(where: { $0 != " " }) {
return "Only interpolation variables and spaces are allowed in '--format' when '--json' is used"
}
}
}
return nil
}
10 changes: 10 additions & 0 deletions Sources/Common/cmdArgs/impl/ListMonitorsCmdArgs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ public struct ListMonitorsCmdArgs: CmdArgs {
options: [
"--focused": boolFlag(\.focused),
"--mouse": boolFlag(\.mouse),

// Formatting flags
"--format": ArgParser(\._format, parseFormat),
"--count": trueBoolFlag(\.outputOnlyCount),
"--json": trueBoolFlag(\.json),
],
arguments: [],
conflictingOptions: [
["--format", "--count"],
["--json", "--count"],
]
)

Expand All @@ -23,6 +27,7 @@ public struct ListMonitorsCmdArgs: CmdArgs {
public var mouse: Bool?
public var _format: [StringInterToken] = []
public var outputOnlyCount: Bool = false
public var json: Bool = false
}

public extension ListMonitorsCmdArgs {
Expand All @@ -35,3 +40,8 @@ public extension ListMonitorsCmdArgs {
: _format
}
}

public func parseListMonitorsCmdArgs(_ args: [String]) -> ParsedCmd<ListMonitorsCmdArgs> {
parseSpecificCmdArgs(ListMonitorsCmdArgs(rawArgs: args), args)
.flatMap { if $0.json, let msg = getErrorIfFormatIsIncompatibleWithJson($0._format) { .failure(msg) } else { .cmd($0) } }
}
Loading

0 comments on commit d914e20

Please sign in to comment.