Your First App

A counter app, built up one piece at a time. The complete file is at the end.

The entry point

import nib


def main(app: nib.App):
    ...


nib.run(main)

nib.run(main) creates the App, calls your function with it, then starts the runtime and blocks until the app quits. Everything you configure happens inside main.

Configure the app

def main(app: nib.App):
    app.title = "Counter"
    app.icon = nib.SFSymbol("number.circle.fill")
    app.width = 280
    app.height = 200

title is shown next to the menu bar icon, icon is any SF Symbols name, width and height size the popover.

Add views

    counter_display = nib.Text("0", font=nib.Font.TITLE)

    app.build(
        nib.VStack(
            controls=[
                counter_display,
                nib.Button("Add +1"),
            ],
            spacing=12,
            padding=24,
        )
    )

app.build() sets the root view. Containers take their children in controls=; styling such as font, spacing and padding is passed as constructor arguments, never as chained method calls.

React to the button

    count = 0

    def increment():
        nonlocal count
        count += 1
        counter_display.content = str(count)

    nib.Button("Add +1", action=increment)

Assigning counter_display.content is the whole reactivity model: Nib diffs the view tree and sends the runtime only what changed. Every public property of a view works this way, including styling (counter_display.foreground_color = nib.Color.RED).

Add a context menu

    def reset():
        nonlocal count
        count = 0
        counter_display.content = "0"

    app.menu = [
        nib.MenuItem("Reset", action=reset, icon="arrow.counterclockwise"),
        nib.MenuDivider(),
        nib.MenuItem("Quit", action=app.quit, shortcut="cmd+q"),
    ]

The menu opens on a right click of the menu bar icon.

Run it

nib run main.py

Click the icon, press the button, right-click the icon. Edit the file and save: the app reloads with the runtime still running.

Complete code

import nib


def main(app: nib.App):
    app.title = "Counter"
    app.icon = nib.SFSymbol("number.circle.fill")
    app.width = 280
    app.height = 200

    count = 0
    counter_display = nib.Text("0", font=nib.Font.TITLE)

    def increment():
        nonlocal count
        count += 1
        counter_display.content = str(count)

    def reset():
        nonlocal count
        count = 0
        counter_display.content = "0"

    app.menu = [
        nib.MenuItem("Reset", action=reset, icon="arrow.counterclockwise"),
        nib.MenuDivider(),
        nib.MenuItem("Quit", action=app.quit, shortcut="cmd+q"),
    ]

    app.build(
        nib.VStack(
            controls=[
                counter_display,
                nib.Button("Add +1", action=increment),
            ],
            spacing=12,
            padding=24,
        )
    )


nib.run(main)

Class-based variant

You can also subclass nib.App and override body(). It is re-evaluated on every render, so state lives in nib.State attributes rather than in view instances:

import nib


class Counter(nib.App):
    count = nib.State(0)

    def body(self) -> nib.View:
        return nib.VStack(
            controls=[
                nib.Text(str(self.count), font=nib.Font.TITLE),
                nib.Button("Add +1", action=self.increment),
            ],
            spacing=12,
            padding=24,
        )

    def increment(self):
        self.count += 1


Counter(title="Counter", icon="number.circle.fill").run()

Both styles work with nib run and nib build. The function style is used throughout these docs.

Next