DatumHueEnterprise v0.0.24datumhue.com
Contents

Launch other apps

Start other apps from your own, give them the permissions and files they need, read what they print, get values from them, and find out how they end.

Start an app

app.spawn starts a child app and returns its App handle at once. Give it Lua source as code, with a name, or a package you loaded with Dir:load_package as package. code compiles as text only, so a compiled chunk ends the child with a fault of kind compile. A child with a UI opens in a window, at the x, y, width and height you give or filling the window it opens in. With service = true, inline code runs with no window at all.

Apps form a tree, and every app an app starts is that app's child. The app you start with datumhue run is the root of its tree. When you start the workstation on its own, with no command, the root is its own window manager, and the apps you open from it are its children.

Your app manages its children, their children and so on: it hears their events, and the datumhue.wm functions move, focus and stop them. An app it did not start, directly or through its children, is out of its reach, and managing one raises.

A child begins only once every app above it has heard its app_spawned and run its handlers for it. An app hears an event only between its callbacks (How your app runs), so a callback that starts a child and then runs long holds the child back until the callback ends.

This program is a small launcher. It starts a clock in a window on the right, a service that ends itself at once, and a worker it stops after half a second, and logs how each one ends:

launcher.lualua
local token = datumhue.theme.token

-- This app's own window, on the left: a log of what happens to
-- the apps it starts.
local page = datumhue.ui.root:panel({
    width = "100%",
    height = "100%",
    flex_direction = "row",
})
local log = page:panel({
    width = "50%",
    padding = 24,
    gap = 6,
    color = token("surface_raised"),
})
log:label({ text = "Launcher", font_size = 24 })
local function note(text)
    log:label({ text = text })
end

-- Every app this one starts reports its end here.
datumhue.events.on("app_terminated", function(e)
    note(e.app_name .. " ended: " .. e.reason)
end)

-- A clock in a window on the right, allowed to take
-- screenshots.
local clock = [[
local card = datumhue.ui.root:panel({ padding = 24, gap = 6 })
card:label({ text = "Clock", font_size = 24 })
local shown = card:label({ text = "" })
local allowed = datumhue.app.has_permission("screenshot")
card:label({ text = "Screenshots allowed: " .. tostring(allowed) })
datumhue.on_update(function()
    shown.text = string.format("%.1f s", datumhue.time.now())
end)
]]
local w = datumhue.screen.width
datumhue.app.spawn({
    name = "clock",
    code = clock,
    x = w / 2,
    y = 0,
    width = w / 2,
    height = datumhue.screen.height,
    permissions = { screenshot = true },
})
note("started clock")

-- A service app, with no window, that ends itself at once.
datumhue.app.spawn({
    name = "brief",
    code = "datumhue.app.terminate()",
    service = true,
})
note("started brief")

-- A service app this one stops after half a second.
local worker = datumhue.app.spawn({
    name = "worker",
    code = "datumhue.time.every(0.1, function() end)",
    service = true,
})
note("started worker")
datumhue.time.after(0.5, function()
    datumhue.wm.terminate_app(worker)
end)

In this example: datumhue.theme.token datumhue.ui.root UiElement:panel UiElement:label datumhue.events.on datumhue.screen.width datumhue.app.spawn datumhue.screen.height datumhue.time.after datumhue.wm.terminate_app

Run a child inside your layout

Give app.spawn one of your app's elements as host, and the child lives in that element rather than in a window. It fills the element's box, moves, resizes, hides and shows with it, and scrolls with what holds it. A press in it gives it the keys. Tab and the arrow keys go into it at the element's place among your widgets, and out again past its first or last widget. A Back it leaves unhandled goes on to your app, up from the element. Removing the element, or a panel holding it, ends the child.

Only a UI child of your own app takes a host, without x, y, width or height, and one element holds one child at a time. Window managers leave such a child where it is.

lua
local stage = datumhue.ui.root:panel({ width = 400, height = 300 })
local sketch = datumhue.app.spawn({
    name = "sketch",
    code = 'datumhue.ui.root:label({ text = "Hello from inside" })',
    host = stage,
})
sketch:on_end(function(e)
    print(e.reason)
end)

