import std/[times, os, strutils, strbasics, strscans, sets, re, tables], parseopt const VERSION = "0.1" const MONTHS = DefaultLocale.MMM.toHashSet + DefaultLocale.MMMM.toHashSet const WEEKDAYS = DefaultLocale.ddd.toHashSet + DefaultLocale.dddd.toHashSet const configDir = expandTilde("~/.config/then/") const defaultConfigFile = configDir & "then.toml" const defaultCalendarFile = configDir & "calendar.then" # Forward declarations proc checkCond(cond: string, date: DateTime): bool # 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() # Check an individual condition (Jan) and return the condition type (month) proc checkType(c: string): string = var cond = c if find(cond, re"^\d{4}$") > -1: return "year" if find(cond, re"^\d{1,2}$") > -1: return "day" if find(cond, re"^\dw$") > -1: return "firstweek" if find(cond, re"^\dl$") > -1: return "lastweek" if find(cond, re"^\([A-Za-z0-9 ]+\)$") > -1: return "dategroup" if find(cond, re"^\d{4}-\d{4}$") > -1: return "rangeyear" if find(cond, re"^\d{1,2}-\d{1,2}$") > -1: return "rangeday" if find(cond, re"^\d{1,2}:\d{2}(-\d{1,2}:\d{2})?$") > -1: return "time" if find(cond, re"^\(?\s*[Jj]\s*%\s*\d+\s*([-+]\d+)?\s*\)?$") > -1: return "julian" cond = cond.toLower.capitalizeAscii if cond in MONTHS: return "month" if cond in WEEKDAYS: return "dayofweek" # Split c on - and check for ranges let cs = cond.split("-", maxsplit = 1) if cs.len != 2: return "unknown" let startRange = cs[0].toLower.capitalizeAscii let endRange = cs[1].toLower.capitalizeAscii if (startRange in MONTHS) and (endRange in MONTHS): return "rangemonth" if (startRange in WEEKDAYS) and (endRange in WEEKDAYS): return "rangedayofweek" if find(startRange, re"^\([A-Za-z0-9 ]+\)$") > -1 and find(endRange, re"^\([A-Za-z0-9 ]+\)$") > -1: return "rangedategroup" return "unknown" proc getWeekFromEnd(date: DateTime): int = let daysInMonth = getDaysInMonth(date.month, date.year) return int((daysInMonth - date.monthday) / 7) + 1 proc getWeek(date: DateTime): int = return int((date.monthDay - 1) / 7) + 1 # Check each condition in a date group like (Jan 1 2025) proc checkDateGroup(s: string, date: DateTime): bool = var conditions = s let cs = conditions[1..conditions.len-2].splitWhitespace for c in cs: if checkCond(c, date) == false: return false return true proc checkRangeYear(s: string, date: DateTime): bool = var years = s.split('-') return date.year >= years[0] and date.year <= years[1] proc checkCond(cond: string, date: DateTime): bool = var condition = cond var invert = false var found = false if condition[0] == '!': condition.delete(0..0) invert = true case checkType(condition): of "time": return true of "year": if parseInt(condition) == date.year: found = true of "day": if condition == date.format("d") or condition == date.format("dd"): found = true of "month": if condition == date.format("MMM") or condition == date.format("MMMM"): found = true of "dayofweek": if condition == date.format("ddd") or condition == date.format("dddd"): found = true of "firstweek": if parseInt(condition[0..1]) == getWeek(date): found = true of "lastweek": if parseInt(condition[0..1]) == getWeekFromEnd(date): found = true of "dategroup": found = checkDateGroup(condition, date) of "rangeyear": found = checkRangeYear(condition, date) of "unknown": echo "ERROR: Unknown condition: ", condition if invert == true: return not found return found # Splits a string on whitespace, but maintains whitespace in date groups proc splitWhitespaceExceptParens(str:string): seq[string] = var result: seq[string] = newSeq[string]() var inParens = false for token in str.splitWhitespace: if inParens: result.add(result.pop & ' ' & token) else: result.add(token) for c in token: if c == '(': inParens = true if c == ')': inParens = false return result # Returns Table[conditionType, seq of conditions] proc parseDateToTable(date:string): Table[string,seq[string]] = var table = initTable[string,seq[string]]() let conditions = splitWhitespaceExceptParens(date) for c in conditions: let condType = checkType(c) if not table.hasKey(condType): table[condType] = @[] table[condType].add(c) return table proc getEvents(cal: seq[string], date: DateTime): seq[string] = var events: seq[string] = newSeq[string]() for line in cal: var (success, dateConditions, event) = scanTuple(line, "$*,$*") if success: dateConditions.strip event.strip let conditions = parseDateToTable(dateConditions) var match = true # key = Condition Type (e.g. month), cs = Seq of conditions for k,cs in conditions: var keyMatched = false for c in cs: #var cond = c.toLower.capitalizeAscii if checkCond(c, date) == true: keyMatched = true break if not keyMatched: match = false break if match: events.add(event) else: echo "WARNING: Cannot parse line: ", line return events proc printEvents(cal: seq[string], date: DateTime, format: string) = let events = getEvents(cal, date) if events.len > 0: for e in events: echo date.format(format), " ", e proc openCal(file: string): seq[string] = let f = open(file) var cal: seq[string] for line in f.lines: if line == "": continue if line.strip[0] == '#': continue cal.add(line) f.close() return cal proc printCalendar() = let cal = openCal(defaultCalendarFile) var past = -3 var future = 7 let now = now() for i in past..future: let curDay = now + i.days printEvents(cal, curDay, "ddd yyyy MMM dd") 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()