Fetching latest headlines…
NoSleep: A Lightweight macOS Menu Bar App with SwiftUI
NORTH AMERICA
πŸ‡ΊπŸ‡Έ United Statesβ€’March 22, 2026

NoSleep: A Lightweight macOS Menu Bar App with SwiftUI

1 views0 likes0 comments
Originally published byDev.to

Have you ever been mid-presentation, watching a long build compile, or waiting for a large file to download β€” only for your Mac to decide it's nap time? macOS ships a built-in tool for exactly this: caffeinate. But running it from the terminal every time is clunky.

So I built NoSleep β€” a tiny macOS menu bar utility that wraps caffeinate in a one-click toggle. No Dock icon. No main window. Just a cup icon in your menu bar.

NoSleep menu bar dropdown

Features

  • One-click toggle β€” start/stop caffeinate from the menu bar
  • Duration presets β€” 15 min, 30 min, 1 hr, 2 hr, 4 hr, 8 hr, 10 hr, or Indefinite
  • Live countdown β€” shows remaining time while active (e.g. 2h 34m)
  • Start at Login β€” optional LaunchAgent so it auto-starts on boot
  • Prevents display + idle sleep β€” uses caffeinate -d -i

The Stack

  • Swift 6.0 with strict concurrency
  • SwiftUI + MenuBarExtra (macOS 13+)
  • Swift Package Manager β€” no Xcode project file required
  • Minimum target: macOS 14 (Sonoma)

App Entry Point: MenuBarExtra

The entire app lives in the menu bar, which SwiftUI makes surprisingly clean with MenuBarExtra:

@main
struct NoSleepApp: App {
    @StateObject private var caffeinateManager = CaffeinateManager()
    @StateObject private var loginManager = LoginItemManager()

    var body: some Scene {
        MenuBarExtra {
            MenuBarView(manager: caffeinateManager, loginManager: loginManager)
        } label: {
            Image(systemName: caffeinateManager.isActive
                  ? "cup.and.saucer.fill"
                  : "cup.and.saucer")
        }
    }
}

That's the whole entry point. MenuBarExtra handles all the menu bar plumbing β€” no NSStatusItem, no AppKit boilerplate. The icon toggles between a filled and outlined cup based on whether caffeinate is running.

Setting LSUIElement: true in Info.plist hides the Dock icon and removes the main window entirely.

Core Logic: CaffeinateManager

The heart of the app is CaffeinateManager β€” an @MainActor ObservableObject that manages the caffeinate child process and a countdown timer.

Spawning the Process

func start() {
    stop()

    let proc = Process()
    proc.executableURL = URL(fileURLWithPath: "/usr/bin/caffeinate")

    var args = ["-d", "-i"]
    if selectedDuration != .indefinite {
        args += ["-t", "\(selectedDuration.rawValue)"]
        remainingSeconds = selectedDuration.rawValue
    }
    proc.arguments = args

    proc.terminationHandler = { [weak self] _ in
        Task { @MainActor [weak self] in
            self?.handleTermination()
        }
    }

    try? proc.run()
    process = proc
    isActive = true

    if selectedDuration != .indefinite {
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            Task { @MainActor [weak self] in
                self?.tick()
            }
        }
    }
}

A few things worth noting:

-d -i flags β€” -d prevents the display from sleeping, -i prevents idle sleep. Together they cover the common use cases.

-t <seconds> β€” when a duration is selected, caffeinate self-terminates after that many seconds. The app also runs a Timer in parallel to track remaining time for the UI.

terminationHandler β€” if caffeinate exits on its own (duration expired, or the system killed it), this handler fires and cleans up app state. The Task { @MainActor in ... } pattern bridges from the background callback thread into the main actor, which Swift 6 strict concurrency requires.

Duration Options

Durations are a typed enum with raw values in seconds:

enum SleepDuration: Int, CaseIterable, Identifiable, Sendable {
    case fifteenMin = 900
    case thirtyMin  = 1800
    case oneHour    = 3600
    case twoHours   = 7200
    case fourHours  = 14400
    case eightHours = 28800
    case tenHours   = 36000
    case indefinite = 0
}

The selected duration is persisted in UserDefaults so the preference survives app restarts.

Login Item: LaunchAgent Plist

Rather than using SMAppService (which requires a sandboxed app), NoSleep writes a LaunchAgent plist directly to ~/Library/LaunchAgents/:

<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.nosleep.app</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/you/Applications/NoSleep.app/Contents/MacOS/NoSleep</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>

This approach works without sandboxing and gives full control over the plist.

Build & Install

The project uses Swift Package Manager β€” no .xcodeproj needed.

# Build (compiles, bundles, ad-hoc code signs)
./build.sh

# Run
open NoSleep.app

# Install to ~/Applications (optional)
./install.sh

Requirements: Swift 6.0+, Xcode Command Line Tools, macOS 14+.

Source Code

NoSleep is open source under the GPLv3.

GitHub: github.com/sergio-farfan/nosleep

Contributions, issues, and stars are all welcome. If you run into any macOS quirks with caffeinate or MenuBarExtra, feel free to open an issue.

Built with Swift 6 and SwiftUI on macOS Sonoma.

Comments (0)

Sign in to join the discussion

Be the first to comment!