hammerspoon/extensions/timer/timer.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