360 lines
15 KiB
Lua
360 lines
15 KiB
Lua
--- === hs.timer ===
|
|
---
|
|
--- Execute functions with various timing rules
|
|
---
|
|
--- **NOTE**: timers use NSTimer internally, which will be paused when computers sleep.
|
|
--- Especially, repeating timers won't be triggered at the specificed time when there are sleeps in between.
|
|
--- The workaround is to prevent system from sleeping, configured in Energy Saver in System Preferences.
|
|
|
|
local module = require("hs.libtimer")
|
|
local log=require'hs.logger'.new('timer',3)
|
|
module.setLogLevel=log.setLogLevel
|
|
local type,ipairs,tonumber,floor,date=type,ipairs,tonumber,math.floor,os.date
|
|
|
|
-- private variables and methods -----------------------------------------
|
|
|
|
local TIME_PATTERNS={'-??:??:??-','-??:??--','??d??h---','-??h??m--','--??m??s-','??d----','-??h---','--??m--','---??s-','----????ms'}
|
|
-- ms unused, but it might be useful in the future
|
|
do
|
|
for i,s in ipairs(TIME_PATTERNS) do
|
|
TIME_PATTERNS[i]='^'..(s:gsub('%?%?%?%?','(%%d%%d?%%d?%%d?)'):gsub('%?%?','(%%d%%d?)'):gsub('%-','()'))..'$'
|
|
end
|
|
end
|
|
local function timeStringToSeconds(time)
|
|
if type(time)=='string' then
|
|
local d,h,m,s,ms
|
|
for _,pattern in ipairs(TIME_PATTERNS) do
|
|
d,h,m,s,ms=time:match(pattern) if d then break end
|
|
end
|
|
if not d then error('invalid time string '..time,3) end
|
|
if type(d)=='number' then d=0 end --remove "missing" captures
|
|
if type(h)=='number' then h=0 end
|
|
if type(m)=='number' then m=0 end
|
|
if type(s)=='number' then s=0 end
|
|
if type(ms)=='number' then ms=0 end
|
|
d=tonumber(d) h=tonumber(h) m=tonumber(m) s=tonumber(s) ms=tonumber(ms)
|
|
if h>=24 or m>=60 or s>=60 then error('invalid time string '..time,3) end
|
|
time=d*86400+h*3600+m*60+s+(ms/1000)
|
|
end
|
|
if type(time)~='number' or time<0 then error('invalid time',3) end
|
|
return time
|
|
end
|
|
|
|
-- Public interface ------------------------------------------------------
|
|
|
|
--- hs.timer.seconds(timeOrDuration) -> seconds
|
|
--- Function
|
|
--- Converts a string with a time of day or a duration into number of seconds
|
|
---
|
|
--- Parameters:
|
|
--- * timeOrDuration - a string that can have any of the following formats:
|
|
--- * "HH:MM:SS" or "HH:MM" - represents a time of day (24-hour clock), returns the number of seconds since midnight
|
|
--- * "DDdHHh", "HHhMMm", "MMmSSs", "DDd", "HHh", "MMm", "SSs", "NNNNms" - represents a duration in days, hours, minutes,
|
|
--- seconds and/or milliseconds
|
|
---
|
|
--- Returns:
|
|
--- * The number of seconds
|
|
function module.seconds(n) return timeStringToSeconds(n) end
|
|
|
|
--- hs.timer.minutes(n) -> seconds
|
|
--- Function
|
|
--- Converts minutes to seconds
|
|
---
|
|
--- Parameters:
|
|
--- * n - A number of minutes
|
|
---
|
|
--- Returns:
|
|
--- * The number of seconds in n minutes
|
|
function module.minutes(n) return 60 * n end
|
|
|
|
--- hs.timer.hours(n) -> seconds
|
|
--- Function
|
|
--- Converts hours to seconds
|
|
---
|
|
--- Parameters:
|
|
--- * n - A number of hours
|
|
---
|
|
--- Returns:
|
|
--- * The number of seconds in n hours
|
|
function module.hours(n) return 60 * 60 * n end
|
|
|
|
--- hs.timer.days(n) -> sec
|
|
--- Function
|
|
--- Converts days to seconds
|
|
---
|
|
--- Parameters:
|
|
--- * n - A number of days
|
|
---
|
|
--- Returns:
|
|
--- * The number of seconds in n days
|
|
function module.days(n) return 60 * 60 * 24 * n end
|
|
|
|
--- hs.timer.weeks(n) -> sec
|
|
--- Function
|
|
--- Converts weeks to seconds
|
|
---
|
|
--- Parameters:
|
|
--- * n - A number of weeks
|
|
---
|
|
--- Returns:
|
|
--- * The number of seconds in n weeks
|
|
function module.weeks(n) return 60 * 60 * 24 * 7 * n end
|
|
|
|
--- hs.timer.waitUntil(predicateFn, actionFn[, checkInterval]) -> timer
|
|
--- Constructor
|
|
--- Creates and starts a timer which will perform `actionFn` when `predicateFn` returns true. The timer is automatically stopped when `actionFn` is called.
|
|
---
|
|
--- Parameters:
|
|
--- * predicateFn - a function which determines when `actionFn` should be called. This function takes no arguments, but should return true when it is time to call `actionFn`.
|
|
--- * actionFn - a function which performs the desired action. This function may take a single argument, the timer itself.
|
|
--- * checkInterval - an optional parameter indicating how often to repeat the `predicateFn` check. Defaults to 1 second.
|
|
---
|
|
--- Returns:
|
|
--- * a timer object
|
|
---
|
|
--- Notes:
|
|
--- * The timer is stopped before `actionFn` is called, but the timer is passed as an argument to `actionFn` so that the actionFn may restart the timer to be called again the next time predicateFn returns true.
|
|
--- * See also `hs.timer.waitWhile`, which is essentially the opposite of this function
|
|
module.waitUntil = function(predicateFn, actionFn, checkInterval)
|
|
checkInterval = checkInterval or 1
|
|
|
|
local stopWatch
|
|
stopWatch = module.new(checkInterval, function()
|
|
if predicateFn() then
|
|
stopWatch:stop()
|
|
actionFn(stopWatch)
|
|
end
|
|
end):start()
|
|
return stopWatch
|
|
end
|
|
|
|
--- hs.timer.doUntil(predicateFn, actionFn[, checkInterval]) -> timer
|
|
--- Constructor
|
|
--- Creates and starts a timer which will perform `actionFn` every `checkinterval` seconds until `predicateFn` returns true. The timer is automatically stopped when `predicateFn` returns true.
|
|
---
|
|
--- Parameters:
|
|
--- * predicateFn - a function which determines when to stop calling `actionFn`. This function takes no arguments, but should return true when it is time to stop calling `actionFn`.
|
|
--- * actionFn - a function which performs the desired action. This function may take a single argument, the timer itself.
|
|
--- * checkInterval - an optional parameter indicating how often to repeat the `predicateFn` check. Defaults to 1 second.
|
|
---
|
|
--- Returns:
|
|
--- * a timer object
|
|
---
|
|
--- Notes:
|
|
--- * The timer is passed as an argument to `actionFn` so that it may stop the timer prematurely (i.e. before predicateFn returns true) if desired.
|
|
--- * See also `hs.timer.doWhile`, which is essentially the opposite of this function
|
|
module.doUntil = function(predicateFn, actionFn, checkInterval)
|
|
checkInterval = checkInterval or 1
|
|
local stopWatch
|
|
stopWatch = module.new(checkInterval, function()
|
|
if not predicateFn() then
|
|
actionFn(stopWatch)
|
|
else
|
|
stopWatch:stop()
|
|
end
|
|
end):start()
|
|
return stopWatch
|
|
end
|
|
|
|
--- hs.timer.doEvery(interval, fn) -> timer
|
|
--- Constructor
|
|
--- Repeats fn every interval seconds.
|
|
---
|
|
--- Parameters:
|
|
--- * interval - A number of seconds between triggers
|
|
--- * fn - A function to call every time the timer triggers
|
|
---
|
|
--- Returns:
|
|
--- * An `hs.timer` object
|
|
---
|
|
--- Notes:
|
|
--- * This function is a shorthand for `hs.timer.new(interval, fn):start()`
|
|
module.doEvery = function(...)
|
|
return module.new(...):start()
|
|
end
|
|
|
|
--- hs.timer.waitWhile(predicateFn, actionFn[, checkInterval]) -> timer
|
|
--- Constructor
|
|
--- Creates and starts a timer which will perform `actionFn` when `predicateFn` returns false. The timer is automatically stopped when `actionFn` is called.
|
|
---
|
|
--- Parameters:
|
|
--- * predicateFn - a function which determines when `actionFn` should be called. This function takes no arguments, but should return false when it is time to call `actionFn`.
|
|
--- * actionFn - a function which performs the desired action. This function may take a single argument, the timer itself.
|
|
--- * checkInterval - an optional parameter indicating how often to repeat the `predicateFn` check. Defaults to 1 second.
|
|
---
|
|
--- Returns:
|
|
--- * a timer object
|
|
---
|
|
--- Notes:
|
|
--- * The timer is stopped before `actionFn` is called, but the timer is passed as an argument to `actionFn` so that the actionFn may restart the timer to be called again the next time predicateFn returns false.
|
|
--- * See also `hs.timer.waitUntil`, which is essentially the opposite of this function
|
|
module.waitWhile = function(predicateFn, ...)
|
|
return module.waitUntil(function() return not predicateFn() end, ...)
|
|
end
|
|
|
|
--- hs.timer.doWhile(predicateFn, actionFn[, checkInterval]) -> timer
|
|
--- Constructor
|
|
--- Creates and starts a timer which will perform `actionFn` every `checkinterval` seconds while `predicateFn` returns true. The timer is automatically stopped when `predicateFn` returns false.
|
|
---
|
|
--- Parameters:
|
|
--- * predicateFn - a function which determines when to stop calling `actionFn`. This function takes no arguments, but should return false when it is time to stop calling `actionFn`.
|
|
--- * actionFn - a function which performs the desired action. This function may take a single argument, the timer itself.
|
|
--- * checkInterval - an optional parameter indicating how often to repeat the `predicateFn` check. Defaults to 1 second.
|
|
---
|
|
--- Returns:
|
|
--- * a timer object
|
|
---
|
|
--- Notes:
|
|
--- * The timer is passed as an argument to `actionFn` so that it may stop the timer prematurely (i.e. before predicateFn returns false) if desired.
|
|
--- * See also `hs.timer.doUntil`, which is essentially the opposite of this function
|
|
module.doWhile = function(predicateFn, ...)
|
|
return module.doUntil(function() return not predicateFn() end, ...)
|
|
end
|
|
|
|
|
|
--- hs.timer.localTime() -> number
|
|
--- Function
|
|
--- Returns the number of seconds since local time midnight
|
|
---
|
|
--- Parameters:
|
|
--- * None
|
|
---
|
|
--- Returns:
|
|
--- * the number of seconds
|
|
module.localTime = function()
|
|
local tnow=date('*t')
|
|
return tnow.sec+tnow.min*60+tnow.hour*3600
|
|
end
|
|
|
|
--- hs.timer.doAt(time[, repeatInterval], fn[, continueOnError]) -> timer
|
|
--- Constructor
|
|
--- Creates and starts a timer which will perform `fn` at the given (local) `time` and then (optionally) repeat it every `interval`.
|
|
---
|
|
--- Parameters:
|
|
--- * time - number of seconds after (local) midnight, or a string in the format "HH:MM" (24-hour local time), indicating
|
|
--- the desired trigger time
|
|
--- * repeatInterval - (optional) number of seconds between triggers, or a string in the format
|
|
--- "DDd", "DDdHHh", "HHhMMm", "HHh" or "MMm" indicating days, hours and/or minutes between triggers; if omitted
|
|
--- or `0` the timer will trigger only once
|
|
--- * fn - a function to call every time the timer triggers
|
|
--- * continueOnError - an optional boolean flag, defaulting to false, which indicates that the timer should not be automatically stopped if the callback function results in an error.
|
|
---
|
|
--- Returns:
|
|
--- * a timer object
|
|
---
|
|
--- Notes:
|
|
--- * The timer can trigger up to 1 second early or late
|
|
--- * The first trigger will be set to the earliest occurrence given the `repeatInterval`; if that's omitted,
|
|
--- and `time` is earlier than the current time, the timer will trigger the next day. If the repeated interval
|
|
--- results in exactly 24 hours you can schedule regular jobs that will run at the expected time independently
|
|
--- of when Hammerspoon was restarted/reloaded. E.g.:
|
|
--- * If it's 19:00, `hs.timer.doAt("20:00",somefn)` will set the timer 1 hour from now
|
|
--- * If it's 21:00, `hs.timer.doAt("20:00",somefn)` will set the timer 23 hours from now
|
|
--- * If it's 21:00, `hs.timer.doAt("20:00","6h",somefn)` will set the timer 5 hours from now (at 02:00)
|
|
--- * To run a job every hour on the hour from 8:00 to 20:00: `for h=8,20 do hs.timer.doAt(h..":00","1d",runJob) end`
|
|
module.doAt = function(time,interval,fn,continueOnError)
|
|
if type(interval)=='function' then continueOnError=fn fn=interval interval=0 end
|
|
interval=timeStringToSeconds(interval)
|
|
if interval~=0 and interval<60 then error('invalid interval',2) end -- degenerate use case for this function
|
|
time=timeStringToSeconds(time)
|
|
local now=module.localTime()
|
|
while time<=now do
|
|
time=time+(interval==0 and module.days(1) or interval)
|
|
end
|
|
local delta=time-now
|
|
time=time%86400 -- for logging
|
|
log.df('timer set for %02d:%02d:%02d, %dh%dm%ds from now',
|
|
floor(time/3600),floor(time/60)%60,floor(time%60),floor(delta/3600),floor(delta/60)%60,floor(delta%60))
|
|
return module.new(interval,fn,continueOnError):start():setNextTrigger(delta)
|
|
end
|
|
|
|
--- === hs.timer.delayed ===
|
|
---
|
|
--- Specialized timer objects to coalesce processing of unpredictable asynchronous events into a single callback
|
|
|
|
--- hs.timer.delayed:start([delay]) -> hs.timer.delayed object
|
|
--- Method
|
|
--- Starts or restarts the callback countdown
|
|
---
|
|
--- Parameters:
|
|
--- * delay - (optional) if provided, sets the countdown duration to this number of seconds for this time only; subsequent calls to `:start()` will revert to the original delay (or to the delay set with `:setDelay(delay)`)
|
|
---
|
|
--- Returns:
|
|
--- * the delayed timer object
|
|
|
|
--- hs.timer.delayed:stop() -> hs.timer.delayed object
|
|
--- Method
|
|
--- Cancels the callback countdown, if running; the callback will therefore not be triggered
|
|
---
|
|
--- Parameters:
|
|
--- * None
|
|
---
|
|
--- Returns:
|
|
--- * the delayed timer object
|
|
|
|
--- hs.timer.delayed:running() -> boolean
|
|
--- Method
|
|
--- Returns a boolean indicating whether the callback countdown is running
|
|
---
|
|
--- Parameters:
|
|
--- * None
|
|
---
|
|
--- Returns:
|
|
--- * a boolean
|
|
|
|
--- hs.timer.delayed:setDelay(delay) -> hs.timer.delayed object
|
|
--- Method
|
|
--- Changes the callback countdown duration
|
|
---
|
|
--- Parameters:
|
|
--- * None
|
|
---
|
|
--- Returns:
|
|
--- * the delayed timer object
|
|
---
|
|
--- Notes:
|
|
--- * if the callback countdown is running, calling this method will restart it
|
|
|
|
--- hs.timer.delayed:nextTrigger() -> number or nil
|
|
--- Method
|
|
--- Returns the time left in the callback countdown
|
|
---
|
|
--- Parameters:
|
|
--- * None
|
|
---
|
|
--- Returns:
|
|
--- * if the callback countdown is running, returns the number of seconds until it triggers; otherwise returns nil
|
|
|
|
--- hs.timer.delayed.new(delay, fn) -> hs.timer.delayed object
|
|
--- Constructor
|
|
--- Creates a new delayed timer
|
|
---
|
|
--- Parameters:
|
|
--- * delay - number of seconds to wait for after a `:start()` invocation (the "callback countdown")
|
|
--- * fn - a function to call after `delay` has fully elapsed without any further `:start()` invocations
|
|
---
|
|
--- Returns:
|
|
--- * a new `hs.timer.delayed` object
|
|
---
|
|
--- Notes:
|
|
--- * These timers are meant to be long-lived: once instantiated, there's no way to remove them from the run loop; create them once at the module level.
|
|
--- * Delayed timers have specialized methods that behave differently from regular timers. When the `:start()` method is invoked, the timer will wait for `delay` seconds before calling `fn()`; this is referred to as the callback countdown. If `:start()` is invoked again before `delay` has elapsed, the countdown starts over again.
|
|
--- * You can use a delayed timer to coalesce processing of unpredictable asynchronous events into a single callback; for example, if you have an event stream that happens in "bursts" of dozens of events at once, set an appropriate `delay` to wait for things to settle down, and then your callback will run just once.
|
|
|
|
local DISTANT_FUTURE=315360000 -- 10 years (roughly)
|
|
module.delayed = {
|
|
new=function(delay,fn)
|
|
local tmr=module.new(DISTANT_FUTURE,fn):start()
|
|
return {
|
|
start=function(self,dl) tmr:setNextTrigger(dl or delay) return self end,
|
|
stop=function(self) tmr:setNextTrigger(DISTANT_FUTURE) return self end,
|
|
nextTrigger=function() local nt=tmr:nextTrigger() return (nt>0 and nt<=delay) and nt or nil end,
|
|
running=function(self) return self:nextTrigger() and true or false end,
|
|
setDelay=function(self,dl) if self:running() then delay=dl self:start() end delay=dl return self end,
|
|
}
|
|
end
|
|
}
|
|
-- Return Module Object --------------------------------------------------
|
|
|
|
return module
|