Skip to content

Commit

Permalink
Aggregate focus events for sequence of commands
Browse files Browse the repository at this point in the history
  • Loading branch information
nikitabobko committed Nov 19, 2023
1 parent 0a7c6ec commit 2accc42
Show file tree
Hide file tree
Showing 27 changed files with 113 additions and 83 deletions.
1 change: 1 addition & 0 deletions src/AeroSpaceApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ struct AeroSpaceApp: App {
init() {
if !isUnitTest { // Prevent SwiftUI app loading during unit testing
signal(SIGINT, { signal in
check(Thread.current.isMainThread)
beforeTermination()
exit(signal)
} as sig_t)
Expand Down
2 changes: 1 addition & 1 deletion src/command/CloseAllWindowsButCurrentCommand.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class CloseAllWindowsButCurrentCommand: Command {
func runWithoutLayout() {
func runWithoutLayout(state: inout FocusState) {
check(Thread.current.isMainThread)
guard let focused = focusedWindowOrEffectivelyFocused else { return }
for window in focused.workspace.allLeafWindowsRecursive {
Expand Down
38 changes: 34 additions & 4 deletions src/command/Command.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
protocol Command: AeroAny { // todo add exit code and messages
// todo Aero: aggregate focus events for sequence of commands
// example: ['focus right', 'focus right'] doesn't work
@MainActor
func runWithoutLayout() async
func runWithoutLayout(state: inout FocusState) async
}

protocol QueryCommand {
Expand All @@ -24,12 +22,44 @@ extension [Command] {
check(Thread.current.isMainThread)
let commands = TrayMenuModel.shared.isEnabled ? self : (singleOrNil() as? EnableCommand).asList()
refresh(layout: false)
var state: FocusState
if let window = focusedWindowOrEffectivelyFocused {
state = .windowIsFocused(window)
} else {
state = .emptyWorkspaceIsFocused(focusedWorkspaceName)
}
for (index, command) in commands.withIndex {
await command.runWithoutLayout()
await command.runWithoutLayout(state: &state)
if index != commands.indices.last {
refresh(layout: false)
}
}
state.window?.focus()
refresh()
}
}

enum FocusState {
case emptyWorkspaceIsFocused(String)
case windowIsFocused(Window)
}

extension FocusState {
var window: Window? {
switch self {
case .windowIsFocused(let window):
return window
case .emptyWorkspaceIsFocused:
return nil
}
}

var workspace: Workspace {
switch self {
case .windowIsFocused(let window):
return window.workspace
case .emptyWorkspaceIsFocused(let workspaceName):
return Workspace.get(byName: workspaceName)
}
}
}
2 changes: 1 addition & 1 deletion src/command/EnableCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ struct EnableCommand: Command {

let targetState: State

func runWithoutLayout() {
func runWithoutLayout(state: inout FocusState) {
check(Thread.current.isMainThread)
let prevState = TrayMenuModel.shared.isEnabled
let newState: Bool
Expand Down
2 changes: 1 addition & 1 deletion src/command/ExecAndForgetCommand.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
struct ExecAndForgetCommand: Command {
let bashCommand: String

func runWithoutLayout() {
func runWithoutLayout(state: inout FocusState) {
check(Thread.current.isMainThread)
// It doesn't throw if exit code is non-zero
try! Process.run(URL(filePath: "/bin/bash"), arguments: ["-c", bashCommand])
Expand Down
2 changes: 1 addition & 1 deletion src/command/ExecAndWaitCommand.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
struct ExecAndWaitCommand: Command {
let bashCommand: String

func runWithoutLayout() async {
func runWithoutLayout(state: inout FocusState) async {
check(Thread.current.isMainThread)
await withCheckedContinuation { (continuation: CheckedContinuation<(), Never>) in
let process = Process()
Expand Down
5 changes: 2 additions & 3 deletions src/command/FlattenWorkspaceTreeCommand.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
struct FlattenWorkspaceTreeCommand: Command {
func runWithoutLayout() {
func runWithoutLayout(state: inout FocusState) {
check(Thread.current.isMainThread)
guard let currentWindow = focusedWindowOrEffectivelyFocused else { return }
let workspace = currentWindow.workspace
let workspace = state.workspace
let windows = workspace.rootTilingContainer.allLeafWindowsRecursive
for window in windows {
window.unbindFromParent()
Expand Down
10 changes: 6 additions & 4 deletions src/command/FocusCommand.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
struct FocusCommand: Command {
let direction: CardinalDirection

func runWithoutLayout() {
func runWithoutLayout(state: inout FocusState) {
check(Thread.current.isMainThread)
guard let currentWindow = focusedWindowOrEffectivelyFocused else { return }
guard let currentWindow = state.window else { return }
let workspace = currentWindow.workspace
// todo floating windows break mru
// todo bug: floating windows break mru
let floatingWindows = makeFloatingWindowsSeenAsTiling(workspace: workspace)
defer {
restoreFloatingWindows(floatingWindows: floatingWindows, workspace: workspace)
Expand All @@ -15,7 +15,9 @@ struct FocusCommand: Command {
let windowToFocus = parent.children[ownIndex + direction.focusOffset]
.findFocusTargetRecursive(snappedTo: direction.opposite)

windowToFocus?.focus()
if let windowToFocus {
state = .windowIsFocused(windowToFocus)
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/command/FullscreenCommand.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
struct FullscreenCommand: Command {
func runWithoutLayout() {
func runWithoutLayout(state: inout FocusState) {
check(Thread.current.isMainThread)
guard let window = focusedWindowOrEffectivelyFocused else { return }
guard let window = state.window else { return }
window.isFullscreen = !window.isFullscreen
}
}
4 changes: 2 additions & 2 deletions src/command/JoinWithCommand.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
struct JoinWithCommand: Command {
let direction: CardinalDirection

func runWithoutLayout() {
func runWithoutLayout(state: inout FocusState) {
check(Thread.current.isMainThread)
guard let currentWindow = focusedWindowOrEffectivelyFocused else { return }
guard let currentWindow = state.window else { return }
guard let (parent, ownIndex) = currentWindow.closestParent(hasChildrenInDirection: direction, withLayout: nil) else { return }
let moveInTarget = parent.children[ownIndex + direction.focusOffset]
let prevBinding = moveInTarget.unbindFromParent()
Expand Down
4 changes: 2 additions & 2 deletions src/command/LayoutCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ struct LayoutCommand: Command {
self.toggleBetween = toggleBetween
}

func runWithoutLayout() {
func runWithoutLayout(state: inout FocusState) {
check(Thread.current.isMainThread)
guard let window = focusedWindowOrEffectivelyFocused else { return }
guard let window = state.window else { return }
let targetDescription: LayoutDescription = toggleBetween.first(where: { !window.matchesDescription($0) })
?? toggleBetween.first!
if window.matchesDescription(targetDescription) {
Expand Down
2 changes: 1 addition & 1 deletion src/command/ModeCommand.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
struct ModeCommand: Command {
let idToActivate: String

func runWithoutLayout() {
func runWithoutLayout(state: inout FocusState) {
check(Thread.current.isMainThread)
activateMode(idToActivate)
}
Expand Down
6 changes: 3 additions & 3 deletions src/command/MoveNodeToWorkspaceCommand.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
struct MoveNodeToWorkspaceCommand: Command {
let targetWorkspaceName: String

func runWithoutLayout() async {
guard let focused = focusedWindowOrEffectivelyFocused else { return }
func runWithoutLayout(state: inout FocusState) async {
guard let focused = state.window else { return }
let preserveWorkspace = focused.workspace
let targetWorkspace = Workspace.get(byName: targetWorkspaceName)
if preserveWorkspace == targetWorkspace {
Expand All @@ -13,6 +13,6 @@ struct MoveNodeToWorkspaceCommand: Command {
// todo different monitor for floating windows
focused.bind(to: targetContainer, adaptiveWeight: WEIGHT_AUTO, index: INDEX_BIND_LAST)

WorkspaceCommand(workspaceName: preserveWorkspace.name).runWithoutLayout()
WorkspaceCommand(workspaceName: preserveWorkspace.name).runWithoutLayout(state: &state)
}
}
4 changes: 2 additions & 2 deletions src/command/MoveThroughCommand.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
struct MoveThroughCommand: Command {
let direction: CardinalDirection

func runWithoutLayout() {
func runWithoutLayout(state: inout FocusState) {
check(Thread.current.isMainThread)
guard let currentWindow = focusedWindowOrEffectivelyFocused else { return }
guard let currentWindow = state.window else { return }
switch currentWindow.parent.kind {
case .tilingContainer(let parent):
let indexOfCurrent = currentWindow.ownIndex
Expand Down
4 changes: 2 additions & 2 deletions src/command/MoveWorkspaceToMonitorCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ struct MoveWorkspaceToMonitorCommand: Command {
case next, prev
}

func runWithoutLayout() {
func runWithoutLayout(state: inout FocusState) {
check(Thread.current.isMainThread)
let focusedWorkspace = Workspace.focused
let focusedWorkspace = state.workspace
let prevMonitor = focusedWorkspace.monitor
let sortedMonitors = sortedMonitors
guard let index = sortedMonitors.firstIndex(where: { $0.rect.topLeftCorner == prevMonitor.rect.topLeftCorner }) else { return }
Expand Down
2 changes: 1 addition & 1 deletion src/command/ReloadConfigCommand.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
struct ReloadConfigCommand: Command {
func runWithoutLayout() {
func runWithoutLayout(state: inout FocusState) {
check(Thread.current.isMainThread)
reloadConfig()
}
Expand Down
4 changes: 2 additions & 2 deletions src/command/ResizeCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ struct ResizeCommand: Command { // todo cover with tests
let mode: ResizeMode
let unit: UInt

func runWithoutLayout() { // todo support key repeat
func runWithoutLayout(state: inout FocusState) { // todo support key repeat
check(Thread.current.isMainThread)

let candidates = focusedWindowOrEffectivelyFocused?.parentsWithSelf
let candidates = state.window?.parentsWithSelf
.filter { ($0.parent as? TilingContainer)?.layout == .tiles }
?? []

Expand Down
4 changes: 2 additions & 2 deletions src/command/SplitCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ struct SplitCommand: Command {

let splitArg: SplitArg

func runWithoutLayout() {
func runWithoutLayout(state: inout FocusState) {
check(Thread.current.isMainThread)
if config.enableNormalizationFlattenContainers {
return // 'split' doesn't work with "flatten container" normalization enabled
}
guard let window = focusedWindowOrEffectivelyFocused else { return }
guard let window = state.window else { return }
switch window.parent.kind {
case .workspace:
return // Nothing to do for floating windows
Expand Down
4 changes: 2 additions & 2 deletions src/command/WorkspaceBackAndForthCommand.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
struct WorkspaceBackAndForthCommand: Command {
func runWithoutLayout() {
func runWithoutLayout(state: inout FocusState) {
check(Thread.current.isMainThread)
guard let previousFocusedWorkspaceName else { return }
WorkspaceCommand(workspaceName: previousFocusedWorkspaceName).runWithoutLayout()
WorkspaceCommand(workspaceName: previousFocusedWorkspaceName).runWithoutLayout(state: &state)
}
}
23 changes: 3 additions & 20 deletions src/command/WorkspaceCommand.swift
Original file line number Diff line number Diff line change
@@ -1,36 +1,19 @@
struct WorkspaceCommand : Command {
let workspaceName: String

func runWithoutLayout() {
func runWithoutLayout(state: inout FocusState) {
check(Thread.current.isMainThread)
let workspace = Workspace.get(byName: workspaceName)
// todo drop anyLeafWindowRecursive. It must not be necessary
if let window = workspace.mostRecentWindow ?? workspace.anyLeafWindowRecursive { // switch to not empty workspace
if !workspace.isVisible { // Only do it for invisible workspaces to avoid flickering when switch to already visible workspace
// Make sure that stack of windows is correct from macOS perspective (important for closing windows)
// Alternative: focus mru window in destroyedObs (con: flickering when windows are closed, because
// focusedWindow is source of truth for workspaces)
workspace.focusMruReversedRecursive() // todo try to reduce flickering
}
focusedWorkspaceSourceOfTruth = .macOs
window.focus()
state = .windowIsFocused(window)
} else { // switch to empty workspace
check(workspace.isEffectivelyEmpty)
focusedWorkspaceSourceOfTruth = .ownModel
state = .emptyWorkspaceIsFocused(workspaceName)
}
check(workspace.monitor.setActiveWorkspace(workspace))
focusedWorkspaceName = workspace.name
}
}

private extension TreeNode {
// todo Switch between workspaces can happen via cmd+tab. Maybe this functionality must be moved to refresh
func focusMruReversedRecursive() {
if let window = self as? Window {
window.focus()
}
for child in mostRecentChildren.reversed() {
child.focusMruReversedRecursive()
}
}
}
4 changes: 2 additions & 2 deletions test/command/ExecCommandTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import XCTest
final class ExecCommandTest: XCTestCase {
func testExecAndWait() async throws {
let before = Date().timeIntervalSince1970
await ExecAndWaitCommand(bashCommand: "sleep 2").runWithoutLayout()
await ExecAndWaitCommand(bashCommand: "sleep 2").testRun()
let after = Date().timeIntervalSince1970
XCTAssertTrue((after - before) > 1)
}

func testExecAndForget() async throws {
let before = Date().timeIntervalSince1970
await ExecAndForgetCommand(bashCommand: "sleep 2").runWithoutLayout()
await ExecAndForgetCommand(bashCommand: "sleep 2").testRun()
let after = Date().timeIntervalSince1970
XCTAssertTrue((after - before) < 1)
}
Expand Down
2 changes: 1 addition & 1 deletion test/command/FlattenWorkspaceTreeCommandTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ final class FlattenWorkspaceTreeCommandTest: XCTestCase {
TestWindow(id: 3, parent: $0) // floating
}

await FlattenWorkspaceTreeCommand().runWithoutLayout()
await FlattenWorkspaceTreeCommand().testRun()
workspace.normalizeContainers()
XCTAssertEqual(workspace.layoutDescription, .workspace([.h_tiles([.window(1), .window(2)]), .window(3)]))
}
Expand Down
Loading

0 comments on commit 2accc42

Please sign in to comment.