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:
@@ -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
|
||||
|
||||
15
crates/apps/home/Cargo.toml
Normal file
15
crates/apps/home/Cargo.toml
Normal 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 }
|
||||
10
crates/apps/home/manifest.toml
Normal file
10
crates/apps/home/manifest.toml
Normal 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"]
|
||||
118
crates/apps/home/src/main.rs
Normal file
118
crates/apps/home/src/main.rs
Normal 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>();
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 })
|
||||
}];
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(¬ify));
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
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()
|
||||
// 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);
|
||||
}
|
||||
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>) {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user