Skip to content

Latest commit

 

History

History
113 lines (85 loc) · 4.86 KB

File metadata and controls

113 lines (85 loc) · 4.86 KB

nativeapi (Python)

Native desktop APIs — windows, tray icons, menus, displays, global shortcuts, dialogs, preferences and secure storage — for Python 3.10+, on macOS, Windows and Linux.

Pure ctypes over the libnativeapi C ABI: no compiled extension module, so one wheel per platform serves every Python 3 version. The typed Python layer is generated from the C++ headers of core by ./codegen; only nativeapi/_library.py, nativeapi/_runtime.py and the event loop shim in src/ are hand-written.

Status: prototype. The whole API surface is generated, but it has not been published and the API may still change.

import asyncio

from nativeapi import Application, Size, Window, WindowClosedEvent, WindowManager


async def main() -> int:
    window = Window()
    window.set_title("Hello")
    window.set_size(Size(800, 600), False)
    window.center()

    def on_event(event):
        if isinstance(event, WindowClosedEvent):
            Application.quit()

    WindowManager.add_listener(on_event)
    return await Application.run_async(window)  # asyncio keeps running


asyncio.run(main())

Model

  • Objects (Window, Menu, TrayIcon, …) wrap a handle. The default constructor is Window(); other constructors are class methods (Preferences.with_scope("app")). A failed creation raises NativeApiError. The reference is released by dispose() / with, or when the wrapper is garbage collected. Calls on a released handle fail safely.
  • Getters without arguments are properties (window.title, window.bounds, window.is_visible). One with a matching one-argument setter is writable too (window.title = "Hello"); the set_title(...) method stays, and setters that need more than the value (window.set_size(size, animate)) are methods only.
  • Values (Point, Size, Rectangle, Color, …) are dataclasses.
  • Enums are IntEnums (TitleBarStyle.HIDDEN); bit sets are IntFlags (ModifierKey.SHIFT | ModifierKey.ALT).
  • Events are frozen dataclasses, one subclass per kind, so they work with match: case WindowMovedEvent(window_id=id, new_position=p).
  • Singletons (Application, WindowManager, DisplayManager, …) are classes with static methods.

The event loop

  • Application.run(window=None) blocks in the platform loop and returns the exit code. Listeners run on the main thread in between. Ctrl+C terminates the process.
  • await Application.run_async(window=None) pumps the platform loop from the running asyncio loop instead, so tasks, timers and I/O keep running while windows are up. Application.quit(code) requests confirmation; the loop resolves with the exit code once all decisions accept. A listener may cancel the request or call event.request.defer() and keep the owned decision while awaiting an asyncio task. Resolve it with accept() / cancel(), then dispose() it. Cancelling the run_async() task invalidates its pending confirmation, so a late decision cannot stop a later run.

Listeners are synchronous. For async confirmation, register a normal function that defers the request and schedules an asyncio task; registering an async def callback is rejected. Listener exceptions veto cancellable requests and reach the asyncio loop's exception handler.

Both must be called on the Python main thread and platform UI thread.

Building

cd examples/python_window_example && uv run main.py   # builds the wheel, runs the example

For work on the binding itself, build the library in place and run from the source tree; nativeapi/_library.py finds it in build/:

cmake -S bindings/python -B bindings/python/build -DNATIVEAPI_PY_BUILD_TESTS=ON
cmake --build bindings/python/build
cd bindings/python && PYTHONPATH=. uvx --with pytest pytest

The test option adds fixture exports to the local library for actual ctypes quit/asyncio regressions without opening windows or sending input. Published wheels build with that option off.

NATIVEAPI_LIBRARY=/path/to/libnativeapi.dylib overrides the lookup.

Contributing

Development happens in nativeapi, which holds every binding and the code generator and checks out the core library as a submodule:

git clone --recursive https://github.com/libnativeapi/nativeapi.git

Files marked AUTO-GENERATED. DO NOT EDIT. are generated from the C++ headers in nativeapi. To change the API, send a pull request there; maintainers regenerate the bindings.