diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bc384f7..d4a1356 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -21,7 +21,7 @@ body: attributes: label: Steps to reproduce placeholder: | - 1. Click the coffee cup in the menu bar + 1. Click the robot in the menu bar 2. Toggle the switch on 3. Close the lid (on battery) validations: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 417ccb2..ea89e1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,11 +42,11 @@ jobs: set -euo pipefail # Build the full bundle (conservative target so it works on the runner's SDK). TARGET="arm64-apple-macos13.0" ./build.sh "$PWD/dist" - ls -la "dist/Sleepless.app/Contents" "dist/Sleepless.app/Contents/MacOS" + ls -la "dist/Sleepless Agents.app/Contents" "dist/Sleepless Agents.app/Contents/MacOS" - name: Upload app artifact uses: actions/upload-artifact@v4 with: name: Sleepless-app - path: dist/Sleepless.app + path: dist/Sleepless Agents.app if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 328eb02..4b931f3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,20 +40,20 @@ jobs: swiftc --version echo "SDK: $(xcrun --sdk macosx --show-sdk-version 2>/dev/null || echo n/a)" - - name: Build Sleepless.app + - name: Build Sleepless Agents.app run: | set -euo pipefail # Conservative deployment target so it compiles against the runner SDK and # runs on macOS 13+. (Local + tested target is arm64-apple-macos26.0.) TARGET="arm64-apple-macos13.0" ./build.sh "$PWD/dist" - codesign --verify --verbose=1 "dist/Sleepless.app" + codesign --verify --verbose=1 "dist/Sleepless Agents.app" - name: Zip the app bundle id: zip run: | set -euo pipefail ASSET="Sleepless-${{ steps.ver.outputs.version }}.zip" - ditto -c -k --keepParent "dist/Sleepless.app" "$ASSET" + ditto -c -k --keepParent "dist/Sleepless Agents.app" "$ASSET" echo "asset=$ASSET" >> "$GITHUB_OUTPUT" ls -la "$ASSET" @@ -98,9 +98,9 @@ jobs: ASSET="${{ steps.zip.outputs.asset }}" if gh release view "$TAG" >/dev/null 2>&1; then gh release upload "$TAG" "$ASSET" SHA256SUMS --clobber - gh release edit "$TAG" --notes-file NOTES.md --title "Sleepless ${{ steps.ver.outputs.version }}" + gh release edit "$TAG" --notes-file NOTES.md --title "Sleepless Agents ${{ steps.ver.outputs.version }}" else gh release create "$TAG" "$ASSET" SHA256SUMS \ - --title "Sleepless ${{ steps.ver.outputs.version }}" \ + --title "Sleepless Agents ${{ steps.ver.outputs.version }}" \ --notes-file NOTES.md fi diff --git a/App.swift b/App.swift index 4c4d254..0008c24 100644 --- a/App.swift +++ b/App.swift @@ -1,4 +1,4 @@ -// App.swift. Sleepless: a standalone menu-bar toggle that keeps the Mac running +// App.swift. Sleepless Agents: a standalone menu-bar toggle that keeps the Mac running // with the lid closed (on battery, no external display) via `pmset disablesleep`. // // Mechanism (verified live on this machine; disablesleep is UNDOCUMENTED in @@ -11,14 +11,15 @@ // disablesleep is runtime-only and resets to 0 on reboot, and that reset is a // deliberate safety feature; the app does NOT auto re-arm. // -// UI: clicking the menu-bar coffee cup opens a small native popover with an NSSwitch +// UI: clicking the menu-bar agent glyph opens a small native popover with an NSSwitch // toggle (the System-Settings control), a state caption, an auto-off timer, the // battery-floor slider, a Launch-at-login switch, and Quit. The menu-bar glyph also // shows state at a glance. // -// The coffee-cup metaphor is literal: an EMPTY cup means the Mac sleeps normally, a -// FULL cup means it is being kept awake (caffeinated), and a full cup with a small -// dot means it is awake on battery with the auto-off safety net live. +// The menu-bar mark is a tiny robot whose eyes read at a glance: asleep (gently closed +// eyes) means the Mac sleeps normally, awake (open eyes) means it is being kept awake, +// and an awake robot with a small dot means it is awake on battery with the auto-off +// safety net live. // // Three small, fail-safe features layer on top, none of which adds a daemon or // persists OS state (so "reboot resets it" still holds): @@ -27,7 +28,7 @@ // 2. Launch at login (SMAppService.mainApp) — OFF by default. The app always // launches reading the TRUE system state, so a login launch can never // re-enable disablesleep on its own. -// 3. Low-Power-Mode auto-off — on battery, if Low Power Mode is on, Sleepless +// 3. Low-Power-Mode auto-off — on battery, if Low Power Mode is on, Sleepless Agents // turns itself off. Same shape as the battery floor, evaluated on the same tick. // // Build (mirrors Nexus.app): Command Line Tools `swiftc`, NO Xcode project. @@ -45,49 +46,84 @@ private let floorKey = "batteryFloorPercent" private let floorDefault = 15 private let floorMin = 5 private let floorMax = 50 +private let appDisplayName = "Sleepless Agents" -// MARK: - Menu-bar coffee glyph (native SF Symbols, MONOCHROME template — state by SHAPE) +// MARK: - Menu-bar robot glyph (hand-drawn MONOCHROME template — state by EXPRESSION) // macOS convention: a menu-bar extra is a template image (no colour) so it adapts to light/dark -// bars and inverts on highlight. State is read from the SILHOUETTE, not colour. The old -// empty-vs-filled cups looked near-identical at 16 px, so we switch the silhouette dramatically -// with steam (a hot cup = awake): -// OFF (sleeps normally) = cup.and.saucer cup resting on its saucer, NO steam (cold/asleep) -// ON (kept awake, on power) = cup.and.heat.waves.fill hot cup with rising steam (awake) -// ARMED (kept awake, on battery) = cup.and.heat.waves.fill + a small dot (awake, safety net live) -// The no-steam → steam change reads instantly even at 16 px; the armed dot is the only extra -// mark. All template (monochrome) — SF Symbols only, no hand-drawn paths. +// bars and inverts on highlight. We draw a BOLD, FILLED robot silhouette (a solid head that fills +// the bar height, with negative-space eyes + smile + little side ears) so it stays clear and +// legible at menu-bar size. The FACE communicates state, matching the app icon's robot identity: +// OFF (sleeps normally) = robot asleep, gently closed eyes +// ON (kept awake, on power) = robot awake, open round eyes +// ARMED (kept awake, on battery) = awake robot + a small dot (auto-off safety net live) +// Drawn from vectors (not SF Symbols, which have no robot glyph) so it re-renders crisply at the +// menu bar's backing scale. enum SleepGlyph { case off case on case armed } -private func makeCupGlyph(_ glyph: SleepGlyph) -> NSImage { - let cfg = NSImage.SymbolConfiguration(pointSize: 15, weight: .regular).applying(.init(scale: .medium)) - let name = (glyph == .off) ? "cup.and.saucer" : "cup.and.heat.waves.fill" - let base = NSImage(systemSymbolName: name, accessibilityDescription: "Sleepless")? - .withSymbolConfiguration(cfg) - ?? NSImage(systemSymbolName: "cup.and.saucer.fill", accessibilityDescription: "Sleepless") - ?? NSImage() - - guard glyph == .armed else { - base.isTemplate = true - return base - } - // ARMED: full steaming cup + a small filled dot top-right (the "auto-off safety net is live" - // mark). Drawn in template black so it tints + inverts with the menu bar exactly like the cup. - let size = base.size - guard size.width > 0, size.height > 0 else { base.isTemplate = true; return base } - let composed = NSImage(size: size) - composed.lockFocus() - base.draw(in: NSRect(origin: .zero, size: size)) - let d = max(size.height * 0.26, 4) - let dot = NSBezierPath(ovalIn: NSRect(x: size.width - d, y: size.height - d, width: d, height: d)) - NSColor.black.setFill() - dot.fill() - composed.unlockFocus() - composed.isTemplate = true - return composed +private func makeRobotGlyph(_ glyph: SleepGlyph) -> NSImage { + let asleep = (glyph == .off) + let showDot = (glyph == .armed) + let W: CGFloat = 19 + let H: CGFloat = 17 + + func crescent(_ cx: CGFloat, _ cy: CGFloat, halfW: CGFloat, thick: CGFloat) -> CGPath { + let p = CGMutablePath() + let l = CGPoint(x: cx - halfW, y: cy) + let r = CGPoint(x: cx + halfW, y: cy) + p.move(to: l) + p.addQuadCurve(to: r, control: CGPoint(x: cx, y: cy - thick)) + p.addQuadCurve(to: l, control: CGPoint(x: cx, y: cy)) + p.closeSubpath() + return p + } + + let img = NSImage(size: NSSize(width: W, height: H), flipped: false) { _ in + guard let cg = NSGraphicsContext.current?.cgContext else { return true } + cg.setFillColor(NSColor.black.cgColor) + + let headW: CGFloat = 13.0, headH: CGFloat = 13.0 + let head = CGRect(x: (W - headW) / 2, y: (H - headH) / 2 - 0.2, width: headW, height: headH) + let corner = headW * 0.32 + let midX = head.midX + + let earW: CGFloat = 2.0, earH: CGFloat = 5.2 + func earRect(_ sign: CGFloat) -> CGRect { + let ex = sign < 0 ? head.minX - earW * 0.55 : head.maxX - earW * 0.45 + return CGRect(x: ex, y: head.midY - earH / 2, width: earW, height: earH) + } + for sign in [-1.0, 1.0] as [CGFloat] { + cg.addPath(CGPath(roundedRect: earRect(sign), cornerWidth: earW / 2, cornerHeight: earW / 2, transform: nil)) + } + cg.fillPath() + + let p = CGMutablePath() + p.addPath(CGPath(roundedRect: head, cornerWidth: corner, cornerHeight: corner, transform: nil)) + let eyeDX: CGFloat = 2.9 + let eyeY = head.midY + 1.0 + if asleep { + for s in [-eyeDX, eyeDX] { p.addPath(crescent(midX + s, eyeY + 0.3, halfW: 1.9, thick: 1.1)) } + } else { + let r: CGFloat = 1.85 + for s in [-eyeDX, eyeDX] { + p.addEllipse(in: CGRect(x: midX + s - r, y: eyeY - r, width: r * 2, height: r * 2)) + } + } + p.addPath(crescent(midX, head.midY - 2.6, halfW: 2.7, thick: 1.05)) + cg.addPath(p) + cg.fillPath(using: .evenOdd) + + if showDot { + let d: CGFloat = 3.0 + cg.fillEllipse(in: CGRect(x: W - d - 0.2, y: H - d - 0.2, width: d, height: d)) + } + return true + } + img.isTemplate = true + return img } // Flipped container so popover content lays out top-down with simple frames. @@ -141,15 +177,15 @@ private final class CardView: NSView { final class AppDelegate: NSObject, NSApplicationDelegate { private var statusItem: NSStatusItem! private var timer: Timer? - private let onGlyph = makeCupGlyph(.on) - private let offGlyph = makeCupGlyph(.off) - private let armedGlyph = makeCupGlyph(.armed) + private let onGlyph = makeRobotGlyph(.on) + private let offGlyph = makeRobotGlyph(.off) + private let armedGlyph = makeRobotGlyph(.armed) // Popover UI private let popover = NSPopover() private var toggleSwitch: NSSwitch! private var mainCard: CardView! // group-1 card; gets the brand-violet wash when awake - private var headerMark: NSImageView! // header coffee mark; tints violet when awake + private var headerMark: NSImageView! // header robot mark; tints violet when awake private var captionLabel: NSTextField! private var floorValueLabel: NSTextField! private var floorSlider: NSSlider! @@ -204,15 +240,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate { root.blendingMode = .behindWindow root.state = .followsWindowActiveState - // Header: small coffee mark + "Sleepless" (quiet system glyph, not a branded logo). - // The mark tints to the brand violet while the Mac is kept awake. - let mark = NSImageView(frame: NSRect(x: pad, y: 14, width: 18, height: 18)) - let headerCup = makeCupGlyph(.on); headerCup.isTemplate = true - mark.image = headerCup + // Header: small robot mark + app name. The mark tints to the brand violet while + // the Mac is kept awake. + let mark = NSImageView(frame: NSRect(x: pad, y: 13, width: 19, height: 18)) + let headerRobot = makeRobotGlyph(.on); headerRobot.isTemplate = true + mark.image = headerRobot mark.contentTintColor = .labelColor root.addSubview(mark) headerMark = mark - let title = makeLabel("Sleepless", font: .systemFont(ofSize: 14, weight: .semibold), color: .labelColor) + let title = makeLabel(appDisplayName, font: .systemFont(ofSize: 14, weight: .semibold), color: .labelColor) title.frame = NSRect(x: pad + 24, y: 14, width: contentW - 24, height: 20) root.addSubview(title) @@ -306,7 +342,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { g4.addSubview(loginSwitch) // Footer — Quit (separated by space, not a hairline) - let quit = NSButton(title: "Quit Sleepless", target: self, action: #selector(quit)) + let quit = NSButton(title: "Quit \(appDisplayName)", target: self, action: #selector(quit)) quit.controlSize = .regular quit.bezelStyle = .rounded quit.sizeToFit() @@ -329,7 +365,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { return t } - // MARK: - Click the menu-bar cup to open/close the popover + // MARK: - Click the menu-bar robot to open/close the popover @objc private func statusClicked() { if popover.isShown { closePopover() } else { openPopover() } } @@ -397,7 +433,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let intro = NSAlert() intro.alertStyle = .informational intro.messageText = "Enable keeping your Mac awake" - intro.informativeText = "Sleepless flips a protected macOS setting (pmset disablesleep), so it needs your permission once. macOS will ask you to authenticate (Touch ID or your password). After that the switch works instantly, with no more prompts." + intro.informativeText = "\(appDisplayName) flips a protected macOS setting (pmset disablesleep), so it needs your permission once. macOS will ask you to authenticate (Touch ID or your password). After that the switch works instantly, with no more prompts." intro.addButton(withTitle: "Enable") intro.addButton(withTitle: "Not now") NSApp.activate(ignoringOtherApps: true) @@ -424,8 +460,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { return false } - // A brief, subtle pulse on the menu-bar glyph whenever the state (and thus the cup - // shape) changes, so the change is noticeable. Opacity-only: no layer geometry is + // A brief, subtle pulse on the menu-bar glyph whenever the robot expression changes, + // so the change is noticeable. Opacity-only: no layer geometry is // mutated, so it can't shift the status item on any macOS version. private func pulseStatusItem() { guard let b = statusItem.button else { return } @@ -478,7 +514,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { autoOffMinutes = 0 autoOffControl?.selectedSegment = 0 applyUI(on: readSleepDisabled()) - notify("Auto-off timer ended. Sleepless turned off.") + notify("Auto-off timer ended. \(appDisplayName) turned off.") } private func startCountdownTicker() { @@ -523,7 +559,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { isOn = on if !on { cancelKeepAwakeTimer() } // going OFF clears any countdown/timer // ARMED = kept awake while actively discharging on battery, so the - // auto-off safety net is live. Distinct menu-bar glyph (cup + dot). + // auto-off safety net is live. Distinct menu-bar glyph (awake robot + dot). var armed = false if on { let (onBattery, discharging, _) = batteryStatus() @@ -531,15 +567,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } if let button = statusItem.button { let newImage = on ? (armed ? armedGlyph : onGlyph) : offGlyph - if button.image !== newImage { // state (cup shape) changed -> swap + pulse + if button.image !== newImage { // state (robot expression) changed -> swap + pulse button.image = newImage pulseStatusItem() } button.toolTip = on ? (armed - ? "Sleepless: on (battery). Auto-off at \(batteryFloorPercent)% or in Low Power Mode." - : "Sleepless: on. Stays awake with the lid closed.") - : "Sleepless: off. Sleeps normally." + ? "\(appDisplayName): on (battery). Auto-off at \(batteryFloorPercent)% or in Low Power Mode." + : "\(appDisplayName): on. Stays awake with the lid closed.") + : "\(appDisplayName): off. Sleeps normally." } toggleSwitch?.state = on ? .on : .off // Brand-violet accent communicates the privileged "awake" state at a glance. @@ -627,14 +663,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate { if percent <= batteryFloorPercent { setDisableSleep(false); userForcedOn = false applyUI(on: readSleepDisabled()) - notify("Battery low (\(percent)%). Sleepless turned off.") + notify("Battery low (\(percent)%). \(appDisplayName) turned off.") return } // Low Power Mode auto-off, UNLESS the user deliberately chose to keep awake this session. if ProcessInfo.processInfo.isLowPowerModeEnabled && !userForcedOn { setDisableSleep(false) applyUI(on: readSleepDisabled()) - notify("Low Power Mode on. Sleepless turned off.") + notify("Low Power Mode on. \(appDisplayName) turned off.") } } @@ -663,7 +699,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { // MARK: - Notification (mirrors Nexus' osascript approach) private func notify(_ message: String) { - let script = "display notification \"\(message)\" with title \"Sleepless\" sound name \"Tink\"" + let script = "display notification \"\(message)\" with title \"\(appDisplayName)\" sound name \"Tink\"" _ = runCapture("/usr/bin/osascript", ["-e", script]) } diff --git a/CHANGELOG.md b/CHANGELOG.md index af0109d..0cf3bbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Renamed the app to Sleepless Agents and updated the app/menu-bar identity to a + robot mark with sleepy, awake, and battery-aware states. + ## [1.2.7] - 2026-06-03 ### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 652b4e1..b4d4696 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,8 +23,8 @@ No Xcode project — just the Command Line Tools: ```sh git clone https://github.com/Aboudjem/Sleepless.git cd Sleepless -./build.sh # builds ./build/Sleepless.app, ad-hoc signed -open build/Sleepless.app +./build.sh # builds ./build/Sleepless Agents.app, ad-hoc signed +open "build/Sleepless Agents.app" ``` `./install.sh` additionally installs the passwordless grant + login item (it prints exactly diff --git a/Info.plist b/Info.plist index 5bf5d26..048b2bb 100644 --- a/Info.plist +++ b/Info.plist @@ -4,8 +4,8 @@ CFBundleExecutable Sleepless CFBundleIdentifier com.aboudjem.Sleepless - CFBundleName Sleepless - CFBundleDisplayName Sleepless + CFBundleName Sleepless Agents + CFBundleDisplayName Sleepless Agents CFBundlePackageType APPL CFBundleIconFile Sleepless CFBundleShortVersionString 1.2.7 diff --git a/README.de.md b/README.de.md index 962fddd..7c0de7d 100644 --- a/README.de.md +++ b/README.de.md @@ -47,7 +47,7 @@ ```sh brew install --cask aboudjem/tap/sleepless -/Applications/Sleepless.app/Contents/Resources/grant.sh # one-time passwordless grant +/Applications/Sleepless\ Agents.app/Contents/Resources/grant.sh # one-time passwordless grant ``` | Weitere Wege | | diff --git a/README.es.md b/README.es.md index f7aa1bb..d48368f 100644 --- a/README.es.md +++ b/README.es.md @@ -47,7 +47,7 @@ ```sh brew install --cask aboudjem/tap/sleepless -/Applications/Sleepless.app/Contents/Resources/grant.sh # one-time passwordless grant +/Applications/Sleepless\ Agents.app/Contents/Resources/grant.sh # one-time passwordless grant ``` | Otras formas | | diff --git a/README.fr.md b/README.fr.md index edb43bb..daca9f5 100644 --- a/README.fr.md +++ b/README.fr.md @@ -47,7 +47,7 @@ ```sh brew install --cask aboudjem/tap/sleepless -/Applications/Sleepless.app/Contents/Resources/grant.sh # one-time passwordless grant +/Applications/Sleepless\ Agents.app/Contents/Resources/grant.sh # one-time passwordless grant ``` | Autres méthodes | | diff --git a/README.ja.md b/README.ja.md index bb89c82..ad40aa8 100644 --- a/README.ja.md +++ b/README.ja.md @@ -47,7 +47,7 @@ ```sh brew install --cask aboudjem/tap/sleepless -/Applications/Sleepless.app/Contents/Resources/grant.sh # one-time passwordless grant +/Applications/Sleepless\ Agents.app/Contents/Resources/grant.sh # one-time passwordless grant ``` | その他の方法 | | diff --git a/README.md b/README.md index dda0727..778f86c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ - Sleepless: keep your Mac awake with the lid closed + Sleepless Agents: keep your Mac awake with the lid closed

