diff --git a/AeroSpace.xcodeproj/project.pbxproj b/AeroSpace.xcodeproj/project.pbxproj index 60db1262..0dc8741a 100644 --- a/AeroSpace.xcodeproj/project.pbxproj +++ b/AeroSpace.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 1311398A83B998908773C54D /* FocusCommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0EAADE8D2FB5D05FA5456B0 /* FocusCommandTest.swift */; }; 1C46EBB55D401C0D1AFD50F0 /* CollectionEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE37C1B8D858C81A396F40 /* CollectionEx.swift */; }; 1D408CDF1A489E527327EB15 /* CompositeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82CD9670B7A6050073E0F76 /* CompositeCommand.swift */; }; + 22175400298B985658E774EE /* ResizeCommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACD4E9C6C0C08F1B0622C57 /* ResizeCommandTest.swift */; }; 238EF26CAAADD1FE11312D7C /* default-config.toml in Resources */ = {isa = PBXBuildFile; fileRef = 8FE45A887100EB70912B07F0 /* default-config.toml */; }; 29AC28A25E1A66C608EBD7ED /* WorkspaceCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DD32B1711B8EFCC834B68E /* WorkspaceCommand.swift */; }; 2C73CDB1A933FCA5616CDFB3 /* WorkspaceEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57B60F7A58DAD4D3AF2788E4 /* WorkspaceEx.swift */; }; @@ -36,6 +37,7 @@ 635733FDDF37E44364372B74 /* MruStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954A434EE57D76F5A9D4140D /* MruStack.swift */; }; 64A058E536F1EEF7F01043AF /* TOMLKit in Frameworks */ = {isa = PBXBuildFile; productRef = EC8E4F2CA4FF8884F9F59975 /* TOMLKit */; }; 66E6CDA75DDD5E4B9647EDE2 /* AeroSpaceApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E81623E8954701269A22322 /* AeroSpaceApp.swift */; }; + 6A16BC0B4B67A45EF90F9DFE /* ResultEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07A342893BEB5525BA1F8B74 /* ResultEx.swift */; }; 6E4E235FDA41307B19F16182 /* ModeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0CD3C2A0E86CDB9DF312AB /* ModeCommand.swift */; }; 70A82A4A9DFC89286C4F7696 /* TilingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00ECDFE176777828D560A737 /* TilingContainer.swift */; }; 77FA83225024151CD556E1ED /* CloseAllWindowsButCurrentCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99853C505D93E41F6531C324 /* CloseAllWindowsButCurrentCommand.swift */; }; @@ -96,6 +98,7 @@ /* Begin PBXFileReference section */ 00ECDFE176777828D560A737 /* TilingContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TilingContainer.swift; sourceTree = ""; }; + 07A342893BEB5525BA1F8B74 /* ResultEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultEx.swift; sourceTree = ""; }; 09685297933511208058F7CF /* AeroSpace.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AeroSpace.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0AEE5470AF418906B180A593 /* mouse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mouse.swift; sourceTree = ""; }; 0D9301F99737BE4888DBC2A3 /* FocusedWorkspaceSourceOfTruth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedWorkspaceSourceOfTruth.swift; sourceTree = ""; }; @@ -128,6 +131,7 @@ 6E330182A6FACBABABB6FC51 /* MoveInCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveInCommand.swift; sourceTree = ""; }; 6F1905935B0C61590A96EFEF /* TestWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestWindow.swift; sourceTree = ""; }; 7A247B616D1951777C565D02 /* MoveThroughCommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveThroughCommandTest.swift; sourceTree = ""; }; + 7ACD4E9C6C0C08F1B0622C57 /* ResizeCommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResizeCommandTest.swift; sourceTree = ""; }; 7DBD002C4A68AED07BB63EFA /* MoveContainerToWorkspaceCommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveContainerToWorkspaceCommandTest.swift; sourceTree = ""; }; 82476B9BEBAC00EB9E32256F /* AeroAny.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AeroAny.swift; sourceTree = ""; }; 883D7F7F87FBE7D0BDE4E87F /* ArrayEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayEx.swift; sourceTree = ""; }; @@ -204,6 +208,7 @@ 7DBD002C4A68AED07BB63EFA /* MoveContainerToWorkspaceCommandTest.swift */, 13E5ED8E885494CAEEC70A90 /* MoveInCommandTest.swift */, 7A247B616D1951777C565D02 /* MoveThroughCommandTest.swift */, + 7ACD4E9C6C0C08F1B0622C57 /* ResizeCommandTest.swift */, ); path = command; sourceTree = ""; @@ -358,6 +363,7 @@ 954A434EE57D76F5A9D4140D /* MruStack.swift */, A9EDFD4A9F45182CA6E0BD7B /* OptionalEx.swift */, 28B788A95DD3C267878E05B5 /* Rect.swift */, + 07A342893BEB5525BA1F8B74 /* ResultEx.swift */, B1316FFED5D1044CB693EA45 /* SelectorComparator.swift */, AAE5DCAEC5EE619CE33859E7 /* SequenceEx.swift */, 14FABE2521026C8394254D97 /* showMessageToUser.swift */, @@ -466,6 +472,7 @@ A55F31B0CC357B37108B8F54 /* MoveContainerToWorkspaceCommandTest.swift in Sources */, 3BE358D5C209F279BB71AC95 /* MoveInCommandTest.swift in Sources */, B1E527CF4941A4E9B8D9C3D0 /* MoveThroughCommandTest.swift in Sources */, + 22175400298B985658E774EE /* ResizeCommandTest.swift in Sources */, 7ED8C2A66DD6F903796F090C /* TestApp.swift in Sources */, E22ACB36C90695FBAC78226E /* TestWindow.swift in Sources */, 70A82A4A9DFC89286C4F7696 /* TilingContainer.swift in Sources */, @@ -510,6 +517,7 @@ 78EE0CEF814ABDBA67941B84 /* Rect.swift in Sources */, FC35D6D0A678CC802972C6FE /* ReloadConfigCommand.swift in Sources */, D24D02B1FD87424B908986AF /* ResizeCommand.swift in Sources */, + 6A16BC0B4B67A45EF90F9DFE /* ResultEx.swift in Sources */, 991943D50DF9EDBF321A66F1 /* SelectorComparator.swift in Sources */, AE76A183D0454E4C8ADCE380 /* SequenceEx.swift in Sources */, E5682579AEC6B84CF6FCE90D /* TilingContainer.swift in Sources */, diff --git a/src/command/ResizeCommand.swift b/src/command/ResizeCommand.swift index 274496b5..865ba02c 100644 --- a/src/command/ResizeCommand.swift +++ b/src/command/ResizeCommand.swift @@ -3,8 +3,13 @@ struct ResizeCommand: Command { // todo cover with tests case width, height, smart } + enum ResizeMode: String { + case set, add, subtract + } + let dimension: Dimension - let diff: Int + let mode: ResizeMode + let unit: UInt func runWithoutLayout() { // todo support key repeat check(Thread.current.isMainThread) @@ -29,7 +34,15 @@ struct ResizeCommand: Command { // todo cover with tests parent = directParent orientation = parent.orientation } - let diff = CGFloat(diff) + let diff: CGFloat + switch mode { + case .set: + diff = CGFloat(unit) - window.getWeight(orientation) + case .add: + diff = CGFloat(unit) + case .subtract: + diff = -CGFloat(unit) + } guard let childDiff = diff.div(parent.children.count - 1) else { return } parent.children.lazy diff --git a/src/command/parseCommand.swift b/src/command/parseCommand.swift index aa1f633c..163ff5e0 100644 --- a/src/command/parseCommand.swift +++ b/src/command/parseCommand.swift @@ -17,7 +17,7 @@ func parseCommand(_ raw: TOMLValueConvertible) -> ParsedCommand { } } -private func parseSingleCommand(_ raw: String) -> ParsedCommand { +func parseSingleCommand(_ raw: String) -> ParsedCommand { let words = raw.split(separator: " ") let args = words[1...].map { String($0) } let firstWord = String(words.first ?? "") @@ -36,13 +36,28 @@ private func parseSingleCommand(_ raw: String) -> ParsedCommand { .flatMap { MoveWorkspaceToDisplayCommand.DisplayTarget(rawValue: $0).orFailure("Can't parse '\(firstWord)' display target") } .map { MoveWorkspaceToDisplayCommand(displayTarget: $0) } } else if firstWord == "resize" { - let arg1 = args.getOrNil(atIndex: 0).orFailure("''\(firstWord)' must have two parameters") + let mustHaveTwoArgsMessage = "''\(firstWord)' command must have two parameters" + let dimension = args.getOrNil(atIndex: 0).orFailure(mustHaveTwoArgsMessage) .flatMap { ResizeCommand.Dimension(rawValue: String($0)).orFailure("Can't parse '\(firstWord)' first arg") } - let arg2 = args.getOrNil(atIndex: 1).orFailure("''\(firstWord)' must have two parameters") - .flatMap { Int($0).orFailure("Can't parse '\(firstWord)' second arg") } - return arg1.flatMap { arg1 in - arg2.map { arg2 in - ResizeCommand(dimension: arg1, diff: arg2) + let secondArg: Result = args.getOrNil(atIndex: 1).orFailure(mustHaveTwoArgsMessage) + let mode = secondArg.map { (secondArg: String) in + if secondArg.starts(with: "+") { + return ResizeCommand.ResizeMode.add + } else if secondArg.starts(with: "-") { + return ResizeCommand.ResizeMode.subtract + } else { + return ResizeCommand.ResizeMode.set + } + } + let unit = secondArg.flatMap { + UInt($0.removePrefix("+").removePrefix("-")) + .orFailure("'\(firstWord)' command: Second arg must be a number") + } + return dimension.flatMap { dimension in + mode.flatMap { mode in + unit.map { unit in + ResizeCommand(dimension: dimension, mode: mode, unit: unit) + } } } } else if firstWord == "exec-and-wait" { diff --git a/src/config/parseConfig.swift b/src/config/parseConfig.swift index 06cec644..16e39f6f 100644 --- a/src/config/parseConfig.swift +++ b/src/config/parseConfig.swift @@ -51,33 +51,12 @@ enum TomlParseError: Error, CustomStringConvertible { private typealias ParsedTomlResult = Result -private extension Result { - func unwrapAndAppendErrors(_ errors: inout [Failure]) -> Success? { - switch self { - case .success(let success): - return success - case .failure(let error): - errors += [error] - return nil - } - } - - func getOrNils() -> (Success?, Failure?) { - switch self { - case .success(let success): - return (success, nil) - case .failure(let failure): - return (nil, failure) - } - } -} - private extension ParserProtocol { func transformRawConfig(_ raw: RawConfig, _ value: TOMLValueConvertible, _ backtrace: TomlBacktrace, _ errors: inout [TomlParseError]) -> RawConfig { - raw.copy(keyPath, parse(value, backtrace).unwrapAndAppendErrors(&errors)) + raw.copy(keyPath, parse(value, backtrace).getOrNil(appendErrorTo: &errors)) } } diff --git a/src/util/ResultEx.swift b/src/util/ResultEx.swift new file mode 100644 index 00000000..fb4b77ca --- /dev/null +++ b/src/util/ResultEx.swift @@ -0,0 +1,20 @@ +extension Result { + func getOrNil(appendErrorTo errors: inout [Failure]) -> Success? { + switch self { + case .success(let success): + return success + case .failure(let error): + errors += [error] + return nil + } + } + + func getOrNils() -> (Success?, Failure?) { + switch self { + case .success(let success): + return (success, nil) + case .failure(let failure): + return (nil, failure) + } + } +} diff --git a/test/command/ResizeCommandTest.swift b/test/command/ResizeCommandTest.swift new file mode 100644 index 00000000..9eac2c76 --- /dev/null +++ b/test/command/ResizeCommandTest.swift @@ -0,0 +1,38 @@ +import XCTest +@testable import AeroSpace_Debug + +final class ResizeCommandTest: XCTestCase { + override func setUpWithError() throws { setUpWorkspacesForTests() } + + func testParseCommand() { + testParseCommandSucc("resize smart +10", .resizeCommand(dimension: .smart, mode: .add, unit: 10)) + testParseCommandSucc("resize smart -10", .resizeCommand(dimension: .smart, mode: .subtract, unit: 10)) + testParseCommandSucc("resize smart 10", .resizeCommand(dimension: .smart, mode: .set, unit: 10)) + + testParseCommandSucc("resize height 10", .resizeCommand(dimension: .height, mode: .set, unit: 10)) + testParseCommandSucc("resize width 10", .resizeCommand(dimension: .width, mode: .set, unit: 10)) + + testParseCommandFail("resize s 10", msg: "Can't parse 'resize' first arg") + testParseCommandFail("resize smart foo", msg: "'resize' command: Second arg must be a number") + } +} + +private func testParseCommandSucc(_ command: String, _ expected: CommandDescription) { + let parsed = parseSingleCommand(command) + switch parsed { + case .success(let command): + XCTAssertEqual(command.describe, expected) + case .failure(let msg): + XCTFail(msg) + } +} + +private func testParseCommandFail(_ command: String, msg expected: String) { + let parsed = parseSingleCommand(command) + switch parsed { + case .success(let command): + XCTFail("\(command) isn't supposed to be parcelable") + case .failure(let msg): + XCTAssertEqual(msg, expected) + } +} diff --git a/test/config/ConfigTest.swift b/test/config/ConfigTest.swift index 3826ce4f..e280c9c3 100644 --- a/test/config/ConfigTest.swift +++ b/test/config/ConfigTest.swift @@ -126,6 +126,8 @@ extension Command { var describe: CommandDescription { if let focus = self as? FocusCommand { return .focusCommand(focus.direction) + } else if let resize = self as? ResizeCommand { + return .resizeCommand(dimension: resize.dimension, mode: resize.mode, unit: resize.unit) } error("Unsupported command: \(self)") } @@ -133,4 +135,5 @@ extension Command { enum CommandDescription: Equatable { case focusCommand(CardinalDirection) + case resizeCommand(dimension: ResizeCommand.Dimension, mode: ResizeCommand.ResizeMode, unit: UInt) }