Skip to content

Repository files navigation

Merenda

Merenda banner

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.

How it looks

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.

Merenda with the modern macOS theme Merenda with the Aqua theme Merenda with the Synthwave83 theme Merenda with the Peachy theme Merenda with the DarkBSD theme

Install and run Merenda

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.

A few small apps

Hello, Merenda

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.

A button that does something

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

A Markdown reader in a handful of lines

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.

A table with model–view–presenter

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.

Change a view's behavior with a protocol

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.

Slide between panels

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.

Kosmo

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.

Install and open a project

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. Add --bg to start Kosmo detached from your shell. 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.

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.

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.

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.

Build from source

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.

Explore more

The examples directory has complete apps you can run and change. These are good places to go once you've tried the basics:

About

Full featured cross platform GUI in pure Nim

Topics

Resources

Stars

32 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages