From 674ba6f645289ff925a5b5d3eff85ee73be7e977 Mon Sep 17 00:00:00 2001 From: Nikita Bobko Date: Tue, 26 Dec 2023 16:27:20 +0100 Subject: [PATCH] Implement list-workspaces https://github.com/nikitabobko/AeroSpace/issues/16 WIP --- AeroSpace.xcodeproj/project.pbxproj | 4 ++ .../Sources/Common/cmdArgs/CmdKind.swift | 1 + .../Sources/Common/cmdArgs/parseCmdArgs.swift | 2 + .../cmdArgs/query/ListWorkspacesCmdArgs.swift | 54 +++++++++++++++++++ docs/aerospace-list-workspaces.adoc | 33 ++++++++++++ docs/aerospace-move-node-to-workspace.adoc | 2 +- docs/aerospace-workspace.adoc | 2 +- docs/commands.adoc | 7 +++ src/command/parseCommand.swift | 2 + src/command/query/ListWorkspacesCommand.swift | 36 +++++++++++++ 10 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 LocalPackage/Sources/Common/cmdArgs/query/ListWorkspacesCmdArgs.swift create mode 100644 docs/aerospace-list-workspaces.adoc create mode 100644 src/command/query/ListWorkspacesCommand.swift diff --git a/AeroSpace.xcodeproj/project.pbxproj b/AeroSpace.xcodeproj/project.pbxproj index 446996b9..26b7d763 100644 --- a/AeroSpace.xcodeproj/project.pbxproj +++ b/AeroSpace.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 2E691C1E3F03B82F8507A435 /* MoveCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1376845BAB666880919B9AA2 /* MoveCommand.swift */; }; 374CE35B85B941B8F584C113 /* FlattenWorkspaceTreeCommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FDECECC773EBA30661EB8A /* FlattenWorkspaceTreeCommandTest.swift */; }; 3774857EF024E97B7AA5DE78 /* MoveNodeToWorkspaceCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25AC44D0E9450867215FCBEC /* MoveNodeToWorkspaceCommand.swift */; }; + 3AFD7EE961B97F38C0914A0C /* ListWorkspacesCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6478C499F3FB86C017D7BB /* ListWorkspacesCommand.swift */; }; 3BD6FF4CC51532977DA0C05A /* AeroAny.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82476B9BEBAC00EB9E32256F /* AeroAny.swift */; }; 42197B9C71A0CDDE65804A6A /* accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D31BF26EAFA96F675D2C14B /* accessibility.swift */; }; 43E3628E37D2439B820FFC82 /* server.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796713A1B3AEEBF4D0D180C7 /* server.swift */; }; @@ -158,6 +159,7 @@ 9857501E54FC080D2A62DCE4 /* MoveCommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveCommandTest.swift; sourceTree = ""; }; 99853C505D93E41F6531C324 /* CloseAllWindowsButCurrentCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseAllWindowsButCurrentCommand.swift; sourceTree = ""; }; 9D31BF26EAFA96F675D2C14B /* accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = accessibility.swift; sourceTree = ""; }; + 9E6478C499F3FB86C017D7BB /* ListWorkspacesCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListWorkspacesCommand.swift; sourceTree = ""; }; 9ECC990B6C2D66D343216A12 /* FullscreenCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenCommand.swift; sourceTree = ""; }; 9F6B8A501483ACBB62560101 /* TreeNodeEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeNodeEx.swift; sourceTree = ""; }; A60709210745C60D64F82D53 /* TreeNodeKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreeNodeKind.swift; sourceTree = ""; }; @@ -353,6 +355,7 @@ children = ( B0185502A3752FF618BB3D36 /* ListAppsCommand.swift */, FD6BB613EC44C1B32CE4A47E /* ListMonitorsCommand.swift */, + 9E6478C499F3FB86C017D7BB /* ListWorkspacesCommand.swift */, D83FDC62BD349F4FDB2D779A /* VersionCommand.swift */, ); path = query; @@ -564,6 +567,7 @@ D12277DFEAAAC7AE19D0B629 /* LazySequenceProtocolEx.swift in Sources */, 63D2357EADCF6137D630F7C5 /* ListAppsCommand.swift in Sources */, F69159A961FEE54847EC9A8D /* ListMonitorsCommand.swift in Sources */, + 3AFD7EE961B97F38C0914A0C /* ListWorkspacesCommand.swift in Sources */, BD6301B2CFC16FDE4223ACB8 /* MacApp.swift in Sources */, EECC59858691B99A95542D72 /* MacWindow.swift in Sources */, 6E4E235FDA41307B19F16182 /* ModeCommand.swift in Sources */, diff --git a/LocalPackage/Sources/Common/cmdArgs/CmdKind.swift b/LocalPackage/Sources/Common/cmdArgs/CmdKind.swift index 5b9300ec..e1bf41bb 100644 --- a/LocalPackage/Sources/Common/cmdArgs/CmdKind.swift +++ b/LocalPackage/Sources/Common/cmdArgs/CmdKind.swift @@ -10,6 +10,7 @@ public enum CmdKind: String, CaseIterable, Equatable { case layout case listApps = "list-apps" case listMonitors = "list-monitors" + case listWorkspaces = "list-workspaces" case mode case move = "move" case moveNodeToWorkspace = "move-node-to-workspace" diff --git a/LocalPackage/Sources/Common/cmdArgs/parseCmdArgs.swift b/LocalPackage/Sources/Common/cmdArgs/parseCmdArgs.swift index ee920c59..94df6e40 100644 --- a/LocalPackage/Sources/Common/cmdArgs/parseCmdArgs.swift +++ b/LocalPackage/Sources/Common/cmdArgs/parseCmdArgs.swift @@ -40,6 +40,8 @@ private func initSubcommands() -> [String: any SubCommandParserProtocol] { result[kind.rawValue] = noArgsSubCommandParser(ListAppsCmdArgs()) case .listMonitors: result[kind.rawValue] = SubCommandParser(parseListMonitorsCmdArgs) + case .listWorkspaces: + result[kind.rawValue] = SubCommandParser(parseListWorkspaces) case .mode: result[kind.rawValue] = SubCommandParser(parseModeCmdArgs) case .moveNodeToWorkspace: diff --git a/LocalPackage/Sources/Common/cmdArgs/query/ListWorkspacesCmdArgs.swift b/LocalPackage/Sources/Common/cmdArgs/query/ListWorkspacesCmdArgs.swift new file mode 100644 index 00000000..5fb6c37f --- /dev/null +++ b/LocalPackage/Sources/Common/cmdArgs/query/ListWorkspacesCmdArgs.swift @@ -0,0 +1,54 @@ +private let onitors = "" + +public struct ListWorkspacesCmdArgs: RawCmdArgs, CmdArgs { + public static let parser: CmdParser = cmdParser( + kind: .listWorkspaces, + allowInConfig: false, + help: """ + USAGE: list-workspaces [-h|--help] [--visible [no]] [--focused [no]] [--mouse [no]] + [--on-monitors \(onitors)] + + OPTIONS: + -h, --help Print help + --visible [no] Filter results to only print currently visible workspaces or not + --mouse [no] Filter results to only print the workspace with the mouse or not + --focused [no] Filter results to only print the focused workspace or not + --on-monitors \(onitors) Filter results to only print the workspaces that are attached to specified monitors. + \(onitors) is a comma separated list of monitor IDs + """, + options: [ + "--visible": boolFlag(\.visible), + "--mouse": boolFlag(\.mouse), + "--focused": boolFlag(\.focused), + "--on-monitors": ArgParser(\.onMonitors, parseMonitorIds) + ], + arguments: [] + ) + + public var visible: Bool? + public var mouse: Bool? + public var focused: Bool? + public var onMonitors: [Int] = [] + + public init() {} +} + +public func parseListWorkspaces(_ args: [String]) -> ParsedCmd { + parseRawCmdArgs(ListWorkspacesCmdArgs(), args) +} + +private func parseMonitorIds(arg: String, nextArgs: inout [String]) -> Parsed<[Int]> { + if let nextArg = nextArgs.nextNonFlagOrNil() { + var monitors: [Int] = [] + for monitor in nextArg.split(separator: ",").map({ String($0) }) { + if let unwrapped = Int(monitor) { + monitors.append(unwrapped - 1) + } else { + return .failure("Can't parse '\(monitor)'. It must be a number") + } + } + return .success(monitors) + } else { + return .failure("\(onitors) is mandatory") + } +} diff --git a/docs/aerospace-list-workspaces.adoc b/docs/aerospace-list-workspaces.adoc new file mode 100644 index 00000000..37fe2303 --- /dev/null +++ b/docs/aerospace-list-workspaces.adoc @@ -0,0 +1,33 @@ += aerospace-list-workspaces(1) +include::util/man-attributes.adoc[] +:manname: aerospace-list-workspaces +// tag::purpose[] +:manpurpose: Prints the list of workspaces on new lines +// end::purpose[] + +== Synopsis +[verse] +// tag::synopsis[] +list-workspaces [-h|--help] [--visible [no]] [--focused [no]] [--mouse [no]] + [--on-monitors ] + +// end::synopsis[] + +== Description + +// tag::body[] +{manpurpose} + +include::util/conditional-options-header.adoc[] + +-h, --help:: Print help +--focused [no]:: Filter results to only print the focused workspace or not +--mouse [no]:: Filter results to only print the workspace with the mouse or not +--focused [no]:: Filter results to only print the focused workspace or not +--on-monitors :: +Filter results to only print the workspaces that are attached to specified monitors. + is a comma separated list of monitor IDs + +// end::body[] + +include::util/man-footer.adoc[] diff --git a/docs/aerospace-move-node-to-workspace.adoc b/docs/aerospace-move-node-to-workspace.adoc index dc698c92..d68b297f 100644 --- a/docs/aerospace-move-node-to-workspace.adoc +++ b/docs/aerospace-move-node-to-workspace.adoc @@ -19,7 +19,7 @@ move-node-to-workspace [-h|--help] // tag::body[] {manpurpose} -`*(next|prev)*` is identical to `*workspace (next|prev)*` +`(next|prev)` is identical to `workspace (next|prev)` // end::body[] diff --git a/docs/aerospace-workspace.adoc b/docs/aerospace-workspace.adoc index f3a556e3..5ba1e28b 100644 --- a/docs/aerospace-workspace.adoc +++ b/docs/aerospace-workspace.adoc @@ -26,7 +26,7 @@ workspace [-h|--help] [--auto-back-and-forth] Focuses next or previous workspace in *the list*. * If stdin is not TTY and stdin contains non whitespace characters then *the list* is taken from stdin -* Otherwise, *the list* is all workspaces in alphabetical order +* Otherwise, *the list* is defined as all workspaces in alphabetical order include::util/conditional-options-header.adoc[] diff --git a/docs/commands.adoc b/docs/commands.adoc index d4ec672f..402003aa 100644 --- a/docs/commands.adoc +++ b/docs/commands.adoc @@ -180,6 +180,13 @@ include::aerospace-list-monitors.adoc[tags=synopsis] include::aerospace-list-monitors.adoc[tags=purpose] include::aerospace-list-monitors.adoc[tags=body] +=== list-workspaces +---- +include::aerospace-list-workspaces.adoc[tags=synopsis] +---- +include::aerospace-list-workspaces.adoc[tags=purpose] +include::aerospace-list-workspaces.adoc[tags=body] + === version ---- include::aerospace-version.adoc[tags=synopsis] diff --git a/src/command/parseCommand.swift b/src/command/parseCommand.swift index c17577cc..723cbf8a 100644 --- a/src/command/parseCommand.swift +++ b/src/command/parseCommand.swift @@ -31,6 +31,8 @@ extension CmdArgs { command = ListAppsCommand() case .listMonitors: command = ListMonitorsCommand(args: self as! ListMonitorsCmdArgs) + case .listWorkspaces: + command = ListWorkspacesCommand(args: self as! ListWorkspacesCmdArgs) case .mode: command = ModeCommand(args: self as! ModeCmdArgs) case .moveNodeToWorkspace: diff --git a/src/command/query/ListWorkspacesCommand.swift b/src/command/query/ListWorkspacesCommand.swift new file mode 100644 index 00000000..6dff3070 --- /dev/null +++ b/src/command/query/ListWorkspacesCommand.swift @@ -0,0 +1,36 @@ +import Common + +struct ListWorkspacesCommand: Command { + let info: CmdStaticInfo = ListWorkspacesCmdArgs.info + let args: ListWorkspacesCmdArgs + + func _run(_ state: CommandMutableState, stdin: String) -> Bool { + check(Thread.current.isMainThread) + var result: [Workspace] = Workspace.all + if let visible = args.visible { + result = result.filter { $0.isVisible == visible } + } + if let mouse = args.mouse { + let mouseWorkspace = mouseLocation.monitorApproximation.activeWorkspace + result = result.filter { ($0 == mouseWorkspace) == mouse } + } + if let focused = args.focused { + result = result.filter { ($0 == Workspace.focused) == focused } + } + if !args.onMonitors.isEmpty { + let sortedMonitors = sortedMonitors + var requested: Set = [] + for id in args.onMonitors { + if let monitor = sortedMonitors.getOrNil(atIndex: id) { + requested.insert(monitor.rect.topLeftCorner) + } else { + state.stdout.append("Invalid monitor ID: \(id)") + return false + } + } + result = result.filter { requested.contains($0.monitor.rect.topLeftCorner) } + } + state.stdout += result.map(\.name) + return true + } +}