feat(k-ui): first slice: App trait + run loop, builders (column/row/text/button), flex layout, theme tokens (Whimsy Dark), tiny-skia + fontdue render (Noto Sans Latin subset), keyboard focus ring; tally demo; manifest memory key (pm spawns with it)

This commit is contained in:
2026-10-28 03:20:41 +01:00
parent c256f4ecf3
commit fe73b89525
27 changed files with 1428 additions and 6 deletions

View File

@@ -0,0 +1,13 @@
[package]
name = "tally"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "tally"
test = false
bench = false
[dependencies]
libkos = { workspace = true }
k-ui = { workspace = true }

View File

@@ -0,0 +1,10 @@
[app]
id = "kos.tally"
name = "Tally"
version = "1.0.0"
binary = "tally"
author = "K-OS"
memory = "3 MiB"
[capabilities]
required = ["display"]

View File

@@ -0,0 +1,61 @@
//! A count, and buttons to change it: the first K-UI app. Tab and the
//! arrows move between the buttons, Enter presses, Escape leaves.
#![no_std]
#![forbid(unsafe_code)]
#![no_main]
extern crate alloc;
extern crate libkos;
use alloc::format;
use k_ui::{App, ButtonVariant, Element, Spacing, TextColour, TextStyle, button, column, row, text};
#[derive(Clone, Copy)]
enum Event {
Add,
Subtract,
Reset,
}
struct Tally(i64);
impl App for Tally {
type Event = Event;
fn init() -> Self {
Tally(0)
}
fn update(&mut self, event: Event) {
match event {
Event::Add => self.0 += 1,
Event::Subtract => self.0 -= 1,
Event::Reset => self.0 = 0,
}
}
fn view(&self) -> Element<Event> {
let mut buttons = row()
.gap(Spacing::Md)
.child(button("Subtract", Event::Subtract))
.child(button("Add", Event::Add).variant(ButtonVariant::Primary));
if self.0 != 0 {
buttons = buttons.child(button("Reset", Event::Reset).variant(ButtonVariant::Destructive));
}
column()
.padding(Spacing::Xl)
.gap(Spacing::Lg)
.child(text("Tally").style(TextStyle::Heading))
.child(text(format!("{}", self.0)).style(TextStyle::Subheading).colour(TextColour::Accent))
.child(buttons)
.child(text("Tab moves, Enter presses, Esc leaves").style(TextStyle::Caption).colour(TextColour::Secondary))
.into()
}
}
libkos::entry!(main);
fn main() {
k_ui::run::<Tally>();
}

View File

