summaryrefslogtreecommitdiff
path: root/HutchWidgetExtension
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-12 00:55:42 -0500
committerChristian Cleberg <[email protected]>2026-04-12 00:55:42 -0500
commit9050405b4f33b2b9b70458fcf3e43b80c940eef7 (patch)
tree983c3a07a560258059cfe421beb3ffcf3c18baf4 /HutchWidgetExtension
parent5f6d545eb3455a9e4da5a3cb4460811e3a48d939 (diff)
downloadhutch-9050405b4f33b2b9b70458fcf3e43b80c940eef7.tar.gz
hutch-9050405b4f33b2b9b70458fcf3e43b80c940eef7.tar.bz2
hutch-9050405b4f33b2b9b70458fcf3e43b80c940eef7.zip
add shortcuts widgets and power-user workflowsv2.15.0
Diffstat (limited to 'HutchWidgetExtension')
-rw-r--r--HutchWidgetExtension/NeedsAttentionWidget.swift79
-rw-r--r--HutchWidgetExtension/SystemStatusWidget.swift177
2 files changed, 223 insertions, 33 deletions
diff --git a/HutchWidgetExtension/NeedsAttentionWidget.swift b/HutchWidgetExtension/NeedsAttentionWidget.swift
index e212af6..574e9eb 100644
--- a/HutchWidgetExtension/NeedsAttentionWidget.swift
+++ b/HutchWidgetExtension/NeedsAttentionWidget.swift
@@ -71,7 +71,7 @@ private struct NeedsAttentionWidgetView: View {
fallbackState
}
}
- .widgetURL(URL(string: "hutch://home"))
+ .widgetURL(family == .systemSmall ? URL(string: "hutch://home") : nil)
.containerBackground(for: .widget) {
Color(.systemBackground)
}
@@ -101,52 +101,64 @@ private struct NeedsAttentionWidgetView: View {
private func mediumView(snapshot: NeedsAttentionSnapshot) -> some View {
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .top, spacing: 12) {
- metricColumn(
+ Link(destination: URL(string: "hutch://home")!) {
+ metricColumn(
+ count: snapshot.unreadInboxThreads,
+ label: "Unread",
+ systemImage: "tray",
+ tint: unreadTint(for: snapshot.unreadInboxThreads)
+ )
+ }
+ Link(destination: URL(string: "hutch://trackers")!) {
+ metricColumn(
+ count: snapshot.assignedOpenTickets,
+ label: "Assigned",
+ systemImage: "ticket"
+ )
+ }
+ Link(destination: URL(string: "hutch://builds")!) {
+ metricColumn(
+ count: snapshot.failedBuilds,
+ label: "Failed",
+ systemImage: "hammer",
+ tint: failedTint(for: snapshot.failedBuilds)
+ )
+ }
+ }
+ Spacer(minLength: 0)
+ }
+ .padding(.horizontal, 16)
+ .padding(.top, 18)
+ .padding(.bottom, 16)
+ }
+
+ private func largeView(snapshot: NeedsAttentionSnapshot) -> some View {
+ VStack(alignment: .leading, spacing: 18) {
+ Link(destination: URL(string: "hutch://home")!) {
+ metricRow(
count: snapshot.unreadInboxThreads,
- label: "Unread",
+ label: "Unread threads",
systemImage: "tray",
tint: unreadTint(for: snapshot.unreadInboxThreads)
)
- metricColumn(
+ }
+ Link(destination: URL(string: "hutch://trackers")!) {
+ metricRow(
count: snapshot.assignedOpenTickets,
- label: "Assigned",
+ label: "Assigned tickets",
systemImage: "ticket"
)
- metricColumn(
+ }
+ Link(destination: URL(string: "hutch://builds")!) {
+ metricRow(
count: snapshot.failedBuilds,
- label: "Failed",
+ label: "Failed builds",
systemImage: "hammer",
tint: failedTint(for: snapshot.failedBuilds)
)
}
Spacer(minLength: 0)
}
- .padding(.horizontal, 16)
- .padding(.top, 18)
- .padding(.bottom, 16)
- }
-
- private func largeView(snapshot: NeedsAttentionSnapshot) -> some View {
- VStack(alignment: .leading, spacing: 18) {
- metricRow(
- count: snapshot.unreadInboxThreads,
- label: "Unread threads",
- systemImage: "tray",
- tint: unreadTint(for: snapshot.unreadInboxThreads)
- )
- metricRow(
- count: snapshot.assignedOpenTickets,
- label: "Assigned tickets",
- systemImage: "ticket"
- )
- metricRow(
- count: snapshot.failedBuilds,
- label: "Failed builds",
- systemImage: "hammer",
- tint: failedTint(for: snapshot.failedBuilds)
- )
- Spacer(minLength: 0)
- }
.padding(20)
}
@@ -303,5 +315,6 @@ struct HutchWidgets: WidgetBundle {
var body: some Widget {
NeedsAttentionWidget()
ContributionGraphWidget()
+ SystemStatusWidget()
}
}
diff --git a/HutchWidgetExtension/SystemStatusWidget.swift b/HutchWidgetExtension/SystemStatusWidget.swift
new file mode 100644
index 0000000..4a3c1e5
--- /dev/null
+++ b/HutchWidgetExtension/SystemStatusWidget.swift
@@ -0,0 +1,177 @@
+import SwiftUI
+import WidgetKit
+
+struct SystemStatusEntry: TimelineEntry {
+ let date: Date
+ let snapshot: SystemStatusWidgetSnapshot?
+}
+
+struct SystemStatusTimelineProvider: TimelineProvider {
+ func placeholder(in _: Context) -> SystemStatusEntry {
+ SystemStatusEntry(
+ date: .now,
+ snapshot: SystemStatusWidgetSnapshot(
+ services: [
+ .init(id: "git", name: "git.sr.ht", status: "Operational", requiresAttention: false),
+ .init(id: "builds", name: "builds.sr.ht", status: "Operational", requiresAttention: false),
+ .init(id: "lists", name: "lists.sr.ht", status: "Operational", requiresAttention: false),
+ .init(id: "todo", name: "todo.sr.ht", status: "Operational", requiresAttention: false),
+ .init(id: "meta", name: "meta.sr.ht", status: "Operational", requiresAttention: false),
+ .init(id: "hg", name: "hg.sr.ht", status: "Operational", requiresAttention: false),
+ ],
+ hasDisruption: false,
+ overallStatusText: "All monitored services operational",
+ bannerSummary: "",
+ updatedAt: .now
+ )
+ )
+ }
+
+ func getSnapshot(in _: Context, completion: @escaping (SystemStatusEntry) -> Void) {
+ completion(
+ SystemStatusEntry(
+ date: .now,
+ snapshot: SystemStatusWidgetSnapshotStore.load()
+ )
+ )
+ }
+
+ func getTimeline(in _: Context, completion: @escaping (Timeline<SystemStatusEntry>) -> Void) {
+ let refreshDate = Calendar.current.date(byAdding: .minute, value: 15, to: .now) ?? .now.addingTimeInterval(900)
+ let entry = SystemStatusEntry(date: .now, snapshot: SystemStatusWidgetSnapshotStore.load())
+ completion(Timeline(entries: [entry], policy: .after(refreshDate)))
+ }
+}
+
+struct SystemStatusWidget: Widget {
+ static let kind = SystemStatusWidgetConfiguration.kind
+
+ var body: some WidgetConfiguration {
+ StaticConfiguration(kind: Self.kind, provider: SystemStatusTimelineProvider()) { entry in
+ SystemStatusWidgetView(entry: entry)
+ }
+ .configurationDisplayName("SourceHut Status")
+ .description("Current status of SourceHut services at a glance.")
+ .supportedFamilies([.systemSmall, .systemMedium])
+ }
+}
+
+private struct SystemStatusWidgetView: View {
+ let entry: SystemStatusEntry
+ @Environment(\.widgetFamily) private var family
+
+ var body: some View {
+ Group {
+ if let snapshot = entry.snapshot, !snapshot.services.isEmpty {
+ switch family {
+ case .systemMedium:
+ mediumView(snapshot: snapshot)
+ default:
+ smallView(snapshot: snapshot)
+ }
+ } else {
+ fallbackView
+ }
+ }
+ .widgetURL(URL(string: "hutch://status"))
+ .containerBackground(for: .widget) {
+ Color(.systemBackground)
+ }
+ }
+
+ private func smallView(snapshot: SystemStatusWidgetSnapshot) -> some View {
+ VStack(alignment: .leading, spacing: 8) {
+ HStack(spacing: 6) {
+ Image(systemName: snapshot.hasDisruption ? "exclamationmark.triangle.fill" : "checkmark.circle.fill")
+ .font(.title3.weight(.semibold))
+ .foregroundStyle(snapshot.hasDisruption ? .orange : .green)
+ Spacer()
+ }
+
+ Text(snapshot.hasDisruption ? snapshot.bannerSummary : "All Clear")
+ .font(.subheadline.weight(.semibold))
+ .lineLimit(2)
+
+ Spacer(minLength: 0)
+
+ if snapshot.hasDisruption {
+ let disrupted = snapshot.services.filter(\.requiresAttention)
+ Text(disrupted.map(\.name).joined(separator: ", "))
+ .font(.caption2)
+ .foregroundStyle(.secondary)
+ .lineLimit(2)
+ } else {
+ Text("\(snapshot.services.count) services monitored")
+ .font(.caption2)
+ .foregroundStyle(.secondary)
+ }
+ }
+ .padding()
+ }
+
+ private func mediumView(snapshot: SystemStatusWidgetSnapshot) -> some View {
+ VStack(alignment: .leading, spacing: 6) {
+ HStack(spacing: 8) {
+ Image(systemName: snapshot.hasDisruption ? "exclamationmark.triangle.fill" : "checkmark.circle.fill")
+ .font(.title3.weight(.semibold))
+ .foregroundStyle(snapshot.hasDisruption ? .orange : .green)
+
+ Text(snapshot.hasDisruption ? snapshot.bannerSummary : "All Services Operational")
+ .font(.subheadline.weight(.semibold))
+ .lineLimit(1)
+
+ Spacer()
+ }
+
+ Divider()
+
+ let columns = Array(snapshot.services.prefix(8))
+ let half = (columns.count + 1) / 2
+ let leftColumn = Array(columns.prefix(half))
+ let rightColumn = Array(columns.suffix(from: half))
+
+ HStack(alignment: .top, spacing: 16) {
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(leftColumn) { service in
+ serviceRow(service)
+ }
+ }
+ VStack(alignment: .leading, spacing: 4) {
+ ForEach(rightColumn) { service in
+ serviceRow(service)
+ }
+ }
+ Spacer(minLength: 0)
+ }
+
+ Spacer(minLength: 0)
+ }
+ .padding(.horizontal, 16)
+ .padding(.top, 14)
+ .padding(.bottom, 12)
+ }
+
+ private func serviceRow(_ service: SystemStatusWidgetSnapshot.ServiceEntry) -> some View {
+ HStack(spacing: 6) {
+ Circle()
+ .fill(service.requiresAttention ? Color.orange : Color.green)
+ .frame(width: 6, height: 6)
+ Text(service.name)
+ .font(.caption2)
+ .foregroundStyle(service.requiresAttention ? .primary : .secondary)
+ .lineLimit(1)
+ }
+ }
+
+ private var fallbackView: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ Spacer(minLength: 0)
+ Text("Open Hutch")
+ .font(.title3.weight(.semibold))
+ Text("Sign in to load system status.")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ .padding()
+ }
+}