aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md25
-rw-r--r--src/soon.nim51
-rw-r--r--src/soon/agenda.nim16
-rw-r--r--src/soon/columnize.nim30
-rw-r--r--src/soon/todos.nim34
-rw-r--r--tests/tests.nim1
6 files changed, 105 insertions, 52 deletions
diff --git a/README.md b/README.md
index f671f7b..6d1fb2d 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ Soon is a minimalist, text file based, CLI calendar inspired by [When](https://w
- Schedule events in a text file with concise syntax
- Group, sort, comment, and columnize events to keep files neat
-- Add multiple lines of details to events
+- Add details to events
- Archive events to remove them from your schedule (e.g. paid bill)
- Optionally keep events on calendar until dismissed (e.g. missed oil change)
- Todo list with upcoming deadlines
@@ -18,7 +18,7 @@ In terms of features and complexity, Soon lies between [When](https://www.lighta
2. Check the example calendar and add some events/todos.
-3. Run `soon` to see your agenda for the next 14 days. Run `soon -cm` to print a reference calendar and your agenda for the next month.
+3. Run `soon` to see your agenda for the next 14 days. Run `soon -cmtd` to print a reference calendar, next month's agenda, todos, and upcoming deadlines.
4. Run `soon -i` to start interactive mode and archive events or todos. Select events/todos with HJKL and archive them with Space. Esc to exit.
@@ -59,11 +59,11 @@ Fri 1W, First Friday of the month
# .soon file syntax
-Each event is a line with date conditions followed by `,` and a description.
+Each line is a series of date conditions followed by `,` and the event name
Lines starting with `-` are todos
-Lines starting with `+` are attached to the above event/todo to add details or deadlines.
+Lines starting with `+` are attached to the above event/todo to add details or deadlines. I like indenting these but you don't have to.
Lines starting with `#` are comments and ignored, as are blank lines.
@@ -127,7 +127,7 @@ May 1L Mon, Memorial Day (Last Monday of May)
## Time
-You can express times in 24h format or AM/PM format like this. Your agenda will be sorted by the start time of events.
+You can express times in 24h format or AM/PM format. Your agenda will be sorted by the start time of events.
```
Fri 16:00, With no am/pm, 24h time is assumed
@@ -137,6 +137,9 @@ Fri 12pm, Noon
# You can use ranges too
Fri 05:00-06:00, Breakfast
Fri 12pm-1pm, Lunch
+
+# If you specify multiple times, the event will show up at each of those times
+6am 12pm 6pm, Walk the dog
```
## Adding details to events
@@ -145,11 +148,11 @@ Any line starting with a `+` is attached to the nearest above event. This can be
```
2025 Jan 5 16:00, Doctor's Appointment
-+ 123 Gentoo Road, San Francisco, CA 12345
-+ Dr. Elias Berkins, +1 (555)-123-1234
+ + 123 Bookworm Road, New York, NY 12345
+ + Dr. Raadt, +1 (555)-123-1234
```
-## Advanced Recurrence (Modified Julian Day `J%x+y`)
+## Advanced Recurrence (Modified Julian Day `J%x-y`)
Soon supports using a Modified Julian Day as `J`. `J` is the number of full days since midnight November 17, 1858. Combined with the modulo operator `%` and an optional `+`/`-` offset this creates complex recurring events.
@@ -162,7 +165,7 @@ J%14+2, Every other Saturday (on the other weeks)
Sat J%2, Every other Saturday expressed in a different way
```
-Julian date modulus is weird, but it's a concise way to express multiple uncommon recurring patterns without increasing program complexity and syntax vocabulary for features I virtually never use. Plus it's an homage to [When](https://www.lightandmatter.com/when/when.html).
+Julian date modulus is weird, but it's a concise way to express multiple uncommon recurring patterns without increasing complexity much for recurrence I virtually never need. Plus it's an homage to [When](https://www.lightandmatter.com/when/when.html).
#### Footnote for astronomers and time nerds
@@ -179,8 +182,8 @@ Attach a line with `+ Due: DATE` and it will show up in a list of upcoming deadl
```
- Wash car
- Clean room
-- Give book back to Sally
- + Address: 555 Oak Street, Openville
+- Give book back to Ada
+ + Address: 555 Oak Street, Portland, OR
- Finish TPS report
+ Due: Aug 7 2025
diff --git a/src/soon.nim b/src/soon.nim
index 2c5f109..61ac16d 100644
--- a/src/soon.nim
+++ b/src/soon.nim
@@ -1,4 +1,4 @@
-import std/[os, strutils, strtabs, sequtils, parseopt]
+import std/[os, strutils, strtabs, sequtils, osproc, parseopt]
import soon/[config, agenda, todos]
const VERSION = "0.1"
@@ -10,14 +10,15 @@ proc printHelp(badCmd: string) =
Usage: soon [options]
Options:
--a print agenda (default behavior if no options)
+-a print agenda
-c print reference calendar
-t print todos
+-d print deadlines
-e open default calendar file in $EDITOR
-i open TUI in interactive mode to schedule/archive events
-w, -m, -y print agenda for upcoming week, month, or year
-j print the modified julian day
--d print today's date
+-i, --date print today's date
-p, --past=DAYS days in past to print agenda (default: 1)
-f, --future=DAYS days in future to print agenda (default: 14)
-h, --help show this help
@@ -44,9 +45,15 @@ proc getFileLines(file: string): seq[string] =
f.close()
proc readAllSoonFiles(path: string): seq[string] =
+ var soonFileExists = false
for file in os.walkDirRec(path):
if file.toLower().endsWith(".soon"):
+ soonFileExists = true
result = result.concat(getFileLines(file))
+ if not soonFileExists:
+ echo "No .soon files exist at " & path
+ echo "Check your paths in ~/.config/soon/soon.conf"
+ quit()
proc processCalendars(path: string, past: int, future: int) =
var calendar = readAllSoonFiles(path)
@@ -67,15 +74,51 @@ proc parseArgs(config: StringTableRef): seq[string] =
result.add(key)
of cmdArgument:
printHelp(key)
+ if result.len == 0:
+ result.add("a")
+ result.add("t")
+ result.add("d")
+ echo "I'm running soon -atd, but I should've checked the user config and run that!"
+
+proc printReference() =
+ discard execCmd("cal -3")
+
+proc sanitize(str: string): string =
+ result = ""
+ for ch in str:
+ if ch.isAlphaNumeric() or ch in {'/', '~', '.'}:
+ result.add(ch)
+
+proc openEditor(path: string, file: string) =
+ var editor = getEnv("EDITOR").sanitize
+ if editor.isEmptyOrWhitespace: editor = "vim"
+ let sanePath = path.sanitize
+ let saneFile = file.sanitize
+ discard execCmd("$1 $2/$3" % [editor, sanePath, saneFile])
proc validateConfig(config: StringTableRef) =
createDefaultCalendarFile(config["path"] / config["defaultFile"])
+proc processCommands(commands: seq[string], conf: StringTableRef) =
+ var calendar = readAllSoonFiles(conf["calendarPath"])
+ var past = 0 - parseInt(conf["past"])
+ var future = parseInt(conf["future"])
+ for command in commands:
+ case command
+ of "a": agenda.printAgenda(calendar, past, future)
+ of "w": agenda.printAgenda(calendar, 0, 7)
+ of "m": agenda.printAgenda(calendar, 0, 28)
+ of "y": agenda.printAgenda(calendar, 0, 365)
+ of "t": todos.printTodos(calendar)
+ of "d": todos.printDeadlineTodos(calendar)
+ of "c": printReference()
+ of "e": openEditor(conf["calendarPath"], conf["defaultFile"])
+
proc main() =
var conf = config.loadConfig()
let commands = parseArgs(conf)
validateConfig(conf)
- processCalendars(conf["calendarPath"], 0 - parseInt(conf["past"]), parseInt(conf["future"]))
+ processCommands(commands, conf)
when isMainModule:
main()
diff --git a/src/soon/agenda.nim b/src/soon/agenda.nim
index 29adf28..1c2ce11 100644
--- a/src/soon/agenda.nim
+++ b/src/soon/agenda.nim
@@ -1,4 +1,4 @@
-import std/[times, strutils, strbasics, strscans, sequtils, tables, algorithm, re]
+import std/[times, strutils, sequtils, tables, algorithm]
import conditionchecker, typechecker, parsetime, columnize
type Event = object
@@ -87,11 +87,11 @@ proc getCalendarForDate(cal: seq[string], date: DateTime): Calendar =
proc compare(a, b: string): int =
cmp(a.len, b.len)
-proc startWithDate(datePrinted: bool, date: DateTime, format: string): seq[string] =
+proc startWithDate(datePrinted: bool, date: DateTime, format: string): seq[Column] =
if datePrinted:
- result.add("")
+ result.add(Column(text: "", color: fgWhite))
else:
- result.add(date.format(format))
+ result.add(Column(text: date.format(format), color: fgYellow))
proc eventTime(event: Event): string =
if event.time.monthday != 1:
@@ -106,15 +106,15 @@ proc getDaysEvents(cal: seq[string], date: DateTime, format: string): seq[Line]
for event in events:
var columns = startWithDate(datePrinted, date, format)
datePrinted = true
- columns.add(eventTime(event))
- columns.add(event.name)
+ columns.add(Column(text: eventTime(event), color: fgBlue))
+ columns.add(Column(text: event.name, color: fgWhite))
result.add(Line(columns: columns, attachments: event.attachments))
proc printAgenda*(calendar: seq[string], past: int, future: int) =
let now = now()
let today = dateTime(now.year, now.month, now.monthday, 00, 00, 00, 00, local())
var agenda = newSeq[Line]()
- for i in past..future:
- let curDay = today + i.days
+ for day in past..future:
+ let curDay = today + day.days
agenda = agenda.concat(getDaysEvents(calendar, curDay, "ddd yyyy MMM dd"))
columnize.echo(agenda)
diff --git a/src/soon/columnize.nim b/src/soon/columnize.nim
index 0fba176..a9074f1 100644
--- a/src/soon/columnize.nim
+++ b/src/soon/columnize.nim
@@ -1,18 +1,22 @@
-import std/[strutils, enumerate, sequtils]
+import std/[strutils, enumerate, sequtils, terminal]
+
+type Column* = object
+ text*: string
+ color*: ForegroundColor
type Line* = object
- columns*: seq[string]
+ columns*: seq[Column]
attachments*: seq[string]
-proc prefixSpaceCount(s: seq[string]): int =
- return foldl(s[0..s.len-2], a + b.len, 0) + (2 * s.len-2)
+proc prefixSpaceCount(s: seq[Column]): int =
+ return foldl(s[0..s.len-2], a + b.text.len, 0) + (2 * s.len-2)
proc pad(table: seq[Line], max: seq[int]): seq[Line] =
for line in table:
- var resLine = newSeq[string]()
+ var resLine = newSeq[Column]()
var resAttach = newSeq[string]()
for i, field in enumerate(line.columns):
- resLine.add(field & repeat(' ', max[i] - field.len))
+ resLine.add(Column(text:field.text & repeat(' ', max[i] - field.text.len), color: field.color))
if line.attachments.len > 0:
let spaces = prefixSpaceCount(resLine)
for attachment in line.attachments:
@@ -23,13 +27,19 @@ proc columnize(table: seq[Line]): seq[Line] =
var max = newSeq[int](table[0].columns.len)
for line in table:
for i, field in enumerate(line.columns):
- if field.len > max[i]:
- max[i] = field.len
+ if field.text.len > max[i]:
+ max[i] = field.text.len
result = pad(table, max)
+proc writeColumns(line: Line) =
+ for column in line.columns[0 .. ^2]:
+ stdout.styledWrite(column.color, column.text & " ")
+ stdout.styledWrite(line.columns[^1].color, line.columns[^1].text)
+
proc echo*(table: seq[Line]) =
let lines = columnize(table)
for line in lines:
- echo line.columns.join(" ")
+ writeColumns(line)
+ stdout.writeLine("")
for attachment in line.attachments:
- echo attachment
+ stdout.styledWriteLine(fgMagenta, attachment)
diff --git a/src/soon/todos.nim b/src/soon/todos.nim
index 569bb8f..cac8c90 100644
--- a/src/soon/todos.nim
+++ b/src/soon/todos.nim
@@ -1,4 +1,4 @@
-import std/[times, strutils, tables, strtabs, re, options]
+import std/[times, strutils, re, options, terminal]
import conditionchecker, columnize
type
@@ -50,35 +50,33 @@ proc splitRegularAndDeadlineTodos(todos: seq[Todo]): (seq[Todo], seq[Todo]) =
else:
result[0].add(todo)
-proc printRegularTodos(todos: seq[Todo]) =
- if todos.len == 0: return
- echo "\nTodo List:"
- for todo in todos:
- echo todo.name
- for attachment in todo.attachments:
- echo attachment
-
proc toLine(todo: Todo): Line =
- result.columns = @[todo.deadline.format("ddd yyyy MMM dd")]
+ result.columns = @[Column(text: todo.deadline.format("ddd yyyy MMM dd"), color: fgYellow)]
let due = (todo.deadline - now()).inDays
if due > 0:
- result.columns.add("(Due in " & due.intToStr & " days)")
+ result.columns.add(Column(text: "(Due in " & due.intToStr & " days)", color: fgGreen))
else:
- result.columns.add("(OVERDUE)")
- result.columns.add(todo.name)
+ result.columns.add(Column(text: "(OVERDUE)", color: fgRed))
+ result.columns.add(Column(text: todo.name, color: fgWhite))
result.attachments = todo.attachments
# TODO: Sort on deadlines
-proc printDeadlineTodos(todos: seq[Todo]) =
- if todos.len == 0: return
+proc printDeadlineTodos*(calendar: seq[string]) =
+ let todos = getTodosFromCalendar(calendar)
+ var (regulars, deadlines) = splitRegularAndDeadlineTodos(todos)
+ if deadlines.len == 0: return
echo "\nUpcoming Deadlines:"
var lines = newSeq[Line]()
- for todo in todos:
+ for todo in deadlines:
lines.add(todo.toLine)
columnize.echo(lines)
proc printTodos*(calendar: seq[string]) =
let todos = getTodosFromCalendar(calendar)
var (regulars, deadlines) = splitRegularAndDeadlineTodos(todos)
- printRegularTodos(regulars)
- printDeadlineTodos(deadlines)
+ if todos.len == 0: return
+ echo "\nTodo List:"
+ for todo in todos:
+ echo todo.name
+ for attachment in todo.attachments:
+ stdout.styledWriteLine(fgMagenta, attachment)
diff --git a/tests/tests.nim b/tests/tests.nim
deleted file mode 100644
index 45134c0..0000000
--- a/tests/tests.nim
+++ /dev/null
@@ -1 +0,0 @@
-include tconditionchecker, ttypechecker