├── .gitignore ├── LICENSE ├── MenubarCountdown.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ └── MenubarCountdown.xcscheme └── xcuserdata │ └── kdj.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── MenubarCountdown ├── AppDelegate.swift ├── AppDelegate_Scripting.swift ├── AppUserDefaults.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── HourglassIcon.imageset │ │ ├── Contents.json │ │ ├── HourglassIconBlack.png │ │ └── hourglass_36x36.png ├── CALayerExtensions.swift ├── Credits.rtf ├── Info.plist ├── Log.swift ├── MainMenu.xib ├── MenuTimerIcon.icns ├── MenubarCountdown.entitlements ├── MenubarCountdown.sdef ├── NSApplication_Scripting.swift ├── Notifications.swift ├── ServicesProvider.swift ├── Speech.swift ├── StartTimerDialog.xib ├── StartTimerDialogController.swift ├── Stopwatch.swift ├── StringExtensions.swift ├── TextField.swift ├── TimerExpiredAlert.xib ├── TimerExpiredAlertController.swift └── UserDefaults.plist ├── MenubarCountdownTests ├── Info.plist └── MenubarCountdownTests.swift ├── MenubarCountdownUITests ├── Info.plist └── MenubarCountdownUITests.swift ├── README.md ├── Scripts ├── AppleScript │ ├── Menubar Countdown System Events Example.applescript │ ├── Pause Timer.applescript │ ├── README.md │ ├── Resume Timer.applescript │ ├── Start 15 Minute Timer.applescript │ ├── Start One Hour Timer.applescript │ ├── Start Pomodoro Timer.applescript │ ├── Stop Timer.applescript │ ├── Test All Settings.applescript │ ├── Test Menubar Countdown Commands.applescript │ └── Test Standard Suite.applescript ├── JavaScript │ ├── README.md │ ├── TestAllSettings.scpt │ ├── TestMenubarCountdownCommands.scpt │ └── TestStandardSuite.scpt ├── README.md └── Swift │ ├── README.md │ ├── pause_menubar_countdown.swift │ ├── resume_menubar_countdown.swift │ ├── start_menubar_countdown.swift │ └── stop_menubar_countdown.swift └── docs ├── .vscode └── tasks.json ├── Makefile ├── MenubarCountdownSettings.png ├── states.gv └── states.png /.gitignore: -------------------------------------------------------------------------------- 1 | CVS 2 | .#* 3 | 4 | .hg 5 | .hgignore 6 | 7 | .svn 8 | 9 | bin 10 | .bin 11 | Bin 12 | obj 13 | Debug 14 | Debug Dist 15 | Release 16 | Release Dist 17 | TestResults 18 | *.obj 19 | *.suo 20 | *.ncb 21 | *.aps 22 | *.user 23 | *.tli 24 | *.tlh 25 | *.idb 26 | *.pdb 27 | *.tlb 28 | *_i.h 29 | *_h.h 30 | *_i.c 31 | *_p.c 32 | *idl.h 33 | dlldata.c 34 | *ps.dll 35 | *ps.exp 36 | *ps.lib 37 | *.sdf 38 | *.opensdf 39 | ipch 40 | PrecompiledWeb 41 | 42 | build 43 | *.pbxuser 44 | *.perspectivev3 45 | .DS_Store 46 | xcuserdata 47 | *.sublime-project 48 | *.sublime-workspace 49 | 50 | .idea/workspace.xml 51 | .idea/tasks.xml 52 | 53 | *.old 54 | *.log 55 | *.out 56 | *.cache 57 | *.orig 58 | logs 59 | 60 | gen 61 | .metadata 62 | local.properties 63 | 64 | *~ 65 | 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2009,2015,2019,2020 Kristopher Johnson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MenubarCountdown.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4E48658E1BEE6CE500C159BF /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E48658D1BEE6CE500C159BF /* Log.swift */; }; 11 | 4E4865941BEE705E00C159BF /* UserDefaults.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4E4865931BEE705E00C159BF /* UserDefaults.plist */; }; 12 | 4E4865981BEE749E00C159BF /* StartTimerDialogController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4865971BEE749E00C159BF /* StartTimerDialogController.swift */; }; 13 | 4E48659A1BEE79EA00C159BF /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4865991BEE79EA00C159BF /* TextField.swift */; }; 14 | 4E48659C1BEE81C200C159BF /* TimerExpiredAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E48659B1BEE81C200C159BF /* TimerExpiredAlertController.swift */; }; 15 | 4E4865A91BEFA07300C159BF /* MenuTimerIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 4E4865A81BEFA07300C159BF /* MenuTimerIcon.icns */; }; 16 | 4E4865AD1BEFA12A00C159BF /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 4E4865AC1BEFA12A00C159BF /* Credits.rtf */; }; 17 | 4E4865AF1BEFA2B900C159BF /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4865AE1BEFA2B900C159BF /* StringExtensions.swift */; }; 18 | 4E4865B31BEFC63700C159BF /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4E4865B01BEFC63700C159BF /* MainMenu.xib */; }; 19 | 4E4865B41BEFC63700C159BF /* StartTimerDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4E4865B11BEFC63700C159BF /* StartTimerDialog.xib */; }; 20 | 4E4865B51BEFC63700C159BF /* TimerExpiredAlert.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4E4865B21BEFC63700C159BF /* TimerExpiredAlert.xib */; }; 21 | 4E532E932376F92F00A9CFE5 /* NSApplication_Scripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E532E922376F92F00A9CFE5 /* NSApplication_Scripting.swift */; }; 22 | 4E70D4D72390087C004DE5D8 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E70D4D62390087C004DE5D8 /* Notifications.swift */; }; 23 | 4E70D4D923900F55004DE5D8 /* Speech.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E70D4D823900F55004DE5D8 /* Speech.swift */; }; 24 | 4EB204231BED89F900D83EF3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB204221BED89F900D83EF3 /* AppDelegate.swift */; }; 25 | 4EB204271BED89F900D83EF3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4EB204261BED89F900D83EF3 /* Assets.xcassets */; }; 26 | 4EB204351BED89F900D83EF3 /* MenubarCountdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB204341BED89F900D83EF3 /* MenubarCountdownTests.swift */; }; 27 | 4EB204401BED89F900D83EF3 /* MenubarCountdownUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB2043F1BED89F900D83EF3 /* MenubarCountdownUITests.swift */; }; 28 | 4EB2044E1BED908300D83EF3 /* Stopwatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB2044D1BED908300D83EF3 /* Stopwatch.swift */; }; 29 | 4EB204501BEE29D700D83EF3 /* AppUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB2044F1BEE29D700D83EF3 /* AppUserDefaults.swift */; }; 30 | 4EB204541BEE307900D83EF3 /* CALayerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB204531BEE307900D83EF3 /* CALayerExtensions.swift */; }; 31 | 4EB58AAF236B881D00150BA5 /* ServicesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB58AAE236B881D00150BA5 /* ServicesProvider.swift */; }; 32 | 4EB58ABA237449B600150BA5 /* MenubarCountdown.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 4EB58AB9237449B600150BA5 /* MenubarCountdown.sdef */; }; 33 | 4EB58AC02374F6D200150BA5 /* AppDelegate_Scripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB58ABF2374F6D200150BA5 /* AppDelegate_Scripting.swift */; }; 34 | /* End PBXBuildFile section */ 35 | 36 | /* Begin PBXContainerItemProxy section */ 37 | 4EB204311BED89F900D83EF3 /* PBXContainerItemProxy */ = { 38 | isa = PBXContainerItemProxy; 39 | containerPortal = 4EB204171BED89F900D83EF3 /* Project object */; 40 | proxyType = 1; 41 | remoteGlobalIDString = 4EB2041E1BED89F900D83EF3; 42 | remoteInfo = MenubarCountdown; 43 | }; 44 | 4EB2043C1BED89F900D83EF3 /* PBXContainerItemProxy */ = { 45 | isa = PBXContainerItemProxy; 46 | containerPortal = 4EB204171BED89F900D83EF3 /* Project object */; 47 | proxyType = 1; 48 | remoteGlobalIDString = 4EB2041E1BED89F900D83EF3; 49 | remoteInfo = MenubarCountdown; 50 | }; 51 | /* End PBXContainerItemProxy section */ 52 | 53 | /* Begin PBXFileReference section */ 54 | 4E48658D1BEE6CE500C159BF /* Log.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; 55 | 4E4865931BEE705E00C159BF /* UserDefaults.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = UserDefaults.plist; sourceTree = ""; }; 56 | 4E4865971BEE749E00C159BF /* StartTimerDialogController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartTimerDialogController.swift; sourceTree = ""; }; 57 | 4E4865991BEE79EA00C159BF /* TextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; 58 | 4E48659B1BEE81C200C159BF /* TimerExpiredAlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimerExpiredAlertController.swift; sourceTree = ""; }; 59 | 4E4865A81BEFA07300C159BF /* MenuTimerIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = MenuTimerIcon.icns; sourceTree = ""; }; 60 | 4E4865AC1BEFA12A00C159BF /* Credits.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 61 | 4E4865AE1BEFA2B900C159BF /* StringExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; 62 | 4E4865B01BEFC63700C159BF /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; 63 | 4E4865B11BEFC63700C159BF /* StartTimerDialog.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = StartTimerDialog.xib; sourceTree = ""; }; 64 | 4E4865B21BEFC63700C159BF /* TimerExpiredAlert.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TimerExpiredAlert.xib; sourceTree = ""; }; 65 | 4E4D0E552367E5D9004B1404 /* MenubarCountdown.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MenubarCountdown.entitlements; sourceTree = ""; }; 66 | 4E532E922376F92F00A9CFE5 /* NSApplication_Scripting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSApplication_Scripting.swift; sourceTree = ""; }; 67 | 4E70D4D62390087C004DE5D8 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; 68 | 4E70D4D823900F55004DE5D8 /* Speech.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Speech.swift; sourceTree = ""; }; 69 | 4EB2041F1BED89F900D83EF3 /* Menubar Countdown.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Menubar Countdown.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 70 | 4EB204221BED89F900D83EF3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 71 | 4EB204261BED89F900D83EF3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 72 | 4EB2042B1BED89F900D83EF3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 73 | 4EB204301BED89F900D83EF3 /* MenubarCountdownTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MenubarCountdownTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 74 | 4EB204341BED89F900D83EF3 /* MenubarCountdownTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenubarCountdownTests.swift; sourceTree = ""; }; 75 | 4EB204361BED89F900D83EF3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 76 | 4EB2043B1BED89F900D83EF3 /* MenubarCountdownUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MenubarCountdownUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 77 | 4EB2043F1BED89F900D83EF3 /* MenubarCountdownUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenubarCountdownUITests.swift; sourceTree = ""; }; 78 | 4EB204411BED89F900D83EF3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 79 | 4EB2044D1BED908300D83EF3 /* Stopwatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stopwatch.swift; sourceTree = ""; }; 80 | 4EB2044F1BEE29D700D83EF3 /* AppUserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppUserDefaults.swift; sourceTree = ""; }; 81 | 4EB204531BEE307900D83EF3 /* CALayerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CALayerExtensions.swift; sourceTree = ""; }; 82 | 4EB58AAE236B881D00150BA5 /* ServicesProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesProvider.swift; sourceTree = ""; }; 83 | 4EB58AB8236DC3B300150BA5 /* Scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Scripts; sourceTree = ""; }; 84 | 4EB58AB9237449B600150BA5 /* MenubarCountdown.sdef */ = {isa = PBXFileReference; lastKnownFileType = text; path = MenubarCountdown.sdef; sourceTree = ""; }; 85 | 4EB58ABF2374F6D200150BA5 /* AppDelegate_Scripting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate_Scripting.swift; sourceTree = ""; }; 86 | /* End PBXFileReference section */ 87 | 88 | /* Begin PBXFrameworksBuildPhase section */ 89 | 4EB2041C1BED89F900D83EF3 /* Frameworks */ = { 90 | isa = PBXFrameworksBuildPhase; 91 | buildActionMask = 2147483647; 92 | files = ( 93 | ); 94 | runOnlyForDeploymentPostprocessing = 0; 95 | }; 96 | 4EB2042D1BED89F900D83EF3 /* Frameworks */ = { 97 | isa = PBXFrameworksBuildPhase; 98 | buildActionMask = 2147483647; 99 | files = ( 100 | ); 101 | runOnlyForDeploymentPostprocessing = 0; 102 | }; 103 | 4EB204381BED89F900D83EF3 /* Frameworks */ = { 104 | isa = PBXFrameworksBuildPhase; 105 | buildActionMask = 2147483647; 106 | files = ( 107 | ); 108 | runOnlyForDeploymentPostprocessing = 0; 109 | }; 110 | /* End PBXFrameworksBuildPhase section */ 111 | 112 | /* Begin PBXGroup section */ 113 | 4E4865A31BEE90D900C159BF /* Diagnostics */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 4E48658D1BEE6CE500C159BF /* Log.swift */, 117 | ); 118 | name = Diagnostics; 119 | sourceTree = ""; 120 | }; 121 | 4E4865A41BEE910600C159BF /* Extensions */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 4EB204531BEE307900D83EF3 /* CALayerExtensions.swift */, 125 | 4E4865AE1BEFA2B900C159BF /* StringExtensions.swift */, 126 | ); 127 | name = Extensions; 128 | sourceTree = ""; 129 | }; 130 | 4E4865AA1BEFA07E00C159BF /* Image Resources */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 4E4865A81BEFA07300C159BF /* MenuTimerIcon.icns */, 134 | 4EB204261BED89F900D83EF3 /* Assets.xcassets */, 135 | ); 136 | name = "Image Resources"; 137 | sourceTree = ""; 138 | }; 139 | 4E4865AB1BEFA0F000C159BF /* Text Resources */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 4E4865AC1BEFA12A00C159BF /* Credits.rtf */, 143 | 4EB2042B1BED89F900D83EF3 /* Info.plist */, 144 | 4E4865931BEE705E00C159BF /* UserDefaults.plist */, 145 | ); 146 | name = "Text Resources"; 147 | sourceTree = ""; 148 | }; 149 | 4E532E942376F94D00A9CFE5 /* Scripting and Services */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 4EB58ABF2374F6D200150BA5 /* AppDelegate_Scripting.swift */, 153 | 4EB58AB9237449B600150BA5 /* MenubarCountdown.sdef */, 154 | 4E532E922376F92F00A9CFE5 /* NSApplication_Scripting.swift */, 155 | 4EB58AAE236B881D00150BA5 /* ServicesProvider.swift */, 156 | ); 157 | name = "Scripting and Services"; 158 | sourceTree = ""; 159 | }; 160 | 4EB204161BED89F900D83EF3 = { 161 | isa = PBXGroup; 162 | children = ( 163 | 4EB204211BED89F900D83EF3 /* MenubarCountdown */, 164 | 4EB204331BED89F900D83EF3 /* MenubarCountdownTests */, 165 | 4EB2043E1BED89F900D83EF3 /* MenubarCountdownUITests */, 166 | 4EB204201BED89F900D83EF3 /* Products */, 167 | 4EB58AB8236DC3B300150BA5 /* Scripts */, 168 | ); 169 | sourceTree = ""; 170 | }; 171 | 4EB204201BED89F900D83EF3 /* Products */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | 4EB2041F1BED89F900D83EF3 /* Menubar Countdown.app */, 175 | 4EB204301BED89F900D83EF3 /* MenubarCountdownTests.xctest */, 176 | 4EB2043B1BED89F900D83EF3 /* MenubarCountdownUITests.xctest */, 177 | ); 178 | name = Products; 179 | sourceTree = ""; 180 | }; 181 | 4EB204211BED89F900D83EF3 /* MenubarCountdown */ = { 182 | isa = PBXGroup; 183 | children = ( 184 | 4EB204221BED89F900D83EF3 /* AppDelegate.swift */, 185 | 4EB2044F1BEE29D700D83EF3 /* AppUserDefaults.swift */, 186 | 4E4865A31BEE90D900C159BF /* Diagnostics */, 187 | 4E4865A41BEE910600C159BF /* Extensions */, 188 | 4E4865AA1BEFA07E00C159BF /* Image Resources */, 189 | 4E4865B01BEFC63700C159BF /* MainMenu.xib */, 190 | 4E4D0E552367E5D9004B1404 /* MenubarCountdown.entitlements */, 191 | 4E70D4D62390087C004DE5D8 /* Notifications.swift */, 192 | 4E532E942376F94D00A9CFE5 /* Scripting and Services */, 193 | 4E70D4D823900F55004DE5D8 /* Speech.swift */, 194 | 4E4865B11BEFC63700C159BF /* StartTimerDialog.xib */, 195 | 4E4865971BEE749E00C159BF /* StartTimerDialogController.swift */, 196 | 4EB2044D1BED908300D83EF3 /* Stopwatch.swift */, 197 | 4E4865AB1BEFA0F000C159BF /* Text Resources */, 198 | 4E4865991BEE79EA00C159BF /* TextField.swift */, 199 | 4E4865B21BEFC63700C159BF /* TimerExpiredAlert.xib */, 200 | 4E48659B1BEE81C200C159BF /* TimerExpiredAlertController.swift */, 201 | ); 202 | path = MenubarCountdown; 203 | sourceTree = ""; 204 | }; 205 | 4EB204331BED89F900D83EF3 /* MenubarCountdownTests */ = { 206 | isa = PBXGroup; 207 | children = ( 208 | 4EB204341BED89F900D83EF3 /* MenubarCountdownTests.swift */, 209 | 4EB204361BED89F900D83EF3 /* Info.plist */, 210 | ); 211 | path = MenubarCountdownTests; 212 | sourceTree = ""; 213 | }; 214 | 4EB2043E1BED89F900D83EF3 /* MenubarCountdownUITests */ = { 215 | isa = PBXGroup; 216 | children = ( 217 | 4EB2043F1BED89F900D83EF3 /* MenubarCountdownUITests.swift */, 218 | 4EB204411BED89F900D83EF3 /* Info.plist */, 219 | ); 220 | path = MenubarCountdownUITests; 221 | sourceTree = ""; 222 | }; 223 | /* End PBXGroup section */ 224 | 225 | /* Begin PBXNativeTarget section */ 226 | 4EB2041E1BED89F900D83EF3 /* Menubar Countdown */ = { 227 | isa = PBXNativeTarget; 228 | buildConfigurationList = 4EB204441BED89F900D83EF3 /* Build configuration list for PBXNativeTarget "Menubar Countdown" */; 229 | buildPhases = ( 230 | 4EB2041B1BED89F900D83EF3 /* Sources */, 231 | 4EB2041C1BED89F900D83EF3 /* Frameworks */, 232 | 4EB2041D1BED89F900D83EF3 /* Resources */, 233 | ); 234 | buildRules = ( 235 | ); 236 | dependencies = ( 237 | ); 238 | name = "Menubar Countdown"; 239 | productName = MenubarCountdown; 240 | productReference = 4EB2041F1BED89F900D83EF3 /* Menubar Countdown.app */; 241 | productType = "com.apple.product-type.application"; 242 | }; 243 | 4EB2042F1BED89F900D83EF3 /* MenubarCountdownTests */ = { 244 | isa = PBXNativeTarget; 245 | buildConfigurationList = 4EB204471BED89F900D83EF3 /* Build configuration list for PBXNativeTarget "MenubarCountdownTests" */; 246 | buildPhases = ( 247 | 4EB2042C1BED89F900D83EF3 /* Sources */, 248 | 4EB2042D1BED89F900D83EF3 /* Frameworks */, 249 | 4EB2042E1BED89F900D83EF3 /* Resources */, 250 | ); 251 | buildRules = ( 252 | ); 253 | dependencies = ( 254 | 4EB204321BED89F900D83EF3 /* PBXTargetDependency */, 255 | ); 256 | name = MenubarCountdownTests; 257 | productName = MenubarCountdownTests; 258 | productReference = 4EB204301BED89F900D83EF3 /* MenubarCountdownTests.xctest */; 259 | productType = "com.apple.product-type.bundle.unit-test"; 260 | }; 261 | 4EB2043A1BED89F900D83EF3 /* MenubarCountdownUITests */ = { 262 | isa = PBXNativeTarget; 263 | buildConfigurationList = 4EB2044A1BED89F900D83EF3 /* Build configuration list for PBXNativeTarget "MenubarCountdownUITests" */; 264 | buildPhases = ( 265 | 4EB204371BED89F900D83EF3 /* Sources */, 266 | 4EB204381BED89F900D83EF3 /* Frameworks */, 267 | 4EB204391BED89F900D83EF3 /* Resources */, 268 | ); 269 | buildRules = ( 270 | ); 271 | dependencies = ( 272 | 4EB2043D1BED89F900D83EF3 /* PBXTargetDependency */, 273 | ); 274 | name = MenubarCountdownUITests; 275 | productName = MenubarCountdownUITests; 276 | productReference = 4EB2043B1BED89F900D83EF3 /* MenubarCountdownUITests.xctest */; 277 | productType = "com.apple.product-type.bundle.ui-testing"; 278 | }; 279 | /* End PBXNativeTarget section */ 280 | 281 | /* Begin PBXProject section */ 282 | 4EB204171BED89F900D83EF3 /* Project object */ = { 283 | isa = PBXProject; 284 | attributes = { 285 | LastSwiftUpdateCheck = 0710; 286 | LastUpgradeCheck = 1110; 287 | ORGANIZATIONNAME = "Kristopher Johnson"; 288 | TargetAttributes = { 289 | 4EB2041E1BED89F900D83EF3 = { 290 | CreatedOnToolsVersion = 7.1; 291 | DevelopmentTeam = D5E423632K; 292 | }; 293 | 4EB2042F1BED89F900D83EF3 = { 294 | CreatedOnToolsVersion = 7.1; 295 | TestTargetID = 4EB2041E1BED89F900D83EF3; 296 | }; 297 | 4EB2043A1BED89F900D83EF3 = { 298 | CreatedOnToolsVersion = 7.1; 299 | TestTargetID = 4EB2041E1BED89F900D83EF3; 300 | }; 301 | }; 302 | }; 303 | buildConfigurationList = 4EB2041A1BED89F900D83EF3 /* Build configuration list for PBXProject "MenubarCountdown" */; 304 | compatibilityVersion = "Xcode 3.2"; 305 | developmentRegion = en; 306 | hasScannedForEncodings = 0; 307 | knownRegions = ( 308 | en, 309 | Base, 310 | ); 311 | mainGroup = 4EB204161BED89F900D83EF3; 312 | productRefGroup = 4EB204201BED89F900D83EF3 /* Products */; 313 | projectDirPath = ""; 314 | projectRoot = ""; 315 | targets = ( 316 | 4EB2041E1BED89F900D83EF3 /* Menubar Countdown */, 317 | 4EB2042F1BED89F900D83EF3 /* MenubarCountdownTests */, 318 | 4EB2043A1BED89F900D83EF3 /* MenubarCountdownUITests */, 319 | ); 320 | }; 321 | /* End PBXProject section */ 322 | 323 | /* Begin PBXResourcesBuildPhase section */ 324 | 4EB2041D1BED89F900D83EF3 /* Resources */ = { 325 | isa = PBXResourcesBuildPhase; 326 | buildActionMask = 2147483647; 327 | files = ( 328 | 4EB58ABA237449B600150BA5 /* MenubarCountdown.sdef in Resources */, 329 | 4E4865941BEE705E00C159BF /* UserDefaults.plist in Resources */, 330 | 4E4865A91BEFA07300C159BF /* MenuTimerIcon.icns in Resources */, 331 | 4E4865B51BEFC63700C159BF /* TimerExpiredAlert.xib in Resources */, 332 | 4EB204271BED89F900D83EF3 /* Assets.xcassets in Resources */, 333 | 4E4865B41BEFC63700C159BF /* StartTimerDialog.xib in Resources */, 334 | 4E4865B31BEFC63700C159BF /* MainMenu.xib in Resources */, 335 | 4E4865AD1BEFA12A00C159BF /* Credits.rtf in Resources */, 336 | ); 337 | runOnlyForDeploymentPostprocessing = 0; 338 | }; 339 | 4EB2042E1BED89F900D83EF3 /* Resources */ = { 340 | isa = PBXResourcesBuildPhase; 341 | buildActionMask = 2147483647; 342 | files = ( 343 | ); 344 | runOnlyForDeploymentPostprocessing = 0; 345 | }; 346 | 4EB204391BED89F900D83EF3 /* Resources */ = { 347 | isa = PBXResourcesBuildPhase; 348 | buildActionMask = 2147483647; 349 | files = ( 350 | ); 351 | runOnlyForDeploymentPostprocessing = 0; 352 | }; 353 | /* End PBXResourcesBuildPhase section */ 354 | 355 | /* Begin PBXSourcesBuildPhase section */ 356 | 4EB2041B1BED89F900D83EF3 /* Sources */ = { 357 | isa = PBXSourcesBuildPhase; 358 | buildActionMask = 2147483647; 359 | files = ( 360 | 4E4865981BEE749E00C159BF /* StartTimerDialogController.swift in Sources */, 361 | 4EB204541BEE307900D83EF3 /* CALayerExtensions.swift in Sources */, 362 | 4E48659A1BEE79EA00C159BF /* TextField.swift in Sources */, 363 | 4E70D4D923900F55004DE5D8 /* Speech.swift in Sources */, 364 | 4EB204501BEE29D700D83EF3 /* AppUserDefaults.swift in Sources */, 365 | 4EB2044E1BED908300D83EF3 /* Stopwatch.swift in Sources */, 366 | 4E48658E1BEE6CE500C159BF /* Log.swift in Sources */, 367 | 4EB58AC02374F6D200150BA5 /* AppDelegate_Scripting.swift in Sources */, 368 | 4E70D4D72390087C004DE5D8 /* Notifications.swift in Sources */, 369 | 4E532E932376F92F00A9CFE5 /* NSApplication_Scripting.swift in Sources */, 370 | 4EB58AAF236B881D00150BA5 /* ServicesProvider.swift in Sources */, 371 | 4E4865AF1BEFA2B900C159BF /* StringExtensions.swift in Sources */, 372 | 4E48659C1BEE81C200C159BF /* TimerExpiredAlertController.swift in Sources */, 373 | 4EB204231BED89F900D83EF3 /* AppDelegate.swift in Sources */, 374 | ); 375 | runOnlyForDeploymentPostprocessing = 0; 376 | }; 377 | 4EB2042C1BED89F900D83EF3 /* Sources */ = { 378 | isa = PBXSourcesBuildPhase; 379 | buildActionMask = 2147483647; 380 | files = ( 381 | 4EB204351BED89F900D83EF3 /* MenubarCountdownTests.swift in Sources */, 382 | ); 383 | runOnlyForDeploymentPostprocessing = 0; 384 | }; 385 | 4EB204371BED89F900D83EF3 /* Sources */ = { 386 | isa = PBXSourcesBuildPhase; 387 | buildActionMask = 2147483647; 388 | files = ( 389 | 4EB204401BED89F900D83EF3 /* MenubarCountdownUITests.swift in Sources */, 390 | ); 391 | runOnlyForDeploymentPostprocessing = 0; 392 | }; 393 | /* End PBXSourcesBuildPhase section */ 394 | 395 | /* Begin PBXTargetDependency section */ 396 | 4EB204321BED89F900D83EF3 /* PBXTargetDependency */ = { 397 | isa = PBXTargetDependency; 398 | target = 4EB2041E1BED89F900D83EF3 /* Menubar Countdown */; 399 | targetProxy = 4EB204311BED89F900D83EF3 /* PBXContainerItemProxy */; 400 | }; 401 | 4EB2043D1BED89F900D83EF3 /* PBXTargetDependency */ = { 402 | isa = PBXTargetDependency; 403 | target = 4EB2041E1BED89F900D83EF3 /* Menubar Countdown */; 404 | targetProxy = 4EB2043C1BED89F900D83EF3 /* PBXContainerItemProxy */; 405 | }; 406 | /* End PBXTargetDependency section */ 407 | 408 | /* Begin XCBuildConfiguration section */ 409 | 4EB204421BED89F900D83EF3 /* Debug */ = { 410 | isa = XCBuildConfiguration; 411 | buildSettings = { 412 | ALWAYS_SEARCH_USER_PATHS = NO; 413 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 414 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 415 | CLANG_CXX_LIBRARY = "libc++"; 416 | CLANG_ENABLE_MODULES = YES; 417 | CLANG_ENABLE_OBJC_ARC = YES; 418 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 419 | CLANG_WARN_BOOL_CONVERSION = YES; 420 | CLANG_WARN_COMMA = YES; 421 | CLANG_WARN_CONSTANT_CONVERSION = YES; 422 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 423 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 424 | CLANG_WARN_EMPTY_BODY = YES; 425 | CLANG_WARN_ENUM_CONVERSION = YES; 426 | CLANG_WARN_INFINITE_RECURSION = YES; 427 | CLANG_WARN_INT_CONVERSION = YES; 428 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 429 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 430 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 431 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 432 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 433 | CLANG_WARN_STRICT_PROTOTYPES = YES; 434 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 435 | CLANG_WARN_UNREACHABLE_CODE = YES; 436 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 437 | CODE_SIGN_IDENTITY = "-"; 438 | COPY_PHASE_STRIP = NO; 439 | DEBUG_INFORMATION_FORMAT = dwarf; 440 | ENABLE_STRICT_OBJC_MSGSEND = YES; 441 | ENABLE_TESTABILITY = YES; 442 | GCC_C_LANGUAGE_STANDARD = gnu99; 443 | GCC_DYNAMIC_NO_PIC = NO; 444 | GCC_NO_COMMON_BLOCKS = YES; 445 | GCC_OPTIMIZATION_LEVEL = 0; 446 | GCC_PREPROCESSOR_DEFINITIONS = ( 447 | "DEBUG=1", 448 | "$(inherited)", 449 | ); 450 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 451 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 452 | GCC_WARN_UNDECLARED_SELECTOR = YES; 453 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 454 | GCC_WARN_UNUSED_FUNCTION = YES; 455 | GCC_WARN_UNUSED_VARIABLE = YES; 456 | MACOSX_DEPLOYMENT_TARGET = 10.14.4; 457 | MTL_ENABLE_DEBUG_INFO = YES; 458 | ONLY_ACTIVE_ARCH = YES; 459 | OTHER_SWIFT_FLAGS = "-DDEBUG"; 460 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 461 | SWIFT_VERSION = 5.0; 462 | }; 463 | name = Debug; 464 | }; 465 | 4EB204431BED89F900D83EF3 /* Release */ = { 466 | isa = XCBuildConfiguration; 467 | buildSettings = { 468 | ALWAYS_SEARCH_USER_PATHS = NO; 469 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 470 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 471 | CLANG_CXX_LIBRARY = "libc++"; 472 | CLANG_ENABLE_MODULES = YES; 473 | CLANG_ENABLE_OBJC_ARC = YES; 474 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 475 | CLANG_WARN_BOOL_CONVERSION = YES; 476 | CLANG_WARN_COMMA = YES; 477 | CLANG_WARN_CONSTANT_CONVERSION = YES; 478 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 479 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 480 | CLANG_WARN_EMPTY_BODY = YES; 481 | CLANG_WARN_ENUM_CONVERSION = YES; 482 | CLANG_WARN_INFINITE_RECURSION = YES; 483 | CLANG_WARN_INT_CONVERSION = YES; 484 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 485 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 486 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 487 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 488 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 489 | CLANG_WARN_STRICT_PROTOTYPES = YES; 490 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 491 | CLANG_WARN_UNREACHABLE_CODE = YES; 492 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 493 | CODE_SIGN_IDENTITY = "-"; 494 | COPY_PHASE_STRIP = NO; 495 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 496 | ENABLE_NS_ASSERTIONS = NO; 497 | ENABLE_STRICT_OBJC_MSGSEND = YES; 498 | GCC_C_LANGUAGE_STANDARD = gnu99; 499 | GCC_NO_COMMON_BLOCKS = YES; 500 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 501 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 502 | GCC_WARN_UNDECLARED_SELECTOR = YES; 503 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 504 | GCC_WARN_UNUSED_FUNCTION = YES; 505 | GCC_WARN_UNUSED_VARIABLE = YES; 506 | MACOSX_DEPLOYMENT_TARGET = 10.14.4; 507 | MTL_ENABLE_DEBUG_INFO = NO; 508 | SWIFT_COMPILATION_MODE = wholemodule; 509 | SWIFT_VERSION = 5.0; 510 | }; 511 | name = Release; 512 | }; 513 | 4EB204451BED89F900D83EF3 /* Debug */ = { 514 | isa = XCBuildConfiguration; 515 | buildSettings = { 516 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 517 | CODE_SIGN_ENTITLEMENTS = MenubarCountdown/MenubarCountdown.entitlements; 518 | CODE_SIGN_IDENTITY = "Apple Development"; 519 | COMBINE_HIDPI_IMAGES = YES; 520 | ENABLE_HARDENED_RUNTIME = YES; 521 | INFOPLIST_FILE = MenubarCountdown/Info.plist; 522 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 523 | PRODUCT_BUNDLE_IDENTIFIER = net.kristopherjohnson.MenubarCountdown; 524 | PRODUCT_NAME = "$(TARGET_NAME)"; 525 | }; 526 | name = Debug; 527 | }; 528 | 4EB204461BED89F900D83EF3 /* Release */ = { 529 | isa = XCBuildConfiguration; 530 | buildSettings = { 531 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 532 | CODE_SIGN_ENTITLEMENTS = MenubarCountdown/MenubarCountdown.entitlements; 533 | CODE_SIGN_IDENTITY = "Apple Development"; 534 | COMBINE_HIDPI_IMAGES = YES; 535 | ENABLE_HARDENED_RUNTIME = YES; 536 | INFOPLIST_FILE = MenubarCountdown/Info.plist; 537 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 538 | PRODUCT_BUNDLE_IDENTIFIER = net.kristopherjohnson.MenubarCountdown; 539 | PRODUCT_NAME = "$(TARGET_NAME)"; 540 | }; 541 | name = Release; 542 | }; 543 | 4EB204481BED89F900D83EF3 /* Debug */ = { 544 | isa = XCBuildConfiguration; 545 | buildSettings = { 546 | BUNDLE_LOADER = "$(TEST_HOST)"; 547 | COMBINE_HIDPI_IMAGES = YES; 548 | INFOPLIST_FILE = MenubarCountdownTests/Info.plist; 549 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 550 | PRODUCT_BUNDLE_IDENTIFIER = net.kristopherjohnson.MenubarCountdownTests; 551 | PRODUCT_NAME = "$(TARGET_NAME)"; 552 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MenubarCountdown.app/Contents/MacOS/MenubarCountdown"; 553 | }; 554 | name = Debug; 555 | }; 556 | 4EB204491BED89F900D83EF3 /* Release */ = { 557 | isa = XCBuildConfiguration; 558 | buildSettings = { 559 | BUNDLE_LOADER = "$(TEST_HOST)"; 560 | COMBINE_HIDPI_IMAGES = YES; 561 | INFOPLIST_FILE = MenubarCountdownTests/Info.plist; 562 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 563 | PRODUCT_BUNDLE_IDENTIFIER = net.kristopherjohnson.MenubarCountdownTests; 564 | PRODUCT_NAME = "$(TARGET_NAME)"; 565 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MenubarCountdown.app/Contents/MacOS/MenubarCountdown"; 566 | }; 567 | name = Release; 568 | }; 569 | 4EB2044B1BED89F900D83EF3 /* Debug */ = { 570 | isa = XCBuildConfiguration; 571 | buildSettings = { 572 | COMBINE_HIDPI_IMAGES = YES; 573 | INFOPLIST_FILE = MenubarCountdownUITests/Info.plist; 574 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 575 | PRODUCT_BUNDLE_IDENTIFIER = net.kristopherjohnson.MenubarCountdownUITests; 576 | PRODUCT_NAME = "$(TARGET_NAME)"; 577 | TEST_TARGET_NAME = MenubarCountdown; 578 | USES_XCTRUNNER = YES; 579 | }; 580 | name = Debug; 581 | }; 582 | 4EB2044C1BED89F900D83EF3 /* Release */ = { 583 | isa = XCBuildConfiguration; 584 | buildSettings = { 585 | COMBINE_HIDPI_IMAGES = YES; 586 | INFOPLIST_FILE = MenubarCountdownUITests/Info.plist; 587 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 588 | PRODUCT_BUNDLE_IDENTIFIER = net.kristopherjohnson.MenubarCountdownUITests; 589 | PRODUCT_NAME = "$(TARGET_NAME)"; 590 | TEST_TARGET_NAME = MenubarCountdown; 591 | USES_XCTRUNNER = YES; 592 | }; 593 | name = Release; 594 | }; 595 | /* End XCBuildConfiguration section */ 596 | 597 | /* Begin XCConfigurationList section */ 598 | 4EB2041A1BED89F900D83EF3 /* Build configuration list for PBXProject "MenubarCountdown" */ = { 599 | isa = XCConfigurationList; 600 | buildConfigurations = ( 601 | 4EB204421BED89F900D83EF3 /* Debug */, 602 | 4EB204431BED89F900D83EF3 /* Release */, 603 | ); 604 | defaultConfigurationIsVisible = 0; 605 | defaultConfigurationName = Release; 606 | }; 607 | 4EB204441BED89F900D83EF3 /* Build configuration list for PBXNativeTarget "Menubar Countdown" */ = { 608 | isa = XCConfigurationList; 609 | buildConfigurations = ( 610 | 4EB204451BED89F900D83EF3 /* Debug */, 611 | 4EB204461BED89F900D83EF3 /* Release */, 612 | ); 613 | defaultConfigurationIsVisible = 0; 614 | defaultConfigurationName = Release; 615 | }; 616 | 4EB204471BED89F900D83EF3 /* Build configuration list for PBXNativeTarget "MenubarCountdownTests" */ = { 617 | isa = XCConfigurationList; 618 | buildConfigurations = ( 619 | 4EB204481BED89F900D83EF3 /* Debug */, 620 | 4EB204491BED89F900D83EF3 /* Release */, 621 | ); 622 | defaultConfigurationIsVisible = 0; 623 | defaultConfigurationName = Release; 624 | }; 625 | 4EB2044A1BED89F900D83EF3 /* Build configuration list for PBXNativeTarget "MenubarCountdownUITests" */ = { 626 | isa = XCConfigurationList; 627 | buildConfigurations = ( 628 | 4EB2044B1BED89F900D83EF3 /* Debug */, 629 | 4EB2044C1BED89F900D83EF3 /* Release */, 630 | ); 631 | defaultConfigurationIsVisible = 0; 632 | defaultConfigurationName = Release; 633 | }; 634 | /* End XCConfigurationList section */ 635 | }; 636 | rootObject = 4EB204171BED89F900D83EF3 /* Project object */; 637 | } 638 | -------------------------------------------------------------------------------- /MenubarCountdown.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MenubarCountdown.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MenubarCountdown.xcodeproj/xcshareddata/xcschemes/MenubarCountdown.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 52 | 58 | 59 | 60 | 61 | 62 | 72 | 74 | 80 | 81 | 82 | 83 | 89 | 91 | 97 | 98 | 99 | 100 | 102 | 103 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /MenubarCountdown.xcodeproj/xcuserdata/kdj.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MenubarCountdown.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 4EB2041E1BED89F900D83EF3 16 | 17 | primary 18 | 19 | 20 | 4EB2042F1BED89F900D83EF3 21 | 22 | primary 23 | 24 | 25 | 4EB2043A1BED89F900D83EF3 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /MenubarCountdown/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MenubarCountdown 4 | // 5 | // Copyright © 2009,2015,2019,2020 Kristopher Johnson. All rights reserved. 6 | 7 | import Cocoa 8 | import AudioToolbox 9 | import UserNotifications 10 | 11 | /** 12 | Application delegate which implements most of the logic of Menubar Countdown. 13 | 14 | From the user's point of view, the application switches between these states: 15 | 16 | - reset: timer is not running 17 | - running: timer has been started and is counting down 18 | - paused: timer has been started but is in a paused state 19 | - expired: timer has reached 00:00:00 and is waiting to be turned off 20 | 21 | The persistent settings of the app are managed by the UserDefaults system. 22 | */ 23 | @NSApplicationMain 24 | final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate { 25 | 26 | /** 27 | Initial timer setting. 28 | */ 29 | var timerSettingSeconds = 25 * 60 30 | 31 | /** 32 | Number of seconds remaining in countdown. 33 | */ 34 | var secondsRemaining = 0 35 | 36 | /** 37 | Indicates whether timer is running. 38 | 39 | KVC compliant so it can be used to enable/disable menu items and other controls 40 | */ 41 | @objc var isTimerRunning = false 42 | 43 | /** 44 | Indicates whether the timer can be paused. 45 | 46 | KVC compliant so it can be used to enable/disable menu items and other controls 47 | */ 48 | @objc var canPause = false 49 | 50 | /** 51 | Indicates whether the timer can be resumed. 52 | 53 | KVC compliant so it can be used to enable/disable menu items and other controls 54 | */ 55 | @objc var canResume = false 56 | 57 | var stopwatch: Stopwatch! 58 | 59 | var statusItem: NSStatusItem! 60 | 61 | var notificationId: String = "" 62 | 63 | /** 64 | Reference to menu loaded from MainMenu.xib. 65 | */ 66 | @IBOutlet var menu: NSMenu! 67 | 68 | /** 69 | Reference to dialog loaded from StartTimerDialog.xib. 70 | */ 71 | @IBOutlet var startTimerDialogController: StartTimerDialogController? 72 | 73 | /** 74 | Reference to dialog loaded from TimerExpiredAlert.xib. 75 | */ 76 | @IBOutlet var timerExpiredAlertController: TimerExpiredAlertController? 77 | 78 | let defaults = UserDefaults.standard 79 | 80 | override init() { 81 | super.init() 82 | AppUserDefaults.registerUserDefaults() 83 | } 84 | 85 | func applicationDidFinishLaunching(_ notification: Notification) { 86 | Log.debug("application did finish launching: \(notification)") 87 | 88 | UNUserNotificationCenter.current().delegate = self 89 | 90 | stopwatch.reset() 91 | 92 | initializeStatusItem() 93 | 94 | if showStartDialogOnLaunch { 95 | showStartTimerDialog(self) 96 | } 97 | 98 | NSApp.servicesProvider = ServicesProvider(appDelegate: self) 99 | } 100 | 101 | func applicationWillTerminate(_ notification: Notification) { 102 | Log.debug("application will terminate") 103 | } 104 | 105 | // MARK: Timer updating 106 | 107 | /** 108 | Determine time to the next top of second, and set a timer to call `nextSecondTimerDidFire` at that time. 109 | */ 110 | func waitForNextSecond() { 111 | let elapsed = stopwatch.elapsedTimeInterval() 112 | let intervalToNextSecond = ceil(elapsed) - elapsed 113 | 114 | let t = Timer.scheduledTimer(timeInterval: intervalToNextSecond, 115 | target: self, 116 | selector: #selector(nextSecondTimerDidFire(_:)), 117 | userInfo: nil, 118 | repeats: false) 119 | t.tolerance = 0.25 120 | } 121 | 122 | /** 123 | Called at the top of each second. 124 | 125 | Updates the timer display if the timer is running. 126 | */ 127 | @objc func nextSecondTimerDidFire(_ timer: Timer) { 128 | if isTimerRunning { 129 | secondsRemaining = Int(round(TimeInterval(timerSettingSeconds) - stopwatch.elapsedTimeInterval())) 130 | 131 | if secondsRemaining <= 0 { 132 | timerDidExpire() 133 | } 134 | else { 135 | updateStatusItemTitle(timeRemaining: secondsRemaining) 136 | waitForNextSecond() 137 | } 138 | } 139 | else { 140 | Log.debug("ignoring tick because timer is not running") 141 | } 142 | } 143 | 144 | /** 145 | Create `statusItem` and set its initial state. 146 | */ 147 | func initializeStatusItem() { 148 | let statusBar = NSStatusBar.system 149 | statusItem = statusBar.statusItem(withLength: NSStatusItem.variableLength) 150 | 151 | statusItem.isVisible = true 152 | statusItem.behavior = [.terminationOnRemoval] 153 | 154 | statusItem.menu = menu 155 | statusItem.button?.wantsLayer = true 156 | statusItem.button?.toolTip = NSLocalizedString("Menubar Countdown", 157 | comment: "Status Item Tooltip") 158 | statusItem.button?.font = NSFont.monospacedDigitSystemFont(ofSize: 0, 159 | weight: .regular) 160 | showStatusItemIcon() 161 | } 162 | 163 | /** 164 | Sets the text of the menu bar status item. 165 | */ 166 | func updateStatusItemTitle(timeRemaining: Int) { 167 | var timeRemaining = timeRemaining 168 | 169 | let includeSecondsInTitle = displaySeconds 170 | if (!includeSecondsInTitle) { 171 | // Round timeRemaining up to the next minute 172 | let minutes = Double(timeRemaining) / 60.0 173 | timeRemaining = Int(ceil(minutes)) * 60 174 | } 175 | 176 | let hours = timeRemaining / 3600 177 | timeRemaining %= 3600 178 | let minutes = timeRemaining / 60 179 | let seconds = timeRemaining % 60 180 | 181 | // TODO: Use localized time-formatting function 182 | var timeString: String 183 | if includeSecondsInTitle { 184 | timeString = NSString(format: "%02d:%02d:%02d", hours, minutes, seconds) as String 185 | } 186 | else { 187 | timeString = NSString(format: "%02d:%02d", hours, minutes) as String 188 | } 189 | statusItem.button?.image = nil 190 | statusItem.button?.title = timeString 191 | } 192 | 193 | /** 194 | Change the status item to an hourglass icon 195 | */ 196 | func showStatusItemIcon() { 197 | statusItem.button?.title = "" 198 | if let image = NSImage(named: "HourglassIcon") { 199 | statusItem.button?.image = image 200 | } 201 | else { 202 | Log.error("unable to load HourglassIcon") 203 | } 204 | } 205 | 206 | func startBlinking() { 207 | statusItem.button?.layer?.addBlinkAnimation() 208 | } 209 | 210 | func stopBlinking() { 211 | statusItem.button?.layer?.removeBlinkAnimation() 212 | } 213 | 214 | // MARK: Timer expiration 215 | 216 | /** 217 | Called when the timer reaches 00:00:00. 218 | 219 | Fires all of the configured notifications. 220 | */ 221 | func timerDidExpire() { 222 | canPause = false 223 | canResume = false 224 | 225 | updateStatusItemTitle(timeRemaining: 0) 226 | 227 | if blinkOnExpiration { 228 | startBlinking() 229 | } 230 | 231 | if playAlertSoundOnExpiration { 232 | playAlertSound() 233 | } 234 | 235 | if speakAnnouncementOnExpiration { 236 | speakTimerExpiredAnnouncement( 237 | text: announcementText) 238 | } 239 | 240 | if showAlertWindowOnExpiration { 241 | showTimerExpiredAlert() 242 | } 243 | 244 | if showNotificationOnExpiration { 245 | self.notificationId = showTimerExpiredNotification( 246 | withSound: playNotificationSoundOnExpiration) 247 | } 248 | } 249 | 250 | /** 251 | Plays the alert sound. 252 | 253 | If configured to repeat, starts a timer to call this method again after a delay. 254 | */ 255 | @objc func playAlertSound() { 256 | if isTimerRunning && (secondsRemaining < 1) { 257 | Log.debug("play alert sound") 258 | AudioServicesPlayAlertSound(kUserPreferredAlert); 259 | 260 | if repeatAlertSoundOnExpiration { 261 | var repeatInterval = TimeInterval(defaults.integer(forKey: AppUserDefaults.alertSoundRepeatIntervalKey)) 262 | if repeatInterval < 1.0 { 263 | repeatInterval = 1.0 264 | } 265 | Log.debug("schedule alert sound repeat \(repeatInterval)s") 266 | Timer.scheduledTimer(timeInterval: repeatInterval, 267 | target: self, 268 | selector: #selector(playAlertSound), 269 | userInfo: nil, 270 | repeats: false) 271 | } 272 | } 273 | } 274 | 275 | /** 276 | Display the alert indicating timer is expired. 277 | */ 278 | func showTimerExpiredAlert() { 279 | Log.debug("show timer-expired alert") 280 | 281 | NSApp.activate(ignoringOtherApps: true) 282 | 283 | if timerExpiredAlertController == nil { 284 | TimerExpiredAlertController.load(owner: self) 285 | assert(timerExpiredAlertController != nil, 286 | "timerExpiredAlertController outlet must be set") 287 | } 288 | timerExpiredAlertController?.showAlert() 289 | } 290 | 291 | 292 | // MARK: Menu item and button event handlers 293 | 294 | /** 295 | Display the StartTimerDialog.xib dialog. 296 | 297 | Called at startup, when the user chooses the Start... menu item, 298 | when the user clicks the Restart Countdown... button on the alert, 299 | or when the `show start dialog` scripting command is invoked. 300 | */ 301 | @IBAction func showStartTimerDialog(_ sender: AnyObject) { 302 | Log.debug("show start timer dialog") 303 | 304 | dismissTimerExpiredAlert(sender) 305 | 306 | if startTimerDialogController == nil { 307 | StartTimerDialogController.load(owner: self) 308 | assert(startTimerDialogController != nil, "startTimerDialogController must be set") 309 | } 310 | startTimerDialogController?.showDialog() 311 | } 312 | 313 | /** 314 | Start the timer. 315 | 316 | Called when the user clicks the Start button in the StartTimerDialog 317 | or invokes the Start Countdown service. 318 | 319 | If user selected "Show notification", then check for authorization and 320 | show the notification-authorization dialog if necessary before dismissing 321 | the dialog and starting the timer. 322 | */ 323 | @IBAction func startTimerDialogStartButtonWasClicked(_ sender: AnyObject) { 324 | Log.debug("start button was clicked") 325 | 326 | dismissTimerExpiredAlert(sender) 327 | 328 | defaults.synchronize() 329 | 330 | if showNotificationOnExpiration { 331 | requestNotificationAuthorization(withSound: playNotificationSoundOnExpiration) { granted in 332 | DispatchQueue.main.async { 333 | if granted { 334 | self.dismissStartTimerDialogAndStartTimer(sender) 335 | } 336 | else { 337 | self.showNotificationOnExpiration = false 338 | } 339 | } 340 | } 341 | } 342 | else { 343 | dismissStartTimerDialogAndStartTimer(sender) 344 | } 345 | } 346 | 347 | /** 348 | Start the timer as a result of receiving a `start timer` script command. 349 | 350 | This is similar to `startTimerDialogStartButtonWasClicked(_:)`, but we 351 | don't want to prompt the user to authorize notifications. 352 | */ 353 | func startTimerViaScript(command: NSScriptCommand) { 354 | dismissTimerExpiredAlert(self) 355 | dismissStartTimerDialogAndStartTimer(self) 356 | } 357 | 358 | func dismissStartTimerDialogAndStartTimer(_ sender: AnyObject) { 359 | if let startTimerDialogController = startTimerDialogController { 360 | startTimerDialogController.dismissDialog(sender) 361 | } 362 | 363 | timerSettingSeconds = (startHours * 3600) + (startMinutes * 60) + startSeconds 364 | secondsRemaining = timerSettingSeconds 365 | 366 | isTimerRunning = true 367 | canPause = true 368 | canResume = false 369 | stopwatch.reset() 370 | 371 | updateStatusItemTitle(timeRemaining: timerSettingSeconds) 372 | 373 | waitForNextSecond() 374 | } 375 | 376 | /** 377 | Reset everything to a not-running state. 378 | 379 | Called when the user clicks the Stop menu item, 380 | clicks the OK button in the TimerExpiredAlert, 381 | invokes the Stop Countdown service, or invokes 382 | the `stop timer` scripting command. 383 | */ 384 | @IBAction func stopTimer(_ sender: AnyObject) { 385 | Log.debug("stop timer") 386 | 387 | isTimerRunning = false 388 | canPause = false 389 | canResume = false 390 | 391 | stopBlinking() 392 | showStatusItemIcon() 393 | } 394 | 395 | /** 396 | Pause the countdown timer. 397 | 398 | Called when the user chooses the Pause menu item, invokes the Pause Countdown service, or invokes the `pause timer` scripting command. 399 | */ 400 | @IBAction func pauseTimer(_ sender: AnyObject) { 401 | Log.debug("pause timer") 402 | if canPause { 403 | isTimerRunning = false 404 | canPause = false 405 | canResume = true 406 | } 407 | else { 408 | Log.error("can't resume in current state") 409 | } 410 | } 411 | 412 | /** 413 | Resume the countdown timer. 414 | 415 | Called when the user chooses the Resume menu item, invokes the Resume Countdown service, or invokes the `resume timer` scripting command. 416 | */ 417 | @IBAction func resumeTimer(_ sender: AnyObject) { 418 | Log.debug("resume timer") 419 | if canResume { 420 | isTimerRunning = true 421 | canPause = true 422 | canResume = false 423 | 424 | timerSettingSeconds = secondsRemaining 425 | 426 | stopwatch.reset() 427 | 428 | updateStatusItemTitle(timeRemaining: timerSettingSeconds) 429 | 430 | waitForNextSecond() 431 | } 432 | else { 433 | Log.error("can't resume in current state") 434 | } 435 | } 436 | 437 | /** 438 | Dismiss the TimerExpiredAlert. 439 | 440 | Called when the user clicks the OK button in that alert, 441 | or whenever the StartTimerDialog is shown. 442 | */ 443 | @IBAction func dismissTimerExpiredAlert(_ sender: AnyObject) { 444 | Log.debug("dismiss timer expired alert") 445 | timerExpiredAlertController?.close() 446 | stopTimer(sender) 447 | } 448 | 449 | /** 450 | Dismiss the TimerExpiredAlert and show the StartTimerDialog. 451 | 452 | Called when the user clicks the Restart Countdown button in the TimerExpiredAlert. 453 | */ 454 | @IBAction func restartCountdownWasClicked(_ sender: AnyObject) { 455 | Log.debug("restart countdown was clicked") 456 | dismissTimerExpiredAlert(sender) 457 | showStartTimerDialog(sender) 458 | } 459 | 460 | /** 461 | Show the About box. 462 | 463 | Called when the user chooses the About Menubar Countdown menu item. 464 | */ 465 | @IBAction func showAboutPanel(_ sender: AnyObject) { 466 | Log.debug("show About panel") 467 | NSApp.activate(ignoringOtherApps: true) 468 | NSApp.orderFrontStandardAboutPanel(sender) 469 | } 470 | 471 | // MARK: UNUserNotificationCenterDelegate methods 472 | 473 | func userNotificationCenter( 474 | _ center: UNUserNotificationCenter, 475 | willPresent notification: UNNotification, 476 | withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) 477 | { 478 | Log.debug("calling completion handler") 479 | 480 | let presentationOptions: UNNotificationPresentationOptions = 481 | playNotificationSoundOnExpiration 482 | ? [.alert, .sound] 483 | : [.alert] 484 | 485 | completionHandler(presentationOptions) 486 | } 487 | 488 | func userNotificationCenter(_ center: UNUserNotificationCenter, 489 | didReceive response: UNNotificationResponse, 490 | withCompletionHandler completionHandler: @escaping () -> Void) { 491 | // If notification is clicked, stop the timer and show the start dialog 492 | DispatchQueue.main.async { 493 | self.dismissTimerExpiredAlert(self) 494 | self.showStartTimerDialog(self) 495 | } 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /MenubarCountdown/AppDelegate_Scripting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate_Scripting.swift 3 | // Menubar Countdown 4 | // 5 | // Copyright © 2019 Kristopher Johnson. All rights reserved. 6 | 7 | import Cocoa 8 | 9 | /* 10 | Support for application scripting properties. 11 | 12 | See MenubarCountdown.sdef for definitions and descriptions 13 | of the scripting properties. 14 | 15 | Do not rename or delete any of these methods without updating 16 | MenubarCountdown.sdef. 17 | 18 | See `NSApplication_Scripting.swift` for implementations of 19 | scripting commands. 20 | */ 21 | 22 | /** 23 | Set of all scripting-related keys that are handled 24 | by the AppDelegate. 25 | */ 26 | fileprivate let scriptingKeys: Set = [ 27 | "timeRemaining", 28 | "timerHasExpired", 29 | "timerIsPaused", 30 | "startHours", 31 | "startMinutes", 32 | "startSeconds", 33 | "blinkOnExpiration", 34 | "playAlertSoundOnExpiration", 35 | "repeatAlertSoundOnExpiration", 36 | "showAlertWindowOnExpiration", 37 | "showNotificationOnExpiration", 38 | "playNotificationSoundOnExpiration", 39 | "speakAnnouncementOnExpiration", 40 | "announcementText", 41 | "displaySeconds", 42 | "showStartDialogOnLaunch" 43 | ] 44 | 45 | extension AppDelegate { 46 | 47 | /** 48 | Called by the application to determine if the delegate implements 49 | a KVO/KVC key. 50 | 51 | Must return `true` for all the scripting properties and commands implemented here. 52 | */ 53 | func application(_ sender: NSApplication, 54 | delegateHandlesKey key: String) -> Bool { 55 | return scriptingKeys.contains(key) 56 | } 57 | 58 | /** 59 | Get the `time remaining` scripting property value. 60 | */ 61 | @objc dynamic var timeRemaining: Int { 62 | if secondsRemaining < 0 { 63 | return 0 64 | } 65 | return secondsRemaining 66 | } 67 | 68 | /** 69 | Get the `timer has expired` scripting property value. 70 | */ 71 | @objc dynamic var timerHasExpired: Bool { 72 | secondsRemaining <= 0 73 | } 74 | 75 | /** 76 | Get the `timer is paused` scripting property value. 77 | */ 78 | @objc dynamic var timerIsPaused: Bool { 79 | canResume 80 | } 81 | 82 | /** 83 | Get/set the `hours` scripting property. 84 | 85 | The value is presented to scripting as an integer, but is stored internally as a string. 86 | */ 87 | @objc dynamic var startHours: Int { 88 | get { 89 | let value = defaults.string(forKey: AppUserDefaults.timerHoursKey) ?? "00" 90 | return Int(value) ?? 0 91 | } 92 | set { 93 | if newValue < 0 || newValue > 99 { 94 | Log.error("new hours value \(newValue) is not valid") 95 | return 96 | } 97 | 98 | let stringValue = NSString(format: "%02d", newValue) 99 | defaults.set(stringValue, forKey: AppUserDefaults.timerHoursKey) 100 | defaults.synchronize() 101 | } 102 | } 103 | 104 | /** 105 | Get/set the `minutes` scripting property. 106 | 107 | The value is presented to scripting as an integer, but is stored internally as a string. 108 | */ 109 | @objc dynamic var startMinutes: Int { 110 | get { 111 | let value = defaults.string(forKey: AppUserDefaults.timerMinutesKey) ?? "00" 112 | return Int(value) ?? 0 113 | } 114 | set { 115 | if newValue < 0 || newValue > 59 { 116 | Log.error("new minutes value \(newValue) is not valid") 117 | return 118 | } 119 | 120 | let stringValue = NSString(format: "%02d", newValue) 121 | defaults.set(stringValue, forKey: AppUserDefaults.timerMinutesKey) 122 | defaults.synchronize() 123 | } 124 | } 125 | 126 | /** 127 | Get/set the `seconds` scripting property. 128 | 129 | The value is presented to scripting as an integer, but is stored internally as a string. 130 | */ 131 | @objc dynamic var startSeconds: Int { 132 | get { 133 | let value = defaults.string(forKey: AppUserDefaults.timerSecondsKey) ?? "00" 134 | return Int(value) ?? 0 135 | } 136 | set { 137 | if newValue < 0 || newValue > 59 { 138 | Log.error("new seconds value \(newValue) is not valid") 139 | return 140 | } 141 | 142 | let stringValue = NSString(format: "%02d", newValue) 143 | defaults.set(stringValue, forKey: AppUserDefaults.timerSecondsKey) 144 | defaults.synchronize() 145 | } 146 | } 147 | 148 | /** 149 | Get/set the `blink` scripting property. 150 | */ 151 | @objc dynamic var blinkOnExpiration: Bool { 152 | get { 153 | defaults.bool(forKey: AppUserDefaults.blinkOnExpirationKey) 154 | } 155 | set { 156 | defaults.set(newValue, forKey: AppUserDefaults.blinkOnExpirationKey) 157 | defaults.synchronize() 158 | } 159 | } 160 | 161 | /** 162 | Get/set the `play alert sound` scripting property. 163 | */ 164 | @objc dynamic var playAlertSoundOnExpiration: Bool { 165 | get { 166 | defaults.bool(forKey: AppUserDefaults.playAlertSoundOnExpirationKey) 167 | } 168 | set { 169 | defaults.set(newValue, forKey: AppUserDefaults.playAlertSoundOnExpirationKey) 170 | defaults.synchronize() 171 | } 172 | } 173 | 174 | /** 175 | Get/set the `repeat alert sound` scripting property. 176 | */ 177 | @objc dynamic var repeatAlertSoundOnExpiration: Bool { 178 | get { 179 | defaults.bool(forKey: AppUserDefaults.repeatAlertSoundOnExpirationKey) 180 | } 181 | set { 182 | defaults.set(newValue, forKey: AppUserDefaults.repeatAlertSoundOnExpirationKey) 183 | defaults.synchronize() 184 | } 185 | } 186 | 187 | /** 188 | Get/set the `show alert window` scripting property. 189 | */ 190 | @objc dynamic var showAlertWindowOnExpiration: Bool { 191 | get { 192 | defaults.bool(forKey: AppUserDefaults.showAlertWindowOnExpirationKey) 193 | } 194 | set { 195 | defaults.set(newValue, forKey: AppUserDefaults.showAlertWindowOnExpirationKey) 196 | defaults.synchronize() 197 | } 198 | } 199 | 200 | /** 201 | Get/set the `show notification` scripting property. 202 | */ 203 | @objc dynamic var showNotificationOnExpiration: Bool { 204 | get { 205 | defaults.bool(forKey: AppUserDefaults.showNotificationOnExpirationKey) 206 | } 207 | set { 208 | defaults.set(newValue, forKey: AppUserDefaults.showNotificationOnExpirationKey) 209 | defaults.synchronize() 210 | } 211 | } 212 | 213 | /** 214 | Get/set the `play notification sound` scripting property. 215 | */ 216 | @objc dynamic var playNotificationSoundOnExpiration: Bool { 217 | get { 218 | defaults.bool(forKey: AppUserDefaults.playSoundWithNotification) 219 | } 220 | set { 221 | defaults.set(newValue, forKey: AppUserDefaults.playSoundWithNotification) 222 | defaults.synchronize() 223 | } 224 | } 225 | 226 | /** 227 | Get/set the `speak announcement` scripting property. 228 | */ 229 | @objc dynamic var speakAnnouncementOnExpiration: Bool { 230 | get { 231 | defaults.bool(forKey: AppUserDefaults.announceExpirationKey) 232 | } 233 | set { 234 | defaults.set(newValue, forKey: AppUserDefaults.announceExpirationKey) 235 | defaults.synchronize() 236 | } 237 | } 238 | 239 | /** 240 | Get/set the `announcement text` scripting property. 241 | */ 242 | @objc dynamic var announcementText: String { 243 | get { 244 | var result = defaults.string(forKey: AppUserDefaults.announcementTextKey) 245 | if (result == nil) || result!.isEmpty { 246 | result = NSLocalizedString("The Menubar Countdown timer has reached zero.", 247 | comment: "Default announcement text") 248 | } 249 | return result! 250 | } 251 | set { 252 | defaults.set(newValue, forKey: AppUserDefaults.announcementTextKey) 253 | defaults.synchronize() 254 | } 255 | } 256 | 257 | /** 258 | Get/set the `display seconds` scripting property. 259 | */ 260 | @objc dynamic var displaySeconds: Bool { 261 | get { 262 | defaults.bool(forKey: AppUserDefaults.showSeconds) 263 | } 264 | set { 265 | defaults.set(newValue, forKey: AppUserDefaults.showSeconds) 266 | defaults.synchronize() 267 | } 268 | } 269 | 270 | /** 271 | Get/set the `show settings on launch` scripting property. 272 | */ 273 | @objc dynamic var showStartDialogOnLaunch: Bool { 274 | get { 275 | defaults.bool(forKey: AppUserDefaults.showStartDialogOnLaunchKey) 276 | } 277 | set { 278 | defaults.set(newValue, forKey: AppUserDefaults.showStartDialogOnLaunchKey) 279 | defaults.synchronize() 280 | } 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /MenubarCountdown/AppUserDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppUserDefaults.swift 3 | // MenubarCountdown 4 | // 5 | // Copyright © 2009,2015,2019 Kristopher Johnson. All rights reserved. 6 | 7 | import Foundation 8 | 9 | /** 10 | Defines UserDefaults keys used by this application. 11 | 12 | The resource `UserDefaults.plist` holds default values for these keys. 13 | */ 14 | struct AppUserDefaults { 15 | static let timerHoursKey = "TimerHours" 16 | static let timerMinutesKey = "TimerMinutes" 17 | static let timerSecondsKey = "TimerSeconds" 18 | static let blinkOnExpirationKey = "BlinkOnExpiration" 19 | static let playAlertSoundOnExpirationKey = "PlayAlertSoundOnExpiration" 20 | static let repeatAlertSoundOnExpirationKey = "RepeatAlertSoundOnExpiration" 21 | static let alertSoundRepeatIntervalKey = "AlertSoundRepeatInterval" 22 | static let announceExpirationKey = "AnnounceExpiration" 23 | static let announcementTextKey = "AnnouncementText" 24 | static let showAlertWindowOnExpirationKey = "ShowAlertWindowOnExpiration" 25 | static let showNotificationOnExpirationKey = "ShowNotificationOnExpiration" 26 | static let playSoundWithNotification = "PlaySoundWithNotification" 27 | static let showStartDialogOnLaunchKey = "ShowStartDialogOnLaunch" 28 | static let showSeconds = "ShowSeconds" 29 | 30 | /** 31 | Adds default values to the registration domain. 32 | 33 | Default values are read from the `UserDefaults.plist` resource. 34 | */ 35 | static func registerUserDefaults() { 36 | if let plistPath = Bundle.main.path(forResource: "UserDefaults", ofType: "plist") { 37 | if let dict = NSDictionary(contentsOfFile: plistPath) as? [String : Any] { 38 | UserDefaults.standard.register(defaults: dict) 39 | } 40 | else { 41 | Log.error("unable to load UserDefaults.plist") 42 | } 43 | } 44 | else { 45 | Log.error("unable to find UserDefaults.plist") 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /MenubarCountdown/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /MenubarCountdown/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /MenubarCountdown/Assets.xcassets/HourglassIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "HourglassIconBlack.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "hourglass_36x36.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "template" 24 | } 25 | } -------------------------------------------------------------------------------- /MenubarCountdown/Assets.xcassets/HourglassIcon.imageset/HourglassIconBlack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristopherjohnson/MenubarCountdown/c5f4b6e903d61ae883b1567c9940ebe8d8a4c375/MenubarCountdown/Assets.xcassets/HourglassIcon.imageset/HourglassIconBlack.png -------------------------------------------------------------------------------- /MenubarCountdown/Assets.xcassets/HourglassIcon.imageset/hourglass_36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristopherjohnson/MenubarCountdown/c5f4b6e903d61ae883b1567c9940ebe8d8a4c375/MenubarCountdown/Assets.xcassets/HourglassIcon.imageset/hourglass_36x36.png -------------------------------------------------------------------------------- /MenubarCountdown/CALayerExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CALayerExtensions.swift 3 | // MenubarCountdown 4 | // 5 | // Copyright © 2009,2015,2019 Kristopher Johnson. All rights reserved. 6 | 7 | import Cocoa 8 | 9 | // MARK: Add/remove blink animation 10 | extension CALayer { 11 | /** 12 | Unique key used to identify animation managed by `addBlinkAnimation` and `removeBlinkAnimation`. 13 | */ 14 | @nonobjc static let blinkAnimationKey 15 | = "MenubarCountdown_CALayerExtensions_BlinkAnimation" 16 | 17 | /** 18 | Add a repeating blinking animiation to the layer. 19 | 20 | Call `removeBlinkAnimation` to stop the animation. 21 | */ 22 | func addBlinkAnimation() { 23 | if let _ = animation(forKey: CALayer.blinkAnimationKey) { 24 | return 25 | } 26 | 27 | let animation = CABasicAnimation(keyPath: "opacity") 28 | 29 | // Repeat forever, once per second 30 | animation.repeatCount = Float.infinity 31 | animation.duration = 0.5 32 | animation.autoreverses = true 33 | 34 | // Cycle between 0 and full opacity 35 | animation.fromValue = 0.0 36 | animation.toValue = 1.0 37 | animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) 38 | 39 | add(animation, forKey: CALayer.blinkAnimationKey) 40 | } 41 | 42 | /** 43 | Remove animation added by `addBlinkAnimation`. 44 | */ 45 | func removeBlinkAnimation() { 46 | removeAnimation(forKey: CALayer.blinkAnimationKey) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /MenubarCountdown/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2513 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | \vieww15480\viewh11640\viewkind0 6 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 7 | 8 | \f0\fs24 \cf0 For information and updates, visit\ 9 | {\field{\*\fldinst{HYPERLINK "https://github.com/kristopherjohnson/MenubarCountdown"}}{\fldrslt https://github.com/kristopherjohnson/MenubarCountdown}}\ 10 | \ 11 | \'a9 2009,2015,2019,2020 Kristopher Johnson\ 12 | \ 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\ 14 | \ 15 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\ 16 | \ 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\ 18 | } -------------------------------------------------------------------------------- /MenubarCountdown/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LSUIElement 6 | 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIconFile 12 | MenuTimerIcon 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 2.2 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | 201016 27 | LSApplicationCategoryType 28 | public.app-category.utilities 29 | LSMinimumSystemVersion 30 | $(MACOSX_DEPLOYMENT_TARGET) 31 | NSHumanReadableCopyright 32 | © 2009,2015,2019,2020 Kristopher Johnson 33 | NSMainNibFile 34 | MainMenu 35 | NSPrincipalClass 36 | NSApplication 37 | NSServices 38 | 39 | 40 | NSMessage 41 | startCountdown 42 | NSPortName 43 | Menubar Countdown 44 | NSServiceDescription 45 | Show the Menubar Countdown start dialog 46 | NSMenuItem 47 | 48 | default 49 | Start Menubar Countdown 50 | 51 | 52 | 53 | NSMessage 54 | stopCountdown 55 | NSPortName 56 | Menubar Countdown 57 | NSServiceDescription 58 | Stop Menubar Countdown timer 59 | NSMenuItem 60 | 61 | default 62 | Stop Menubar Countdown 63 | 64 | 65 | 66 | NSMessage 67 | pauseCountdown 68 | NSPortName 69 | Menubar Countdown 70 | NSServiceDescription 71 | Pause the Menubar Countdown timer 72 | NSMenuItem 73 | 74 | default 75 | Pause Menubar Countdown 76 | 77 | 78 | 79 | NSMessage 80 | resumeCountdown 81 | NSPortName 82 | Menubar Countdown 83 | NSServiceDescription 84 | Resume the Menubar Countdown timer when paused 85 | NSMenuItem 86 | 87 | default 88 | Resume Menubar Countdown 89 | 90 | 91 | 92 | NSAppleScriptEnabled 93 | 94 | OSAScriptingDefinition 95 | MenubarCountdown.sdef 96 | 97 | 98 | -------------------------------------------------------------------------------- /MenubarCountdown/Log.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Log.swift 3 | // MenubarCountdown 4 | // 5 | // Copyright © 2009,2015,2019 Kristopher Johnson. All rights reserved. 6 | 7 | import Foundation 8 | import os.log 9 | 10 | /** 11 | Application logging functions. 12 | 13 | All app components should call these functions instead of using `NSLog()` or `os_log()`. 14 | */ 15 | struct Log { 16 | 17 | /** 18 | Log an error message. 19 | 20 | - parameters: 21 | - message: Text to be logged. 22 | - function: Name of calling function. 23 | - file: Source file name of calling function. 24 | - line: Source line number of call location. 25 | */ 26 | static func error(_ message: String, 27 | function: String = #function, file: String = #file, line: Int32 = #line) 28 | { 29 | let filename = file.lastPathComponent 30 | 31 | let msg = "ERROR: \(message) [\(function) \(filename):\(line)]" 32 | os_log("%{public}@", type: .error, msg) 33 | } 34 | 35 | /** 36 | Log a debug-level message. 37 | 38 | Has no effect in a Release build. 39 | 40 | - parameters: 41 | - message: Text to be logged. 42 | - function: Name of calling function. 43 | - file: Source file name of calling function. 44 | - line: Source line number of call location. 45 | */ 46 | static func debug(_ message: String, 47 | function: String = #function, file: String = #file, line: Int32 = #line) 48 | { 49 | #if DEBUG 50 | let filename = file.lastPathComponent 51 | 52 | let msg = "DEBUG: \(message) [\(function) \(filename):\(line)]" 53 | 54 | // Note: we log using default level rather than .debug so that the 55 | // messages always go into the log for a debug build 56 | os_log("%{public}@", msg) 57 | #endif 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /MenubarCountdown/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /MenubarCountdown/MenuTimerIcon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristopherjohnson/MenubarCountdown/c5f4b6e903d61ae883b1567c9940ebe8d8a4c375/MenubarCountdown/MenuTimerIcon.icns -------------------------------------------------------------------------------- /MenubarCountdown/MenubarCountdown.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MenubarCountdown/MenubarCountdown.sdef: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | 44 | 45 | 49 | 50 | 51 | 56 | 60 | 61 | 62 | 66 | 67 | 68 | 74 | 75 | 76 | 82 | 83 | 84 | 90 | 91 | 92 | 97 | 98 | 99 | 104 | 105 | 106 | 111 | 112 | 113 | 118 | 119 | 120 | 125 | 126 | 127 | 132 | 133 | 134 | 139 | 140 | 141 | 146 | 147 | 148 | 153 | 154 | 155 | 160 | 161 | 162 | 167 | 168 | 169 | 174 | 175 | 176 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 |  204 | 205 | -------------------------------------------------------------------------------- /MenubarCountdown/NSApplication_Scripting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSApplication_Scripting.swift 3 | // Menubar Countdown 4 | // 5 | // Copyright © 2019 Kristopher Johnson. All rights reserved. 6 | 7 | import Cocoa 8 | 9 | /* 10 | Extend `NSApplication` to support scripting commands. 11 | 12 | See `MenubarCountdown.sdef` for command definitions. 13 | 14 | See `AppDelegate_Scripting.swift` for implementations of 15 | scriptable properties. 16 | */ 17 | 18 | extension NSApplication { 19 | 20 | /** 21 | Perform the `start timer` scripting command. 22 | */ 23 | @objc func startTimerViaScript(_ command: NSScriptCommand) { 24 | Log.debug("\"start timer\" was invoked") 25 | 26 | if let appDelegate = NSApplication.shared.delegate as? AppDelegate { 27 | appDelegate.startTimerViaScript(command: command) 28 | } 29 | } 30 | 31 | /** 32 | Perform the `stop timer` scripting command. 33 | */ 34 | @objc func stopTimerViaScript(_ command: NSScriptCommand) { 35 | Log.debug("\"stop timer\" was invoked") 36 | 37 | if let appDelegate = NSApplication.shared.delegate as? AppDelegate { 38 | appDelegate.stopTimer(command) 39 | } 40 | } 41 | 42 | /** 43 | Perform the `pause timer` scripting command. 44 | */ 45 | @objc func pauseTimerViaScript(_ command: NSScriptCommand) { 46 | Log.debug("\"pause timer\" was invoked") 47 | 48 | if let appDelegate = NSApplication.shared.delegate as? AppDelegate { 49 | appDelegate.pauseTimer(command) 50 | } 51 | } 52 | 53 | /** 54 | Perform the `resume timer` scripting command. 55 | */ 56 | @objc func resumeTimerViaScript(_ command: NSScriptCommand) { 57 | Log.debug("\"resume timer\" was invoked") 58 | 59 | if let appDelegate = NSApplication.shared.delegate as? AppDelegate { 60 | appDelegate.resumeTimer(command) 61 | } 62 | } 63 | 64 | /** 65 | Perform the `show start dialog` scripting command. 66 | */ 67 | @objc func showStartDialogViaScript(_ command: NSScriptCommand) { 68 | Log.debug("\"show start dialog\" was invoked") 69 | 70 | if let appDelegate = NSApplication.shared.delegate as? AppDelegate { 71 | appDelegate.showStartTimerDialog(command) 72 | } 73 | } 74 | 75 | /** 76 | Perform the `quit` scripting command. 77 | 78 | Note that this application's `quit` is not defined as part of 79 | the Standard Suite in `MenubarCountdown.sdef`. I couldn't 80 | get the standard Cocoa implementation to work reliably; sometimes 81 | it would work but other times the application would stay open. 82 | I couldn't figure out why. 83 | 84 | So we define it as part of the Menubar Countdown suite to 85 | bypass all of the Cocoa/NSApplication implementation. 86 | (There is probably a better way to deal with this.) 87 | */ 88 | @objc func quitViaScript(_ command: NSScriptCommand) { 89 | Log.debug("\"quit\" was invoked") 90 | 91 | // We can't just call `terminate()` here, because the caller 92 | // is waiting for a response and the scripting machinery will 93 | // keep the process alive until it gets it. So queue up a 94 | // call to `terminate()` at the earliest opportunity. 95 | DispatchQueue.main.async { 96 | self.terminate(command) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /MenubarCountdown/Notifications.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notifications.swift 3 | // Menubar Countdown 4 | // 5 | // Copyright © 2019 Kristopher Johnson. All rights reserved. 6 | 7 | import Cocoa 8 | import UserNotifications 9 | 10 | /** 11 | Request appropriate authorization for displaying user notification. 12 | 13 | - parameters: 14 | - withSound: whether to request playing a sound with the notification 15 | - completionHandler: called with argument indicating whether authorization was granted. 16 | */ 17 | func requestNotificationAuthorization(withSound: Bool, 18 | completionHandler: @escaping (Bool) -> Void) { 19 | let authorizationOptions: UNAuthorizationOptions = 20 | withSound ? [.alert, .sound] 21 | : [.alert] 22 | 23 | let notificationCenter = UNUserNotificationCenter.current() 24 | notificationCenter.requestAuthorization(options: authorizationOptions) { (granted, error) in 25 | if let error = error { 26 | Log.debug("user notification authorization error: \(error)") 27 | } 28 | 29 | if granted { 30 | completionHandler(true) 31 | } 32 | else { 33 | Log.debug("user notification authorization was not granted") 34 | completionHandler(false) 35 | } 36 | } 37 | } 38 | 39 | /** 40 | Display user notification indicating timer is expired. 41 | 42 | - parameters: 43 | - withSound: whether to play a sound along with the notification 44 | 45 | - returns: notification ID 46 | */ 47 | func showTimerExpiredNotification(withSound: Bool) -> String { 48 | Log.debug("show timer-expired notification") 49 | 50 | let notificationId = UUID().uuidString 51 | 52 | let notificationCenter = UNUserNotificationCenter.current() 53 | 54 | notificationCenter.getNotificationSettings { settings in 55 | guard settings.authorizationStatus == .authorized else { 56 | Log.debug("not authorized to display notifications") 57 | return 58 | } 59 | 60 | if settings.alertSetting == .enabled { 61 | let content = UNMutableNotificationContent() 62 | content.title = NSLocalizedString("Menubar Countdown Expired", 63 | comment: "Notification title") 64 | content.body = NSLocalizedString("The countdown timer has reached 00:00:00", 65 | comment: "Notification body") 66 | 67 | if withSound && settings.soundSetting == .enabled { 68 | content.sound = UNNotificationSound.default 69 | } 70 | 71 | let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, 72 | repeats: false) 73 | 74 | let request = UNNotificationRequest(identifier: notificationId, 75 | content: content, 76 | trigger: trigger) 77 | 78 | UNUserNotificationCenter.current().add(request) { error in 79 | if let error = error { 80 | Log.error("notification error: \(error.localizedDescription)") 81 | } 82 | } 83 | } 84 | } 85 | 86 | return notificationId 87 | } 88 | -------------------------------------------------------------------------------- /MenubarCountdown/ServicesProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServicesProvider.swift 3 | // Menubar Countdown 4 | // 5 | // Copyright © 2019 Kristopher Johnson. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import Cocoa 10 | 11 | /** 12 | Implements the Services menu items for the application. 13 | 14 | The provided services are 15 | 16 | - Start Countdown: show the start dialog 17 | - Stop Countdown: reset the timer 18 | - Pause Countdown: pause the timer 19 | - Resume Countdown: resume paused timer 20 | 21 | See also 22 | 23 | - The `NSServices` entries in `Info.plist` 24 | - Construction and registration of the service provider in `AppDelegate.applicationDidFinishLaunching()` 25 | 26 | */ 27 | @objc final class ServicesProvider: NSObject { 28 | 29 | private var appDelegate: AppDelegate 30 | 31 | init(appDelegate: AppDelegate) { 32 | self.appDelegate = appDelegate 33 | } 34 | 35 | /** 36 | Handle a Start Countdown service request. 37 | */ 38 | @objc func startCountdown(_ pboard: NSPasteboard, userData: String, error: NSErrorPointer) { 39 | Log.debug("Start Countdown service was requested") 40 | appDelegate.showStartTimerDialog(self) 41 | } 42 | 43 | /** 44 | Handle a Stop Countdown service request. 45 | */ 46 | @objc func stopCountdown(_ pboard: NSPasteboard, userData: String, error: NSErrorPointer) { 47 | Log.debug("Stop Countdown service was requested") 48 | appDelegate.stopTimer(self) 49 | } 50 | 51 | /** 52 | Handle a Pause Countdown service request. 53 | */ 54 | @objc func pauseCountdown(_ pboard: NSPasteboard, userData: String, error: NSErrorPointer) { 55 | Log.debug("Pause Countdown service was requested") 56 | appDelegate.pauseTimer(self) 57 | } 58 | 59 | /** 60 | Handle a Resume Countdown service request. 61 | */ 62 | @objc func resumeCountdown(_ pboard: NSPasteboard, userData: String, error: NSErrorPointer) { 63 | Log.debug("Resume Countdown service was requested") 64 | appDelegate.resumeTimer(self) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /MenubarCountdown/Speech.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Speech.swift 3 | // Menubar Countdown 4 | // 5 | // Copyright © 2019 Kristopher Johnson. All rights reserved. 6 | 7 | import Cocoa 8 | 9 | /** 10 | Speak the configured timer-expired announcement. 11 | 12 | - parameters: 13 | - text: announcement to be spoken 14 | */ 15 | func speakTimerExpiredAnnouncement(text: String) { 16 | Log.debug("speaking announcement \"\(text)\"") 17 | if let synth = NSSpeechSynthesizer(voice: nil) { 18 | synth.startSpeaking(text) 19 | } 20 | else { 21 | Log.error("unable to initialize speech synthesizer") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MenubarCountdown/StartTimerDialog.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 192 | 205 | 219 | 235 | 248 | 262 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 302 | 318 | 334 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | -------------------------------------------------------------------------------- /MenubarCountdown/StartTimerDialogController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartTimerDialogController.swift 3 | // MenubarCountdown 4 | // 5 | // Copyright © 2009,2015,2019 Kristopher Johnson. All rights reserved. 6 | 7 | import Cocoa 8 | 9 | /** 10 | Window controller for the "Menubar Countdown Settings" dialog that is used to start the countdown. 11 | 12 | Also see StartTimerDialog.xib. 13 | */ 14 | final class StartTimerDialogController: NSWindowController { 15 | @IBOutlet var startTimerDialog: NSWindow! 16 | 17 | /** 18 | Load the dialog from the nib. 19 | 20 | The `owner` argument must be an object with a `startTimerDialogController` 21 | property, which will be set to an instance of this class. 22 | */ 23 | static func load(owner: NSObject) { 24 | Bundle.main.loadNibNamed("StartTimerDialog", 25 | owner: owner, 26 | topLevelObjects: nil) 27 | } 28 | 29 | /** 30 | Display the dialog and bring it to the front. 31 | */ 32 | func showDialog() { 33 | NSApp.activate(ignoringOtherApps: true) 34 | if !startTimerDialog.isVisible { 35 | startTimerDialog.center() 36 | startTimerDialog.makeFirstResponder(nil) 37 | } 38 | startTimerDialog.makeKeyAndOrderFront(self) 39 | } 40 | 41 | /** 42 | Hide the dialog. 43 | */ 44 | @IBAction func dismissDialog(_ sender: AnyObject) { 45 | if !startTimerDialog.makeFirstResponder(nil) { 46 | Log.error("first responder didn't resign") 47 | } 48 | startTimerDialog.orderOut(sender); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MenubarCountdown/Stopwatch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stopwatch.swift 3 | // MenubarCountdown 4 | // 5 | // Copyright © 2009,2015,2019 Kristopher Johnson. All rights reserved. 6 | 7 | import Cocoa 8 | 9 | /** 10 | Timekeeping object 11 | 12 | A Stopwatch computes the absolute time interval between 13 | the current time and the last call to `reset` (or `init`). 14 | */ 15 | @objc final class Stopwatch: NSObject { 16 | 17 | /** 18 | Start of timing interval 19 | */ 20 | private var startTime: TimeInterval 21 | 22 | /** 23 | Initialize with current time as start point. 24 | */ 25 | override init() { 26 | startTime = CACurrentMediaTime() 27 | super.init() 28 | } 29 | 30 | /** 31 | Reset start point to current time. 32 | */ 33 | func reset() { 34 | startTime = CACurrentMediaTime() 35 | } 36 | 37 | /** 38 | Calculate elapsed time since initialization or last call to `reset`. 39 | */ 40 | func elapsedTimeInterval() -> TimeInterval { 41 | return CACurrentMediaTime() - startTime 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /MenubarCountdown/StringExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringExtensions.swift 3 | // MenubarCountdown 4 | // 5 | // Copyright © 2009,2015,2019 Kristopher Johnson. All rights reserved. 6 | 7 | import Foundation 8 | 9 | extension String { 10 | /// The last path component of the string 11 | var lastPathComponent: String { 12 | return (self as NSString).lastPathComponent 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MenubarCountdown/TextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextField.swift 3 | // MenubarCountdown 4 | // 5 | // Copyright © 2009,2015,2019 Kristopher Johnson. All rights reserved. 6 | 7 | import Cocoa 8 | 9 | /** 10 | Subclass of NSTextField that handles Cmd-X, Cmd-C, Cmd-V, and Cmd-A 11 | 12 | This class is used instead of the standard NSTextField in the Start.. dialog 13 | to allow the user to use the standard edit keyboard shortcuts even though the 14 | application has no Edit menu. 15 | 16 | This class is based on code found at 17 | which was written by James Huddleston, and improvements discussed at 18 | . 19 | */ 20 | final class TextField: NSTextField { 21 | override func performKeyEquivalent(with key: NSEvent) -> Bool { 22 | // Map Command-X to Cut 23 | // Command-C to Copy 24 | // Command-V to Paste 25 | // Command-A to Select All 26 | if key.type == .keyDown { 27 | let commandKeyMask = NSEvent.ModifierFlags.command.rawValue 28 | 29 | let modifierFlagsMask = key.modifierFlags.rawValue 30 | & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue 31 | 32 | if modifierFlagsMask == commandKeyMask { 33 | if let chars = key.charactersIgnoringModifiers { 34 | switch chars { 35 | case "x": 36 | return sendToFirstResponder(#selector(NSText.cut(_:))) 37 | case "c": 38 | return sendToFirstResponder(#selector(NSObject.copy as () -> Any)) 39 | case "v": 40 | return sendToFirstResponder(#selector(NSText.paste(_:))) 41 | case "a": 42 | return sendToFirstResponder(#selector(NSStandardKeyBindingResponding.selectAll(_:))) 43 | default: 44 | break 45 | } 46 | } 47 | } 48 | } 49 | 50 | return super.performKeyEquivalent(with: key) 51 | } 52 | 53 | /** 54 | Send the specified selector to the first responder. 55 | 56 | - parameter action: The selector to be sent. 57 | 58 | - returns: `true` if the action was successfully sent; otherwise `false`. 59 | */ 60 | func sendToFirstResponder(_ action: Selector) -> Bool { 61 | return NSApp.sendAction(action, 62 | to: self.window?.firstResponder, 63 | from: self) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /MenubarCountdown/TimerExpiredAlert.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 71 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /MenubarCountdown/TimerExpiredAlertController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimerExpiredAlertController.swift 3 | // MenubarCountdown 4 | // 5 | // Copyright © 2009,2015,2019 Kristopher Johnson. All rights reserved. 6 | 7 | import Cocoa 8 | 9 | /** 10 | Alert displayed when the timer reaches 00:00:00. 11 | 12 | Also see TimerExpiredAlert.xib. 13 | */ 14 | final class TimerExpiredAlertController: NSWindowController { 15 | @IBOutlet var messageText: NSTextField! 16 | 17 | /** 18 | Load the alert from the NIB. 19 | 20 | The `owner` must be an object with a `timerExpiredAlertController` property, 21 | which will be set to an instance of this class. 22 | */ 23 | static func load(owner: NSObject) { 24 | Bundle.main.loadNibNamed("TimerExpiredAlert", 25 | owner: owner, 26 | topLevelObjects: nil) 27 | } 28 | 29 | /** 30 | Show the alert and bring it to the front. 31 | */ 32 | func showAlert() { 33 | if let w = self.window { 34 | w.makeFirstResponder(nil) 35 | w.level = .floating 36 | w.center() 37 | w.makeKeyAndOrderFront(self) 38 | } 39 | else { 40 | Log.error("no alert window") 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /MenubarCountdown/UserDefaults.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TimerHours 6 | 0 7 | TimerMinutes 8 | 25 9 | TimerSeconds 10 | 0 11 | BlinkOnExpiration 12 | 13 | PlayAlertSoundOnExpiration 14 | 15 | RepeatAlertSoundOnExpiration 16 | 17 | AlertSoundRepeatInterval 18 | 5 19 | AnnounceExpiration 20 | 21 | AnnouncementText 22 | The Menubar Countdown timer has reached zero. 23 | ShowAlertWindowOnExpiration 24 | 25 | ShowStartDialogOnLaunch 26 | 27 | ShowSeconds 28 | 29 | ShowNotificationOnExpiration 30 | 31 | PlaySoundWithNotification 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /MenubarCountdownTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /MenubarCountdownTests/MenubarCountdownTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenubarCountdownTests.swift 3 | // MenubarCountdownTests 4 | // 5 | // Copyright © 2009,2015,2019 Kristopher Johnson. All rights reserved. 6 | 7 | import XCTest 8 | @testable import MenubarCountdown 9 | 10 | final class MenubarCountdownTests: XCTestCase { 11 | 12 | override func setUp() { 13 | super.setUp() 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDown() { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | super.tearDown() 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /MenubarCountdownUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /MenubarCountdownUITests/MenubarCountdownUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenubarCountdownUITests.swift 3 | // MenubarCountdownUITests 4 | // 5 | // Copyright © 2009,2015,2019 Kristopher Johnson. All rights reserved. 6 | 7 | import XCTest 8 | 9 | final class MenubarCountdownUITests: XCTestCase { 10 | 11 | override func setUp() { 12 | super.setUp() 13 | 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | 16 | // In UI tests it is usually best to stop immediately when a failure occurs. 17 | continueAfterFailure = false 18 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 19 | XCUIApplication().launch() 20 | 21 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 22 | } 23 | 24 | override func tearDown() { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | super.tearDown() 27 | } 28 | 29 | func testExample() { 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Menubar Countdown 2 | ----------------- 3 | 4 | _Menubar Countdown_ is a simple countdown timer displayed in the 5 | macOS menu bar. It can be used as a [pomodoro timer](https://en.wikipedia.org/wiki/Pomodoro_Technique), 6 | to remind yourself to get back to work after a break, or whenever you want 7 | to quickly set a time limit on some activity. 8 | 9 | Screenshot 10 | 11 | To set the timer, click the menu bar icon and select the **Start...** menu item. 12 | A dialog will appear allowing you to specify the countdown time in hours, 13 | minutes, and seconds. The dialog also allows you to specify which of the 14 | following forms of notification you want when the timer gets down to 00:00:00: 15 | 16 | - Blink the icon in the menu bar. 17 | - Play the system alert sound. 18 | - Display an alert window. 19 | - Display a notification in Notification Center. 20 | - Make a spoken announcement. You can specify the text to be spoken. 21 | 22 | The countdown timer can be controlled using AppleScript, JavaScript, or Swift. 23 | See the examples in the 24 | [Scripts](https://github.com/kristopherjohnson/MenubarCountdown/tree/master/Scripts) 25 | directory for details. 26 | 27 | Releases are available from: 28 | - The [Mac App Store](https://apps.apple.com/us/app/menubar-countdown/id1485343244?mt=12) 29 | - 30 | - [Homebrew](https://formulae.brew.sh/cask/menubar-countdown) (e.g. `brew install --cask menubar-countdown`) 31 | 32 | Download on the Mac App Store 33 | 34 | The current [2.1 version](https://github.com/kristopherjohnson/MenubarCountdown/releases/tag/2.1) 35 | of Menubar Countdown requires macOS Mojave 10.14.4 or 36 | newer. It is compatible with macOS Catalina 10.15. 37 | 38 | For macOS versions 10.6 through 10.14.3, use 39 | [version 1.3](https://github.com/kristopherjohnson/MenubarCountdown/releases/tag/1.3). 40 | 41 | For macOS 10.5, use version 1.2, available from 42 | 43 | 44 | To build in XCode 45 | - Product > Clean Build Folder 46 | - Change "Team" under "Signing & Capabilities" 47 | 48 | ## License 49 | 50 | Copyright 2009,2015,2019,2020 Kristopher Johnson 51 | 52 | Permission is hereby granted, free of charge, to any person obtaining a 53 | copy of this software and associated documentation files (the 54 | "Software"), to deal in the Software without restriction, including 55 | without limitation the rights to use, copy, modify, merge, publish, 56 | distribute, sublicense, and/or sell copies of the Software, and to 57 | permit persons to whom the Software is furnished to do so, subject to 58 | the following conditions: 59 | 60 | The above copyright notice and this permission notice shall be included 61 | in all copies or substantial portions of the Software. 62 | 63 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 64 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 65 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 66 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 67 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 68 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 69 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 70 | 71 | 72 | ## Release Notes 73 | 74 | v2.2 (in progress) 75 | 76 | - Support Command+drag icon from menu bar to terminate 77 | - Add tolerance to once-per-second timer to improve energy usage 78 | 79 | v2.1 (2019/11/10) 80 | 81 | - Add scriptable interface for AppleScript and OSA languages. 82 | - Add service-provider interface for control via the Services menu in other applications. 83 | - High-resolution menubar icon for Retina displays. 84 | 85 | v2.0 (2019/10/28) 86 | 87 | - Updated for macOS 10.14.4 and newer. 88 | - Distributed through the Mac App Store. 89 | - Supports dark mode. 90 | - When timer is not active, menubar displays small hourglass icon rather than 00:00:00, to conserve menubar space. 91 | - Added option to repeat alert sound after timer expiration, until it is acknowledged. 92 | - Added option to blink 00:00:00 in the menu bar after timer expiration, until it is acknowledged. 93 | - Added option to show a notification alert in Notification Center. 94 | - Added Pause and Resume menu items. 95 | - Translated code to Swift. 96 | 97 | v1.3 (2019/10/21) 98 | 99 | - Works on macOS 10.15 Catalina. 100 | - Minimum macOS version supported is now 10.6. (10.5 is no longer supported). 101 | - 32-bit processors are no longer supported. 102 | - Growl is no longer supported. 103 | 104 | v1.2 (2009/06/22) 105 | 106 | - New application icon 107 | - Command-X, Command-C, Command-V, and Command-A now work in the text fields in the settings dialog 108 | - Command-R is now a shortcut key for the Restart Countdown... button in the alert window 109 | - Add option to hide seconds in menu bar 110 | - Show start-timer dialog when application launches 111 | - Add Growl notifications. The Announcement text specified in the Start dialog will be displayed in the Growl notification window. 112 | 113 | v1.1 (2009/04/20) 114 | 115 | - timer-expired alert window floats above other applications' windows 116 | - added application icon 117 | - added Doxygen comments to source code 118 | 119 | v1.0 (2009/04/09) 120 | 121 | - initial release 122 | 123 | 124 | -------------------------------------------------------------------------------- /Scripts/AppleScript/Menubar Countdown System Events Example.applescript: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristopherjohnson/MenubarCountdown/c5f4b6e903d61ae883b1567c9940ebe8d8a4c375/Scripts/AppleScript/Menubar Countdown System Events Example.applescript -------------------------------------------------------------------------------- /Scripts/AppleScript/Pause Timer.applescript: -------------------------------------------------------------------------------- 1 | tell application "Menubar Countdown" to pause timer -------------------------------------------------------------------------------- /Scripts/AppleScript/README.md: -------------------------------------------------------------------------------- 1 | The scripts in this directory are examples of using AppleScript to control Menubar Countdown. 2 | 3 | Open the scripts in the macOS Script Editor application to run them. 4 | 5 | -------------------------------------------------------------------------------- /Scripts/AppleScript/Resume Timer.applescript: -------------------------------------------------------------------------------- 1 | tell application "Menubar Countdown" to resume timer -------------------------------------------------------------------------------- /Scripts/AppleScript/Start 15 Minute Timer.applescript: -------------------------------------------------------------------------------- 1 | -- Start a 15-minute timer 2 | 3 | tell application "Menubar Countdown" 4 | 5 | set hours to 0 6 | set minutes to 15 7 | set seconds to 0 8 | 9 | start timer 10 | 11 | end tell 12 | -------------------------------------------------------------------------------- /Scripts/AppleScript/Start One Hour Timer.applescript: -------------------------------------------------------------------------------- 1 | -- Start a one hour timer 2 | 3 | tell application "Menubar Countdown" 4 | 5 | set hours to 1 6 | set minutes to 0 7 | set seconds to 0 8 | 9 | start timer 10 | 11 | end tell 12 | -------------------------------------------------------------------------------- /Scripts/AppleScript/Start Pomodoro Timer.applescript: -------------------------------------------------------------------------------- 1 | -- Start a 25-minute pomodoro timer 2 | 3 | tell application "Menubar Countdown" 4 | 5 | set hours to 0 6 | set minutes to 25 7 | set seconds to 0 8 | 9 | set blink to true 10 | set play alert sound to false 11 | set show alert window to false 12 | -- Note: Notifications must be enabled for Menubar Countdown, or this won't work. 13 | set show notification to true 14 | set play notification sound to false 15 | set speak announcement to true 16 | set announcement text to "The timer has expired." 17 | 18 | set display seconds to true 19 | 20 | start timer 21 | 22 | end tell 23 | -------------------------------------------------------------------------------- /Scripts/AppleScript/Stop Timer.applescript: -------------------------------------------------------------------------------- 1 | tell application "Menubar Countdown" to stop timer -------------------------------------------------------------------------------- /Scripts/AppleScript/Test All Settings.applescript: -------------------------------------------------------------------------------- 1 | -- This script sets the values of all elements of the Settings window, 2 | -- pauses for two seconds, then sets all the elements to different values. 3 | 4 | tell application "Menubar Countdown" 5 | activate 6 | 7 | show start dialog 8 | 9 | set hours to 99 10 | set minutes to 59 11 | set seconds to 59 12 | 13 | set blink to false 14 | set play alert sound to false 15 | set repeat alert sound to false 16 | set show alert window to false 17 | set show notification to false 18 | set play notification sound to false 19 | set speak announcement to false 20 | 21 | set announcement text to "The checkboxes should all be off now." 22 | 23 | set display seconds to false 24 | set show start dialog on launch to false 25 | 26 | delay 2 27 | 28 | set hours to 0 29 | set minutes to 25 30 | set seconds to 0 31 | 32 | set blink to true 33 | set play alert sound to true 34 | set repeat alert sound to true 35 | set show alert window to true 36 | set show notification to true 37 | set play notification sound to true 38 | set speak announcement to true 39 | 40 | set announcement text to "The checkboxes should all be on now." 41 | 42 | set display seconds to true 43 | set show start dialog on launch to true 44 | end tell 45 | -------------------------------------------------------------------------------- /Scripts/AppleScript/Test Menubar Countdown Commands.applescript: -------------------------------------------------------------------------------- 1 | -- Demonstration of using these commands: 2 | -- 3 | -- - show start dialog 4 | -- - start timer 5 | -- - pause timer 6 | -- - resume timer 7 | -- - stop timer 8 | 9 | tell application "Menubar Countdown" 10 | 11 | activate 12 | 13 | show start dialog 14 | 15 | say "Setting up test" 16 | 17 | set hours to 0 18 | set minutes to 0 19 | set seconds to 30 20 | set display seconds to true 21 | set announcement text to "Testing Menubar Countdown commands." 22 | 23 | delay 2 24 | 25 | start timer 26 | say "Timer started." 27 | say (time remaining as text) & " seconds remaining." 28 | 29 | if timer has expired then 30 | display dialog "Timer has expired, but should not have." 31 | end if 32 | if timer is paused then 33 | display dialog "Timer is paused, but should not be." 34 | end if 35 | 36 | delay 5 37 | 38 | pause timer 39 | if timer is paused then 40 | say "Timer is paused." 41 | say (time remaining as text) & " seconds remaining." 42 | else 43 | display dialog "Timer should be paused, but is not." 44 | end if 45 | 46 | delay 5 47 | 48 | resume timer 49 | if timer is paused then 50 | display dialog "Timer should not be paused, but is." 51 | else 52 | say "Timer resumed." 53 | say (time remaining as text) & " seconds remaining." 54 | end if 55 | 56 | delay 5 57 | 58 | stop timer 59 | say "Timer stopped." 60 | say (time remaining as text) & " seconds remaining." 61 | 62 | quit 63 | end tell 64 | -------------------------------------------------------------------------------- /Scripts/AppleScript/Test Standard Suite.applescript: -------------------------------------------------------------------------------- 1 | -- Demonstrates how to use these Standard Suite scripting properties: 2 | -- 3 | -- - name 4 | -- - version 5 | -- - frontmost 6 | 7 | tell application "Menubar Countdown" 8 | activate 9 | 10 | show start dialog 11 | 12 | say "The name of the application is " & name & "." 13 | say "The version is " & version & "." 14 | if frontmost then 15 | say "The application is frontmost." 16 | else 17 | say "The application is not frontmost." 18 | end if 19 | 20 | quit 21 | end tell 22 | -------------------------------------------------------------------------------- /Scripts/JavaScript/README.md: -------------------------------------------------------------------------------- 1 | The scripts in this directory are examples of using JavaScript to control Menubar Countdown. 2 | 3 | Open the scripts in the macOS Script Editor application to run them. 4 | 5 | -------------------------------------------------------------------------------- /Scripts/JavaScript/TestAllSettings.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristopherjohnson/MenubarCountdown/c5f4b6e903d61ae883b1567c9940ebe8d8a4c375/Scripts/JavaScript/TestAllSettings.scpt -------------------------------------------------------------------------------- /Scripts/JavaScript/TestMenubarCountdownCommands.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristopherjohnson/MenubarCountdown/c5f4b6e903d61ae883b1567c9940ebe8d8a4c375/Scripts/JavaScript/TestMenubarCountdownCommands.scpt -------------------------------------------------------------------------------- /Scripts/JavaScript/TestStandardSuite.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristopherjohnson/MenubarCountdown/c5f4b6e903d61ae883b1567c9940ebe8d8a4c375/Scripts/JavaScript/TestStandardSuite.scpt -------------------------------------------------------------------------------- /Scripts/README.md: -------------------------------------------------------------------------------- 1 | The subdirectories of this directory contain examples of scripting Menubar Countdown. 2 | -------------------------------------------------------------------------------- /Scripts/Swift/README.md: -------------------------------------------------------------------------------- 1 | This directory contains automation scripts that can be used to control Menubar 2 | Countdown from a command line or from utilities that can run scripts. 3 | 4 | These scripts require version 2.1 or higher of Menubar Countdown. 5 | 6 | The scripts are written in Swift. The `/usr/bin/swift` interpreter is available 7 | on the versions of macOS supported by Menubar Countdown 2.1 and higher. You can 8 | pass the scripts to the Swift interpreter on the command line like this: 9 | 10 | /usr/bin/swift start_menubar_countdown.swift 11 | 12 | or you can mark the scripts executable and run them directly like this: 13 | 14 | chmod +x start_menubar_countdown.swift 15 | ./start_menubar_countdown.swift 16 | -------------------------------------------------------------------------------- /Scripts/Swift/pause_menubar_countdown.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/swift 2 | 3 | /** 4 | This script pauses the Menubar Countdown settings dialog, as if the user chose 5 | the "Pause" menu item. 6 | 7 | To run this script from the command line, do this: 8 | 9 | /usr/bin/swift pause_menubar_countdown.swift 10 | 11 | or make it executable and then just run it directly, like this: 12 | 13 | chmod +x pause_menubar_countdown.swift 14 | ./pause_menubar_countdown.swift 15 | */ 16 | 17 | import AppKit 18 | import Darwin 19 | 20 | let success = NSPerformService("Pause Menubar Countdown", nil) 21 | 22 | exit(success ? 0 : 1) 23 | -------------------------------------------------------------------------------- /Scripts/Swift/resume_menubar_countdown.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/swift 2 | 3 | /** 4 | This script resumes the Menubar Countdown timer, as if the user chose the 5 | "Resume" menu item. 6 | 7 | To run this script from the command line, do this: 8 | 9 | /usr/bin/swift resume_menubar_countdown.swift 10 | 11 | or make it executable and then just run it directly, like this: 12 | 13 | chmod +x resume_menubar_countdown.swift 14 | ./resume_menubar_countdown.swift 15 | */ 16 | 17 | import AppKit 18 | import Darwin 19 | 20 | let success = NSPerformService("Resume Menubar Countdown", nil) 21 | 22 | exit(success ? 0 : 1) 23 | -------------------------------------------------------------------------------- /Scripts/Swift/start_menubar_countdown.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/swift 2 | 3 | /** 4 | This script shows the Menubar Countdown settings dialog, as if the user chose 5 | the "Start…" menu item. 6 | 7 | To run this script from the command line, do this: 8 | 9 | /usr/bin/swift start_menubar_countdown.swift 10 | 11 | or make it executable and then just run it directly, like this: 12 | 13 | chmod +x start_menubar_countdown.swift 14 | ./start_menubar_countdown.swift 15 | */ 16 | 17 | import AppKit 18 | import Darwin 19 | 20 | let success = NSPerformService("Start Menubar Countdown", nil) 21 | 22 | exit(success ? 0 : 1) 23 | -------------------------------------------------------------------------------- /Scripts/Swift/stop_menubar_countdown.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/swift 2 | 3 | /** 4 | This script stops the Menubar Countdown timer, as if the user chose the 5 | "Stop" menu item. 6 | 7 | To run this script from the command line, do this: 8 | 9 | /usr/bin/swift stop_menubar_countdown.swift 10 | 11 | or make it executable and then just run it directly, like this: 12 | 13 | chmod +x stop_menubar_countdown.swift 14 | ./stop_menubar_countdown.swift 15 | */ 16 | 17 | import AppKit 18 | import Darwin 19 | 20 | let success = NSPerformService("Stop Menubar Countdown", nil) 21 | 22 | exit(success ? 0 : 1) 23 | -------------------------------------------------------------------------------- /docs/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "make", 8 | "type": "shell", 9 | "command": "make", 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | } 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | DOT:=dot 2 | 3 | states.png : states.gv 4 | ${DOT} -Tpng -o states.png states.gv 5 | 6 | clean: 7 | - ${RM} states.png 8 | .phony: clean 9 | -------------------------------------------------------------------------------- /docs/MenubarCountdownSettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristopherjohnson/MenubarCountdown/c5f4b6e903d61ae883b1567c9940ebe8d8a4c375/docs/MenubarCountdownSettings.png -------------------------------------------------------------------------------- /docs/states.gv: -------------------------------------------------------------------------------- 1 | # State machine diagram for Menubar Countdown 2 | # 3 | # To generate PNG from this file: 4 | # 5 | # dot -T png -o states.png states.gv 6 | # 7 | # `dot` is part of Graphviz 8 | 9 | strict digraph States { 10 | graph [ 11 | forcelabels=true; 12 | compound=true; 13 | ]; 14 | 15 | node [ 16 | shape=Mrecord; 17 | fontsize=14; 18 | label="{ \N | }"; 19 | ]; 20 | 21 | edge [ 22 | fontsize=9; 23 | ]; 24 | 25 | # Initial state 26 | init [ 27 | shape=point; 28 | height=0.10; 29 | width=0.10; 30 | ]; 31 | 32 | # Terminal state 33 | exit [ 34 | shape=point; 35 | height=0.10; 36 | width=0.10; 37 | peripheries=2; 38 | ]; 39 | 40 | stopped [ 41 | label="{ \N | entry / resetTimer() }"; 42 | ]; 43 | 44 | dialog [ 45 | label="{ showing settings dialog | entry / resetTimer(); showDialog()\nexit / hideDialog() }"; 46 | ]; 47 | 48 | subgraph cluster_started { 49 | label="started"; 50 | 51 | running [ 52 | label="{ \N | entry / showTimer()\ndo / tickEachSecond() }" 53 | ]; 54 | paused [ 55 | ]; 56 | expired [ 57 | label="{ \N | entry / showExpired() }" 58 | ]; 59 | 60 | running -> paused [ label=PAUSE ]; 61 | paused -> running [ label=RESUME ]; 62 | 63 | decrement [ 64 | label=""; 65 | shape=diamond; 66 | fixedsize=true; 67 | height=0.25; 68 | width=0.25; 69 | ]; 70 | 71 | running -> decrement [ label="TICK / decrementTimer()" ]; 72 | decrement -> running [ label="[timer > 0]" ]; 73 | decrement -> expired [ label="[timer <= 0]" ]; 74 | } 75 | 76 | terminating [ 77 | ]; 78 | 79 | init -> stopped; 80 | 81 | { stopped, dialog } -> dialog [ 82 | label=SHOW_DIALOG; 83 | color=darkgreen; 84 | fontcolor=darkgreen; 85 | ]; 86 | running -> dialog [ 87 | label=SHOW_DIALOG; 88 | color=darkgreen; 89 | fontcolor=darkgreen; 90 | ltail=cluster_started; 91 | ]; 92 | 93 | dialog -> running [ label=START ]; 94 | dialog -> stopped [ label=CLOSE_DIALOG ]; 95 | 96 | # cluster_started -> stopped 97 | expired -> stopped [ 98 | label=STOP; 99 | ltail=cluster_started; 100 | ]; 101 | 102 | { stopped, dialog } -> terminating [ 103 | label=QUIT; 104 | color=red; 105 | fontcolor=red; 106 | ]; 107 | 108 | # cluster_started -> terminating 109 | running -> terminating [ 110 | label=QUIT; 111 | color=red; 112 | fontcolor=red; 113 | ltail=cluster_started; 114 | ]; 115 | 116 | terminating -> exit; 117 | } 118 | -------------------------------------------------------------------------------- /docs/states.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kristopherjohnson/MenubarCountdown/c5f4b6e903d61ae883b1567c9940ebe8d8a4c375/docs/states.png --------------------------------------------------------------------------------