Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Start at login preference with a separate option to activate Caffeine after login.
- Preference to deactivate Caffeine when the screen locks.

### Changed

- Caffeine activates by default on normal app launches, while login-item launches stay inactive and quiet unless enabled explicitly.
- Improved Ukrainian translation.

### Fixed
Expand Down
5 changes: 4 additions & 1 deletion src/Caffeine/Classes/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, SPUStandardUserDriverDelegat

func applicationDidFinishLaunching(_: Notification) {
// Create the menu bar controller
self.menuBarController = MenuBarController(updaterController: self.updaterController)
self.menuBarController = MenuBarController(
updaterController: self.updaterController,
launchContext: LaunchContext.current
)

// Hide the dock icon - this is a menu bar only app
NSApp.setActivationPolicy(.accessory)
Expand Down
23 changes: 23 additions & 0 deletions src/Caffeine/Classes/Models/LaunchContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// LaunchContext.swift
// Caffeine
//

import AppKit
import Carbon

enum LaunchContext {
case standard
case loginItem

static var current: LaunchContext {
guard let event = NSAppleEventManager.shared().currentAppleEvent else {
return .standard
}

let launchedAsLoginItem = event.eventID == kAEOpenApplication &&
event.paramDescriptor(forKeyword: keyAEPropData)?.enumCodeValue == keyAELaunchedAsLogInItem

return launchedAsLoginItem ? .loginItem : .standard
}
}
42 changes: 42 additions & 0 deletions src/Caffeine/Classes/Models/LoginItemManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// LoginItemManager.swift
// Caffeine
//

import Combine
import DZFoundation
import Foundation
import ServiceManagement

@MainActor
final class LoginItemManager: ObservableObject {
static let shared = LoginItemManager()

@Published private(set) var startsAtLogin = false
@Published private(set) var requiresApproval = false

private init() {
self.refresh()
}

func refresh() {
let status = SMAppService.mainApp.status

self.startsAtLogin = status == .enabled || status == .requiresApproval
self.requiresApproval = status == .requiresApproval
}

func setStartsAtLogin(_ startsAtLogin: Bool) {
do {
if startsAtLogin, !self.startsAtLogin {
try SMAppService.mainApp.register()
} else if !startsAtLogin, self.startsAtLogin {
try SMAppService.mainApp.unregister()
}
} catch {
DZErrorLog(error)
}

self.refresh()
}
}
57 changes: 53 additions & 4 deletions src/Caffeine/Classes/ViewModels/CaffeineViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import SwiftUI
/// Main view model for the Caffeine application
@MainActor
class CaffeineViewModel: ObservableObject {
private static let screenIsLockedNotification = Notification.Name("com.apple.screenIsLocked")

// MARK: - Published Properties

@Published var isActive = false
Expand All @@ -26,20 +28,24 @@ class CaffeineViewModel: ObservableObject {

// MARK: - Initialization

init() {
init(launchContext: LaunchContext = .standard) {
self.registerDefaultPreferences()

// Explicitly ensure we start inactive
self.isActive = false
self.timeRemaining = nil

self.setupObservers()

// Check if we should activate at launch
if UserDefaults.standard.bool(forKey: PreferenceKeys.activateAtLaunch) {
if self.shouldActivateOnLaunch(launchContext: launchContext) {
self.activate()
}

// Show preferences on first launch
if !UserDefaults.standard.bool(forKey: PreferenceKeys.suppressLaunchMessage) {
// Show preferences on standard launches only, so login-item launches stay quiet.
if launchContext == .standard,
!UserDefaults.standard.bool(forKey: PreferenceKeys.suppressLaunchMessage)
{
self.showPreferences = true
}
}
Expand Down Expand Up @@ -182,6 +188,25 @@ class CaffeineViewModel: ObservableObject {
}
.store(in: &self.cancellables)

// Deactivate when the user session is locked or otherwise leaves the active console session.
NSWorkspace.shared.notificationCenter.publisher(for: NSWorkspace.sessionDidResignActiveNotification)
.sink { [weak self] _ in
Task { @MainActor in
self?.deactivateForScreenLockIfNeeded()
}
}
.store(in: &self.cancellables)

// Manual Lock Screen posts a distributed notification even when the workspace
// session notification is not delivered to this app.
DistributedNotificationCenter.default().publisher(for: Self.screenIsLockedNotification)
.sink { [weak self] _ in
Task { @MainActor in
self?.deactivateForScreenLockIfNeeded()
}
}
.store(in: &self.cancellables)

// Run-loop timers don't advance during sleep, so on wake check whether
// the activation period elapsed and deactivate if so
NSWorkspace.shared.notificationCenter.publisher(for: NSWorkspace.didWakeNotification)
Expand All @@ -202,14 +227,38 @@ class CaffeineViewModel: ObservableObject {
self.displayTimer?.invalidate()
self.displayTimer = nil
}

private func deactivateForScreenLockIfNeeded() {
guard UserDefaults.standard.bool(forKey: PreferenceKeys.deactivateOnScreenLock) else { return }
self.deactivate()
}

private func registerDefaultPreferences() {
UserDefaults.standard.register(defaults: [
PreferenceKeys.activateAtLaunch: true,
PreferenceKeys.activateAfterLogin: false,
PreferenceKeys.deactivateOnScreenLock: true,
])
}

private func shouldActivateOnLaunch(launchContext: LaunchContext) -> Bool {
switch launchContext {
case .standard:
UserDefaults.standard.bool(forKey: PreferenceKeys.activateAtLaunch)
case .loginItem:
UserDefaults.standard.bool(forKey: PreferenceKeys.activateAfterLogin)
}
}
}

// MARK: - Preference Keys

enum PreferenceKeys {
static let activateAtLaunch = "CAActivateAtLaunch"
static let activateAfterLogin = "CAActivateAfterLogin"
static let defaultDuration = "CADefaultDuration"
static let suppressLaunchMessage = "CASuppressLaunchMessage"
static let deactivateOnManualSleep = "CADeactivateOnManualSleep"
static let deactivateOnScreenLock = "CADeactivateOnScreenLock"
static let keepAppsActive = "CAKeepAppsActive"
}
4 changes: 2 additions & 2 deletions src/Caffeine/Classes/Views/MenuBarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ class MenuBarController: NSObject {
private var cancellables = Set<AnyCancellable>()
private let updaterController: SPUStandardUpdaterController

init(updaterController: SPUStandardUpdaterController) {
init(updaterController: SPUStandardUpdaterController, launchContext: LaunchContext) {
self.updaterController = updaterController
self.viewModel = CaffeineViewModel()
self.viewModel = CaffeineViewModel(launchContext: launchContext)
super.init()
self.setupMenuBar()
self.setupObservers()
Expand Down
26 changes: 26 additions & 0 deletions src/Caffeine/Classes/Views/PreferencesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import SwiftUI

struct PreferencesView: View {
@ObservedObject var viewModel: CaffeineViewModel
@ObservedObject private var loginItemManager = LoginItemManager.shared
@AppStorage(PreferenceKeys.defaultDuration) private var defaultDuration = 0
@AppStorage(PreferenceKeys.activateAtLaunch) private var activateAtLaunch = false
@AppStorage(PreferenceKeys.activateAfterLogin) private var activateAfterLogin = false
@AppStorage(PreferenceKeys.suppressLaunchMessage) private var suppressLaunchMessage = false
@AppStorage(PreferenceKeys.deactivateOnManualSleep) private var deactivateOnManualSleep = false
@AppStorage(PreferenceKeys.deactivateOnScreenLock) private var deactivateOnScreenLock = true
@AppStorage(PreferenceKeys.keepAppsActive) private var keepAppsActive = false

var body: some View {
Expand Down Expand Up @@ -70,9 +73,29 @@ struct PreferencesView: View {
Toggle("Activate when starting Caffeine", isOn: self.$activateAtLaunch)
.font(.system(size: 13))

Toggle("Start Caffeine when logging in", isOn: Binding(
get: { self.loginItemManager.startsAtLogin },
set: { self.loginItemManager.setStartsAtLogin($0) }
))
.font(.system(size: 13))

if self.loginItemManager.requiresApproval {
Text("Approve Caffeine in System Settings to allow it to start at login.")
.font(.system(size: 11))
.foregroundColor(.secondary)
.padding(.leading, 20)
}

Toggle("Activate when started at login", isOn: self.$activateAfterLogin)
.font(.system(size: 13))
.disabled(!self.loginItemManager.startsAtLogin)

Toggle("Deactivate when device goes to sleep manually", isOn: self.$deactivateOnManualSleep)
.font(.system(size: 13))

Toggle("Deactivate when screen locks", isOn: self.$deactivateOnScreenLock)
.font(.system(size: 13))

Toggle("Show this message when starting Caffeine", isOn: Binding(
get: { !self.suppressLaunchMessage },
set: { self.suppressLaunchMessage = !$0 }
Expand Down Expand Up @@ -121,6 +144,9 @@ struct PreferencesView: View {
.padding(.horizontal, 20)
.frame(width: 640)
.fixedSize(horizontal: false, vertical: true)
.onAppear {
self.loginItemManager.refresh()
}
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/Caffeine/Resources/de.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
/* Preferences labels */
"Default duration:" = "Standarddauer:";
"Activate when starting Caffeine" = "Caffeine beim Start aktivieren";
"Start Caffeine when logging in" = "Caffeine beim Anmelden starten";
"Approve Caffeine in System Settings to allow it to start at login." = "Erlaube Caffeine in den Systemeinstellungen, damit es beim Anmelden gestartet werden kann.";
"Activate when started at login" = "Beim Starten nach Anmeldung aktivieren";
"Deactivate when device goes to sleep manually" = "Beim manuellen Ruhezustand deaktivieren";
"Deactivate when screen locks" = "Beim Sperren des Bildschirms deaktivieren";
"Show this message when starting Caffeine" = "Diese Nachricht beim Start von Caffeine anzeigen";
"Keep apps active" = "Apps aktiv halten";
"Prevents apps from becoming inactive and the screen saver from starting." = "Verhindert, dass Apps inaktiv werden und der Bildschirmschoner startet.";
Expand All @@ -42,4 +46,3 @@

/* System messages */
"Caffeine prevents sleep" = "Caffeine verhindert den Ruhezustand";

5 changes: 4 additions & 1 deletion src/Caffeine/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
/* Preferences labels */
"Default duration:" = "Default duration:";
"Activate when starting Caffeine" = "Activate when starting Caffeine";
"Start Caffeine when logging in" = "Start Caffeine when logging in";
"Approve Caffeine in System Settings to allow it to start at login." = "Approve Caffeine in System Settings to allow it to start at login.";
"Activate when started at login" = "Activate when started at login";
"Deactivate when device goes to sleep manually" = "Deactivate when device goes to sleep manually";
"Deactivate when screen locks" = "Deactivate when screen locks";
"Show this message when starting Caffeine" = "Show this message when starting Caffeine";
"Keep apps active" = "Keep apps active";
"Prevents apps from becoming inactive and the screen saver from starting." = "Prevents apps from becoming inactive and the screen saver from starting.";
Expand All @@ -42,4 +46,3 @@

/* System messages */
"Caffeine prevents sleep" = "Caffeine prevents sleep";

5 changes: 4 additions & 1 deletion src/Caffeine/Resources/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
/* Preferences labels */
"Default duration:" = "Duración predeterminada:";
"Activate when starting Caffeine" = "Activar al iniciar Caffeine";
"Start Caffeine when logging in" = "Iniciar Caffeine al iniciar sesión";
"Approve Caffeine in System Settings to allow it to start at login." = "Aprueba Caffeine en Ajustes del Sistema para permitir que se inicie al iniciar sesión.";
"Activate when started at login" = "Activar al iniciarse tras iniciar sesión";
"Deactivate when device goes to sleep manually" = "Desactivar cuando el dispositivo entra en reposo manualmente";
"Deactivate when screen locks" = "Desactivar cuando se bloquea la pantalla";
"Show this message when starting Caffeine" = "Mostrar este mensaje al iniciar Caffeine";
"Keep apps active" = "Mantener apps activas";
"Prevents apps from becoming inactive and the screen saver from starting." = "Evita que las apps se vuelvan inactivas y que se inicie el salvapantallas.";
Expand All @@ -42,4 +46,3 @@

/* System messages */
"Caffeine prevents sleep" = "Caffeine impide el reposo";

5 changes: 4 additions & 1 deletion src/Caffeine/Resources/fr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
/* Preferences labels */
"Default duration:" = "Durée par défaut :";
"Activate when starting Caffeine" = "Activer au lancement de Caffeine";
"Start Caffeine when logging in" = "Lancer Caffeine à l’ouverture de session";
"Approve Caffeine in System Settings to allow it to start at login." = "Autorisez Caffeine dans Réglages Système pour lui permettre de se lancer à l’ouverture de session.";
"Activate when started at login" = "Activer au lancement à l’ouverture de session";
"Deactivate when device goes to sleep manually" = "Désactiver lorsque l’appareil se met en veille manuellement";
"Deactivate when screen locks" = "Désactiver lorsque l’écran se verrouille";
"Show this message when starting Caffeine" = "Afficher ce message au lancement de Caffeine";
"Keep apps active" = "Garder les apps actives";
"Prevents apps from becoming inactive and the screen saver from starting." = "Empêche les apps de devenir inactives et l'économiseur d'écran de démarrer.";
Expand All @@ -42,4 +46,3 @@

/* System messages */
"Caffeine prevents sleep" = "Caffeine empêche la mise en veille";

4 changes: 4 additions & 0 deletions src/Caffeine/Resources/it.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
/* Preferences labels */
"Default duration:" = "Durata predefinita:";
"Activate when starting Caffeine" = "Attiva all'avvio di Caffeine";
"Start Caffeine when logging in" = "Avvia Caffeine all'accesso";
"Approve Caffeine in System Settings to allow it to start at login." = "Autorizza Caffeine in Impostazioni di Sistema per consentirne l'avvio all'accesso.";
"Activate when started at login" = "Attiva all'avvio dopo l'accesso";
"Deactivate when device goes to sleep manually" = "Disattiva quando il dispositivo va in stop manualmente";
"Deactivate when screen locks" = "Disattiva quando lo schermo si blocca";
"Show this message when starting Caffeine" = "Mostra questo messaggio all'avvio di Caffeine";
"Keep apps active" = "Mantieni le app attive";
"Prevents apps from becoming inactive and the screen saver from starting." = "Impedisce alle app di diventare inattive e l'avvio del salvaschermo.";
Expand Down
5 changes: 4 additions & 1 deletion src/Caffeine/Resources/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
/* Preferences labels */
"Default duration:" = "デフォルトの時間:";
"Activate when starting Caffeine" = "Caffeine 起動時に有効化";
"Start Caffeine when logging in" = "ログイン時に Caffeine を起動";
"Approve Caffeine in System Settings to allow it to start at login." = "ログイン時に起動できるよう、システム設定で Caffeine を許可してください。";
"Activate when started at login" = "ログイン時の起動で有効にする";
"Deactivate when device goes to sleep manually" = "手動でスリープしたときに無効化";
"Deactivate when screen locks" = "画面ロック時に無効化";
"Show this message when starting Caffeine" = "Caffeine 起動時にこのメッセージを表示";
"Keep apps active" = "アプリをアクティブに保つ";
"Prevents apps from becoming inactive and the screen saver from starting." = "アプリが非アクティブになるのを防ぎ、スクリーンセーバーの起動を防止します。";
Expand All @@ -42,4 +46,3 @@

/* System messages */
"Caffeine prevents sleep" = "Caffeine がスリープを防ぎます";

4 changes: 4 additions & 0 deletions src/Caffeine/Resources/ko.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
/* Preferences labels */
"Default duration:" = "기본 지속 시간:";
"Activate when starting Caffeine" = "Caffeine 시작 시 활성화";
"Start Caffeine when logging in" = "로그인할 때 Caffeine 시작";
"Approve Caffeine in System Settings to allow it to start at login." = "로그인할 때 시작할 수 있도록 시스템 설정에서 Caffeine을 승인하세요.";
"Activate when started at login" = "로그인 시 시작할 때 활성화";
"Deactivate when device goes to sleep manually" = "수동으로 잠자기에 들어가면 비활성화";
"Deactivate when screen locks" = "화면이 잠기면 비활성화";
"Show this message when starting Caffeine" = "Caffeine 시작 시 이 메시지 표시";
"Keep apps active" = "앱 활성 상태 유지";
"Prevents apps from becoming inactive and the screen saver from starting." = "앱이 비활성 상태가 되거나 화면 보호기가 시작되는 것을 방지합니다.";
Expand Down
4 changes: 4 additions & 0 deletions src/Caffeine/Resources/nl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
/* Preferences labels */
"Default duration:" = "Standaardduur:";
"Activate when starting Caffeine" = "Caffeine bij starten inschakelen";
"Start Caffeine when logging in" = "Start Caffeine bij inloggen";
"Approve Caffeine in System Settings to allow it to start at login." = "Sta Caffeine toe in Systeeminstellingen om bij inloggen te starten.";
"Activate when started at login" = "Activeer wanneer gestart bij inloggen";
"Deactivate when device goes to sleep manually" = "Uitschakelen wanneer het apparaat handmatig in sluimerstand gaat";
"Deactivate when screen locks" = "Uitschakelen wanneer het scherm vergrendelt";
"Show this message when starting Caffeine" = "Dit bericht tonen bij het starten van Caffeine";
"Keep apps active" = "Apps actief houden";
"Prevents apps from becoming inactive and the screen saver from starting." = "Voorkomt dat apps inactief worden en dat de schermbeveiliging start.";
Expand Down
4 changes: 4 additions & 0 deletions src/Caffeine/Resources/pt-BR.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
/* Preferences labels */
"Default duration:" = "Duração padrão:";
"Activate when starting Caffeine" = "Ativar ao iniciar o Caffeine";
"Start Caffeine when logging in" = "Iniciar o Caffeine ao fazer login";
"Approve Caffeine in System Settings to allow it to start at login." = "Aprove o Caffeine nos Ajustes do Sistema para permitir que ele seja iniciado ao fazer login.";
"Activate when started at login" = "Ativar ao iniciar pelo login";
"Deactivate when device goes to sleep manually" = "Desativar quando o dispositivo entrar em repouso manualmente";
"Deactivate when screen locks" = "Desativar quando a tela for bloqueada";
"Show this message when starting Caffeine" = "Mostrar esta mensagem ao iniciar o Caffeine";
"Keep apps active" = "Manter apps ativos";
"Prevents apps from becoming inactive and the screen saver from starting." = "Impede que os apps fiquem inativos e que a proteção de tela seja iniciada.";
Expand Down
Loading