WolOS
Reference contents

Reference · Apps and the UI kit

Your first Wolkit app

A to-do list in eight short steps: a window, a text field, a list, a style sheet, a test without a display and a package.

On this page

What it is

A tutorial that builds a small to-do app with Wolkit, the WolOS UI kit for Crystal, in about fifteen minutes. Each step adds one idea: a signal, a text field, a keyed list, a style sheet, a test and a package.

Why

The fastest way to learn a UI kit is to build something real with it. Every program on this page that starts with require "wolkit" is type-checked against the kit by a script in the WolOS repository, so the code here cannot drift from the code that ships.

How

  1. Make the project: wolkit-new todos, then cd todos and make run. wolkit-new comes with the wolkit package, which is in preview; from the next update wol app new todos does the same. You see a window with a greeting and a button that counts.
  2. Follow the steps below in order; each one replaces src/todos.cr with a slightly larger program.
  3. Run the test: make spec drives the app in a headless engine, with no display.

If something goes wrong

What you seeWhat to do
icon_button needs `label:` Every icon button needs a label: screen readers and tooltips use it.
A compile error that lists a scaleA size off the design scale, such as gap: 13. Use a name: :sm, :md, :lg.
A colour is refusedColours are theme tokens (:dim, :card), so dark mode and contrast follow without code.
The window shows no pictureThe engine does not paint images yet; it keeps them and paints them when the paint stage lands.

Your first app in Crystal · Wolkit reference · Wolkit style · Cryswol · The Cryswol page

Reference

A tutorial for writing a WolOS app with the wolkit Crystal shard: a to-do list in eight steps. It builds the program in wolos-crystal/examples/todos/src/todos.cr; the reference is docs/modules/wolkit.md and the engine is docs/modules/libwolkit.md (code in crates/wolkit). Every program in this tutorial that starts with require "wolkit" is type-checked by wolos-crystal/shards/wolkit/spec/tutorial_spec.cr, so it cannot drift from the shard. You need the Crystal toolchain, the wolkit and cryswol shards (/usr/lib/wolos/crystal) and libwolkit.

1. Make the project (1 minute)

wolkit-new todos
cd todos
make run

wolkit-new (the work behind wol app new todos --template app) writes shard.yml, src/app.cr, src/views/main.cr, src/components/greeting.cr, src/styles/app.wolcss, spec/app_spec.cr, a Makefile and a README, and the project builds and passes its own spec as it stands. make run opens a window with a greeting and a button that counts. Close the window and the program ends.

2. Read the smallest app (1 minute)

Replace the generated files with one file, src/todos.cr:

require "wolkit"

class Todos < Wolkit::App
  @n = signal(0)

  def view
    window "Todos", width: 420, height: 560 do
      column gap: :md, pad: :lg do
        text "#{@n} left", role: :caption
        button "More", on_click: -> { @n.value += 1 }
      end
    end
  end
end

Wolkit.run Todos

view runs once. It does not draw: it describes. signal(0) is a value that remembers who read it. text "#{@n} left" reads @n while the window is built, so Wolkit keeps one small effect for that text; clicking the button writes @n, that effect runs, and one SET_TEXT goes to the engine. Nothing else is touched. Spacing and type are names from the design scale (:md, :caption); there is no number of pixels, no colour, no markup.

3. Watch it change while it runs (2 minutes)

Build with -Ddev and the running app listens for changes: a saved .wolcss sheet restyles the window with no restart (under 100 ms), a saved .cr file rebuilds and the new process takes over with the old process's signals put back, and a compile error is shown as a banner in the window that is still running. wol app run --watch drives this (docs/modules/wolkit.md "Developer tools"). Try it now: change "More" to "Add one", save, and the window follows.

4. A text field and a button (2 minutes)

require "wolkit"

