From 24e1f080714829b7067a6dd9a7a0f205a783ae8b Mon Sep 17 00:00:00 2001 From: Nikita Bobko Date: Sun, 13 Oct 2024 16:41:46 +0200 Subject: [PATCH] Implement volume command It's unbelievably stupid that I need a 3rd party library for such a simple task... Oh, Apple... --- Package.resolved | 9 +++++ Package.swift | 2 + Sources/AppBundle/command/cmdManifest.swift | 2 + .../command/impl/VolumeCommand.swift | 26 ++++++++++++ .../Cli/subcommandDescriptionsGenerated.swift | 1 + Sources/Common/cmdArgs/cmdArgsManifest.swift | 3 ++ .../Common/cmdArgs/impl/VolumeCmdArgs.swift | 40 +++++++++++++++++++ Sources/Common/cmdHelpGenerated.swift | 5 +++ docs/aerospace-volume.adoc | 39 ++++++++++++++++++ docs/commands.adoc | 7 ++++ docs/config-examples/default-config.toml | 4 ++ grammar/commands-bnf-grammar.txt | 4 ++ legal/README.md | 5 +++ .../LICENSE-ISSoundAdditions.txt | 21 ++++++++++ 14 files changed, 168 insertions(+) create mode 100644 Sources/AppBundle/command/impl/VolumeCommand.swift create mode 100644 Sources/Common/cmdArgs/impl/VolumeCmdArgs.swift create mode 100644 docs/aerospace-volume.adoc create mode 100644 legal/third-party-license/LICENSE-ISSoundAdditions.txt diff --git a/Package.resolved b/Package.resolved index 8c01fd88..4787b3d2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -27,6 +27,15 @@ "version" : "0.1.3" } }, + { + "identity" : "issoundadditions", + "kind" : "remoteSourceControl", + "location" : "https://github.com/InerziaSoft/ISSoundAdditions", + "state" : { + "revision" : "4b555f0354e6c280917bae8a598a258efe87ab98", + "version" : "2.0.1" + } + }, { "identity" : "swift-collections", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 375d6c59..f2a6e428 100644 --- a/Package.swift +++ b/Package.swift @@ -17,6 +17,7 @@ let package = Package( .library(name: "AppBundle", targets: ["AppBundle"]), ], dependencies: [ + .package(url: "https://github.com/InerziaSoft/ISSoundAdditions", from: "2.0.1"), .package(url: "https://github.com/Kitura/BlueSocket", exact: "2.0.4"), .package(url: "https://github.com/soffes/HotKey", exact: "0.1.3"), .package(url: "https://github.com/LebJe/TOMLKit", exact: "0.5.5"), @@ -53,6 +54,7 @@ let package = Package( .product(name: "Antlr4Static", package: "antlr4"), .product(name: "Socket", package: "BlueSocket"), .product(name: "HotKey", package: "HotKey"), + .product(name: "ISSoundAdditions", package: "ISSoundAdditions"), .product(name: "TOMLKit", package: "TOMLKit"), .product(name: "Collections", package: "swift-collections"), ] diff --git a/Sources/AppBundle/command/cmdManifest.swift b/Sources/AppBundle/command/cmdManifest.swift index d6d0b765..86da833f 100644 --- a/Sources/AppBundle/command/cmdManifest.swift +++ b/Sources/AppBundle/command/cmdManifest.swift @@ -70,6 +70,8 @@ extension CmdArgs { command = ServerVersionInternalCommandCommand() case .triggerBinding: command = TriggerBindingCommand(args: self as! TriggerBindingCmdArgs) + case .volume: + command = VolumeCommand(args: self as! VolumeCmdArgs) case .workspace: command = WorkspaceCommand(args: self as! WorkspaceCmdArgs) case .workspaceBackAndForth: diff --git a/Sources/AppBundle/command/impl/VolumeCommand.swift b/Sources/AppBundle/command/impl/VolumeCommand.swift new file mode 100644 index 00000000..d73cede3 --- /dev/null +++ b/Sources/AppBundle/command/impl/VolumeCommand.swift @@ -0,0 +1,26 @@ +import AppKit +import Common +import ISSoundAdditions + +struct VolumeCommand: Command { + let args: VolumeCmdArgs + + func run(_ env: CmdEnv, _ io: CmdIo) -> Bool { + check(Thread.current.isMainThread) + switch args.action.val { + case .up: + Sound.output.increaseVolume(by: 0.0625, autoMuteUnmute: true) + case .down: + Sound.output.decreaseVolume(by: 0.0625, autoMuteUnmute: true) + case .muteToggle: + Sound.output.isMuted.toggle() + case .muteOn: + Sound.output.isMuted = true + case .muteOff: + Sound.output.isMuted = false + case .set(let int): + Sound.output.setVolume(Float(int) / 100, autoMuteUnmute: true) + } + return true + } +} diff --git a/Sources/Cli/subcommandDescriptionsGenerated.swift b/Sources/Cli/subcommandDescriptionsGenerated.swift index 80f56b93..b27b3d5e 100644 --- a/Sources/Cli/subcommandDescriptionsGenerated.swift +++ b/Sources/Cli/subcommandDescriptionsGenerated.swift @@ -33,6 +33,7 @@ let subcommandDescriptions = [ [" 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"], + [" volume", "Manipulate volume"], [" 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 c81ee352..39c342b3 100644 --- a/Sources/Common/cmdArgs/cmdArgsManifest.swift +++ b/Sources/Common/cmdArgs/cmdArgsManifest.swift @@ -33,6 +33,7 @@ public enum CmdKind: String, CaseIterable, Equatable { case split case summonWorkspace = "summon-workspace" case triggerBinding = "trigger-binding" + case volume case workspace case workspaceBackAndForth = "workspace-back-and-forth" @@ -115,6 +116,8 @@ func initSubcommands() -> [String: any SubCommandParserProtocol] { } case .triggerBinding: result[kind.rawValue] = SubCommandParser(parseTriggerBindingCmdArgs) + case .volume: + result[kind.rawValue] = SubCommandParser(VolumeCmdArgs.init) case .workspace: result[kind.rawValue] = SubCommandParser(parseWorkspaceCmdArgs) case .workspaceBackAndForth: diff --git a/Sources/Common/cmdArgs/impl/VolumeCmdArgs.swift b/Sources/Common/cmdArgs/impl/VolumeCmdArgs.swift new file mode 100644 index 00000000..2db27960 --- /dev/null +++ b/Sources/Common/cmdArgs/impl/VolumeCmdArgs.swift @@ -0,0 +1,40 @@ +public struct VolumeCmdArgs: CmdArgs { + public let rawArgs: EquatableNoop<[String]> + public init(rawArgs: [String]) { self.rawArgs = .init(rawArgs) } + public static let parser: CmdParser = cmdParser( + kind: .volume, + allowInConfig: true, + help: volume_help_generated, + options: [:], + arguments: [newArgParser(\.action, parseVolumeAction, mandatoryArgPlaceholder: VolumeAction.argsUnion)] + ) + + public var windowId: UInt32? + public var workspaceName: WorkspaceName? + + public var action: Lateinit = .uninitialized +} + +public enum VolumeAction: Equatable { + case up, down, muteToggle, muteOn, muteOff + case set(Int) + + static let argsUnion: String = "(up|down|mute-toggle|mute-on|mute-off|set)" +} + +func parseVolumeAction(arg: String, nextArgs: inout [String]) -> Parsed { + switch arg { + case "up": return .success(.up) + case "down": return .success(.down) + case "mute-toggle": return .success(.muteToggle) + case "mute-off": return .success(.muteOff) + case "mute-on": return .success(.muteOn) + case "set": + guard let arg = nextArgs.nextNonFlagOrNil() else { return .failure("set argument must be followed by ") } + guard let int = Int(arg) else { return .failure("Can't parse number '\(arg)'") } + if !(0 ... 100).contains(int) { return .failure("\(int) must be in range from 0 to 100") } + return .success(.set(int)) + default: + return .failure("Unknown argument '\(arg)'. Possible values: \(VolumeAction.argsUnion)") + } +} diff --git a/Sources/Common/cmdHelpGenerated.swift b/Sources/Common/cmdHelpGenerated.swift index 795e930e..e27a6b68 100644 --- a/Sources/Common/cmdHelpGenerated.swift +++ b/Sources/Common/cmdHelpGenerated.swift @@ -126,6 +126,11 @@ let summon_workspace_help_generated = """ let trigger_binding_help_generated = """ USAGE: trigger-binding [-h|--help] --mode """ +let volume_help_generated = """ + USAGE: volume [-h|--help] (up|down) + OR: volume [-h|--help] (mute-toggle|mute-off|mute-on) + OR: volume [-h|--help] set + """ let workspace_back_and_forth_help_generated = """ USAGE: workspace-back-and-forth [-h|--help] """ diff --git a/docs/aerospace-volume.adoc b/docs/aerospace-volume.adoc new file mode 100644 index 00000000..30ae6c0d --- /dev/null +++ b/docs/aerospace-volume.adoc @@ -0,0 +1,39 @@ += aerospace-volume(1) +include::util/man-attributes.adoc[] +// tag::purpose[] +:manpurpose: Manipulate volume +// end::purpose[] +:manname: aerospace-volume + +// =========================================================== Synopsis +== Synopsis +[verse] +// tag::synopsis[] +aerospace volume [-h|--help] (up|down) +aerospace volume [-h|--help] (mute-toggle|mute-off|mute-on) +aerospace volume [-h|--help] set + +// end::synopsis[] + +// =========================================================== Description +== Description + +// tag::body[] +{manpurpose} + +// =========================================================== Options +include::./util/conditional-options-header.adoc[] + +-h, --help:: Print help + +// =========================================================== Arguments +include::./util/conditional-arguments-header.adoc[] + +(up|down):: Increase or decrease the volume +(mute-toggle|mute-on|mute-off):: Toggle/On/Off mute +set :: Set volume to the exact value on scale from 0 to 100 + +// end::body[] + +// =========================================================== Footer +include::util/man-footer.adoc[] diff --git a/docs/commands.adoc b/docs/commands.adoc index c11b9c32..f010dbf7 100644 --- a/docs/commands.adoc +++ b/docs/commands.adoc @@ -182,6 +182,13 @@ include::aerospace-trigger-binding.adoc[tags=synopsis] include::aerospace-trigger-binding.adoc[tags=purpose] include::aerospace-trigger-binding.adoc[tags=body] +== volume +---- +include::./aerospace-volume.adoc[tags=synopsis] +---- +include::./aerospace-volume.adoc[tags=purpose] +include::./aerospace-volume.adoc[tags=body] + == workspace ---- include::aerospace-workspace.adoc[tags=synopsis] diff --git a/docs/config-examples/default-config.toml b/docs/config-examples/default-config.toml index 378228b7..c50d36a1 100644 --- a/docs/config-examples/default-config.toml +++ b/docs/config-examples/default-config.toml @@ -202,3 +202,7 @@ alt-shift-h = ['join-with left', 'mode main'] alt-shift-j = ['join-with down', 'mode main'] alt-shift-k = ['join-with up', 'mode main'] alt-shift-l = ['join-with right', 'mode main'] + +down = 'volume down' +up = 'volume up' +shift-down = ['volume set 0', 'mode main'] diff --git a/grammar/commands-bnf-grammar.txt b/grammar/commands-bnf-grammar.txt index ad85c7bf..995a031b 100644 --- a/grammar/commands-bnf-grammar.txt +++ b/grammar/commands-bnf-grammar.txt @@ -64,6 +64,10 @@ aerospace -h; | trigger-binding --mode | trigger-binding --mode + | volume (up|down) + | volume (mute-toggle|mute-on|mute-off) + | volume set + | workspace [--auto-back-and-forth|--fail-if-noop]... [--auto-back-and-forth|--fail-if-noop]... | workspace [--wrap-around] (next|prev) [--wrap-around] diff --git a/legal/README.md b/legal/README.md index c5ece73d..ba0dd432 100644 --- a/legal/README.md +++ b/legal/README.md @@ -36,6 +36,11 @@ ANTLR is used to parse AeroSpace built-in shell like language. [swift-collections Apache 2.0 license](./third-party-license/LICENSE-swift-collections.txt). swift-collections is used for more advanced Swift collections. +**ISSoundAdditions** +[ISSoundAdditions GitHub link](https://github.com/InerziaSoft/ISSoundAdditions). +[ISSoundAdditions MIT license](./third-party-license/LICENSE-ISSoundAdditions.txt). +ISSoundAdditions is used as a convenient API to change system volume. + **Mac OS X Snow Leopard • Wallpaper**. Created by Fons Mans. [Figma design file link](https://www.figma.com/community/file/1228988440310597758). diff --git a/legal/third-party-license/LICENSE-ISSoundAdditions.txt b/legal/third-party-license/LICENSE-ISSoundAdditions.txt new file mode 100644 index 00000000..67f26285 --- /dev/null +++ b/legal/third-party-license/LICENSE-ISSoundAdditions.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 InerziaSoft - Massimo and Alessio Moiso. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.