Files
ProdDash/viewer/macos/Sources/AppDelegate.swift
T
Waters ChurchandClaude Opus 5.5 8d8c0ee0c3 ProdDash Viewer 1.0.0: a Mac app that only shows ProdDash, with a tab per installation
A new Apple-silicon app in viewer/macos (Swift, no dependencies, ~1 MB): one
window showing a ProdDash dashboard in WebKit, and nothing else. It runs no
server; it points at any ProdDash on the network.

- Connections (name + address) in Settings; the add/edit sheet asks
  /api/modules whether ProdDash answers as you type. No port means 24500.
- Two or more connections bring a slim tab bar of their names. In a window
  it floats over the dashboard and comes out while the pointer is on the
  title bar; it is not part of the title bar, because anything there makes
  WebKit push the page down. In full screen it is a titlebar accessory that
  comes down with the menu bar, and it flashes up after each switch.
  ⌃Tab / ⌃⇧Tab and ⌘1–9 switch from the keyboard.
- Full screen, reopening in full screen if it quit there; window size and
  the selected tab are kept too. The dashboard's own full-screen button
  works as well.
- Menus: View (Back, Forward, Reload Page, Enter Full Screen), Settings,
  Edit (copy/paste in the page's text fields), Window, Help (a guide that
  ships in the app).
- Every installation keeps its own live web view. One that can't be
  reached says why and retries at 2, 4, 8, then every 15 s; a redirect off
  the installation is reported, not handed to the browser. JS alert,
  confirm and prompt show as sheets; links to other sites open in the
  browser.
- General settings: open at login (SMAppService), keep the display awake
  while a dashboard is on screen (default on).

build.sh signs with the Developer ID when one is installed and makes a
notarised DMG with --notarize, as the launcher's does. It needs Xcode: with
only the command line tools the macOS 27 SDK can't expand SwiftUI's @State.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:05:14 -04:00

111 lines
5.4 KiB
Swift

import AppKit
/**
ProdDash Viewer: one window showing a ProdDash dashboard, a tab per
installation when there's more than one, and Settings to say which.
The menus are deliberately short — Back, Forward, Reload Page and Settings,
plus what every Mac app has: the app menu, Edit (without which Copy and
Paste do nothing in the dashboard's text fields), Window and Help.
*/
final class AppDelegate: NSObject, NSApplicationDelegate {
static var version: String {
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"
}
private var viewer: ViewerWindowController!
private lazy var settingsWindow = SettingsWindowController()
private lazy var helpWindow = HelpWindowController()
func applicationWillFinishLaunching(_ notification: Notification) {
NSWindow.allowsAutomaticWindowTabbing = false
NSApp.mainMenu = makeMainMenu()
}
func applicationDidFinishLaunching(_ notification: Notification) {
viewer = ViewerWindowController()
viewer.onAddConnection = { [weak self] in self?.settingsWindow.show(addingConnection: true) }
viewer.showWindow(nil)
if ViewerSettings.shared.reopenInFullScreen {
DispatchQueue.main.async { self.viewer.window?.toggleFullScreen(nil) }
}
}
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
if !flag { viewer.showWindow(nil) }
return true
}
func applicationWillTerminate(_ notification: Notification) {
viewer?.rememberFullScreen()
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { true }
@objc func showSettings(_ sender: Any?) { settingsWindow.show() }
@objc func showViewerHelp(_ sender: Any?) { helpWindow.show() }
// MARK: - Menus
private func makeMainMenu() -> NSMenu {
let name = "ProdDash Viewer"
let main = NSMenu()
let app = submenu(of: main, title: name)
app.addItem(withTitle: "About \(name)",
action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
app.addItem(.separator())
app.addItem(withTitle: "Settings…", action: #selector(showSettings(_:)), keyEquivalent: ",")
app.addItem(.separator())
let services = submenu(of: app, title: "Services")
NSApp.servicesMenu = services
app.addItem(.separator())
app.addItem(withTitle: "Hide \(name)", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h")
app.addItem(withTitle: "Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)),
keyEquivalent: "h").keyEquivalentModifierMask = [.command, .option]
app.addItem(withTitle: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: "")
app.addItem(.separator())
app.addItem(withTitle: "Quit \(name)", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
let edit = submenu(of: main, title: "Edit")
edit.addItem(withTitle: "Undo", action: Selector(("undo:")), keyEquivalent: "z")
edit.addItem(withTitle: "Redo", action: Selector(("redo:")), keyEquivalent: "Z")
edit.addItem(.separator())
edit.addItem(withTitle: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")
edit.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
edit.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
edit.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
let view = submenu(of: main, title: "View")
view.addItem(withTitle: "Back", action: #selector(ViewerWindowController.goBackInPage(_:)), keyEquivalent: "[")
view.addItem(withTitle: "Forward", action: #selector(ViewerWindowController.goForwardInPage(_:)), keyEquivalent: "]")
view.addItem(withTitle: "Reload Page", action: #selector(ViewerWindowController.reloadPage(_:)), keyEquivalent: "r")
view.addItem(.separator())
// AppKit retitles this "Exit Full Screen" while the window is in full screen.
view.addItem(withTitle: "Enter Full Screen", action: #selector(NSWindow.toggleFullScreen(_:)),
keyEquivalent: "f").keyEquivalentModifierMask = [.command, .control]
let window = submenu(of: main, title: "Window")
window.addItem(withTitle: "Close", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w")
window.addItem(withTitle: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m")
window.addItem(withTitle: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: "")
window.addItem(.separator())
window.addItem(withTitle: "Bring All to Front", action: #selector(NSApplication.arrangeInFront(_:)), keyEquivalent: "")
NSApp.windowsMenu = window
let help = submenu(of: main, title: "Help")
help.addItem(withTitle: "\(name) Help", action: #selector(showViewerHelp(_:)), keyEquivalent: "?")
NSApp.helpMenu = help
return main
}
@discardableResult
private func submenu(of parent: NSMenu, title: String) -> NSMenu {
let menu = NSMenu(title: title)
parent.addItem(withTitle: title, action: nil, keyEquivalent: "").submenu = menu
return menu
}
}