mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 19:34:31 +00:00
- 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
368 lines
11 KiB
Bash
Executable File
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
|