Give a child what it needs

A child starts with no permissions and no files. Hand them over in app.spawn. permissions lists the permissions it may use, as the clock gets the screenshot permission. dirs and files pass handles to what it may read or write, each under the name the child finds it by in datumhue.args.

data_mounts, http_mounts and ingress_mounts pass on the mounts your deployment granted, the same way.

You can pass only what your app holds itself, and a grant only narrows: pass a folder with mode = "r", and the child can read it but not write it.

lua
local photos = datumhue.args.dirs.photos
datumhue.app.spawn({
    package = photos:load_package("viewer"),
    dirs = { pictures = { handle = photos, mode = "r" } },
})

Permissions and capabilities explains how grants flow down the tree.

Read what a child prints

Give a child you start from code an on_print callback, and your app gets the lines it prints. The callback runs at most once a frame, with the lines printed since the last call, one per print call. One call carries at most 1000 lines and 64 KiB; when a child prints more between calls, dropped counts the rest. The lines still go to the workstation's log.

counter.lualua
datumhue.app.spawn({
    name = "counter",
    code = "for i = 1, 3 do print('tick ' .. i) end",
    service = true,
    on_print = function(e)
        for _, line in ipairs(e.lines) do
            print("heard " .. line)
        end
        if e.dropped > 0 then
            print("and " .. e.dropped .. " more")
        end
    end,
})

In this example: datumhue.app.spawn

Your app does not end while it waits for them: on_print keeps it running until the child has ended and its last lines have arrived.

Get values from a child

Printed lines are text. For numbers, strings and tables as they are, the child calls app.post_message, and the on_message callback you gave it gets a copy of each. The callback runs at most once a frame, with every message posted since the last call, in order, and none is left out. Integers and floats stay apart, so 5 and 5.0 arrive as they were posted, and a table arrives as it was when it was posted.

scores.lualua
local code = [[
local score = { name = "Hue", stars = 3 }
datumhue.app.post_message(score)
score.stars = 4
datumhue.app.post_message(score)
datumhue.app.post_message(2.5)
]]
datumhue.app.spawn({
    name = "scorer",
    code = code,
    service = true,
    on_message = function(e)
        for _, message in ipairs(e.messages) do
            if type(message) == "table" then
                local stars = message.stars
                print(message.name .. " has " .. stars .. " stars")
            else
                print("then " .. message)
            end
        end
    end,
})

In this example: datumhue.app.spawn

This app prints Hue has 3 stars, Hue has 4 stars and then 2.5. A message holds booleans, numbers, strings and tables of these, at most 64 KiB of them, and a function or a handle in it raises. So does a message posted while 1000 messages or 1 MiB of them wait for your app.

Every message reaches your app before its App:on_end callback for the child runs, and on_message keeps your app running until the child has ended and its last messages have arrived. A message posted once your app has ended is dropped. on_message works only with code.

Keep a child's data apart

A child you start from Lua source works with your app's data: it opens the same stores, named documents, network messaging topics and presence groups. Start it with storage = "own" to give it a set of its own instead. It cannot reach your app's data, and your app cannot reach its data either. All the children your app starts this way share that one set, and it is still there the next time you start one, so a game a person types in can keep its high score. It also means a program opened from a link can read what an earlier one saved.

Such a child still reaches what every app can reach: local messaging topics, a store opened with storage.open_shared by its UUID, and a document or network topic opened with shared = true.

Use it for code your app did not write, such as a program a person typed in or opened from a link:

lua
local typed = 'print("hello")'
datumhue.app.spawn({
    name = "sketch",
    code = typed,
    service = true,
    storage = "own",
})

The children such a child starts from Lua source share its set, unless it starts them with storage = "own" in turn. A package you start always has data of its own, so storage works only with code.

Hear how an app ends

When an app ends, for any reason, the app_terminated event reaches its parent and every app above that. Its payload says how the app ended, as the launcher's log shows. A child that never started ends this way too, so one handler covers every end.

When a child you started from code crashes, its app_terminated event carries fault: what ended it, Lua's message, the line of the child's code it ended on, and the calls that led there, each with its line, function and arguments. A child started from a package keeps its error in the workstation's log.

