diff --git a/Cargo.lock b/Cargo.lock index 1122195..9ed004f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 613f200..d69b7a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/Makefile b/Makefile index f02ff64..e3544fc 100644 --- a/Makefile +++ b/Makefile @@ -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). diff --git a/crates/adapter-riscv/src/boot/loader.rs b/crates/adapter-riscv/src/boot/loader.rs index 600ee95..249d324 100644 --- a/crates/adapter-riscv/src/boot/loader.rs +++ b/crates/adapter-riscv/src/boot/loader.rs @@ -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 diff --git a/crates/apps/home/Cargo.toml b/crates/apps/home/Cargo.toml new file mode 100644 index 0000000..09720de --- /dev/null +++ b/crates/apps/home/Cargo.toml @@ -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 } diff --git a/crates/apps/home/manifest.toml b/crates/apps/home/manifest.toml new file mode 100644 index 0000000..8ce9c08 --- /dev/null +++ b/crates/apps/home/manifest.toml @@ -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"] diff --git a/crates/apps/home/src/main.rs b/crates/apps/home/src/main.rs new file mode 100644 index 0000000..a72b4f6 --- /dev/null +++ b/crates/apps/home/src/main.rs @@ -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, + entries: Vec, + /// What went wrong last, shown until the next press. + trouble: Option, +} + +#[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 { + 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::(); +} diff --git a/crates/apps/shell/src/main.rs b/crates/apps/shell/src/main.rs index 0a0a6c9..6d3541e 100644 --- a/crates/apps/shell/src/main.rs +++ b/crates/apps/shell/src/main.rs @@ -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: [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) { diff --git a/crates/apps/snake/src/main.rs b/crates/apps/snake/src/main.rs index 7321ee4..53bf513 100644 --- a/crates/apps/snake/src/main.rs +++ b/crates/apps/snake/src/main.rs @@ -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, } } diff --git a/crates/drivers/input/virtio/src/keymap.rs b/crates/drivers/input/virtio/src/keymap.rs index 336adca..975207b 100644 --- a/crates/drivers/input/virtio/src/keymap.rs +++ b/crates/drivers/input/virtio/src/keymap.rs @@ -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::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 { 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))); diff --git a/crates/drivers/input/virtio/src/lib.rs b/crates/drivers/input/virtio/src/lib.rs index 758d454..1e9e7a8 100644 --- a/crates/drivers/input/virtio/src/lib.rs +++ b/crates/drivers/input/virtio/src/lib.rs @@ -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)); diff --git a/crates/init/src/manifest.rs b/crates/init/src/manifest.rs index 109dca6..8deca9c 100644 --- a/crates/init/src/manifest.rs +++ b/crates/init/src/manifest.rs @@ -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 }) }]; diff --git a/crates/k-ui/src/run.rs b/crates/k-ui/src/run.rs index 436062a..a6d5122 100644 --- a/crates/k-ui/src/run.rs +++ b/crates/k-ui/src/run.rs @@ -71,6 +71,7 @@ pub fn run() -> ! { dirty = true; continue; } + Event::Closed => syscall::exit(), Event::Input(InputEvent::KeyDown(k)) => k, Event::Input(_) => continue, }; diff --git a/crates/libkos/src/display.rs b/crates/libkos/src/display.rs index 8509f6d..4600246 100644 --- a/crates/libkos/src/display.rs +++ b/crates/libkos/src/display.rs @@ -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 { 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 { } } -/// `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 { 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 => {} } } } diff --git a/crates/manifest/src/capability.rs b/crates/manifest/src/capability.rs index fa585e1..c9bed41 100644 --- a/crates/manifest/src/capability.rs +++ b/crates/manifest/src/capability.rs @@ -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 { diff --git a/crates/servers/display-server/src/server.rs b/crates/servers/display-server/src/server.rs index c673e72..b227414 100644 --- a/crates/servers/display-server/src/server.rs +++ b/crates/servers/display-server/src/server.rs @@ -94,6 +94,16 @@ impl DisplayServer { &mut self.host } + #[cfg(test)] + pub fn windows_front_for_test(&self) -> Option { + self.windows.front() + } + + #[cfg(test)] + pub fn slot_for_test(&self, badge: usize) -> Option { + 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 DisplayServer { 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 DisplayServer { 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 DisplayServer { } } -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 { 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 Server for DisplayServer { @@ -306,14 +341,14 @@ impl Server for DisplayServer { } } - /// 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; } diff --git a/crates/servers/display-server/src/tests/server.rs b/crates/servers/display-server/src/tests/server.rs index 85c9c19..eb6f1e0 100644 --- a/crates/servers/display-server/src/tests/server.rs +++ b/crates/servers/display-server/src/tests/server.rs @@ -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()); +} diff --git a/crates/servers/display-server/src/tests/windows.rs b/crates/servers/display-server/src/tests/windows.rs index 9fe6cb4..4dab0bd 100644 --- a/crates/servers/display-server/src/tests/windows.rs +++ b/crates/servers/display-server/src/tests/windows.rs @@ -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::>(), [a, b]); } + +#[test] +fn closing_the_front_window_brings_back_the_one_before_it() { + let mut w: Windows = 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)); +} diff --git a/crates/servers/display-server/src/windows.rs b/crates/servers/display-server/src/windows.rs index ff77a9c..0e52ec2 100644 --- a/crates/servers/display-server/src/windows.rs +++ b/crates/servers/display-server/src/windows.rs @@ -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 { accent: Option, /// 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, + /// When it last came to the front (`Windows::raises`). + raised: u64, } pub struct Windows { @@ -61,11 +66,13 @@ pub struct Windows { front: Option, /// 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 Windows { 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 Windows { 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 Windows { 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 Windows { pub fn cycle(&mut self) -> Option { 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 { self.front } @@ -140,6 +154,40 @@ impl Windows { } } + /// 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 { + self.slots.get(index)?.as_ref()?.role + } + + /// The first open window that is `role`. + pub fn with_role(&self, role: usize) -> Option { + 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 { + 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 { self.slots[self.slot(badge)?].as_ref()?.accent } diff --git a/crates/servers/permission-manager/src/lib.rs b/crates/servers/permission-manager/src/lib.rs index e7c699d..9aa5e61 100644 --- a/crates/servers/permission-manager/src/lib.rs +++ b/crates/servers/permission-manager/src/lib.rs @@ -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) } diff --git a/crates/servers/permission-manager/src/server.rs b/crates/servers/permission-manager/src/server.rs index 86789cd..6468795 100644 --- a/crates/servers/permission-manager/src/server.rs +++ b/crates/servers/permission-manager/src/server.rs @@ -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; 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 { 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) { } /// 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) { diff --git a/crates/servers/permission-manager/src/store.rs b/crates/servers/permission-manager/src/store.rs index 57dfb61..9f34b48 100644 --- a/crates/servers/permission-manager/src/store.rs +++ b/crates/servers/permission-manager/src/store.rs @@ -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 { diff --git a/crates/servers/permission-manager/src/tests/mod.rs b/crates/servers/permission-manager/src/tests/mod.rs index c7b1cca..20fb198 100644 --- a/crates/servers/permission-manager/src/tests/mod.rs +++ b/crates/servers/permission-manager/src/tests/mod.rs @@ -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)); } diff --git a/crates/servers/storage-server/src/server.rs b/crates/servers/storage-server/src/server.rs index 9e7d7c4..e37dd81 100644 --- a/crates/servers/storage-server/src/server.rs +++ b/crates/servers/storage-server/src/server.rs @@ -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; diff --git a/crates/types/src/apps.rs b/crates/types/src/apps.rs index 7c65251..0ba541a 100644 --- a/crates/types/src/apps.rs +++ b/crates/types/src/apps.rs @@ -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; diff --git a/crates/types/src/display.rs b/crates/types/src/display.rs index a20d669..9bd57ab 100644 --- a/crates/types/src/display.rs +++ b/crates/types/src/display.rs @@ -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. diff --git a/docs/apps.md b/docs/apps.md index 6de14ed..94c82ea 100644 --- a/docs/apps.md +++ b/docs/apps.md @@ -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//`, 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 diff --git a/docs/ipc.md b/docs/ipc.md index 0c91f04..17f5eee 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -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 diff --git a/scripts/make-disk-image.sh b/scripts/make-disk-image.sh index 853a27a..a8f9b2e 100755 --- a/scripts/make-disk-image.sh +++ b/scripts/make-disk-image.sh @@ -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/"