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. 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.nim
The 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
To use Merenda in your own project, add this dependency to your .nimble file
and run atlas install -tuk from that project:
requires "https://github.com/elcritch/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.
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.cbor
See Tekton's authoring workflow for supported resources and how to load the saved interface in your app.
A 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.nim
NimKit'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.
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.md
That 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.
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, and Git diffs. You can use it on its own or explore its source to see how a larger Merenda app fits together.
Use the Files and Find icons at the left of the status bar to switch sidebar views. Click the active icon again to collapse the sidebar and give the editor the full window width; click either icon to reopen it at its previous width.
Press Cmd-F on macOS or Ctrl-F elsewhere to search an editor, Markdown
preview, or Git diff. Use Enter / the arrow buttons to move through matches,
Cmd/Ctrl-G and Shift-Cmd/Ctrl-G for next and previous, and Escape to close.
Editor matches scroll to the center of the pane. Markdown and diff viewers use
case-insensitive literal search; diff search includes collapsed sections and
loads ordinary patches within the viewer's size limits. Open oversized patches
explicitly to include their contents.
Editor search and the Find sidebar open with replacement controls collapsed.
Click the chevron beside the search field to expand or collapse them without
clearing the query or results. On macOS, Option-Cmd-F opens editor replacement
and Option-Shift-Cmd-F opens replacement in files; elsewhere use
Alt-Ctrl-F and Alt-Shift-Ctrl-F. The regular Cmd/Ctrl-F editor shortcut and
Shift-Cmd/Ctrl-F file-search shortcut always return to search only.
Editor replacement offers Replace and Replace All, with one undo step per
operation. In the Find sidebar, run a search, expand replacement, enter text, then
choose Replace for the selected match or Replace All. Both fields treat
text literally by default. Enable .* to use Reni regular expressions in the
query and capture templates in the replacement: $0 is the entire match, $1
and later numbers are captures, ${name} is a named capture, and $$ inserts a
dollar sign. For example, search (?<name>cat|dog) and replace with ${name}!.
When named groups are present, Reni treats unnamed groups as noncapturing.
Matching is line by line, with Unicode-aware offsets; replacement text may
contain newlines. Invalid patterns or capture references show an error, and
invalid replacement templates leave files and buffers untouched. Empty
replacement text deletes matches. File replacement
saves the displayed results, including any search limits, checks that matched lines
still agree with the results, and skips files with unsaved editor changes. Its status
reports replacements and skipped files, then refreshes the search.
Filesystem notifications keep the browser and Quick Open inventory current. On macOS, one FSEvents stream covers a project tree; linked folders and Git metadata outside that tree retain their own streams. If native monitoring cannot cover a path, Kosmo polls periodically and logs the cause and affected paths. Missing directories and exhausted watch capacity are retried automatically.
Open, Open Folder, and Save As share a resizable file browser with Places shortcuts,
Back/Forward/Up navigation, and an editable location field. Enter an absolute path,
a relative folder, or ~/ and press Return to navigate. The file list and Name
column expand with the dialog; Save As keeps the filename below the browser so you
can change folders without losing the name you typed.
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 | bash
On 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
These commands reuse a running Kosmo instance. Use kosmo --bg ./folder/ to
start a new instance detached from your shell, or kosmo --new ./folder/ to
start a new instance in the foreground. kosmo -v and kosmo --version print
the version and exit. On macOS, you can also open Kosmo.app from Finder.
Add one or more folders to the existing Kosmo window with --add:
kosmo --add ../shared-library
Use Quick Open to find a file, drag tabs to arrange your panes, or choose File → New Terminal to open a shell. Markdown files open as previews, with a control to switch to the source editor. Merenda Settings places the theme and UI scale in Appearance, fonts in Typography, and scrolling in Behavior.
In Moe's normal mode, :e path opens a Kosmo document tab or selects the file's
existing tab.
:help and :config open reusable tabs; :config keeps Moe's interactive
settings viewer and its selection when you switch tabs, and closes with :q.
:split (:sp) opens the current buffer in a pane below, and :vsplit (:vs)
opens it in a pane to the right. Add a filename to open that file in the new
pane; relative paths use the editor's working directory. :new and :vnew
create empty buffers in those panes. Moe mappings for these commands and
mode_switch config use the same Kosmo tabs and panes.
:wq, :x, and ZZ save and close the selected native tab; other unsaved
tabs stay open. :bd accepts a buffer number or filename and removes that
buffer from every pane, refusing unsaved changes unless you add !.
Undo and Redo also work while typing, including when Input mode is forced.
Vim's "+ and "* registers use the native clipboard; on platforms with one
clipboard, both registers share it.
Kosmo advances Moe's background work from the native event loop, including
when Git monitoring is disabled. Hook output, build output, and :jobs use a
reusable Command Output tab. :jobs! stops running external commands.
:terminal, shell commands, and manual pages open native terminal tabs.
Kosmo loads [Hook] settings from ~/.config/moe/moerc.toml; Moe's read/write
hook rules apply to native file opens and saves too.
Unsaved work is checkpointed every five seconds after changes into Kosmo's
recovery cache and removed on a clean close. After an interrupted session,
Kosmo opens Recovered Work when preserved copies exist. Use :recover to
open that tab again. Restore brings a copy into an unsaved editor buffer as
an undoable edit; Discard requires a second click to confirm removal.
Settings changes apply to the current Kosmo instance immediately. Choose Save as Default to use the committed theme, fonts, scale, and scrolling choices on the next launch; Reset restores the last saved values. You can also enable Remember changes for future launches to save each committed change automatically.
Terminal tabs sleep on PTY readiness while idle. A dedicated Sigils dispatcher
owns their sessions, reads and parses output, and accepts input and resize commands.
The UI consumes owned RChan snapshots without waiting for the parser. This worker
path is shared across platforms; native PTY startup currently requires POSIX.
For native timing measurements with cmatrix, ps, or a 10,000-line burst, see
terminal latency diagnostics.
Kosmo Settings → Moe Themes includes Catppuccin Latte, Catppuccin Mocha,
Kanagawa Wave, One Dark, and Tokyo Night Moon. These themes are embedded in
the executable and work from any launch directory. Add your own TOML themes
in ~/.config/moe/themes; a user theme with the same name overrides a bundled
theme.
To use a Nim language server, add nimLspCommand to Kosmo's
~/.config/kosmo/config.json and restart Kosmo. The command must be an
absolute executable path followed by any arguments. Nimdex 0.1.2 defaults to
nim ic and requires a compiler with --genBif:on support, plus matching
nifler and nifmake companions beside Nim or on PATH. For example, build
Nimdex from its checkout with that compiler:
cd ../nimdex
mkdir -p bin
/absolute/path/to/bif-enabled/nim c -d:release -o:bin/nimdex src/nimdex.nim
Add this field to the JSON config, using absolute paths without spaces:
{
"nimLspCommand": "/absolute/path/to/nimdex/bin/nimdex daemon --compiler /absolute/path/to/bif-enabled/nim --log-file /absolute/path/to/nimdex.log"
}
Kosmo enables Moe's LSP client when this field is set. In normal mode, press
g then d to go to a definition, or K to show hover information. Remove
the field or set it to an empty string to disable LSP on the next launch.
Nimdex's daemon command communicates over standard input and output and stays
attached to Kosmo; it does not need to detach itself.
--log-file appends daemon stderr to the chosen file and creates missing parent
directories. Omit it to send logs through Kosmo's LSP message log.
To run Nimdex separately, start its persistent LSP TCP listener in another terminal:
/absolute/path/to/nimdex/bin/nimdex daemon --lsp-listen 9257 --compiler /absolute/path/to/bif-enabled/nim --log-file /absolute/path/to/nimdex.log
Set nimLspCommand to the listener's tcp://host:port address and restart Kosmo:
{
"nimLspCommand": "tcp://127.0.0.1:9257"
}
Hostnames, IPv4 addresses, and bracketed IPv6 addresses such as
tcp://[::1]:9257 are accepted. Kosmo connects in its dedicated LSP child
process and forwards standard LSP Content-Length frames in both directions.
It does not launch the server; connection errors appear in the LSP message log,
and closing Kosmo disconnects its session while Nimdex keeps listening for the
next connection. Nimdex serves one editor session at a time, so use a separate
listener for each concurrently connected Kosmo window.
Kosmo initializes each connection with its editor's project directory as the
LSP workspace root.
Nimdex binds to 127.0.0.1 by default. --lsp-listen 0 chooses an available
port and records it in the daemon log. For another bind address, including a
Tailscale address, add --lsp-host ADDRESS and use that address in Kosmo's TCP
URL. The server must be able to access the same project and document paths;
TCP does not translate paths between machines. Use --lsp-listen for editor
LSP; Nimdex's separate --listen option serves its CLI query protocol.
Syntax colors arrive progressively as background workers finish small batches. Markdown previews display their content before fenced-code coloring finishes; selection, code-block scroll positions, and text layout survive those color updates.
To add language highlighting, open Kosmo Settings → TextMate Grammars and
search the built-in language grammars in the open-source microsoft/vscode
repository by language name, file extension, or TextMate scope. Select a result
to download, validate, and install its grammar files into Kosmo's user
configuration. This searches VS Code's source repository, not its extension
Marketplace.
You can also send a Git diff straight to Kosmo:
git diff | kosmo --diff
Run 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.
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, run ./kosmo.exe . after compiling.
On macOS, install the current checkout as a complete Kosmo.app with its icon,
bundled notices, debug symbols, and local code signature:
nim install_kosmo
This 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.
The examples directory has complete apps you can run and change. These are good places to go once you've tried the basics:
5 followers · starred Jul 2026
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. 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.nim
The 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
To use Merenda in your own project, add this dependency to your .nimble file
and run atlas install -tuk from that project:
requires "https://github.com/elcritch/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.
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.cbor
See Tekton's authoring workflow for supported resources and how to load the saved interface in your app.
A 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.nim
NimKit'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.
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.md
That 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.
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, and Git diffs. You can use it on its own or explore its source to see how a larger Merenda app fits together.
Use the Files and Find icons at the left of the status bar to switch sidebar views. Click the active icon again to collapse the sidebar and give the editor the full window width; click either icon to reopen it at its previous width.
Press Cmd-F on macOS or Ctrl-F elsewhere to search an editor, Markdown
preview, or Git diff. Use Enter / the arrow buttons to move through matches,
Cmd/Ctrl-G and Shift-Cmd/Ctrl-G for next and previous, and Escape to close.
Editor matches scroll to the center of the pane. Markdown and diff viewers use
case-insensitive literal search; diff search includes collapsed sections and
loads ordinary patches within the viewer's size limits. Open oversized patches
explicitly to include their contents.
Editor search and the Find sidebar open with replacement controls collapsed.
Click the chevron beside the search field to expand or collapse them without
clearing the query or results. On macOS, Option-Cmd-F opens editor replacement
and Option-Shift-Cmd-F opens replacement in files; elsewhere use
Alt-Ctrl-F and Alt-Shift-Ctrl-F. The regular Cmd/Ctrl-F editor shortcut and
Shift-Cmd/Ctrl-F file-search shortcut always return to search only.
Editor replacement offers Replace and Replace All, with one undo step per
operation. In the Find sidebar, run a search, expand replacement, enter text, then
choose Replace for the selected match or Replace All. Both fields treat
text literally by default. Enable .* to use Reni regular expressions in the
query and capture templates in the replacement: $0 is the entire match, $1
and later numbers are captures, ${name} is a named capture, and $$ inserts a
dollar sign. For example, search (?<name>cat|dog) and replace with ${name}!.
When named groups are present, Reni treats unnamed groups as noncapturing.
Matching is line by line, with Unicode-aware offsets; replacement text may
contain newlines. Invalid patterns or capture references show an error, and
invalid replacement templates leave files and buffers untouched. Empty
replacement text deletes matches. File replacement
saves the displayed results, including any search limits, checks that matched lines
still agree with the results, and skips files with unsaved editor changes. Its status
reports replacements and skipped files, then refreshes the search.
Filesystem notifications keep the browser and Quick Open inventory current. On macOS, one FSEvents stream covers a project tree; linked folders and Git metadata outside that tree retain their own streams. If native monitoring cannot cover a path, Kosmo polls periodically and logs the cause and affected paths. Missing directories and exhausted watch capacity are retried automatically.
Open, Open Folder, and Save As share a resizable file browser with Places shortcuts,
Back/Forward/Up navigation, and an editable location field. Enter an absolute path,
a relative folder, or ~/ and press Return to navigate. The file list and Name
column expand with the dialog; Save As keeps the filename below the browser so you
can change folders without losing the name you typed.
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 | bash
On 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
These commands reuse a running Kosmo instance. Use kosmo --bg ./folder/ to
start a new instance detached from your shell, or kosmo --new ./folder/ to
start a new instance in the foreground. kosmo -v and kosmo --version print
the version and exit. On macOS, you can also open Kosmo.app from Finder.
Add one or more folders to the existing Kosmo window with --add:
kosmo --add ../shared-library
Use Quick Open to find a file, drag tabs to arrange your panes, or choose File → New Terminal to open a shell. Markdown files open as previews, with a control to switch to the source editor. Merenda Settings places the theme and UI scale in Appearance, fonts in Typography, and scrolling in Behavior.
In Moe's normal mode, :e path opens a Kosmo document tab or selects the file's
existing tab.
:help and :config open reusable tabs; :config keeps Moe's interactive
settings viewer and its selection when you switch tabs, and closes with :q.
:split (:sp) opens the current buffer in a pane below, and :vsplit (:vs)
opens it in a pane to the right. Add a filename to open that file in the new
pane; relative paths use the editor's working directory. :new and :vnew
create empty buffers in those panes. Moe mappings for these commands and
mode_switch config use the same Kosmo tabs and panes.
:wq, :x, and ZZ save and close the selected native tab; other unsaved
tabs stay open. :bd accepts a buffer number or filename and removes that
buffer from every pane, refusing unsaved changes unless you add !.
Undo and Redo also work while typing, including when Input mode is forced.
Vim's "+ and "* registers use the native clipboard; on platforms with one
clipboard, both registers share it.
Kosmo advances Moe's background work from the native event loop, including
when Git monitoring is disabled. Hook output, build output, and :jobs use a
reusable Command Output tab. :jobs! stops running external commands.
:terminal, shell commands, and manual pages open native terminal tabs.
Kosmo loads [Hook] settings from ~/.config/moe/moerc.toml; Moe's read/write
hook rules apply to native file opens and saves too.
Unsaved work is checkpointed every five seconds after changes into Kosmo's
recovery cache and removed on a clean close. After an interrupted session,
Kosmo opens Recovered Work when preserved copies exist. Use :recover to
open that tab again. Restore brings a copy into an unsaved editor buffer as
an undoable edit; Discard requires a second click to confirm removal.
Settings changes apply to the current Kosmo instance immediately. Choose Save as Default to use the committed theme, fonts, scale, and scrolling choices on the next launch; Reset restores the last saved values. You can also enable Remember changes for future launches to save each committed change automatically.
Terminal tabs sleep on PTY readiness while idle. A dedicated Sigils dispatcher
owns their sessions, reads and parses output, and accepts input and resize commands.
The UI consumes owned RChan snapshots without waiting for the parser. This worker
path is shared across platforms; native PTY startup currently requires POSIX.
For native timing measurements with cmatrix, ps, or a 10,000-line burst, see
terminal latency diagnostics.
Kosmo Settings → Moe Themes includes Catppuccin Latte, Catppuccin Mocha,
Kanagawa Wave, One Dark, and Tokyo Night Moon. These themes are embedded in
the executable and work from any launch directory. Add your own TOML themes
in ~/.config/moe/themes; a user theme with the same name overrides a bundled
theme.
To use a Nim language server, add nimLspCommand to Kosmo's
~/.config/kosmo/config.json and restart Kosmo. The command must be an
absolute executable path followed by any arguments. Nimdex 0.1.2 defaults to
nim ic and requires a compiler with --genBif:on support, plus matching
nifler and nifmake companions beside Nim or on PATH. For example, build
Nimdex from its checkout with that compiler:
cd ../nimdex
mkdir -p bin
/absolute/path/to/bif-enabled/nim c -d:release -o:bin/nimdex src/nimdex.nim
Add this field to the JSON config, using absolute paths without spaces:
{
"nimLspCommand": "/absolute/path/to/nimdex/bin/nimdex daemon --compiler /absolute/path/to/bif-enabled/nim --log-file /absolute/path/to/nimdex.log"
}
Kosmo enables Moe's LSP client when this field is set. In normal mode, press
g then d to go to a definition, or K to show hover information. Remove
the field or set it to an empty string to disable LSP on the next launch.
Nimdex's daemon command communicates over standard input and output and stays
attached to Kosmo; it does not need to detach itself.
--log-file appends daemon stderr to the chosen file and creates missing parent
directories. Omit it to send logs through Kosmo's LSP message log.
To run Nimdex separately, start its persistent LSP TCP listener in another terminal:
/absolute/path/to/nimdex/bin/nimdex daemon --lsp-listen 9257 --compiler /absolute/path/to/bif-enabled/nim --log-file /absolute/path/to/nimdex.log
Set nimLspCommand to the listener's tcp://host:port address and restart Kosmo:
{
"nimLspCommand": "tcp://127.0.0.1:9257"
}
Hostnames, IPv4 addresses, and bracketed IPv6 addresses such as
tcp://[::1]:9257 are accepted. Kosmo connects in its dedicated LSP child
process and forwards standard LSP Content-Length frames in both directions.
It does not launch the server; connection errors appear in the LSP message log,
and closing Kosmo disconnects its session while Nimdex keeps listening for the
next connection. Nimdex serves one editor session at a time, so use a separate
listener for each concurrently connected Kosmo window.
Kosmo initializes each connection with its editor's project directory as the
LSP workspace root.
Nimdex binds to 127.0.0.1 by default. --lsp-listen 0 chooses an available
port and records it in the daemon log. For another bind address, including a
Tailscale address, add --lsp-host ADDRESS and use that address in Kosmo's TCP
URL. The server must be able to access the same project and document paths;
TCP does not translate paths between machines. Use --lsp-listen for editor
LSP; Nimdex's separate --listen option serves its CLI query protocol.
Syntax colors arrive progressively as background workers finish small batches. Markdown previews display their content before fenced-code coloring finishes; selection, code-block scroll positions, and text layout survive those color updates.
To add language highlighting, open Kosmo Settings → TextMate Grammars and
search the built-in language grammars in the open-source microsoft/vscode
repository by language name, file extension, or TextMate scope. Select a result
to download, validate, and install its grammar files into Kosmo's user
configuration. This searches VS Code's source repository, not its extension
Marketplace.
You can also send a Git diff straight to Kosmo:
git diff | kosmo --diff
Run 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.
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, run ./kosmo.exe . after compiling.
On macOS, install the current checkout as a complete Kosmo.app with its icon,
bundled notices, debug symbols, and local code signature:
nim install_kosmo
This 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.
The examples directory has complete apps you can run and change. These are good places to go once you've tried the basics:
5 followers · starred Jul 2026