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 { Settings {
SettingsRootView(state: state) 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 w = geo.size.width
let h = geo.size.height let h = geo.size.height
let bodyWidth = w * 0.78 let bodyWidth = w * 0.8
let bodyHeight = h * 0.58 let bodyHeight = h * 0.56
let bodyRect = CGRect(x: (w - bodyWidth) / 2, y: h * 0.18, width: bodyWidth, height: bodyHeight) let bodyCorner = w * 0.08
let bodyY = h * 0.18
let armWidth = w * 0.2 let earWidth = w * 0.22
let armHeight = bodyHeight * 0.6 let earHeight = bodyHeight * 0.7
let armCorner = armWidth * 0.24 let earCorner = earWidth * 0.22
let legWidth = w * 0.11 let legWidth = w * 0.12
let legHeight = h * 0.26 let legHeight = h * 0.28
let legSpacing = w * 0.08 let legSpacing = w * 0.08
let legStartX = bodyRect.minX + w * 0.05 let legStartX = (w - (4 * legWidth + 3 * legSpacing)) / 2
let legY = bodyRect.maxY - legHeight * 0.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 eyeWidth = bodyWidth * 0.18
let eyeHeight = bodyHeight * 0.22 * eyeOpen let eyeHeight = bodyHeight * 0.24 * eyeOpen
let eyeY = bodyRect.midY - bodyHeight * 0.08 let eyeY = bodyY + bodyHeight * 0.45
let eyeOffset = bodyWidth * 0.2 let eyeOffset = bodyWidth * 0.22
Path { path in ZStack {
path.addRoundedRect(in: bodyRect, cornerSize: CGSize(width: w * 0.08, height: w * 0.08)) RoundedRectangle(cornerRadius: bodyCorner, style: .continuous)
.frame(width: bodyWidth, height: bodyHeight)
.offset(y: bodyY)
path.addRoundedRect( RoundedRectangle(cornerRadius: earCorner, style: .continuous)
in: CGRect(x: bodyRect.minX - armWidth * 0.65, y: bodyRect.midY - armHeight / 2, width: armWidth, height: armHeight), .frame(width: earWidth, height: earHeight)
cornerSize: CGSize(width: armCorner, height: armCorner) .offset(x: -bodyWidth * 0.5 + earWidth * 0.35, y: bodyY + bodyHeight * 0.02)
)
path.addRoundedRect( RoundedRectangle(cornerRadius: earCorner, style: .continuous)
in: CGRect(x: bodyRect.maxX - armWidth * 0.35, y: bodyRect.midY - armHeight / 2, width: armWidth, height: armHeight), .frame(width: earWidth, height: earHeight)
cornerSize: CGSize(width: armCorner, height: armCorner) .offset(x: bodyWidth * 0.5 - earWidth * 0.35, y: bodyY + bodyHeight * 0.02)
)
for i in 0 ..< 4 { Path { path in
let x = legStartX + CGFloat(i) * (legWidth + legSpacing) for i in 0 ..< 4 {
path.addRoundedRect( let x = legStartX + CGFloat(i) * (legWidth + legSpacing)
in: CGRect(x: x, y: legY, width: legWidth, height: legHeight), path.addRoundedRect(
cornerSize: CGSize(width: legWidth * 0.35, height: legWidth * 0.35) 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 ZStack {
path.move(to: CGPoint(x: leftEyeX - eyeWidth / 2, y: eyeY - eyeHeight)) Path { path in
path.addLine(to: CGPoint(x: leftEyeX + eyeWidth / 2, y: eyeY)) let centerX = w / 2 - eyeOffset
path.addLine(to: CGPoint(x: leftEyeX - eyeWidth / 2, y: eyeY + eyeHeight)) path.move(to: CGPoint(x: centerX - eyeWidth / 2, y: eyeY - eyeHeight))
path.closeSubpath() path.addLine(to: CGPoint(x: centerX + eyeWidth / 2, y: eyeY))
path.addLine(to: CGPoint(x: centerX - eyeWidth / 2, y: eyeY + eyeHeight))
let rightEyeX = bodyRect.midX + eyeOffset path.closeSubpath()
path.move(to: CGPoint(x: rightEyeX + eyeWidth / 2, y: eyeY - eyeHeight)) }
path.addLine(to: CGPoint(x: rightEyeX - eyeWidth / 2, y: eyeY)) Path { path in
path.addLine(to: CGPoint(x: rightEyeX + eyeWidth / 2, y: eyeY + eyeHeight)) let centerX = w / 2 + eyeOffset
path.closeSubpath() 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 @State private var selectedTab: SettingsTab = .general
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { TabView(selection: $selectedTab) {
Picker("", selection: $selectedTab) { GeneralSettings(state: state)
ForEach(SettingsTab.allCases, id: \.self) { tab in .tabItem { Label("General", systemImage: "gearshape") }
Text(tab.title).tag(tab) .tag(SettingsTab.general)
}
}
.pickerStyle(.segmented)
.padding([.top, .horizontal])
Divider() PermissionsSettings(status: permStatus, refresh: refreshPerms, showOnboarding: { OnboardingController.shared.show() })
.tabItem { Label("Permissions", systemImage: "lock.shield") }
.tag(SettingsTab.permissions)
Group { DebugSettings()
switch selectedTab { .tabItem { Label("Debug", systemImage: "ant") }
case .general: .tag(SettingsTab.debug)
GeneralSettings(state: state)
case .permissions: AboutSettings()
PermissionsSettings(status: permStatus, refresh: refreshPerms, showOnboarding: { OnboardingController.shared.show() }) .tabItem { Label("About", systemImage: "info.circle") }
case .debug: .tag(SettingsTab.about)
DebugSettings()
case .about:
AboutSettings()
}
}
.padding(16)
} }
.padding(12)
.task { await refreshPerms() } .task { await refreshPerms() }
} }
@@ -603,6 +607,8 @@ enum SettingsTab: CaseIterable {
struct GeneralSettings: View { struct GeneralSettings: View {
@ObservedObject var state: AppState @ObservedObject var state: AppState
@State private var isInstallingCLI = false
@State private var cliStatus: String?
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
@@ -624,7 +630,69 @@ struct GeneralSettings: View {
.labelsHidden() .labelsHidden()
.frame(width: 140) .frame(width: 140)
} }
Divider().padding(.vertical, 6)
cliInstaller
Spacer() 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: "; ")
} }
} }
} }