diff --git a/Source/CountDown.spoon/init.lua b/Source/CountDown.spoon/init.lua index f91e4a50..8d4a971e 100644 --- a/Source/CountDown.spoon/init.lua +++ b/Source/CountDown.spoon/init.lua @@ -1,85 +1,642 @@ --- === CountDown === --- ---- Tiny countdown with visual indicator +--- Countdown with visual indicator --- --- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/CountDown.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/CountDown.spoon.zip) +--- local obj = {} obj.__index = obj -- Metadata obj.name = "CountDown" -obj.version = "1.0" -obj.author = "ashfinal " +obj.version = "2" +obj.author = "ashfinal and Daniel Marques and Omar El-Domeiri and Daniel German " obj.homepage = "https://github.com/Hammerspoon/Spoons" obj.license = "MIT - https://opensource.org/licenses/MIT" -obj.canvas = nil +-- This countdown timer has three different progress indicators: + +-- horizontal bar at the bottom of the screen (always enabled) +-- Warning: every few minutes, display the minutes left (optional) +-- MenuBar: an icon that displays the current state of the timer (optional) +-- it also displays the time left +-- +-- When the time is up an alert message is displayed. + +-- User-configurable variables + +--- CountDown.defaultLenMinutes +--- Variable +--- Default timer in minutes. +obj.defaultLenMinutes = 25 + +--- CountDown.useLastTimerAsDefault +--- Variable +--- if true, make defaultLenMinutes the last time length used +obj.useLastTimerAsDefault = true + +--- CountDown.nofity +--- Variable +--- set to nil to turn off notification when time's up or provide a hs.notify notification +obj.notify = true + +--- CountDown.defaultKeyBindings +--- Variable +--- default key bindings +obj.defaultKeyBindings = { + startFor = {{"cmd", "ctrl", "alt"}, "T"}, + startInteractive = {{"cmd", "ctrl", "alt", "shift"}, "T"}, + pauseOrResume = {{"cmd", "ctrl", "alt"}, "P"}, + cancel = {{"cmd", "ctrl", "alt"}, "C"} +} + +--- CountDown.messageDuration +--- Variable +--- Duration of notification messages +obj.messageDuration = 2 + +--- CountDown.messageAttributes +--- Variable +--- Properties of progress message +obj.messageAttributes = {atScreenEdge = 0, textSize = 40} + +-- bar: progress bar at the bottom of the screen + +--- CountDown.barCanvasHeight +--- Variable +--- indicator bar at the bottom of the screen +obj.barCanvasHeight = 5 + +--- CountDown.barTransparency +--- Variable +--- Transparency for progress bar +obj.barTransparency = 0.8 + +--- CountDown.barFillColorPassed +--- Variable +--- Color for time passed in progress bar +obj.barFillColorPassed = hs.drawing.color.osx_green + +--- CountDown.barFillColorToPass +--- Variable +--- Color for time to pass in progress bar +obj.barFillColorToPass = hs.drawing.color.osx_red + +-- alert: what happens when the timer is up? + +--- CountDown.alertLen +--- Variable +--- time to show the end-of-time alert. 0 implies do not show +obj.alertLen = 5 + +--- CountDown.alertSound +--- Variable +--- Sounds to play when time is up. No sound if nil +obj.alertSound = "Sonar" + +--- CountDown.alertAttributes +--- Variable +--- how to display timer is up notification +obj.alertAttributes = {atScreenEdge = 1} + +-- warning related configuration + +--- CountDown.warningShow +--- Variable +--- Do we show progress warnings. A progress warning happens +--- at logarithmic intervals: 1, 2, 4, 8, 16... minutes +--- before timer expiration +obj.warningShow = true + +--- CountDown.warningFormat +--- Variable +--- Format to display the warning. +--- It takes two integers: hours and minutes +obj.warningFormat = "Time left %02d:%02d" + +--- CountDown.warningshowDuration +--- Variable +--- for how many seconds to show the warning +obj.warningshowDuration = 3 + +-- menu bar related + +--- CountDown.menuBarAlwaysShow +--- Variable +--- If true, always show the menu bar icon. +--- if false, only show when timer active +--- (shows pause, cancel toggle) +obj.menuBarAlwaysShow=false + +--- CountDown.menuBarIconIdle +--- Variable +--- icon to show in menu bar when idle +obj.menuBarIconIdle = "⏰" +--- CountDown.menuBarIconActive +--- Variable +--- icon to show in menu bar when active +obj.menuBarIconActive = "☣️" +--- CountDown.menuBarIconPlay +--- Variable +--- icon for resume playing in menu bar submenu +obj.menuBarIconPlay = "▶️️" +--- CountDown.menuBarIconPause +--- Variable +--- icon for pause playing in menu bar submenu +obj.menuBarIconPause = "⏸️" +--- CountDown.menuBarIconStop +--- Variable +--- icon for cancelling timer in menu bar submenu +obj.menuBarIconStop = "🛑" + +-- state variables + +obj.barTimer= nil +-- timerRunning true if current timer running +obj.timerRunning = nil obj.timer = nil +-- time when timer should end, in absolute seconds +-- since epoch +obj.endingTime = nil +-- minutes the timer is expected to run +-- the actual length might be affected by pausing timer +obj.timerLenMinutes = nil + +obj.currentIcon = nil +-- moment the timer is paused +obj.pausedAt = nil +-- status bar +obj.menuBar = nil +-- progress bar related +-- proportion of time that has elapsed [0 to 1] +obj.barProgress = 0 +obj.barCanvas = nil + +-- events + +-- callback when timer is up +-- callback is only called when timer succeeds +-- if timer is cancelled, it is not called +-- takes two parms: event and minutes passed +obj.callback = nil +obj.timerEventResume = "resumed" +obj.timerEventPause = "paused" +obj.timerEventReset = "cancelled" +obj.timerEventStart = "started" +obj.timerEventEnd = "ended" +obj.timerEventSetProgress = "setProgress" + + function obj:init() - self.canvas = hs.canvas.new({x=0, y=0, w=0, h=0}):show() - self.canvas:behavior(hs.canvas.windowBehaviors.canJoinAllSpaces) - self.canvas:level(hs.canvas.windowLevels.status) - self.canvas:alpha(0.35) - self.canvas[1] = { - type = "rectangle", - action = "fill", - fillColor = hs.drawing.color.osx_red, - frame = {x="0%", y="0%", w="0%", h="100%"} - } - self.canvas[2] = { - type = "rectangle", - action = "fill", - fillColor = hs.drawing.color.osx_green, - frame = {x="0%", y="0%", w="0%", h="100%"} - } -end - ---- CountDown:startFor(minutes) + obj:bar_init() + obj:menuBar_init() + + obj:reset_timer() +end + +-- some support functions + +function obj:time_absolute_seconds() + -- return hs.timer.absoluteTime() in seconds + local timeNow = hs.timer.absoluteTime() + return math.floor(timeNow / 1e9) +end + +function obj:is_power_of_2(j) + -- is j a power of 2? + -- j is float... + i = math.floor(j) + if j ~= i then + return false + end + i = math.floor(i) + while (i % 2 == 0) do + i = i // 2 + end + return i ==1 +end + +function obj:show_message(msg, duration) + -- display any notification, including warnings + -- but not the final alert when time is up + if not duration then + duration = obj.messageDuration + end + print(">>>>>>>>>>>>>>>>", duration) + hs.alert.show(msg, obj.messageAttributes, nil, duration) +end + +function obj:reset_timer() + obj.timerRunning = false + + obj:timers_cleanup() + obj.timeLeft = 0 + + obj:bar_reset() + obj:menuBar_reset() +end + +function obj:timers_cleanup() + if obj.timer then + obj.timer:stop() + obj.timer = nil + end + if barTimer then + barTimer:stop() + barTimer = nil + end +end + +function obj:end_of_timer_notify(requestedMinutes) + -- end of timer notification + -- + -- we have 2 ways of notification: event and message + -- + -- save the window, so we can go back + -- when user acknowledges the timer + local currentWin = hs.window.focusedWindow() + + -- do callback if defined + if obj.callback then + obj.callback(obj.timerEventEnd, requestedMinutes) + end + + -- determine which one is the current screen + if currentWin then + screen = currentWin:screen() + else + screen = hs.screen.mainScreen() + end + hs.focus() + local mainRes = screen:fullFrame() + + if not requestedMinutes then + message = "Time is up" + else + message = string.format("Time is up: %d minutes", requestedMinutes) + end + strTime = "Time is " .. os.date("%X") + + if obj.alertSound then + hs.notify.new({ + title = message, + informativeText = strTime, + hasActionButton = true, + withdrawAfter = 100 + }):soundName(obj.alertSound):send() + end + + if obj.alertLen > 0 then + hs.alert.show(message, obj.alertAttributes, screen, obj.alertLen) + end + -- display alert in active screen + -- stealing focus, return to where it + -- when closing window + if obj.notify then + hs.dialog.alert(mainRes.x + mainRes.w/2-50, + mainRes.y + mainRes.h/2-50, + function(result) + currentWin:focus() + end, + message, + strTime, "OK") + end +end + +-- indication bar related functions + +function obj:bar_init() + -- initialize all the horizontal bar related values + obj.barCanvas = hs.canvas.new({x=0, y=0, w=0, h=0}):show() + obj.barCanvas:behavior(hs.canvas.windowBehaviors.canJoinAllSpaces) + obj.barCanvas:level(hs.canvas.windowLevels.status) + obj.barCanvas:alpha(obj.barTransparency) + obj.barCanvas[1] = { + type = "rectangle", + action = "fill", + fillColor = obj.barFillColorPassed, + frame = {x="0%", y="0%", w="0%", h="100%"} + } + obj.barCanvas[2] = { + type = "rectangle", + action = "fill", + fillColor = obj.barFillColorToPass, + frame = {x="0%", y="0%", w="0%", h="100%"} + } +end + +function obj:bar_updateProgress() + -- advance the progress bar according to the value of obj.barProgress + progress = obj.barProgress + if obj.barCanvas:frame().h == 0 then + -- Make the canvas actully visible + local mainScreen = hs.screen.primaryScreen() + local mainRes = mainScreen:fullFrame() + obj.barCanvas:frame({ + x=mainRes.x, + y=mainRes.h-obj.barCanvasHeight, + w=mainRes.w, + h=obj.barCanvasHeight}) + end + if progress >= 1 then + -- do nothing... the timer is up + else + -- advance the timer... + obj.barCanvas[1].frame.w = tostring(progress) + obj.barCanvas[2].frame.x = tostring(progress) + obj.barCanvas[2].frame.w = tostring(1-progress) + end +end + +function obj:bar_reset() + -- make the canvas invisible + obj.barCanvas[1].frame.w = "0%" + obj.barCanvas[2].frame.x = "0%" + obj.barCanvas[2].frame.w = "0%" + obj.barCanvas:frame({x=0, y=0, w=0, h=0}) + obj.barProgress = 0 +end + +function obj:bar_create_timer() + -- create the horizontal bar timer callback + minutes = obj.timerLenMinutes + local mainScreen = hs.screen.primaryScreen() + local mainRes = mainScreen:fullFrame() + + obj.barCanvas:frame( + {x=mainRes.x, + y=mainRes.h-obj.barCanvasHeight, + w=mainRes.w, + h=obj.barCanvasHeight}) + + -- compute the minimal step at which the bar will be updated + -- + -- Set minimum visual step to 2px (i.e. Make sure every + -- trigger updates 2px on screen at least.) + local minimumStep = 2 + local secCount = math.ceil(60*minutes) + obj.currentIcon = obj.menuBarIconActive + obj.loopCount = 0 + + if mainRes.w/secCount >= minimumStep then + -- do every second + barTimer = hs.timer.doEvery(1, function() + obj:bar_updateProgress() + end) + else + -- we need few seconds to advance two pixels + local interval = 2/(mainRes.w/secCount) + barTimer = hs.timer.doEvery(interval, + function() + obj:barSetProgresspercentage(obj.barProgress) + end + ) + end +end + +---- warning related functions + +function obj:warning_show(min) + obj:show_message(string.format(obj.warningFormat, min // 60, min % 60), + obj.warningShowDuration) +end + +function obj:warning_update() + if obj.warningShow and obj.timeLeft > 0 then + minLeft = obj.timeLeft / 60 + if minLeft < obj.timerLenMinutes and + minLeft > 0 and obj:is_power_of_2(minLeft) then + obj:warning_show(minLeft) + end + end +end + +------- menuBar bar related functions +function obj:menuBar_init() + obj.menuBar = hs.menubar.new(obj.menuBarAlwaysShow) + obj:menuBar_reset() +end + +function obj:menuBar_reset() + obj.menuBar:setTitle(obj.menuBarIconIdle) + local items = { + { title = string.format("%s Start %d min", obj.menuBarIconActive, obj.defaultLenMinutes), + fn = function() obj:startFor(obj.defaultLenMinutes) end + }, + { title = string.format("%s Start for ... ", obj.menuBarIconActive), + fn = function() obj:startForInteractive() end + }, + } + obj.menuBar:setMenu(items) + if not obj.menuBarAlwaysShow then + obj.menuBar:removeFromMenuBar() + end +end + +function obj:menuBar_updateTimerString() + local minutes = math.floor((obj.timeLeft % 3600) / 60) + local hours = math.floor(obj.timeLeft / 3600) + local seconds = obj.timeLeft % 60 + local timeString + if hours > 0 then + timerString = string.format("%s %d:%02d:%02d", obj.currentIcon, hours, minutes, seconds) + else + timerString = string.format("%s %02d:%02d", obj.currentIcon, minutes, seconds) + end + obj.menuBar:setTitle(timerString) +end + +function obj:menuBar_update(isPaused) + -- update the icons and textual information in menu bar + -- if active, indicate time left + if not obj.menuBar:isInMenuBar() then + obj.menuBar:returnToMenuBar() + end + local label = nil + if isPaused then + label = string.format("%s Resume", obj.menuBarIconActive) + obj.currentIcon = obj.menuBarIconPause + else + obj.currentIcon = obj.menuBarIconActive + label = string.format("%s Pause", obj.menuBarIconPause) + end + local items = { + { title = string.format("%s Stop", obj.menuBarIconStop), + fn = function() obj:cancel() end }, + { title = label, fn = function() obj:pauseOrResume() end } + } + obj.menuBar:setMenu(items) + obj:menuBar_updateTimerString() +end + +function obj:tick() + -- main timer callback + -- keeps track of the time passed + -- ticks every second... + -- updates barProgress + -- and potentially issues a warning + if not obj.timerRunning then + print("This should not happen!!") + return + end + local timeNow = obj:time_absolute_seconds() + obj.timeLeft = obj.endingTime - timeNow + if obj.timeLeft <= 0 then + obj.timerRunning = false + -- we need to save this before we reset + -- and we need to reset before we notify + local requestedMinutes = obj.timerLenMinutes + obj:reset_timer() + obj:end_of_timer_notify(requestedMinutes) + else --update progress ratio + obj.barProgress = 1 - obj.timeLeft/(obj.timerLenMinutes * 60) + obj:warning_update() + obj:menuBar_updateTimerString() + end +end + +function obj:create_tick_timer() + obj.endingTime = obj:time_absolute_seconds() + obj.timerLenMinutes * 60 + obj.timerRunning = true + obj:tick() + obj.timer = hs.timer.doWhile(function() return obj.timerRunning end, + function() obj:tick() end, + 1) +end + +-- API ------------------------------- + + +--- CountDown:startFor(minutes, callback) --- Method --- Start a countdown for `minutes` minutes immediately. Calling this method again will kill the existing countdown instance. --- --- Parameters: --- * minutes - How many minutes +--- Defaults to obj.defaultLenMinutes +--- * callback: optional: a function to call when the timer +--- is up. it takes one parameters (minutes) +--- the minutes that were requested. +--- The callback is not called if timer is cancelled +--- Returns: +--- * None +function obj:startFor(minutes, callback) + if not minutes then + minutes = obj.defaultLenMinutes + end + if obj.timerRunning then + obj:show_message("Error. Timer is already running. It is not possible to start another one.") + return + end + if math.type(minutes) ~= "integer" then + message = string.format("Error. Minutes should be an integer [%s]", tostring(minutes)) + obj:show_message(message) + return + end + if minutes < 0 then + message = string.format("Error. Trying to start a timer for negative minutes [%d]", minutes) + obj:show_message(message) + return + end + if callback and type(callback) ~= 'function' then + obj:show_message("Error. Second parameter should be a function") + return + end -local function canvasCleanup() - if obj.timer then - obj.timer:stop() - obj.timer = nil - end - obj.canvas[1].frame.w = "0%" - obj.canvas[2].frame.x = "0%" - obj.canvas[2].frame.w = "0%" - obj.canvas:frame({x=0, y=0, w=0, h=0}) + obj.timerLenMinutes = minutes + obj.callback = callback + + obj.requestedMinutes = tostring(minutes) .. " minutes" + + -- we create two timers, one for the horizontal bar and one for + -- keeping track of the time + -- the reason is that we update the bar much less frequently than the time + + obj:bar_create_timer() + + obj:create_tick_timer() + + obj:menuBar_update(false) + + if obj.useLastTimerAsDefault then + obj.defaultLenMinutes = minutes + end + obj:show_message(string.format("Timer started for %d minutes", obj.defaultLenMinutes)) + + if callback then + obj.callback(obj.timerEventStart, 0) + end + + return self end -function obj:startFor(minutes) - if obj.timer then - canvasCleanup() +--- CountDown:startUntil(time, callback) +--- Method +--- Start a countdown until time indicated in parameter. +--- +--- Parameters: +--- * time - A string of the form: hh:mm:ss, or mm:ss +--- if time is before current time, assume it is +--- for tomorrow. +--- * callback: optional: a function to call when the timer +--- is up. it takes one parameters (minutes) +--- the minutes that were requested. +--- Returns: +--- * None +function obj:startUntil(time, callback) + local message + local _, _, hour,min= string.find(time, "(%d+):(%d+)") + if hour and min then + totalMin = hour * 60 + min + currentTime = os.date("*t") + currentMin = currentTime["hour"] * 60 + currentTime["min"] + -- modulus is always positive + min = (totalMin - currentMin) % (24 * 60) + obj:startFor(min, callback) + else + message = string.format("Illegal time [%s] provided. Must be :", time) + end + hs.alert.show(message, nil, hs.screen.mainScreen()) +end + +--- CountDown:startUntil(time) +--- Method +--- Start a countdown until time indicated in parameter. +--- +--- Parameters: +--- * callback: optional: a function to call when the timer +--- is up. it takes one parameters (minutes) +--- the minutes that were requested. +function obj:startForInteractive(callback) + -- query the user for the timer duration + -- and start it + local currentWin = hs.window.focusedWindow() + hs.focus() + local button, time = hs.dialog.textPrompt("Enter time", "in minutes or specific time (e.g. 10:30)", + string.format("%s", obj.defaultLenMinutes), "OK", "Cancel") + if button == "OK" then + obj.requestedMinutes = time + if string.find(time, ":") then + -- time given as absolute time + -- we need to convert time to seconds + -- if time is before current time, assume it is tomorrow + obj:startUntil(time, callback) else - local mainScreen = hs.screen.mainScreen() - local mainRes = mainScreen:fullFrame() - obj.canvas:frame({x=mainRes.x, y=mainRes.h-5, w=mainRes.w, h=5}) - -- Set minimum visual step to 2px (i.e. Make sure every trigger updates 2px on screen at least.) - local minimumStep = 2 - local secCount = math.ceil(60*minutes) - obj.loopCount = 0 - if mainRes.w/secCount >= 2 then - obj.timer = hs.timer.doEvery(1, function() - obj.loopCount = obj.loopCount+1/secCount - obj:setProgress(obj.loopCount, minutes) - end) - else - local interval = 2/(mainRes.w/secCount) - obj.timer = hs.timer.doEvery(interval, function() - obj.loopCount = obj.loopCount+1/mainRes.w*2 - obj:setProgress(obj.loopCount, minutes) - end) - end + -- assume it is minutes, given as a string + local mins = tonumber(time) + if not mins then + message = string.format("Illegal number [%s]", time) + hs.alert.show(message, nil, hs.screen.mainScreen()) + else + obj:startFor(mins, callback) + end end - - return self + end + if currentWin then + currentWin:focus() + end end --- CountDown:pauseOrResume() @@ -88,44 +645,113 @@ end --- --- Parameters: --- * None - +--- Returns: +--- * None function obj:pauseOrResume() - if obj.timer then - if obj.timer:running() then - obj.timer:stop() - else - obj.timer:start() - end - end + -- if the timer is paused, we need to offset + -- the starting time for as long as the timer is paused + + if obj.timer then + + if obj.timer:running() then + obj.pausedAt = obj:time_absolute_seconds() + -- stop callbacks + obj.timer:stop() + barTimer:stop() + + obj:menuBar_update(true) + if obj.callback then + obj.callback(obj.timerEventPause, obj.timeLeft/60) + end + obj:show_message("Timer paused") + else + -- offset the starting time by as many seconds as we were paused + local pausedSeconds = obj:time_absolute_seconds() - obj.pausedAt + obj.endingTime = obj.endingTime + pausedSeconds + obj.pausedAt = nil + -- restart timers + obj.timer:start() + barTimer:start() + + obj:menuBar_update(false) + if obj.callback then + obj.callback(obj.timerEventResume, obj.timeLeft/60) + end + obj:show_message("Timer resumed") + end + end +end + +--- CountDown:cancel() +--- Method +--- Reset the timer +--- +--- Parameters: +--- * None +--- Returns: +--- * None +function obj:cancel() + if obj.timerRunning then + local minLeft = obj.timeLeft/60 + if obj.callback then + obj.callback(obj.timerEventReset, minLeft) + end + obj:reset_timer() + obj:show_message( + string.format("Timer was cancelled (time left %3.1f min).", minLeft)) + else + obj:show_message("Error. Timer not running ") + end end --- CountDown:setProgress(progress) --- Method ---- Set the progress of visual indicator to `progress`. +--- Set the progress of visual indicator to progress. --- --- Parameters: ---- * progress - an number specifying the value of progress (0.0 - 1.0) - -function obj:setProgress(progress, notifystr) - if obj.canvas:frame().h == 0 then - -- Make the canvas actully visible - local mainScreen = hs.screen.mainScreen() - local mainRes = mainScreen:fullFrame() - obj.canvas:frame({x=mainRes.x, y=mainRes.h-5, w=mainRes.w, h=5}) - end - if progress >= 1 then - canvasCleanup() - if notifystr then - hs.notify.new({ - title = "Time(" .. notifystr .. " mins) is up!", - informativeText = "Now is " .. os.date("%X") - }):send() - end - else - obj.canvas[1].frame.w = tostring(progress) - obj.canvas[2].frame.x = tostring(progress) - obj.canvas[2].frame.w = tostring(1-progress) - end +--- * progress +--- a relative number specifying the new progress (0.0 - 1.0) +--- Returns: +--- * None +function obj:setProgress(progress) + if obj.timerRunning then + -- obj.endingTime containts ending time in seconds + -- timerLenMinutes starting time + local newMinutesLeft = obj.timerLenMinutes * (1- progress) + obj.endingTime = obj:time_absolute_seconds() + newMinutesLeft *60 + obj:show_message( + string.format("Timer reset to %.1f%% (%.1f minutes left)", + progress*100, newMinutesLeft)) + if obj.callback then + obj.callback(obj.timerEventSetProgress, newMinutesLeft) + end + else + obj:show_message("Error. Timer not running ") + end +end + + +--- CountDown:bindHotkeys(mapping) +--- Method +--- Bind hotkeys for this spoon +--- +--- Parameters: +--- * mapping: a table with the callbacks +--- Returns: +--- * None +function obj:bindHotkeys(mapping) + local def = { + startFor = function() obj:startFor() end, + startInteractive = function() obj:startForInteractive() end, + cancel = function() obj:cancel() end, + pauseOrResume = function() obj:pauseOrResume() end + } + + if mapping then + hs.spoons.bindHotkeysToSpec(def, mapping) + else + hs.spoons.bindHotkeysToSpec(def, obj.defaultKeyBindings) + end end return obj diff --git a/Source/CountDown.spoon/readme.org b/Source/CountDown.spoon/readme.org new file mode 100644 index 00000000..d3190c1c --- /dev/null +++ b/Source/CountDown.spoon/readme.org @@ -0,0 +1,162 @@ + + +* CountDown spoon + +This spoon is intended to be a simple timer. The user indicates how long (in minutes) the timer will run. + +- During the timer, a horizontal progress bar is displayed at the bottom of the screen +- Optionally, every few minutes, a warning is displayed with the remaining time +- A menu bar item displays the current progress and allows the user to pause/continue and reset the timer. + +When the time expires, a notification is issued and a message is displayed. + +Only one timer can be active. + +* Simple usage + +#+begin_src lua :exports both +countDown=hs.loadSpoon("CountDown") +if countDown then +-- configure status bar + countDown.barCanvasHeight = 8 + countDown.barTrasparency = 0.9 +-- number of minutes of default timer + countDown.defaultDuration = 25 +-- configure some bindings + countDown:bindHotkeys({ + startFor = {{"cmd", "ctrl", "alt"}, "P"}, + startInteractive = {{"cmd", "ctrl", "alt", "shift"}, "P"}, + cancel = {{"cmd", "ctrl", "alt", "shift"}, "K"}, + }) +end +#+end_src + + + +* Functions + +There are three main methods in this Spoon: + +** startFor(minutes, callback) + + Starts a timer for specified /minutes/. /callback/ is a function (optional). + + The callback takes two parameters: event (string), and minutesLeft (float). See callback below + +#+begin_src lua :exports both +-- start timer for 25 minutes +countDown=hs.loadSpoon("CountDown") +countDown:startFor(25) +#+end_src + + +** startUntil(time, callback) + +Starts a timer that will end at given time. Time is a string of the form "[0-9]+:[0-9]+:" (24 hrs time) + +This time is converted to minutes from now and the corresponding timer started. If the given time is after current time, +it assumes the time is in the next day. + +#+begin_src lua :exports both +countDown=hs.loadSpoon("CountDown") +countDown:startUntil(12:45) +#+end_src + + +** startForInteractive(callback) + +Prompt the user for a time for the timer (response can be a time of the day, or minutes). + +#+begin_src lua :exports both +countDown=hs.loadSpoon("CountDown") +countDown:startForInteractive() +#+end_src + + +** pauseOrResume() + +Pause or resume the timer. + +#+begin_src lua :exports both +countDown=hs.loadSpoon("CountDown") +countDown:pause() +#+end_src + + +** cancel() + +Cancel the timer. + +#+begin_src lua :exports both +countDown=hs.loadSpoon("CountDown") +countDown:cancel() +#+end_src + +* bindHotkeys(bindings) + +Configure bindings. It defines 4 events, each for their corresponding +functions. + +The default values are (the no parameter is passed): + +#+begin_src lua :exports both +obj.defaultKeyBindings = { + startFor = {{"cmd", "ctrl", "alt"}, "T"}, + startInteractive = {{"cmd", "ctrl", "alt", "shift"}, "T"}, + pauseOrResume = {{"cmd", "ctrl", "alt"}, "P"}, + cancel = {{"cmd", "ctrl", "alt"}, "C"}, +} +#+end_src + + + +* Configuration + +The Spoon can be configured in many ways. I recommend you look at the API for all the configuration variables. + +Some configurations require further discussion: + +By default, 3 types of notifications are enabled: + +1. The horizontal bar at the bottom of the main screen. It visually shows the timer advancing. It cannot be disabled. + +2. A notification warning. This warning displays the minutes left. It does it in increments of power of 2 of the + remaining time. For instance it will be displayed 1, 2, 4, 8, 16, 32, 64.. etc minutes before the timer is up. + Disable by setting /warningShow/ to false + +3. Menu Bar: an item is added to the menu bar. When the timer is active, this item displays the time left. + If /menuBarAlwaysShow/ is false do not display the item when the timer is not active (the menu item is always displayed + when the timer is up) + +3. Timer is up notification. There are three different notifications. + + 1. A timer alert (its duration is controlled by the variable alertLet, if zero, it is not displayed). + 2. A sound. It can be configured using /alertSound/ + 3. A MacOS dialog alert. It steals focus but returns to previous app when selecting "OK". Disable by setting + /notify/ to false + +* callback + +The callback is a function that takes two parameters: event (string), and minutesLeft(float). +There are four events: + +1. *started*: Timer is started +2. *ended*: Timer is up +3. *paused*: the timer was paused +4. *resumed*: the timer was resumed +5. *cancelled*: the timer was reset + +Example: The following timer is started for 2 minutes, and will print the event and time left. + +#+begin_src lua :exports both +countDown=hs.loadSpoon("CountDown") + +countDown:startFor(2, + function (event, min) print(string.format("> Timer callback: event [%s] time [%f]", event, min)) +end) +#+end_src + + + + +