dsharhon
dsharhon

Reputation: 547

What is the minimally-viable GUI for command-line Swift scripts?

I need a little helper GUI for my Swift script on macOS. It just needs a text entry field and an OK button.

I don't want to go the whole bloated Xcode route just for this little popup. However, Apple's documentation fails me because keyboard input isn't captured by my NSWindow. Help!

Upvotes: 8

Views: 1574

Answers (2)

Stephane Piriou
Stephane Piriou

Reputation: 326

I wrote a little script which is able to show a window in swift. The goal was to show the output of several shell commands (brew update & brew upgrade & brew cleanup & brew doctor). These commands can take significant amount of time if you do not perform them everyday and I was tired of having to wait sometime 10 minutes just for the 2 first commands to complete.

I could have simply launched a cron job or used launchd with a shell script, but I wanted to be able to check the success or failure of the commands, especially the brew doctor, to know if I needed to perform some action to clean my homebrew install on my machine.

So I needed a window to show up with the error and standard output of the commands and more, I wanted to produce a binary of it.

After searching a bit on Google and Github, I found swift-sh, which allow to import Github repository (in a standardized way through Swift Package Manager) and use it in a swift script and compile it if needed ; and ShellOut, by the same guy, which allows to perform shell commands from a swift script and collects the output of the command in an swift object.

Basically, it should have been a little window with a textview in a scrollview, which showed the output of the shell commands while being able to scroll it.

Here the Script :


#!/usr/bin/swift sh


import AppKit
import Foundation
// importing ShellOut from GitHub repository
// The magic of swift-sh happens
import ShellOut // @JohnSundell

// Declare the Application context
let app = NSApplication.shared

// Create the delegate class responsible for the window and crontrol creation
class AppDelegate: NSObject, NSApplicationDelegate {
    var str: String? = ""

    // Construct the window
    let theWindow = NSWindow(contentRect: NSMakeRect(200, 200, 400, 200),
                          styleMask: [.titled, .closable, .miniaturizable, .resizable],
                          backing: .buffered,
                          defer: false,
                          screen: nil)

    var output: String? = ""

    // What happens once application context launched
    func applicationDidFinishLaunching(_ notification: Notification) {

        var str = ""

        // The shell commands and the collect of output
        do {
            str =  try shellOut(to: "brew", arguments: ["update"] )
            output = output! + str
        } catch {
            let error1 = error as! ShellOutError
            //print(error1.message)
            output = output! + error1.message
        }

        do {
            str = try shellOut(to: "brew", arguments: ["upgrade"] )
            output = output! + "\n" + str
            //print("step 2")
        } catch {
            let error2 = error as! ShellOutError
            //print(error2.message)
            output = output! + "\n" + error2.message
        }

        do {
            str = try shellOut(to: "brew", arguments: ["cleanup"] )
            output = output! + "\n" + str
            //print("step 3")
        } catch {
            let error3 = error as! ShellOutError
            //print(error3.message)
            output = output! + "\n" + error3.message
        }

        do {
            str = try shellOut(to: "brew", arguments: ["doctor"] )
            output = output! + "\n" + str
             //print("step 4")
        } catch {
            let error4 = error as! ShellOutError
            //print(error4.message)
            output = output! + "\n" + error4.message
        }

        // Controls placement and content goes here
        // ScrollView...
        var theScrollview = NSScrollView(frame: theWindow.contentView!.bounds)
        var contentSize = theScrollview.contentSize
        theScrollview.borderType = .noBorder
        theScrollview.hasVerticalScroller = true
        theScrollview.hasHorizontalScroller = false
        theScrollview.autoresizingMask = NSView.AutoresizingMask(rawValue: NSView.AutoresizingMask.width.rawValue | NSView.AutoresizingMask.height.rawValue | NSView.AutoresizingMask.minYMargin.rawValue | NSView.AutoresizingMask.minYMargin.rawValue)

        // TextView...
        var theTextView = NSTextView(frame: NSMakeRect(0, 0, contentSize.width, contentSize.height))
        theTextView.minSize = NSMakeSize(0.0, contentSize.height)
        theTextView.maxSize = NSMakeSize(CGFloat.greatestFiniteMagnitude, CGFloat.greatestFiniteMagnitude)
        theTextView.isVerticallyResizable = true
        theTextView.isHorizontallyResizable = false
        theTextView.autoresizingMask = NSView.AutoresizingMask(rawValue: NSView.AutoresizingMask.width.rawValue | NSView.AutoresizingMask.height.rawValue | NSView.AutoresizingMask.minYMargin.rawValue | NSView.AutoresizingMask.minYMargin.rawValue)
        theTextView.textContainer?.containerSize = NSMakeSize(contentSize.width, CGFloat.greatestFiniteMagnitude)
        theTextView.backgroundColor = .white
        theTextView.textContainer?.widthTracksTextView = true
        theTextView.textStorage?.append(NSAttributedString(string: output!))

        theScrollview.documentView = theTextView

        theWindow.contentView = theScrollview
        theWindow.makeKeyAndOrderFront(nil)
        theWindow.makeFirstResponder(theTextView)
    }

    // What happens when we click the close button of the window
    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
      return true;
    }
}

// Instantiation of the application delegate class
// and launching
let delegate = AppDelegate()
    app.delegate = delegate
    app.run()

Upvotes: 2

dsharhon
dsharhon

Reputation: 547

No thanks to Apple's documentation, I finally figured out the magical incantations 🧙‍♂️ needed to launch a simple AppKit/Cocoa GUI from a command-line Swift app that accepts keyboard input. Without Xcode!

This is also needed to accept text input in WKWebViews.

// main.swift // Dylan Sharhon // Tested on Catalina, Nov 2019
import AppKit // import Cocoa if you also need Foundation functionality

let app = NSApplication.shared
app.setActivationPolicy(.regular) // Magic to accept keyboard input and be docked!

let window = NSWindow.init(
  contentRect: NSRect(x: 300, y: 300, width: 200, height: 85),
  styleMask:   [
    NSWindow.StyleMask.titled     // Magic needed to accept keyboard input
  ],
  backing:     NSWindow.BackingStoreType.buffered,
  defer:       false
)
window.makeKeyAndOrderFront(nil)  // Magic needed to display the window

// Text input field
let text = NSTextField.init(string: "")
text.frame = NSRect(x: 10, y: 45, width: 180, height: 25)
window.contentView!.addSubview(text)

// Button
class Target {
  @objc func onClick () {         // Magic @objc needed for the button action
    print(text.stringValue)       // Goes to stdout
    exit(0)
  }
}
let target = Target()
let button = NSButton.init(
  title:  "OK",
  target: target,
  action: #selector(Target.onClick)
)
button.frame = NSRect(x:50, y:10, width:100, height:30)
window.contentView!.addSubview(button)

app.run()

To noobs like me: This is the entire app and you can run it with swift main.swift or compile it with swiftc main.swift and rename the resulting (merely 40 KB) executable to whatever you want to be in the menubar.

Upvotes: 11

Related Questions