summary refs log tree commit diff stats
path: root/examples/cross_todo/nim_commandline
diff options
context:
space:
mode:
authordef <dennis@felsin9.de>2015-02-15 00:05:16 +0100
committerdef <dennis@felsin9.de>2015-02-15 00:06:57 +0100
commitd19a4ca827ab04edc5b497b73132e3f40369e1a8 (patch)
tree11e70d0efc327e9d53607ff8c9a2a99f458be75c /examples/cross_todo/nim_commandline
parent6244cc2e4fb7aaabd14352a21add5236220a8198 (diff)
downloadNim-d19a4ca827ab04edc5b497b73132e3f40369e1a8.tar.gz
Fix cross_todo example
Diffstat (limited to 'examples/cross_todo/nim_commandline')
-rw-r--r--examples/cross_todo/nim_commandline/nim.cfg4
-rw-r--r--examples/cross_todo/nim_commandline/nimtodo.nim327
-rw-r--r--examples/cross_todo/nim_commandline/readme.txt19
3 files changed, 350 insertions, 0 deletions
diff --git a/examples/cross_todo/nim_commandline/nim.cfg b/examples/cross_todo/nim_commandline/nim.cfg
new file mode 100644
index 000000000..41c034430
--- /dev/null
+++ b/examples/cross_todo/nim_commandline/nim.cfg
@@ -0,0 +1,4 @@
+# Nimrod configuration file.
+# The file is used only to add the path of the backend to the compiler options.
+
+path="../nim_backend"
diff --git a/examples/cross_todo/nim_commandline/nimtodo.nim b/examples/cross_todo/nim_commandline/nimtodo.nim
new file mode 100644
index 000000000..4ab17e7a2
--- /dev/null
+++ b/examples/cross_todo/nim_commandline/nimtodo.nim
@@ -0,0 +1,327 @@
+# Implements a command line interface against the backend.
+
+import backend, db_sqlite, os, parseopt, parseutils, strutils, times
+
+const
+  USAGE = """nimtodo - Nimrod cross platform todo manager
+
+Usage:
+  nimtodo [command] [list options]
+
+Commands:
+  -a=int text Adds a todo entry with the specified priority and text.
+  -c=int      Marks the specified todo entry as done.
+  -u=int      Marks the specified todo entry as not done.
+  -d=int|all  Deletes a single entry from the database, or all entries.
+  -g          Generates some rows with values for testing.
+  -l          Lists the contents of the database.
+  -h, --help  shows this help
+
+List options (optional):
+  -p=+|-      Sorts list by ascending|desdencing priority. Default:desdencing.
+  -m=+|-      Sorts list by ascending|desdencing date. Default:desdencing.
+  -t          Show checked entries. By default they are not shown.
+  -z          Hide unchecked entries. By default they are shown.
+
+Examples:
+  nimtodo -a=4 Water the plants
+  nimtodo -c:87
+  nimtodo -d:2
+  nimtodo -d:all
+  nimtodo -l -p=+ -m=- -t
+
+"""
+
+type
+  TCommand = enum     # The possible types of commands
+    cmdAdd            # The user wants to add a new todo entry.
+    cmdCheck          # User wants to check a todo entry.
+    cmdUncheck        # User wants to uncheck a todo entry.
+    cmdDelete         # User wants to delete a single todo entry.
+    cmdNuke           # User wants to purge all database entries.
+    cmdGenerate       # Add random rows to the database, for testing.
+    cmdList           # User wants to list contents.
+
+  TParamConfig = object
+    # Structure containing the parsed options from the commandline.
+    command: TCommand         # Store the type of operation
+    addPriority: int          # Only valid with cmdAdd, stores priority.
+    addText: seq[string]      # Only valid with cmdAdd, stores todo text.
+    todoId: int64             # The todo id for operations like check or delete.
+    listParams: TPagedParams  # Uses the backend structure directly for params.
+
+
+proc initDefaults(params: var TParamConfig) =
+  ## Initialises defaults value in the structure.
+  ##
+  ## Most importantly we want to have an empty list for addText.
+  params.listParams.initDefaults
+  params.addText = @[]
+
+
+proc abort(message: string, value: int) =
+  # Simple wrapper to abort also displaying the help to the user.
+  stdout.write(USAGE)
+  quit(message, value)
+
+
+template parseTodoIdAndSetCommand(newCommand: TCommand): stmt =
+  ## Helper to parse a big todo identifier into todoId and set command.
+  try:
+    let numChars = val.parseBiggestInt(newId)
+    if numChars < 1: raise newException(ValueError, "Empty string?")
+    result.command = newCommand
+    result.todoId = newId
+  except OverflowError:
+    raise newException(ValueError, "Value $1 too big" % val)
+
+
+template verifySingleCommand(actions: stmt): stmt =
+  ## Helper to make sure only one command has been specified so far.
+  if specifiedCommand:
+    abort("Only one command can be specified at a time! (extra:$1)" % [key], 2)
+  else:
+    actions
+    specifiedCommand = true
+
+
+proc parsePlusMinus(val: string, debugText: string): bool =
+  ## Helper to process a plus or minus character from the commandline.
+  ##
+  ## Pass the string to parse and the type of parameter for debug errors.
+  ## The processed parameter will be returned as true for a '+' and false for a
+  ## '-'. The proc aborts with a debug message if the passed parameter doesn't
+  ## contain one of those values.
+  case val
+  of "+":
+    return true
+  of "-":
+    return false
+  else:
+    abort("$1 parameter should be + or - but was '$2'." % [debugText, val], 4)
+
+
+proc parseCmdLine(): TParamConfig =
+  ## Parses the commandline.
+  ##
+  ## Returns a TParamConfig structure filled with the proper values or directly
+  ## calls quit() with the appropriate error message.
+  var
+    specifiedCommand = false
+    usesListParams = false
+    p = initOptParser()
+    key, val: TaintedString
+    newId: BiggestInt
+
+  result.initDefaults
+
+  try:
+    while true:
+      next(p)
+      key = p.key
+      val = p.val
+
+      case p.kind
+      of cmdArgument:
+        if specifiedCommand and cmdAdd == result.command:
+          result.addText.add(key)
+        else:
+          abort("Argument ($1) detected without add command." % [key], 1)
+      of cmdLongOption, cmdShortOption:
+        case normalize(key)
+        of "help", "h":
+          stdout.write(USAGE)
+          quit(0)
+        of "a":
+          verifySingleCommand:
+            result.command = cmdAdd
+            result.addPriority = val.parseInt
+        of "c":
+          verifySingleCommand:
+            parseTodoIdAndSetCommand(cmdCheck)
+        of "u":
+          verifySingleCommand:
+            parseTodoIdAndSetCommand cmdUncheck
+        of "d":
+          verifySingleCommand:
+            if "all" == val:
+              result.command = cmdNuke
+            else:
+              parseTodoIdAndSetCommand cmdDelete
+        of "g":
+          verifySingleCommand:
+            if val.len > 0:
+              abort("Unexpected value '$1' for switch l." % [val], 3)
+            result.command = cmdGenerate
+        of "l":
+          verifySingleCommand:
+            if val.len > 0:
+              abort("Unexpected value '$1' for switch l." % [val], 3)
+            result.command = cmdList
+        of "p":
+          usesListParams = true
+          result.listParams.priorityAscending = parsePlusMinus(val, "Priority")
+        of "m":
+          usesListParams = true
+          result.listParams.dateAscending = parsePlusMinus(val, "Date")
+        of "t":
+          usesListParams = true
+          if val.len > 0:
+            abort("Unexpected value '$1' for switch t." % [val], 5)
+          result.listParams.showChecked = true
+        of "z":
+          usesListParams = true
+          if val.len > 0:
+            abort("Unexpected value '$1' for switch z." % [val], 5)
+          result.listParams.showUnchecked = false
+        else:
+          abort("Unexpected option '$1'." % [key], 6)
+      of cmdEnd:
+        break
+  except ValueError:
+    abort("Invalid integer value '$1' for parameter '$2'." % [val, key], 7)
+
+  if not specifiedCommand:
+    abort("Didn't specify any command.", 8)
+
+  if cmdAdd == result.command and result.addText.len < 1:
+    abort("Used the add command, but provided no text/description.", 9)
+
+  if usesListParams and cmdList != result.command:
+    abort("Used list options, but didn't specify the list command.", 10)
+
+
+proc generateDatabaseRows(conn: TDbConn) =
+  ## Adds some rows to the database ignoring errors.
+  discard conn.addTodo(1, "Watch another random youtube video")
+  discard conn.addTodo(2, "Train some starcraft moves for the league")
+  discard conn.addTodo(3, "Spread the word about Nimrod")
+  discard conn.addTodo(4, "Give fruit superavit to neighbours")
+  var todo = conn.addTodo(4, "Send tax form through snail mail")
+  todo.isDone = true
+  discard todo.save(conn)
+  discard conn.addTodo(1, "Download new anime to watch")
+  todo = conn.addTodo(2, "Build train model from scraps")
+  todo.isDone = true
+  discard todo.save(conn)
+  discard conn.addTodo(5, "Buy latest Britney Spears album")
+  discard conn.addTodo(6, "Learn a functional programming language")
+  echo("Generated some entries, they were added to your database.")
+
+
+proc listDatabaseContents(conn: TDbConn; listParams: TPagedParams) =
+  ## Dumps the database contents formatted to the standard output.
+  ##
+  ## Pass the list/filter parameters parsed from the commandline.
+  var params = listParams
+  params.pageSize = -1
+
+  let todos = conn.getPagedTodos(params)
+  if todos.len < 1:
+    echo("Database empty")
+    return
+
+  echo("Todo id, is done, priority, last modification date, text:")
+  # First detect how long should be our columns for formatting.
+  var cols: array[0..2, int]
+  for todo in todos:
+    cols[0] = max(cols[0], ($todo.getId).len)
+    cols[1] = max(cols[1], ($todo.priority).len)
+    cols[2] = max(cols[2], ($todo.getModificationDate).len)
+
+  # Now dump all the rows using the calculated alignment sizes.
+  for todo in todos:
+    echo("$1 $2 $3, $4, $5" % [
+      ($todo.getId).align(cols[0]),
+      if todo.isDone: "[X]" else: "[-]",
+      ($todo.priority).align(cols[1]),
+      ($todo.getModificationDate).align(cols[2]),
+      todo.text])
+
+
+proc deleteOneTodo(conn: TDbConn; todoId: int64) =
+  ## Deletes a single todo entry from the database.
+  let numDeleted = conn.deleteTodo(todoId)
+  if numDeleted > 0:
+    echo("Deleted todo id " & $todoId)
+  else:
+    quit("Couldn't delete todo id " & $todoId, 11)
+
+
+proc deleteAllTodos(conn: TDbConn) =
+  ## Deletes all the contents from the database.
+  ##
+  ## Note that it would be more optimal to issue a direct DELETE sql statement
+  ## on the database, but for the sake of the example we will restrict
+  ## ourselfves to the API exported by backend.
+  var
+    counter: int64
+    params: TPagedParams
+
+  params.initDefaults
+  params.pageSize = -1
+  params.showUnchecked = true
+  params.showChecked = true
+
+  let todos = conn.getPagedTodos(params)
+  for todo in todos:
+    if conn.deleteTodo(todo.getId) > 0:
+      counter += 1
+    else:
+      quit("Couldn't delete todo id " & $todo.getId, 12)
+
+  echo("Deleted $1 todo entries from database." % $counter)
+
+
+proc setTodoCheck(conn: TDbConn; todoId: int64; value: bool) =
+  ## Changes the check state of a todo entry to the specified value.
+  let
+    newState = if value: "checked" else: "unchecked"
+    todo = conn.getTodo(todoId)
+
+  if todo == nil:
+    quit("Can't modify todo id $1, its not in the database." % $todoId, 13)
+
+  if todo[].isDone == value:
+    echo("Todo id $1 was already set to $2." % [$todoId, newState])
+    return
+
+  todo[].isDone = value
+  if todo[].save(conn):
+    echo("Todo id $1 set to $2." % [$todoId, newState])
+  else:
+    quit("Error updating todo id $1 to $2." % [$todoId, newState])
+
+
+proc addTodo(conn: TDbConn; priority: int; tokens: seq[string]) =
+  ## Adds to the database a todo with the specified priority.
+  ##
+  ## The tokens are joined as a single string using the space character. The
+  ## created id will be displayed to the user.
+  let todo = conn.addTodo(priority, tokens.join(" "))
+  echo("Created todo entry with id:$1 for priority $2 and text '$3'." % [
+    $todo.getId, $todo.priority, todo.text])
+
+
+when isMainModule:
+  ## Main entry point.
+  let
+    opt = parseCmdLine()
+    dbPath = getConfigDir() / "nimtodo.sqlite3"
+
+  if not dbPath.existsFile:
+    createDir(getConfigDir())
+    echo("No database found at $1, it will be created for you." % dbPath)
+
+  let conn = openDatabase(dbPath)
+  try:
+    case opt.command
+    of cmdAdd: addTodo(conn, opt.addPriority, opt.addText)
+    of cmdCheck: setTodoCheck(conn, opt.todoId, true)
+    of cmdUncheck: setTodoCheck(conn, opt.todoId, false)
+    of cmdDelete: deleteOneTodo(conn, opt.todoId)
+    of cmdNuke: deleteAllTodos(conn)
+    of cmdGenerate: generateDatabaseRows(conn)
+    of cmdList: listDatabaseContents(conn, opt.listParams)
+  finally:
+    conn.close
diff --git a/examples/cross_todo/nim_commandline/readme.txt b/examples/cross_todo/nim_commandline/readme.txt
new file mode 100644
index 000000000..ca4b67521
--- /dev/null
+++ b/examples/cross_todo/nim_commandline/readme.txt
@@ -0,0 +1,19 @@
+This directory contains the Nim commandline version of the todo cross
+platform example.
+
+The commandline interface can be used only through switches, running the binary
+once will spit out the basic help. The commands you can use are the typical on
+such an application: add, check/uncheck and delete (further could be added,
+like modification at expense of parsing/option complexity). The list command is
+the only one which dumps the contents of the database. The output can be
+filtered and sorted through additional parameters.
+
+When you run the program for the first time the todo database will be generated
+in your user's data directory. To cope with an empty database, a special
+generation switch can be used to fill the database with some basic todo entries
+you can play with.
+
+Compilation is fairly easy despite having the source split in different
+directories. Thanks to the Nim.cfg file, which adds the ../Nim_backend
+directory as a search path, you can compile and run the example just fine from
+the command line with 'nim c -r nimtodo.nim'.