#!/bin/bash
# =============================================================================
# Rec — Builder & Installer
# This script downloads the source (if needed), builds it locally on your Mac,
# and generates an installable application bundle.
# =============================================================================
APP_NAME="Rec"
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
# =============================================================================
# Repository Configuration (Update these for your internal GitHub instance)
# =============================================================================
REPO_BASE_URL="https://github.com/arunofhyd/Rec"
REPO_RAW_URL="https://raw.githubusercontent.com/arunofhyd/Rec/main"
APP_VERSION=""
# Try local version.json first, then fetch from GitHub
if [ -f version.json ]; then
APP_VERSION=$(python3 -c "import json; print(json.load(open('version.json'))['version'])" 2>/dev/null || true)
elif [ -f "$SCRIPT_DIR/version.json" ]; then
APP_VERSION=$(python3 -c "import json; print(json.load(open('$SCRIPT_DIR/version.json'))['version'])" 2>/dev/null || true)
fi
# If no local version.json, fetch it from the repo
if [ -z "$APP_VERSION" ]; then
APP_VERSION=$(curl -fsSL "${REPO_RAW_URL}/version.json" 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin)['version'])" 2>/dev/null || true)
fi
if [ -z "$APP_VERSION" ]; then
APP_VERSION=$(grep -m1 'appVersion' main.swift 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true)
fi
if [ -z "$APP_VERSION" ]; then APP_VERSION="1.5.2"; fi
# ---- Terminal styling ------------------------------------
BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m'
BLUE='\033[38;5;39m'; GREEN='\033[38;5;35m'; YELLOW='\033[38;5;220m'; RED='\033[38;5;196m'; GREY='\033[38;5;245m'
line() { printf '%b\n' "${DIM}────────────────────────────────────────────────────────────${NC}"; }
step() { printf '%b▸%b %b%s%b\n' "${RED}${BOLD}" "${NC}" "${BOLD}" "$1" "${NC}"; }
ok() { printf ' %b✓%b %s\n' "${GREEN}" "${NC}" "$1"; }
warn() { printf ' %b!%b %s\n' "${YELLOW}" "${NC}" "$1"; }
fail() { printf ' %b✗ %s%b\n' "${RED}" "$1" "${NC}"; }
clear
printf '\n'
printf '%b\n' "${RED}${BOLD} Rec${NC}"
printf '%b\n\n' "${GREY} Simple, clean, native-feeling screen recording.${NC}"
line
printf '\n'
# ---- Step 1: Command Line Tools (compiler) -------------------------------
step "Checking for build tools…"
if ! xcode-select -p >/dev/null 2>&1; then
warn "Apple's Command Line Tools are needed to build the app."
printf '%b\n\n' " ${GREY}A small official Apple installer will pop up. Please click ${BOLD}Install${NC}${GREY} and wait for it to finish.${NC}"
xcode-select --install >/dev/null 2>&1
printf '%b' " ${YELLOW}When the installation is COMPLETE, press [Enter] here to continue…${NC}"
read -r
if ! xcode-select -p >/dev/null 2>&1; then
fail "Build tools still not found."
printf '%b\n\n' " ${GREY}Please finish the Apple installer, then run this file again.${NC}"
exit 1
fi
fi
ok "Build tools ready."
printf '\n'
# ---- Step 2: Workspace ----------------------------------------------------
step "Preparing a clean workspace…"
BUILD_DIR="$(mktemp -d)"
trap 'rm -rf "$BUILD_DIR"' EXIT
cd "$BUILD_DIR" || { fail "Could not create workspace."; exit 1; }
cat > Entitlements.plist <<'ENTEOF'
com.apple.security.device.audio-input
com.apple.security.device.camera
com.apple.security.cs.disable-library-validation
ENTEOF
ok "Workspace ready."
printf '\n'
# ---- Step 3: Download the source -----------------------------------------
step "Downloading Rec source…"
if ! curl -fsSL "${REPO_RAW_URL}/main.swift" -o main.swift 2>/dev/null; then
if [ -f "$OLDPWD/main.swift" ]; then
cp "$OLDPWD/main.swift" .
else
fail "Could not download the app source."
printf '%b\n\n' " ${GREY}Check your internet connection and try again.${NC}"
exit 1
fi
fi
ok "Source ready."
printf '\n'
# ---- Step 3: Generate the app icon ---------------------------------------
step "Creating the app icon…"
cat > MakeIcon.swift <<'ICONEOF'
import Cocoa
let px: CGFloat = 1024
let img = NSImage(size: NSSize(width: px, height: px))
img.lockFocus()
guard let ctx = NSGraphicsContext.current?.cgContext else { fatalError() }
let padding: CGFloat = 100
let size = px - 2 * padding
ctx.translateBy(x: padding, y: padding)
let scale = size / px
ctx.scaleBy(x: scale, y: scale)
// Dark rounded rect background
let appScale = px / 120.0
ctx.scaleBy(x: appScale, y: appScale)
let bgPath = NSBezierPath(roundedRect: NSRect(x: 0, y: 0, width: 120, height: 120), xRadius: 27, yRadius: 27)
let topColor = NSColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 1.0)
let botColor = NSColor(red: 26/255.0, green: 26/255.0, blue: 26/255.0, alpha: 1.0)
bgPath.addClip()
NSGradient(starting: topColor, ending: botColor)?.draw(in: bgPath, angle: -90)
// Shiny edges
let shineRect = CGRect(x: 1, y: 1, width: 118, height: 118)
let cgPath = CGPath(roundedRect: shineRect, cornerWidth: 26, cornerHeight: 26, transform: nil)
ctx.saveGState()
ctx.addPath(cgPath)
ctx.setLineWidth(2)
ctx.replacePathWithStrokedPath()
ctx.clip()
let shineGradient = NSGradient(colors: [
NSColor.white.withAlphaComponent(0.6),
NSColor.white.withAlphaComponent(0.0),
NSColor.white.withAlphaComponent(0.0),
NSColor.white.withAlphaComponent(0.6)
], atLocations: [0.0, 0.3, 0.7, 1.0], colorSpace: .deviceRGB)
shineGradient?.draw(in: NSRect(x: 0, y: 0, width: 120, height: 120), angle: -45)
ctx.restoreGState()
// Record Circle Outer
let outerPath = NSBezierPath(ovalIn: NSRect(x: 30, y: 30, width: 60, height: 60))
outerPath.lineWidth = 6
NSColor.white.setStroke()
outerPath.stroke()
// Record Circle Inner
let innerPath = NSBezierPath(ovalIn: NSRect(x: 40, y: 40, width: 40, height: 40))
NSColor(red: 1.0, green: 59/255.0, blue: 48/255.0, alpha: 1.0).setFill()
innerPath.fill()
img.unlockFocus()
if let tiff = img.tiffRepresentation,
let rep = NSBitmapImageRep(data: tiff),
let png = rep.representation(using: .png, properties: [:]) {
try? png.write(to: URL(fileURLWithPath: "AppIcon.png"))
}
ICONEOF
swiftc MakeIcon.swift -o MakeIcon >/dev/null 2>&1 && ./MakeIcon
if [ -f AppIcon.png ]; then
mkdir -p AppIcon.iconset
for pair in "16 16x16" "32 16x16@2x" "32 32x32" "64 32x32@2x" "128 128x128" "256 128x128@2x" "256 256x256" "512 256x256@2x" "512 512x512" "1024 512x512@2x"; do
set -- $pair
sips -z "$1" "$1" AppIcon.png --out "AppIcon.iconset/icon_${2}.png" >/dev/null 2>&1
done
iconutil -c icns AppIcon.iconset >/dev/null 2>&1
ok "Icon created."
else
warn "Icon generation skipped (app will still work)."
fi
printf "\n"
# ---- Step 4: Assemble the app bundle -------------------------------------
step "Building ${APP_NAME}.app…"
APP="$APP_NAME.app"
rm -rf "$APP"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
[ -f AppIcon.icns ] && cp AppIcon.icns "$APP/Contents/Resources/"
cat > "$APP/Contents/Info.plist" <
CFBundleName$APP_NAME
CFBundleIdentifiercom.aoh.rec
CFBundleVersion$APP_VERSION
CFBundleShortVersionString$APP_VERSION
CFBundleExecutable$APP_NAME
CFBundleIconFileAppIcon
CFBundlePackageTypeAPPL
LSMinimumSystemVersion13.0
NSHighResolutionCapable
LSUIElement
NSScreenCaptureUsageDescription
This app requires screen capture access to record your screen.
NSMicrophoneUsageDescription
This app needs access to the microphone and internal audio to record sound.
NSCameraUsageDescription
This app needs camera access to show a camera overlay during screen recording.
CFBundleDocumentTypes
CFBundleTypeName
Video
CFBundleTypeRole
Editor
LSHandlerRank
Alternate
LSItemContentTypes
public.movie
public.video
com.apple.quicktime-movie
public.mpeg-4
public.avi
PLIST
BUILD_OK=false
if swiftc -O -target arm64-apple-macos13.0 -o "${APP_NAME}_arm64" main.swift -framework Cocoa -framework ScreenCaptureKit -framework AVFoundation -framework VideoToolbox -framework SwiftUI -framework QuartzCore 2>build_errors.txt && \
swiftc -O -target x86_64-apple-macos13.0 -o "${APP_NAME}_x86_64" main.swift -framework Cocoa -framework ScreenCaptureKit -framework AVFoundation -framework VideoToolbox -framework SwiftUI -framework QuartzCore 2>>build_errors.txt; then
if lipo -create -output "$APP/Contents/MacOS/$APP_NAME" "${APP_NAME}_arm64" "${APP_NAME}_x86_64" 2>>build_errors.txt; then
rm -f "${APP_NAME}_arm64" "${APP_NAME}_x86_64"
BUILD_OK=true
fi
fi
if [ "$BUILD_OK" != "true" ]; then
warn "Local compilation failed."
printf '%b\n' " ${GREY}Falling back to downloading pre-built release...${NC}"
ZIP_URL="${REPO_BASE_URL}/releases/download/v${APP_VERSION}/Rec.zip"
if ! curl -fsSL "$ZIP_URL" -o Rec.zip; then
ZIP_URL="${REPO_BASE_URL}/releases/latest/download/Rec.zip"
curl -fsSL "$ZIP_URL" -o Rec.zip || { fail "Download failed."; exit 1; }
fi
rm -rf "$APP"
unzip -o -q Rec.zip
ok "Pre-built app downloaded successfully."
else
chmod +x "$APP/Contents/MacOS/$APP_NAME"
ENTITLEMENTS_FLAG=""
[ -f Entitlements.plist ] && ENTITLEMENTS_FLAG="--entitlements Entitlements.plist"
codesign --force --options runtime $ENTITLEMENTS_FLAG --sign - --requirements '=designated => identifier "com.aoh.rec"' "$APP" >/dev/null 2>&1 || true
ok "App built."
fi
printf '\n'
if [ "$CI" = "true" ]; then
mkdir -p "$OLDPWD/Build"
cp -R "$APP" "$OLDPWD/Build/"
ok "CI mode detected. App copied to Build/$APP"
exit 0
fi
# ---- Step 5: Install to /Applications -------------------------------------
step "Installing Rec to /Applications…"
pkill -9 -x "$APP_NAME" >/dev/null 2>&1 || true
# Wait for process to fully exit (up to 5s)
attempts=0
while [ "$attempts" -lt 10 ] && pgrep -x "$APP_NAME" >/dev/null 2>&1; do
sleep 0.5
attempts=$((attempts + 1))
done
DEST="/Applications/$APP_NAME.app"
INSTALLED=false
if [ "$FORCE_MODAL" = "1" ] || [ "$1" = "--modal" ] || [ "$1" = "--gui" ] || [ "$2" = "--modal" ]; then
INSTALLED=false
elif [ -w "/Applications" ]; then
rm -rf "$DEST" 2>/dev/null || true
if cp -R "$APP" "$DEST" 2>/dev/null; then
INSTALLED=true
fi
fi
if [ "$INSTALLED" = "true" ]; then
xattr -dr com.apple.quarantine "$DEST" >/dev/null 2>&1 || true
ok "Rec installed to /Applications."
printf '\n'
line
printf '\n%b\n\n' " ${GREEN}${BOLD}✓ Rec is installed and running!${NC}"
printf '%b\n\n' " ${GREY}Look for the floating record button on your screen.${NC}"
open "$DEST"
exit 0
fi
# Fallback: Build drag-and-drop installer modal if permission required
warn "Standard install required elevated privileges. Opening installer modal…"
cat > Installer.swift <<'INSTEOF'
import Cocoa
import QuartzCore
let appName = "Rec"
let sourcePath = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : ""
func getHighResIcon(path: String) -> NSImage {
let icns = "\(path)/Contents/Resources/AppIcon.icns"
if FileManager.default.fileExists(atPath: icns), let img = NSImage(contentsOfFile: icns) {
img.size = NSSize(width: 128, height: 128)
return img
}
let fallback = NSWorkspace.shared.icon(forFile: path)
fallback.size = NSSize(width: 128, height: 128)
return fallback
}
func performInstallation(src: URL) -> Bool {
let dest = URL(fileURLWithPath: "/Applications").appendingPathComponent(src.lastPathComponent)
do {
if FileManager.default.fileExists(atPath: dest.path) {
try FileManager.default.removeItem(at: dest)
}
try FileManager.default.copyItem(at: src, to: dest)
} catch {
let script = "do shell script \"rm -rf '/Applications/\(appName).app'; cp -R '\(src.path)' /Applications/\" with administrator privileges"
if let s = NSAppleScript(source: script) {
var err: NSDictionary?
s.executeAndReturnError(&err)
if err != nil {
let a = NSAlert(); a.messageText = "Installation failed"
a.informativeText = "Could not copy into Applications."
a.runModal(); return false
}
}
}
let clean = Process()
clean.launchPath = "/usr/bin/xattr"
clean.arguments = ["-dr", "com.apple.quarantine", dest.path]
clean.standardOutput = Pipe()
clean.standardError = Pipe()
try? clean.run(); clean.waitUntilExit()
NSSound(named: "Glass")?.play()
let a = NSAlert()
a.messageText = "Rec installed!"
a.informativeText = "Look for the floating record button."
a.addButton(withTitle: "Launch Rec")
a.addButton(withTitle: "Quit")
if a.runModal() == .alertFirstButtonReturn { NSWorkspace.shared.open(dest) }
NSApp.terminate(nil)
return true
}
class ActionTarget: NSObject {
@objc static func oneClickInstall() {
if !sourcePath.isEmpty {
let src = URL(fileURLWithPath: sourcePath)
_ = performInstallation(src: src)
}
}
}
// Icon you can drag with crisp 2x Retina rendering.
class DragIcon: NSImageView, NSDraggingSource {
var fileURL: URL?
func draggingSession(_ s: NSDraggingSession, sourceOperationMaskFor c: NSDraggingContext) -> NSDragOperation { .copy }
override func mouseDown(with event: NSEvent) {
guard let url = fileURL, let originalImg = image else { return }
let item = NSPasteboardItem()
item.setString(url.absoluteString, forType: .fileURL)
let drag = NSDraggingItem(pasteboardWriter: item)
let dragImg = NSImage(size: bounds.size)
dragImg.lockFocus()
if let ctx = NSGraphicsContext.current {
ctx.imageInterpolation = .high
}
originalImg.draw(in: bounds)
dragImg.unlockFocus()
drag.setDraggingFrame(bounds, contents: dragImg)
beginDraggingSession(with: [drag], event: event, source: self)
}
}
// The Applications folder drop target.
class DropZone: NSImageView {
override init(frame f: NSRect) { super.init(frame: f); registerForDraggedTypes([.fileURL]) }
required init?(coder: NSCoder) { super.init(coder: coder); registerForDraggedTypes([.fileURL]) }
override func draggingEntered(_ s: NSDraggingInfo) -> NSDragOperation { .copy }
override func performDragOperation(_ s: NSDraggingInfo) -> Bool {
guard let str = s.draggingPasteboard.propertyList(forType: .fileURL) as? String,
let src = URL(string: str) else { return false }
return performInstallation(src: src)
}
}
let app = NSApplication.shared
app.setActivationPolicy(.regular)
let W: CGFloat = 620, H: CGFloat = 410
let win = NSWindow(contentRect: NSRect(x: 0, y: 0, width: W, height: H),
styleMask: [.titled, .closable], backing: .buffered, defer: false)
win.title = "Install Rec"
win.center()
let bg = NSVisualEffectView(frame: NSRect(x: 0, y: 0, width: W, height: H))
bg.material = .windowBackground; bg.state = .active
win.contentView = bg
let title = NSTextField(labelWithString: "Install Rec")
title.frame = NSRect(x: 0, y: H - 65, width: W, height: 30)
title.alignment = .center
title.font = NSFont.systemFont(ofSize: 22, weight: .bold)
bg.addSubview(title)
let sub = NSTextField(labelWithString: "Drag Rec to Applications or click Instant Install below")
sub.frame = NSRect(x: 0, y: H - 90, width: W, height: 20)
sub.alignment = .center
sub.font = NSFont.systemFont(ofSize: 13)
sub.textColor = .secondaryLabelColor
bg.addSubview(sub)
let iconSize: CGFloat = 128
let midY: CGFloat = 145
// App icon (draggable)
let appIcon = DragIcon(frame: NSRect(x: 90, y: midY, width: iconSize, height: iconSize))
appIcon.imageScaling = .scaleProportionallyUpOrDown
appIcon.image = getHighResIcon(path: sourcePath)
appIcon.fileURL = URL(fileURLWithPath: sourcePath)
bg.addSubview(appIcon)
let appLabel = NSTextField(labelWithString: appName)
appLabel.frame = NSRect(x: 90, y: midY - 26, width: iconSize, height: 18)
appLabel.alignment = .center
appLabel.font = NSFont.systemFont(ofSize: 12, weight: .medium)
bg.addSubview(appLabel)
// Gradient Arrow View (Fading Stem Arrow)
class GradientArrowView: NSView {
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let w = bounds.width
let h = bounds.height
let midY = h / 2.0
let stemH: CGFloat = 10
let headH: CGFloat = 26
let headW: CGFloat = 20
let stemR = w - headW
let path = NSBezierPath()
path.move(to: NSPoint(x: 0, y: midY - stemH/2))
path.line(to: NSPoint(x: stemR, y: midY - stemH/2))
path.line(to: NSPoint(x: stemR, y: midY - headH/2))
path.line(to: NSPoint(x: w, y: midY))
path.line(to: NSPoint(x: stemR, y: midY + headH/2))
path.line(to: NSPoint(x: stemR, y: midY + stemH/2))
path.line(to: NSPoint(x: 0, y: midY + stemH/2))
path.close()
let startColor = NSColor.labelColor.withAlphaComponent(0.0)
let endColor = NSColor.labelColor.withAlphaComponent(0.50)
let gradient = NSGradient(starting: startColor, ending: endColor)
gradient?.draw(in: path, angle: 0)
}
}
let arrow = GradientArrowView(frame: NSRect(x: (W - 70)/2, y: midY + iconSize/2 - 18, width: 70, height: 36))
bg.addSubview(arrow)
// Applications folder (drop zone)
let drop = DropZone(frame: NSRect(x: W - 90 - iconSize, y: midY, width: iconSize, height: iconSize))
drop.imageScaling = .scaleProportionallyUpOrDown
let appsIcon = NSWorkspace.shared.icon(forFile: "/Applications")
appsIcon.size = NSSize(width: 128, height: 128)
drop.image = appsIcon
bg.addSubview(drop)
let appsLabel = NSTextField(labelWithString: "Applications")
appsLabel.frame = NSRect(x: W - 90 - iconSize, y: midY - 26, width: iconSize, height: 18)
appsLabel.alignment = .center
appsLabel.font = NSFont.systemFont(ofSize: 12, weight: .medium)
bg.addSubview(appsLabel)
class AnimatedInstallButton: NSButton {
private var trackingArea: NSTrackingArea?
private var isHovered = false { didSet { animateState() } }
private var isPressed = false { didSet { animateState() } }
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
wantsLayer = true
isBordered = false
focusRingType = .none
layer?.cornerRadius = 10
layer?.backgroundColor = NSColor(red: 0.18, green: 0.18, blue: 0.20, alpha: 1.0).cgColor
contentTintColor = .white
}
override func updateTrackingAreas() {
super.updateTrackingAreas()
if let area = trackingArea { removeTrackingArea(area) }
let area = NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .activeAlways], owner: self, userInfo: nil)
addTrackingArea(area)
trackingArea = area
}
override func mouseEntered(with event: NSEvent) {
isHovered = true
NSCursor.pointingHand.set()
}
override func mouseExited(with event: NSEvent) {
isHovered = false
NSCursor.arrow.set()
}
override func mouseDown(with event: NSEvent) {
isPressed = true
super.mouseDown(with: event)
}
override func mouseUp(with event: NSEvent) {
isPressed = false
super.mouseUp(with: event)
}
private func animateState() {
NSAnimationContext.runAnimationGroup { ctx in
ctx.duration = 0.15
ctx.allowsImplicitAnimation = true
if isPressed {
layer?.backgroundColor = NSColor(red: 0.14, green: 0.14, blue: 0.16, alpha: 1.0).cgColor
layer?.transform = CATransform3DMakeScale(0.95, 0.95, 1.0)
} else if isHovered {
layer?.backgroundColor = NSColor(red: 0.28, green: 0.28, blue: 0.30, alpha: 1.0).cgColor
layer?.transform = CATransform3DMakeScale(1.04, 1.04, 1.0)
layer?.shadowColor = NSColor.black.cgColor
layer?.shadowRadius = 8
layer?.shadowOpacity = 0.35
layer?.shadowOffset = .zero
} else {
layer?.backgroundColor = NSColor(red: 0.18, green: 0.18, blue: 0.20, alpha: 1.0).cgColor
layer?.transform = CATransform3DIdentity
layer?.shadowOpacity = 0
}
}
}
}
// Instant 1-Click Install Button
let installBtn = AnimatedInstallButton(frame: NSRect(x: (W - 270)/2, y: 35, width: 270, height: 42))
installBtn.title = "⚡ One-Click Install to /Applications"
installBtn.font = NSFont.systemFont(ofSize: 13, weight: .bold)
installBtn.target = ActionTarget.self
installBtn.action = #selector(ActionTarget.oneClickInstall)
bg.addSubview(installBtn)
win.makeKeyAndOrderFront(nil)
app.activate(ignoringOtherApps: true)
app.run()
INSTEOF
if swiftc -O -o Installer Installer.swift -framework Cocoa >/dev/null 2>&1; then
./Installer "$BUILD_DIR/$APP"
else
osascript -e "do shell script \"rm -rf '$DEST'; cp -R '$BUILD_DIR/$APP' '/Applications/'\" with administrator privileges" >/dev/null 2>&1 || true
xattr -dr com.apple.quarantine "$DEST" >/dev/null 2>&1 || true
open "$DEST"
fi
printf "\n"