prole/prole-app/build.sh
chrisfu 8a6e8738db ProleStatus: app-window default, light splash restored, status bar controls; endpoint config via properties; build + docs
- Default to Application Window mode with full app menu; status bar overlay remains available
- Restore clickable startup Splash Tip (5s) with animated GIF and live counter; use light theme (Aqua)
- Status bar item: monospaced "P" icon; left click shows overlay (status mode) or main window (app mode)
- Overlay: add Maximize button (□) to toggle back to main window; keep click‑through elsewhere
- Global hotkey Cmd+Opt+Shift+P toggles modes; Prole menu item mirrors the same toggle and updates title dynamically
- Application menu (Prole): Show Status Bar / Show Main Window (contextual), Refresh Now, Quit
- Application Window layout: non‑scrolling, vertical 1‑line rows (svc, k3s aggregate, local) with top‑right timestamp; bottom‑left controls (⟳ Refresh, _ Minimize)
- Parameterize endpoints via `prole.properties` (bundled + user override). Replace legacy raspberry with retropie defaults
- ServiceChecker + UI read endpoints from Config loader; tooltips/labels reflect configured hosts/ports
- Build script: generate `Info.plist` with `LSUIElement=false`; bundle resources (`prole-type.gif`, `prole.properties`); ad‑hoc codesign. Universal build supported
- Documentation: rewrite README with technical build/run/config details and operational posture

Files:
- proleStatus/Sources/: AppDelegate.swift, OverlayWindow.swift, StatusView.swift, StatusItemController.swift,
  SplashTipWindowController.swift, MainWindowController.swift, AppStatusView.swift, ServiceChecker.swift, Config.swift
- proleStatus/build.sh
- proleStatus/prole.properties
- proleStatus/README.md

Notes:
- Build verified via `./build.sh build` (arm64). App starts in App Window mode with light theme; menu + hotkey + overlay toggle operate as intended
2025-12-01 21:08:30 -08:00

