8dab7236d4
Context: AV-Live needs a single-click way to boot its two heavy components (the SuperCollider sound engine and the openFrameworks oscope-of visualizer) together, with their logs aggregated in one place. Running them by hand from terminal during a live set is fragile. Approach: Swift Package Manager + a small build.sh that wraps the release binary into a real .app bundle (Info.plist with LSUIElement=true to keep it out of the Dock). The UI uses NSStatusItem + NSPopover so it works back to macOS 11 (Big Sur) — MenuBarExtra would require 13+ which the current target MBP can't run. Changes: - launcher/Package.swift : SwiftPM manifest, single executable target, macOS 11 minimum - AVLiveLauncherApp.swift : @main App + AppDelegate that owns the status item, popover and lazily-created log window - ProcessManager.swift : ObservableObject spawning sclang and oscope-of via Foundation Process, capturing stdout/stderr through Pipe + readabilityHandler, ring-buffered log array (2000 lines), termination handlers that flip @Published flags back. Paths persisted in UserDefaults with sane defaults - MenuBarContent.swift : popover view with two ProcessRow toggles, Logs / Paths / Quit, Settings sheet using NSOpenPanel - LogView.swift : separate window with filter, auto-scroll, color-coded source column, monospaced font. #available guard for textSelection - Info.plist : bundle id cc.saillant.AVLiveLauncher, GPL-3 copyright, LSUIElement true, min macOS 11 - build.sh : swift build -c release, copies binary + Info.plist into AVLiveLauncher.app, ad-hoc codesigns - .gitignore + README Impact: 'cd launcher && ./build.sh && open build/AVLiveLauncher.app' gives a working .app that brings up the AV stack from the menubar. Smoke-tested on arm64 — Intel MBP needs a local build or a future universal -arch x86_64 flag in build.sh.
81 lines
2.8 KiB
Swift
81 lines
2.8 KiB
Swift
import SwiftUI
|
|
|
|
struct LogView: View {
|
|
@ObservedObject var processManager: ProcessManager
|
|
@State private var filter: String = ""
|
|
@State private var autoScroll = true
|
|
|
|
private static let timeFormatter: DateFormatter = {
|
|
let f = DateFormatter()
|
|
f.dateFormat = "HH:mm:ss"
|
|
return f
|
|
}()
|
|
|
|
private var filtered: [LogLine] {
|
|
guard !filter.isEmpty else { return processManager.logs }
|
|
return processManager.logs.filter {
|
|
$0.text.localizedCaseInsensitiveContains(filter)
|
|
|| $0.source.localizedCaseInsensitiveContains(filter)
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
HStack {
|
|
TextField("Filter", text: $filter)
|
|
.textFieldStyle(.roundedBorder)
|
|
Toggle("Auto-scroll", isOn: $autoScroll)
|
|
Button("Clear") { processManager.clearLogs() }
|
|
}
|
|
.padding(8)
|
|
|
|
Divider()
|
|
|
|
ScrollViewReader { proxy in
|
|
ScrollView {
|
|
LazyVStack(alignment: .leading, spacing: 1) {
|
|
ForEach(filtered) { line in
|
|
HStack(alignment: .firstTextBaseline, spacing: 6) {
|
|
Text(Self.timeFormatter.string(from: line.timestamp))
|
|
.font(.system(.caption, design: .monospaced))
|
|
.foregroundColor(.secondary)
|
|
Text(line.source)
|
|
.font(.system(.caption, design: .monospaced))
|
|
.foregroundColor(color(for: line.source))
|
|
.frame(width: 70, alignment: .leading)
|
|
selectableText(line.text)
|
|
.font(.system(.caption, design: .monospaced))
|
|
}
|
|
.id(line.id)
|
|
.padding(.horizontal, 8)
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
.onChange(of: filtered.count) { _ in
|
|
if autoScroll, let last = filtered.last {
|
|
withAnimation(.linear(duration: 0.05)) {
|
|
proxy.scrollTo(last.id, anchor: .bottom)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func color(for source: String) -> Color {
|
|
if source.hasPrefix("sclang") { return .blue }
|
|
if source.hasPrefix("oscope") { return .purple }
|
|
return .secondary
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func selectableText(_ s: String) -> some View {
|
|
if #available(macOS 12.0, *) {
|
|
Text(s).textSelection(.enabled)
|
|
} else {
|
|
Text(s)
|
|
}
|
|
}
|
|
}
|