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

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),
_ => Input::Other,
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,
}
}