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[""] 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 Directory to search for plan files. -f --date-format 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 Use as the source of configuration for daily-notification. -p --pidfile Daemon PID filename -L --logfile Log file. -E --errfile Error file. -h --help Print this usage information. -s --notification-seconds 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)