From cbc58c0142cf6bd2c5feda270c0907733be5b2e1 Mon Sep 17 00:00:00 2001 From: Jiun Bae Date: Wed, 7 Oct 2026 13:45:25 +0900 Subject: [PATCH] chore: drop the unused widget tree renderer; no compiler warnings The desktop widget draws templates since R14-B, so WidgetNodeView only kept its palette in use; that moves to WidgetPalette.swift. Installed is Sendable, which was the last Swift 6 warning. Co-Authored-By: Claude Opus 5.5 (1M context) --- Sources/MenubucketCore/UpdateInstaller.swift | 2 +- .../BarShelfWidgets.xcodeproj/project.pbxproj | 12 +- .../BarShelfWidgets/WidgetNodeView.swift | 265 ------------------ .../BarShelfWidgets/WidgetPalette.swift | 36 +++ 4 files changed, 43 insertions(+), 272 deletions(-) delete mode 100644 WidgetExtension/BarShelfWidgets/WidgetNodeView.swift create mode 100644 WidgetExtension/BarShelfWidgets/WidgetPalette.swift diff --git a/Sources/MenubucketCore/UpdateInstaller.swift b/Sources/MenubucketCore/UpdateInstaller.swift index a88ad28..159290d 100644 --- a/Sources/MenubucketCore/UpdateInstaller.swift +++ b/Sources/MenubucketCore/UpdateInstaller.swift @@ -184,7 +184,7 @@ public enum UpdateInstaller { /// failure that made it necessary was refused by the kernel for the /// destination path specifically, so the same bundle launched from a /// staging directory came up perfectly. - public struct Installed { + public struct Installed: Sendable { public let app: URL public let version: String? /// The previous bundle, parked next to `app`. Nil when there was diff --git a/WidgetExtension/BarShelfWidgets.xcodeproj/project.pbxproj b/WidgetExtension/BarShelfWidgets.xcodeproj/project.pbxproj index 6a80487..b24a96d 100644 --- a/WidgetExtension/BarShelfWidgets.xcodeproj/project.pbxproj +++ b/WidgetExtension/BarShelfWidgets.xcodeproj/project.pbxproj @@ -13,16 +13,16 @@ 36AC0C5864DB27615C69D8BC /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 7B505F8DF3C2A9D1FF94FE91 /* Localizable.xcstrings */; }; 4195A0A5241CEF6A27FD0695 /* ShelfWidgetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2961B05761B714139E0AC3AB /* ShelfWidgetIntent.swift */; }; 435CBB33D3D3C9EC304AC1D9 /* ShelfTemplates.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0936B164A6CC8C20AA933D7 /* ShelfTemplates.swift */; }; - 446AF0B171DBCDADC2B8DF85 /* WidgetNodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61BB6D5CEA5D1651EF109D8 /* WidgetNodeView.swift */; }; 476453C0C400EC58CDCF3F2A /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 7B505F8DF3C2A9D1FF94FE91 /* Localizable.xcstrings */; }; 480743A44B71F0316790DCBD /* ShelfWidgetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2961B05761B714139E0AC3AB /* ShelfWidgetIntent.swift */; }; 5236C8258B5158648CAFFFFE /* MenubucketCore in Frameworks */ = {isa = PBXBuildFile; productRef = 910104821328EDAEEB51C053 /* MenubucketCore */; }; - 5A8E9EA08A74F81BCB6A909A /* WidgetNodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C61BB6D5CEA5D1651EF109D8 /* WidgetNodeView.swift */; }; 7984B92A85BDD37317BDAA01 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 49D0524C67478B39B0C53A99 /* WidgetKit.framework */; }; + 7F9C9A39C95831AB300FC7BD /* WidgetPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95789FAD0FF181FBF12F1A5E /* WidgetPalette.swift */; }; B162097A519560CD0658C1AE /* MenubucketCore in Frameworks */ = {isa = PBXBuildFile; productRef = F712C895B770A2690B947749 /* MenubucketCore */; }; BCC5CD171C8CF94762900E0B /* ShelfWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BB49CE264140519F81A6BFF /* ShelfWidget.swift */; }; D053038D4DF70749934C093A /* ShelfWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BB49CE264140519F81A6BFF /* ShelfWidget.swift */; }; E79A3C28FA10A32C6F34A361 /* ShelfTemplates.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0936B164A6CC8C20AA933D7 /* ShelfTemplates.swift */; }; + EE48C9CD5D54AD7B4287D537 /* WidgetPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95789FAD0FF181FBF12F1A5E /* WidgetPalette.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -35,11 +35,11 @@ 5FE557425549274E252DF4E9 /* barshelf */ = {isa = PBXFileReference; lastKnownFileType = folder; name = barshelf; path = ..; sourceTree = SOURCE_ROOT; }; 7B505F8DF3C2A9D1FF94FE91 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 7BB49CE264140519F81A6BFF /* ShelfWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShelfWidget.swift; sourceTree = ""; }; + 95789FAD0FF181FBF12F1A5E /* WidgetPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetPalette.swift; sourceTree = ""; }; 9E670D45E7A7EBE006E48708 /* BarShelfWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = BarShelfWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; }; B0936B164A6CC8C20AA933D7 /* ShelfTemplates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShelfTemplates.swift; sourceTree = ""; }; B7E2C8D8F0992AF74253EA6A /* BarShelfWidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarShelfWidgetsBundle.swift; sourceTree = ""; }; BAFDAA46AAA4E5811FE4F1DF /* BarShelfWidgetsPreview */ = {isa = PBXFileReference; includeInIndex = 0; path = BarShelfWidgetsPreview; sourceTree = BUILT_PRODUCTS_DIR; }; - C61BB6D5CEA5D1651EF109D8 /* WidgetNodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetNodeView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -119,7 +119,7 @@ B0936B164A6CC8C20AA933D7 /* ShelfTemplates.swift */, 7BB49CE264140519F81A6BFF /* ShelfWidget.swift */, 2961B05761B714139E0AC3AB /* ShelfWidgetIntent.swift */, - C61BB6D5CEA5D1651EF109D8 /* WidgetNodeView.swift */, + 95789FAD0FF181FBF12F1A5E /* WidgetPalette.swift */, ); path = BarShelfWidgets; sourceTree = ""; @@ -229,7 +229,7 @@ E79A3C28FA10A32C6F34A361 /* ShelfTemplates.swift in Sources */, BCC5CD171C8CF94762900E0B /* ShelfWidget.swift in Sources */, 480743A44B71F0316790DCBD /* ShelfWidgetIntent.swift in Sources */, - 446AF0B171DBCDADC2B8DF85 /* WidgetNodeView.swift in Sources */, + 7F9C9A39C95831AB300FC7BD /* WidgetPalette.swift in Sources */, 137DD31FFDC6820F023437B7 /* main.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -242,7 +242,7 @@ 435CBB33D3D3C9EC304AC1D9 /* ShelfTemplates.swift in Sources */, D053038D4DF70749934C093A /* ShelfWidget.swift in Sources */, 4195A0A5241CEF6A27FD0695 /* ShelfWidgetIntent.swift in Sources */, - 5A8E9EA08A74F81BCB6A909A /* WidgetNodeView.swift in Sources */, + EE48C9CD5D54AD7B4287D537 /* WidgetPalette.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/WidgetExtension/BarShelfWidgets/WidgetNodeView.swift b/WidgetExtension/BarShelfWidgets/WidgetNodeView.swift deleted file mode 100644 index 2479286..0000000 --- a/WidgetExtension/BarShelfWidgets/WidgetNodeView.swift +++ /dev/null @@ -1,265 +0,0 @@ -import MenubucketCore -import SwiftUI -import WidgetKit - -/// Draws a BarShelf view tree inside a macOS widget. -/// -/// A smaller sibling of the app's `ViewTreeRenderer`: widgets cannot scroll, -/// take clicks on parts of themselves, load images from the network, or run -/// a 1 Hz timer, so (R14 §2): -/// - `scroll` shows its content clipped, `list` its first rows, `grid` a -/// fixed grid; -/// - countdowns use the system timer views, which tick without reloads; -/// - images other than SF Symbols and monograms fall back to a monogram; -/// - buttons become plain labels — the whole widget opens BarShelf. -struct WidgetNodeView: View { - let node: UINode - let accent: Color - /// Rows a `list` or `grid` shows before it is cut. - var rowLimit: Int = 8 - - var body: some View { - if node.hidden == true { - EmptyView() - } else { - content - .padding(.all, node.padding ?? 0) - .frame(maxWidth: node.widthFill == true ? .infinity : nil, alignment: .leading) - } - } - - private func child(_ node: UINode) -> WidgetNodeView { - WidgetNodeView(node: node, accent: accent, rowLimit: rowLimit) - } - - private func children(_ nodes: [UINode]) -> some View { - ForEach(Array(nodes.enumerated()), id: \.offset) { _, node in child(node) } - } - - @ViewBuilder - private var content: some View { - switch UINode.KnownType(rawValue: node.type) { - case .vstack: - VStack(alignment: .leading, spacing: node.spacing ?? 6) { children(node.children ?? []) } - case .hstack: - HStack(alignment: hstackAlignment, spacing: node.spacing ?? 6) { children(node.children ?? []) } - case .zstack: - ZStack { children(node.children ?? []) } - case .scroll: - if let inner = node.child?.node { child(inner) } - case .list: - VStack(alignment: .leading, spacing: node.spacing ?? 4) { - children(Array((node.items ?? node.children ?? []).prefix(rowLimit))) - } - case .grid: - grid - case .section: - VStack(alignment: .leading, spacing: node.spacing ?? 4) { - if let title = node.title { - Text(title).font(.caption.weight(.semibold)).foregroundStyle(.secondary) - } - children(node.children ?? []) - } - case .card: - let color = WidgetPalette.color(node.tone ?? node.tint, accent: accent) ?? accent - VStack(alignment: .leading, spacing: node.spacing ?? 6) { children(node.children ?? []) } - .padding(node.padding ?? 8) - .frame(maxWidth: .infinity, alignment: .leading) - .background(RoundedRectangle(cornerRadius: 8, style: .continuous).fill(color.opacity(0.10))) - case .text: - text - case .image: - image - case .progress: - progress - case .button: - // The widget as a whole opens BarShelf; a button is just its label. - Label(node.title ?? node.text ?? "", systemImage: node.icon ?? "arrow.up.forward.app") - .font(.caption) - .foregroundStyle(accent) - case .badge: - let color = WidgetPalette.color(node.tint ?? node.tone, accent: accent) ?? .secondary - Text(node.text ?? node.title ?? "") - .font(.system(size: 10, weight: .medium)) - .lineLimit(1) - .fixedSize() - .padding(.horizontal, 6) - .padding(.vertical, 2) - .foregroundStyle(color) - .background(Capsule().fill(color.opacity(0.15))) - case .banner: - let color = WidgetPalette.color(node.tone ?? node.tint, accent: accent) ?? .orange - Label(node.text ?? node.title ?? "", systemImage: node.icon ?? "exclamationmark.triangle.fill") - .font(.caption) - .foregroundStyle(color) - case .empty: - VStack(spacing: 4) { - if let icon = node.icon { Image(systemName: icon).foregroundStyle(.secondary) } - if let title = node.title { Text(title).font(.caption.weight(.semibold)) } - if let subtitle = node.subtitle { - Text(subtitle).font(.caption2).foregroundStyle(.secondary) - } - } - .frame(maxWidth: .infinity) - case .divider: - Divider() - case .spacer: - Spacer(minLength: node.minLength ?? 0) - case .some(.none), nil: - EmptyView() - } - } - - private var hstackAlignment: VerticalAlignment { - switch node.alignment { - case "top": return .top - case "bottom": return .bottom - case "baseline": return .firstTextBaseline - default: return .center - } - } - - // MARK: - Leaves - - private var text: some View { - var font: Font - var color: Color? = WidgetPalette.color(node.foreground, accent: accent) - switch node.role { - case "title": font = .system(size: 13, weight: .semibold) - case "caption": font = .caption; color = color ?? .secondary - case "code": font = .system(size: 11, design: .monospaced) - default: font = .system(size: 12) - } - if let size = node.size { - font = .system( - size: CGFloat(size), - weight: node.role == "title" ? .bold : .regular, - design: node.role == "code" ? .monospaced : .default - ) - } - if node.monospacedDigit == true { font = font.monospacedDigit() } - return Text(node.text ?? "") - .font(font) - .foregroundStyle(color ?? .primary) - .lineLimit(node.lineLimit ?? 2) - .minimumScaleFactor(0.7) - } - - @ViewBuilder - private var image: some View { - let size = CGFloat(node.size ?? 14) - if let source = node.source, source.kind == "sfSymbol", let name = source.name { - Image(systemName: name) - .font(.system(size: size)) - .foregroundStyle(WidgetPalette.color(node.tint ?? node.foreground, accent: accent) ?? .primary) - } else { - // Files, thumbnails, remote images, and brand marks need the app; - // a monogram keeps the row's shape. - let letter = node.source?.monogram ?? node.source?.name ?? node.accessibilityLabel ?? "•" - Text(String(letter.prefix(1)).uppercased()) - .font(.system(size: size * 0.55, weight: .semibold)) - .foregroundStyle(.white) - .frame(width: size, height: size) - .background(Circle().fill(WidgetPalette.color(node.tint, accent: accent) ?? accent)) - } - } - - @ViewBuilder - private var progress: some View { - let tint = WidgetPalette.color(node.tint, accent: accent) ?? accent - if let countdown = node.countdown { - // The system timer views tick on their own, with no reloads. - let interval = Date(timeIntervalSince1970: countdown.from / 1000)...Date(timeIntervalSince1970: max(countdown.from, countdown.until) / 1000) - if node.style == "ring" { - ProgressView(timerInterval: interval, countsDown: true) { EmptyView() } currentValueLabel: { EmptyView() } - .progressViewStyle(.circular) - .tint(tint) - .frame(width: CGFloat(node.size ?? 26)) - } else { - HStack(spacing: 6) { - if let label = node.label { - Text(label).font(.caption).foregroundStyle(.secondary).lineLimit(1) - } - ProgressView(timerInterval: interval, countsDown: true) { EmptyView() } currentValueLabel: { EmptyView() } - .tint(tint) - } - } - } else if node.style == "ring" { - let diameter = CGFloat(node.size ?? 26) - ZStack { - Circle().stroke(Color.primary.opacity(0.12), lineWidth: 3) - Circle() - .trim(from: 0, to: min(max(node.value ?? 0, 0), 1)) - .stroke(tint, style: StrokeStyle(lineWidth: 3, lineCap: .round)) - .rotationEffect(.degrees(-90)) - .widgetAccentable() - } - .frame(width: diameter, height: diameter) - } else { - HStack(spacing: 6) { - if let label = node.label { - Text(label).font(.caption).foregroundStyle(.secondary).lineLimit(1) - } - // The app's meter: a capsule track and fill, tinted the same. - GeometryReader { proxy in - ZStack(alignment: .leading) { - Capsule().fill(Color.primary.opacity(0.10)) - Capsule() - .fill(tint) - .frame(width: max(4, proxy.size.width * min(max(node.value ?? 0, 0), 1))) - .widgetAccentable() - } - } - .frame(height: 6) - } - } - } - - private var grid: some View { - let items = Array((node.items ?? node.children ?? []).prefix(rowLimit * max(node.columns ?? 3, 1))) - let columns = Array( - repeating: GridItem(.flexible(), spacing: node.spacing ?? 8), - count: max(node.columns ?? 3, 1) - ) - return LazyVGrid(columns: columns, alignment: .center, spacing: node.spacing ?? 8) { - ForEach(Array(items.enumerated()), id: \.offset) { _, item in - child(item).frame(maxWidth: .infinity) - } - } - } -} - -/// BarShelf's color names, as widgets draw them. Mirrors `nodeColor` and -/// `WidgetAppearance.accentColor` in the app. -enum WidgetPalette { - static func color(_ name: String?, accent: Color = .accentColor) -> Color? { - guard let raw = name?.trimmingCharacters(in: .whitespaces).lowercased(), !raw.isEmpty else { return nil } - switch raw { - case "primary": return .primary - case "secondary": return .secondary - case "tertiary": return .secondary.opacity(0.6) - case "accent": return accent - case "good", "green": return .green - case "warning", "orange": return .orange - case "danger", "red": return .red - case "neutral", "gray", "grey": return .gray - case "blue": return .blue - case "purple": return .purple - case "pink": return .pink - case "yellow": return .yellow - case "default": return nil - default: - return hex(raw) - } - } - - private static func hex(_ value: String) -> Color? { - let digits = value.hasPrefix("#") ? String(value.dropFirst()) : value - guard digits.count == 6, let number = UInt32(digits, radix: 16) else { return nil } - return Color( - red: Double((number >> 16) & 0xFF) / 255, - green: Double((number >> 8) & 0xFF) / 255, - blue: Double(number & 0xFF) / 255 - ) - } -} diff --git a/WidgetExtension/BarShelfWidgets/WidgetPalette.swift b/WidgetExtension/BarShelfWidgets/WidgetPalette.swift new file mode 100644 index 0000000..1b875a6 --- /dev/null +++ b/WidgetExtension/BarShelfWidgets/WidgetPalette.swift @@ -0,0 +1,36 @@ +import SwiftUI + +/// BarShelf's color names, as widgets draw them. Mirrors `nodeColor` and +/// `WidgetAppearance.accentColor` in the app. +enum WidgetPalette { + static func color(_ name: String?, accent: Color = .accentColor) -> Color? { + guard let raw = name?.trimmingCharacters(in: .whitespaces).lowercased(), !raw.isEmpty else { return nil } + switch raw { + case "primary": return .primary + case "secondary": return .secondary + case "tertiary": return .secondary.opacity(0.6) + case "accent": return accent + case "good", "green": return .green + case "warning", "orange": return .orange + case "danger", "red": return .red + case "neutral", "gray", "grey": return .gray + case "blue": return .blue + case "purple": return .purple + case "pink": return .pink + case "yellow": return .yellow + case "default": return nil + default: + return hex(raw) + } + } + + private static func hex(_ value: String) -> Color? { + let digits = value.hasPrefix("#") ? String(value.dropFirst()) : value + guard digits.count == 6, let number = UInt32(digits, radix: 16) else { return nil } + return Color( + red: Double((number >> 16) & 0xFF) / 255, + green: Double((number >> 8) & 0xFF) / 255, + blue: Double(number & 0xFF) / 255 + ) + } +}