First App Example In Nim

First impression of Nim was, wow! I’m a big Python guy so the syntax made me feel at home. I installed initially with apt and found it to be a tad old. Still, things worked great until I installed a few packages. None would import no matter what I did. I got close but they would not be found or load. I purged and installed with choosenim. Same problem! I dug into the configs and code, still couldn’t get imports to work with the exception of UI.

At this point I was aggravated. UI works so I assume it’s the packages themselves and not an issue with Nim in general. I removed everything and manually installed Nim nightly. Same shit. Now this isn’t a big deal if you want to spend time trying to figure out what’s up, but I am new to this language out of a sea of really great languages so should I waste my time just to be able to use Glade to build the UI?

Let’s get into this. Let’s install Nim manually, install UI, and rough out a quick application. We will just skip GIntro and UIBuilder, which is bummer. I almost went to WX, because even UI is lacking, but fuck it, I think I will stick with Rust and Vala.

Install is simple. Download Nim from here. This example is for Linux. You can do apt or choosenim also, both worked for me, but you will still need to add to your .profile. If you are doing manual just unzip and paste in home. Open ~/.profile and add the path, in my case:

export PATH="$HOME/nim-0.20.99/bin:$PATH"
Logoff, or do source /.profile. Now install UI.
$ nimble install ui

The below code checks if it’s the app’s first run and creates an app folder and SQLite database with a welcome if not. We will query the new database entry and add it to a GTK edit box. To compile just save the code as shadow.nim, cd to the file’s folder, and type:

nim compile --app:gui --run shadow.nim
import ui # nimble install ui
import db_sqlite
import os
import strformat
import times

proc first_run(user_path: string): bool =
  result = true
  if existsDir(user_path): result = false

proc create_app_path(app_path: string): void =
  createDir(app_path)
  
proc get_app_path(home: string): string =  
  os.joinPath(home,".shadowbook")

proc get_db_path(home: string): string =  
  os.joinPath(home, "entries.db")

proc create_table(db_path: string): void = 
  
  var timestamp = now().format("yyyy-MM-dd HH:mm:ss")
  var title = "Welcome to Shadowbook!"
  var rtf_body = "Shadowbook is an electronic Book of Shadows."
  #r"{\rtf1\ansi\deff0 {\fonttbl {\f0 Theban;}}\f0\fs60 Shadowbook is an electronic Book of Shadows.}"

  var createTable = fmt("""CREATE TABLE entries (ID INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    created_dt        TEXT    NOT NULL,
    title             TEXT    NOT NULL,
    body              TEXT    NOT NULL,
    tags              TEXT    NULL,
    moon_phase        TEXT    NULL)""")
  var createUniqueIndex = "CREATE UNIQUE INDEX entry_idx ON entries(created_dt, title)"
  var insertDefaults = fmt("INSERT INTO entries (ID,created_dt,title,body) VALUES (null,'{timestamp}','{title}','{rtf_body}')")

  let db = open(db_path, "", "", "")
  #db.exec(sql"DROP TABLE IF EXISTS entries")  
  db.exec(sql(createTable))
  db.exec(sql(createUniqueIndex))
  db.exec(sql(insertDefaults))
  db.close()

proc get_entry(db_path: string): string = 
  result = ""
  var select = fmt("SELECT * FROM entries WHERE ID=1")

  let db = open(db_path, "", "", "") 

  for row in db.instantRows(sql(select)):
    result = row[2]
    #echo x
  db.close()

proc initApp(): string =
  result = ""
  try:
    var user_path = getEnv("HOME")
    var app_path = get_app_path(user_path)
    var db_path = get_db_path(app_path)
    result = db_path

    if first_run(app_path):
      create_app_path(app_path)
      createAppPath(appPath)
      create_table(db_path)
  except:
    let
      e = getCurrentException()
      msg = getCurrentExceptionMsg()
    echo "Got exception ", repr(e), " with message ", msg

