// // ContentView.swift // MSG Desktop // // Created by Chad Nelson on 3/8/25. // import SwiftUI struct ViewHeightKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } } struct ContentView: View { @ObservedObject var appState = AppState.shared private let maxLines = 300 @State private var showCancelButton: Bool = false @State private var showReinitPopup = false @State private var showCancelPopup = false @State private var imageHeight: CGFloat = 0 var body: some View { GeometryReader { geometry in VStack(spacing: 10) { // App icon let appIcon = NSImage(named: NSImage.applicationIconName) Image(nsImage: appIcon ?? NSImage()) .resizable() .scaledToFit() .frame(maxHeight: 150) .background( GeometryReader { proxy in Color.clear .preference(key: ViewHeightKey.self, value: proxy.size.height) } ) .onPreferenceChange(ViewHeightKey.self) { height in self.imageHeight = height } // ScrollView fills remaining height ScrollViewReader { proxy in ScrollView { VStack(alignment: .leading) { TextEditor(text: $appState.output) .font(.system(.body, design: .monospaced)) .frame( width: geometry.size.width - 60, height: max(0, geometry.size.height - imageHeight - 120) // - rough height of button row and padding ) .padding() .disabled(true) Text("") .id("bottom") } } .onChange(of: appState.output) { _, _ in trimLinesIfNeeded() withAnimation { proxy.scrollTo("bottom", anchor: .bottom) } } } // Buttons HStack { Button("Init") { DispatchQueue.main.async { self.showCancelButton = true } runShellCommandAsync(command: "msg machine init", outputText: $appState.output, showButton: $showCancelButton) } Button("Reinit") { showReinitPopup.toggle() } .confirmationDialog("Are you sure?", isPresented: $showReinitPopup, titleVisibility: .visible) { Button("Confirm", role: .destructive) { self.showCancelButton = true runShellCommandAsync(command: "rm -rf ~/.guix && msg machine init", outputText: $appState.output, showButton: $showCancelButton) } Button("Cancel", role: .cancel) {} } Button("Start") { runShellCommandAsync(command: "msg machine start", outputText: $appState.output, showButton: nil) } if showCancelButton { Button("Cancel") { showCancelPopup.toggle() } .confirmationDialog("Are you sure?", isPresented: $showCancelPopup, titleVisibility: .visible) { Button("Confirm", role: .destructive) { runShellCommandAsync(command: "kill $(ps aux | grep '[g]uile' | awk '{print $2}')", outputText: $appState.output, showButton: nil) runShellCommandAsync(command: "kill $(ps aux | grep '[q]emu' | awk '{print $2}')", outputText: $appState.output, showButton: nil) DispatchQueue.main.async { self.showCancelButton = false } } Button("Cancel", role: .cancel) {} } } if !showCancelButton { Button("Stop") { runShellCommandAsync(command: "msg machine stop", outputText: $appState.output, showButton: nil) } } Button("Shell") { runShellCommandAsync( command: "osascript -e 'tell application \"Terminal\" to do script \"msg shell\"' -e 'tell application \"Terminal\" to activate'", outputText: $appState.output, showButton: nil ) } } } .padding() .frame(maxHeight: .infinity) .onAppear { // Silences error due to metallib on resizing of window let device = MTLCreateSystemDefaultDevice() _ = device?.makeDefaultLibrary() } } } private func trimLinesIfNeeded() { let lines = AppState.shared.output.split(separator: "\n") if lines.count > maxLines { let trimmedLines = lines.suffix(maxLines) AppState.shared.output = trimmedLines.joined(separator: "\n") } } } #Preview { ContentView() }