Skip to content

Commit 3645b19

Browse files
Fix app install packaging and add workspace memory
1 parent 6431d38 commit 3645b19

14 files changed

Lines changed: 550 additions & 22 deletions

Makefile

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
.PHONY: help build build-release run app dmg install uninstall open-dist-app open-installed-app clean
2+
3+
APP_NAME := AeroMux
4+
DIST_DIR := dist
5+
APP_BUNDLE := $(DIST_DIR)/$(APP_NAME).app
6+
APP_INSTALL_DIR ?= /Applications
7+
APP_INSTALL_PATH := $(APP_INSTALL_DIR)/$(APP_NAME).app
8+
VERSION ?= $(shell git describe --tags --always --dirty)
9+
10+
help: ## Show available targets
11+
@awk 'BEGIN {FS = ":.*## "}; /^[a-zA-Z0-9_.-]+:.*## / {printf "%-18s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
12+
13+
build: ## Build the debug binary with SwiftPM
14+
swift build
15+
16+
build-release: ## Build the release binary with SwiftPM
17+
swift build -c release
18+
19+
run: ## Run the app from source with SwiftPM
20+
swift run
21+
22+
app: ## Build the macOS .app bundle into dist/
23+
VERSION="$(VERSION)" ./scripts/build-release-app.sh
24+
25+
dmg: ## Build the DMG into dist/
26+
VERSION="$(VERSION)" ./scripts/build-release-dmg.sh
27+
28+
install: app ## Install the built app bundle into /Applications
29+
rm -rf "$(APP_INSTALL_PATH)"
30+
/usr/bin/ditto "$(APP_BUNDLE)" "$(APP_INSTALL_PATH)"
31+
@printf 'Installed %s\n' "$(APP_INSTALL_PATH)"
32+
33+
uninstall: ## Remove the installed app bundle from /Applications
34+
rm -rf "$(APP_INSTALL_PATH)"
35+
@printf 'Removed %s\n' "$(APP_INSTALL_PATH)"
36+
37+
open-dist-app: app ## Open the locally built app bundle from dist/
38+
open "$(APP_BUNDLE)"
39+
40+
open-installed-app: ## Open the installed app from /Applications
41+
open "$(APP_INSTALL_PATH)"
42+
43+
clean: ## Remove SwiftPM and packaging build artifacts
44+
swift package clean
45+
rm -rf "$(DIST_DIR)"

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ let package = Package(
1313
.executableTarget(
1414
name: "AeroMux",
1515
path: "Sources",
16-
resources: [
17-
.process("Resources"),
16+
exclude: [
17+
"Resources",
1818
]
1919
),
2020
]

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This is an early release MVP. It now ships as a GitHub Releases DMG and can stil
1717
- Supports a localhost refresh hook for lower-latency updates
1818
- Detects whether your AeroSpace left gap is large enough to avoid overlap
1919
- Lets you keep workspace positions stable instead of moving the active one to the top
20+
- Supports local workspace titles/descriptions from `~/.config/aeromux/workspaces.json`
2021

2122
## Why It Exists
2223

@@ -117,6 +118,42 @@ A helper script is included at `scripts/aerospace-refresh-hook.sh`.
117118

118119
The refresh listener binds only to `127.0.0.1:39173`.
119120

121+
## Workspace Memory File
122+
123+
AeroMux can read custom titles and descriptions for AeroSpace workspaces from:
124+
125+
```bash
126+
~/.config/aeromux/workspaces.json
127+
```
128+
129+
If `XDG_CONFIG_HOME` is set, AeroMux uses:
130+
131+
```bash
132+
$XDG_CONFIG_HOME/aeromux/workspaces.json
133+
```
134+
135+
The file is created automatically on first run and populated with the currently discovered AeroSpace workspaces:
136+
137+
```json
138+
{
139+
"workspaces": [
140+
{
141+
"workspace": "1",
142+
"title": "1",
143+
"description": null
144+
}
145+
]
146+
}
147+
```
148+
149+
Behavior:
150+
151+
- `workspace` must match the AeroSpace workspace name exactly
152+
- `title` replaces the default `Task <workspace>` label
153+
- `description` is shown under the title
154+
- when a custom title is present, AeroMux also shows the raw workspace name underneath as `Workspace <name>`
155+
- you can edit title and description directly from the sidebar using the pencil button on each workspace card
156+
120157
## AeroSpace Commands Used
121158

122159
AeroMux currently relies on these AeroSpace CLI commands:
@@ -222,3 +259,5 @@ Issues and compatibility reports are useful, especially for:
222259
## Releasing
223260

224261
If you are maintaining this repository, local packaging and tag-based GitHub Releases are documented in [docs/RELEASING.md](docs/RELEASING.md).
262+
263+
General local contributor workflow is documented in [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).

Sources/App/AppEnvironment.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ final class AppEnvironment {
1111
let bridgeServer: RefreshBridgeServer
1212
let windowController: SidebarWindowController
1313
let statusItemController: StatusItemController
14+
let workspaceMemoryStore: WorkspaceMemoryStore
1415

1516
init() {
1617
logger = AppLogger()
1718
settings = SettingsStore()
19+
workspaceMemoryStore = WorkspaceMemoryStore(logger: logger)
1820
let aerospaceExecutablePath = AeroSpaceExecutableResolver.resolve()
1921
let commandRunner = ProcessCommandRunner(logger: logger)
2022
let client = AeroSpaceClient(
@@ -37,6 +39,7 @@ final class AppEnvironment {
3739
settings: settings,
3840
client: client,
3941
configService: configService,
42+
workspaceMemoryStore: workspaceMemoryStore,
4043
stateStore: stateStore,
4144
logger: logger
4245
)
@@ -46,7 +49,9 @@ final class AppEnvironment {
4649
windowController = SidebarWindowController(
4750
settings: settings,
4851
stateStore: stateStore,
49-
focusService: focusService
52+
focusService: focusService,
53+
workspaceMemoryStore: workspaceMemoryStore,
54+
refreshCoordinator: refreshCoordinator
5055
)
5156
statusItemController = StatusItemController(
5257
settings: settings,

Sources/App/AppIconProvider.swift

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,31 @@ import AppKit
22

33
enum AppIconProvider {
44
static func applicationIconImage() -> NSImage? {
5-
loadImage(named: "AeroMuxAppIcon", extension: "png")
5+
loadImage(resourceName: "AeroMux.icns")
6+
?? loadDevelopmentImage(at: "../../../Packaging/AeroMux.icns")
67
}
78

89
static func statusItemImage() -> NSImage? {
9-
let image = loadImage(named: "AeroMuxStatusTemplate", extension: "png")
10+
let image = loadImage(resourceName: "AeroMuxStatusTemplate.png")
11+
?? loadDevelopmentImage(at: "../../../Sources/Resources/AeroMuxStatusTemplate.png")
1012
?? NSImage(systemSymbolName: "paperplane.fill", accessibilityDescription: "AeroMux")
1113
image?.isTemplate = true
1214
image?.size = NSSize(width: 18, height: 18)
1315
return image
1416
}
1517

16-
private static func loadImage(named name: String, extension ext: String) -> NSImage? {
17-
guard let url = Bundle.module.url(forResource: name, withExtension: ext) else {
18-
return nil
19-
}
18+
private static func loadImage(resourceName: String) -> NSImage? {
19+
guard let resourceURL = Bundle.main.resourceURL else { return nil }
20+
let url = resourceURL.appendingPathComponent(resourceName)
21+
return NSImage(contentsOf: url)
22+
}
23+
24+
private static func loadDevelopmentImage(at relativePath: String) -> NSImage? {
25+
let sourceFileURL = URL(fileURLWithPath: #filePath)
26+
let url = sourceFileURL
27+
.deletingLastPathComponent()
28+
.appending(path: relativePath)
29+
.standardizedFileURL
2030
return NSImage(contentsOf: url)
2131
}
2232
}

Sources/Models/WorkspaceState.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ struct WorkspaceGroup: Identifiable, Equatable {
4343
let workspaceName: String
4444
let windows: [WindowItem]
4545
let isFocused: Bool
46+
let titleOverride: String?
47+
let descriptionOverride: String?
4648
}
4749

4850
struct WindowItem: Identifiable, Equatable {
@@ -70,6 +72,10 @@ extension WorkspaceState {
7072
var visibleWorkspaceCount: Int {
7173
workspaces.count
7274
}
75+
76+
var focusedWorkspaceGroup: WorkspaceGroup? {
77+
workspaces.first(where: \.isFocused)
78+
}
7379
}
7480

7581
extension WindowItem {
@@ -83,3 +89,23 @@ extension WindowItem {
8389
return NSWorkspace.shared.runningApplications.first(where: { $0.localizedName == appName })?.icon
8490
}
8591
}
92+
93+
extension WorkspaceGroup {
94+
var displayTitle: String {
95+
titleOverride ?? "Task \(workspaceName)"
96+
}
97+
98+
var metadataLine: String? {
99+
if let titleOverride, titleOverride != workspaceName {
100+
return "Workspace \(workspaceName)"
101+
}
102+
return nil
103+
}
104+
105+
var detailLine: String {
106+
if let descriptionOverride {
107+
return descriptionOverride
108+
}
109+
return "\(windows.count) window\(windows.count == 1 ? "" : "s")"
110+
}
111+
}

Sources/Services/AeroSpaceClient.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,9 @@ extension AeroSpaceClient {
233233
WorkspaceGroup(
234234
workspaceName: workspaceName,
235235
windows: sortWindows(windows),
236-
isFocused: workspaceName == focusedWorkspaceName
236+
isFocused: workspaceName == focusedWorkspaceName,
237+
titleOverride: nil,
238+
descriptionOverride: nil
237239
)
238240
}
239241

@@ -242,7 +244,9 @@ extension AeroSpaceClient {
242244
WorkspaceGroup(
243245
workspaceName: focusedWorkspaceName,
244246
windows: [],
245-
isFocused: true
247+
isFocused: true,
248+
titleOverride: nil,
249+
descriptionOverride: nil
246250
)
247251
)
248252
}

Sources/Services/RefreshCoordinator.swift

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,24 @@ final class RefreshCoordinator {
1212
private let settings: SettingsStore
1313
private let client: AeroSpaceClient
1414
private let configService: AeroSpaceConfigService
15+
private let workspaceMemoryStore: WorkspaceMemoryStore
1516
private let stateStore: SidebarStateStore
1617
private let logger: AppLogger
1718
private var scheduledRefresh: Task<Void, Never>?
1819
private var pollingTask: Task<Void, Never>?
1920

20-
init(settings: SettingsStore, client: AeroSpaceClient, configService: AeroSpaceConfigService, stateStore: SidebarStateStore, logger: AppLogger) {
21+
init(
22+
settings: SettingsStore,
23+
client: AeroSpaceClient,
24+
configService: AeroSpaceConfigService,
25+
workspaceMemoryStore: WorkspaceMemoryStore,
26+
stateStore: SidebarStateStore,
27+
logger: AppLogger
28+
) {
2129
self.settings = settings
2230
self.client = client
2331
self.configService = configService
32+
self.workspaceMemoryStore = workspaceMemoryStore
2433
self.stateStore = stateStore
2534
self.logger = logger
2635
}
@@ -62,12 +71,26 @@ final class RefreshCoordinator {
6271
async let integrationTask = configService.integrationStatus(sidebarWidth: settings.sidebarWidth)
6372
let snapshot = try await snapshotTask
6473
let integrationStatus = await integrationTask
65-
let totalWindowCount = snapshot.workspaces.reduce(0) { $0 + $1.windows.count }
74+
async let workspaceMemoryTask = workspaceMemoryStore.metadataByWorkspace(
75+
for: snapshot.workspaces.map(\.workspaceName)
76+
)
77+
let workspaceMemory = await workspaceMemoryTask
78+
let annotatedWorkspaces = snapshot.workspaces.map { workspace in
79+
let metadata = workspaceMemory[workspace.workspaceName]
80+
return WorkspaceGroup(
81+
workspaceName: workspace.workspaceName,
82+
windows: workspace.windows,
83+
isFocused: workspace.isFocused,
84+
titleOverride: metadata?.title,
85+
descriptionOverride: metadata?.description
86+
)
87+
}
88+
let totalWindowCount = annotatedWorkspaces.reduce(0) { $0 + $1.windows.count }
6689
let status: SidebarStatus = totalWindowCount == 0 ? .empty : .ready
6790
let workspaceState = WorkspaceState(
6891
workspaceName: snapshot.workspaceName,
6992
monitorName: snapshot.monitorName,
70-
workspaces: snapshot.workspaces,
93+
workspaces: annotatedWorkspaces,
7194
focusedWindowId: snapshot.focusedWindowId,
7295
integrationStatus: integrationStatus,
7396
lastUpdatedAt: .now,

0 commit comments

Comments
 (0)