From a48d25200e1fe1264efd76d1879aad86229e70e3 Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Wed, 2 Nov 2016 19:53:03 -0500 Subject: [PATCH] Initial implementation. Still needs stop, reconfigure commands. --- daily_notifier.nim | 174 +++++++++++++++++++++++++++++++++--------- daily_notifier.nimble | 2 +- 2 files changed, 140 insertions(+), 36 deletions(-) diff --git a/daily_notifier.nim b/daily_notifier.nim index 34623a6..6f4b728 100644 --- a/daily_notifier.nim +++ b/daily_notifier.nim @@ -1,25 +1,52 @@ -import docopt, os, nre, sequtils, strutils, times, timeutils +import daemonize, docopt, json, os, nre, sequtils, strutils, times, timeutils +from posix import SIGTERM, SIGHUP, signal, kill -type ParseState = enum BeforeHeading, ReadingPlans, AfterPlans +type + ParseState = enum BeforeHeading, ReadingPlans, AfterPlans + PlanItem* = tuple[time: TimeInfo, note: string] + DPConfig = tuple[planDir, dateFmt, pidfile, logfile, errfile: string, + notificationSecs: int] -type PlanItem* = tuple[time: TimeInfo, note: string] + 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.0" +const timeFmt = "HH:mm" + +var args: Table[string, Value] +var cfg: DPConfig proc parseDailyPlan(filename: string): seq[PlanItem] = - var planItems: seq[PlanItem] = @[] + result = @[] + + if not existsFile(filename): return + var parseState = BeforeHeading let planItemRe = re"\s*(\d{4})\s+(.*)" - let timeFmt = "HHmm" + 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 + if line.strip.startsWith("## Timeline"): parseState = ReadingPlans of ReadingPlans: let match = line.find(planItemRe) - if match.isSome(): planItems.add(( + if match.isSome(): result.add(( time: parse(match.get().captures[0], timeFmt), note: match.get().captures[1])) else: parseState = AfterPlans @@ -27,33 +54,20 @@ proc parseDailyPlan(filename: string): seq[PlanItem] = of AfterPlans: break else: break - return planItems +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\"'") -when isMainModule: + of "linux": + discard execShellCmd("notify-send 'Daily Notifier v" & VERSION & "' '" & item.note & "'") - let doc = """ -Usage: - daily_notifier [options] + else: quit("Unsupported host OS: '" & hostOS & "'.") -Options: - - -d --plan-directory Directory to search for plan files. - - -f --plan-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. - - -h --help Print this usage information. -""" - let args = docopt(doc, version = "daily_notifier 0.2.0") - - if args["--help"]: - echo doc - quit() +proc loadConfig(s: cint): void {. exportc, noconv .} = # Find and parse the .dailynotificationrc file let rcLocations = @[ @@ -61,10 +75,100 @@ Options: ".dailynotificationrc", $getEnv("DAILY_NOTIFICATION_RC"), $getEnv("HOME") & "/.dailynotificationrc"] - var ptkrcFilename: string = + var rcFilename: string = foldl(rcLocations, if len(a) > 0: a elif existsFile(b): b else: "") - # Determine our plan directory and file template - # Start our daemon process (if needed) - # exit + var jsonCfg: JsonNode + try: jsonCfg = parseFile(rcFilename) + except: jsonCfg = newJObject() + + var cfg = CombinedConfig(docopt: args, json: jsonCfg) + + cfg = ( + cfg.getVal("plans-directory", "plans"), + cfg.getVal("date-format", "yyyy-MM-dd"), + cfg.getVal("pidfile", "/tmp/daily-plans.pid"), + cfg.getVal("logfile", "/tmp/daily-plans.log"), + cfg.getVal("errfile", "/tmp/daily-plans.error.log"), + parseInt(cfg.getVal("notification-seconds", "600")) + ) + + if not existsDir(result.planDir): + quit("daily_notifier: plan directory does not exist: '" & result.planDir & "'", 1) + +proc mainLoop(args: Table[string, Value]): void = + loadConfig(args) + 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: + 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() + + if args["start"]: + # Start our daemon process (if needed) + daemonize(cfg.pidfile, "/dev/null", cfg.logfile, cfg.errfile): + mainLoop(args) + #elif args["stop"]: + #loadConfig() + + #kill( diff --git a/daily_notifier.nimble b/daily_notifier.nimble index bcfb7c0..20cb0bd 100644 --- a/daily_notifier.nimble +++ b/daily_notifier.nimble @@ -8,5 +8,5 @@ bin = @["daily_notifier", "deploy_plans_via_ftp"] # Dependencies -requires @["nim >= 0.15.0", "docopt", "timeutils", "tempfile"] +requires @["nim >= 0.15.0", "docopt", "timeutils", "tempfile", "daemonize"]