Initial implementation. Still needs stop, reconfigure commands.

This commit is contained in:
Jonathan Bernard 2016-11-02 19:53:03 -05:00
parent ae72c6b1a0
commit a48d25200e
2 changed files with 140 additions and 36 deletions

View File

@ -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 <dir> Directory to search for plan files.
-f --plan-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.
-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 <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()
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(

View File

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