@@ -138,7 +138,7 @@ pub const SESSION: &[Program] = &[Program {
uses: &["terminal", "display"],
storage: &[users(b"", true), disk(b"apps", false)],
// Room for a few apps at a time.
spawns: Some(Limits { pages: 512, endpoints: 8, notifications: 8, regions: 8 }),
spawns: Some(Limits { pages: 2048, endpoints: 8, notifications: 8, regions: 8 }),
// The shell holds handles to the apps it ran, which die with it.
restarts_clients: true,
// Installing waits for the user's consent.

12
crates/k-ui/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "k-ui"
version = "0.1.0"
edition = "2024"
[dependencies]
types = { workspace = true }
libkos = { workspace = true }
port-framebuffer = { workspace = true }
tiny-skia = { version = "0.11", default-features = false, features = ["no-std-float"] }
fontdue = { version = "0.9", default-features = false, features = ["hashbrown"] }
libm = "0.2"

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Binary file not shown.

Binary file not shown.

158
crates/k-ui/src/element.rs Normal file
View File

@@ -0,0 +1,158 @@
//! What a view is made of, built with typed builders: `column()`, `row()`,
//! `text(..)`, `button(..)`. Each only offers what makes sense for it
//! (a text has a style, a button a variant); appearance comes from the
//! theme, never from here.
use alloc::string::String;
use alloc::vec::Vec;
use crate::theme::{ButtonVariant, Spacing, TextColour, TextStyle};
/// How much of its parent's main axis an element takes.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Size {
/// What its content needs.
Shrink,
/// A share of what is left.
Fill,
/// Exactly this many pixels: use sparingly.
Fixed(f32),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Axis {
Horizontal,
Vertical,
}
pub struct Flex<E> {
pub axis: Axis,
pub gap: Spacing,
pub padding: Spacing,
pub children: Vec<Element<E>>,
}
pub struct Text {
pub content: String,
pub style: TextStyle,
pub colour: TextColour,
}
pub struct Button<E> {
pub label: String,
pub variant: ButtonVariant,
pub on_press: E,
}
pub enum Kind<E> {
Flex(Flex<E>),
Text(Text),
Button(Button<E>),
}
pub struct Element<E> {
pub kind: Kind<E>,
pub size: Size,
}
/// Children stacked top to bottom.
pub fn column<E>() -> FlexBuilder<E> {
FlexBuilder(Flex { axis: Axis::Vertical, gap: Spacing::None, padding: Spacing::None, children: Vec::new() }, Size::Shrink)
}
/// Children side by side.
pub fn row<E>() -> FlexBuilder<E> {
FlexBuilder(Flex { axis: Axis::Horizontal, gap: Spacing::None, padding: Spacing::None, children: Vec::new() }, Size::Shrink)
}
pub fn text(content: impl Into<String>) -> TextBuilder {
TextBuilder(Text { content: content.into(), style: TextStyle::Body, colour: TextColour::Primary }, Size::Shrink)
}
/// A button labelled `label` that sends `on_press` when pressed.
pub fn button<E>(label: impl Into<String>, on_press: E) -> ButtonBuilder<E> {
ButtonBuilder(Button { label: label.into(), variant: ButtonVariant::Secondary, on_press }, Size::Shrink)
}
pub struct FlexBuilder<E>(Flex<E>, Size);
pub struct TextBuilder(Text, Size);
pub struct ButtonBuilder<E>(Button<E>, Size);
macro_rules! sizing {
() => {
pub fn fill(mut self) -> Self {
self.1 = Size::Fill;
self
}
pub fn fixed(mut self, px: f32) -> Self {
self.1 = Size::Fixed(px);
self
}
};
}
impl<E> FlexBuilder<E> {
sizing!();
pub fn gap(mut self, s: Spacing) -> Self {
self.0.gap = s;
self
}
pub fn padding(mut self, s: Spacing) -> Self {
self.0.padding = s;
self
}
pub fn child(mut self, c: impl Into<Element<E>>) -> Self {
self.0.children.push(c.into());
self
}
pub fn children<C: Into<Element<E>>>(mut self, cs: impl IntoIterator<Item = C>) -> Self {
self.0.children.extend(cs.into_iter().map(Into::into));
self
}
}
impl TextBuilder {
sizing!();
pub fn style(mut self, s: TextStyle) -> Self {
self.0.style = s;
self
}
pub fn colour(mut self, c: TextColour) -> Self {
self.0.colour = c;
self
}
}
impl<E> ButtonBuilder<E> {
sizing!();
pub fn variant(mut self, v: ButtonVariant) -> Self {
self.0.variant = v;
self
}
}
impl<E> From<FlexBuilder<E>> for Element<E> {
fn from(b: FlexBuilder<E>) -> Self {
Element { kind: Kind::Flex(b.0), size: b.1 }
}
}
impl<E> From<TextBuilder> for Element<E> {
fn from(b: TextBuilder) -> Self {
Element { kind: Kind::Text(b.0), size: b.1 }
}
}
impl<E> From<ButtonBuilder<E>> for Element<E> {
fn from(b: ButtonBuilder<E>) -> Self {
Element { kind: Kind::Button(b.0), size: b.1 }
}
}

118
crates/k-ui/src/layout.rs Normal file
View File

@@ -0,0 +1,118 @@
//! Where everything goes. A tree is laid out into one rectangle per
//! element, in pre-order (an element, then its children), which is the
//! order the renderer and focus walk it in too.
//!
//! Along a container's main axis children take what their content needs
//! (`Shrink`), exactly their `Fixed` size, or a share of what is left
//! (`Fill`). Across it, containers stretch to fill it and everything else
//! keeps what it needs, from the start.
use alloc::vec::Vec;
use crate::element::{Axis, Element, Kind, Size};
use crate::theme::{Spacing, TextStyle, Theme};
/// The smallest a pressable thing may be, either way (touch targets).
pub const MIN_TARGET: f32 = 44.0;
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct Rect {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
/// How big text is, from whoever has the fonts.
pub trait Measure {
fn text(&self, s: &str, style: TextStyle) -> (f32, f32);
}
/// A button's room around its label.
pub fn button_padding(theme: &Theme) -> (f32, f32) {
(theme.space(Spacing::Lg), theme.space(Spacing::Sm))
}
/// What `e` needs, width and height.
pub fn natural<E>(e: &Element<E>, m: &impl Measure, t: &Theme) -> (f32, f32) {
match &e.kind {
Kind::Text(text) => m.text(&text.content, text.style),
Kind::Button(b) => {
let (w, h) = m.text(&b.label, TextStyle::Label);
let (px, py) = button_padding(t);
((w + 2.0 * px).max(MIN_TARGET), (h + 2.0 * py).max(MIN_TARGET))
}
Kind::Flex(f) => {
let pad = 2.0 * t.space(f.padding);
let gaps = t.space(f.gap) * f.children.len().saturating_sub(1) as f32;
let (mut main, mut cross) = (0.0f32, 0.0f32);
for c in &f.children {
let (w, h) = natural(c, m, t);
let (cm, cc) = along(f.axis, w, h);
main += match c.size {
Size::Fixed(px) => px,
_ => cm,
};
cross = cross.max(cc);
}
let (w, h) = across(f.axis, main + gaps, cross);
(w + pad, h + pad)
}
}
}
/// (main, cross) of a (width, height) along `axis`.
fn along(axis: Axis, w: f32, h: f32) -> (f32, f32) {
match axis {
Axis::Horizontal => (w, h),
Axis::Vertical => (h, w),
}
}
/// (width, height) of a (main, cross) along `axis`.
fn across(axis: Axis, main: f32, cross: f32) -> (f32, f32) {
along(axis, main, cross)
}
/// Every element's rectangle, in pre-order, the root filling `area`.
pub fn layout<E>(root: &Element<E>, area: Rect, m: &impl Measure, t: &Theme) -> Vec<Rect> {
let mut out = Vec::new();
place(root, area, m, t, &mut out);
out
}
fn place<E>(e: &Element<E>, r: Rect, m: &impl Measure, t: &Theme, out: &mut Vec<Rect>) {
out.push(r);
let Kind::Flex(f) = &e.kind else { return };
let pad = t.space(f.padding);
let inner = Rect { x: r.x + pad, y: r.y + pad, w: (r.w - 2.0 * pad).max(0.0), h: (r.h - 2.0 * pad).max(0.0) };
let (inner_main, inner_cross) = along(f.axis, inner.w, inner.h);
let gap = t.space(f.gap);
let naturals: Vec<(f32, f32)> = f.children.iter().map(|c| natural(c, m, t)).map(|(w, h)| along(f.axis, w, h)).collect();
let taken: f32 = f
.children
.iter()
.zip(&naturals)
.map(|(c, (main, _))| match c.size {
Size::Fixed(px) => px,
Size::Fill => 0.0,
Size::Shrink => *main,
})
.sum::<f32>()
+ gap * f.children.len().saturating_sub(1) as f32;
let fills = f.children.iter().filter(|c| c.size == Size::Fill).count();
let share = if fills > 0 { (inner_main - taken).max(0.0) / fills as f32 } else { 0.0 };
let mut at = 0.0;
for (c, (main, cross)) in f.children.iter().zip(&naturals) {
let main = match c.size {
Size::Fixed(px) => px,
Size::Fill => share,
Size::Shrink => *main,
};
let cross = if matches!(c.kind, Kind::Flex(_)) { inner_cross } else { cross.min(inner_cross) };
let (dx, dy) = across(f.axis, at, 0.0);
let (w, h) = across(f.axis, main, cross);
place(c, Rect { x: inner.x + dx, y: inner.y + dy, w, h }, m, t, out);
at += main + gap;
}
}

25
crates/k-ui/src/lib.rs Normal file
View File

@@ -0,0 +1,25 @@
//! K-UI (see the vision's K-UI note): an app says what its UI is, as a
//! pure function of its state; K-UI lays it out, draws it in the theme,
//! and handles focus and keys. It runs inside the app and draws into the
//! app's window.
#![no_std]
#![forbid(unsafe_code)]
extern crate alloc;
pub mod element;
pub mod layout;
pub mod render;
pub mod run;
pub mod text;
pub mod theme;
pub mod ui;
pub use element::{Element, Size, button, column, row, text};
pub use run::run;
pub use theme::{ButtonVariant, Spacing, TextColour, TextStyle};
pub use ui::App;
#[cfg(test)]
mod tests;

131
crates/k-ui/src/render.rs Normal file
View File

@@ -0,0 +1,131 @@
//! Drawing a laid-out tree into a window's pixels (RGBA, premultiplied):
//! the theme decides every colour and shape.
use tiny_skia::{Color, FillRule, Paint, PathBuilder, PixmapMut, Stroke, Transform};
use crate::element::{Element, Kind};
use crate::layout::Rect;
use crate::text::Fonts;
use crate::theme::{Rgba, TextStyle, Theme, Weight};
/// The focus ring's distance outside the button, and its width.
const RING_GAP: f32 = 3.0;
const RING_WIDTH: f32 = 2.0;
fn paint(c: Rgba) -> Paint<'static> {
let mut p = Paint::default();
p.set_color(Color::from_rgba8(c.0, c.1, c.2, c.3));
p.anti_alias = true;
p
}
/// A rectangle with corners of radius `r` (at most half its height).
fn rounded(rect: Rect, r: f32) -> Option<tiny_skia::Path> {
let r = r.min(rect.h / 2.0).min(rect.w / 2.0).max(0.0);
let (x0, y0, x1, y1) = (rect.x, rect.y, rect.x + rect.w, rect.y + rect.h);
let mut p = PathBuilder::new();
p.move_to(x0 + r, y0);
p.line_to(x1 - r, y0);
p.quad_to(x1, y0, x1, y0 + r);
p.line_to(x1, y1 - r);
p.quad_to(x1, y1, x1 - r, y1);
p.line_to(x0 + r, y1);
p.quad_to(x0, y1, x0, y1 - r);
p.line_to(x0, y0 + r);
p.quad_to(x0, y0, x0 + r, y0);
p.close();
p.finish()
}
/// What drawing needs at hand: the pixels, the fonts, the theme, and which
/// element (pre-order) has focus.
struct Painter<'p, 'a> {
pm: &'p mut PixmapMut<'a>,
fonts: &'p Fonts,
theme: &'p Theme,
focused: Option<usize>,
}
impl Painter<'_, '_> {
/// Glyph coverage blended over the pixels in colour `c`, the pen from
/// `x` on `baseline`.
fn glyphs(&mut self, s: &str, (size, weight): (f32, Weight), x: f32, baseline: f32, c: Rgba) {
let (pw, ph) = (self.pm.width() as i32, self.pm.height() as i32);
let data = self.pm.data_mut();
self.fonts.glyphs(s, size, weight, |pen, g| {
let m = &g.metrics;
let gx = libm::roundf(x + pen) as i32 + m.xmin;
let gy = libm::roundf(baseline) as i32 - m.height as i32 - m.ymin;
for row in 0..m.height as i32 {
let y = gy + row;
if !(0..ph).contains(&y) {
continue;
}
for col in 0..m.width as i32 {
let x = gx + col;
if !(0..pw).contains(&x) {
continue;
}
let cov = g.coverage[(row * m.width as i32 + col) as usize] as u32 * c.3 as u32 / 255;
if cov == 0 {
continue;
}
let at = ((y * pw + x) * 4) as usize;
let px = &mut data[at..at + 4];
let inv = 255 - cov;
for (i, src) in [c.0, c.1, c.2].into_iter().enumerate() {
px[i] = ((src as u32 * cov + px[i] as u32 * inv) / 255) as u8;
}
px[3] = (cov + px[3] as u32 * inv / 255) as u8;
}
}
});
}
/// Text `s` centred vertically in `r`, from its left (or centred
/// across it too).
fn text_in(&mut self, s: &str, style: TextStyle, r: Rect, centre: bool, c: Rgba) {
let (size, weight) = self.theme.text(style);
let (w, h, ascent) = self.fonts.line(s, size, weight);
let x = if centre { r.x + (r.w - w) / 2.0 } else { r.x };
let baseline = r.y + (r.h - h) / 2.0 + ascent;
self.glyphs(s, (size, weight), x, baseline, c);
}
fn draw<E>(&mut self, e: &Element<E>, rects: &[Rect], index: &mut usize) {
let me = *index;
*index += 1;
let Some(&r) = rects.get(me) else { return };
let theme = self.theme;
match &e.kind {
Kind::Flex(f) => {
for c in &f.children {
self.draw(c, rects, index);
}
}
Kind::Text(t) => self.text_in(&t.content, t.style, r, false, theme.text_colour(t.colour)),
Kind::Button(b) => {
let (fill, label) = theme.button(b.variant);
if let (Some(fill), Some(path)) = (fill, rounded(r, theme.corner_radius)) {
self.pm.fill_path(&path, &paint(fill), FillRule::Winding, Transform::identity(), None);
}
self.text_in(&b.label, TextStyle::Label, r, true, label);
if self.focused == Some(me) {
let ring = Rect { x: r.x - RING_GAP, y: r.y - RING_GAP, w: r.w + 2.0 * RING_GAP, h: r.h + 2.0 * RING_GAP };
if let Some(path) = rounded(ring, theme.corner_radius + RING_GAP) {
let stroke = Stroke { width: RING_WIDTH, ..Stroke::default() };
self.pm.stroke_path(&path, &paint(theme.focus), &stroke, Transform::identity(), None);
}
}
}
}
}
}
/// Draws `root`, laid out as `rects`, with `focused` (a pre-order index)
/// ringed.
pub fn render<E>(pm: &mut PixmapMut, root: &Element<E>, rects: &[Rect], fonts: &Fonts, theme: &Theme, focused: Option<usize>) {
let bg = theme.background;
pm.fill(Color::from_rgba8(bg.0, bg.1, bg.2, bg.3));
Painter { pm, fonts, theme, focused }.draw(root, rects, &mut 0);
}

65
crates/k-ui/src/run.rs Normal file
View File

@@ -0,0 +1,65 @@
//! An app's event loop on K-OS: its window (which the permission manager
//! opened, named from its manifest), keys from the display while it is in
//! front, a redraw after every change. Escape leaves.
use libkos::display::{self, Canvas};
use libkos::input::{InputEvent, Key as Pressed};
use libkos::syscall;
use port_framebuffer::Framebuffer;
use tiny_skia::PixmapMut;
use types::display::ScreenCoord;
use crate::text::Fonts;
use crate::theme::Theme;
use crate::ui::{App, Key, Ui};
fn key(k: Pressed) -> Option<Key> {
match k {
Pressed::Tab | Pressed::Down | Pressed::Right => Some(Key::Next),
Pressed::Up | Pressed::Left => Some(Key::Previous),
Pressed::Enter | Pressed::Space => Some(Key::Press),
_ => None,
}
}
/// Runs `A` until Escape (or until it is stopped).
pub fn run<A: App>() -> ! {
let window = libkos::start::display();
let Some(mut canvas) = window.and_then(Canvas::attach) else {
libkos::println!("k-ui: no window (does the manifest ask for the display?)");
syscall::exit();
};
let (Ok(notify), Some(fonts)) = (syscall::create_notification(), Fonts::load()) else {
libkos::println!("k-ui: cannot start");
syscall::exit();
};
if !display::subscribe(canvas.window(), notify, types::badge::Badge::new(1)) {
libkos::println!("k-ui: no input");
syscall::exit();
}
let mut ui = Ui::<A>::new(&Theme::WHIMSY_DARK);
let mut dirty = true;
loop {
if dirty {
draw(&ui, &mut canvas, &fonts);
dirty = false;
}
let _ = syscall::wait(notify);
while let Some(event) = display::poll_input(canvas.window()) {
match event {
InputEvent::KeyDown(Pressed::Escape) => syscall::exit(),
InputEvent::KeyDown(k) => dirty |= key(k).is_some_and(|k| ui.key(k)),
InputEvent::KeyUp(_) => {}
}
}
}
}
fn draw<A: App>(ui: &Ui<A>, canvas: &mut Canvas, fonts: &Fonts) {
let (w, h) = (canvas.width(), canvas.height());
if let Some(mut pm) = PixmapMut::from_bytes(canvas.pixels_mut(), w.raw() as u32, h.raw() as u32) {
ui.draw(&mut pm, fonts);
}
canvas.touch(ScreenCoord::new(0), ScreenCoord::new(0), w, h);
canvas.flush();
}

124
crates/k-ui/src/tests.rs Normal file
View File

@@ -0,0 +1,124 @@
extern crate std;
use alloc::format;
use alloc::vec::Vec;
use crate::element::{Element, button, column, row, text};
use crate::layout::{MIN_TARGET, Measure, Rect, layout};
use crate::text::Fonts;
use crate::theme::{ButtonVariant, Spacing, TextStyle, Theme};
use crate::ui::{App, Key, Ui};
/// Every character 10 wide, every line 20 high.
struct Fixed;
impl Measure for Fixed {
fn text(&self, s: &str, _: TextStyle) -> (f32, f32) {
(10.0 * s.len() as f32, 20.0)
}
}
const T: &Theme = &Theme::WHIMSY_DARK;
const AREA: Rect = Rect { x: 0.0, y: 0.0, w: 400.0, h: 300.0 };
#[test]
fn a_column_stacks_with_gaps_and_padding() {
let view: Element<()> = column().padding(Spacing::Md).gap(Spacing::Sm).child(text("ab")).child(text("abcd")).into();
let r = layout(&view, AREA, &Fixed, T);
// Padding Md is 16, gap Sm 8.
assert_eq!(r[1], Rect { x: 16.0, y: 16.0, w: 20.0, h: 20.0 });
assert_eq!(r[2], Rect { x: 16.0, y: 44.0, w: 40.0, h: 20.0 });
}
#[test]
fn fill_shares_what_is_left_and_fixed_is_exact() {
let view: Element<()> = row().child(text("abc")).child(text("x").fill()).child(text("y").fixed(50.0)).child(text("z").fill()).into();
let r = layout(&view, AREA, &Fixed, T);
// 400 - 30 - 50 = 320, two fills: 160 each.
let widths: Vec<f32> = r[1..].iter().map(|r| r.w).collect();
assert_eq!(widths, [30.0, 160.0, 50.0, 160.0]);
assert_eq!(r[4].x, 240.0);
}
#[test]
fn buttons_are_at_least_a_touch_target_and_containers_stretch_across() {
let view: Element<()> = column().child(row().child(button("+", ()))).into();
let r = layout(&view, AREA, &Fixed, T);
assert_eq!(r[1].w, 400.0, "the row stretches across the column");
assert!(r[2].w >= MIN_TARGET && r[2].h >= MIN_TARGET);
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum Event {
Add,
Reset,
}
struct Counter(u32);
impl App for Counter {
type Event = Event;
fn init() -> Self {
Counter(0)
}
fn update(&mut self, e: Event) {
match e {
Event::Add => self.0 += 1,
Event::Reset => self.0 = 0,
}
}
fn view(&self) -> Element<Event> {
let mut c = column().padding(Spacing::Md).gap(Spacing::Md).child(text(format!("{}", self.0)).style(TextStyle::Heading));
c = c.child(button("Add", Event::Add).variant(ButtonVariant::Primary));
if self.0 > 0 {
c = c.child(button("Reset", Event::Reset).variant(ButtonVariant::Destructive));
}
c.into()
}
}
#[test]
fn keys_move_focus_and_press_the_focused_button() {
let mut ui = Ui::<Counter>::new(T);
assert!(ui.key(Key::Press), "Add has focus first");
assert!(ui.key(Key::Press));
assert_eq!(ui.app.0, 2);
assert!(ui.key(Key::Next));
assert!(ui.key(Key::Press), "Reset");
assert_eq!(ui.app.0, 0);
// Reset is gone, so focus is back on Add.
assert!(ui.key(Key::Press));
assert_eq!(ui.app.0, 1);
assert!(ui.key(Key::Previous));
assert!(ui.key(Key::Press));
assert_eq!(ui.app.0, 0, "Previous wraps round to Reset");
}
/// The straight colour at (x, y) of a premultiplied pixmap, roughly.
fn at(pm: &tiny_skia::Pixmap, x: u32, y: u32) -> (u8, u8, u8) {
let p = pm.pixel(x, y).unwrap().demultiply();
(p.red(), p.green(), p.blue())
}
#[test]
fn the_view_is_drawn_in_the_theme_with_a_focus_ring() {
let fonts = Fonts::load().expect("the bundled fonts parse");
let ui = Ui::<Counter>::new(T);
let mut pm = tiny_skia::Pixmap::new(320, 200).unwrap();
ui.draw(&mut pm.as_mut(), &fonts);
let bg = T.background;
assert_eq!(at(&pm, 319, 199), (bg.0, bg.1, bg.2), "the background is the theme's");
// Lay it out the same way to find the button.
let view = ui.app.view();
let rects = layout(&view, Rect { x: 0.0, y: 0.0, w: 320.0, h: 200.0 }, &crate::text::Measurer { fonts: &fonts, theme: T }, T);
let b = rects[2];
let accent = T.accent;
assert_eq!(at(&pm, (b.x + 4.0) as u32, (b.y + b.h / 2.0) as u32), (accent.0, accent.1, accent.2), "a primary button is the accent");
let focus = T.focus;
assert_eq!(at(&pm, (b.x + b.w / 2.0) as u32, (b.y - 3.0) as u32), (focus.0, focus.1, focus.2), "the focus ring sits around it");
// The heading's text is drawn: something brighter than the background
// in its rectangle.
let h = rects[1];
let lit = (h.x as u32..(h.x + h.w) as u32).any(|x| (h.y as u32..(h.y + h.h) as u32).any(|y| at(&pm, x, y).0 > 150));
assert!(lit, "the count is drawn");
}

77
crates/k-ui/src/text.rs Normal file
View File

@@ -0,0 +1,77 @@
//! Fonts: Noto Sans (a Latin subset, see fonts/LICENSE-noto), regular and
//! semibold, rasterised by fontdue; glyphs are kept once made.
use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use core::cell::RefCell;
use fontdue::{Font, FontSettings, Metrics};
use crate::layout::Measure;
use crate::theme::{TextStyle, Theme, Weight};
static REGULAR: &[u8] = include_bytes!("../fonts/NotoSans-Regular-Latin.ttf");
static SEMIBOLD: &[u8] = include_bytes!("../fonts/NotoSans-SemiBold-Latin.ttf");
/// A glyph's coverage, a byte per pixel, and where it sits.
pub struct Glyph {
pub metrics: Metrics,
pub coverage: Vec<u8>,
}
pub struct Fonts {
regular: Font,
semibold: Font,
glyphs: RefCell<BTreeMap<(char, u32, bool), Glyph>>,
}
impl Fonts {
pub fn load() -> Option<Self> {
let font = |bytes| Font::from_bytes(bytes, FontSettings::default()).ok();
Some(Self { regular: font(REGULAR)?, semibold: font(SEMIBOLD)?, glyphs: RefCell::new(BTreeMap::new()) })
}
fn font(&self, w: Weight) -> &Font {
match w {
Weight::Regular => &self.regular,
Weight::SemiBold => &self.semibold,
}
}
/// The text's width, and a line's height and ascent, at `size` pixels.
pub fn line(&self, s: &str, size: f32, w: Weight) -> (f32, f32, f32) {
let font = self.font(w);
let width = s.chars().map(|c| font.metrics(c, size).advance_width).sum();
let (height, ascent) = font.horizontal_line_metrics(size).map_or((size * 1.3, size), |m| (m.new_line_size, m.ascent));
(width, height, ascent)
}
/// Calls `draw` with each glyph of `s` and the pen's x for it.
pub fn glyphs(&self, s: &str, size: f32, w: Weight, mut draw: impl FnMut(f32, &Glyph)) {
let font = self.font(w);
let mut glyphs = self.glyphs.borrow_mut();
let mut x = 0.0;
for c in s.chars() {
let key = (c, size.to_bits(), w == Weight::SemiBold);
let g = glyphs.entry(key).or_insert_with(|| {
let (metrics, coverage) = font.rasterize(c, size);
Glyph { metrics, coverage }
});
draw(x, g);
x += g.metrics.advance_width;
}
}
}
/// Text measured in the theme's styles.
pub struct Measurer<'a> {
pub fonts: &'a Fonts,
pub theme: &'a Theme,
}
impl Measure for Measurer<'_> {
fn text(&self, s: &str, style: TextStyle) -> (f32, f32) {
let (size, weight) = self.theme.text(style);
let (w, h, _) = self.fonts.line(s, size, weight);
(w, h)
}
}

130
crates/k-ui/src/theme.rs Normal file
View File

@@ -0,0 +1,130 @@
//! The theme's tokens (see the vision's K-UI note): apps say what a thing
//! is (a primary button, a heading), never its colour or size. One theme
//! for now, built in; loading themes from TOML comes later.
/// A colour as the theme names it: straight (not premultiplied) RGBA.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Rgba(pub u8, pub u8, pub u8, pub u8);
impl Rgba {
pub const fn opaque(r: u8, g: u8, b: u8) -> Self {
Self(r, g, b, 255)
}
}
/// Steps of the spacing scale.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Spacing {
None,
Xs,
Sm,
Md,
Lg,
Xl,
}
/// Steps of the type scale.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TextStyle {
Heading,
Subheading,
Body,
Caption,
Label,
}
/// What a text is, for its colour.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TextColour {
Primary,
Secondary,
Accent,
OnAccent,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ButtonVariant {
Primary,
Secondary,
Ghost,
Destructive,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Weight {
Regular,
SemiBold,
}
pub struct Theme {
pub background: Rgba,
pub surface: Rgba,
pub surface_raised: Rgba,
pub accent: Rgba,
pub on_accent: Rgba,
pub text_primary: Rgba,
pub text_secondary: Rgba,
pub destructive: Rgba,
pub focus: Rgba,
pub corner_radius: f32,
/// The spacing scale's unit, in pixels.
pub spacing_base: f32,
}
impl Theme {
pub const WHIMSY_DARK: Theme = Theme {
background: Rgba::opaque(0x0f, 0x0e, 0x17),
surface: Rgba::opaque(0x1a, 0x1a, 0x2e),
surface_raised: Rgba::opaque(0x2a, 0x2a, 0x44),
accent: Rgba::opaque(0x4f, 0xc3, 0xf7),
on_accent: Rgba::opaque(0x0f, 0x0e, 0x17),
text_primary: Rgba::opaque(0xe0, 0xe0, 0xe0),
text_secondary: Rgba::opaque(0x9e, 0x9e, 0xb8),
destructive: Rgba::opaque(0xef, 0x53, 0x50),
focus: Rgba::opaque(0xff, 0xff, 0xff),
corner_radius: 16.0,
spacing_base: 4.0,
};
pub fn space(&self, s: Spacing) -> f32 {
self.spacing_base
* match s {
Spacing::None => 0.0,
Spacing::Xs => 1.0,
Spacing::Sm => 2.0,
Spacing::Md => 4.0,
Spacing::Lg => 6.0,
Spacing::Xl => 8.0,
}
}
/// A text style's size in pixels and its weight.
pub fn text(&self, s: TextStyle) -> (f32, Weight) {
match s {
TextStyle::Heading => (32.0, Weight::SemiBold),
TextStyle::Subheading => (22.0, Weight::SemiBold),
TextStyle::Body => (17.0, Weight::Regular),
TextStyle::Caption => (13.0, Weight::Regular),
TextStyle::Label => (17.0, Weight::SemiBold),
}
}
pub fn text_colour(&self, c: TextColour) -> Rgba {
match c {
TextColour::Primary => self.text_primary,
TextColour::Secondary => self.text_secondary,
TextColour::Accent => self.accent,
TextColour::OnAccent => self.on_accent,
}
}
/// A button's fill (none for a ghost) and its label's colour.
pub fn button(&self, v: ButtonVariant) -> (Option<Rgba>, Rgba) {
match v {
ButtonVariant::Primary => (Some(self.accent), self.on_accent),
ButtonVariant::Secondary => (Some(self.surface_raised), self.text_primary),
ButtonVariant::Ghost => (None, self.accent),
ButtonVariant::Destructive => (Some(self.destructive), self.text_primary),
}
}
}

107
crates/k-ui/src/ui.rs Normal file
View File

@@ -0,0 +1,107 @@
//! An app and what K-UI keeps about it: which pressable thing has focus.
//! Keys move focus (Tab, the arrows) and press (Enter, Space); a press is
//! the button's event, handed to the app. Pure, apart from the pixels it
//! draws into.
use alloc::vec::Vec;
use tiny_skia::PixmapMut;
use crate::element::{Element, Kind};
use crate::layout::{self, Rect};
use crate::render;
use crate::text::{Fonts, Measurer};
use crate::theme::Theme;
/// Every K-UI app.
pub trait App {
type Event: Clone;
fn init() -> Self;
/// Changes the state; the view follows.
fn update(&mut self, event: Self::Event);
/// What the state looks like: a pure function of it.
fn view(&self) -> Element<Self::Event>;
}
/// A key, as far as K-UI cares.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Key {
Next,
Previous,
Press,
}
pub struct Ui<A: App> {
pub app: A,
/// Which pressable element has focus: its place among them, in
/// pre-order.
focus: usize,
theme: &'static Theme,
}
/// The pre-order indices of every pressable element in `e`.
fn pressables<E>(e: &Element<E>, index: &mut usize, out: &mut Vec<usize>) {
if matches!(e.kind, Kind::Button(_)) {
out.push(*index);
}
*index += 1;
if let Kind::Flex(f) = &e.kind {
for c in &f.children {
pressables(c, index, out);
}
}
}
/// The event of the pre-order `target`th element, if it is a button.
fn event_at<E: Clone>(e: &Element<E>, index: &mut usize, target: usize) -> Option<E> {
let me = *index;
*index += 1;
match &e.kind {
Kind::Button(b) if me == target => Some(b.on_press.clone()),
Kind::Flex(f) => f.children.iter().find_map(|c| event_at(c, index, target)),
_ => None,
}
}
impl<A: App> Ui<A> {
pub fn new(theme: &'static Theme) -> Self {
Self { app: A::init(), focus: 0, theme }
}
fn pressables(&self, view: &Element<A::Event>) -> Vec<usize> {
let mut out = Vec::new();
pressables(view, &mut 0, &mut out);
out
}
/// Handles a key; whether anything changed.
pub fn key(&mut self, key: Key) -> bool {
let view = self.app.view();
let targets = self.pressables(&view);
if targets.is_empty() {
return false;
}
let n = targets.len();
match key {
Key::Next => self.focus = (self.focus + 1) % n,
Key::Previous => self.focus = (self.focus + n - 1) % n,
Key::Press => {
let Some(event) = event_at(&view, &mut 0, targets[self.focus.min(n - 1)]) else { return false };
self.app.update(event);
// The view may have fewer pressable things now.
let now = self.pressables(&self.app.view()).len();
self.focus = self.focus.min(now.saturating_sub(1));
}
}
true
}
/// Lays the view out over the whole of `pm` and draws it.
pub fn draw(&self, pm: &mut PixmapMut, fonts: &Fonts) {
let view = self.app.view();
let area = Rect { x: 0.0, y: 0.0, w: pm.width() as f32, h: pm.height() as f32 };
let rects = layout::layout(&view, area, &Measurer { fonts, theme: self.theme }, self.theme);
let focused = self.pressables(&view).get(self.focus).copied();
render::render(pm, &view, &rects, fonts, self.theme, focused);
}
}

