#!/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" APP_VERSION="" 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 [ -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.2.8"; fi REPO_RAW="." # Use current directory for now, but usually from github # ---- 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 "${DIM}────────────────────────────────────────────────────────────${NC}\n"; } step() { printf "${RED}${BOLD}▸${NC} ${BOLD}%s${NC}\n" "$1"; } ok() { printf " ${GREEN}✓${NC} %s\n" "$1"; } warn() { printf " ${YELLOW}!${NC} %s\n" "$1"; } fail() { printf " ${RED}✗ %s${NC}\n" "$1"; } clear printf "\n" printf "${RED}${BOLD} Rec${NC}\n" printf "${GREY} Simple, clean, native-feeling screen recording.${NC}\n\n" 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 " ${GREY}A small official Apple installer will pop up. Please click ${BOLD}Install${NC}${GREY} and wait for it to finish.${NC}\n\n" xcode-select --install >/dev/null 2>&1 printf " ${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 " ${GREY}Please finish the Apple installer, then run this file again.${NC}\n\n" 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; } ok "Workspace ready." printf " " # ---- Step 3: Download the source ----------------------------------------- step "Downloading Rec source…" if ! curl -fsSL "https://raw.githubusercontent.com/arunofhyd/Rec/main/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 " ${GREY}Check your internet connection and try again.${NC} " exit 1 fi fi ok "Source ready." printf " " # ---- 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 16" "32 16@2x" "32 32" "64 32@2x" "128 128" "256 128@2x" "256 256" "512 256@2x" "512 512" "1024 512@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. PLIST if ! swiftc -O -o "$APP/Contents/MacOS/$APP_NAME" main.swift -framework Cocoa -framework ScreenCaptureKit -framework AVFoundation 2>build_errors.txt; then fail "Compilation failed." printf "${GREY}"; cat build_errors.txt; printf "${NC}\n" exit 1 fi chmod +x "$APP/Contents/MacOS/$APP_NAME" codesign --force --deep --sign - --requirements '=designated => identifier "com.aoh.rec"' "$APP" >/dev/null 2>&1 || true ok "App built." 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 -x "$APP_NAME" >/dev/null 2>&1 || true 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 ${GREEN}${BOLD}✓ Rec is installed and running!${NC}\n\n" printf " ${GREY}Look for the floating record button on your screen.${NC}\n\n" 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"