Skip to content

Commit

Permalink
Add possibility to focus window by id
Browse files Browse the repository at this point in the history
  • Loading branch information
nikitabobko committed Apr 22, 2024
1 parent f2194ab commit 0684d53
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 33 deletions.
24 changes: 16 additions & 8 deletions Sources/AppBundle/command/FocusCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,25 @@ struct FocusCommand: Command {
defer {
restoreFloatingWindows(floatingWindows: floatingWindows, workspace: workspace)
}
let direction = args.direction.val

var result: Bool = true
if let (parent, ownIndex) = window?.closestParent(hasChildrenInDirection: direction, withLayout: nil) {
guard let windowToFocus = parent.children[ownIndex + direction.focusOffset]
.findFocusTargetRecursive(snappedTo: direction.opposite) else { return false }
state.subject = .window(windowToFocus)
} else {
result = hitWorkspaceBoundaries(state, args, direction) && result
switch args.target {
case .direction(let direction):
if let (parent, ownIndex) = window?.closestParent(hasChildrenInDirection: direction, withLayout: nil) {
guard let windowToFocus = parent.children[ownIndex + direction.focusOffset]
.findFocusTargetRecursive(snappedTo: direction.opposite) else { return false }
state.subject = .window(windowToFocus)
} else {
result = hitWorkspaceBoundaries(state, args, direction) && result
}
case .windowId(let windowId):
if let windowToFocus = MacWindow.allWindowsMap[windowId] {
state.subject = .window(windowToFocus)
} else {
state.stderr.append("Can't find window with ID \(windowId)")
return false
}
}

switch state.subject {
case .emptyWorkspace(let name):
result = WorkspaceCommand.run(state, name) && result
Expand Down
2 changes: 1 addition & 1 deletion Sources/AppBundle/tree/MacWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ final class MacWindow: Window, CustomStringConvertible {
super.init(id: id, app, lastFloatingSize: axWindow.get(Ax.sizeAttr), parent: parent, adaptiveWeight: adaptiveWeight, index: index)
}

private static var allWindowsMap: [CGWindowID: MacWindow] = [:]
static var allWindowsMap: [CGWindowID: MacWindow] = [:]
static var allWindows: [MacWindow] { Array(allWindowsMap.values) }

static func get(app: MacApp, axWindow: AXUIElement, startup: Bool) -> MacWindow? {
Expand Down
8 changes: 5 additions & 3 deletions Sources/AppBundleTests/command/FocusCommandTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ final class FocusCommandTest: XCTestCase {

func testParse() {
XCTAssertTrue(parseCommand("focus --boundaries left").errorOrNil?.contains("Possible values") == true)
testParseCommandSucc("focus --boundaries workspace left", FocusCmdArgs(direction: .left))
var expected = FocusCmdArgs(direction: .left)
expected.rawBoundaries = .workspace
testParseCommandSucc("focus --boundaries workspace left", expected)

XCTAssertEqual(
parseCommand("focus --boundaries workspace --boundaries workspace left").errorOrNil,
Expand Down Expand Up @@ -83,8 +85,8 @@ final class FocusCommandTest: XCTestCase {

//FocusCommand(args: FocusCmdArgs(boundaries: .workspace, boundariesAction: .stop, direction: .left)).run(.focused)
var args = FocusCmdArgs(direction: .left)
args.boundaries = .workspace
args.boundariesAction = .stop
args.rawBoundaries = .workspace
args.rawBoundariesAction = .stop
FocusCommand(args: args).run(.focused)
XCTAssertEqual(focusedWindow?.windowId, 1)
}
Expand Down
65 changes: 48 additions & 17 deletions Sources/Common/cmdArgs/FocusCmdArgs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ public struct FocusCmdArgs: CmdArgs, RawCmdArgs, Equatable, AeroAny {
kind: .focus,
allowInConfig: true,
help: """
USAGE: focus [<OPTIONS>] \(CardinalDirection.unionLiteral)
USAGE: focus [<options>] \(CardinalDirection.unionLiteral)
OR: focus [-h|--help] --window-id <window-id>
OPTIONS:
-h, --help Print help
--window-id <window-id> Focus window with specified <window-id>
--boundaries \(boundar) Defines focus boundaries.
\(boundar) possible values: \(FocusCmdArgs.Boundaries.unionLiteral)
The default is: \(FocusCmdArgs.Boundaries.workspace.rawValue)
Expand All @@ -19,24 +21,28 @@ public struct FocusCmdArgs: CmdArgs, RawCmdArgs, Equatable, AeroAny {
ARGUMENTS:
(left|down|up|right) Focus direction
""", // todo focus [OPTIONS] window-id <id>
// ARGUMENTS:
// <id> ID of window to focus
""",
options: [
"--boundaries": ArgParser(\.boundaries, parseBoundaries),
"--boundaries-action": ArgParser(\.boundariesAction, parseBoundariesAction)
"--boundaries": ArgParser(\.rawBoundaries, upcastArgParserFun(parseBoundaries)),
"--boundaries-action": ArgParser(\.rawBoundariesAction, upcastArgParserFun(parseBoundariesAction)),
"--window-id": ArgParser(\.windowId, upcastArgParserFun(parseArgWithUInt32))
],
arguments: [newArgParser(\.direction, parseCardinalDirectionArg, mandatoryArgPlaceholder: CardinalDirection.unionLiteral)]
arguments: [ArgParser(\.direction, upcastArgParserFun(parseCardinalDirectionArg))]
)

public var boundaries: Boundaries = .workspace // todo cover boundaries wrapping with tests
public var boundariesAction: WhenBoundariesCrossed = .wrapAroundTheWorkspace
public var direction: Lateinit<CardinalDirection> = .uninitialized
public var rawBoundaries: Boundaries? = nil // todo cover boundaries wrapping with tests
public var rawBoundariesAction: WhenBoundariesCrossed? = nil
public var windowId: UInt32? = nil
public var direction: CardinalDirection? = nil

fileprivate init() {}

public init(direction: CardinalDirection) {
self.direction = .initialized(direction)
self.direction = direction
}

public init(windowId: UInt32) {
self.windowId = windowId
}

public enum Boundaries: String, CaseIterable, Equatable {
Expand All @@ -50,13 +56,38 @@ public struct FocusCmdArgs: CmdArgs, RawCmdArgs, Equatable, AeroAny {
}
}

public enum FocusCmdTarget {
case direction(CardinalDirection)
case windowId(UInt32)
}

public extension FocusCmdArgs {
var target: FocusCmdTarget {
if let direction {
return .direction(direction)
}
if let windowId {
return .windowId(windowId)
}
error("Parser invariants are broken")
}

var boundaries: Boundaries { rawBoundaries ?? .workspace }
var boundariesAction: WhenBoundariesCrossed { rawBoundariesAction ?? .wrapAroundTheWorkspace }
}

public func parseFocusCmdArgs(_ args: [String]) -> ParsedCmd<FocusCmdArgs> {
parseRawCmdArgs(FocusCmdArgs(), args)
.flatMap { raw in
if raw.boundaries == .workspace && raw.boundariesAction == .wrapAroundAllMonitors {
return .failure("\(raw.boundaries.rawValue) and \(raw.boundariesAction.rawValue) is an invalid combination of values")
}
return .cmd(raw)
return parseRawCmdArgs(FocusCmdArgs(), args)
.flatMap { (raw: FocusCmdArgs) -> ParsedCmd<FocusCmdArgs> in
raw.boundaries == .workspace && raw.boundariesAction == .wrapAroundAllMonitors
? .failure("\(raw.boundaries.rawValue) and \(raw.boundariesAction.rawValue) is an invalid combination of values")
: .cmd(raw)
}
.filter("Mandatory argument is missing. '\(CardinalDirection.unionLiteral)' or --window-id") {
$0.direction != nil || $0.windowId != nil
}
.filter("--window-id is incompatible with other options") {
$0.windowId == nil || $0 == FocusCmdArgs(windowId: $0.windowId!)
}
}

Expand Down
18 changes: 14 additions & 4 deletions Sources/Common/cmdArgs/other/parseCmdArgsUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,22 +174,22 @@ private extension ArgParserProtocol {
}
}

public typealias ArgParserFun<K> = (/*arg*/ String, /*nextArgs*/ inout [String]) -> Parsed<K>
public protocol ArgParserProtocol<T> {
associatedtype K
associatedtype T where T: Copyable
var argPlaceholderIfMandatory: String? { get }
var keyPath: WritableKeyPath<T, K> { get }
var parse: (/*arg*/ String, /*nextArgs*/ inout [String]) -> Parsed<K> { get }
var parse: ArgParserFun<K> { get }
}

public struct ArgParser<T: Copyable, K>: ArgParserProtocol {
public let keyPath: WritableKeyPath<T, K>
public let parse: (String, inout [String]) -> Parsed<K>
public let parse: ArgParserFun<K>
public let argPlaceholderIfMandatory: String?

public init(
_ keyPath: WritableKeyPath<T, K>,
_ parse: @escaping (String, inout [String]) -> Parsed<K>,
_ parse: @escaping ArgParserFun<K>,
argPlaceholderIfMandatory: String? = nil
) {
self.keyPath = keyPath
Expand Down Expand Up @@ -259,3 +259,13 @@ public func parseEnum<T: RawRepresentable>(_ raw: String, _ _: T.Type) -> Parsed
public func parseCardinalDirectionArg(arg: String, nextArgs: inout [String]) -> Parsed<CardinalDirection> {
parseEnum(arg, CardinalDirection.self)
}

public func parseArgWithUInt32(arg: String, nextArgs: inout [String]) -> Parsed<UInt32> {
if let arg = nextArgs.nextNonFlagOrNil() {
return UInt32(arg).orFailure("Can't parse '\(arg)'. It must be a positive number")
} else {
return .failure("'\(arg)' must be followed by mandatory UInt32")
}
}

func upcastArgParserFun<T>(_ fun: @escaping ArgParserFun<T>) -> ArgParserFun<T?> { { fun($0, &$1).map { $0 } } }
4 changes: 4 additions & 0 deletions docs/aerospace-focus.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ include::util/man-attributes.adoc[]
// tag::synopsis[]
aerospace focus [-h|--help] [--boundaries <boundary>]
[--boundaries-action <action>] (left|down|up|right)
aerospace focus [-h|--help] --window-id <window-id>

// end::synopsis[]

Expand Down Expand Up @@ -41,6 +42,9 @@ Defines the behavior when requested to cross the `<boundary>`. +
`<action>` possible values: `(stop|wrap-around-the-workspace|wrap-around-all-monitors)` +
The default is: `wrap-around-the-workspace`

--window-id <window-id>::
Focus the window with specified `<window-id>`

// end::body[]

include::util/man-footer.adoc[]

0 comments on commit 0684d53

Please sign in to comment.