Merenda is a desktop GUI toolkit written in Nim, inspired by Cocoa and OpenStep.
It gives you buttons, text editors, tables, menus, and layouts for building desktop
apps, with themes you can change to suit your app. Its public module is called
NimKit: import merenda/nimkit.
The project is under active development, targeting macOS, Linux, FreeBSD, and Windows. Kosmo, a code editor built with Merenda, is a way to try it without writing any code.
Merenda draws its own controls, so you can use the same theme across platforms. Choose a familiar macOS look, glossy Aqua buttons, or something more colorful. DarkBSD is the default.
You'll need Nim 2.2.6 or newer, a C compiler, Git, and
Atlas to install the Nim
dependencies. Use a current Atlas release: the Atlas 0.14 bundled with Nim
2.2.12 cannot read merenda.nimble or the -u flag used below; Atlas 0.16.3
works. On Linux and FreeBSD, you'll also need the system libraries for your
windowing and graphics backend; Merenda uses Siwin for windows and FigDraw for
rendering.
To try the examples, clone the repository and run the controls showcase:
git clone https://github.com/elcritch/merenda.git
cd merenda
atlas install -tuk
nim r examples/controls_showcase.nimThe showcase lets you try the controls together in one window. To see another
theme, run it with NIMKIT_THEME set (in a POSIX shell):
NIMKIT_THEME=macos nim r examples/controls_showcase.nim
NIMKIT_THEME=aqua nim r examples/controls_showcase.nim
NIMKIT_THEME=punch nim r examples/controls_showcase.nimThe built-in themes are embedded CSS stylesheets with shared widget defaults. Application CSS can extend them, and typed Nim styling APIs remain available for programmatic overrides.
The Punch theme ports RUI2’s Punch (fat) theme: warm cream, orange accents, thick ink outlines, bold uppercase buttons and hard shadows.
Use CSS for control colors, fonts, gradients, state styles, edge pinning and
native layout constraints. Style StackView/GridView spacing and alignment and
control-specific metrics through -nimkit-* properties. The CSS preview
lays out its toolbar, sidebar and editor from a stylesheet and reloads it when
you click Reload CSS:
nim r examples/css_theme_demo.nimRead the CSS theming guide for selectors, variables, supported properties and appearance scope.
You can also describe native GUIs in lightweight markup,
using HTML5 syntax and data-* attributes for native widget options:
<nk-main>
<nk-window id="window" title="Hello" data-frame="100 100 400 240">
<nk-stack-view id="root" data-axis="vertical" data-padding="24" data-spacing="12">
<nk-label data-label-style="title">Hello from Merenda</nk-label>
<nk-text-field id="name" placeholder="Your name"/>
<button id="save" data-action="saveDocument">Save</button>
</nk-stack-view>
</nk-window>
</nk-main>loadGuiResourceBundle reads the interface and instantiateResources constructs
its native controls. One document can define independent windows. Built-in elements
use nk-; application widgets can use names such as some-app-widget. Use IDs to
look up controls and connect behavior in Nim.
Try the GUI resource example with
nim ic -r examples/html_ui_demo.nim. It includes compact preferences controls,
Preview and Reset actions, and a separate About window. The Appearance dropdown
switches both windows between the custom stylesheet and the built-in CSS themes.
The NIF version duplicates this demo with NIF GUI markup and a NIF theme. The theme uses tagged selectors, hex colors, numbers with units, bare keywords, and shared token references. Enable its optional dependency and run it with:
atlas install -tk --feature=nifDemo
nim ic -r examples/nif_ui_demo.nimIt uses OpenParser 0.4.1 or newer
with Nim 2.2.10 or newer. The demo-local adapter feeds the existing GUI and CSS
loaders; see the NIF demo format for its vocabulary.
The shorthand version uses native widget
property nodes and line-based style rules for the same preferences window.
To use Merenda in your own project, add this dependency to your .nimble file
and run atlas install -tuk from that project:
requires "merenda"Build your app with threads enabled and ARC or ORC, for example
nim r --threads:on --mm:arc main.nim. The examples in this repository already
have those settings.
FigDraw is compiled into the application. Merenda's experimental
useNativeDynlib mode and build_dynlib task have been removed; omit
-d:useNativeDynlib from existing build commands.
Compile Merenda apps with -d:merenda.devControl for optional local screenshots and input automation.
Setting MERENDA_DEV_CONTROL=1 then starts and control socket.
MERENDA_BACKGROUND=1 supports automated runs without activating the app.
See development control for the PID/socket CLI and protocol.
MERENDA_DEV_CONTROL=1 MERENDA_BACKGROUND=1 ./examples/controls_showcase &
app_pid=$!
./deps/merenda-dev --pid "$app_pid" windows
./deps/merenda-dev --pid "$app_pid" screenshot --window w1 --output /tmp/showcase.pngA window, a label, and an application loop:
import merenda/nimkit
let
app = sharedApplication()
window = newWindow("Hello", frame = rect(100, 100, 360, 180))
root = newView()
greeting = newTitleLabel("Hello, Merenda!")
root.addSubview(greeting)
greeting.pinEdges(
toGuide = root.contentLayoutGuide(insets(24.0)),
edges = {leLeft, leTop, leRight},
)
app.runWindow(window, root)Save this as examples/greeting.nim in your checkout and run
nim r examples/greeting.nim.
Here's a counter. A stack view arranges the controls, and the button's action updates the label.
import merenda/nimkit
import sigils/selectors
let
app = sharedApplication()
window = newWindow("Counter", frame = rect(100, 100, 320, 220))
root = newView()
layout = newStackView(laVertical)
label = newStatusLabel("Clicked 0 times")
button = newButton("Click")
clickAction = actionSelector("counterClicked")
var clicks = 0
proc onClick(sender: DynamicAgent) =
if not sender.isNil:
inc clicks
label.text = "Clicked " & $clicks & " times"
button.target = newActionTarget(clickAction, onClick)
button.action = clickAction
layout.spacing = 12.0
layout.alignment = svaFill
layout.addArrangedSubview(label, button)
root.addSubview(layout)
layout.pinEdges(
toGuide = root.contentLayoutGuide(insets(44.0, 44.0, 0.0, 44.0)),
edges = {leLeft, leTop, leRight},
)
app.runWindow(window, root)This is examples/quick_start.nim. Run it with:
nim r examples/quick_start.nimNimKit's larger controls handle more of the work for you. This app opens a
Markdown file with selectable text, links, code blocks, tables, and images.
The view handles scrolling and layout as you resize the window. Up/Down arrows
scroll the reader, j/k select headings and paragraphs in reading order, and
Enter expands or collapses the selected block's section.
import std/os
import merenda/nimkit
let
path = absolutePath(paramStr(1))
app = sharedApplication()
window = newWindow(path.extractFilename(), frame = rect(120, 80, 820, 700))
root = newView()
viewer = newMarkdownView(readFile(path), imageBasePath = path.parentDir)
root.addSubview(viewer)
viewer.pinEdges(toGuide = root.contentLayoutGuide(insets(20.0)))
app.runWindow(window, root, viewer)Save it as examples/reader.nim, then run
nim r examples/reader.nim README.md. It expects a readable file path.
For a version with a built-in sample document, run:
nim r examples/markdown_viewer_demo.nim README.mdThat is the kind of efficiency NimKit aims for: you write the app's behavior, while the controls take care of text selection, focus, drawing, and layout. For an app with more interaction, try the to-do list or its table-based version, which adds row selection and drag reordering.
For a small app, MVP doesn't need a class hierarchy. Here, tasks holds the
model data in an ArrayController, the table and button are the view, and
markDone acts as the presenter. Select a row and click Mark done: the
presenter updates the model and refreshes the table.
import merenda/nimkit
import sigils/selectors
let
app = sharedApplication()
window = newWindow("Tasks", frame = rect(100, 100, 460, 320))
root = newView()
table = newTableView(frame = rect(24, 24, 412, 200))
doneButton = newButton("Mark done", frame = rect(24, 244, 140, 32))
tasks = newArrayController(columns = [
modelColumn("task", "Task", "task", 260.0),
modelColumn("state", "State", "state", 100.0),
])
for index, title in ["Write release notes", "Try the demo"]:
tasks.addItem(modelItem($index, fields = [
modelField("task", toObj(title)),
modelField("state", toObj("To do")),
]))
table.bindTableView(tasks)
table.selectionMode = tsmSingle
# The presenter turns a user action into a model update.
proc markDone(sender: DynamicAgent) =
discard sender
let selected = tasks.selectionController().selectedIdentifier()
if selected.len > 0:
tasks.setValue(selected, "state", toObj("Done"))
table.reloadData()
let doneAction = actionSelector("markTaskDone")
doneButton.target = newActionTarget(doneAction, markDone)
doneButton.action = doneAction
root.addSubview(table)
root.addSubview(doneButton)
app.runWindow(window, root, table)Save this as examples/tasks_mvp.nim and run nim r examples/tasks_mvp.nim.
The table binding supplies the columns and row values, so you only write the
action specific to your app. For a larger version, see the
table-based to-do app or the
model controller examples.
Sigils protocols let you attach methods to an individual object, even when its
type comes from a library. Here, an ordinary View gets a custom drawing method.
Click Inspect layout to replace that method with one that displays the
view's dimensions; click again to restore the preview.
import merenda/nimkit
import sigils/selectors
protocol PreviewDrawing of ViewDrawingProtocol:
method draw(view: View, context: DrawContext) =
context.addRectangle(view.bounds, fill(color(0.18, 0.32, 0.55)))
context.addText(view.bounds, "Design preview", color(1, 1, 1), taCenter)
protocol LayoutDrawing of ViewDrawingProtocol:
method draw(view: View, context: DrawContext) =
context.addRectangle(view.bounds, fill(color(0.12, 0.22, 0.24)))
let size = view.bounds.size
context.addText(
view.bounds, $size.width & " x " & $size.height, color(1, 1, 1), taCenter
)
let
app = sharedApplication()
window = newWindow("Dynamic drawing", frame = rect(100, 100, 420, 260))
root = newView()
preview = newView(frame = rect(24, 24, 372, 140))
button = newButton("Inspect layout", frame = rect(24, 188, 160, 32))
inspectAction = actionSelector("toggleLayoutDrawing")
preview.withProtocol(PreviewDrawing)
var inspecting = false
proc toggleLayout(sender: DynamicAgent) =
discard sender
inspecting = not inspecting
if inspecting:
preview.withProtocol(LayoutDrawing)
else:
preview.withProtocol(PreviewDrawing)
preview.needsDisplay = true
button.target = newActionTarget(inspectAction, toggleLayout)
button.action = inspectAction
root.addSubview(preview)
root.addSubview(button)
app.runWindow(window, root)Save this as examples/protocol_drawing.nim and run
nim r examples/protocol_drawing.nim.
Both implementations are compiled Nim code with typed View and DrawContext
arguments. NimKit calls the drawing protocol, and Sigils dispatches to the method
currently installed on preview. Replacing it leaves other views alone and
keeps this view's identity, layout, and place in the window intact.
This is useful for adding diagnostics, swapping rendering strategies, or customizing a library object without introducing a subclass for every variation. The same pattern works for view controller loading and table delegates.
An animationGroup turns ordinary property assignments into a coordinated
animation. These two panels slide together over 800 ms with linear motion.
Next slides to the details; Back reverses the trip.
import merenda/nimkit
import sigils/selectors
let
app = sharedApplication()
window = newWindow("Carousel", frame = rect(100, 100, 420, 260))
root = newView()
viewport = newView(frame = rect(24, 24, 372, 140))
first = newGroupBox("Welcome", frame = rect(0, 0, 372, 140))
second = newGroupBox("Details", frame = rect(372, 0, 372, 140))
button = newButton("Next", frame = rect(24, 188, 160, 32))
slideAction = actionSelector("slidePanels")
first.contentView = newLabel("Your first panel.")
second.contentView = newLabel("A little more information.")
viewport.clipsToBounds = true
var showingDetails = false
proc finishSlide(button: Button) {.slot.} =
button.enabled = true
proc slidePanels(sender: DynamicAgent) =
discard sender
if button.enabled:
button.enabled = false
showingDetails = not showingDetails
button.title = if showingDetails: "Back" else: "Next"
let size = viewport.bounds.size
let offset =
if showingDetails:
-size.width
else:
0.0'f32
let slide = animationGroup(duration = 800.ms, curve = acLinear):
first.frame = rect(offset, 0, size.width, size.height)
second.frame = rect(offset + size.width, 0, size.width, size.height)
slide.connect(finished, button, finishSlide)
discard app.startAnimation(slide)
button.target = newActionTarget(slideAction, slidePanels)
button.action = slideAction
viewport.addSubview(first)
viewport.addSubview(second)
root.addSubview(viewport)
root.addSubview(button)
app.runWindow(window, root)Run the carousel example with
nim r examples/carousel_demo.nim. The viewport clips the panels as they move,
and the animation's finished signal enables the button for the next transition.
For more, see property animations and sequences.
A standard Box can blur the content behind its rounded bounds through FigDraw.
Its child controls stay sharp, and its fill follows the active theme:
let overlay = newBox(frame = rect(24, 24, 320, 96))
overlay.addStyleClass(PopoverBoxStyleClass)
overlay.backdropBlurRadius = 20
overlay.backdropTintOpacity = 0.78
overlay.addContentSubview(newTextField("Search"))
root.addSubview(overlay)Set backdropBlurRadius to zero to restore the ordinary box fill.
backdropTintOpacity controls the themed tint over the blurred content, from
zero to one. This effect is rendered inside the application, independently of
native window backdrop effects.
NimKit apps can reuse the same controls through SearchBar or attach a
TextSearchController to any host view:
let preview = newTextView("λcat and dog")
let search = newTextSearchController(
preview, "document", textViewSearchAdapter(preview)
)
# Attach preview to a window before opening the controls.
discard search.showSearch()Keep the controller alive while its controls are attached, and call search.close()
before releasing it if the host remains alive. Custom TextSearchAdapter callbacks
provide text snapshots, reveal matches, and clear selections for terminals,
multiple documents, or other data sources. An optional replace callback adds
collapsed replacement controls and receives a validated Reni pattern and capture
template. The adapter owns edits, undo, and checking that source content is current.
Hosts can route find shortcuts through search.handleSearchKey(event) and call
search.refreshSearch() when their source changes.
SearchBar is a regular Box containing standard text fields and buttons, arranged
with StackView. For embedded controls, add newSearchBar(frame = ...) to a
container (or use layout constraints) and set hidden = false. The host chooses
whether it floats; query, error, and replacement changes do not override an
embedded frame. TextSearchController anchors the floating controls with ordinary
layout constraints, allowing their height to follow their content.
Kosmo is a code editor built with Merenda and Moe's Vim-style editing engine. It brings together a file browser, split panes, terminal tabs, Markdown previews, Git diffs, and GitHub issues and PRs. You can use it on its own or explore its source to see how a larger Merenda app fits together.
In terminal tabs, Command-click a file path on macOS, or Control-click on Linux
and Windows, to open it in Kosmo. Links such as src/main.nim:12:5 jump to the
given line and column (both start at 1). Relative paths use the terminal's reported
current directory, or its starting directory when the shell does not report one.
The terminal link setting controls both file links and web links.
Nim diagnostics such as src/main.nim(12, 5) and src/main.nim(12) are clickable too.
Show Git Diff keeps a separate viewer for each repository. Detach their tabs to compare projects in separate windows; invoking the shortcut again returns to that project's viewer.
In the GitHub view, expand a pull request and choose Open diff in Kosmo to compare its base branch with its head. Kosmo fetches dedicated PR refs in the background, including for fork PRs, and leaves your working branch and files intact. Fetch failures appear in the diff viewer; Refresh retries the comparison.
You don't need Nim to use a prebuilt Kosmo release. Run the installer from a shell (Git Bash on Windows):
curl -fsSL https://raw.githubusercontent.com/elcritch/merenda/HEAD/install.sh | bashOn macOS, it installs Kosmo.app in ~/Applications and a kosmo command in
~/.local/bin. On Linux, FreeBSD, and Windows, the command goes in
~/.local/bin. Make sure that directory is on your PATH.
Open the current folder or a file:
kosmo .
kosmo README.md
kosmo src/main.nim:12:5
kosmo 'src/main.nim(12, 5)'These commands reuse a running Kosmo instance.
Use kosmo --bg ./folder/ to start a new instance detached from your shell.
Start a new instance with kosmo --new ./folder/.
kosmo -v and kosmo --version print the version and exit.
On macOS, you can also open Kosmo.app from Finder.
kosmo --add ../shared-library
kosmo --bg ../shared-libraryRun kosmo --help for command-line options. See the
keyboard shortcut guide for navigation
and Vim bindings, or release and installer details
for supported builds, custom install locations, and the static Linux build.
FigDraw 0.46.3 discovers HarfBuzz 7 or newer and FriBidi at runtime. Kosmo uses
them for complex-script shaping, ligatures, and automatic system-font fallback;
if either library is unavailable, it uses Pixie for text layout and rasterization.
The native text libraries are optional for building and running ordinary source
builds. Install harfbuzz and fribidi with Homebrew on macOS, or libharfbuzz0b
and libfribidi0 on Debian/Ubuntu, to enable shaping.
Use -d:figdrawTextBackend=pixie to build with Pixie only, or
-d:figdrawTextBackend=harfbuzzy to require native shaping. Bundled static
builds (-d:harfbuzzyStatic) retain the Harfbuzzy backend by default.
From your Merenda checkout, install the extra Kosmo dependencies, then build and launch it:
atlas install -tuk --features:kosmo
nim c -o:kosmo src/merenda/kosmo/kosmo.nim
./kosmo .On Windows, use MinGW-w64 GCC. CI and releases use MSYS2's UCRT64 environment:
pacman -S mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-harfbuzz mingw-w64-ucrt-x86_64-fribidi
nim c --cc:gcc -o:kosmo.exe src/merenda/kosmo/kosmo.nim
./kosmo.exe .Keep the UCRT64 bin directory on PATH for the compiler and optional text DLLs.
Release archives bundle the text DLLs and their dependencies, and retain native
debug information inside kosmo.exe.
On macOS, install the current checkout as a complete Kosmo.app with its icon,
bundled notices, debug symbols, and local code signature:
brew install meson ninja pkg-config
nim install_kosmoThis uses Atlas to resolve dependencies, replaces ~/Applications/Kosmo.app,
and updates the ~/.local/bin/kosmo link. Restart Kosmo if it was already
running. Set KOSMO_INSTALL_DIR or KOSMO_BIN_DIR to choose other locations.
Like CI, this builds shared HarfBuzz and FriBidi libraries for macOS 13, bundles
them in Contents/Frameworks, and loads them at runtime. The installed app does
not require Homebrew at runtime.
The examples directory has complete apps you can run and change. These are good places to go once you've tried the basics:
- Layouts: stacks and constraints, constraint playground, and the layout guide.
- Tables and trees: table example, filter row (including date range pickers and token fields), outline example, and model controllers.
- App workflows: documents and windows, preferences, and settings and themes.
- Text and drawing: Markdown viewer, canvas, and terminal.
- Building UI from resources: resource guide and example.
- Development checks: reliability and memory profiling.
- Windows on FreeBSD: disposable bhyve test VM and GPU passthrough.
- Windows VirtualBox: setup and FreeBSD GPU test results.
- Under the hood: NimKit design, Kosmo workspace updates, FigDraw, Siwin, and Sigils.
Build a resource document with Tekton's widget palette, property inspector, and live preview. Add layout guides and constraints, pin views to their parent, undo changes, and save the result for your Merenda app. Use Interact to try controls in the preview.
nim r src/merenda/tekton.nim
# Or open an existing interface:
nim r src/merenda/tekton.nim path/to/interface.cborSee Tekton's authoring workflow for supported resources and how to load the saved interface in your app.