I’m always using scripting languages like python or powershell to glue together existing tools and make my computer do what I want. At a previous workplace I made a powershell script that proved so useful that I started receiving feature requests. One was a half-joking request for a 'full GUI'. I was surprised to find out that there are actually a number of ways to create GUI windows from powershell, so I used XAML to create a small GUI. Here’s a guide on that.

SwiftUI on MacOS for Nerds

A simple way to create a GUI directly via MacOS is actually with SwiftUI. You don’t need to create a whole project in Xcode, you don’t even need swift playground. A single file is all you need to make a window pop up.

#!/usr/bin/env swift

import Foundation
import SwiftUI

struct App: SwiftUI.App {
  var body: some Scene {
    WindowGroup {
      VStack {
      Image(systemName: "globe")
        .imageScale(.large)
        .foregroundColor(.accentColor)
      Text("Hello, world!")
      Button("Exit") { exit(0) }
      }
    }
  }
}

App.main()

Now I know many iOS devs hated SwiftUI when it first came out, complaining it’s too simple and limited. But that seems fine, since I’m my scripts are simple and limited too.

Once saved you can run it directly, or with swift if you can’t be bothered adding the executable permission.

./hello-world.swift
swift hello-word.swift

Even though it has to compile before running, being such a simple program it launches quickly anyway.

Connecting with the terminal world

Having swift’s Foundation and SwiftUI gives us a lot, but I’m usually automating things with bash/Terminal tools. George Lyon wrote a wonderful little demo that takes input from the pipe and can output to shell. I wanted an easy way to run commands and check their output, so I had claude create a wrapper function runShellScript for me. I also added some weird annoying lines to fix problems I had with the window appearing appearing behind the terminal.

Here’s a barebones template that serves as a great starting off point.

#!/usr/bin/env swift

import Foundation
import SwiftUI

func runShellScript(_ script: String) -> (output: String, error: String, exitCode: Int32) {
  let task = Process()
  let outputPipe = Pipe()
  let errorPipe = Pipe()

  task.executableURL = URL(fileURLWithPath: "/bin/zsh")
  task.arguments = ["-c", script]
  task.standardOutput = outputPipe
  task.standardError = errorPipe

  do {
    try task.run()
    task.waitUntilExit()

    let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
	let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()

	let output = (String(data: outputData, encoding: .utf8) ?? "").trimmingCharacters(in: .newlines)
	let error = (String(data: errorData, encoding: .utf8) ?? "").trimmingCharacters(in: .newlines)

	return (output: output, error: error, exitCode: task.terminationStatus)
  } catch {
	return (output: "", error: "Failed to run script: \(error.localizedDescription)", exitCode: -1)
  }
}

struct App: SwiftUI.App {
  @State private var commandOutput = "Output"
  @State private var command = "uname -v"
  var body: some Scene {
    WindowGroup {
    VStack {
        Text("Swift UI shell Demo").font(.title).padding(10)
        TextField("Command Input", text: $command)
        .onSubmit {
          commandOutput = runShellScript(command).output
        }
        .padding(10)
        //Button that also runs the command
        Button("Run") { commandOutput = runShellScript(command).output }.padding(10)
        Text(commandOutput).padding(10)
        Button("Exit") { exit(0) }.padding(10)
      }
      .padding(50)
      .onAppear {
      //Without this, the GUI window won't gain focus.
        DispatchQueue.main.async {
          if let window = NSApp.windows.first {
            window.level = .floating
		    window.makeKeyAndOrderFront(nil)
            NSApplication.shared.setActivationPolicy(.regular)
            NSApp.activate(ignoringOtherApps: true)
          }
        }
      }
    }
    //Window will have the title 'swift-frontend' by default.
    .windowStyle(HiddenTitleBarWindowStyle())
  }
}

App.main()

It features an input for a command and displays the output, showing the basics of how to run commands and use the output.

Compile if you’d like

Once you’re happy with your script, you can compile it so it’ll run more quickly and on other systems that don’t have Xcode or swift installed.

swiftc -O my-gui-tool.swift
./my-gui-tool

A real demo

Using this technique I made a simple GUI to adjust my webcam’s settings using uvc-util. The webcam’s official software is a monstrosity filled with AI and I only want two settings anyway. I can have the benefits of a tiny, fast cli app, with a slightly complicated GUI to make viewing and changing the settings easier.

Of course only now I notice the typos in the zoom slider 🙄

Alternatives

Now this wouldn’t be an honest guide if I didn’t mention some viable alternatives for quickly GUI-ing your scripts.

Platypus

An amazing utility that lets you point to a script and it’ll wrap an app bundle around it. Offers some basic GUI options. I like this as it makes it easy to chuck in any other binaries your script depends on, giving you a neat package to provide to non nerds.

Shortcuts

Apple’s built in Shortcuts.app has a 'shell script' function, and the ability to present a list of options and take user input.

Insert Python GUI Library

There are plenty of python GUI libraries out there. This adds a dependency, and even with uv your target audience might not find it easy to set up.