Skip to content

Add launchd-based system scheduling and UI support for system-scheduled timers - #2

Open
holgerkrupp wants to merge 1 commit into
mainfrom
codex/add-system-integrated-execution-methods-g96o50
Open

Add launchd-based system scheduling and UI support for system-scheduled timers#2
holgerkrupp wants to merge 1 commit into
mainfrom
codex/add-system-integrated-execution-methods-g96o50

Conversation

@holgerkrupp

Copy link
Copy Markdown
Owner

Motivation

  • Allow repeating timers to run even when the app is not in the foreground by installing per-user LaunchAgent jobs so macOS launchd handles recurring launches.
  • Surface and reconcile the system-scheduled state in the model and UI so scheduled timers show accurate next-fire times and progress.
  • Fall back to the in-app Timer when launchd installation fails and support correct countdown visuals for system-scheduled timers.

Description

  • Add LaunchAgentScheduler utility with install, uninstall, isInstalled, label, and helper methods that write a plist into ~/Library/LaunchAgents and call launchctl to bootstrap/enable jobs.
  • Extend TimerItem with isSystemScheduleEnabled and systemScheduleStartDate, integrate LaunchAgentScheduler in startTimer/stopTimer, add reconcileSystemScheduleState, nextSystemFireDate, and update calcProgress to compute progress for system-scheduled timers without an in-app Timer.
  • Update views: call timer.reconcileSystemScheduleState() from TimerItemList, show a "Scheduled by macOS launchd" caption and pass repeats into CountdownView in TimerSummaryView, and add a clock ticker to advance nextFireDate for system-scheduled timers; change CountdownView to accept repeats and compute wrap-around remaining time via remainingTime(until:from:).
  • Add a short scheduling note to README.md describing the launchd behavior and when the app falls back to in-app timers.

Testing

  • No automated tests were executed for this change.

Codex Task

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 80e6b627ae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +90 to +93
guard let home = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first else {
throw SchedulerError.unsupportedHomeDirectory
}
return home.appendingPathComponent("LaunchAgents", isDirectory: true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the real per-user LaunchAgents directory

In sandboxed builds (the app entitlement enables App Sandbox), this resolves the user's Library to the app container rather than /Users/.../Library, so the plist is written under Containers/.../Data/Library/LaunchAgents. launchctl bootstrap can load that path for the current GUI session, but after logout/reboot launchd will not auto-load it from the real ~/Library/LaunchAgents; reconcileSystemScheduleState() will still see the file and mark the timer active, so repeating timers silently stop until manually restarted. Use the actual home LaunchAgents directory or re-bootstrap on reconciliation.

Useful? React with 👍 / 👎.

Comment thread AutoRun/TimerItem.swift
Comment on lines +166 to +168
print("launchd scheduling failed; falling back to in-app timer: \(error)")
isSystemScheduleEnabled = false
systemScheduleStartDate = nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clean up partial LaunchAgent installs before fallback

If launchctl fails after the plist has been written (or after bootstrap succeeded but enable failed), this catch only clears the flags and falls through to the in-app timer. The partially installed plist/job is left behind, so it can fire at next login or immediately in parallel with the fallback, causing duplicate or unrequested launches. Call uninstall/bootout before falling back.

Useful? React with 👍 / 👎.

@holgerkrupp holgerkrupp reopened this Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant