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:
15
crates/apps/settings/Cargo.toml
Normal file
15
crates/apps/settings/Cargo.toml
Normal 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 }
|
||||
10
crates/apps/settings/manifest.toml
Normal file
10
crates/apps/settings/manifest.toml
Normal 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"]
|
||||
239
crates/apps/settings/src/main.rs
Normal file
239
crates/apps/settings/src/main.rs
Normal 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>();
|
||||
}
|
||||
Reference in New Issue
Block a user