From 6ec3fdc42a1069a52a261b6675decd9d5046ff55 Mon Sep 17 00:00:00 2001 From: Ensar Bavrk Date: Thu, 22 Oct 2026 14:10:36 +0200 Subject: [PATCH 1/4] Add a jiggler that keeps the Mac awake and nudges the cursor when idle --- macos/Sources/DevDashBar/Jiggler.swift | 90 ++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 macos/Sources/DevDashBar/Jiggler.swift diff --git a/macos/Sources/DevDashBar/Jiggler.swift b/macos/Sources/DevDashBar/Jiggler.swift new file mode 100644 index 0000000..6aa84ed --- /dev/null +++ b/macos/Sources/DevDashBar/Jiggler.swift @@ -0,0 +1,90 @@ +import AppKit +import IOKit.pwr_mgt +import Observation + +/// Keeps the Mac awake and the user active while on: holds a display sleep assertion and, once +/// the user has been idle for a while, nudges the cursor one point and back so chat apps stay green. +/// The nudge needs Accessibility access; without it only the power assertion and a declared user +/// activity are used. +@MainActor +@Observable +final class Jiggler { + static let shared = Jiggler() + + private static let enabledKey = "jigglerEnabled" + private static let checkInterval: Duration = .seconds(20) + private static let idleThreshold: TimeInterval = 50 + + private(set) var isOn = false + /// Whether the cursor nudge can be posted; refreshed on every check. + private(set) var canNudge = CGPreflightPostEventAccess() + + @ObservationIgnored private var assertion: IOPMAssertionID = 0 + @ObservationIgnored private var activity: IOPMAssertionID = 0 + @ObservationIgnored private var loop: Task? + + /// Restores the state from the last launch. + func start() { + if UserDefaults.standard.bool(forKey: Self.enabledKey) { setOn(true) } + } + + func toggle() { + // Shows the system prompt once; the nudge starts working as soon as access is granted. + if !isOn, !CGPreflightPostEventAccess() { CGRequestPostEventAccess() } + setOn(!isOn) + } + + func setOn(_ on: Bool) { + guard on != isOn else { return } + isOn = on + UserDefaults.standard.set(on, forKey: Self.enabledKey) + if on { + IOPMAssertionCreateWithName( + kIOPMAssertionTypePreventUserIdleDisplaySleep as CFString, + IOPMAssertionLevel(kIOPMAssertionLevelOn), + "devdash jiggler" as CFString, + &assertion + ) + loop = Task { [weak self] in + while !Task.isCancelled { + self?.tick() + try? await Task.sleep(for: Self.checkInterval) + } + } + } else { + loop?.cancel() + loop = nil + release(&assertion) + release(&activity) + } + } + + private func tick() { + canNudge = CGPreflightPostEventAccess() + guard idleSeconds >= Self.idleThreshold else { return } + if canNudge { nudge() } + // Resets the system idle timer even without Accessibility access. + IOPMAssertionDeclareUserActivity("devdash jiggler" as CFString, kIOPMUserActiveLocal, &activity) + } + + private var idleSeconds: TimeInterval { + // kCGAnyInputEventType: time since any keyboard, mouse or tablet event. + CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: CGEventType(rawValue: ~0)!) + } + + /// Moves the cursor one point and straight back, too fast to see. + private func nudge() { + guard let location = CGEvent(source: nil)?.location else { return } + let source = CGEventSource(stateID: .hidSystemState) + for point in [CGPoint(x: location.x + 1, y: location.y), location] { + CGEvent(mouseEventSource: source, mouseType: .mouseMoved, mouseCursorPosition: point, mouseButton: .left)? + .post(tap: .cghidEventTap) + } + } + + private func release(_ id: inout IOPMAssertionID) { + guard id != 0 else { return } + IOPMAssertionRelease(id) + id = 0 + } +} From b6a7f3f4f19ba6c4ed6b7f447d60d16c9e6a3810 Mon Sep 17 00:00:00 2001 From: Ensar Bavrk Date: Thu, 22 Oct 2026 14:10:36 +0200 Subject: [PATCH 2/4] Add a keep awake toggle to the popover header and show a cup in the menu bar while it is on --- macos/Sources/DevDashBar/DevDashBarApp.swift | 8 ++++++-- macos/Sources/DevDashBar/Views/PopoverView.swift | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/macos/Sources/DevDashBar/DevDashBarApp.swift b/macos/Sources/DevDashBar/DevDashBarApp.swift index e425eec..91f9087 100644 --- a/macos/Sources/DevDashBar/DevDashBarApp.swift +++ b/macos/Sources/DevDashBar/DevDashBarApp.swift @@ -61,7 +61,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { // Also applies when run unbundled via `swift run`, where LSUIElement is absent. NSApp.setActivationPolicy(.accessory) - MainActor.assumeIsolated { Updater.shared.start() } + MainActor.assumeIsolated { + Updater.shared.start() + Jiggler.shared.start() + } } func applicationWillTerminate(_ notification: Notification) { @@ -95,7 +98,8 @@ struct MenuBarLabel: View { private var symbol: String { switch store.connection { case .failed, .missingBinary: "exclamationmark.triangle" - default: store.isUnderPressure ? "flame" : "server.rack" + default: + if store.isUnderPressure { "flame" } else if Jiggler.shared.isOn { "cup.and.saucer.fill" } else { "server.rack" } } } } diff --git a/macos/Sources/DevDashBar/Views/PopoverView.swift b/macos/Sources/DevDashBar/Views/PopoverView.swift index ca0d3c4..3c5652c 100644 --- a/macos/Sources/DevDashBar/Views/PopoverView.swift +++ b/macos/Sources/DevDashBar/Views/PopoverView.swift @@ -209,6 +209,11 @@ private struct HeaderView: View { .foregroundStyle(.tertiary) } } + IconButton( + symbol: jiggler.isOn ? "cup.and.saucer.fill" : "cup.and.saucer", + help: jigglerHelp, + tint: jiggler.isOn ? .accentColor : .primary + ) { jiggler.toggle() } IconButton(symbol: "arrow.clockwise", help: "Refresh now") { store.refresh() } } .padding(.horizontal, 12) @@ -216,6 +221,15 @@ private struct HeaderView: View { .padding(.bottom, 8) } + private var jiggler: Jiggler { .shared } + + private var jigglerHelp: String { + guard jiggler.isOn else { return "Keep awake: stop the Mac from sleeping and you from showing as away" } + return jiggler.canNudge + ? "Keeping awake. Click to stop" + : "Keeping awake, without the cursor nudge: allow devdash under Privacy & Security > Accessibility" + } + private var statusColor: Color { switch store.connection { case .connected: .green From 5b796417cd6803ea4de8f864806b46ddd96d0dbd Mon Sep 17 00:00:00 2001 From: Ensar Bavrk Date: Thu, 22 Oct 2026 14:10:43 +0200 Subject: [PATCH 3/4] Document keep awake --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d7a89a..e953a44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - **Separate Claude accounts**: `bin/claude-profile` runs Claude Code with the account that owns the current folder, from `~/.config/claude-profiles`, and its `guard` hook blocks prompts sent from the wrong account. - The Claude tab has an account switcher for projects, sessions, cost and plan limits, and running Claude sessions show which account they use. - **GitHub accounts per folder**: `claude-profile gh` runs `gh` as the GitHub login a profile names in a `github` line of `~/.config/claude-profiles`; the account switcher shows each profile's login, or a "Sign in to GitHub" button. +- **Keep awake**: a cup button in the menu bar app header stops the Mac from sleeping and, after 50 seconds idle, nudges the cursor one point and back so chat apps keep you active. The menu bar icon turns into a cup while it is on. The nudge needs Accessibility access. ## 0.3.0 From 0a36e4f3ccb8f963065001f2d0c6821255d77def Mon Sep 17 00:00:00 2001 From: Ensar Bavrk Date: Thu, 22 Oct 2026 15:42:05 +0200 Subject: [PATCH 4/4] Document keep awake in the README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e34fe15..d55bfd7 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ devdash shows all of it on one screen: - **Claude Code**: running sessions, projects and recent sessions. Resume any of them in a new Terminal window. - **Claude usage**: your plan limits (5-hour session and weekly) and what your usage would cost at API prices. - **Separate Claude accounts**: work and personal Claude Code accounts on one Mac, picked by the folder you start in. +- **Keep awake**: a mouse jiggler in the menu bar. Stops the Mac sleeping and keeps Teams and Slack from marking you away. - **System**: CPU, memory, swap, disk, network and the heaviest processes. ## Install @@ -62,7 +63,7 @@ Run it again to update. You can also run `devdash --update`, or use the prompt t open ~/Applications/DevDash.app ``` -The menu bar shows how many dev servers and containers are running, plus your current Claude session usage (for example `9 · 42%`). The icon switches to a flame when CPU or memory goes over your configured threshold. Click it to open the dashboard. +The menu bar shows how many dev servers and containers are running, plus your current Claude session usage (for example `9 · 42%`). The icon switches to a flame when CPU or memory goes over your configured threshold, and to a cup while keep awake is on. Click it to open the dashboard. | Running, by project | Project overview | System | |---|---|---| @@ -85,6 +86,7 @@ What you can do: - project pages with one-click launch: new session, continue, plan mode, resume picker, skip permissions, or a plain terminal in that folder. - **Cleanup**: listed under Needs you when devdash finds idle, orphaned or zombie processes, or containers running longer than your stale threshold. Pick which ones to remove and clean them up together. - **Footer**: Disk tree, dependency graph and activity heatmap. The `...` menu has JSON export, the terminal UI, What's new, Check for updates and Quit. +- **Keep awake**: the cup in the header. While it's on, the Mac doesn't sleep, and after 50 seconds without input devdash moves the cursor one point and back, too fast to see, so Teams and Slack keep you active. Moving the cursor needs Accessibility access, which macOS asks for the first time you turn it on; without it only the sleep prevention works. It stays on across restarts. More in [Mouse jiggler for Mac](https://bavrk.com/articles/mouse-jiggler-for-mac). - **Keyboard**: Cmd+1 to Cmd+3 switch tabs, Cmd+F filters, Esc clears the filter. - **Notifications**: a macOS notification when a dev server starts or exits, a container stops, or a watched port comes up. - **Settings**: launch at login, editor (VS Code or Cursor), whether the menu bar shows the count and usage percentage, notifications, and a custom `devdash` path.