lua
datumhue.events.on("app_terminated", function(e)
    if e.fault then
        print(e.app_name, e.fault.line, e.fault.message)
    end
end)
datumhue.app.spawn({
    name = "sketch",
    code = "local x = nil\nprint(x.y)",
    service = true,
})

An app that starts others under a different parent, as a launcher does with parent = datumhue.app.bootstrap(), does not get their events. App:on_end runs a callback when the app on that handle ends, for any app that holds the handle.

A child spawned from the package catalog, with package_ref, starts once its package has arrived. A registry that has not answered within 30 seconds counts as unreachable: the child then starts from a copy saved on this workstation if that copy is the version asked for, and otherwise never starts.

Name a child's code

Lua names the code of a child you start from code after the child's name, as pkg:@local/<name>/init.lua, and every position in its messages starts with that. Give chunk_name to choose the name instead: compile and runtime errors, error positions, the messages pcall returns, tracebacks and the child's fault all use it, exactly as you gave it. It names the code and nothing else: the app keeps its name, its title and its data.

stars.lualua
local code = [[
local stars = { Vega = 25, Deneb = 2600 }
local function distance(name)
    return stars[name] or error("no such star")
end
print(select(2, pcall(distance, "Altair")))
]]
datumhue.app.spawn({
    name = "stars",
    chunk_name = "stars.lua",
    code = code,
    service = true,
})

In this example: datumhue.app.spawn

The child prints stars.lua:3: no such star. A name is 1 to 59 bytes of UTF-8 with no control characters, and chunk_name works only with code: a package names its own files.

Stop a runaway child

A child that loops without pausing runs on, a slice of time each frame, until something ends it. Give it max_steps to end such a loop instead. A step is about one Lua instruction, and the same code takes the same steps on every device, so the limit stops a loop at the same point on a phone as on a fast desktop.

The count starts with each callback, the child's script included, and starts again whenever the callback waits on a call such as time.wait_frames. A callback that runs past the limit without pausing ends the child as crashed, with a fault of kind steps on the line it stopped at. pcall cannot catch it.

lua
local child = datumhue.app.spawn({
    name = "runaway",
    chunk_name = "runaway.lua",
    code = "local n = 0\nwhile true do n = n + 1 end",
    service = true,
    max_steps = 100000,
})
child:on_end(function(e)
    print(e.reason, e.fault.kind, e.fault.line)
    print(e.fault.message)
end)

This app prints crashed, steps and 2, then It ran more than 100000 steps without pausing. The count is checked about every 4096 steps, so a child may run up to that many past its limit. The apps a limited child starts run under its limit too, or under their own when it is lower. max_steps works with a package as well as with code.

Stop an app

app.terminate ends your own app, and wm.terminate_app ends one your app manages. Ending an app also ends every app below it in the tree. datumhue.on_terminate runs a last callback as your own app ends, to save what it must.

See also

Reference used on this page: datumhue.theme.token datumhue.ui.root UiElement:panel UiElement:label datumhue.events.on datumhue.screen.width datumhue.app.spawn datumhue.screen.height datumhue.time.after datumhue.wm.terminate_app App:on_end datumhue.args Dir:load_package datumhue.wm datumhue.app.post_message datumhue.storage.open_shared datumhue.time.wait_frames datumhue.app.terminate datumhue.on_terminate

datumhue.com

(c) 2026 Shuusei AB. Quote excerpts of this manual, with attribution to DatumHue, in your own documentation, tutorials, course materials, reviews, and other writing or video. Republishing it in whole or in substantial part needs written permission: support@datumhue.com.

Victor Mono is licensed under the SIL Open Font License 1.1
Copyright (c) 2024, Rune Bjørnerås (https://github.com/rubjo)

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Try

    ↑↓ selectEnter openTab filter

    Settings

    Theme
    Text
    Print

    Keyboard shortcuts

    / or Ctrl KSearch
    ↑ ↓ EnterPick a result
    TabNext result filter
    j kNext or previous entry
    [ ]Previous or next page
    yCopy a link to the current entry
    ?These shortcuts
    EscClose

    On this page