Skip to content

Commit 52f51ee

Browse files
Refine compact sidebar layout
1 parent 4b323a4 commit 52f51ee

8 files changed

Lines changed: 75 additions & 79 deletions

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ This is an early release MVP. It now ships as a GitHub Releases DMG and can stil
1010

1111
- Shows non-empty AeroSpace workspaces in a persistent left sidebar
1212
- Highlights the focused workspace and focused window
13-
- Lists windows inside each workspace with optional app icons
13+
- Lists windows inside each workspace by app name
1414
- Lets you click a row to focus that window through the AeroSpace CLI
1515
- Adds a menu bar item for show/hide, refresh, and quit
1616
- Lets you change the sidebar width from the menu bar
17+
- Lets you enable compact mode from the menu bar
1718
- Lets you enable launch at login from the menu bar
1819
- Polls AeroSpace every second by default
1920
- Supports a localhost refresh hook for lower-latency updates
@@ -141,6 +142,7 @@ The file is created automatically on first run. Current keys:
141142

142143
```json
143144
{
145+
"compactMode": false,
144146
"launchAtLogin": false,
145147
"pinActiveWorkspaceFirst": false,
146148
"sidebarWidth": 260
@@ -149,7 +151,8 @@ The file is created automatically on first run. Current keys:
149151

150152
Behavior:
151153

152-
- `sidebarWidth` is the sidebar width in pixels and is clamped to whole numbers between `180` and `600`
154+
- `sidebarWidth` is the sidebar width in pixels and is clamped to whole numbers between `100` and `600`
155+
- `compactMode` enables a denser sidebar layout with slightly smaller text
153156
- `pinActiveWorkspaceFirst` controls whether the focused workspace is moved to the top
154157
- `launchAtLogin` controls whether AeroMux asks macOS to launch it at login
155158
- changes made in the file are picked up the next time AeroMux launches

Sources/App/StatusItemController.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ final class StatusItemController: NSObject, NSMenuDelegate {
1313
private let toggleSidebarItem = NSMenuItem()
1414
private let sidebarWidthItem = NSMenuItem()
1515
private let reorderWorkspacesItem = NSMenuItem()
16+
private let compactModeItem = NSMenuItem()
1617
private let launchAtLoginItem = NSMenuItem()
1718
private let refreshItem = NSMenuItem()
1819
private let quitItem = NSMenuItem()
@@ -63,6 +64,10 @@ final class StatusItemController: NSObject, NSMenuDelegate {
6364
reorderWorkspacesItem.target = self
6465
reorderWorkspacesItem.action = #selector(toggleWorkspaceReordering)
6566

67+
compactModeItem.title = "Compact Mode"
68+
compactModeItem.target = self
69+
compactModeItem.action = #selector(toggleCompactMode)
70+
6671
launchAtLoginItem.title = "Launch at Login"
6772
launchAtLoginItem.target = self
6873
launchAtLoginItem.action = #selector(toggleLaunchAtLogin)
@@ -79,6 +84,7 @@ final class StatusItemController: NSObject, NSMenuDelegate {
7984
toggleSidebarItem,
8085
sidebarWidthItem,
8186
reorderWorkspacesItem,
87+
compactModeItem,
8288
launchAtLoginItem,
8389
.separator(),
8490
refreshItem,
@@ -91,6 +97,7 @@ final class StatusItemController: NSObject, NSMenuDelegate {
9197
toggleSidebarItem.title = windowController.isVisible ? "Hide Sidebar" : "Show Sidebar"
9298
sidebarWidthItem.title = "Sidebar Width: \(Int(settings.sidebarWidth)) px"
9399
reorderWorkspacesItem.state = settings.reordersFocusedWorkspaceToTop ? .on : .off
100+
compactModeItem.state = settings.compactMode ? .on : .off
94101
launchAtLoginItem.state = settings.launchesAtLogin ? .on : .off
95102
}
96103

@@ -112,6 +119,13 @@ final class StatusItemController: NSObject, NSMenuDelegate {
112119
updateMenuState()
113120
}
114121

122+
@objc
123+
private func toggleCompactMode() {
124+
settings.compactMode.toggle()
125+
settings.persist()
126+
updateMenuState()
127+
}
128+
115129
@objc
116130
private func toggleLaunchAtLogin() {
117131
let desiredState = !settings.launchesAtLogin

Sources/Models/WorkspaceState.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,7 @@ extension WorkspaceGroup {
102102
return nil
103103
}
104104

105-
var detailLine: String {
106-
if let descriptionOverride {
107-
return descriptionOverride
108-
}
109-
return "\(windows.count) window\(windows.count == 1 ? "" : "s")"
105+
var detailLine: String? {
106+
descriptionOverride
110107
}
111108
}

Sources/Services/AeroSpaceClient.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,10 +277,6 @@ extension AeroSpaceClient {
277277

278278
private static func sortWindows(_ windows: [WindowItem]) -> [WindowItem] {
279279
windows.sorted { lhs, rhs in
280-
if lhs.isFocused != rhs.isFocused {
281-
return lhs.isFocused && !rhs.isFocused
282-
}
283-
284280
let appOrder = lhs.appName.localizedStandardCompare(rhs.appName)
285281
if appOrder != .orderedSame {
286282
return appOrder == .orderedAscending

Sources/Services/SettingsStore.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@ final class SettingsStore: ObservableObject {
88
}
99

1010
static let defaultSidebarWidth: CGFloat = 260
11-
static let sidebarWidthRange: ClosedRange<CGFloat> = 180 ... 600
11+
static let sidebarWidthRange: ClosedRange<CGFloat> = 100 ... 600
1212

1313
@Published var sidebarWidth: CGFloat
1414
@Published var monitorMode: MonitorMode
1515
@Published var pollInterval: TimeInterval
16-
@Published var showsAppIcons: Bool
1716
@Published var usesDarkAppearance: Bool
1817
@Published var enableDebugLogging: Bool
1918
@Published var reordersFocusedWorkspaceToTop: Bool
2019
@Published var launchesAtLogin: Bool
20+
@Published var compactMode: Bool
2121

2222
private let defaults: UserDefaults
2323
private let fileManager: FileManager
@@ -58,13 +58,13 @@ final class SettingsStore: ObservableObject {
5858
)
5959
monitorMode = MonitorMode(rawValue: defaults.string(forKey: Keys.monitorMode) ?? "") ?? .main
6060
pollInterval = defaults.object(forKey: Keys.pollInterval) as? TimeInterval ?? 1.0
61-
showsAppIcons = defaults.object(forKey: Keys.showsAppIcons) as? Bool ?? true
6261
usesDarkAppearance = defaults.object(forKey: Keys.usesDarkAppearance) as? Bool ?? true
6362
enableDebugLogging = defaults.object(forKey: Keys.enableDebugLogging) as? Bool ?? false
6463
reordersFocusedWorkspaceToTop = persistedConfig?.pinActiveWorkspaceFirst
6564
?? defaults.object(forKey: Keys.reordersFocusedWorkspaceToTop) as? Bool
6665
?? false
6766
launchesAtLogin = persistedConfig?.launchAtLogin ?? false
67+
compactMode = persistedConfig?.compactMode ?? false
6868

6969
if shouldBootstrapConfig {
7070
persistConfig()
@@ -78,9 +78,9 @@ final class SettingsStore: ObservableObject {
7878
removeLegacyConfigDefaults()
7979
defaults.set(monitorMode.rawValue, forKey: Keys.monitorMode)
8080
defaults.set(pollInterval, forKey: Keys.pollInterval)
81-
defaults.set(showsAppIcons, forKey: Keys.showsAppIcons)
8281
defaults.set(usesDarkAppearance, forKey: Keys.usesDarkAppearance)
8382
defaults.set(enableDebugLogging, forKey: Keys.enableDebugLogging)
83+
defaults.removeObject(forKey: Keys.showsAppIcons)
8484
}
8585

8686
func setSidebarWidth(_ width: CGFloat) {
@@ -97,7 +97,8 @@ final class SettingsStore: ObservableObject {
9797
let payload = PersistedConfig(
9898
sidebarWidth: Double(sidebarWidth),
9999
pinActiveWorkspaceFirst: reordersFocusedWorkspaceToTop,
100-
launchAtLogin: launchesAtLogin
100+
launchAtLogin: launchesAtLogin,
101+
compactMode: compactMode
101102
)
102103
let encoder = JSONEncoder()
103104
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
@@ -138,6 +139,7 @@ final class SettingsStore: ObservableObject {
138139
var sidebarWidth: Double?
139140
var pinActiveWorkspaceFirst: Bool?
140141
var launchAtLogin: Bool?
142+
var compactMode: Bool?
141143
}
142144

143145
private enum Keys {

Sources/UI/SidebarRootView.swift

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,16 @@ struct SidebarRootView: View {
1212
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
1313
.ignoresSafeArea()
1414

15-
VStack(alignment: .leading, spacing: 16) {
15+
VStack(alignment: .leading, spacing: 12) {
1616
header
1717
if let integrationMessage = stateStore.state.integrationStatus.message {
1818
integrationWarning(message: integrationMessage)
1919
}
2020
content
2121
Spacer(minLength: 0)
2222
}
23-
.padding(.horizontal, 16)
24-
.padding(.vertical, 18)
23+
.padding(.horizontal, 10)
24+
.padding(.vertical, 12)
2525
}
2626
.frame(maxWidth: .infinity, maxHeight: .infinity)
2727
.preferredColorScheme(settings.usesDarkAppearance ? .dark : .light)
@@ -33,21 +33,17 @@ struct SidebarRootView: View {
3333
}
3434

3535
private var header: some View {
36-
VStack(alignment: .leading, spacing: 8) {
36+
VStack(alignment: .leading, spacing: 6) {
3737
Text("AeroMux")
38-
.font(.system(size: 11, weight: .semibold, design: .rounded))
38+
.font(.system(size: 10, weight: .semibold, design: .rounded))
3939
.foregroundStyle(.secondary)
4040

41-
Text("Active: \(stateStore.state.focusedWorkspaceGroup?.displayTitle ?? stateStore.state.workspaceName)")
42-
.font(.system(size: 24, weight: .bold, design: .rounded))
43-
.lineLimit(1)
44-
4541
HStack(spacing: 6) {
4642
Text("\(stateStore.state.visibleWorkspaceCount) task\(stateStore.state.visibleWorkspaceCount == 1 ? "" : "s")")
4743
Text("")
4844
Text("\(stateStore.state.totalWindowCount) window\(stateStore.state.totalWindowCount == 1 ? "" : "s")")
4945
}
50-
.font(.system(size: 12, weight: .medium, design: .rounded))
46+
.font(.system(size: 10, weight: .medium, design: .rounded))
5147
.foregroundStyle(.secondary)
5248
}
5349
}
@@ -63,46 +59,46 @@ struct SidebarRootView: View {
6359
stateView(title: "No windows open", message: "Open a window in any AeroSpace workspace to populate the task rail.")
6460
case .ready:
6561
ScrollView {
66-
LazyVStack(alignment: .leading, spacing: 10) {
62+
LazyVStack(alignment: .leading, spacing: 6) {
6763
ForEach(stateStore.state.workspaces) { workspace in
6864
WorkspaceSectionView(
6965
workspace: workspace,
70-
showsIcons: settings.showsAppIcons,
66+
isCompact: settings.compactMode,
7167
focusService: focusService,
7268
allWorkspaceNames: stateStore.state.workspaces.map(\.workspaceName),
7369
workspaceMemoryStore: workspaceMemoryStore,
7470
refreshCoordinator: refreshCoordinator
7571
)
7672
}
7773
}
78-
.padding(.top, 4)
74+
.padding(.top, 2)
7975
}
8076
}
8177
}
8278

8379
private var loadingView: some View {
84-
HStack(spacing: 12) {
80+
HStack(spacing: 10) {
8581
ProgressView()
8682
.controlSize(.small)
8783
Text("Refreshing workspace")
88-
.font(.system(size: 13, weight: .medium, design: .rounded))
84+
.font(.system(size: 11, weight: .medium, design: .rounded))
8985
}
9086
.foregroundStyle(.secondary)
9187
}
9288

9389
private func integrationWarning(message: String) -> some View {
9490
VStack(alignment: .leading, spacing: 6) {
9591
Text("AeroSpace Integration")
96-
.font(.system(size: 12, weight: .semibold, design: .rounded))
92+
.font(.system(size: 10, weight: .semibold, design: .rounded))
9793
Text(message)
98-
.font(.system(size: 12, weight: .regular, design: .rounded))
94+
.font(.system(size: 10, weight: .regular, design: .rounded))
9995
.foregroundStyle(.secondary)
10096
Text("Expected: `outer.left = [{ monitor.main = \(Int(settings.sidebarWidth)) }, 0]`")
101-
.font(.system(size: 11, weight: .medium, design: .rounded))
97+
.font(.system(size: 10, weight: .medium, design: .rounded))
10298
.foregroundStyle(.secondary)
10399
.textSelection(.enabled)
104100
}
105-
.padding(12)
101+
.padding(10)
106102
.frame(maxWidth: .infinity, alignment: .leading)
107103
.background(
108104
RoundedRectangle(cornerRadius: 14, style: .continuous)
@@ -117,12 +113,12 @@ struct SidebarRootView: View {
117113
private func stateView(title: String, message: String) -> some View {
118114
VStack(alignment: .leading, spacing: 10) {
119115
Text(title)
120-
.font(.system(size: 15, weight: .semibold, design: .rounded))
116+
.font(.system(size: 13, weight: .semibold, design: .rounded))
121117
Text(message)
122-
.font(.system(size: 13, weight: .regular, design: .rounded))
118+
.font(.system(size: 11, weight: .regular, design: .rounded))
123119
.foregroundStyle(.secondary)
124120
}
125-
.padding(14)
121+
.padding(10)
126122
.frame(maxWidth: .infinity, alignment: .leading)
127123
.background(
128124
RoundedRectangle(cornerRadius: 14, style: .continuous)

Sources/UI/WindowRowView.swift

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,27 @@ import SwiftUI
22

33
struct WindowRowView: View {
44
let item: WindowItem
5-
let showsIcon: Bool
5+
let isCompact: Bool
66

77
var body: some View {
8-
HStack(alignment: .top, spacing: 10) {
8+
HStack(alignment: .center, spacing: 8) {
99
Circle()
1010
.fill(item.isFocused ? Color.green : Color.clear)
1111
.overlay(
1212
Circle()
1313
.stroke(item.isFocused ? Color.green : Color.white.opacity(0.35), lineWidth: 1)
1414
)
15-
.frame(width: 10, height: 10)
16-
.padding(.top, 5)
15+
.frame(width: 8, height: 8)
1716

18-
if showsIcon, let icon = item.resolvedIcon {
19-
Image(nsImage: icon)
20-
.resizable()
21-
.aspectRatio(contentMode: .fit)
22-
.frame(width: 18, height: 18)
23-
.clipShape(RoundedRectangle(cornerRadius: 4, style: .continuous))
24-
.padding(.top, 2)
25-
}
26-
27-
VStack(alignment: .leading, spacing: 3) {
28-
Text(item.appName)
29-
.font(.system(size: 13, weight: .semibold, design: .rounded))
30-
.foregroundStyle(.primary)
31-
.lineLimit(1)
32-
33-
Text(item.windowTitle.isEmpty ? "Untitled window" : item.windowTitle)
34-
.font(.system(size: 12, weight: .regular, design: .rounded))
35-
.foregroundStyle(.secondary)
36-
.lineLimit(1)
37-
.truncationMode(.tail)
38-
}
17+
Text(item.appName)
18+
.font(.system(size: 11, weight: .semibold, design: .rounded))
19+
.foregroundStyle(.primary)
20+
.lineLimit(1)
3921

4022
Spacer(minLength: 0)
4123
}
42-
.padding(12)
24+
.padding(.horizontal, 8)
25+
.padding(.vertical, 7)
4326
.frame(maxWidth: .infinity, alignment: .leading)
4427
.background(
4528
RoundedRectangle(cornerRadius: 14, style: .continuous)

0 commit comments

Comments
 (0)