proc main*() =

  var db_path = initApp()

  var mainwin: Window

  var menu = newMenu("File")
  menu.addItem("Open", proc() =
    let filename = ui.openFile(mainwin)
    if filename.len == 0:
      msgBoxError(mainwin, "No file selected", "Don't be alarmed!")
    else:
      msgBox(mainwin, "File selected", filename)
  )

  menu.addItem("Save", proc() =
    let filename = ui.saveFile(mainwin)
    if filename.len == 0:
      msgBoxError(mainwin, "No file selected", "Don't be alarmed!")
    else:
      msgBox(mainwin, "File selected (don't worry, it's still there)", filename)
  )
  menu.addQuitItem(proc(): bool {.closure.} = return true)

  menu = newMenu("Edit")
  menu.addCheckItem("Checkable Item", proc() = discard)
  menu.addSeparator()
  let item = menu.addItem("Disabled Item", proc() = discard)
  item.disable()

  menu.addPreferencesItem(proc() = discard)
  menu = newMenu("Help")
  menu.addItem("Help", proc () = discard)
  menu.addAboutItem(proc () = discard)

  mainwin = newWindow("Shadow Diary", 640, 480, true)
  mainwin.margined = true
  mainwin.onClosing = (proc (): bool = return true)

  let box = newVerticalBox(true)
  mainwin.setChild(box)

  var textEdit = newMultilineEntry()
  box.add(textEdit, true)
  textEdit.text = get_entry(db_path)

  show(mainwin)
  mainLoop()

init()
main() 
import ui # nimble install ui
import db_sqlite
import os
import strformat
import times

proc first_run(user_path: string): bool =
  result = true
  if existsDir(user_path): result = false

proc create_app_path(app_path: string): void =
  createDir(app_path)
  
proc get_app_path(home: string): string =  
  os.joinPath(home,".shadowbook")

proc get_db_path(home: string): string =  
  os.joinPath(home, "entries.db")

proc create_table(db_path: string): void = 
  
  var timestamp = now().format("yyyy-MM-dd HH:mm:ss")
  var title = "Welcome to Shadowbook!"
  var rtf_body = "Shadowbook is an electronic Book of Shadows."
  #r"{\rtf1\ansi\deff0 {\fonttbl {\f0 Theban;}}\f0\fs60 Shadowbook is an electronic Book of Shadows.}"

  var createTable = fmt("""CREATE TABLE entries (ID INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    created_dt        TEXT    NOT NULL,
    title             TEXT    NOT NULL,
    body              TEXT    NOT NULL,
    tags              TEXT    NULL,
    moon_phase        TEXT    NULL)""")
  var createUniqueIndex = "CREATE UNIQUE INDEX entry_idx ON entries(created_dt, title)"
  var insertDefaults = fmt("INSERT INTO entries (ID,created_dt,title,body) VALUES (null,'{timestamp}','{title}','{rtf_body}')")

  let db = open(db_path, "", "", "")
  #db.exec(sql"DROP TABLE IF EXISTS entries")  
  db.exec(sql(createTable))
  db.exec(sql(createUniqueIndex))
  db.exec(sql(insertDefaults))
  db.close()

proc get_entry(db_path: string): string = 
  result = ""
  var select = fmt("SELECT * FROM entries WHERE ID=1")

  let db = open(db_path, "", "", "") 

  for row in db.instantRows(sql(select)):
    result = row[2]
    #echo x
  db.close()

proc initApp(): string =
  result = ""
  try:
    var user_path = getEnv("HOME")
    var app_path = get_app_path(user_path)
    var db_path = get_db_path(app_path)
    result = db_path

    if first_run(app_path):
      create_app_path(app_path)
      createAppPath(appPath)
      create_table(db_path)
  except:
    let
      e = getCurrentException()
      msg = getCurrentExceptionMsg()
    echo "Got exception ", repr(e), " with message ", msg

proc main*() =

  var db_path = initApp()

  var mainwin: Window

  var menu = newMenu("File")
  menu.addItem("Open", proc() =
    let filename = ui.openFile(mainwin)
    if filename.len == 0:
      msgBoxError(mainwin, "No file selected", "Don't be alarmed!")
    else:
      msgBox(mainwin, "File selected", filename)
  )

  menu.addItem("Save", proc() =
    let filename = ui.saveFile(mainwin)
    if filename.len == 0:
      msgBoxError(mainwin, "No file selected", "Don't be alarmed!")
    else:
      msgBox(mainwin, "File selected (don't worry, it's still there)", filename)
  )
  menu.addQuitItem(proc(): bool {.closure.} = return true)

  menu = newMenu("Edit")
  menu.addCheckItem("Checkable Item", proc() = discard)
  menu.addSeparator()
  let item = menu.addItem("Disabled Item", proc() = discard)
  item.disable()

  menu.addPreferencesItem(proc() = discard)
  menu = newMenu("Help")
  menu.addItem("Help", proc () = discard)
  menu.addAboutItem(proc () = discard)

  mainwin = newWindow("Shadow Diary", 640, 480, true)
  mainwin.margined = true
  mainwin.onClosing = (proc (): bool = return true)

  let box = newVerticalBox(true)
  mainwin.setChild(box)

  var textEdit = newMultilineEntry()
  box.add(textEdit, true)
  textEdit.text = get_entry(db_path)

  show(mainwin)
  mainLoop()

init()
main()