Skip to content

Core API ​

The application object, entry points, and window lifecycle. Import from neony.application.

NeonApplication ​

The application object — owns the windows, the theme, and shared state. Construct with a Config, build a Page, then run().

python
from neony.application import Config, NeonApplication, Page, Theme, WebViewConfig, WindowConfig

app = NeonApplication(
    Config(
        window=WindowConfig(title="Demo", width=480, height=360),
        webview=WebViewConfig(devtools=True),
    )
)
app.state.count = 0  # shared mutable state
app.theme = Theme.get("nightglow-light")  # pick the initial preset before run()


def main() -> None:
    app.run(page)

Typed state: state defaults to a bare SimpleNamespace. Pass any object — a dataclass, pydantic model, or plain class — via the state= argument to get typed attribute access and IDE completion:

python
from dataclasses import dataclass


@dataclass
class AppState:
    count: int = 0
    user_name: str = ""


app = NeonApplication(Config(...), state=AppState())
app.state.count += 1  # typed as int
app.state.user_name = "Ada"

All windows share the same state object, so this is the imperative counterpart to SharedSignal for cross-window data.

Attributes: config, state, theme, ready_handler, close_handler

Window methods (all async):

set_title(title), set_size(w, h), minimize(), toggle_maximize(), is_maximized(), set_fullscreen(f), start_dragging(), close(), apply_blur(color?), apply_acrylic(color?), apply_mica(), clear_effect(effect), eval_js(script), set_icon(icon).

transparent=True already applies the platform material automatically (Wayland blur on Linux where supported, Acrylic on Windows, Blur on macOS). The apply_* methods are manual overrides and platform-limited: apply_blur is macOS/Windows; acrylic / mica are Windows 11.

App methods: exit(code=0) — graceful app shutdown (sync). With close_to_tray=True window closes hide the app instead of quitting, so exit() is the way out — e.g. a tray "Quit" menu item.

Theme / rendering:

set_theme(theme), sync_theme(), set_background(url), render()

File dialogs (all async — system-native): open_file(...) -> str | None, open_files(...) -> list[str], save_file(...) -> str | None, select_folder(...) -> str | None.

Cancelling returns None (or [] for the multi-select); a dialog that can't be shown also returns None — never an exception.

python
path = await app.open_file(
    title="Open image", default_dir="~/Pictures", filetypes=[("PNG images", "*.png"), ("All files", "*.*")]
)
if path is None:
    return  # cancelled
paths = await app.open_files(...)  # [] on cancel
dest = await app.save_file(default_name="out.txt")  # str | None
folder = await app.select_folder()  # str | None

The dialogs are the platform's own — zenity on Linux (most desktops ship it), osascript on macOS, PowerShell on Windows, with a tkinter fallback. They open asynchronously, so the app's event loop keeps running while they're up, and nothing is drawn by Neony itself: the look, navigation and filters are exactly what the OS provides.

filetypes maps onto the native filter UI ([("PNG images", "*.png"), ("All files", "*.*")]); default_dir / default_name preselect the starting location.

launch() ​

One-liner entry point — builds a Config from keyword arguments.

python
from neony.application import Page, launch

launch(page, title="Demo", width=480, height=360, devtools=True)

Accepts all WindowConfig / WebViewConfig fields plus mount_selector, auto_render, and state (a custom state object — see NeonApplication) — and protocols (see Custom protocols).

Custom protocols ​

Serve Python-generated content to the page through neony://<key>/… URLs. Handlers are plain functions or methods declared with the @protocol decorator; sync handlers run on the app thread pool, async handlers on the app event loop.

python
from neony.application import Page, launch, local_files, local_url, protocol
from neony.application.protocols import Request, Response


@protocol("qr")
def qr_codes(request: Request) -> Response:
    key = request.path.strip("/")  # neony://qr/<key> → "<key>"
    return Response(body=make_qr_png(key), headers={"Content-Type": "image/png"})


