feat(settings): settings app: apps list + per-capability allow/revoke, about (who/role), sign out; system-only caps permissions/session (install refused); pm MANAGE_BADGE copy (list/apps/allow/revoke only), APPS op; init WHO + session pass-on; factory decisions seeded for new system apps; k-ui list_item/toggle, screens/back/scroll

This commit is contained in:
2026-10-28 13:14:35 +01:00
parent a2d4a7d085
commit 29becf297c
38 changed files with 947 additions and 85 deletions

View File

@@ -0,0 +1,15 @@
[package]
name = "settings"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "settings"
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.settings"
name = "Settings"
version = "1.0.0"
binary = "settings"
author = "K-OS"
memory = "3 MiB"
[capabilities]
required = ["display", "permissions", "session"]

View File

@@ -0,0 +1,239 @@
//! Settings: what each app may use (allow or revoke, through the
//! permission manager), who is signed in, and signing out. A system app:
//! `permissions` and `session` are 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, ButtonVariant, Element, Spacing, TextColour, TextStyle, button, column, list_item, text, toggle};
use libkos::apps::Apps;
use libkos::session::Role;
use manifest::{Capabilities, Capability};
use types::address::VirtualAddress;
/// Where the request page for the permission manager is mapped.
const REQUEST_PAGE_VA: VirtualAddress = VirtualAddress::from_raw(0x0190_0000);
const LISTING: usize = 4096;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Screen {
Home,
Apps,
/// One app, by its place in the list.
App(usize),
About,
}
#[derive(Clone, Copy)]
enum Event {
Show(Screen),
/// Allow or revoke this capability of the app on screen.
Flip(Capability),
SignOut,
}
struct AppRow {
id: String,
name: String,
required: Capabilities,
requested: Capabilities,
granted: Capabilities,
}
struct Settings {
screen: Screen,
apps: Option<Apps>,
rows: Vec<AppRow>,
/// Who is signed in, and how.
who: Option<(String, Role)>,
/// What went wrong last, shown until the next change.
trouble: Option<String>,
}
/// What a capability lets an app do, as the user reads it.
fn describe(c: Capability) -> &'static str {
match c {
Capability::Display => "A window, and the keyboard while it is in front",
Capability::Storage => "A folder of its own",
Capability::Permissions => "Change what other apps may use",
Capability::Session => "See who is signed in, and sign out",
Capability::Network => "The network",
_ => c.name(),
}
}
impl Settings {
fn reload(&mut self) {
self.rows.clear();
let Some(apps) = &self.apps else { return };
let mut buf = [0u8; LISTING];
let Ok(n) = apps.apps(&mut buf) else {
self.trouble = Some("The permission manager did not answer.".to_string());
return;
};
let text = core::str::from_utf8(&buf[..n]).unwrap_or("");
for e in libkos::apps::entries(text) {
let required = Capabilities::from_bits(e.required);
self.rows.push(AppRow {
id: e.id.to_string(),
name: e.name.to_string(),
required,
requested: required.union(Capabilities::from_bits(e.optional)),
granted: Capabilities::from_bits(e.granted),
});
}
}
fn flip(&mut self, index: usize, cap: Capability) {
let (Some(apps), Some(row)) = (&self.apps, self.rows.get(index)) else { return };
let change = if row.granted.contains(cap) { apps.revoke(&row.id, cap.index()) } else { apps.allow(&row.id, cap.index()) };
self.trouble = change.err().map(|r| format!("That did not change (code {}).", r.code));
self.reload();
}
fn heading(title: &str) -> k_ui::element::FlexBuilder<Event> {
column().padding(Spacing::Xl).gap(Spacing::Md).child(text(title).style(TextStyle::Heading))
}
fn hint(view: k_ui::element::FlexBuilder<Event>, s: &str) -> k_ui::element::FlexBuilder<Event> {
view.child(text(s).style(TextStyle::Caption).colour(TextColour::Secondary))
}
fn home(&self) -> Element<Event> {
Self::heading("Settings")
.child(list_item("Apps", Event::Show(Screen::Apps)).subtitle("What each app may use"))
.child(list_item("About", Event::Show(Screen::About)).subtitle("Who is signed in, and this device"))
.child(button("Sign out", Event::SignOut).variant(ButtonVariant::Destructive))
.into()
}
fn app_list(&self) -> Element<Event> {
let rows = self.rows.iter().enumerate().map(|(i, r)| {
let allowed = r.granted.intersect(r.requested).iter().count();
let asked = r.requested.iter().count();
list_item(r.name.clone(), Event::Show(Screen::App(i))).subtitle(format!("{} · {} of {} allowed", r.id, allowed, asked))
});
let view = Self::hint(Self::heading("Apps"), "Esc goes back").children(rows);
self.with_trouble(view).into()
}
fn app(&self, index: usize) -> Element<Event> {
let Some(r) = self.rows.get(index) else { return self.app_list() };
let mut view = Self::hint(Self::heading(&r.name), &r.id);
if r.requested.is_empty() {
view = Self::hint(view, "It asks for nothing.");
}
for cap in r.requested.iter() {
let label = match r.required.contains(cap) {
true => format!("{} (needed to run)", describe(cap)),
false => describe(cap).to_string(),
};
view = view.child(toggle(label, r.granted.contains(cap), Event::Flip(cap)));
}
if !r.requested.is_empty() {
view = Self::hint(view, "Taking something away from a running app stops it.");
}
self.with_trouble(view).into()
}
fn about(&self) -> Element<Event> {
let (name, role) = match &self.who {
Some((n, Role::Owner)) => (n.as_str(), "an owner of this device"),
Some((n, Role::Standard)) => (n.as_str(), "a user of this device"),
Some((_, Role::Guest)) => ("Guest", "a guest: nothing is kept"),
None => ("?", "unknown"),
};
Self::hint(Self::heading("About"), "Esc goes back")
.child(text(format!("Signed in as {name}")).style(TextStyle::Subheading))
.child(text(format!("You are {role}.")))
.child(text(format!("K-OS {}", env!("CARGO_PKG_VERSION"))))
.child(text("RISC-V, on QEMU's virt board").colour(TextColour::Secondary))
.into()
}
fn with_trouble(&self, view: k_ui::element::FlexBuilder<Event>) -> k_ui::element::FlexBuilder<Event> {
match &self.trouble {
Some(t) => view.child(text(t.clone()).colour(TextColour::Accent)),
None => view,
}
}
}
impl App for Settings {
type Event = Event;
fn init() -> Self {
let apps = libkos::start::permissions().and_then(|ep| Apps::connect(ep, REQUEST_PAGE_VA));
let mut name = [0u8; types::name::BYTES];
let who = libkos::start::session()
.and_then(|s| libkos::session::who(s, &mut name))
.map(|(len, role)| (String::from_utf8_lossy(&name[..len]).into_owned(), role));
let mut s = Settings { screen: Screen::Home, apps, rows: Vec::new(), who, trouble: None };
s.reload();
s
}
fn update(&mut self, event: Event) {
match event {
Event::Show(screen) => {
self.trouble = None;
if screen == Screen::Apps {
self.reload();
}
self.screen = screen;
}
Event::Flip(cap) => {
if let Screen::App(i) = self.screen {
self.flip(i, cap);
}
}
Event::SignOut => {
if let Some(s) = libkos::start::session() {
libkos::session::sign_out(s);
}
}
}
}
fn view(&self) -> Element<Event> {
match self.screen {
Screen::Home => self.home(),
Screen::Apps => self.app_list(),
Screen::App(i) => self.app(i),
Screen::About => self.about(),
}
}
fn back(&mut self) -> bool {
self.trouble = None;
self.screen = match self.screen {
Screen::Home => return false,
Screen::App(_) => Screen::Apps,
Screen::Apps | Screen::About => Screen::Home,
};
true
}
fn screen(&self) -> usize {
match self.screen {
Screen::Home => 0,
Screen::Apps => 1,
Screen::About => 2,
Screen::App(i) => 3 + i,
}
}
}
libkos::entry!(main);
fn main() {
k_ui::run::<Settings>();
}