class Todos < Wolkit::App
  @draft = signal("")
  @added = signal(0)

  def view
    window "Todos", width: 420, height: 560 do
      column gap: :md, pad: :lg do
        row gap: :sm do
          text_field @draft, placeholder: "What needs doing?", grow: 1, on_submit: -> { add }
          button "Add", primary: true, disabled: @draft.blank?, on_click: -> { add }
        end
        text "#{@added} added", role: :caption
      end
    end
  end

  def add
    @added.value += 1 unless @draft.blank?
    @draft.value = ""
  end
end

Wolkit.run Todos

text_field @draft is two-way: typing writes the signal and clearing the signal clears the field. disabled: @draft.blank? is not a literal, so it is an effect that reads @draft; the button is disabled while the field is empty, and typing sends one small SET_STATE, never the text back. The field has a placeholder, and Wolkit uses it as the accessible name; a part without a name is an error in tests and a warning while developing.

5. A list (3 minutes)

require "wolkit"

class Todos < Wolkit::App
  @items = list(String)
  @draft = signal("")

  def view
    window "Todos", width: 420, height: 560 do
      column gap: :md, pad: :lg do
        row gap: :sm do
          text_field @draft, placeholder: "What needs doing?", grow: 1, on_submit: -> { add }
          button "Add", primary: true, disabled: @draft.blank?, on_click: -> { add }
        end
        each @items, key: ->(t : String) { t } do |todo|
          row { text todo, grow: 1; icon_button :trash, label: "Delete #{todo}", on_click: -> { @items.delete(todo) } }
        end
        text "#{@items.size} left", role: :caption
      end
    end
  end

  def add
    @items << @draft.value.strip unless @draft.blank?
    @draft.value = ""
  end
end

Wolkit.run Todos

list(String) is an array that says what changed. each follows those change records: adding a todo builds one row, deleting one frees exactly that row. There is no diff and no rebuild of the list. icon_button must have label:; leave it out and the compiler stops, pointing at your line: icon_button needs `label:`. Screen readers and tooltips use it. This is the whole app of wolos-crystal/examples/todos.

6. Style it (2 minutes)

Inline arguments are checked at compile time: pad: :lg, bg: :card, radius: :m. A size off the scale, such as gap: 13, is a compile error that lists the scale; a colour must be a theme token, so the app follows dark mode, contrast and the accent colour with no code. For your own classes, write src/styles/app.wolcss:

.done { color: dim; text-decoration: strikethrough; }

make styles compiles it, after the kit look (build/00-kit.wolcss, so the default look keeps its class ids), with wolkit-stylec into build/styles.wsb and src/gen/styles.cr (one constant per class); load the sheet with Wolkit.use_styles("build/styles.wsb") and name a class with class: Wolkit::Styles::DONE. The language, its properties and its errors are in docs/modules/wolkit-style.md.

7. Test it without a display (2 minutes)

require "spec"
require "wolkit/test"
require "./todos"

describe Todos do
  it "adds and deletes a todo" do
    Wolkit::Test.run(Todos) do |t|
      t.type "milk", into: "What needs doing?"
      t.click "Add"
      t.text("1 left").should eq "1 left"
      t.click "Delete milk"
      t.text("0 left").should eq "0 left"
    end
  end
end

Wolkit::Test.run starts the real engine with no display and drives it with pointer and key events; t.a11y_snapshot prints the accessibility tree and t.screenshot returns a PNG that is identical on every machine. make spec runs it; make check only type-checks.

8. Package it (1 minute)

make builds a release binary (crystal build --release); a package recipe for a Crystal app is the same as for any program in docs/modules/wopkg.md: build in the chroot, install the binary and a .desktop file, depend on libwolkit and libwol. Give the app a reverse-DNS id ($WOLKIT_APP_ID or org.wolos.<name>) so the system can show its permissions.

docs/modules/wolkit.md for components, the HIG parts (app_window, toolbar, sidebar, sheet, menu), the navigator, services (notifications, file panels, share, clipboard), resources and timers; docs/modules/wolkit-style.md for the style language.

From the developer documentation, wonderlandos/wolos docs/modules/wolkit-tutorial.md.