import std/[times, os, strutils, strbasics, strscans, sets, re, deques], parseopt const VERSION = "0.1" const configDir = expandTilde("~/.config/then/") const defaultConfigFile = configDir & "then.toml" const defaultCalendarFile = configDir & "calendar.then" type Token = object token: string precedence: int # Creates a default configuration file if it doesn't exist proc createDefaultConfigFile() = if not fileExists(defaultConfigFile): if not dirExists(configDir): createDir(configDir) writeFile(defaultConfigFile, """ [config] directory = "~/.config/then/" """) # Creates a default calendar file if it doesn't exist proc createDefaultCalendarFile() = if not fileExists(defaultCalendarFile): if not dirExists(configDir): createDir(configDir) writeFile(defaultCalendarFile, """ # Add your events here """) proc printHelp(badCmd: string) = if badCmd != "": echo badCmd & " is not a valid option\n" echo """ Usage: then [options] [commands] OPTIONS -h, --help show list of CLI options -v, --version show then version --future=DAYS set future --past=DAYS set past COMMANDS a print upcoming agenda (default command) e open default calendar file in $EDITOR j print the modified julian day """ quit() proc printVersion() = echo "then " & VERSION quit() proc checkType(cond: string): string = if cond.len == 4 and parseInt(cond) != 0: return "year" if cond.len in [1, 2] and parseInt(cond) != 0: return "day" if cond in DefaultLocale.MMM.toHashSet or cond in DefaultLocale.MMMM.toHashSet: return "month" if cond in DefaultLocale.ddd.toHashSet or cond in DefaultLocale.dddd.toHashSet: return "dayofweek" if find(cond, re"\d{1,2}:\d{2}") > -1: return "time" if find(cond, re"\dw") > -1: return "firstweek" if find(cond, re"\dl") > -1: return "lastweek" if find(cond, re"J%\d([-+]\d)?") > -1: return "julian" return "unknown" proc checkCond(cond: string, day: DateTime): bool = case checkType(cond): of "year": if parseInt(cond) == day.year: return true of "day": if parseInt(cond) == parseInt(day.format("d")): return true if parseInt(cond) == parseInt(day.format("dd")): return true of "month": if cond == day.format("MMM"): return true if cond == day.format("MMMM"): return true of "dayofweek": if cond == day.format("ddd"): return true if cond == day.format("dddd"): return true of "time": return true of "firstweek": discard # Uh oh we didn't think about this hard enough of "unknown": echo "UNKNOWN THING ", cond return false proc getConditions(dateStr: string): seq[string] = var conditions: seq[string] conditions = dateStr.rsplit(' ') return conditions # TODO simplify?? Would it be easier to identify conditions and replace them with TRUE/FALSE then just process the string? # Shunting yard to parse conditions, parens, and logical operators # Does NOT parse out individual conditions yet because some individual # conditions (like 1W) require context of surrounding conditions. # The logic around parentheses is a little weird because conditions like # "<(March 15)" contain parentheses that we don't want to throw out proc parseDateToEvents(dateStr: string): seq[string] = var holdingStack = initDeque[Token]() var resultStack = initDeque[Token]() var newToken: string = "" for c in dateStr: echo c case c: of '(': if newToken.len == 1: if newToken[0] == '>' or newToken[0] == '<': newToken.add(c) else echo "ERROR: newToken exists but we got an open paren?" else: holdingStack.addLast(Token(token: "(", precedence: 0)) of ')': if newToken.len > 0: # Closing token like <(March 15). Push the token. resultStack.addLast(Token(token: newToken, precedence: 0)) newToken = "" else: # Logical closing paren. Pop stack until (. while holdingStack.len > 0 and holdingStack.peekLast.token != "(": if holdingStack.len == 0: echo "UH OH, ERROR MISMATCHED PARENS" resultStack.addLast(holdingStack.popLast) discard holdingStack.popLast # Pop the ( of '!': holdingStack.addLast(Token(token: "!", precedence: 3)) of '|': while holdingStack.len > 0 and holdingStack.peekLast.precedence > 2: resultStack.addLast(holdingStack.popLast) holdingStack.addLast(Token(token: "|", precedence: 2)) of ' ': if newToken.len > 1 and newToken[1] == '(' and newToken[newToken.len-1] != ')': # Middle of token like <(March 15). newToken.add(c) else: # End of new token, implicit & # Add newToken to result resultStack.addLast(Token(token: newToken, precedence: 0)) newToken = "" # Add implicit & to holding while holdingStack.len > 0 and holdingStack.peekLast.precedence > 1: resultStack.addLast(holdingStack.popLast) holdingStack.addLast(Token(token:"&", precedence: 1)) of '>','<',':','-','a'..'z','A'..'Z','0'..'9': newToken.add(c) else: echo "Weird character in dateStr: ", c discard # Push last token if newToken.len > 0: resultStack.addLast(Token(token: newToken, precedence: 0)) # Clear the holding stack while holdingStack.len > 0: var t = holdingStack.popLast if t.token != "(": resultStack.addLast(t) echo resultStack proc getEvents(cal: seq[string], day: DateTime): seq[string] = var events: seq[string] for line in cal: var (success, dateStr, eventStr) = scanTuple(line, "$*,$*") if success: dateStr.strip eventStr.strip discard parseDateToEvents(dateStr) #let conditions = getConditions(dateStr) # var match = true # for c in conditions: # c = c.toLower.capitalizeAscii # if checkCond(c, day) == false: # match = false # break # if match: # events.add(eventStr) else: echo "WARNING: Cannot parse line: ", line return events proc printEvents(cal: seq[string], day: DateTime) = let events = getEvents(cal, day) if events.len > 0: echo day.format("ddd yyyy MMM dd") for e in events: echo " - ", e proc openCal(file: string): seq[string] = let f = open(file) var cal: seq[string] for line in f.lines: cal.add(line) f.close() return cal proc printCalendar() = let cal = openCal(defaultCalendarFile) var past = 3 var future = 7 let now = now() while past > 0: let curDay = now - past.days printEvents(cal, curDay) dec past printEvents(cal, now) for i in 1..future: let curDay = now + i.days printEvents(cal, curDay) proc main() = var past: int = 1 var future: int = 14 var noArg: bool = true for kind, key, val in getopt(): case kind of cmdLongOption, cmdShortOption: noArg = false case key of "help", "h": printHelp("") of "version", "v": printVersion() else: printHelp(key) else: discard if noArg: printCalendar() when isMainModule: main()