Async

libui-python has first-class asyncio support. Your application runs as an async function, and event callbacks can be either sync or async.

Background tasks

Use asyncio.create_task() to run background work while the UI stays responsive:

"""Async background tasks — concurrent work with UI updates."""

import asyncio
import libui
from libui.declarative import (
    App,
    Window,
    VBox,
    Label,
    Button,
    ProgressBar,
    State,
)


async def main():
    app = App()
    status = State("Ready.")
    progress = State(0)

    async def do_work():
        status.set("Working...")
        for i in range(1, 101):
            await asyncio.sleep(0.03)
            progress.set(i)
        status.set("Done!")
        await asyncio.sleep(1)
        progress.set(0)
        status.set("Ready.")

    # Background ticker
    async def ticker():
        n = 0
        while True:
            await asyncio.sleep(2)
            n += 1
            if progress.value == 0:
                status.set(f"Idle tick #{n}")

    asyncio.create_task(ticker())

    app.build(
        window=Window(
            "Async Background",
            400,
            200,
            child=VBox(
                Label(text=status),
                ProgressBar(value=progress),
                Button("Start Work", on_clicked=do_work),
            ),
        )
    )

    app.show()
    await app.wait()


libui.run(main())
Background tasks

The proxy layer handles thread safety — you can set widget properties from async code without worrying about which thread you’re on.

Async callbacks

Event handlers can be async def functions. The framework automatically schedules them on the asyncio loop:

"""Async callbacks — event handlers that are coroutines."""

import asyncio
import libui
from libui.declarative import (
    App,
    Window,
    VBox,
    Label,
    Button,
    ProgressBar,
    State,
)


async def main():
    app = App()
    status = State("Click a button.")
    progress = State(0)

    async def fetch_data():
        """Simulate an async network request."""
        status.set("Fetching data...")
        for i in range(1, 101):
            await asyncio.sleep(0.02)
            progress.set(i)
        status.set("Data loaded!")
        progress.set(0)

    async def save_data():
        """Simulate saving with a dialog."""
        path = await app.save_file_async()
        if not path:
            status.set("Save cancelled.")
            return
        status.set(f"Saving to {path}...")
        await asyncio.sleep(1)
        status.set(f"Saved to {path}")

    def sync_action():
        """A sync callback runs on the main thread."""
        status.set("Sync action executed!")

    app.build(
        window=Window(
            "Async Callbacks",
            450,
            250,
            child=VBox(
                Label(text=status),
                ProgressBar(value=progress),
                Button("Fetch Data (async)", on_clicked=fetch_data),
                Button("Save Data (async)", on_clicked=save_data),
                Button("Sync Action", on_clicked=sync_action),
            ),
        )
    )

    app.show()
    await app.wait()


libui.run(main())
Async callbacks

Key points:

  • Sync callbacks run immediately on the main thread

  • Async callbacks are scheduled on the asyncio event loop (background thread)

  • Both types can safely update widget properties through the proxy layer

  • Use await app.msg_box_async(...) instead of app.msg_box(...) from async callbacks

How it works

libui.run(coro) creates a two-thread architecture:

Main thread                    Background thread
  |                              |
  |-- init libui                 |
  |-- pump main_step()    <-->   |-- asyncio.run(coro)
  |     ^                        |     |
  |     |-- queue_main(fn) ------|-----|
  |                              |
  |-- cleanup                    |-- done
  • Main thread: owns the native UI event loop

  • Background thread: runs your async def main() coroutine

  • queue_main(fn): enqueues a function to run on the main thread

  • Proxy layer: automatically dispatches property access through queue_main

Cross-thread helpers

For advanced use cases with libui.core directly:

from libui.loop import invoke_on_main, invoke_on_main_async

# Blocking (from sync code on any thread):
result = invoke_on_main(core_function, arg1, arg2)

# Non-blocking (from async code):
result = await invoke_on_main_async(core_function, arg1, arg2)