class Avatars:
    def __init__(self, db) -> None:
        self.db = db

    @protocol("avatar")
    async def handle(self, request: Request) -> Response:  # state via self
        data = await self.db.fetch_avatar(request.path.strip("/"))
        return Response(status=404) if data is None else Response(body=data, headers={"Content-Type": "image/jpeg"})


launch(page, title="Demo", protocols=[qr_codes, Avatars(db), local_files])

Rules

  • Declare everything before launch() / run() — webview schemes are registered once at window creation and cannot be added afterwards.
  • Keys must match ^[a-z][a-z0-9-]*$ (the key is the URL authority, which browsers lowercase). Duplicate keys raise ValueError.
  • The handler receives a frozen pydantic Request: key, path (percent-decoded payload), method, url, query, headers, plus a case-insensitive request.header(name) helper. Return a frozen Response(status, headers, body) (Response.text() / Response.json() for convenience).
  • URLs are built by local_url(path) → neony://local/… and protocol_url(key, value) → neony://<key>/….
  • Requests to unregistered keys answer 404; handler exceptions log and answer 500.

Built-in local_files serves any absolute filesystem path over neony://local/… — the custom-scheme twin of file_url(). WebViews block file:// subresources when the page is loaded from an HTML string, so this is the way to show local media:

python
from neony.application import Image, launch, local_files, local_url

launch(page, protocols=[local_files])
Image(local_url("~/Music/song.mp3"))

It supports HTTP Range requests (206 Partial Content, 416 for an unsatisfiable range), answers HEAD, guesses MIME types, and sends ETag / Last-Modified / Accept-Ranges. There is no path allow-list: a Neony page is trusted application content. See demo_protocols.py.

Media playback — the managed Video / Audio components load neony://… sources automatically, so local media plays and seeking works where file:// subresources are blocked. Raw <audio> / <video> DOM elements are not handled this way — use the components (see Video / Audio). The whole file is held in memory while playing — ideal for voice clips and sound effects; mind the size for long videos.

Config, WindowConfig, WebViewConfig ​

Pydantic config models. WindowConfig covers geometry and appearance (title, width, height, decorations, transparent, always_on_top, resizable, icon, …). WebViewConfig covers runtime options (devtools, incognito, user_agent, javascript, …).

WindowConfig.icon — file path (PNG, ICO, …) or raw RGBA data (bytes, width, height), shown in the OS window chrome of decorated windows. Frameless windows have no OS chrome — see the TitleBar icon parameter for inline icons, and NeonApplication.set_icon() to swap at runtime.

WebViewConfig.default_context_menus — off by default: the app draws its own menus (the Menu component, contextmenu events) and the webview's native right-click menu would cover them. Set True for the platform default menu.

Page ​

Top-level flex-column container. Two layers: a full-viewport backdrop and a width-constrained, centered content column.

python
Page(gap="16px", padding="24px", max_width="720px")
Page(fill=True, radius="12px")  # chrome layouts

Options: direction, gap, padding (default "24px"), align, justify, width, max_width (default "600px"), glass, fill, radius

fill=True stretches to the full window height. radius rounds the window frame (for transparent frameless windows).

Methods: add(child) (chainable), on_close(fn) (chainable — see Lifecycle), on_focus(fn) / on_blur(fn) (chainable), on_keydown(fn) / on_keyup(fn) (chainable), on_shortcut(combo, fn) (chainable), build() → DOMElement

Lifecycle ​

Startup and teardown are declared as plain attributes — the framework owns the wiring to the native window events.

python
async def on_ready() -> None:
    print("windows are up")


async def on_shutdown() -> None:
    save_state(app.state)  # runs after all windows close


app.ready_handler = on_ready
app.close_handler = on_shutdown

close_handler runs exactly once, after the last window closes and before the event loop stops — the last chance for async cleanup.

