feat(desktop): home app (system-only launch cap, LAUNCH_BADGE: apps+launch only), pm starts it with session (relaunch <=5); display keys F1/Alt+Tab next, F2 home, F4 close app (ROLE from launcher, terminal exempt), close returns to previous window; closed window -> app exits; shell 'name &'; launch by id; pm stack 8, storage 16 clients

This commit is contained in:
2026-10-28 15:50:05 +01:00
parent c3eb70191b
commit 18e8b3ecb8
29 changed files with 520 additions and 68 deletions

10
Cargo.lock generated
View File

@@ -409,6 +409,16 @@ dependencies = [
"libkos",
]
[[package]]
name = "home"
version = "0.1.0"
dependencies = [
"k-ui",
"libkos",
"manifest",
"types",
]
[[package]]
name = "hw-virtio"
version = "0.1.0"

View File

@@ -6,6 +6,7 @@ members = [
"crates/apps/flaky",
"crates/apps/tally",
"crates/apps/settings",
"crates/apps/home",
"crates/theme",
"crates/apps/hello",
"crates/apps/shell",

View File

@@ -54,7 +54,7 @@ kani: kani-types kani-domain kani-kernel
PROGRAMS = -p init -p echo -p sleep -p snake \
-p display-qemu -p input-qemu -p storage-qemu \
-p log-server -p permission-manager -p session-server -p smptest -p spawntest -p hello \
-p terminal-server -p shell -p cat -p ls -p counter -p gatekeeper -p flaky -p tally -p settings
-p terminal-server -p shell -p cat -p ls -p counter -p gatekeeper -p flaky -p tally -p settings -p home
# Programs first: the kernel image packs init and storage (see the
# adapter's build.rs).

View File

@@ -39,11 +39,12 @@ pub fn load_init(uart: &Uart) -> LoadedProcess {
/// Storage's budget, carved from init's.
const STORAGE: Limits = Limits {
pages: 256,
// A 16-page buffer per client (16), its image and cache.
pages: 512,
endpoints: 2,
notifications: 2,
// A buffer per client (8) and the disk's DMA.
regions: 12,
// A buffer per client and the disk's DMA.
regions: 20,
};
/// Storage, which init needs to read anything else, its budget carved

View File

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

View File

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

View File

@@ -0,0 +1,118 @@
//! Home: the session's desktop, which the permission manager starts with
//! it. It lists every app with a window and opens one when it is pressed,
//! as many as there is room for, each in its own window. A system app:
//! `launch` is only ever granted to apps on the system image.
#![no_std]
#![forbid(unsafe_code)]
#![no_main]
extern crate alloc;
extern crate libkos;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use k_ui::{App, Element, Spacing, TextColour, TextStyle, column, list_item, text};
use libkos::apps::Apps;
use manifest::{Capabilities, Capability};
use types::address::VirtualAddress;
use types::apps::reply;
/// Where the request page for the permission manager is mapped.
const REQUEST_PAGE_VA: VirtualAddress = VirtualAddress::from_raw(0x0190_0000);
const LISTING: usize = 4096;
const ME: &str = "kos.home";
struct Entry {
id: String,
name: String,
}
struct Home {
apps: Option<Apps>,
entries: Vec<Entry>,
/// What went wrong last, shown until the next press.
trouble: Option<String>,
}
#[derive(Clone, Copy)]
enum Event {
/// Open the `n`th app listed.
Open(usize),
}
impl Home {
/// Every installed app that has a window, but Home.
fn reload(&mut self) {
let Some(apps) = &self.apps else { return };
// On the heap: an app's stack is a few pages.
let mut buf = alloc::vec![0u8; LISTING];
let Ok(n) = apps.apps(&mut buf) else { return };
let text = core::str::from_utf8(&buf[..n]).unwrap_or("");
self.entries = libkos::apps::entries(text)
.filter(|e| e.id != ME)
.filter(|e| Capabilities::from_bits(e.required).union(Capabilities::from_bits(e.optional)).contains(Capability::Display))
.map(|e| Entry { id: e.id.to_string(), name: e.name.to_string() })
.collect();
}
fn open(&mut self, index: usize) {
let (Some(apps), Some(e)) = (&self.apps, self.entries.get(index)) else { return };
self.trouble = match apps.launch(&[e.id.as_str()], None, None) {
// Its window comes to the front; it runs on its own.
Ok((process, _)) => {
libkos::syscall::drop_cap(process);
None
}
Err(r) if r.code == reply::BUSY => Some("There is no room for another app: close one first (F4).".to_string()),
Err(r) => Some(format!("{} did not open (code {}).", e.name, r.code)),
};
self.reload();
}
}
impl App for Home {
type Event = Event;
fn init() -> Self {
let apps = libkos::start::permissions().and_then(|ep| Apps::connect(ep, REQUEST_PAGE_VA));
let mut home = Home { apps, entries: Vec::new(), trouble: None };
home.reload();
home
}
fn update(&mut self, event: Event) {
match event {
Event::Open(i) => self.open(i),
}
}
fn view(&self) -> Element<Event> {
let hint = "Enter opens an app. Alt+Tab or F1: the next window. F2: back here. F4: close the app in front.";
let mut view = column()
.padding(Spacing::Xl)
.gap(Spacing::Md)
.child(text("Home").style(TextStyle::Heading))
.child(text(hint).style(TextStyle::Caption).colour(TextColour::Secondary))
.children(self.entries.iter().enumerate().map(|(i, e)| list_item(e.name.clone(), Event::Open(i)).subtitle(e.id.clone())));
if self.entries.is_empty() {
view = view.child(text("No apps with a window are installed."));
}
if let Some(t) = &self.trouble {
view = view.child(text(t.clone()).colour(TextColour::Accent));
}
view.into()
}
/// Home is not left: Escape stays.
fn back(&mut self) -> bool {
true
}
}
libkos::entry!(main);
fn main() {
k_ui::run::<Home>();
}

View File

@@ -140,8 +140,11 @@ fn refused(what: &str, r: Refused) {
}
/// Runs app `args[0]` with `args` in the current folder, and waits for it
/// to finish or Ctrl+C.
/// to finish or Ctrl+C; with a last argument `&`, does not wait.
fn run_program(sh: &mut Shell, args: &[&str]) {
if let Some((&"&", args)) = args.split_last() {
return start_program(sh, args);
}
let folder = sh.fs.grant(sh.cwd.as_bytes(), false).ok();
// A Ctrl+C typed since the last program is not for this one.
let _ = syscall::poll(sh.done);
@@ -166,6 +169,21 @@ fn run_program(sh: &mut Shell, args: &[&str]) {
}
}
/// Starts app `args[0]` and lets it run on its own (its window, if it has
/// one, is closed with F4). It does not get the current folder: nobody
/// would be there to take the grant back when it is done.
fn start_program(sh: &mut Shell, args: &[&str]) {
let Some(name) = args.first() else { return println!("usage: <app> [args] &") };
match sh.apps.launch(args, Some(TERMINAL), None) {
Ok((process, _)) => {
// Not waited on: the handle is not needed.
syscall::drop_cap(process);
println!("{} started", name);
}
Err(r) => refused(name, r),
}
}
/// Installs the app in folder `from` (its manifest and binary); the owner
/// is asked on the terminal.
fn install(sh: &mut Shell, from: &str) {
@@ -232,6 +250,7 @@ fn run(sh: &mut Shell, line: &str) {
println!("anything else runs an app: try ls, cat readme.txt, snake");
println!("apps see the current directory, read-only, and only what you allowed");
println!("Ctrl+C stops a running program; snake quits with q");
println!("end with & to leave it running (snake &); F4 closes the app in front");
}
Some("apps") => list(sh),
Some("install") => match args.get(1) {

View File

@@ -77,25 +77,23 @@ enum Input {
Other,
}
/// The next key, if one is waiting: q or Escape quits.
/// The next key, if one is waiting: q or Escape quits, and so does the
/// window going.
fn next_input(window: CapSlot) -> Input {
let Ok([tag, kind, ch, _]) = syscall::request(window, [types::display::window::POLL, 0, 0, 0]) else {
return Input::None;
};
if tag == 0 {
return Input::None; // NoEvent: queue drained
}
if tag != 1 {
return Input::Other; // a key let go
}
// Arrows, or WASD (kind 9 is a character).
match (kind, ch as u8) {
(5, _) | (9, b'q') => Input::Quit,
(0, _) | (9, b'w') => Input::Turn(Direction::Up),
(1, _) | (9, b's') => Input::Turn(Direction::Down),
(2, _) | (9, b'a') => Input::Turn(Direction::Left),
(3, _) | (9, b'd') => Input::Turn(Direction::Right),
use libkos::display::Event;
use libkos::input::{InputEvent, Key};
match libkos::display::poll(window) {
None => Input::None,
Some(Event::Closed) => Input::Quit,
Some(Event::Input(InputEvent::KeyDown(k))) => match k {
Key::Escape | Key::Char(b'q') => Input::Quit,
Key::Up | Key::Char(b'w') => Input::Turn(Direction::Up),
Key::Down | Key::Char(b's') => Input::Turn(Direction::Down),
Key::Left | Key::Char(b'a') => Input::Turn(Direction::Left),
Key::Right | Key::Char(b'd') => Input::Turn(Direction::Right),
_ => Input::Other,
},
Some(_) => Input::Other,
}
}

View File

@@ -6,6 +6,16 @@ pub const LEFT_SHIFT: u16 = 42;
pub const RIGHT_SHIFT: u16 = 54;
pub const LEFT_CTRL: u16 = 29;
pub const RIGHT_CTRL: u16 = 97;
pub const LEFT_ALT: u16 = 56;
pub const RIGHT_ALT: u16 = 100;
/// Alt+Tab is the display's window switch, the same key as F1
/// (`types::display::window::SWITCH_KEY`).
const SWITCH: Key = Key::Function(1);
/// What a key is with Alt held: only Alt+Tab means anything.
pub fn with_alt(key: Key) -> Option<Key> {
(key == Key::Tab).then_some(SWITCH)
}
/// A letter typed with Ctrl held is its control character, as on a
/// terminal: Ctrl+C is 3. Anything else is itself.
@@ -54,6 +64,12 @@ pub fn key(code: u16, shift: bool) -> Option<Key> {
mod tests {
use super::*;
#[test]
fn alt_tab_switches_windows_and_other_alt_keys_are_nothing() {
assert_eq!(with_alt(Key::Tab), Some(Key::Function(1)));
assert_eq!(with_alt(Key::Char(b'a')), None);
}
#[test]
fn function_keys_are_numbered() {
assert_eq!(key(59, false), Some(Key::Function(1)));

View File

@@ -36,6 +36,7 @@ pub struct VirtioInputDevice {
next_avail: u16,
shift: bool,
ctrl: bool,
alt: bool,
}
impl VirtioInputDevice {
@@ -61,6 +62,7 @@ impl VirtioInputDevice {
next_avail: 0,
shift: false,
ctrl: false,
alt: false,
};
dev.fill_queue();
Some(dev)
@@ -129,8 +131,13 @@ impl InputDevice for VirtioInputDevice {
self.ctrl = ev_value != 0;
continue;
}
if ev_type == EV_KEY && matches!(ev_code, keymap::LEFT_ALT | keymap::RIGHT_ALT) {
self.alt = ev_value != 0;
continue;
}
if ev_type == EV_KEY {
let key = keymap::key(ev_code, self.shift).map(|k| if self.ctrl { keymap::with_ctrl(k) } else { k });
let key = if self.alt { key.and_then(keymap::with_alt) } else { key };
if let Some(key) = key {
if ev_value == KEY_PRESS {
return Some(InputEvent::KeyDown(key));

View File

@@ -157,6 +157,8 @@ pub const SESSION: &[Program] = &[Program {
session: Session::PassOn,
// Installing waits for the user's consent.
pinged: false,
// A launch (manifest, decision, arguments) is KiBs of stack.
stack_pages: 8,
..server("permission", b"bin/permission", Limits { pages: 96, ..SMALL })
}];

View File

@@ -71,6 +71,7 @@ pub fn run<A: App>() -> ! {
dirty = true;
continue;
}
Event::Closed => syscall::exit(),
Event::Input(InputEvent::KeyDown(k)) => k,
Event::Input(_) => continue,
};

View File

@@ -37,6 +37,12 @@ pub fn set_accent(window: CapSlot, rgb: u32) -> bool {
syscall::request(window, [w::ACCENT, rgb as usize, 0, 0]).is_ok_and(|r| r[0] == w::OK)
}
/// A launcher says what the window it opened is (`window::role`), before
/// the app runs; whether the display took it.
pub fn set_role(window: CapSlot, role: usize) -> bool {
syscall::request(window, [w::ROLE, role, 0, 0]).is_ok_and(|r| r[0] == w::OK)
}
/// Closes `window`: every copy of it, and its buffer, stop working.
pub fn close(window: CapSlot) {
let _ = syscall::request(window, [w::CLOSE, 0, 0, 0]);
@@ -62,15 +68,20 @@ pub enum Event {
Input(crate::input::InputEvent),
/// Ask for the theme again (`theme`).
ThemeChanged,
/// The window is gone (the user closed it, or the display went):
/// the app is done.
Closed,
}
/// `window`'s next event, if one is waiting; ones not known here are
/// skipped.
pub fn poll(window: CapSlot) -> Option<Event> {
loop {
let words = syscall::request(window, [w::POLL, 0, 0, 0]).ok()?;
let Ok(words) = syscall::request(window, [w::POLL, 0, 0, 0]) else { return Some(Event::Closed) };
match words[0] {
0 => return None,
// What the display answers through a window it has closed.
usize::MAX => return Some(Event::Closed),
w::THEME_CHANGED => return Some(Event::ThemeChanged),
_ => {
if let Some(e) = crate::input::decode(words) {
@@ -81,11 +92,14 @@ pub fn poll(window: CapSlot) -> Option<Event> {
}
}
/// `window`'s next input event, if one is waiting.
/// `window`'s next input event, if one is waiting. A program whose window
/// is gone exits here.
pub fn poll_input(window: CapSlot) -> Option<crate::input::InputEvent> {
loop {
if let Event::Input(e) = poll(window)? {
return Some(e);
match poll(window)? {
Event::Input(e) => return Some(e),
Event::Closed => syscall::exit(),
Event::ThemeChanged => {}
}
}
}

View File

@@ -62,6 +62,7 @@ catalog! {
// System-only (see `Capabilities::SYSTEM_ONLY`).
Permissions = "permissions",
Session = "session",
Launch = "launch",
}
impl Capability {
@@ -81,7 +82,7 @@ impl Capabilities {
/// refused, so no prompt can talk a user into handing them over.
/// `permissions`: changing what other apps may use. `session`: who is
/// signed in, and signing out.
pub const SYSTEM_ONLY: Self = Self(1 << Capability::Permissions as u32 | 1 << Capability::Session as u32);
pub const SYSTEM_ONLY: Self = Self(1 << Capability::Permissions as u32 | 1 << Capability::Session as u32 | 1 << Capability::Launch as u32);
/// As sent in a message word.
pub const fn bits(self) -> u32 {

View File

@@ -94,6 +94,16 @@ impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
&mut self.host
}
#[cfg(test)]
pub fn windows_front_for_test(&self) -> Option<usize> {
self.windows.front()
}
#[cfg(test)]
pub fn slot_for_test(&self, badge: usize) -> Option<usize> {
self.windows.slot(badge)
}
/// A window's size: the screen below the bar.
fn window_size(&self) -> (Dimension, Dimension) {
(self.fb.width(), Dimension::new(self.fb.height().raw().saturating_sub(BAR_HEIGHT)))
@@ -193,6 +203,28 @@ impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
raster::draw_text(&mut self.fb, ScreenCoord::new(at), ScreenCoord::new(0), bar_fg, bar_bg, user);
}
/// What a window key does.
fn window_key(&mut self, key: usize) {
let changed = match key {
w::SWITCH_KEY => self.windows.cycle().is_some(),
w::HOME_KEY => self.windows.with_role(w::role::HOME).is_some_and(|i| self.windows.raise(i)),
_ => match self.windows.front().filter(|i| self.windows.role(*i) == Some(w::role::APP)) {
Some(index) => {
// Woken, it finds its window gone, and is done.
if let Some(notify) = self.windows.notify(index) {
self.host.signal(notify);
}
self.close(index);
false
}
None => false,
},
};
if changed {
self.repaint();
}
}
/// Every window is told to ask for the theme again; the bar wears it.
fn theme_changed(&mut self) {
let mut told = [None; MAX_WINDOWS];
@@ -220,6 +252,7 @@ impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
None => ([MALFORMED, 0, 0, 0], None),
}
}
(w::ROLE, _) if matches!(words[1], w::role::APP | w::role::HOME) && self.windows.set_role(badge, words[1]) => ([w::OK, 0, 0, 0], None),
(w::ACCENT, _) if words[1] <= 0xff_ffff && self.windows.set_accent(badge, words[1] as u32) => ([w::OK, 0, 0, 0], None),
(w::PRESENT, _) => {
let (x, y, width, height) = w::unpack_rect([words[1], words[2]]);
@@ -248,9 +281,11 @@ impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
}
}
fn is_switch_key(event: [usize; 4]) -> bool {
/// The display's own key in `event`, if it is one (pressed or let go).
fn display_key(event: [usize; 4]) -> Option<usize> {
use types::input::event::{FUNCTION_KEY, KEY_DOWN, KEY_UP};
matches!(event[0], KEY_DOWN | KEY_UP) && event[1] == FUNCTION_KEY && event[2] == w::SWITCH_KEY
let ours = matches!(event[0], KEY_DOWN | KEY_UP) && event[1] == FUNCTION_KEY;
(ours && matches!(event[2], w::SWITCH_KEY | w::HOME_KEY | w::CLOSE_KEY)).then_some(event[2])
}
impl<F: Framebuffer, H: Host> Server for DisplayServer<F, H> {
@@ -306,14 +341,14 @@ impl<F: Framebuffer, H: Host> Server for DisplayServer<F, H> {
}
}
/// The input server has events: the switch key is the display's, the
/// rest go to the window in front.
/// The input server has events: the window keys are the display's,
/// the rest go to the window in front.
fn signal(&mut self, _bits: usize) {
let mut told = None;
while let Some(event) = self.host.input() {
if is_switch_key(event) {
if event[0] == types::input::event::KEY_DOWN && self.windows.cycle().is_some() {
self.repaint();
if let Some(key) = display_key(event) {
if event[0] == types::input::event::KEY_DOWN {
self.window_key(key);
}
continue;
}

View File

@@ -259,3 +259,42 @@ fn only_the_launcher_sets_an_accent_and_it_is_in_the_theme() {
assert_eq!(accent(&mut s, other), theme::Theme::WHIMSY_DARK.accent);
assert_eq!(accent(&mut s, app), theme::with_accent(theme::Theme::WHIMSY_DARK, theme::Rgba::rgb(pink as u32)).accent);
}
#[test]
fn window_keys_raise_home_and_close_only_apps() {
let key = |s: &mut Srv, k: usize| {
s.host_mut_for_test().events = vec![[1, 10, k, 0], [2, 10, k, 0]];
s.signal(0);
};
let mut s = server();
let terminal = open(&mut s, 1, b"Terminal");
let home = open(&mut s, 2, b"Home");
let app = open(&mut s, 2, b"Tally");
assert_eq!(s.handle_call(home, [w::ROLE, w::role::HOME, 0, 0], None).0[0], w::OK);
assert_eq!(s.handle_call(app, [w::ROLE, w::role::APP, 0, 0], None).0[0], w::OK);
assert_eq!(s.handle_call(app, [w::ROLE, w::role::HOME, 0, 0], None).0[0], MALFORMED, "once");
s.handle_call(terminal, [w::BUFFER, 0, 0, 0], None);
assert_eq!(s.handle_call(terminal, [w::ROLE, w::role::APP, 0, 0], None).0[0], MALFORMED, "not once it runs");
let notify = CapSlot::new(9).unwrap();
s.handle_call(app, [w::SUBSCRIBE, 0, 0, 0], Some(notify));
key(&mut s, w::HOME_KEY);
assert_eq!(s.windows_front_for_test(), s.slot_for_test(home));
key(&mut s, w::CLOSE_KEY);
assert!(s.slot_for_test(home).is_some(), "Home is not closed");
// The app in front: closed, and told.
s.handle_call(app, [w::POLL, 0, 0, 0], None);
key(&mut s, w::SWITCH_KEY);
assert_eq!(s.windows_front_for_test(), s.slot_for_test(app));
key(&mut s, w::CLOSE_KEY);
assert_eq!(s.slot_for_test(app), None);
assert!(s.host().signalled.contains(&notify));
assert_eq!(s.handle_call(app, [w::POLL, 0, 0, 0], None).0[0], MALFORMED, "its window is gone");
// The terminal in front: kept.
key(&mut s, w::SWITCH_KEY);
while s.windows_front_for_test() != s.slot_for_test(terminal) {
key(&mut s, w::SWITCH_KEY);
}
key(&mut s, w::CLOSE_KEY);
assert!(s.slot_for_test(terminal).is_some());
}

View File

@@ -67,3 +67,19 @@ fn a_launchers_windows_are_found_by_its_badge() {
let (b, _) = w.open(5, b"two").unwrap();
assert_eq!(w.opened_by(5).collect::<Vec<_>>(), [a, b]);
}
#[test]
fn closing_the_front_window_brings_back_the_one_before_it() {
let mut w: Windows<u8> = Windows::new();
let (t, _) = w.open(1, b"Terminal").unwrap();
let (home, _) = w.open(2, b"Home").unwrap();
let (app, _) = w.open(2, b"Tally").unwrap();
assert!(w.close(app));
assert_eq!(w.front(), Some(home), "not the next slot round");
w.raise(t);
let (app2, _) = w.open(2, b"Snake").unwrap();
w.raise(home);
w.raise(app2);
w.close(app2);
assert_eq!(w.front(), Some(home));
}

View File

@@ -1,7 +1,8 @@
//! Which windows there are and which is in front (see `types::display::
//! window`). Every window fills the screen below the OS bar; one is in
//! front at a time, and only it is shown and gets input. A new window comes
//! to the front; a key cycles through them. Pure: the server keeps the
//! to the front; a key cycles through them. Closing the one in front brings
//! back the one in front before it. Pure: the server keeps the
//! pixels.
use types::display::window::{BADGE, NAME_BYTES};
@@ -54,6 +55,10 @@ struct Window<N> {
accent: Option<u32>,
/// Its buffer was asked for: the app runs, so the accent is settled.
started: bool,
/// What it is (`types::display::window::role`), if it was said.
role: Option<usize>,
/// When it last came to the front (`Windows::raises`).
raised: u64,
}
pub struct Windows<N: Copy> {
@@ -61,11 +66,13 @@ pub struct Windows<N: Copy> {
front: Option<usize>,
/// Makes each window's badge new, so a closed window's never returns.
opened: usize,
/// Times a window came to the front: what was in front most lately.
raises: u64,
}
impl<N: Copy> Windows<N> {
pub const fn new() -> Self {
Self { slots: [const { None }; MAX_WINDOWS], front: None, opened: 0 }
Self { slots: [const { None }; MAX_WINDOWS], front: None, opened: 0, raises: 0 }
}
/// A window for `name`, opened through `opener`'s capability, in
@@ -77,8 +84,8 @@ impl<N: Copy> Windows<N> {
let mut n = [0u8; NAME_BYTES];
let name_len = name.len().min(NAME_BYTES);
n[..name_len].copy_from_slice(&name[..name_len]);
self.slots[index] = Some(Window { badge, opener, name: n, name_len, notify: None, queue: Queue::new(), accent: None, started: false });
self.front = Some(index);
self.slots[index] = Some(Window { badge, opener, name: n, name_len, notify: None, queue: Queue::new(), accent: None, started: false, role: None, raised: 0 });
self.bring_forward(index);
Some((index, badge))
}
@@ -95,8 +102,7 @@ impl<N: Copy> Windows<N> {
if self.front != Some(index) {
return false;
}
self.front = None;
self.front = self.after(index);
self.front = self.slots.iter().enumerate().filter_map(|(i, w)| w.as_ref().map(|w| (w.raised, i))).max().map(|(_, i)| i);
true
}
@@ -114,11 +120,19 @@ impl<N: Copy> Windows<N> {
pub fn cycle(&mut self) -> Option<usize> {
let next = self.after(self.front?)?;
(Some(next) != self.front).then(|| {
self.front = Some(next);
self.bring_forward(next);
next
})
}
fn bring_forward(&mut self, index: usize) {
self.raises += 1;
if let Some(w) = self.slots[index].as_mut() {
w.raised = self.raises;
}
self.front = Some(index);
}
pub fn front(&self) -> Option<usize> {
self.front
}
@@ -140,6 +154,40 @@ impl<N: Copy> Windows<N> {
}
}
/// Says what the window `badge` is: once, before it starts.
pub fn set_role(&mut self, badge: usize, role: usize) -> bool {
let Some(w) = self.slot(badge).and_then(|i| self.slots[i].as_mut()) else { return false };
let ok = !w.started && w.role.is_none();
if ok {
w.role = Some(role);
}
ok
}
pub fn role(&self, index: usize) -> Option<usize> {
self.slots.get(index)?.as_ref()?.role
}
/// The first open window that is `role`.
pub fn with_role(&self, role: usize) -> Option<usize> {
self.slots.iter().position(|w| w.as_ref().is_some_and(|w| w.role == Some(role)))
}
/// Who is told of the window in `index`'s input.
pub fn notify(&self, index: usize) -> Option<N> {
self.slots.get(index)?.as_ref()?.notify
}
/// Brings the window in `index` to the front; whether it changed.
pub fn raise(&mut self, index: usize) -> bool {
let open = self.slots.get(index).is_some_and(Option::is_some);
let changed = open && self.front != Some(index);
if changed {
self.bring_forward(index);
}
changed
}
pub fn accent(&self, badge: usize) -> Option<u32> {
self.slots[self.slot(badge)?].as_ref()?.accent
}

View File

@@ -49,6 +49,7 @@ pub fn run() -> ! {
let consent = consent::TerminalConsent { terminal: TERMINAL };
let ports = server::Ports { init: INIT, display: Some(DISPLAY), session: Some(SESSION), endpoint: EP };
let mut srv = server::PermissionServer::new(store, consent, ports, deaths);
srv.start_home();
libkos::println!("permission: serving");
server_framework::serve(&mut srv, EP)
}

View File

@@ -15,6 +15,7 @@ use types::bounded::CapSlot;
use types::budget::Limits;
use types::mapping::PagePerms;
use types::permission::{Permission, PermissionSet};
use types::display::window::role as display_role;
use types::start_arg::StartArg;
use crate::consent::{CapabilityNames, TerminalConsent, version};
@@ -27,6 +28,12 @@ const PROGRAM: Limits = Limits { pages: 0, endpoints: 1, notifications: 1, regio
/// Apps running at once; a death is a bit each on the notification.
const MAX_RUNNING: usize = 8;
const MAX_ARGS: usize = 16;
/// The session's desktop, started with it and again if it dies.
const HOME: &str = "kos.home";
/// Home's starts in a session at most: one that keeps dying is left down.
const MAX_HOMES: u8 = 5;
/// A launch that no launcher asked for (Home's): nothing staged is its.
const NO_LAUNCHER: usize = usize::MAX;
/// Where a client's request page is mapped during its call.
const PAGE_VA: VirtualAddress = VirtualAddress::from_raw(0x01A0_0000);
@@ -66,6 +73,8 @@ pub struct PermissionServer {
deaths: CapSlot,
staged: Staged,
running: [Option<Running>; MAX_RUNNING],
/// Home's starts this session.
homes: u8,
}
fn refused(r: Refusal) -> [usize; 4] {
@@ -127,12 +136,12 @@ fn parts(page: &[u8]) -> impl Iterator<Item = &str> {
impl PermissionServer {
pub fn new(store: DiskStore, consent: TerminalConsent, ports: Ports, deaths: CapSlot) -> Self {
let Ports { init, display, session, endpoint } = ports;
Self { store, consent, init, display, session, endpoint, deaths, staged: Staged::default(), running: [const { None }; MAX_RUNNING] }
Self { store, consent, init, display, session, endpoint, deaths, staged: Staged::default(), running: [const { None }; MAX_RUNNING], homes: 0 }
}
/// What this device has a server for.
fn available(&self) -> Capabilities {
let mut c = Capabilities::NONE.with(Capability::Storage).with(Capability::Permissions);
let mut c = Capabilities::NONE.with(Capability::Storage).with(Capability::Permissions).with(Capability::Launch);
if self.display.is_some() {
c = c.with(Capability::Display);
}
@@ -177,7 +186,27 @@ impl PermissionServer {
*slot = p;
n += 1;
}
let args = &args[..n];
self.run(badge, &args[..n])
}
/// Starts Home, the session's desktop, unless it runs already or has
/// died too often.
pub fn start_home(&mut self) {
let running = self.running.iter().flatten().any(|r| r.id.as_str() == HOME);
if running || self.homes >= MAX_HOMES {
return;
}
self.homes += 1;
match self.run(NO_LAUNCHER, &[HOME]) {
Ok((process, _)) => {
syscall::drop_cap(process);
}
Err(words) => libkos::println!("permission: home did not start (refused: {})", words[0]),
}
}
/// Runs `args[0]` with `args` for the launcher behind `badge`.
fn run(&mut self, badge: usize, args: &[&str]) -> Result<(CapSlot, Capabilities), [usize; 4]> {
let command = *args.first().ok_or(code(reply::NOT_INSTALLED))?;
let available = self.available();
let plan = policy::plan_launch(&mut self.store, command, available).map_err(refused)?;
@@ -201,18 +230,27 @@ impl PermissionServer {
true => self.display.and_then(|d| libkos::display::open(d, plan.app.name.as_str().as_bytes())).map(|o| o.window),
false => None,
};
// Its brand colour too, which the display adapts to the theme.
if let (Some(w), Some(accent)) = (window, plan.app.accent) {
// Its brand colour too, which the display adapts to the theme, and
// what it is: Home, or an app the user may close.
if let Some(w) = window {
if let Some(accent) = plan.app.accent {
libkos::display::set_accent(w, accent.0);
}
// Managing others through a copy of this endpoint that can do no
// more (see `MANAGE_BADGE`).
let manage = match plan.granted.contains(Capability::Permissions) {
true => {
let perms = PermissionSet::empty().with(Permission::Execute).with(Permission::Delegate);
syscall::mint(self.endpoint, types::badge::Badge::new(wire::MANAGE_BADGE | index), perms).ok()
let role = if plan.app.id.as_str() == HOME { display_role::HOME } else { display_role::APP };
libkos::display::set_role(w, role);
}
// Managing others, or launching them, through a copy of this
// endpoint that can do no more (see `MANAGE_BADGE`, `LAUNCH_BADGE`).
let bits = [(Capability::Permissions, wire::MANAGE_BADGE), (Capability::Launch, wire::LAUNCH_BADGE)]
.iter()
.filter(|(c, _)| plan.granted.contains(*c))
.fold(0, |b, (_, bit)| b | bit);
let manage = match bits {
0 => None,
bits => {
let perms = PermissionSet::empty().with(Permission::Execute).with(Permission::Delegate);
syscall::mint(self.endpoint, types::badge::Badge::new(bits | index), perms).ok()
}
false => None,
};
let session = self.session.filter(|_| plan.granted.contains(Capability::Session));
let staged = (badge == self.staged.badge).then_some(&self.staged);
@@ -319,8 +357,9 @@ impl PermissionServer {
code(reply::OK)
}
/// Apps behind `bits` died: their data grants go back.
/// Apps behind `bits` died: their data grants go back. Home comes back.
fn reap(&mut self, bits: usize) {
let home_died = self.running.iter().enumerate().any(|(i, r)| bits & (1 << i) != 0 && r.as_ref().is_some_and(|r| r.id.as_str() == HOME));
for (i, slot) in self.running.iter_mut().enumerate() {
if bits & (1 << i) == 0 {
continue;
@@ -334,6 +373,9 @@ impl PermissionServer {
syscall::drop_cap(r.process);
}
}
if home_died {
self.start_home();
}
}
}
@@ -407,9 +449,14 @@ fn close_window(window: Option<CapSlot>) {
}
/// Whether `op` may come through the copy of this endpoint badged `badge`:
/// a management copy (`permissions`) decides, never installs or launches.
/// a management copy (`permissions`) decides, never installs or launches;
/// a launcher's (`launch`) lists and launches, passing nothing along.
pub fn allowed(badge: usize, op: usize) -> bool {
badge & wire::MANAGE_BADGE == 0 || matches!(op, wire::LIST | wire::APPS | wire::REVOKE | wire::ALLOW)
let (manage, launch) = (badge & wire::MANAGE_BADGE != 0, badge & wire::LAUNCH_BADGE != 0);
if !manage && !launch {
return true;
}
(manage && matches!(op, wire::LIST | wire::APPS | wire::REVOKE | wire::ALLOW)) || (launch && matches!(op, wire::APPS | wire::LAUNCH))
}
fn revoke(cap: Option<CapSlot>) {

View File

@@ -202,7 +202,8 @@ impl Store for DiskStore {
n += 1;
}
});
ids.into_iter().flatten().find(|id| self.manifest(id).is_some_and(|m| m.command.as_str() == command))
// By its id, or the command the shell runs it as.
ids.into_iter().flatten().find(|id| id.as_str() == command || self.manifest(id).is_some_and(|m| m.command.as_str() == command))
}
fn decision(&mut self, id: &AppId) -> Option<Decision> {

View File

@@ -11,5 +11,18 @@ fn a_management_copy_only_decides() {
for op in [LAUNCH, INSTALL, PASS_OUTPUT, PASS_FOLDER, 99] {
assert!(!allowed(manager, op), "op {op}");
}
assert!(allowed(5, LAUNCH) && allowed(5, INSTALL), "a launcher's copy does everything");
assert!(allowed(5, LAUNCH) && allowed(5, INSTALL), "the shell's copy does everything");
}
#[test]
fn a_home_copy_only_lists_and_launches() {
use crate::server::allowed;
use types::apps::*;
let home = LAUNCH_BADGE | 2;
assert!(allowed(home, APPS) && allowed(home, LAUNCH));
for op in [PASS_OUTPUT, PASS_FOLDER, INSTALL, LIST, REVOKE, ALLOW] {
assert!(!allowed(home, op), "op {op}");
}
let both = LAUNCH_BADGE | MANAGE_BADGE | 2;
assert!(allowed(both, LAUNCH) && allowed(both, REVOKE) && !allowed(both, INSTALL));
}

View File

@@ -9,7 +9,8 @@ use crate::scope::Scope;
mod session;
const MAX_CLIENTS: usize = 8;
/// Connections at once: every program reading files, apps among them.
const MAX_CLIENTS: usize = 16;
/// Handles at or past this are never valid (the file systems hand out
/// fewer), so ownership fits a small table.
const MAX_HANDLES: usize = 16;

View File

@@ -33,6 +33,10 @@ pub const APPS: usize = 8;
/// an app granted `permissions`: through it only `LIST`, `APPS`, `REVOKE`
/// and `ALLOW` work, never installing or launching.
pub const MANAGE_BADGE: usize = 1 << 61;
/// On the copy of the endpoint an app granted `launch` gets (Home): it may
/// only `APPS` and `LAUNCH`, and pass nothing along (no `PASS_OUTPUT`,
/// `PASS_FOLDER`). One copy may carry both bits.
pub const LAUNCH_BADGE: usize = 1 << 60;
/// The request page's size.
pub const PAGE_SIZE: usize = 4096;

View File

@@ -113,13 +113,26 @@ pub mod window {
/// accent, `0xrrggbb`. The theme the window is handed carries it where
/// it passes contrast (`theme::with_accent`).
pub const ACCENT: usize = 12;
/// Through a window, before its buffer is asked for and once: word 1
/// what it is (`role`). A window given none (the terminal's) is
/// neither closed by `CLOSE_KEY` nor brought up by `HOME_KEY`.
pub const ROLE: usize = 13;
pub mod role {
/// An app's: `CLOSE_KEY` closes it.
pub const APP: usize = 1;
/// Home: `HOME_KEY` brings it to the front.
pub const HOME: usize = 2;
}
/// The event (in place of an input event's tag) every window gets when
/// the theme changes: ask for it again.
pub const THEME_CHANGED: usize = 16;
/// The function key that brings the next window to the front; the
/// display keeps it to itself.
/// Function keys the display keeps to itself: the next window to the
/// front (Alt+Tab is the same key), Home to the front, and the app in
/// front closed (its window goes, so its next call to it fails).
pub const SWITCH_KEY: usize = 1;
pub const HOME_KEY: usize = 2;
pub const CLOSE_KEY: usize = 4;
/// On the badge of every window capability; launchers' badges are
/// small, so the two never meet.

View File

@@ -91,12 +91,23 @@ optional one is simply not granted.
| `display` | a window of its own, named from its manifest: its pixels, and the keyboard while it is in front |
| `storage` | the user's `data/<id>/`, read-write (its own folder only) |
| `permissions` | a management copy of the permission manager's endpoint (below) |
| `session` | init's session copy: who is signed in (`WHO`), and signing out |
| `session` | init's session copy: who is signed in (`WHO`), the theme, and signing out |
| `launch` | a launcher copy of the permission manager's endpoint: `APPS` and `LAUNCH` only |
`permissions` and `session` are **system only**: only an app on the system
`permissions`, `session` and `launch` are **system only**: only an app on the system
image (with a factory decision) has them. Installing an app that asks for
either, required or optional, is refused (`SYSTEM_ONLY`). Settings
(`kos.settings`) is the app that holds them.
(`kos.settings`) holds the first two, Home (`kos.home`) the third.
Home is the session's desktop: the permission manager starts it when the
session starts (and again if it dies, five times at most), and it lists
every app with a window and opens the one pressed, each in its own
window, as many as there is room for (8 windows, 24 MiB between the
apps). Its copy of the endpoint is badged `LAUNCH_BADGE`: it may list and
launch, but pass nothing along, so what it opens gets no output and no
folder. The shell still runs apps and waits; `name &` starts one without
waiting (and without the current folder, which nobody would take back).
An app is named to `LAUNCH` by its command or its id.
## Decisions

View File

@@ -173,7 +173,16 @@ The display server alone holds the screen (`types::display::window`,
client `libkos::display`). It draws the OS bar along the top: every
window's name, the one in front highlighted, and who is signed in (init
says, `SET_USER`). Below it, the window in front fills the screen; one is
in front at a time, a new one comes to the front, and F1 brings the next.
in front at a time, a new one comes to the front, and F1 (or Alt+Tab) brings
the next; closing the one in front brings back the one in front before
it. At most 8 windows are open. The display keeps three keys to itself:
F1/Alt+Tab, F2 (Home to the front) and F4 (close the app in front). What
a window is comes from its launcher before the app runs (`ROLE`): the
permission manager says `HOME` for Home and `APP` for every other app; a
window given no role (the terminal's) is never closed by F4. Closing an
app's window revokes it and signals the app, whose next call through it
fails: `libkos::display::poll` says `Closed`, and K-UI apps and
`poll_input` users exit, which the permission manager sees as a death.
A window is opened (`OPEN`, with the name the bar shows) only through a
launcher's copy of the display endpoint, one init gave a system program:
the terminal opens its own, the permission manager one for each app

View File

@@ -56,6 +56,7 @@ app sleep
app snake
app tally
app settings
app home
staged counter inbox/counter
# A user's first files.
cp -r disk/home/. "${ROOT}/factory/home/"