Write a window manager or launcher
Frame the apps you start in windows of your own design, raise them when pressed, decide what Back does in them, reserve shortcuts, and ask the person using the workstation about permissions.
A window manager is an ordinary app that starts other apps and arranges
them; the workstation's own is one. The functions in datumhue.wm work
on the apps yours manages: the ones it started, and theirs. When you
run a program with datumhue run, it is the root app, and manages
every app it starts.
Frame each app
wm.wrap_app puts an app in a frame and returns the frame, a
UiElement. Its insets reserve room around the app for your
decorations: a title bar on top, a border on the other sides. Build the
decorations on the frame as you build any UI. Wrap each app as the
app_spawned event reports it, unless its has_ui is false: a service
app has no window to frame.
This program frames two apps with a title bar and a Close button, raises a window when it is pressed, minimizes a window on an unhandled Back, and cycles through the windows with Control+Tab:
local wm = datumhue.wm
local token = datumhue.theme.token
local BAR = 36
-- The framed apps, in the order they opened.
local windows = {}
-- Frame each app as it opens: a title bar with a Close button.
datumhue.events.on("app_spawned", function(e)
-- A service app has no window to frame.
if not e.has_ui then
return
end
local app = e.app
local frame = wm.wrap_app(app, {
top = BAR,
left = 2,
right = 2,
bottom = 2,
})
frame.color = token("panel_border")
local bar = frame:panel({
width = "100%",
height = BAR,
flex_direction = "row",
align_items = "center",
justify_content = "space_between",
padding = { left = 12, right = 4 },
})
bar:label({ text = e.app_name })
bar:button({
text = "Close",
on_click = function()
wm.terminate_app(app)
end,
})
-- A Back the app leaves unhandled minimizes its window.
frame:on_cancel(function()
wm.set_app_minimized({ app = app, minimized = true })
end)
table.insert(windows, app)
end)
-- Forget a window when its app ends.
datumhue.events.on("app_terminated", function(e)
for i, app in ipairs(windows) do
if app == e.app then
table.remove(windows, i)
break
end
end
end)
-- Bring a window forward and give it focus.
local function raise(app)
wm.set_app_minimized({ app = app, minimized = false })
wm.set_app_focused(app)
end
-- A press anywhere in a window raises it.
datumhue.events.on("app_frame_pressed", function(e)
raise(e.app)
end)
-- Control+Tab raises the next window in turn.
local turn = 0
wm.grab_input({
chords = { { key = "tab", modifiers = { "control" } } },
on_press = function()
if #windows > 0 then
turn = turn % #windows + 1
raise(windows[turn])
end
end,
})
-- Two apps to manage.
datumhue.app.spawn({
name = "notes",
code = 'datumhue.ui.root:label({ text = "Notes go here" })',
x = 60,
y = 100,
width = 320,
height = 240,
})
datumhue.app.spawn({
name = "paint",
code = [[
local canvas = datumhue.draw.new({ width = 200, height = 150 })
canvas:mount({ width = 200, height = 150 })
canvas:circle({ radius = 60, color = datumhue.color("#e67e80") })
]],
x = 420,
y = 160,
width = 320,
height = 240,
})
In this example: datumhue.wm datumhue.theme.token datumhue.events.on datumhue.wm.wrap_app UiElement.color UiElement:panel UiElement:label UiElement:button datumhue.wm.terminate_app UiElement:on_cancel datumhue.wm.set_app_minimized datumhue.wm.set_app_focused datumhue.wm.grab_input datumhue.app.spawn
wm.set_app_bounds moves and resizes a window, wm.set_app_minimized
hides and restores it, and wm.set_frame_insets changes a frame's
insets, to hide the title bar of a window gone full screen. Removing a
frame hands its app back unframed.
Raise the pressed window
Windows stack with the most recently focused on top.
wm.set_app_focused gives an app focus, which brings its window to the
front. A press anywhere in a frame, on your decorations or in the app,
reaches you as the app_frame_pressed event, so the program answers it
by raising that window.
The root app's own UI is the workstation's base layer: focusing it gives it input without covering the windows it manages.
Decide what Back does
A Back an app leaves unhandled (Escape, gamepad B, a browser's or
phone's back) reaches the frame you wrapped it in. Set
UiElement:on_cancel on the frame to decide what Back means there: the
program minimizes the window; a phone-style manager might show its home
screen. A frame without a handler passes the request on to your own UI,
and from there toward the root.
wm.send_cancel delivers a Back to an app as if the person had pressed
it, for a Back button in your title bar. Handle Back
covers Back inside an app.
Reserve shortcuts
wm.grab_input reserves key chords and gamepad buttons for your window
manager: a grabbed press, made while your app or one it manages has
focus, goes to your on_press and to no app. The program's Control+Tab
raises the next window on each press. A switcher that shows a list
instead steps its selection in on_press and switches in on_release,
which runs once as Control comes up. wm.GrabOptions says which keys
and buttons you can grab.
Ask about permissions
When an app asks for a permission it does not hold, with
app.request_permission or by a manifest that requires it, its parent
gets the app_permission_request event. Its request is a
PermissionRequest: answer with PermissionRequest:grant or
PermissionRequest:deny, now or once the person has chosen. The asking
app waits until you answer, and a request you drop unanswered is
denied. When the parent has no handler for the event, the request is
denied at once.
Describe the permission with app.permission_summary: one plain
sentence about what it allows, in your app's language, the same words
the workstation's own prompts use.
datumhue.events.on("app_permission_request", function(e)
local request = e.request
local card = datumhue.ui.root:panel({ padding = 16, gap = 8 })
card:label({ text = datumhue.app.permission_summary(e.permission) })
card:button({
text = "Allow",
on_click = function()
request:grant()
card:remove()
end,
})
card:button({
text = "Don't allow",
on_click = function()
request:deny()
card:remove()
end,
})
end)
Apps you have not framed
An app you start but do not frame opens in the window of the nearest
app above it that has one, or at the top level when no app but the root
is above it. app.spawn describes exactly where. It moves, hides and
stacks with that window. Wrapping it later lifts it into a frame of its
own.
An app started with a host lives in an element of its parent instead
(Launch other apps).
Its app_spawned reaches you as any app's does, but wm.wrap_app,
wm.set_app_bounds and wm.set_app_minimized raise for it: the element
places and shows it.
Recover from a failed start
When the workstation starts with errors, it launches @datumhue/fallback
as the root app in place of the usual one, from the namespace the
workstation starts from. The fallback reads the errors with app.errors
and holds the root app's powers, so it can show what went wrong and start
what still works.
See also
Reference used on this page: datumhue.wm datumhue.theme.token datumhue.events.on datumhue.wm.wrap_app UiElement.color UiElement:panel UiElement:label UiElement:button datumhue.wm.terminate_app UiElement:on_cancel datumhue.wm.set_app_minimized datumhue.wm.set_app_focused datumhue.wm.grab_input datumhue.app.spawn datumhue.ui.root datumhue.app.permission_summary UiElement:remove PermissionRequest:deny datumhue.wm.set_app_bounds datumhue.wm.set_frame_insets datumhue.wm.send_cancel wm.GrabOptions datumhue.app.request_permission PermissionRequest:grant datumhue.app.errors