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:
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.
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.
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.
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.
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:
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.
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.
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.
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
- Messages, events and shared state
- Write a window manager or launcher
- Permissions and capabilities
- How your app runs
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