Build your first desktop app

In about twenty minutes you'll build a real to-do list app: a window with a text box, a button and a table, that remembers your tasks. It runs on Windows, macOS and Linux with the TB app.

1.Make a window

Save this as todo.tb in your TB folder and run it with tb todo.tb (on Windows you can also just double-click it). A window opens and waits until you close it.

Desktop code: save it as a .tb file and run it with the TB app.

import gui

let win = gui.window("To-do", {"width": 480, "height": 520})
win.label("Things to do", {"style": "title"})
win.run()

gui.window makes the window. Everything you add to it with win.label, win.button and friends appears top to bottom. win.run() shows it; it always comes last.

2.Add a text box and a button

A row puts things side by side. The button gets a function to call when it's clicked.

Desktop code: save it as a .tb file and run it with the TB app.

import gui

let win = gui.window("To-do", {"width": 480, "height": 520})
win.label("Things to do", {"style": "title"})

let row = win.row()
let box = row.input({"placeholder": "What needs doing?"})
row.button("Add", fn() {
  gui.alert(f"You typed: {box.value()}")
}, {"primary": true})

win.run()

box.value() reads what's in the text box. gui.alert pops up a little message.

3.Show the tasks in a table

Keep the tasks in a list of maps, and draw them in a table. Every time the list changes, show() redraws it.

Desktop code: save it as a .tb file and run it with the TB app.

let tasks = []
let list = win.table(["Done", "Task"])

fn show() {
  list.set_rows(tasks.map(fn(t) {
    if t.done { return ["โœ“", t.text] }
    return ["", t.text]
  }))
}

fn add(text) {
  if text.trim() == "" { return }
  tasks.push({"text": text.trim(), "done": false})
  box.clear()
  show()
}

Then point the button (and the Enter key) at add: row.button("Add", fn() { add(box.value()) }) and give the input {"on_enter": fn(text) { add(text) }}.

4.Tick tasks off and remember them

Double-clicking a row calls the table's on_open. Save the list with the json library after every change, in a file next to your program (fs.here finds that folder), and read it back when the app starts.

Here is the whole app:

Desktop code: save it as a .tb file and run it with the TB app.

import gui
import json
import fs

let FILE = fs.here("tasks.json")
let tasks = []
if fs.exists(FILE) {
  tasks = json.read(FILE)
}

let win = gui.window("To-do", {"width": 480, "height": 520})
win.label("Things to do", {"style": "title"})

let row = win.row()
let box = row.input({"placeholder": "What needs doing?", "on_enter": fn(text) { add(text) }})
row.button("Add", fn() { add(box.value()) }, {"primary": true})

let list = win.table(["Done", "Task"], {"on_open": fn(r, i) { toggle(i) }})
let status = win.status("")

fn show() {
  list.set_rows(tasks.map(fn(t) {
    if t.done { return ["โœ“", t.text] }
    return ["", t.text]
  }))
  let left = len(tasks.filter(fn(t) { return not t.done }))
  status.set(f"{left} to do")
}

fn add(text) {
  if text.trim() == "" { return }
  tasks.push({"text": text.trim(), "done": false})
  box.clear()
  json.write(FILE, tasks)
  show()
}

fn toggle(i) {
  tasks[i].done = not tasks[i].done
  json.write(FILE, tasks)
  show()
}

show()
win.run()

5.Run it

Run tb todo.tb. Add a few tasks, double-click one to tick it, close the window and open it again: your tasks are still there.

The finished To-do app with a few tasks
The To-do app from apps/todo, which adds Delete and Clear done buttons.

Where next

The full To-do app adds deleting and clearing. The gui reference lists every widget: menus, dialogs, timers, dark mode and keyboard shortcuts. For something bigger, read the Notes and Log triage apps.