daily-notifier/daily_notifier.nim
2016-11-02 22:27:17 -05:00

186 lines
5.3 KiB
Nim

import daemonize, docopt, json, os, nre, sequtils, times, timeutils
from posix import SIGTERM, SIGHUP, signal, kill
import strutils except toUpper
from unicode import toUpper
type
ParseState = enum BeforeHeading, ReadingPlans, AfterPlans
PlanItem* = tuple[time: TimeInfo, note: string]
DPConfig = tuple[planDir, dateFmt, pidfile, logfile, errfile: string,
notificationSecs: int]
CombinedConfig = object
docopt: Table[string, Value]
json: JsonNode
proc getVal(cfg: CombinedConfig, key, default: string): string =
let argKey = "--" & key
let envKey = key.replace('-', '_').toUpper
let jsonKey = key.replace(re"(-\w)", proc (m: RegexMatch): string = ($m)[1..1].toUpper)
if cfg.docopt[argKey]: return $cfg.docopt[argKey]
elif existsEnv(envKey): return getEnv(envKey)
elif cfg.json.hasKey(jsonKey): return cfg.json[jsonKey].getStr
else: return default
const VERSION = "0.2.1"
const timeFmt = "HH:mm"
var args: Table[string, Value]
var cfg: DPConfig
proc parseDailyPlan(filename: string): seq[PlanItem] =
result = @[]
if not existsFile(filename): return
var parseState = BeforeHeading
let planItemRe = re"\s*(\d{4})\s+(.*)"
const timeFmt = "HHmm"
for line in lines filename:
if line.strip.len == 0: continue
case parseState
of BeforeHeading:
if line.strip.startsWith("## Timeline"): parseState = ReadingPlans
of ReadingPlans:
let match = line.find(planItemRe)
if match.isSome(): result.add((
time: parse(match.get().captures[0], timeFmt),
note: match.get().captures[1]))
else: parseState = AfterPlans
of AfterPlans: break
else: break
proc notifyDailyPlanItem(item: PlanItem): void =
case hostOS
of "macosx":
discard execShellCmd("osascript -e 'display notification \"" &
item.time.format(timeFmt) & " - " & item.note &
"\" with title \"Daily Notifier v" & VERSION &
"\" sound name \"default\"'")
of "linux":
discard execShellCmd("notify-send 'Daily Notifier v" & VERSION & "' '" & item.note & "'")
else: quit("Unsupported host OS: '" & hostOS & "'.")
proc loadConfig(s: cint): void {. exportc, noconv .} =
# Find and parse the .dailynotificationrc file
let rcLocations = @[
if args["--config"]: $args["<cfgFile>"] else:"",
".dailynotificationrc", $getEnv("DAILY_NOTIFICATION_RC"),
$getEnv("HOME") & "/.dailynotificationrc"]
var rcFilename: string =
foldl(rcLocations, if len(a) > 0: a elif existsFile(b): b else: "")
var jsonCfg: JsonNode
try: jsonCfg = parseFile(rcFilename)
except: jsonCfg = newJObject()
var ccfg = CombinedConfig(docopt: args, json: jsonCfg)
cfg = (
ccfg.getVal("plans-directory", "plans"),
ccfg.getVal("date-format", "yyyy-MM-dd"),
ccfg.getVal("pidfile", "/tmp/daily-plans.pid"),
ccfg.getVal("logfile", "/tmp/daily-plans.log"),
ccfg.getVal("errfile", "/tmp/daily-plans.error.log"),
parseInt(ccfg.getVal("notification-seconds", "600"))
)
if not existsDir(cfg.planDir):
quit("daily_notifier: plan directory does not exist: '" & cfg.planDir & "'", 1)
proc mainLoop(args: Table[string, Value]): void =
loadConfig(0)
signal(SIGHUP, loadConfig)
var curDay: TimeInfo = getLocalTime(fromSeconds(0))
var todaysItems: seq[PlanItem] = @[]
while true:
# Check the date
let now = getLocalTime(getTime())
var today = startOfDay(now)
# Load today's plan items
if today != curDay or todaysItems.len == 0:
curDay = today
let todaysPlanFile =
cfg.planDir & "/" & today.format(cfg.dateFmt) & "-plan.md"
todaysItems = parseDailyPlan(todaysPlanFile)
# Check to see if any items are happening soon.
let cutoff = now + seconds(cfg.notificationSecs)
let needsNotification = todaysItems.filterIt(it.time > now and it.time < cutoff)
todaysItems.keepItIf(not needsNotification.contains(it))
for item in needsNotification:
notifyDailyPlanItem(item)
# Sleep for a while
sleep(30 * 1000)
when isMainModule:
let doc = """
Usage:
daily_notifier start [options]
daily_notifier reconfigure [options]
daily_notifier stop
Options:
-d --plans-directory <dir> Directory to search for plan files.
-f --date-format <fmt>
Date pattern for identifying plan files. This is used in conjunction
with plan directory to idenfity files that should be parsed as plan files.
-c --config <cfgFile> Use <cfgFile> as the source of configuration for
daily-notification.
-p --pidfile <pidfile> Daemon PID filename
-L --logfile <logfile> Log file.
-E --errfile <errfile> Error file.
-h --help Print this usage information.
-s --notification-seconds <sec>
Notification period for plan items.
"""
args = docopt(doc, version = "daily_notifier " & VERSION)
if args["--help"]:
echo doc
quit()
loadConfig(0)
if args["start"]:
# Start our daemon process (if needed)
daemonize(cfg.pidfile, "/dev/null", cfg.logfile, cfg.errfile):
mainLoop(args)
elif args["stop"] or args["reconfigure"]:
if not fileExists(cfg.pidfile):
echo "daily_notifier is not running"
quit(QuitSuccess)
let pid = parseInt(readFile(cfg.pidfile).strip)
if args["stop"]: discard kill(pid, SIGTERM)
else: discard kill(pid, SIGHUP)