Skip to content

Commit

Permalink
feat: added SketchyBar integration (#10)
Browse files Browse the repository at this point in the history
Resolves #2
  • Loading branch information
wojciech-kulik authored Jan 24, 2025
1 parent e7fa1f1 commit dc1f9cc
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 10 deletions.
20 changes: 15 additions & 5 deletions FlashSpace.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
16999A192D3D978E0006EA91 /* ShortcutRecorder in Frameworks */ = {isa = PBXBuildFile; productRef = 16999A182D3D978E0006EA91 /* ShortcutRecorder */; };
16999AE42D3DC77A0006EA91 /* ServiceManagement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16999AE32D3DC77A0006EA91 /* ServiceManagement.framework */; };
2C5A21F7CFA65934F22ECA93 /* AXUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14242986FC549EA6F2EE11B8 /* AXUIElement.swift */; };
5C8832A1764AC1F6505BB8E4 /* Integrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01904D0D8EB8017AEF4CD453 /* Integrations.swift */; };
CA65DED962CD106A86C389C2 /* NSRunningApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2E1D011156B9FFF828F2E9 /* NSRunningApplication.swift */; };
DD48ACCA45929209284841E9 /* IntegrationsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF62C0D4D9928BC04344735A /* IntegrationsSettingsView.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
01904D0D8EB8017AEF4CD453 /* Integrations.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Integrations.swift; sourceTree = "<group>"; };
07AA7A031B97E585161DBEFC /* FocusManagementView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FocusManagementView.swift; sourceTree = "<group>"; };
0F6030DE3CF8A0BBDBFC3E29 /* SettingsRepository.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsRepository.swift; sourceTree = "<group>"; };
14242986FC549EA6F2EE11B8 /* AXUIElement.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AXUIElement.swift; sourceTree = "<group>"; };
Expand All @@ -35,6 +38,7 @@
A6A0526ACA637050524E22C6 /* WorkspaceManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WorkspaceManager.swift; sourceTree = "<group>"; };
AFA065A8C8B0E063D8C60A76 /* NotificationName.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationName.swift; sourceTree = "<group>"; };
B44F2DBC6749954D39EDFF45 /* FocusManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FocusManager.swift; sourceTree = "<group>"; };
BF62C0D4D9928BC04344735A /* IntegrationsSettingsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = IntegrationsSettingsView.swift; sourceTree = "<group>"; };
CDF522B8B2538D4D42526AAA /* SettingsView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
CE2E1D011156B9FFF828F2E9 /* NSRunningApplication.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NSRunningApplication.swift; sourceTree = "<group>"; };
CE48B9CB7CAE419712CAE0AE /* HotKeysMonitor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HotKeysMonitor.swift; sourceTree = "<group>"; };
Expand All @@ -48,6 +52,8 @@
membershipExceptions = (
Extensions/AXUIElement.swift,
Extensions/NSRunningApplication.swift,
Settings/IntegrationsSettingsView.swift,
Utils/Integrations.swift,
);
target = 16999A042D3D97770006EA91 /* FlashSpace */;
};
Expand Down Expand Up @@ -169,6 +175,7 @@
FE5885AA7A5B96DC59D8290B /* GeneralSettingsView.swift */,
07AA7A031B97E585161DBEFC /* FocusManagementView.swift */,
0F6030DE3CF8A0BBDBFC3E29 /* SettingsRepository.swift */,
BF62C0D4D9928BC04344735A /* IntegrationsSettingsView.swift */,
);
path = Settings;
sourceTree = "<group>";
Expand All @@ -178,6 +185,7 @@
children = (
36E4723ECEA5C6DD2BDA559F /* FileChooser.swift */,
5AB52E58777D6FCD2FD77ADD /* AutostartService.swift */,
01904D0D8EB8017AEF4CD453 /* Integrations.swift */,
);
path = Utils;
sourceTree = "<group>";
Expand Down Expand Up @@ -278,6 +286,8 @@
files = (
CA65DED962CD106A86C389C2 /* NSRunningApplication.swift in Sources */,
2C5A21F7CFA65934F22ECA93 /* AXUIElement.swift in Sources */,
DD48ACCA45929209284841E9 /* IntegrationsSettingsView.swift in Sources */,
5C8832A1764AC1F6505BB8E4 /* Integrations.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -411,7 +421,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = ZL6756Q8Q2;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
Expand All @@ -425,7 +435,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 0.3.4;
MARKETING_VERSION = 0.4.5;
PRODUCT_BUNDLE_IDENTIFIER = pl.wojciechkulik.FlashSpace.dev;
PRODUCT_NAME = "$(TARGET_NAME)-Dev";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand All @@ -442,8 +452,8 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = ZL6756Q8Q2;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = JT56B63DU5;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
Expand All @@ -456,7 +466,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 0.3.4;
MARKETING_VERSION = 0.4.5;
PRODUCT_BUNDLE_IDENTIFIER = pl.wojciechkulik.FlashSpace;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand Down
35 changes: 35 additions & 0 deletions FlashSpace/Settings/IntegrationsSettingsView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// IntegrationsSettingsView.swift
//
// Created by Wojciech Kulik on 24/01/2025.
// Copyright © 2025 Wojciech Kulik. All rights reserved.
//

import SwiftUI

struct IntegrationsSettingsView: View {
@StateObject var settings = AppDependencies.shared.settingsRepository

var body: some View {
Form {
Section {
Toggle("Enable integrations", isOn: $settings.enableIntegrations)
}

Section(
header: Text("Run script on workspace change:"),
footer: Text(
"""
$WORKSPACE will be replaced with the active workspace name
$DISPLAY will be replaced with the corresponding display name
"""
)
.foregroundStyle(.secondary)
) {
TextField("", text: $settings.runScriptOnWorkspaceChange)
}
}
.formStyle(.grouped)
.navigationTitle("Integrations")
}
}
29 changes: 28 additions & 1 deletion FlashSpace/Settings/SettingsRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Copyright © 2025 Wojciech Kulik. All rights reserved.
//

import Combine
import Foundation

struct AppSettings: Codable {
Expand All @@ -15,9 +16,14 @@ struct AppSettings: Codable {
var focusDown: HotKeyShortcut?
var focusNextWorkspaceApp: HotKeyShortcut?
var focusPreviousWorkspaceApp: HotKeyShortcut?

var enableIntegrations: Bool?
var runScriptOnWorkspaceChange: String?
}

final class SettingsRepository: ObservableObject {
static let defaultScript = "sketchybar --trigger flashspace_workspace_change WORKSPACE=\"$WORKSPACE\" DISPLAY=\"$DISPLAY\""

@Published var enableFocusManagement: Bool = true {
didSet { updateSettings() }
}
Expand Down Expand Up @@ -46,17 +52,33 @@ final class SettingsRepository: ObservableObject {
didSet { updateSettings() }
}

@Published var enableIntegrations: Bool = false {
didSet { updateSettings() }
}

@Published var runScriptOnWorkspaceChange: String = "" {
didSet { debouncedUpdateSettings.send(()) }
}

private var currentSettings = AppSettings()
private var shouldUpdate = false
private var cancellables = Set<AnyCancellable>()

private let debouncedUpdateSettings = PassthroughSubject<(), Never>()
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
private let dataUrl = FileManager.default
.homeDirectoryForCurrentUser
.appendingPathComponent(".config/flashspace/settings.json")

init() {
encoder.outputFormatting = .prettyPrinted
loadFromDisk()

debouncedUpdateSettings
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
.sink { [weak self] in self?.updateSettings() }
.store(in: &cancellables)
}

private func updateSettings() {
Expand All @@ -69,7 +91,9 @@ final class SettingsRepository: ObservableObject {
focusUp: focusUp,
focusDown: focusDown,
focusNextWorkspaceApp: focusNextWorkspaceApp,
focusPreviousWorkspaceApp: focusPreviousWorkspaceApp
focusPreviousWorkspaceApp: focusPreviousWorkspaceApp,
enableIntegrations: enableIntegrations,
runScriptOnWorkspaceChange: runScriptOnWorkspaceChange
)
saveToDisk()
AppDependencies.shared.hotKeysManager.refresh()
Expand Down Expand Up @@ -97,5 +121,8 @@ final class SettingsRepository: ObservableObject {
focusDown = settings.focusDown
focusNextWorkspaceApp = settings.focusNextWorkspaceApp
focusPreviousWorkspaceApp = settings.focusPreviousWorkspaceApp

enableIntegrations = settings.enableIntegrations ?? false
runScriptOnWorkspaceChange = settings.runScriptOnWorkspaceChange ?? Self.defaultScript
}
}
4 changes: 4 additions & 0 deletions FlashSpace/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ struct SettingsView: View {
.tag("General")
Label("Focus Management", systemImage: "macwindow.on.rectangle")
.tag("Focus")
Label("Integrations", systemImage: "link")
.tag("Integrations")
}
.toolbar(removing: .sidebarToggle)
.listStyle(.sidebar)
Expand All @@ -49,6 +51,8 @@ struct SettingsView: View {
GeneralSettingsView()
case "Focus":
FocusManagementView()
case "Integrations":
IntegrationsSettingsView()
default:
EmptyView()
}
Expand Down
27 changes: 27 additions & 0 deletions FlashSpace/Utils/Integrations.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// Integrations.swift
//
// Created by Wojciech Kulik on 24/01/2025.
// Copyright © 2025 Wojciech Kulik. All rights reserved.
//

import Foundation

enum Integrations {
private static let settings = AppDependencies.shared.settingsRepository

static func runIfNeeded(workspace: Workspace) {
let script = settings.runScriptOnWorkspaceChange.trimmingCharacters(in: .whitespaces)

guard settings.enableIntegrations, !script.isEmpty else { return }

let scriptWithReplacements = script
.replacingOccurrences(of: "$WORKSPACE", with: workspace.name)
.replacingOccurrences(of: "$DISPLAY", with: workspace.display)

let task = Process()
task.launchPath = "/bin/bash"
task.arguments = ["-c", scriptWithReplacements]
task.launch()
}
}
2 changes: 2 additions & 0 deletions FlashSpace/Workspaces/WorkspaceManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ final class WorkspaceManager {
print("\n\nWORKSPACE: \(workspace.name)")
print("----")

Integrations.runIfNeeded(workspace: workspace)

lastWorkspaceActivation = Date()
activeWorkspace[workspace.display] = workspace
showApps(in: workspace, setFocus: setFocus)
Expand Down
79 changes: 75 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ https://github.com/user-attachments/assets/af5951ce-8386-48d5-918e-914474d2c2b8

- [x] Blazingly fast workspace switching
- [x] Multiple displays support
- [x] Global hotkeys
- [x] Activate workspace on app focus
- [x] Move apps between workspaces with a hotkey
- [x] Focus management - set hotkeys to switch between apps quickly
- [x] [SketchyBar] integration

## ⚙️ Installation

Expand Down Expand Up @@ -48,14 +48,14 @@ Now you can switch to the workspace using the configured hotkey.

## 🪟 Focus Management

FlashSpace allows you to switch focus between windows quickly.
FlashSpace enables fast switching of focus between windows. Use hotkeys to
shift focus in any desired direction. It also allows you to jump between
displays.

<img width="892" alt="FlashSpace-Focus" src="https://github.com/user-attachments/assets/bce3d431-22c3-43d1-8078-af62ffbb5f90" />

## 📝 Notes

### Workspaces

FlashSpace doesn't manage windows, so if you switch to a workspace and call
another app that is not assigned to the workspace, it will be shown on top of
the workspace apps.
Expand All @@ -68,8 +68,79 @@ when a small pop-up window is shown or some unexpected app is opened.

If you want to hide the new app, you can simply use the hotkey again.

## 🖥️ SketchyBar Integration

FlashSpace can be integrated with [SketchyBar] and other tools. The app runs a
configurable script when the workspace is changed.

You can enable the integration in the app settings.

### Only Active Workspace

##### `sketchybarrc`

```bash
sketchybar --add item flashspace left \
--set flashspace \
background.color=0x22ffffff \
background.corner_radius=5 \
label.padding_left=5 \
label.padding_right=5 \
script="$CONFIG_DIR/plugins/flashspace.sh" \
--add event flashspace_workspace_change \
--subscribe flashspace flashspace_workspace_change
```

##### `plugins/flashspace.sh`

```bash
#!/bin/bash

sketchybar --set $NAME label="$WORKSPACE - $DISPLAY"
```

### All Workspaces

##### `sketchybarrc`

```bash
sketchybar --add event flashspace_workspace_change

SID=1
WORKSPACES=$(cat ~/.config/flashspace/workspaces.json | jq -r ".[].name")

for workspace in $WORKSPACES; do
sketchybar --add item flashspace.$SID left \
--subscribe flashspace.$SID flashspace_workspace_change \
--set flashspace.$SID \
background.color=0x22ffffff \
background.corner_radius=5 \
background.padding_left=5 \
label.padding_left=5 \
label.padding_right=5 \
label="$workspace" \
script="$CONFIG_DIR/plugins/flashspace.sh $workspace"

SID=$((SID + 1))
done
```

##### `plugins/flashspace.sh`

```bash
#!/bin/bash

if [ "$1" = "$WORKSPACE" ]; then
sketchybar --set $NAME label.color=0xffff0000
else
sketchybar --set $NAME label.color=0xffffffff
fi
```

## 🚧 Limitations

The app is still in early development and has some limitations:

- It doesn't support individual app windows yet.

[SketchyBar]: https://github.com/FelixKratz/SketchyBar

0 comments on commit dc1f9cc

Please sign in to comment.