From 2fcf32d031f7ab1492fffd305ceb989dd8f37e98 Mon Sep 17 00:00:00 2001 From: Nikita Bobko Date: Sun, 13 Oct 2024 00:51:43 +0200 Subject: [PATCH] Implement summon-workspace command _fixes https://github.com/nikitabobko/AeroSpace/issues/338 https://github.com/nikitabobko/AeroSpace/issues/186 The command is a replacement for planned `move-workspace-to-monitor --workspace focused`. The problem with `move-workspace-to-monitor` is that, unlike `move-workspace-to-monitor next|prev`, it changes the focused workspace. That's why I think it should be a separate command with its own semantic --- Sources/AppBundle/command/cmdManifest.swift | 2 + .../command/impl/SummonWorkspaceCommand.swift | 17 ++++++++ .../command/SummonWorkspaceCommandTest.swift | 11 +++++ .../Cli/subcommandDescriptionsGenerated.swift | 3 +- Sources/Common/cmdArgs/cmdArgsManifest.swift | 3 ++ .../cmdArgs/impl/SummonWorkspaceCmdArgs.swift | 23 +++++++++++ Sources/Common/cmdHelpGenerated.swift | 3 ++ docs/aerospace-summon-workspace.adoc | 41 +++++++++++++++++++ docs/commands.adoc | 7 ++++ grammar/commands-bnf-grammar.txt | 2 + 10 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 Sources/AppBundle/command/impl/SummonWorkspaceCommand.swift create mode 100644 Sources/AppBundleTests/command/SummonWorkspaceCommandTest.swift create mode 100644 Sources/Common/cmdArgs/impl/SummonWorkspaceCmdArgs.swift create mode 100644 docs/aerospace-summon-workspace.adoc diff --git a/Sources/AppBundle/command/cmdManifest.swift b/Sources/AppBundle/command/cmdManifest.swift index 3bb2a461..d6d0b765 100644 --- a/Sources/AppBundle/command/cmdManifest.swift +++ b/Sources/AppBundle/command/cmdManifest.swift @@ -64,6 +64,8 @@ extension CmdArgs { command = ResizeCommand(args: self as! ResizeCmdArgs) case .split: command = SplitCommand(args: self as! SplitCmdArgs) + case .summonWorkspace: + command = SummonWorkspaceCommand(args: self as! SummonWorkspaceCmdArgs) case .serverVersionInternalCommand: command = ServerVersionInternalCommandCommand() case .triggerBinding: diff --git a/Sources/AppBundle/command/impl/SummonWorkspaceCommand.swift b/Sources/AppBundle/command/impl/SummonWorkspaceCommand.swift new file mode 100644 index 00000000..97a324d4 --- /dev/null +++ b/Sources/AppBundle/command/impl/SummonWorkspaceCommand.swift @@ -0,0 +1,17 @@ +import AppKit +import Common + +struct SummonWorkspaceCommand: Command { + let args: SummonWorkspaceCmdArgs + + func run(_ env: CmdEnv, _ io: CmdIo) -> Bool { + check(Thread.current.isMainThread) + let workspace = Workspace.get(byName: args.target.val.raw) + let monitor = focus.workspace.workspaceMonitor + if monitor.activeWorkspace == workspace { + io.err("Workspace '\(workspace.name)' is already visible on the focused monitor. Tip: use --fail-if-noop to exit with non-zero code") + return !args.failIfNoop + } + return monitor.setActiveWorkspace(workspace) && workspace.focusWorkspace() + } +} diff --git a/Sources/AppBundleTests/command/SummonWorkspaceCommandTest.swift b/Sources/AppBundleTests/command/SummonWorkspaceCommandTest.swift new file mode 100644 index 00000000..edd37faa --- /dev/null +++ b/Sources/AppBundleTests/command/SummonWorkspaceCommandTest.swift @@ -0,0 +1,11 @@ +@testable import AppBundle +import Common +import XCTest + +final class SummonWorkspaceCommandTest: XCTestCase { + override func setUpWithError() throws { setUpWorkspacesForTests() } + + func testParse() { + assertEquals(parseCommand("summon-workspace").errorOrNil, "ERROR: Argument '' is mandatory") + } +} diff --git a/Sources/Cli/subcommandDescriptionsGenerated.swift b/Sources/Cli/subcommandDescriptionsGenerated.swift index 0042287c..80f56b93 100644 --- a/Sources/Cli/subcommandDescriptionsGenerated.swift +++ b/Sources/Cli/subcommandDescriptionsGenerated.swift @@ -26,11 +26,12 @@ let subcommandDescriptions = [ [" move-mouse", "Move mouse to the requested position"], [" move-node-to-monitor", "Move window to monitor targeted by relative direction, by order, or by pattern"], [" move-node-to-workspace", "Move the focused window to the specified workspace"], - [" move-workspace-to-monitor", "Move the focused workspace to the next or previous monitor"], + [" move-workspace-to-monitor", "Move the focused workspace to the next or previous monitor."], [" move", "Move the focused window in the given direction"], [" reload-config", "Reload currently active config"], [" resize", "Resize the focused window"], [" split", "Split focused window"], + [" summon-workspace", "Move the requested workspace to the focused monitor."], [" trigger-binding", "Trigger AeroSpace binding as if it was pressed by user"], [" workspace-back-and-forth", "Switch between the focused workspace and previously focused workspace back and forth"], [" workspace", "Focus the specified workspace"], diff --git a/Sources/Common/cmdArgs/cmdArgsManifest.swift b/Sources/Common/cmdArgs/cmdArgsManifest.swift index c0d1c30a..a75e3bcd 100644 --- a/Sources/Common/cmdArgs/cmdArgsManifest.swift +++ b/Sources/Common/cmdArgs/cmdArgsManifest.swift @@ -31,6 +31,7 @@ public enum CmdKind: String, CaseIterable, Equatable { case reloadConfig = "reload-config" case resize case split + case summonWorkspace = "summon-workspace" case triggerBinding = "trigger-binding" case workspace case workspaceBackAndForth = "workspace-back-and-forth" @@ -106,6 +107,8 @@ func initSubcommands() -> [String: any SubCommandParserProtocol] { result[kind.rawValue] = SubCommandParser(parseResizeCmdArgs) case .split: result[kind.rawValue] = SubCommandParser(parseSplitCmdArgs) + case .summonWorkspace: + result[kind.rawValue] = SubCommandParser(SummonWorkspaceCmdArgs.init) case .serverVersionInternalCommand: if isServer { result[kind.rawValue] = SubCommandParser(ServerVersionInternalCommandCmdArgs.init) diff --git a/Sources/Common/cmdArgs/impl/SummonWorkspaceCmdArgs.swift b/Sources/Common/cmdArgs/impl/SummonWorkspaceCmdArgs.swift new file mode 100644 index 00000000..c4143cc9 --- /dev/null +++ b/Sources/Common/cmdArgs/impl/SummonWorkspaceCmdArgs.swift @@ -0,0 +1,23 @@ +public struct SummonWorkspaceCmdArgs: CmdArgs { + public let rawArgs: EquatableNoop<[String]> + public init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) } + public static let parser: CmdParser = cmdParser( + kind: .summonWorkspace, + allowInConfig: true, + help: workspace_help_generated, + options: [ + "--fail-if-noop": trueBoolFlag(\.failIfNoop), + ], + arguments: [newArgParser(\.target, parseWorkspaceName, mandatoryArgPlaceholder: "")] + ) + + public var windowId: UInt32? // unused + public var workspaceName: WorkspaceName? // unused + + public var target: Lateinit = .uninitialized + public var failIfNoop: Bool = false +} + +private func parseWorkspaceName(arg: String, nextArgs: inout [String]) -> Parsed { + WorkspaceName.parse(arg) +} diff --git a/Sources/Common/cmdHelpGenerated.swift b/Sources/Common/cmdHelpGenerated.swift index c22ec568..ebdfbc4a 100644 --- a/Sources/Common/cmdHelpGenerated.swift +++ b/Sources/Common/cmdHelpGenerated.swift @@ -118,6 +118,9 @@ let resize_help_generated = """ let split_help_generated = """ USAGE: split [-h|--help] [--window-id ] (horizontal|vertical|opposite) """ +let summon_workspace_help_generated = """ + USAGE: summon-workspace [-h|--help] [--fail-if-noop] + """ let trigger_binding_help_generated = """ USAGE: trigger-binding [-h|--help] --mode """ diff --git a/docs/aerospace-summon-workspace.adoc b/docs/aerospace-summon-workspace.adoc new file mode 100644 index 00000000..67f7388d --- /dev/null +++ b/docs/aerospace-summon-workspace.adoc @@ -0,0 +1,41 @@ += aerospace-summon-workspace(1) +include::util/man-attributes.adoc[] +// tag::purpose[] +:manpurpose: Move the requested workspace to the focused monitor. +// end::purpose[] +:manname: aerospace-summon-workspace + +// =========================================================== Synopsis +== Synopsis +[verse] +// tag::synopsis[] +aerospace summon-workspace [-h|--help] [--fail-if-noop] + +// end::synopsis[] + +// =========================================================== Description +== Description + +// tag::body[] +{manpurpose} +The moved workspace becomes focused. +The behavior is identical to Xmonad. + +The command makes sense only in multi-monitor setup. +In single monitor setup the command is identical to `workspace` command. + +// =========================================================== Options +include::./util/conditional-options-header.adoc[] + +-h, --help:: Print help +--fail-if-noop:: Exit with non-zero exit code if the workspace already visible on the focused monitor. + +// =========================================================== Arguments +include::./util/conditional-arguments-header.adoc[] + +:: The workspace to operate on. + +// end::body[] + +// =========================================================== Footer +include::util/man-footer.adoc[] diff --git a/docs/commands.adoc b/docs/commands.adoc index 2e87f0a1..c11b9c32 100644 --- a/docs/commands.adoc +++ b/docs/commands.adoc @@ -168,6 +168,13 @@ include::aerospace-split.adoc[tags=synopsis] include::aerospace-split.adoc[tags=purpose] include::aerospace-split.adoc[tags=body] +== summon-workspace +---- +include::./aerospace-summon-workspace.adoc[tags=synopsis] +---- +include::./aerospace-summon-workspace.adoc[tags=purpose] +include::./aerospace-summon-workspace.adoc[tags=body] + == trigger-binding ---- include::aerospace-trigger-binding.adoc[tags=synopsis] diff --git a/grammar/commands-bnf-grammar.txt b/grammar/commands-bnf-grammar.txt index d50a0614..14fc780b 100644 --- a/grammar/commands-bnf-grammar.txt +++ b/grammar/commands-bnf-grammar.txt @@ -60,6 +60,8 @@ aerospace -h; | split [--window-id ] (horizontal|vertical|opposite) [--window-id ] + | summon-workspace [--fail-if-noop] [--fail-if-noop] + | trigger-binding --mode | trigger-binding --mode