fix(mac): show critter menubar icon

This commit is contained in:
Peter Steinberger
2025-12-06 00:10:36 +01:00
parent 4aa275e13c
commit 0ec9c6c3cf

View File

@@ -358,7 +358,7 @@ struct ClawdisApp: App {
Settings {
SettingsRootView(state: state)
.frame(minWidth: 520, minHeight: 420)
.frame(minWidth: 520, minHeight: 460)
}
}
@@ -456,60 +456,71 @@ struct CritterGlyph: View {
let w = geo.size.width
let h = geo.size.height
let bodyWidth = w * 0.78
let bodyHeight = h * 0.58
let bodyRect = CGRect(x: (w - bodyWidth) / 2, y: h * 0.18, width: bodyWidth, height: bodyHeight)
let bodyWidth = w * 0.8
let bodyHeight = h * 0.56
let bodyCorner = w * 0.08
let bodyY = h * 0.18
let armWidth = w * 0.2
let armHeight = bodyHeight * 0.6
let armCorner = armWidth * 0.24
let earWidth = w * 0.22
let earHeight = bodyHeight * 0.7
let earCorner = earWidth * 0.22
let legWidth = w * 0.11
let legHeight = h * 0.26
let legWidth = w * 0.12
let legHeight = h * 0.28
let legSpacing = w * 0.08
let legStartX = bodyRect.minX + w * 0.05
let legY = bodyRect.maxY - legHeight * 0.2
let legStartX = (w - (4 * legWidth + 3 * legSpacing)) / 2
let legY = bodyY + bodyHeight - legHeight * 0.18
let eyeOpen = max(0.02, 1 - blinkAmount)
let eyeOpen = max(0.08, 1 - blinkAmount)
let eyeWidth = bodyWidth * 0.18
let eyeHeight = bodyHeight * 0.22 * eyeOpen
let eyeY = bodyRect.midY - bodyHeight * 0.08
let eyeOffset = bodyWidth * 0.2
let eyeHeight = bodyHeight * 0.24 * eyeOpen
let eyeY = bodyY + bodyHeight * 0.45
let eyeOffset = bodyWidth * 0.22
Path { path in
path.addRoundedRect(in: bodyRect, cornerSize: CGSize(width: w * 0.08, height: w * 0.08))
ZStack {
RoundedRectangle(cornerRadius: bodyCorner, style: .continuous)
.frame(width: bodyWidth, height: bodyHeight)
.offset(y: bodyY)
path.addRoundedRect(
in: CGRect(x: bodyRect.minX - armWidth * 0.65, y: bodyRect.midY - armHeight / 2, width: armWidth, height: armHeight),
cornerSize: CGSize(width: armCorner, height: armCorner)
)
RoundedRectangle(cornerRadius: earCorner, style: .continuous)
.frame(width: earWidth, height: earHeight)
.offset(x: -bodyWidth * 0.5 + earWidth * 0.35, y: bodyY + bodyHeight * 0.02)
path.addRoundedRect(
in: CGRect(x: bodyRect.maxX - armWidth * 0.35, y: bodyRect.midY - armHeight / 2, width: armWidth, height: armHeight),
cornerSize: CGSize(width: armCorner, height: armCorner)
)
RoundedRectangle(cornerRadius: earCorner, style: .continuous)
.frame(width: earWidth, height: earHeight)
.offset(x: bodyWidth * 0.5 - earWidth * 0.35, y: bodyY + bodyHeight * 0.02)
for i in 0 ..< 4 {
let x = legStartX + CGFloat(i) * (legWidth + legSpacing)
path.addRoundedRect(
in: CGRect(x: x, y: legY, width: legWidth, height: legHeight),
cornerSize: CGSize(width: legWidth * 0.35, height: legWidth * 0.35)
)
Path { path in
for i in 0 ..< 4 {
let x = legStartX + CGFloat(i) * (legWidth + legSpacing)
path.addRoundedRect(
in: CGRect(x: x, y: legY, width: legWidth, height: legHeight),
cornerSize: CGSize(width: legWidth * 0.36, height: legWidth * 0.36)
)
}
}
.fill()
let leftEyeX = bodyRect.midX - eyeOffset
path.move(to: CGPoint(x: leftEyeX - eyeWidth / 2, y: eyeY - eyeHeight))
path.addLine(to: CGPoint(x: leftEyeX + eyeWidth / 2, y: eyeY))
path.addLine(to: CGPoint(x: leftEyeX - eyeWidth / 2, y: eyeY + eyeHeight))
path.closeSubpath()
let rightEyeX = bodyRect.midX + eyeOffset
path.move(to: CGPoint(x: rightEyeX + eyeWidth / 2, y: eyeY - eyeHeight))
path.addLine(to: CGPoint(x: rightEyeX - eyeWidth / 2, y: eyeY))
path.addLine(to: CGPoint(x: rightEyeX + eyeWidth / 2, y: eyeY + eyeHeight))
path.closeSubpath()
ZStack {
Path { path in
let centerX = w / 2 - eyeOffset
path.move(to: CGPoint(x: centerX - eyeWidth / 2, y: eyeY - eyeHeight))
path.addLine(to: CGPoint(x: centerX + eyeWidth / 2, y: eyeY))
path.addLine(to: CGPoint(x: centerX - eyeWidth / 2, y: eyeY + eyeHeight))
path.closeSubpath()
}
Path { path in
let centerX = w / 2 + eyeOffset
path.move(to: CGPoint(x: centerX + eyeWidth / 2, y: eyeY - eyeHeight))
path.addLine(to: CGPoint(x: centerX - eyeWidth / 2, y: eyeY))
path.addLine(to: CGPoint(x: centerX + eyeWidth / 2, y: eyeY + eyeHeight))
path.closeSubpath()
}
}
.compositingGroup()
.blendMode(.destinationOut)
}
.fill(style: FillStyle(eoFill: true, antialiased: true))
.compositingGroup()
}
}
}
@@ -552,31 +563,24 @@ struct SettingsRootView: View {
@State private var selectedTab: SettingsTab = .general
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Picker("", selection: $selectedTab) {
ForEach(SettingsTab.allCases, id: \.self) { tab in
Text(tab.title).tag(tab)
}
}
.pickerStyle(.segmented)
.padding([.top, .horizontal])
TabView(selection: $selectedTab) {
GeneralSettings(state: state)
.tabItem { Label("General", systemImage: "gearshape") }
.tag(SettingsTab.general)
Divider()
PermissionsSettings(status: permStatus, refresh: refreshPerms, showOnboarding: { OnboardingController.shared.show() })
.tabItem { Label("Permissions", systemImage: "lock.shield") }
.tag(SettingsTab.permissions)
Group {
switch selectedTab {
case .general:
GeneralSettings(state: state)
case .permissions:
PermissionsSettings(status: permStatus, refresh: refreshPerms, showOnboarding: { OnboardingController.shared.show() })
case .debug:
DebugSettings()
case .about:
AboutSettings()
}
}
.padding(16)
DebugSettings()
.tabItem { Label("Debug", systemImage: "ant") }
.tag(SettingsTab.debug)
AboutSettings()
.tabItem { Label("About", systemImage: "info.circle") }
.tag(SettingsTab.about)
}
.padding(12)
.task { await refreshPerms() }
}
@@ -603,6 +607,8 @@ enum SettingsTab: CaseIterable {
struct GeneralSettings: View {
@ObservedObject var state: AppState
@State private var isInstallingCLI = false
@State private var cliStatus: String?
var body: some View {
VStack(alignment: .leading, spacing: 12) {
@@ -624,7 +630,69 @@ struct GeneralSettings: View {
.labelsHidden()
.frame(width: 140)
}
Divider().padding(.vertical, 6)
cliInstaller
Spacer()
HStack {
Spacer()
Button("Quit Clawdis") { NSApp.terminate(nil) }
.buttonStyle(.borderedProminent)
}
}
}
private var cliInstaller: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 10) {
Button {
Task { await installCLI() }
} label: {
if isInstallingCLI {
ProgressView().controlSize(.small)
} else {
Text("Install CLI helper")
}
}
.disabled(isInstallingCLI)
if let status = cliStatus {
Text(status)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
}
}
Text("Symlink \"clawdis-mac\" into /usr/local/bin and /opt/homebrew/bin for scripts.")
.font(.callout)
.foregroundStyle(.secondary)
}
}
private func installCLI() async {
guard !isInstallingCLI else { return }
isInstallingCLI = true
defer { isInstallingCLI = false }
let helper = Bundle.main.bundleURL.appendingPathComponent("Contents/MacOS/ClawdisCLI")
guard FileManager.default.isExecutableFile(atPath: helper.path) else {
await MainActor.run { cliStatus = "Helper missing in bundle; rebuild via scripts/package-mac-app.sh" }
return
}
let targets = ["/usr/local/bin/clawdis-mac", "/opt/homebrew/bin/clawdis-mac"]
var messages: [String] = []
for target in targets {
do {
try FileManager.default.createDirectory(atPath: (target as NSString).deletingLastPathComponent, withIntermediateDirectories: true)
try? FileManager.default.removeItem(atPath: target)
try FileManager.default.createSymbolicLink(atPath: target, withDestinationPath: helper.path)
messages.append("Linked \(target)")
} catch {
messages.append("Failed \(target): \(error.localizedDescription)")
}
}
await MainActor.run {
cliStatus = messages.joined(separator: "; ")
}
}
}