diff --git a/CHANGELOG.md b/CHANGELOG.md index f7e3713..49283ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Configurable global keyboard shortcut to toggle Caffeine from anywhere. Set it in Preferences. No default shortcut is provided to avoid conflicts with other apps. + ### Changed - Improved Ukrainian translation. diff --git a/src/Caffeine.xcodeproj/project.pbxproj b/src/Caffeine.xcodeproj/project.pbxproj index f46a778..c04e4d6 100644 --- a/src/Caffeine.xcodeproj/project.pbxproj +++ b/src/Caffeine.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + B3CF99362FC71C2A00929FE3 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = B3CF99352FC71C2A00929FE3 /* KeyboardShortcuts */; }; F23FEFAF2F9623DB00C445B2 /* DZFoundation in Embed Frameworks */ = {isa = PBXBuildFile; productRef = F2634EAA2F375A5200C56D92 /* DZFoundation */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; F26346032F27633300C56D92 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = F26346022F27633300C56D92 /* Sparkle */; }; F2634EAB2F375A5200C56D92 /* DZFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = F2634EAA2F375A5200C56D92 /* DZFoundation */; }; @@ -58,6 +59,7 @@ files = ( F26346032F27633300C56D92 /* Sparkle in Frameworks */, F2634EAB2F375A5200C56D92 /* DZFoundation in Frameworks */, + B3CF99362FC71C2A00929FE3 /* KeyboardShortcuts in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -103,6 +105,7 @@ packageProductDependencies = ( F26346022F27633300C56D92 /* Sparkle */, F2634EAA2F375A5200C56D92 /* DZFoundation */, + B3CF99352FC71C2A00929FE3 /* KeyboardShortcuts */, ); productName = Caffeine; productReference = F2F53BA42EC354AE008A08B6 /* Caffeine.app */; @@ -147,6 +150,7 @@ packageReferences = ( F26346012F27633300C56D92 /* XCRemoteSwiftPackageReference "Sparkle" */, F2634EA92F375A5200C56D92 /* XCRemoteSwiftPackageReference "DZFoundation" */, + B3CF99342FC71C2A00929FE3 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */, ); preferredProjectObjectVersion = 77; productRefGroup = F2F53BA52EC354AE008A08B6 /* Products */; @@ -393,6 +397,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + B3CF99342FC71C2A00929FE3 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; + }; + }; F26346012F27633300C56D92 /* XCRemoteSwiftPackageReference "Sparkle" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sparkle-project/Sparkle"; @@ -412,6 +424,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + B3CF99352FC71C2A00929FE3 /* KeyboardShortcuts */ = { + isa = XCSwiftPackageProductDependency; + package = B3CF99342FC71C2A00929FE3 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; + productName = KeyboardShortcuts; + }; F26346022F27633300C56D92 /* Sparkle */ = { isa = XCSwiftPackageProductDependency; package = F26346012F27633300C56D92 /* XCRemoteSwiftPackageReference "Sparkle" */; diff --git a/src/Caffeine.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/src/Caffeine.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index bb304fe..3ab4ec2 100644 --- a/src/Caffeine.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/src/Caffeine.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "f3ae84e51710b8b303c278df4d30705024d51db8c0218ec2c5db8a05e7e50baa", + "originHash" : "8f1242e76b4b50cce206f270a8860d40e4d8984c4fbf7790e49756bb89ce38ca", "pins" : [ { "identity" : "dzfoundation", @@ -10,6 +10,15 @@ "version" : "1.0.0" } }, + { + "identity" : "keyboardshortcuts", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sindresorhus/KeyboardShortcuts", + "state" : { + "revision" : "1aef85578fdd4f9eaeeb8d53b7b4fc31bf08fe27", + "version" : "2.4.0" + } + }, { "identity" : "sparkle", "kind" : "remoteSourceControl", diff --git a/src/Caffeine/Classes/AppDelegate.swift b/src/Caffeine/Classes/AppDelegate.swift index 5a9ca3f..97bc5e9 100644 --- a/src/Caffeine/Classes/AppDelegate.swift +++ b/src/Caffeine/Classes/AppDelegate.swift @@ -6,6 +6,8 @@ // import Cocoa +import DZFoundation +import KeyboardShortcuts import Sparkle import SwiftUI @@ -25,6 +27,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, SPUStandardUserDriverDelegat // Hide the dock icon - this is a menu bar only app NSApp.setActivationPolicy(.accessory) + + self.registerGlobalShortcut() + } + + private func registerGlobalShortcut() { + KeyboardShortcuts.onKeyUp(for: .toggleActive) { [weak self] in + Task { @MainActor in + self?.menuBarController?.toggleActive() + } + } + DZLog("Registered global toggle shortcut") } func applicationWillTerminate(_: Notification) { diff --git a/src/Caffeine/Classes/Models/KeyboardShortcutsName+Caffeine.swift b/src/Caffeine/Classes/Models/KeyboardShortcutsName+Caffeine.swift new file mode 100644 index 0000000..3e6a22d --- /dev/null +++ b/src/Caffeine/Classes/Models/KeyboardShortcutsName+Caffeine.swift @@ -0,0 +1,10 @@ +// +// KeyboardShortcutsName+Caffeine.swift +// Caffeine +// + +import KeyboardShortcuts + +extension KeyboardShortcuts.Name { + static let toggleActive = Self("toggleActive") +} diff --git a/src/Caffeine/Classes/Views/MenuBarController.swift b/src/Caffeine/Classes/Views/MenuBarController.swift index 8747766..bad6865 100644 --- a/src/Caffeine/Classes/Views/MenuBarController.swift +++ b/src/Caffeine/Classes/Views/MenuBarController.swift @@ -7,6 +7,7 @@ import Cocoa import Combine +import KeyboardShortcuts import Sparkle import SwiftUI @@ -37,6 +38,10 @@ class MenuBarController: NSObject { } } + func toggleActive() { + self.viewModel.toggleActive() + } + private func setupMenuBar() { self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) @@ -90,6 +95,22 @@ class MenuBarController: NSObject { private func showContextMenu() { let menu = NSMenu() + // Toggle activate/deactivate + let toggleTitle = self.viewModel.isActive + ? String(localized: "Deactivate") + : String(localized: "Activate") + let toggleItem = NSMenuItem( + title: toggleTitle, + action: #selector(self.toggleActiveFromMenu(_:)), + keyEquivalent: "" + ) + toggleItem.target = self + // Informational only: NSMenu key equivalents fire only while the menu is open. + // Real global triggering is via the KeyboardShortcuts listener in AppDelegate. + toggleItem.setShortcut(for: .toggleActive) + menu.addItem(toggleItem) + menu.addItem(NSMenuItem.separator()) + // Status info (only show if active) if self.viewModel.isActive, let timeString = viewModel.formattedTimeRemaining() { let infoItem = NSMenuItem(title: timeString, action: nil, keyEquivalent: "") @@ -187,6 +208,11 @@ class MenuBarController: NSObject { self.viewModel.activate(withTimeout: seconds) } + @objc + private func toggleActiveFromMenu(_: NSMenuItem) { + self.viewModel.toggleActive() + } + @objc private func showPreferences(_: Any?) { self.showPreferencesWindow() diff --git a/src/Caffeine/Classes/Views/PreferencesView.swift b/src/Caffeine/Classes/Views/PreferencesView.swift index 9b9c2d1..80df765 100644 --- a/src/Caffeine/Classes/Views/PreferencesView.swift +++ b/src/Caffeine/Classes/Views/PreferencesView.swift @@ -5,6 +5,7 @@ // Created by Dominic Rodemer on 11.11.25. // +import KeyboardShortcuts import SwiftUI struct PreferencesView: View { @@ -97,6 +98,28 @@ struct PreferencesView: View { .padding(.leading, 20) } + Divider() + .padding(.vertical, 8) + + VStack(alignment: .leading, spacing: 8) { + Text("Keyboard Shortcut") + .font(.system(size: 13, weight: .semibold)) + + HStack(spacing: 8) { + Text("Toggle Caffeine:") + .font(.system(size: 13)) + KeyboardShortcuts.Recorder(for: .toggleActive) + Spacer() + } + + Text( + "Press a key combination to toggle Caffeine from anywhere. The shortcut is registered system-wide. Click the field and press Delete to clear." + ) + .font(.system(size: 11)) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + Spacer() .frame(height: 30) diff --git a/src/Caffeine/Resources/de.lproj/Localizable.strings b/src/Caffeine/Resources/de.lproj/Localizable.strings index fddf93f..c4a7150 100644 --- a/src/Caffeine/Resources/de.lproj/Localizable.strings +++ b/src/Caffeine/Resources/de.lproj/Localizable.strings @@ -43,3 +43,11 @@ /* System messages */ "Caffeine prevents sleep" = "Caffeine verhindert den Ruhezustand"; +/* Keyboard shortcut */ +"Keyboard Shortcut" = "Tastaturkurzbefehl"; +"Toggle Caffeine:" = "Caffeine umschalten:"; +"Press a key combination to toggle Caffeine from anywhere. The shortcut is registered system-wide. Click the field and press Delete to clear." = "Drücke eine Tastenkombination, um Caffeine von überall aus umzuschalten. Der Kurzbefehl wird systemweit registriert. Klicke in das Feld und drücke die Entf-Taste, um ihn zu löschen."; + +/* Context menu activation */ +"Activate" = "Aktivieren"; +"Deactivate" = "Deaktivieren"; diff --git a/src/Caffeine/Resources/en.lproj/Localizable.strings b/src/Caffeine/Resources/en.lproj/Localizable.strings index 66e5e4c..3669c00 100644 --- a/src/Caffeine/Resources/en.lproj/Localizable.strings +++ b/src/Caffeine/Resources/en.lproj/Localizable.strings @@ -43,3 +43,11 @@ /* System messages */ "Caffeine prevents sleep" = "Caffeine prevents sleep"; +/* Keyboard shortcut */ +"Keyboard Shortcut" = "Keyboard Shortcut"; +"Toggle Caffeine:" = "Toggle Caffeine:"; +"Press a key combination to toggle Caffeine from anywhere. The shortcut is registered system-wide. Click the field and press Delete to clear." = "Press a key combination to toggle Caffeine from anywhere. The shortcut is registered system-wide. Click the field and press Delete to clear."; + +/* Context menu activation */ +"Activate" = "Activate"; +"Deactivate" = "Deactivate"; diff --git a/src/Caffeine/Resources/es.lproj/Localizable.strings b/src/Caffeine/Resources/es.lproj/Localizable.strings index e708b41..7b12d8b 100644 --- a/src/Caffeine/Resources/es.lproj/Localizable.strings +++ b/src/Caffeine/Resources/es.lproj/Localizable.strings @@ -43,3 +43,11 @@ /* System messages */ "Caffeine prevents sleep" = "Caffeine impide el reposo"; +/* Keyboard shortcut */ +"Keyboard Shortcut" = "Atajo de teclado"; +"Toggle Caffeine:" = "Alternar Caffeine:"; +"Press a key combination to toggle Caffeine from anywhere. The shortcut is registered system-wide. Click the field and press Delete to clear." = "Pulsa una combinación de teclas para alternar Caffeine desde cualquier lugar. El atajo se registra en todo el sistema. Haz clic en el campo y pulsa Suprimir para borrarlo."; + +/* Context menu activation */ +"Activate" = "Activar"; +"Deactivate" = "Desactivar"; diff --git a/src/Caffeine/Resources/fr.lproj/Localizable.strings b/src/Caffeine/Resources/fr.lproj/Localizable.strings index e1388d1..9e521cb 100644 --- a/src/Caffeine/Resources/fr.lproj/Localizable.strings +++ b/src/Caffeine/Resources/fr.lproj/Localizable.strings @@ -43,3 +43,11 @@ /* System messages */ "Caffeine prevents sleep" = "Caffeine empêche la mise en veille"; +/* Keyboard shortcut */ +"Keyboard Shortcut" = "Raccourci clavier"; +"Toggle Caffeine:" = "Activer/désactiver Caffeine :"; +"Press a key combination to toggle Caffeine from anywhere. The shortcut is registered system-wide. Click the field and press Delete to clear." = "Appuyez sur une combinaison de touches pour activer ou désactiver Caffeine depuis n'importe où. Le raccourci est enregistré à l'échelle du système. Cliquez sur le champ et appuyez sur Suppr pour l'effacer."; + +/* Context menu activation */ +"Activate" = "Activer"; +"Deactivate" = "Désactiver"; diff --git a/src/Caffeine/Resources/it.lproj/Localizable.strings b/src/Caffeine/Resources/it.lproj/Localizable.strings index 69891b9..0ec84ff 100644 --- a/src/Caffeine/Resources/it.lproj/Localizable.strings +++ b/src/Caffeine/Resources/it.lproj/Localizable.strings @@ -42,3 +42,11 @@ /* System messages */ "Caffeine prevents sleep" = "Caffeine impedisce lo stop"; +/* Keyboard shortcut */ +"Keyboard Shortcut" = "Abbreviazione da tastiera"; +"Toggle Caffeine:" = "Attiva/disattiva Caffeine:"; +"Press a key combination to toggle Caffeine from anywhere. The shortcut is registered system-wide. Click the field and press Delete to clear." = "Premi una combinazione di tasti per attivare o disattivare Caffeine da qualsiasi punto. L'abbreviazione viene registrata a livello di sistema. Fai clic sul campo e premi Cancella per cancellarla."; + +/* Context menu activation */ +"Activate" = "Attiva"; +"Deactivate" = "Disattiva"; diff --git a/src/Caffeine/Resources/ja.lproj/Localizable.strings b/src/Caffeine/Resources/ja.lproj/Localizable.strings index c020ed0..5c20c7d 100644 --- a/src/Caffeine/Resources/ja.lproj/Localizable.strings +++ b/src/Caffeine/Resources/ja.lproj/Localizable.strings @@ -43,3 +43,11 @@ /* System messages */ "Caffeine prevents sleep" = "Caffeine がスリープを防ぎます"; +/* Keyboard shortcut */ +"Keyboard Shortcut" = "キーボードショートカット"; +"Toggle Caffeine:" = "Caffeine の切り替え:"; +"Press a key combination to toggle Caffeine from anywhere. The shortcut is registered system-wide. Click the field and press Delete to clear." = "キーの組み合わせを押すと、どこからでも Caffeine を切り替えられます。ショートカットはシステム全体に登録されます。フィールドをクリックして Delete キーを押すと消去できます。"; + +/* Context menu activation */ +"Activate" = "有効化"; +"Deactivate" = "無効化"; diff --git a/src/Caffeine/Resources/ko.lproj/Localizable.strings b/src/Caffeine/Resources/ko.lproj/Localizable.strings index f05d37e..6c876c5 100644 --- a/src/Caffeine/Resources/ko.lproj/Localizable.strings +++ b/src/Caffeine/Resources/ko.lproj/Localizable.strings @@ -42,3 +42,11 @@ /* System messages */ "Caffeine prevents sleep" = "Caffeine이 잠자기를 방지합니다"; +/* Keyboard shortcut */ +"Keyboard Shortcut" = "키보드 단축키"; +"Toggle Caffeine:" = "Caffeine 전환:"; +"Press a key combination to toggle Caffeine from anywhere. The shortcut is registered system-wide. Click the field and press Delete to clear." = "키 조합을 누르면 어디서든 Caffeine을 전환할 수 있습니다. 단축키는 시스템 전체에 등록됩니다. 필드를 클릭한 후 Delete 키를 누르면 지워집니다."; + +/* Context menu activation */ +"Activate" = "활성화"; +"Deactivate" = "비활성화"; diff --git a/src/Caffeine/Resources/nl.lproj/Localizable.strings b/src/Caffeine/Resources/nl.lproj/Localizable.strings index ce3caa7..f164d6c 100644 --- a/src/Caffeine/Resources/nl.lproj/Localizable.strings +++ b/src/Caffeine/Resources/nl.lproj/Localizable.strings @@ -42,3 +42,11 @@ /* System messages */ "Caffeine prevents sleep" = "Caffeine voorkomt sluimerstand"; +/* Keyboard shortcut */ +"Keyboard Shortcut" = "Toetscombinatie"; +"Toggle Caffeine:" = "Caffeine in- of uitschakelen:"; +"Press a key combination to toggle Caffeine from anywhere. The shortcut is registered system-wide. Click the field and press Delete to clear." = "Druk op een toetscombinatie om Caffeine vanaf elke plek in of uit te schakelen. De toetscombinatie wordt systeembreed geregistreerd. Klik op het veld en druk op Delete om hem te wissen."; + +/* Context menu activation */ +"Activate" = "Inschakelen"; +"Deactivate" = "Uitschakelen"; diff --git a/src/Caffeine/Resources/pt-BR.lproj/Localizable.strings b/src/Caffeine/Resources/pt-BR.lproj/Localizable.strings index f9764e8..b849b2b 100644 --- a/src/Caffeine/Resources/pt-BR.lproj/Localizable.strings +++ b/src/Caffeine/Resources/pt-BR.lproj/Localizable.strings @@ -42,3 +42,11 @@ /* System messages */ "Caffeine prevents sleep" = "O Caffeine impede o repouso"; +/* Keyboard shortcut */ +"Keyboard Shortcut" = "Atalho de teclado"; +"Toggle Caffeine:" = "Alternar o Caffeine:"; +"Press a key combination to toggle Caffeine from anywhere. The shortcut is registered system-wide. Click the field and press Delete to clear." = "Pressione uma combinação de teclas para alternar o Caffeine de qualquer lugar. O atalho é registrado em todo o sistema. Clique no campo e pressione a tecla Delete para apagá-lo."; + +/* Context menu activation */ +"Activate" = "Ativar"; +"Deactivate" = "Desativar"; diff --git a/src/Caffeine/Resources/pt.lproj/Localizable.strings b/src/Caffeine/Resources/pt.lproj/Localizable.strings index 53c6852..9ce67d9 100644 --- a/src/Caffeine/Resources/pt.lproj/Localizable.strings +++ b/src/Caffeine/Resources/pt.lproj/Localizable.strings @@ -42,3 +42,11 @@ /* System messages */ "Caffeine prevents sleep" = "O Caffeine impede o repouso"; +/* Keyboard shortcut */ +"Keyboard Shortcut" = "Atalho de teclado"; +"Toggle Caffeine:" = "Alternar o Caffeine:"; +"Press a key combination to toggle Caffeine from anywhere. The shortcut is registered system-wide. Click the field and press Delete to clear." = "Prima uma combinação de teclas para alternar o Caffeine a partir de qualquer lado. O atalho é registado em todo o sistema. Clique no campo e prima a tecla Apagar para o limpar."; + +/* Context menu activation */ +"Activate" = "Ativar"; +"Deactivate" = "Desativar"; diff --git a/src/Caffeine/Resources/ru.lproj/Localizable.strings b/src/Caffeine/Resources/ru.lproj/Localizable.strings index a83a207..251f3e9 100644 --- a/src/Caffeine/Resources/ru.lproj/Localizable.strings +++ b/src/Caffeine/Resources/ru.lproj/Localizable.strings @@ -42,3 +42,11 @@ /* System messages */ "Caffeine prevents sleep" = "Caffeine предотвращает переход в сон"; +/* Keyboard shortcut */ +"Keyboard Shortcut" = "Сочетание клавиш"; +"Toggle Caffeine:" = "Переключение Caffeine:"; +"Press a key combination to toggle Caffeine from anywhere. The shortcut is registered system-wide. Click the field and press Delete to clear." = "Нажмите сочетание клавиш, чтобы переключать Caffeine из любого приложения. Сочетание регистрируется во всей системе. Щёлкните поле и нажмите Delete, чтобы очистить."; + +/* Context menu activation */ +"Activate" = "Активировать"; +"Deactivate" = "Деактивировать"; diff --git a/src/Caffeine/Resources/uk.lproj/Localizable.strings b/src/Caffeine/Resources/uk.lproj/Localizable.strings index b1cd894..6d1b028 100644 --- a/src/Caffeine/Resources/uk.lproj/Localizable.strings +++ b/src/Caffeine/Resources/uk.lproj/Localizable.strings @@ -43,3 +43,11 @@ /* System messages */ "Caffeine prevents sleep" = "Caffeine запобігає переходу в режим сну"; +/* Keyboard shortcut */ +"Keyboard Shortcut" = "Сполучення клавіш"; +"Toggle Caffeine:" = "Перемикання Caffeine:"; +"Press a key combination to toggle Caffeine from anywhere. The shortcut is registered system-wide. Click the field and press Delete to clear." = "Натисніть сполучення клавіш, щоб перемикати Caffeine з будь-якого застосунку. Сполучення реєструється в усій системі. Клацніть поле та натисніть Delete, щоб очистити."; + +/* Context menu activation */ +"Activate" = "Увімкнути"; +"Deactivate" = "Вимкнути"; diff --git a/src/Caffeine/Resources/zh-Hans.lproj/Localizable.strings b/src/Caffeine/Resources/zh-Hans.lproj/Localizable.strings index 768aca3..0bcb64d 100644 --- a/src/Caffeine/Resources/zh-Hans.lproj/Localizable.strings +++ b/src/Caffeine/Resources/zh-Hans.lproj/Localizable.strings @@ -43,3 +43,11 @@ /* System messages */ "Caffeine prevents sleep" = "Caffeine 会阻止睡眠"; +/* Keyboard shortcut */ +"Keyboard Shortcut" = "键盘快捷键"; +"Toggle Caffeine:" = "切换 Caffeine:"; +"Press a key combination to toggle Caffeine from anywhere. The shortcut is registered system-wide. Click the field and press Delete to clear." = "按下一组按键即可从任何位置切换 Caffeine。该快捷键将在系统范围内注册。点按字段并按 Delete 键即可清除。"; + +/* Context menu activation */ +"Activate" = "激活"; +"Deactivate" = "停用";