Merge branch 'master' of github.com:Araq/Nimrod
This commit is contained in:
commit
9fea5b8f69
9 changed files with 741 additions and 16 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -16,12 +16,17 @@ compiler/pas2nim/nimcache
|
||||||
misc
|
misc
|
||||||
doc/*.html
|
doc/*.html
|
||||||
doc/*.idx
|
doc/*.idx
|
||||||
web/upload/*.html
|
/web/upload
|
||||||
koch
|
koch
|
||||||
compiler/nimrod*
|
compiler/nimrod*
|
||||||
build/[0-9]_[0-9]
|
build/[0-9]_[0-9]
|
||||||
bin/nimrod
|
bin/nimrod
|
||||||
examples/cross_calculator/nimrod_commandline/nimcalculator
|
examples/cross_calculator/nimrod_commandline/nimcalculator
|
||||||
|
examples/cross_todo/nimrod_backend/*.html
|
||||||
|
examples/cross_todo/nimrod_backend/backend
|
||||||
|
examples/cross_todo/nimrod_backend/testbackend
|
||||||
|
examples/cross_todo/nimrod_backend/todo.sqlite3
|
||||||
|
examples/cross_todo/nimrod_commandline/nimtodo
|
||||||
|
|
||||||
# iOS specific wildcards.
|
# iOS specific wildcards.
|
||||||
*.mode1v3
|
*.mode1v3
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,5 @@ platforms.
|
||||||
To avoid duplication of code, the backend code lies in a separate directory and
|
To avoid duplication of code, the backend code lies in a separate directory and
|
||||||
each platform compiles it with a different custom build process, usually
|
each platform compiles it with a different custom build process, usually
|
||||||
generating C code in a temporary build directory.
|
generating C code in a temporary build directory.
|
||||||
|
|
||||||
|
For a more ellaborate and useful example see the cross_todo example.
|
||||||
|
|
|
||||||
222
examples/cross_todo/nimrod_backend/backend.nim
Normal file
222
examples/cross_todo/nimrod_backend/backend.nim
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
# Backend for a simple todo program with sqlite persistence.
|
||||||
|
#
|
||||||
|
# Most procs dealing with a TDbConn object may raise an EDb exception.
|
||||||
|
|
||||||
|
import db_sqlite
|
||||||
|
import parseutils
|
||||||
|
import strutils
|
||||||
|
import times
|
||||||
|
|
||||||
|
|
||||||
|
type
|
||||||
|
TTodo* = object of TObject
|
||||||
|
## A todo object holding the information serialized to the database.
|
||||||
|
id: int64 ## Unique identifier of the object in the
|
||||||
|
## database, use the getId() accessor to read it.
|
||||||
|
text*: string ## Description of the task to do.
|
||||||
|
priority*: int ## The priority can be any user defined integer.
|
||||||
|
isDone*: bool ## Done todos are still kept marked.
|
||||||
|
modificationDate: TTime ## The modification time can't be modified from
|
||||||
|
## outside of this module, use the
|
||||||
|
## getModificationDate accessor.
|
||||||
|
|
||||||
|
TPagedParams* = object of TObject
|
||||||
|
## Contains parameters for a query, initialize default values with
|
||||||
|
## initDefaults().
|
||||||
|
pageSize*: int64 ## Lines per returned query page, -1 for
|
||||||
|
## unlimited.
|
||||||
|
priorityAscending*: bool ## Sort results by ascending priority.
|
||||||
|
dateAscending*: bool ## Sort results by ascending modification date.
|
||||||
|
showUnchecked*: bool ## Get unchecked objects.
|
||||||
|
showChecked*: bool ## Get checked objects.
|
||||||
|
|
||||||
|
|
||||||
|
# - General procs
|
||||||
|
#
|
||||||
|
proc initDefaults*(params: var TPagedParams) =
|
||||||
|
## Sets sane defaults for a TPagedParams object.
|
||||||
|
##
|
||||||
|
## Note that you should always provide a non zero pageSize, either a specific
|
||||||
|
## positive value or negative for unbounded query results.
|
||||||
|
params.pageSize = high(int64)
|
||||||
|
params.priorityAscending = false
|
||||||
|
params.dateAscending = false
|
||||||
|
params.showUnchecked = true
|
||||||
|
params.showChecked = false
|
||||||
|
|
||||||
|
|
||||||
|
proc openDatabase*(path: string): TDbConn =
|
||||||
|
## Creates or opens the sqlite3 database.
|
||||||
|
##
|
||||||
|
## Pass the path to the sqlite database, if the database doesn't exist it
|
||||||
|
## will be created. The proc may raise a EDB exception
|
||||||
|
let
|
||||||
|
conn = db_sqlite.open(path, "user", "pass", "db")
|
||||||
|
query = sql"""CREATE TABLE IF NOT EXISTS Todos (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
priority INTEGER NOT NULL,
|
||||||
|
is_done BOOLEAN NOT NULL,
|
||||||
|
desc TEXT NOT NULL,
|
||||||
|
modification_date INTEGER NOT NULL,
|
||||||
|
CONSTRAINT Todos UNIQUE (id))"""
|
||||||
|
|
||||||
|
db_sqlite.exec(conn, query)
|
||||||
|
result = conn
|
||||||
|
|
||||||
|
|
||||||
|
# - Procs related to TTodo objects
|
||||||
|
#
|
||||||
|
proc initFromDB(id: int64; text: string; priority: int, isDone: bool;
|
||||||
|
modificationDate: TTime): TTodo =
|
||||||
|
## Returns an initialized TTodo object created from database parameters.
|
||||||
|
##
|
||||||
|
## The proc assumes all values are right. Note this proc is NOT exported.
|
||||||
|
assert(id >= 0, "Identity identifiers should not be negative")
|
||||||
|
result.id = id
|
||||||
|
result.text = text
|
||||||
|
result.priority = priority
|
||||||
|
result.isDone = isDone
|
||||||
|
result.modificationDate = modificationDate
|
||||||
|
|
||||||
|
|
||||||
|
proc getId*(todo: TTodo): int64 =
|
||||||
|
## Accessor returning the value of the private id property.
|
||||||
|
return todo.id
|
||||||
|
|
||||||
|
|
||||||
|
proc getModificationDate*(todo: TTodo): TTime =
|
||||||
|
## Returns the last modification date of a TTodo entry.
|
||||||
|
return todo.modificationDate
|
||||||
|
|
||||||
|
|
||||||
|
proc update*(todo: var TTodo; conn: TDbConn): bool =
|
||||||
|
## Checks the database for the object and refreshes its variables.
|
||||||
|
##
|
||||||
|
## Use this method if you (or another entity) have modified the database and
|
||||||
|
## want to update the object you have with whatever the database has stored.
|
||||||
|
## Returns true if the update suceeded, or false if the object was not found
|
||||||
|
## in the database any more, in which case you should probably get rid of the
|
||||||
|
## TTodo object.
|
||||||
|
assert(todo.id >= 0, "The identifier of the todo entry can't be negative")
|
||||||
|
let query = sql"""SELECT desc, priority, is_done, modification_date
|
||||||
|
FROM Todos WHERE id = ?"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
let rows = conn.GetAllRows(query, $todo.id)
|
||||||
|
if len(rows) < 1:
|
||||||
|
return
|
||||||
|
assert(1 == len(rows), "Woah, didn't expect so many rows")
|
||||||
|
todo.text = rows[0][0]
|
||||||
|
todo.priority = rows[0][1].parseInt
|
||||||
|
todo.isDone = rows[0][2].parseBool
|
||||||
|
todo.modificationDate = TTime(rows[0][3].parseInt)
|
||||||
|
result = true
|
||||||
|
except:
|
||||||
|
echo("Something went wrong selecting for id " & $todo.id)
|
||||||
|
|
||||||
|
|
||||||
|
proc save*(todo: var TTodo; conn: TDbConn): bool =
|
||||||
|
## Saves the current state of text, priority and isDone to the database.
|
||||||
|
##
|
||||||
|
## Returns true if the database object was updated (in which case the
|
||||||
|
## modification date will have changed). The proc can return false if the
|
||||||
|
## object wasn't found, for instance, in which case you should drop that
|
||||||
|
## object anyway and create a new one with addTodo(). Also EDb can be raised.
|
||||||
|
assert(todo.id >= 0, "The identifier of the todo entry can't be negative")
|
||||||
|
let
|
||||||
|
currentDate = getTime()
|
||||||
|
query = sql"""UPDATE Todos
|
||||||
|
SET desc = ?, priority = ?, is_done = ?, modification_date = ?
|
||||||
|
WHERE id = ?"""
|
||||||
|
rowsUpdated = conn.execAffectedRows(query, $todo.text,
|
||||||
|
$todo.priority, $todo.isDone, $int(currentDate), $todo.id)
|
||||||
|
|
||||||
|
if 1 == rowsUpdated:
|
||||||
|
todo.modificationDate = currentDate
|
||||||
|
result = true
|
||||||
|
|
||||||
|
|
||||||
|
# - Procs dealing directly with the database
|
||||||
|
#
|
||||||
|
proc addTodo*(conn: TDbConn; priority: int; text: string): TTodo =
|
||||||
|
## Inserts a new todo into the database.
|
||||||
|
##
|
||||||
|
## Returns the generated todo object. If there is an error EDb will be raised.
|
||||||
|
let
|
||||||
|
currentDate = getTime()
|
||||||
|
query = sql"""INSERT INTO Todos
|
||||||
|
(priority, is_done, desc, modification_date)
|
||||||
|
VALUES (?, 'false', ?, ?)"""
|
||||||
|
todoId = conn.insertId(query, priority, text, $int(currentDate))
|
||||||
|
|
||||||
|
result = initFromDB(todoId, text, priority, false, currentDate)
|
||||||
|
|
||||||
|
|
||||||
|
proc deleteTodo*(conn: TDbConn; todoId: int64): int64 {.discardable.} =
|
||||||
|
## Deletes the specified todo identifier.
|
||||||
|
##
|
||||||
|
## Returns the number of rows which were affected (1 or 0)
|
||||||
|
let query = sql"""DELETE FROM Todos WHERE id = ?"""
|
||||||
|
result = conn.execAffectedRows(query, $todoId)
|
||||||
|
|
||||||
|
|
||||||
|
proc getNumEntries*(conn: TDbConn): int =
|
||||||
|
## Returns the number of entries in the Todos table.
|
||||||
|
##
|
||||||
|
## If the function succeeds, returns the zero or positive value, if something
|
||||||
|
## goes wrong a negative value is returned.
|
||||||
|
let query = sql"""SELECT COUNT(id) FROM Todos"""
|
||||||
|
try:
|
||||||
|
let row = conn.getRow(query)
|
||||||
|
result = row[0].parseInt
|
||||||
|
except:
|
||||||
|
echo("Something went wrong retrieving number of Todos entries")
|
||||||
|
result = -1
|
||||||
|
|
||||||
|
|
||||||
|
proc getPagedTodos*(conn: TDbConn; params: TPagedParams;
|
||||||
|
page = 0'i64): seq[TTodo] =
|
||||||
|
## Returns the todo entries for a specific page.
|
||||||
|
##
|
||||||
|
## Pages are calculated based on the params.pageSize parameter, which can be
|
||||||
|
## set to a negative value to specify no limit at all. The query will be
|
||||||
|
## affected by the TPagedParams, which should have sane values (call
|
||||||
|
## initDefaults).
|
||||||
|
assert(page >= 0, "You should request a page zero or bigger than zero")
|
||||||
|
result = @[]
|
||||||
|
|
||||||
|
# Well, if you don't want to see anything, there's no point in asking the db.
|
||||||
|
if not params.showUnchecked and not params.showChecked: return
|
||||||
|
|
||||||
|
let
|
||||||
|
order_by = [
|
||||||
|
if params.priorityAscending: "ASC" else: "DESC",
|
||||||
|
if params.dateAscending: "ASC" else: "DESC"]
|
||||||
|
|
||||||
|
query = sql("""SELECT id, desc, priority, is_done, modification_date
|
||||||
|
FROM Todos
|
||||||
|
WHERE is_done = ? OR is_done = ?
|
||||||
|
ORDER BY priority $1, modification_date $2, id DESC
|
||||||
|
LIMIT ? * ?,?""" % order_by)
|
||||||
|
|
||||||
|
args = @[$params.showChecked, $(not params.showUnchecked),
|
||||||
|
$params.pageSize, $page, $params.pageSize]
|
||||||
|
|
||||||
|
#echo("Query " & string(query))
|
||||||
|
#echo("args: " & args.join(", "))
|
||||||
|
|
||||||
|
var newId: biggestInt
|
||||||
|
for row in conn.fastRows(query, args):
|
||||||
|
let numChars = row[0].parseBiggestInt(newId)
|
||||||
|
assert(numChars > 0, "Huh, couldn't parse identifier from database?")
|
||||||
|
result.add(initFromDB(int64(newId), row[1], row[2].parseInt,
|
||||||
|
row[3].parseBool, TTime(row[4].parseInt)))
|
||||||
|
|
||||||
|
|
||||||
|
proc getTodo*(conn: TDbConn; todoId: int64): ref TTodo =
|
||||||
|
## Returns a reference to a TTodo or nil if the todo could not be found.
|
||||||
|
var tempTodo: TTodo
|
||||||
|
tempTodo.id = todoId
|
||||||
|
if tempTodo.update(conn):
|
||||||
|
new(result)
|
||||||
|
result[] = tempTodo
|
||||||
14
examples/cross_todo/nimrod_backend/readme.txt
Normal file
14
examples/cross_todo/nimrod_backend/readme.txt
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
This directory contains the nimrod backend code for the todo cross platform
|
||||||
|
example.
|
||||||
|
|
||||||
|
Unlike the cross platform calculator example, this backend features more code,
|
||||||
|
using an sqlite database for storage. Also a basic test module is provided, not
|
||||||
|
to be included with the final program but to test the exported functionality.
|
||||||
|
The test is not embedded directly in the backend.nim file to avoid being able
|
||||||
|
to access internal data types and procs not exported and replicate the
|
||||||
|
environment of client code.
|
||||||
|
|
||||||
|
In a bigger project with several people you could run `nimrod doc backend.nim`
|
||||||
|
(or use the doc2 command for a whole project) and provide the generated html
|
||||||
|
documentation to another programer for her to implement an interface without
|
||||||
|
having to look at the source code.
|
||||||
86
examples/cross_todo/nimrod_backend/testbackend.nim
Normal file
86
examples/cross_todo/nimrod_backend/testbackend.nim
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
# Tests the backend code.
|
||||||
|
|
||||||
|
import backend
|
||||||
|
import db_sqlite
|
||||||
|
import strutils
|
||||||
|
import times
|
||||||
|
|
||||||
|
|
||||||
|
proc showPagedResults(conn: TDbConn; params: TPagedParams) =
|
||||||
|
## Shows the contents of the database in pages of specified size.
|
||||||
|
##
|
||||||
|
## Hmm... I guess this is more of a debug proc which should be moved outside,
|
||||||
|
## or to a commandline interface (hint).
|
||||||
|
var
|
||||||
|
page = 0'i64
|
||||||
|
rows = conn.getPagedTodos(params)
|
||||||
|
|
||||||
|
while rows.len > 0:
|
||||||
|
echo("page " & $page)
|
||||||
|
for row in rows:
|
||||||
|
echo("row id:$1, text:$2, priority:$3, done:$4, date:$5" % [$row.getId,
|
||||||
|
$row.text, $row.priority, $row.isDone,
|
||||||
|
$row.getModificationDate])
|
||||||
|
# Query the database for the next page or quit.
|
||||||
|
if params.pageSize > 0:
|
||||||
|
page = page + 1
|
||||||
|
rows = conn.getPagedTodos(params, page)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
proc dumTest() =
|
||||||
|
let conn = openDatabase("todo.sqlite3")
|
||||||
|
try:
|
||||||
|
let numTodos = conn.getNumEntries
|
||||||
|
echo("Current database contains " & $numTodos & " todo items.")
|
||||||
|
if numTodos < 10:
|
||||||
|
# Fill some dummy rows if there are not many entries yet.
|
||||||
|
discard conn.addTodo(3, "Filler1")
|
||||||
|
discard conn.addTodo(4, "Filler2")
|
||||||
|
|
||||||
|
var todo = conn.addTodo(2, "Testing")
|
||||||
|
echo("New todo added with id " & $todo.getId)
|
||||||
|
|
||||||
|
# Try changing it and updating the database.
|
||||||
|
var clonedTodo = conn.getTodo(todo.getId)[]
|
||||||
|
assert(clonedTodo.text == todo.text, "Should be equal")
|
||||||
|
todo.text = "Updated!"
|
||||||
|
todo.priority = 7
|
||||||
|
todo.isDone = true
|
||||||
|
if todo.save(conn):
|
||||||
|
echo("Updated priority $1, done $2" % [$todo.priority, $todo.isDone])
|
||||||
|
else:
|
||||||
|
assert(false, "Uh oh, I wasn't expecting that!")
|
||||||
|
|
||||||
|
# Verify our cloned copy is different but can be updated.
|
||||||
|
assert(clonedTodo.text != todo.text, "Should be different")
|
||||||
|
discard clonedTodo.update(conn)
|
||||||
|
assert(clonedTodo.text == todo.text, "Should be equal")
|
||||||
|
|
||||||
|
var params : TPagedParams
|
||||||
|
params.initDefaults
|
||||||
|
conn.showPagedResults(params)
|
||||||
|
conn.deleteTodo(todo.getId)
|
||||||
|
echo("Deleted rows for id 3? ")
|
||||||
|
let res = conn.deleteTodo(todo.getId)
|
||||||
|
echo("Deleted rows for id 3? " & $res)
|
||||||
|
if todo.update(conn):
|
||||||
|
echo("Later priority $1, done $2" % [$todo.priority, $todo.isDone])
|
||||||
|
else:
|
||||||
|
echo("Can't update object $1 from db!" % $todo.getId)
|
||||||
|
|
||||||
|
# Try to list content in a different way.
|
||||||
|
params.pageSize = 5
|
||||||
|
params.priorityAscending = true
|
||||||
|
params.dateAscending = true
|
||||||
|
params.showChecked = true
|
||||||
|
conn.showPagedResults(params)
|
||||||
|
finally:
|
||||||
|
conn.close
|
||||||
|
echo("Database closed")
|
||||||
|
|
||||||
|
|
||||||
|
# Code that will be run only on the commandline.
|
||||||
|
when isMainModule:
|
||||||
|
dumTest()
|
||||||
347
examples/cross_todo/nimrod_commandline/nimtodo.nim
Normal file
347
examples/cross_todo/nimrod_commandline/nimtodo.nim
Normal file
|
|
@ -0,0 +1,347 @@
|
||||||
|
# Implements a command line interface against the backend.
|
||||||
|
|
||||||
|
import backend
|
||||||
|
import db_sqlite
|
||||||
|
import os
|
||||||
|
import parseopt
|
||||||
|
import parseutils
|
||||||
|
import strutils
|
||||||
|
import 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
|
||||||
|
commandAdd # The user wants to add a new todo entry.
|
||||||
|
commandCheck # User wants to check a todo entry.
|
||||||
|
commandUncheck # User wants to uncheck a todo entry.
|
||||||
|
commandDelete # User wants to delete a single todo entry.
|
||||||
|
commandNuke # User wants to purge all database entries.
|
||||||
|
commandGenerate # Add random rows to the database, for testing.
|
||||||
|
commandList # User wants to list contents.
|
||||||
|
|
||||||
|
TParamConfig = object of TObject
|
||||||
|
# Structure containing the parsed options from the commandline.
|
||||||
|
command: TCommand # Store the type of operation
|
||||||
|
addPriority: int # Only valid with commandAdd, stores priority.
|
||||||
|
addText: seq[string] # Only valid with commandAdd, 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 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 commandAdd == result.command:
|
||||||
|
result.addText.add(key)
|
||||||
|
else:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("Argument ($1) detected without add command." % [key], 1)
|
||||||
|
of cmdLongOption, cmdShortOption:
|
||||||
|
case normalize(key)
|
||||||
|
of "help", "h":
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit(0)
|
||||||
|
of "a":
|
||||||
|
if specifiedCommand:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("Only one command can be specified at a time! ($1)" % [val], 2)
|
||||||
|
else:
|
||||||
|
result.command = commandAdd
|
||||||
|
result.addPriority = val.parseInt
|
||||||
|
specifiedCommand = true
|
||||||
|
of "c":
|
||||||
|
if specifiedCommand:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("Only one command can be specified at a time! ($1)" % [val], 2)
|
||||||
|
else:
|
||||||
|
result.command = commandCheck
|
||||||
|
let numChars = string(val).parseBiggestInt(newId)
|
||||||
|
if numChars < 1: raise newException(EInvalidValue, "Empty string?")
|
||||||
|
result.todoId = newId
|
||||||
|
specifiedCommand = true
|
||||||
|
of "u":
|
||||||
|
if specifiedCommand:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("Only one command can be specified at a time! ($1)" % [val], 2)
|
||||||
|
else:
|
||||||
|
result.command = commandUncheck
|
||||||
|
let numChars = val.parseBiggestInt(newId)
|
||||||
|
if numChars < 1: raise newException(EInvalidValue, "Empty string?")
|
||||||
|
result.todoId = newId
|
||||||
|
specifiedCommand = true
|
||||||
|
of "d":
|
||||||
|
if specifiedCommand:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("Only one command can be specified at a time! ($1)" % [val], 2)
|
||||||
|
else:
|
||||||
|
if "all" == val:
|
||||||
|
result.command = commandNuke
|
||||||
|
else:
|
||||||
|
result.command = commandDelete
|
||||||
|
let numChars = val.parseBiggestInt(newId)
|
||||||
|
if numChars < 1:
|
||||||
|
raise newException(EInvalidValue, "Empty string?")
|
||||||
|
result.todoId = newId
|
||||||
|
specifiedCommand = true
|
||||||
|
of "g":
|
||||||
|
if specifiedCommand:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("Only one command can be specified at a time! ($1)" % [val], 2)
|
||||||
|
else:
|
||||||
|
if val.len > 0:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("Unexpected value '$1' for switch l." % [val], 3)
|
||||||
|
result.command = commandGenerate
|
||||||
|
specifiedCommand = true
|
||||||
|
of "l":
|
||||||
|
if specifiedCommand:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("Only one command can be specified at a time! ($1)" % [val], 2)
|
||||||
|
else:
|
||||||
|
if val.len > 0:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("Unexpected value '$1' for switch l." % [val], 3)
|
||||||
|
result.command = commandList
|
||||||
|
specifiedCommand = true
|
||||||
|
of "p":
|
||||||
|
usesListParams = true
|
||||||
|
if "+" == val:
|
||||||
|
result.listParams.priorityAscending = true
|
||||||
|
elif "-" == val:
|
||||||
|
result.listParams.priorityAscending = false
|
||||||
|
else:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("Priority parameter ($1) should be + or |." % [val], 4)
|
||||||
|
of "m":
|
||||||
|
usesListParams = true
|
||||||
|
if "+" == val:
|
||||||
|
result.listParams.dateAscending = true
|
||||||
|
elif "-" == val:
|
||||||
|
result.listParams.dateAscending = false
|
||||||
|
else:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("Date parameter ($1) should be + or |." % [val], 4)
|
||||||
|
of "t":
|
||||||
|
usesListParams = true
|
||||||
|
if val.len > 0:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("Unexpected value '$1' for switch t." % [val], 5)
|
||||||
|
result.listParams.showChecked = true
|
||||||
|
of "z":
|
||||||
|
usesListParams = true
|
||||||
|
if val.len > 0:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("Unexpected value '$1' for switch z." % [val], 5)
|
||||||
|
result.listParams.showUnchecked = false
|
||||||
|
else:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("Unexpected option '$1'." % [key], 6)
|
||||||
|
of cmdEnd:
|
||||||
|
break
|
||||||
|
except EInvalidValue:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("Invalid int value '$1' for parameter '$2'." % [val, key], 7)
|
||||||
|
|
||||||
|
if not specifiedCommand:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("Didn't specify any command.", 8)
|
||||||
|
|
||||||
|
if commandAdd == result.command and result.addText.len < 1:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("Used the add command, but provided no text/description.", 9)
|
||||||
|
|
||||||
|
if usesListParams and commandList != result.command:
|
||||||
|
stdout.write(USAGE)
|
||||||
|
quit("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 commandAdd: addTodo(conn, opt.addPriority, opt.addText)
|
||||||
|
of commandCheck: setTodoCheck(conn, opt.todoId, true)
|
||||||
|
of commandUncheck: setTodoCheck(conn, opt.todoId, false)
|
||||||
|
of commandDelete: deleteOneTodo(conn, opt.todoId)
|
||||||
|
of commandNuke: deleteAllTodos(conn)
|
||||||
|
of commandGenerate: generateDatabaseRows(conn)
|
||||||
|
of commandList: listDatabaseContents(conn, opt.listParams)
|
||||||
|
finally:
|
||||||
|
conn.close
|
||||||
18
examples/cross_todo/nimrod_commandline/readme.txt
Normal file
18
examples/cross_todo/nimrod_commandline/readme.txt
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
This directory contains the nimrod 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 of the interface is fairly easy, just include the path to the
|
||||||
|
backend in your compilation command. A basic build.sh is provided for unix like
|
||||||
|
platforms with the correct parameters.
|
||||||
5
examples/cross_todo/readme.txt
Normal file
5
examples/cross_todo/readme.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
The cross platform todo illustrates how to use Nimrod to create a backend
|
||||||
|
called by different native user interfaces.
|
||||||
|
|
||||||
|
This example builds on the knowledge learned from the cross_calculator example.
|
||||||
|
Check it out first to learn how to set up nimrod on different platforms.
|
||||||
|
|
@ -1,26 +1,52 @@
|
||||||
# Test the SDL interface:
|
# Test the SDL interface:
|
||||||
|
|
||||||
import
|
import
|
||||||
SDL
|
sdl, sdl_image, colors
|
||||||
|
|
||||||
var
|
var
|
||||||
screen, greeting: PSurface
|
screen, greeting: PSurface
|
||||||
r: TRect
|
r: TRect
|
||||||
|
event: TEvent
|
||||||
|
bgColor = colChocolate.int32
|
||||||
|
|
||||||
|
if init(INIT_VIDEO) != 0:
|
||||||
|
quit "SDL failed to initialize!"
|
||||||
|
|
||||||
if Init(INIT_VIDEO) == 0:
|
|
||||||
screen = SetVideoMode(640, 480, 16, SWSURFACE or ANYFORMAT)
|
screen = SetVideoMode(640, 480, 16, SWSURFACE or ANYFORMAT)
|
||||||
if screen == nil:
|
if screen.isNil:
|
||||||
write(stdout, "screen is nil!\n")
|
quit($sdl.getError())
|
||||||
else:
|
|
||||||
greeting = LoadBmp("backgrnd.bmp")
|
|
||||||
if greeting == nil:
|
|
||||||
write(stdout, "greeting is nil!")
|
|
||||||
r.x = 0'i16
|
|
||||||
r.y = 0'i16
|
|
||||||
discard blitSurface(greeting, nil, screen, addr(r))
|
|
||||||
discard flip(screen)
|
|
||||||
Delay(3000)
|
|
||||||
else:
|
|
||||||
write(stdout, "SDL_Init failed!\n")
|
|
||||||
|
|
||||||
|
greeting = IMG_load("tux.png")
|
||||||
|
if greeting.isNil:
|
||||||
|
echo "Failed to load tux.png"
|
||||||
|
else:
|
||||||
|
## convert the image to alpha and free the old one
|
||||||
|
var s = greeting.displayFormatAlpha()
|
||||||
|
swap(greeting, s)
|
||||||
|
s.freeSurface()
|
||||||
|
|
||||||
|
r.x = 0
|
||||||
|
r.y = 0
|
||||||
|
|
||||||
|
block game_loop:
|
||||||
|
while true:
|
||||||
|
|
||||||
|
while pollEvent(addr event) > 0:
|
||||||
|
case event.kind
|
||||||
|
of QUITEV:
|
||||||
|
break game_loop
|
||||||
|
of KEYDOWN:
|
||||||
|
if EvKeyboard(addr event).keysym.sym == K_ESCAPE:
|
||||||
|
break game_loop
|
||||||
|
else:
|
||||||
|
discard
|
||||||
|
|
||||||
|
discard fillRect(screen, nil, bgColor)
|
||||||
|
discard blitSurface(greeting, nil, screen, addr r)
|
||||||
|
discard flip(screen)
|
||||||
|
|
||||||
|
greeting.freeSurface()
|
||||||
|
screen.freeSurface()
|
||||||
sdl.Quit()
|
sdl.Quit()
|
||||||
|
|
||||||
|
## fowl wuz here 10/2012
|
||||||
Loading…
Add table
Add a link
Reference in a new issue