Initial implementation. Still needs stop, reconfigure commands.
This commit is contained in:
parent
ae72c6b1a0
commit
a48d25200e
@ -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(
|
||||
|
@ -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"]
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user