View File

@@ -144,3 +144,32 @@ impl Accent {
u32::from_str_radix(hex, 16).ok().map(Self)
}
}
/// How much memory an app may use, in 4 KiB pages: written `"512 KiB"` or
/// `"3 MiB"`, from 64 KiB to 16 MiB.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Memory(u16);
impl Memory {
/// What an app gets unless its manifest says more: enough for a small
/// command-line program.
pub const DEFAULT: Self = Self(128);
const MIN_PAGES: u32 = 16;
const MAX_PAGES: u32 = 4096;
pub fn parse(s: &str) -> Option<Self> {
let (n, unit) = s.split_once(' ')?;
let n: u32 = n.parse().ok()?;
let kib = match unit {
"KiB" => n,
"MiB" => n.checked_mul(1024)?,
_ => return None,
};
let pages = kib.div_ceil(4);
(kib % 4 == 0 && (Self::MIN_PAGES..=Self::MAX_PAGES).contains(&pages)).then_some(Self(pages as u16))
}
pub fn pages(&self) -> usize {
self.0 as usize
}
}

View File

@@ -16,7 +16,7 @@ mod tests;
pub use capability::{Capabilities, Capability};
pub use decision::{Decision, MAX_TEXT as MAX_DECISION_TEXT};
pub use fields::{Accent, AppId, Command, FileName, Text, Version};
pub use fields::{Accent, AppId, Command, FileName, Memory, Text, Version};
pub use manifest::Manifest;
/// Why a manifest or decision was refused; `line` is 1-based.

