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"]