// // 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 } ZStack { Color.black .ignoresSafeArea() // ScrollView fills remaining height ScrollViewReader { proxy in ScrollView { LazyVStack(alignment: .leading, spacing: 0) { let lines = appState.output.components(separatedBy: .newlines) ForEach(lines.indices, id: \.self) { index in Text(lines[index]) .font(.system(.body, design: .monospaced)) .foregroundColor(.gray) .frame(maxWidth: .infinity, alignment: .leading) } Color.clear.frame(height: 1).id("bottom") } .padding() } .onChange(of: appState.output) { _, _ in trimLinesIfNeeded() withAnimation { proxy.scrollTo("bottom", anchor: .bottom) } } // 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() }