@@ -47,7 +47,7 @@ ```sh brew install --cask aboudjem/tap/sleepless -/Applications/Sleepless.app/Contents/Resources/grant.sh # one-time passwordless grant +/Applications/Sleepless\ Agents.app/Contents/Resources/grant.sh # one-time passwordless grant ``` | Other ways | | @@ -55,13 +55,13 @@ brew install --cask aboudjem/tap/sleepless | **Download** | Grab the [latest release](https://github.com/Aboudjem/Sleepless/releases/latest), unzip to `/Applications`, then approve it in **System Settings → Privacy & Security → Open Anyway** (it is ad-hoc signed). | | **Build from source** | `git clone https://github.com/Aboudjem/Sleepless.git && cd Sleepless && ./install.sh` (no Gatekeeper prompt). | -Then click the cup in the menu bar, flip the switch, and close the lid. +Then click the robot in the menu bar, flip the switch, and close the lid. ## Features | | | | |---|---|---| -| ☕ | **One switch** | Click the menu-bar cup, flip the toggle. | +| 🤖 | **One switch** | Click the menu-bar robot, flip the toggle. | | ⏲️ | **Auto-off timer** | 1h or 2h with a live countdown, then off. | | 🔋 | **Battery floor** | Auto-off at 5–50% on battery (default 15%). | | 🪫 | **Low Power Mode** | Steps aside when LPM is on, on battery. | @@ -69,11 +69,11 @@ Then click the cup in the menu bar, flip the switch, and close the lid. | 🚀 | **Launch at login** | Optional, off by default, always starts idle. | | 🪶 | **Tiny + native** | One AppKit file. No Dock icon, daemon, or kext. | -**Menu-bar glyph:** empty cup = off · full cup = awake · full cup + dot = awake on battery (auto-off live). +**Menu-bar glyph:** sleepy robot = off · awake robot = awake · awake robot + dot = awake on battery (auto-off live). ## Sleepless vs the alternatives -| | **Sleepless** | Amphetamine | KeepingYouAwake | `caffeinate` | +| | **Sleepless Agents** | Amphetamine | KeepingYouAwake | `caffeinate` | |---|:---:|:---:|:---:|:---:| | Awake, lid closed, no monitor | ✅ ¹ | ⚠️ ² | ❌ ³ | ❌ | | On battery | ✅ | ✅ | ✅ lid open | ⚠️ ⁴ | diff --git a/README.zh-CN.md b/README.zh-CN.md index 19b66ff..1741e58 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -49,7 +49,7 @@ ```sh brew install --cask aboudjem/tap/sleepless -/Applications/Sleepless.app/Contents/Resources/grant.sh # one-time passwordless grant +/Applications/Sleepless\ Agents.app/Contents/Resources/grant.sh # one-time passwordless grant ``` | 其他方式 | | diff --git a/assets/Sleepless-1024.png b/assets/Sleepless-1024.png index b284739..25d631e 100644 Binary files a/assets/Sleepless-1024.png and b/assets/Sleepless-1024.png differ diff --git a/assets/Sleepless.icns b/assets/Sleepless.icns index 181a9d7..cc429f3 100644 Binary files a/assets/Sleepless.icns and b/assets/Sleepless.icns differ diff --git a/assets/social-preview.png b/assets/social-preview.png index 73ff412..e865648 100644 Binary files a/assets/social-preview.png and b/assets/social-preview.png differ diff --git a/assets/social-preview.svg b/assets/social-preview.svg index 415c9e5..f2e5a95 100644 --- a/assets/social-preview.svg +++ b/assets/social-preview.svg @@ -9,7 +9,7 @@ - + Sleepless Keep your Mac awake. Lid closed. On battery. No external display. Native menu-bar app. diff --git a/build.sh b/build.sh index 156d041..542210a 100755 --- a/build.sh +++ b/build.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash -# build.sh — compile Sleepless.app from source with the Command Line Tools only. +# build.sh — compile Sleepless Agents.app from source with the Command Line Tools only. # # No Xcode project, no Package.swift: just `swiftc` + a hand-assembled .app bundle, # ad-hoc signed. Works from any clone (no hardcoded paths or usernames). # # Usage: -# ./build.sh # build into ./build/Sleepless.app +# ./build.sh # build into ./build/Sleepless Agents.app # ./build.sh /Applications # build straight into /Applications # DEST=/Applications ./build.sh # same, via env # ./build.sh --regen-icon # re-render the .icns from make-icon.swift first @@ -15,7 +15,8 @@ set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -APP_NAME="Sleepless" +APP_NAME="Sleepless Agents" +EXECUTABLE_NAME="Sleepless" # macOS arm64 target. Sleepless is verified on macOS 26 (Tahoe) / Apple Silicon. # Override with TARGET=... (e.g. CI on a runner whose SDK predates macOS 26). TARGET="${TARGET:-arm64-apple-macos26.0}" @@ -42,13 +43,13 @@ echo " target: $TARGET" command -v swiftc >/dev/null || { echo "error: swiftc not found. Install the Command Line Tools: xcode-select --install" >&2; exit 1; } # 1. Optionally regenerate the icon from the SF Symbol (needs a GUI session for AppKit). -ICNS="$REPO/assets/$APP_NAME.icns" +ICNS="$REPO/assets/$EXECUTABLE_NAME.icns" if [ "$REGEN_ICON" = "1" ]; then echo "==> Regenerating icon from make-icon.swift" TMP_ICON="$(mktemp -d)" swiftc -O -framework AppKit "$REPO/make-icon.swift" -o "$TMP_ICON/mkicon" "$TMP_ICON/mkicon" "$TMP_ICON" - iconutil -c icns "$TMP_ICON/$APP_NAME.iconset" -o "$REPO/assets/$APP_NAME.icns" + iconutil -c icns "$TMP_ICON/$EXECUTABLE_NAME.iconset" -o "$REPO/assets/$EXECUTABLE_NAME.icns" rm -rf "$TMP_ICON" fi [ -f "$ICNS" ] || { echo "error: missing $ICNS (run ./build.sh --regen-icon)" >&2; exit 1; } @@ -57,16 +58,17 @@ fi echo "==> Compiling App.swift" BIN_TMP="$(mktemp -d)" swiftc -O -parse-as-library -target "$TARGET" -framework AppKit -framework ServiceManagement \ - "$REPO/App.swift" -o "$BIN_TMP/$APP_NAME" + "$REPO/App.swift" -o "$BIN_TMP/$EXECUTABLE_NAME" # 3. Assemble the bundle: Contents/{Info.plist, MacOS/, Resources/.icns} echo "==> Assembling bundle" rm -rf "$APP" +rm -rf "$DEST/Sleepless.app" mkdir -p "$CONTENTS/MacOS" "$CONTENTS/Resources" cp "$REPO/Info.plist" "$CONTENTS/Info.plist" -cp "$BIN_TMP/$APP_NAME" "$CONTENTS/MacOS/$APP_NAME" -cp "$ICNS" "$CONTENTS/Resources/$APP_NAME.icns" -chmod +x "$CONTENTS/MacOS/$APP_NAME" +cp "$BIN_TMP/$EXECUTABLE_NAME" "$CONTENTS/MacOS/$EXECUTABLE_NAME" +cp "$ICNS" "$CONTENTS/Resources/$EXECUTABLE_NAME.icns" +chmod +x "$CONTENTS/MacOS/$EXECUTABLE_NAME" # Ship the grant + uninstall scripts inside the bundle so Homebrew-cask users (who get # only the .app) can run the one-time passwordless grant and a clean uninstall. cp "$REPO/grant.sh" "$REPO/uninstall.sh" "$CONTENTS/Resources/" diff --git a/docs/AUDIT.md b/docs/AUDIT.md index 8fa12b9..674266a 100644 --- a/docs/AUDIT.md +++ b/docs/AUDIT.md @@ -65,7 +65,7 @@ swiftc -O -parse-as-library -target arm64-apple-macos13.0 \ # Unzip the release and compare the Mach-O inside the bundle. ditto -x -k Sleepless-.zip /tmp/rel -shasum -a 256 /tmp/Sleepless-rebuilt /tmp/rel/Sleepless.app/Contents/MacOS/Sleepless +shasum -a 256 /tmp/Sleepless-rebuilt "/tmp/rel/Sleepless Agents.app/Contents/MacOS/Sleepless" ``` Caveats, stated honestly: @@ -115,11 +115,11 @@ xcrun notarytool store-credentials "notarytool-password" \ # Re-sign with a Developer ID cert + hardened runtime + secure timestamp. codesign --force --options runtime --timestamp \ - --sign "Developer ID Application: ()" Sleepless.app + --sign "Developer ID Application: ()" "Sleepless Agents.app" -ditto -c -k --keepParent Sleepless.app Sleepless.zip +ditto -c -k --keepParent "Sleepless Agents.app" Sleepless.zip xcrun notarytool submit Sleepless.zip --keychain-profile "notarytool-password" --wait -xcrun stapler staple Sleepless.app +xcrun stapler staple "Sleepless Agents.app" ``` Prerequisite: [Apple Developer Program, $99/yr](https://developer.apple.com/programs/whats-included/), diff --git a/docs/LAUNCH.md b/docs/LAUNCH.md index a03e109..adf8df1 100644 --- a/docs/LAUNCH.md +++ b/docs/LAUNCH.md @@ -123,10 +123,10 @@ fresh launch window. - **When:** schedule for **12:01am Pacific**; for a dev tool, a weekend (esp. Sunday) is a known lower-competition slot. Prime ~300–400 warm people beforehand; PH amplifies momentum, it does not create it. "Launched" is not "Featured" (PH curates the homepage). -- **Name:** Sleepless +- **Name:** Sleepless Agents - **Tagline (≤60 chars):** `Keep your Mac awake with the lid closed` - **Assets:** gallery images **1270×760**, thumbnail **240×240** (reuse the violet brand + - coffee cup; `assets/social-preview.png` is a good base), a short demo (use `assets/demo.gif`). + robot mark; `assets/social-preview.png` is a good base), a short demo (use `assets/demo.gif`). - **Topics:** Mac, Developer Tools, Productivity, Open Source. - **First maker comment:** diff --git a/docs/icon.png b/docs/icon.png index 08d1856..449c134 100644 Binary files a/docs/icon.png and b/docs/icon.png differ diff --git a/docs/index.html b/docs/index.html index 35f7aff..3b7a7ec 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,18 +3,18 @@ -Sleepless: keep your Mac awake with the lid closed - +Sleepless Agents: keep your Mac awake with the lid closed + - + - + @@ -29,7 +29,7 @@ { "@context": "https://schema.org", "@type": "SoftwareApplication", - "name": "Sleepless", + "name": "Sleepless Agents", "operatingSystem": "macOS 26 (Apple Silicon)", "applicationCategory": "UtilitiesApplication", "description": "A native macOS menu-bar app that keeps your MacBook awake with the lid closed, on battery, with no external display, using pmset disablesleep, with an auto-off timer and a battery-floor cutoff.", @@ -44,7 +44,7 @@ "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [ - {"@type":"Question","name":"How do I keep my MacBook awake with the lid closed without a monitor?","acceptedAnswer":{"@type":"Answer","text":"Install Sleepless, click the coffee cup in the menu bar, flip the switch on, and close the lid (the laptop screen, also called the flap). It keeps the Mac awake on battery with no external display, using pmset disablesleep. No dummy HDMI plug or clamshell adapter is needed."}}, + {"@type":"Question","name":"How do I keep my MacBook awake with the lid closed without a monitor?","acceptedAnswer":{"@type":"Answer","text":"Install Sleepless Agents, click the robot in the menu bar, flip the switch on, and close the lid (the laptop screen, also called the flap). It keeps the Mac awake on battery with no external display, using pmset disablesleep. No dummy HDMI plug or clamshell adapter is needed."}}, {"@type":"Question","name":"Why does my MacBook sleep when I close the lid even with Amphetamine or KeepingYouAwake?","acceptedAnswer":{"@type":"Answer","text":"Those tools are built on macOS power assertions, which stop the idle timer but cannot override the hardware lid-close trigger. KeepingYouAwake wraps caffeinate, which its maintainer confirms cannot do lid-close. pmset disablesleep, which Sleepless uses, is a lower-level setting that can."}}, {"@type":"Question","name":"Does pmset disablesleep still work on Apple Silicon?","acceptedAnswer":{"@type":"Answer","text":"Yes. pmset -a disablesleep 1 sets the kernel's SleepDisabled flag on Apple Silicon, confirmed firsthand on macOS 26.3, which keeps a MacBook awake with the lid closed on battery. Apple does not officially document the setting, so verify it with pmset -g | grep SleepDisabled. Most claims that it stopped working actually describe caffeinate, a different mechanism."}}, {"@type":"Question","name":"Do I need a dummy HDMI display plug to use clamshell mode?","acceptedAnswer":{"@type":"Answer","text":"No. Apple's official clamshell mode needs external power and a display, but Sleepless keeps the Mac awake with the lid closed on battery alone, with no display and no HDMI dongle."}} @@ -288,7 +288,7 @@