
Unified system APIs across multiple platforms
Android β’ iOS β’ Linux β’ macOS β’ Windows
nimble install nkit
nkit is a Nim toolkit for building desktop and mobile applications that look and behave like real native applications. Instead of drawing its own widgets, it talks to the platform's own UI framework through small C and Objective-C shims, so every button, menu, alert or tray icon you create is the genuine OS control your users already know.
The same philosophy applies below the GUI layer: clipboard, dialogs, notifications, global shortcuts, secure storage, display queries and friends are thin, unified wrappers over the platform services, exposed through one idiomatic Nim surface.
- Native widgets on macOS, iOS, and Linux using platform UI frameworks
- Unified clipboard, notifications, dialogs, and system services
- Global keyboard/mouse monitors and hotkey support
- Secure storage with Keychain and libsecret integration
- Dark and light theme with customizable accent colors
- Flexible layout system with flex, alignment, and spacing controls
- Three API tiers: low-level shims, compositional sugar, or macro DSL
- Built-in WebView with full configuration support
- Menu bar and system tray integration
- Display management and multi-monitor handling
- Cross-platform CLI for scaffolding, building, running and packaging apps
- Disk image packaging for macOS, including drag-to-install volumes
$ nkit
Development
init <?name> β Scaffold nkit.yaml, a nimble file and a starter app
build <platform> β Produces an .app bundle under build/
run <platform> <?device> β Targets the booted simulator, or a device by name
Diagnostics
devices β List iOS Simulator devices known to Xcode
Simulator runtimes
runtimes.list Show the runtimes installed on this machine
runtimes.install <name> β Download e.g. "iOS 18" or "watchOS 10"
logs <platform> β Stream simulator logs for the app process
Distribution
bundle β Package a macOS .app into a .dmg (macOS only)
clean Remove the build directory
nkit build macos produces build/macos/<name>.app. nkit bundle turns an .app bundle into
a compressed disk image, the shape a .dmg is normally distributed in.
bundle reads no project configuration at all: there is nothing to initialise and no
nkit.yaml in scope. You name the bundle to package, and with no argument it takes the
single .app under build/macos. It also works on an app built by other means.
$ nkit bundle build/macos/Counter.app -f README.md -d licensesThe mounted volume holds the app, every file and directory you added, and a shortcut to
/Applications for dragging the app across. Files and directories are placed at the volume
root under their own name, so -d licenses becomes <volume>/licenses. Passing
-f .VolumeIcon.icns sets a custom volume icon, which is how create-dmg does it too.
| Argument | Default | Purpose |
|---|---|---|
<app> |
the sole .app under build/macos |
The .app bundle to package |
-f, --file |
none | Add a file to the volume root; repeat for more |
-d, --dir |
none | Add a directory to the volume root; repeat for more |
-o, --out |
beside the app, <name>.dmg |
Where to write the image |
--volname |
the app name | Name the mounted volume |
--format |
hdiutil's choice | ULFO (lzfse), UDZO (zlib) or UDBZ |
--fs |
derived from the source | HFS+ or APFS |
--no-applications |
off | Leave out the /Applications shortcut |
--format and --fs are only passed to hdiutil when you ask for them, so the defaults
track macOS rather than being frozen into this tool. The image is written beside the app,
which keeps it inside build/ and lets nkit clean remove it along with the scratch
directory used to assemble the volume. Icon positions inside the mounted window are
Finder's own default; there is no Finder scripting involved, which keeps the command
scriptable and free of Automation permission prompts.
The same counter app, written against each of the three tiers.
You touch exactly what the shim exposes, one call at a time. Nothing hides allocation, event registration or geometry from you. Best for wrapping your own custom controls, auditing hot paths, or porting an existing ObjC/C++ design line by line.
import nkit
import nkit/gui/view
import nkit/gui/theme
import nkit/gui/stack
import nkit/gui/label
import nkit/gui/button
let app = initApplication()
let win = newWindow()
win.setTitle("Counter")
win.setSize(size(420.0, 320.0), false)
var clicks = 0
let countLabel = newLabel("0")
countLabel.setFontSize(48.0)
countLabel.setFontWeight(fwLight)
let root = newStack(stVertical, spacing = 16.0)
root.setPadding(24.0)
let plusBtn = newButton("increment", bsPush)
discard onClick(plusBtn, proc(e: ButtonClickEvent) =
inc clicks
setText(countLabel, $clicks))
addArranged(root, countLabel)
addArranged(root, plusBtn)
setContent(win, View(root))
win.show()
discard app.run()Widgets are values. Constructors like button,
h1, slider, card return nodes that compose into row/column
trees laid out by a portable solver with flex, spacing and nine-point
alignment. Handlers wire at construction, setText/getText route by
runtime type, and the whole tree stays backend agnostic. Best for everyday
screen building where you want structure without ceremony.
import nkit
import nkit/gui/sugar
let app = initApplication()
let win = newWindow()
win.setTitle("Counter")
win.setSize(size(420.0, 320.0), false)
var clicks = 0
var countLabel = text("0", 48.0, fwLight)
var statusText = p("press increment to start")
layout(
padding(
column(
h1("Counter"),
expanded(centered(countLabel)),
row(
button("increment", proc(e: ButtonClickEvent) =
inc clicks
setText(countLabel, $clicks)),
button("reset", proc(e: ButtonClickEvent) =
clicks = 0
setText(countLabel, "0"))
).spacing(12).crossAlign(caStretch),
centered(statusText)
).spacing(16),
all(24.0)
)
)
win.show()
discard app.run()The boilerplate disappears. initApp creates the
window, applies your size, runs the render block once, mounts the last
expression as the root and starts the run loop. State lives in one block,
UI in another, which keeps screens readable and encourages keeping widget
references in scope. Best for full applications and quick prototypes that
should still ship as real native windows.
import nkit/gui/appdsl_cocoa
initApp("Counter") do:
state do:
var clicks = 0
var countLabel = text("0", 48.0, fwLight)
var statusText = p("press increment to start")
render do:
padding(
column(
h1("Counter"),
expanded(centered(countLabel)),
row(
button("increment", proc(e: ButtonClickEvent) =
inc clicks
setText(countLabel, $clicks)
setText(statusText, "clicked " & $clicks & "x")),
button("reset", proc(e: ButtonClickEvent) =
clicks = 0
setText(countLabel, "0")
setText(statusText, "counter cleared"))
).spacing(12).crossAlign(caStretch),
centered(statusText)
).spacing(16),
all(24.0)
)All three tiers interoperate freely: a DSL app can drop to the low level for one tricky view, and low-level apps can adopt sugar constructors incrementally.
Platform support:
| Platform | Status |
|---|---|
| macOS (Cocoa/AppKit) | Done, active development |
| Windows | Planned |
| Linux | Active development |
| iOS | Active development |
| Android | Active development |
Planned work:
- Windows and Linux implementations of the existing service contracts (window, dialogs, clipboard, notifications, menus, tray)
- Backend ports of the layout host layer, reusing the pure-Nim solver
- More composite widgets (tables, trees, rich text)
- Multi-window and multi-display ergonomics
- Packaging story for bundling, icons and code signing
- π Found a bug? Create a new Issue
- π Wanna help? Fork it!
MIT license | (c) 2026 George Lemon for Nimbase Community.