Per-window close — Page.on_close(fn) (sync or async, chainable, multiple handlers stack). Fires when that page's window is closing, before it actually closes; exceptions are logged and never block the close. For a confirm-before-close dialog, take over the titlebar close button instead — see TitleBar.override_close.

python
page = Page()
page.on_close(lambda: print("window closing"))

Focus tracking — Page.on_focus(fn) / Page.on_blur(fn) (sync or async, chainable, multiple handlers stack) fire when the page's window gains / loses keyboard focus — useful for pausing timers, updating a status bar, or knowing which window is active in a multi-window app.

python
page = Page()
page.on_focus(lambda: print("active"))
page.on_blur(lambda: print("inactive"))

Keyboard & shortcuts — Page.on_keydown(fn) / Page.on_keyup(fn) receive every key event, including keys typed while a child input has focus. Page.on_shortcut(combo, fn) registers an in-app shortcut; the combo is a string like "Ctrl+S" or a per-platform dict ({"darwin": "Meta+S", "default": "Ctrl+S"}). Shortcuts fire regardless of which element has focus.

python
page = Page()
page.on_keydown(lambda e: print(e.key, e.code, e.ctrl_key))
page.on_shortcut("Ctrl+S", save)
page.on_shortcut({"darwin": "Meta+K", "default": "Ctrl+K"}, open_search)

Multi-window ​

run() accepts several pages — each opens its own window. All windows share one event loop and the app's state namespace; an event handler only re-renders the window it came from.

python
app = NeonApplication(Config(...))
app.run(page_one, page_two)


async def on_ready() -> None:
    await app.set_title("Counter", window_index=0)
    await app.set_title("Display", window_index=1)


app.ready_handler = on_ready

Every window-control method takes window_index (default 0).

launch([page_one, page_two], ...) accepts a list too.

A link or redirect inside the page would otherwise navigate the webview away from your UI. Neony installs safe defaults on every window — navigation blocked, new-window requests denied, downloads cancelled — so nothing can escape without your say-so. Override them per-page.

Decision policies — a single handler, the last one registered wins (a decision can't be merged):

python
# Allow only your own site; everything else is blocked.
page.on_navigation(lambda url: url.startswith("https://myapp.example"))

# target="_blank" links and window.open(): "allow" or "deny".
page.on_new_window(lambda url: "deny")

# Return True to allow, False to cancel, or a path to redirect the
# download to a custom location.
page.on_download_started(lambda url, path: "/downloads/")

Notifications — multiple handlers stack, all run:

python
# url, final path (or None if cancelled), success flag.
page.on_download_completed(lambda url, path, ok: print(f"downloaded {path}"))

Tray & TrayItem — system tray (native menu) ​

A tray icon with a native context menu. Assign app.tray before run(); the icon materializes once the app is up.

python
from neony.application import Tray, TrayItem

app.tray = Tray(
    icon="tray.png",  # file path or raw RGBA (bytes, width, height)
    tooltip="My App",
    items=[
        TrayItem("Show Window", id="show", on_activate=show_handler),
        TrayItem.separator(),
        TrayItem("Quit", id="quit", accelerator="CmdOrCtrl+Q", on_activate=quit_handler),
    ],
    menu_on_left_click=False,  # free the left button for on_left_click
    on_left_click=toggle_handler,  # sync or async
    close_to_tray=True,  # close hides the app instead of quitting
)
  • TrayItem — text, optional id (carried by activation callbacks), accelerator (shortcut syntax; Windows may not fire it from the keyboard), on_activate (sync or async, run asynchronously), checked=True for a check item; TrayItem.separator() for a divider.

  • close_to_tray=True — every window's close request is prevented and the app hides (restore from the menu / tray click; on macOS a Dock click via ReopenEvent). Page.on_close handlers still run.

  • on_left_click — fires on a released left click when menu_on_left_click=False (typical use: toggle the window).

  • Platform notes: Linux needs libayatana-appindicator; the tooltip is unsupported there and the menu cannot be replaced after creation.

    See demo_tray.py.