368 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Prole build script — builds a macOS .app bundle without launching Xcode
# Requirements: Xcode Command Line Tools (swiftc, codesign, plutil, lipo)
APP_NAME="Prole"
BUNDLE_ID="org.prole.${APP_NAME}"
MIN_MACOS="12.0"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT_DIR="$SCRIPT_DIR"
SRC_DIR="$ROOT_DIR/Sources"
BUILD_DIR="$ROOT_DIR/.build-cli"
DIST_DIR="$ROOT_DIR/dist"
APP_DIR="$DIST_DIR/${APP_NAME}.app"
CONTENTS_DIR="$APP_DIR/Contents"
MACOS_DIR="$CONTENTS_DIR/MacOS"
RESOURCES_DIR="$CONTENTS_DIR/Resources"
INFO_PLIST="$CONTENTS_DIR/Info.plist"
PARENT_DIR="$(cd "$ROOT_DIR/.." && pwd)"
ARCH_CURRENT="$(uname -m)" # arm64 or x86_64
usage() {
cat <<EOF
Usage: $(basename "$0") [command] [options]
Commands:
build Build ${APP_NAME}.app for the current architecture (default)
build-universal Build a universal (arm64+x86_64) ${APP_NAME}.app
clean Remove build artifacts
run Build (if needed) and run the app
debug Build (if needed) and run in foreground with verbose logs
package Zip the built app into dist/${APP_NAME}.zip
Options (for build):
--arch <arch> Build for a specific arch (arm64 or x86_64). Defaults to host arch.
Examples:
./build.sh build
./build.sh build --arch arm64
./build.sh build-universal
./build.sh run
./build.sh package
EOF
}
ensure_dirs() {
mkdir -p "$BUILD_DIR" "$DIST_DIR" "$MACOS_DIR" "$RESOURCES_DIR"
}
# Read a key from prole.properties (very simple parser)
prop_get() {
local key="$1"
local file="$ROOT_DIR/prole.properties"
if [[ -f "$file" ]]; then
local line
line=$(grep -E "^${key}=" "$file" | tail -n1 || true)
if [[ -n "$line" ]]; then
echo "${line#*=}"
return 0
fi
fi
return 1
}
gen_statusbar_icon_png() {
# Generates a small monochrome template PNG for the status bar (18x18)
local out_png="$RESOURCES_DIR/statusIcon.png"
# Render a simple 'P' using AppKit to avoid extra deps
/usr/bin/env xcrun swift -F /System/Library/PrivateFrameworks - <<'SWIFT' "$out_png"
import AppKit
import Foundation
let args = CommandLine.arguments
guard args.count > 1 else { exit(2) }
let path = args[1]
let size = NSSize(width: 18, height: 18)
let img = NSImage(size: size)
img.lockFocus()
NSColor.clear.setFill()
NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill()
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = .center
let attrs: [NSAttributedString.Key: Any] = [
.font: NSFont.monospacedSystemFont(ofSize: 14, weight: .bold),
.foregroundColor: NSColor.labelColor,
.paragraphStyle: paragraph
]
let s = NSString(string: "P")
let rect = NSRect(x: 0, y: -2, width: size.width, height: size.height)
s.draw(in: rect, withAttributes: attrs)
img.unlockFocus()
guard let tiff = img.tiffRepresentation,
let rep = NSBitmapImageRep(data: tiff),
let png = rep.representation(using: .png, properties: [:]) else {
exit(3)
}
try! png.write(to: URL(fileURLWithPath: path))
SWIFT
# Mark as template via extended attribute for system tinting (optional)
}
gen_app_icns() {
# Create an .icns for the app. Prefer a configured source image (ui.icon),
# otherwise fall back to generating a bold 'P'.
local icon_name="${APP_NAME}"
local iconset_dir="$BUILD_DIR/${icon_name}.iconset"
rm -rf "$iconset_dir"
mkdir -p "$iconset_dir"
local base_png="$BUILD_DIR/${icon_name}_1024.png"
# Try configured ui.icon relative to repo root
local ui_icon
ui_icon="$(prop_get ui.icon || true)"
if [[ -n "$ui_icon" && -f "$PARENT_DIR/$ui_icon" ]]; then
# Use the provided icon image as base; if not already 1024x1024, sips will resize for each size
cp "$PARENT_DIR/$ui_icon" "$base_png"
else
# Generate a 1024 base PNG using AppKit so we don't depend on ImageMagick/Pillow
/usr/bin/env xcrun swift -F /System/Library/PrivateFrameworks - <<'SWIFT' "$base_png"
import AppKit
import Foundation
let args = CommandLine.arguments
guard args.count > 1 else { exit(2) }
let outPath = args[1]
let size = NSSize(width: 1024, height: 1024)
let img = NSImage(size: size)
img.lockFocus()
NSColor.clear.setFill()
NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill()
// Draw a rounded rect background to make it look like an app icon
let bgRect = NSRect(x: 0, y: 0, width: 1024, height: 1024)
let radius: CGFloat = 220
let roundRectPath = NSBezierPath(roundedRect: bgRect, xRadius: radius, yRadius: radius)
NSColor.windowBackgroundColor.setFill()
roundRectPath.fill()
// Draw the letter 'P' centered
let paragraph = NSMutableParagraphStyle()
paragraph.alignment = .center
let attrs: [NSAttributedString.Key: Any] = [
.font: NSFont.monospacedSystemFont(ofSize: 720, weight: .black),
.foregroundColor: NSColor.labelColor,
.paragraphStyle: paragraph
]
let s = NSString(string: "P")
let rect = NSRect(x: 0, y: 70, width: 1024, height: 820)
s.draw(in: rect, withAttributes: attrs)
img.unlockFocus()
guard let tiff = img.tiffRepresentation,
let rep = NSBitmapImageRep(data: tiff),
let png = rep.representation(using: .png, properties: [:]) else {
exit(3)
}
try! png.write(to: URL(fileURLWithPath: outPath))
SWIFT
fi
# Create all iconset sizes from the base image
for s in 16 32 128 256 512; do
/usr/bin/sips -z "$s" "$s" "$base_png" --out "$iconset_dir/icon_${s}x${s}.png" >/dev/null
/usr/bin/sips -z "$((s*2))" "$((s*2))" "$base_png" --out "$iconset_dir/icon_${s}x${s}@2x.png" >/dev/null
done
# Build icns
/usr/bin/iconutil -c icns "$iconset_dir" -o "$RESOURCES_DIR/${icon_name}.icns"
}
gen_plist() {
cat > "$INFO_PLIST" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key><string>en</string>
<key>CFBundleExecutable</key><string>${APP_NAME}</string>
<key>CFBundleIdentifier</key><string>${BUNDLE_ID}</string>
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
<key>CFBundleName</key><string>${APP_NAME}</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>1.0</string>
<key>CFBundleVersion</key><string>1</string>
<key>LSMinimumSystemVersion</key><string>${MIN_MACOS}</string>
<key>NSHighResolutionCapable</key><true/>
<key>NSPrincipalClass</key><string>NSApplication</string>
<key>LSApplicationCategoryType</key><string>public.app-category.developer-tools</string>
<!-- UIElement must be false so the app can present a normal window and app menu when in Application Window mode. -->
<key>LSUIElement</key><false/>
<key>CFBundleIconFile</key><string>${APP_NAME}.icns</string>
</dict>
</plist>
PLIST
}
copy_extra_resources() {
# Copy animated tip GIF into app resources if it exists in repo
local gif_src="$PARENT_DIR/www/images/prole-type.gif"
if [[ -f "$gif_src" ]]; then
mkdir -p "$RESOURCES_DIR"
cp "$gif_src" "$RESOURCES_DIR/prole-type.gif"
echo "[resources] Copied prole-type.gif into Resources"
else
echo "[resources] prole-type.gif not found at $gif_src (skipping)"
fi
# Copy configured background image (ui.background) into Resources, keeping basename
local ui_bg
ui_bg="$(prop_get ui.background || true)"
if [[ -n "$ui_bg" && -f "$PARENT_DIR/$ui_bg" ]]; then
mkdir -p "$RESOURCES_DIR"
local base
base="$(basename "$ui_bg")"
cp "$PARENT_DIR/$ui_bg" "$RESOURCES_DIR/$base"
echo "[resources] Copied background image ($base) into Resources"
else
echo "[resources] ui.background not set or file missing (skipping)"
fi
# Copy default properties file if present
local props_src="$ROOT_DIR/prole.properties"
if [[ -f "$props_src" ]]; then
cp "$props_src" "$RESOURCES_DIR/prole.properties"
echo "[resources] Copied prole.properties into Resources"
else
echo "[resources] prole.properties not found at $props_src (skipping)"
fi
}
swiftc_compile() {
local arch="$1"; shift
local out="$1"; shift
# Map arch to -target
local target="${arch}-apple-macosx${MIN_MACOS}"
echo "[swiftc] Building for ${arch} -> ${out}"
# shellcheck disable=SC2046
xcrun swiftc \
-target "$target" \
-O \
-sdk "$(xcrun --show-sdk-path --sdk macosx)" \
-framework AppKit \
-framework Carbon \
-framework Network \
$(/usr/bin/find "$SRC_DIR" -name "*.swift" | sort) \
-o "$out"
}
codesign_app() {
echo "[codesign] Ad-hoc signing ${APP_DIR}"
xcrun codesign --force --deep -s - "$APP_DIR"
}
build_one_arch() {
local arch="$1"
ensure_dirs
gen_plist
gen_app_icns
gen_statusbar_icon_png
copy_extra_resources
local bin="$BUILD_DIR/${APP_NAME}-${arch}"
swiftc_compile "$arch" "$bin"
mkdir -p "$MACOS_DIR"
cp "$bin" "$MACOS_DIR/${APP_NAME}"
codesign_app
echo "Built: $APP_DIR"
}
build_universal() {
ensure_dirs
gen_plist
local bin_arm64="$BUILD_DIR/${APP_NAME}-arm64"
local bin_x86="$BUILD_DIR/${APP_NAME}-x86_64"
swiftc_compile arm64 "$bin_arm64"
swiftc_compile x86_64 "$bin_x86"
mkdir -p "$MACOS_DIR"
xcrun lipo -create -output "$MACOS_DIR/${APP_NAME}" "$bin_arm64" "$bin_x86"
codesign_app
echo "Built universal: $APP_DIR"
}
clean() {
rm -rf "$BUILD_DIR" "$DIST_DIR"
echo "Cleaned build artifacts."
}
run_app() {
if [ ! -d "$APP_DIR" ]; then
"$0" build
fi
echo "[run] Launching ${APP_NAME}.app"
# Launch app in background without activating it; return immediately.
# Then confirm it is running and print its PID.
open -gn "$APP_DIR"
# Wait briefly for the app to start and obtain PID
for i in {1..20}; do
PID=$(pgrep -x "$APP_NAME" || true)
if [ -n "${PID:-}" ]; then
echo "[run] ${APP_NAME} is running (pid: $PID)."
return 0
fi
sleep 0.1
done
echo "[run] Warning: could not confirm ${APP_NAME} is running. Check Console/Activity Monitor." >&2
}
package_app() {
if [ ! -d "$APP_DIR" ]; then
"$0" build
fi
local zip="$DIST_DIR/${APP_NAME}.zip"
(cd "$DIST_DIR" && /usr/bin/zip -qry "$(basename "$zip")" "$(basename "$APP_DIR")")
echo "Packaged: $zip"
}
cmd="${1:-build}"
shift || true
case "$cmd" in
-h|--help|help)
usage ;;
clean)
clean ;;
build)
arch="${ARCH_CURRENT}"
while [ $# -gt 0 ]; do
case "$1" in
--arch) arch="$2"; shift 2 ;;
*) echo "Unknown option: $1"; usage; exit 1 ;;
esac
done
build_one_arch "$arch" ;;
build-universal)
build_universal ;;
run)
run_app ;;
debug)
# Build if needed, then run the binary directly in the foreground
if [ ! -d "$APP_DIR" ]; then
"$0" build
fi
BIN="$MACOS_DIR/${APP_NAME}"
if [ ! -x "$BIN" ]; then
echo "[debug] error: binary not found at $BIN" >&2
exit 1
fi
echo "[debug] Running ${APP_NAME} in foreground with verbose logs"
echo "[debug] Press Ctrl+C to terminate. To quit from UI, use the context menu or Cmd+Q."
export PROLESTATUS_DEBUG=1
"$BIN"
status=$?
echo "[debug] ${APP_NAME} exited with status ${status}"
exit $status
;;
package)
package_app ;;
*)
echo "Unknown command: $cmd"; usage; exit 1 ;;
esac