View File

@@ -2,7 +2,7 @@
use crate::Error;
use crate::capability::{Capabilities, Capability};
use crate::fields::{Accent, AppId, Command, FileName, Text, Version};
use crate::fields::{Accent, AppId, Command, FileName, Memory, Text, Version};
use crate::toml::{self, Entry, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -15,6 +15,8 @@ pub struct Manifest {
pub command: Command,
pub author: Option<Text<64>>,
pub icon: Option<FileName>,
/// How much memory it may use.
pub memory: Memory,
/// Denied = not installed.
pub required: Capabilities,
/// The owner decides each.
@@ -32,6 +34,7 @@ struct Draft {
command: Option<Command>,
author: Option<Text<64>>,
icon: Option<FileName>,
memory: Option<Memory>,
required: Option<Capabilities>,
optional: Option<Capabilities>,
accent: Option<Accent>,
@@ -81,6 +84,7 @@ impl Draft {
("app", "command") => set(&mut self.command, s.and_then(Command::new), l),
("app", "author") => set(&mut self.author, s.and_then(Text::new), l),
("app", "icon") => set(&mut self.icon, s.and_then(FileName::new), l),
("app", "memory") => set(&mut self.memory, s.and_then(Memory::parse), l),
("capabilities", "required") => {
set(&mut self.required, Some(capabilities(e.value, l)?), l)
}
@@ -116,6 +120,7 @@ impl Manifest {
command,
author: d.author,
icon: d.icon,
memory: d.memory.unwrap_or(Memory::DEFAULT),
required,
optional,
accent: d.accent,

View File

@@ -239,3 +239,13 @@ fn every_capability_name_round_trips_and_fits_the_set() {
all
);
}
#[test]
fn memory_is_in_kib_or_mib_within_bounds() {
use crate::Memory;
assert_eq!(Memory::parse("3 MiB").map(|m| m.pages()), Some(768));
assert_eq!(Memory::parse("512 KiB").map(|m| m.pages()), Some(128));
for bad in ["3MiB", "3 GiB", "17 MiB", "32 KiB", "6 KiB", "-1 MiB", "x MiB"] {
assert_eq!(Memory::parse(bad), None, "{bad}");
}
}

View File

@@ -22,7 +22,8 @@ use crate::policy::{self, Refusal, Store};
use crate::store::DiskStore;
/// What each app gets to run in, out of the permission manager's allowance.
const PROGRAM: Limits = Limits { pages: 128, endpoints: 1, notifications: 1, regions: 1 };
/// An app's limits; its pages are what its manifest asks for.
const PROGRAM: Limits = Limits { pages: 0, endpoints: 1, notifications: 1, regions: 1 };
/// Apps running at once; a death is a bit each on the notification.
const MAX_RUNNING: usize = 8;
const MAX_ARGS: usize = 16;
@@ -167,7 +168,7 @@ impl PermissionServer {
let index = self.running.iter().position(Option::is_none).ok_or(code(reply::BUSY))?;
let file = self.store.binary_grant(&plan.app).ok_or(code(reply::FAILED))?;
let spawned = libkos::spawn::spawn(self.init, file, PROGRAM);
let spawned = libkos::spawn::spawn(self.init, file, Limits { pages: plan.app.memory.pages(), ..PROGRAM });
let _ = libkos::fs::release(file);
let child = spawned.map_err(|e| match e {
libkos::spawn::SpawnError::OverAllowance => code(reply::BUSY),