feat(theme): themes as files (theme crate: toml subset + ints, contrast check, wire); whimsy dark + light; display reads themes/, serves THEME/THEME_NAME, init SET_THEME -> THEME_CHANGED to all windows, bar themed; k-ui follows; settings appearance; init keeps choice in sealed users/<id>/settings/, reapplies at sign-in + display restart

This commit is contained in:
2026-10-28 13:36:57 +01:00
parent 29becf297c
commit 6827e1455d
43 changed files with 1140 additions and 179 deletions

View File

@@ -0,0 +1,90 @@
//! Which theme each user chose. Settings asks through init's session copy
//! (`types::spawn::SET_THEME`); init keeps the name in the user's folder
//! (`users/<id>/settings/theme`, sealed) and tells the display, at
//! sign-in, at sign-out (the default), and when the display restarts.
use libkos::fs::Storage;
use types::display::window::NAME_BYTES;
use types::user::UserFolder;
use crate::launch::Launcher;
/// A theme's name; empty is the display's default.
#[derive(Clone, Copy)]
pub(crate) struct Choice {
name: [u8; NAME_BYTES],
len: usize,
}
impl Choice {
pub(crate) const DEFAULT: Self = Self { name: [0; NAME_BYTES], len: 0 };
fn of(name: &[u8]) -> Option<Self> {
// What a theme file's name may be (`theme::parse`).
let fits = name.len() <= NAME_BYTES && name.iter().all(|b| (b' '..=b'~').contains(b) && *b != b'"' && *b != b'\\');
fits.then(|| {
let mut c = Self::DEFAULT;
c.name[..name.len()].copy_from_slice(name);
c.len = name.len();
c
})
}
}
fn path(folder: UserFolder, out: &mut [u8; 64]) -> Option<&[u8]> {
let parts: [&[u8]; 3] = [b"users/", folder.as_bytes(), b"/settings/theme"];
let mut n = 0;
for p in parts {
out.get_mut(n..n + p.len())?.copy_from_slice(p);
n += p.len();
}
Some(&out[..n])
}
/// What `folder`'s user chose, or the default.
pub(crate) fn load(fs: &mut Storage, folder: UserFolder) -> Choice {
let mut buf = [0u8; 64];
let Some(path) = path(folder, &mut buf) else { return Choice::DEFAULT };
let Ok(h) = fs.open(path) else { return Choice::DEFAULT };
let mut name = [0u8; NAME_BYTES + 1];
let read = fs.read(h, &mut name);
let _ = fs.close(h);
read.ok().and_then(|n| Choice::of(&name[..n])).unwrap_or(Choice::DEFAULT)
}
fn save(fs: &mut Storage, folder: UserFolder, c: &Choice) -> bool {
let mut buf = [0u8; 64];
let Some(path) = path(folder, &mut buf) else { return false };
// Gone or not, it is written anew.
let _ = fs.delete(path);
let Ok(h) = fs.create(path) else { return false };
let written = fs.write(h, &c.name[..c.len]) == Ok(c.len);
fs.close(h).is_ok() && written
}
/// Settings chose the theme named in `words`: kept, and shown.
pub(crate) fn choose(l: &mut Launcher, words: [usize; 3]) -> bool {
let (name, len) = types::name::unpack(words);
let (Some(choice), Some(user)) = (Choice::of(&name[..len]), l.user) else { return false };
let (fs, _) = l.storage();
if !save(fs, user.folder, &choice) {
return false;
}
if let Some(u) = l.user.as_mut() {
u.theme = choice;
}
tell_display(l);
true
}
/// The display hears who is in and their theme (nobody, the default).
pub(crate) fn tell_display(l: &Launcher) {
let Some(display) = l.endpoint("display") else { return };
let (name, theme) = match &l.user {
Some(u) => (u.name(), u.theme),
None => (&[][..], Choice::DEFAULT),
};
crate::output::post(display, libkos::display::set_user_words(name));
let [a, b, c] = types::name::pack(&theme.name[..theme.len]);
crate::output::post(display, [types::display::window::SET_THEME, a, b, c]);
}

View File

@@ -8,7 +8,13 @@ use libkos::fs::Storage;
use types::storage::MAX_PATH;
/// Each area of a user's folder, and what it starts with.
const AREAS: [(&str, &str); 4] = [("home", "factory/home"), ("permissions", "factory/permissions"), ("data", "factory/data"), ("apps", "factory/apps")];
const AREAS: [(&str, &str); 5] = [
("home", "factory/home"),
("permissions", "factory/permissions"),
("settings", "factory/settings"),
("data", "factory/data"),
("apps", "factory/apps"),
];
/// Entries handled per listing; a folder with more is listed again.
const BATCH: usize = 32;
const NAME: usize = types::storage::DIR_NAME_SIZE;

View File

@@ -3,6 +3,7 @@
extern crate alloc;
mod appearance;
mod devices;
mod factory;
mod launch;

View File

@@ -111,13 +111,15 @@ pub const SERVERS: &[Program] = &[
Program {
devices: &[Device::Gpu],
uses: &["input"],
// The themes, read once at start.
storage: &[disk(b"themes", false)],
// Its clients subscribe and take focus.
restarts_clients: true,
// Its windows' state (input queues among it) is KiBs.
stack_pages: 8,
// The framebuffer is ~1000 pages at 1280x800, and so is each of
// the (at most 4) windows' pixel buffers, which it owns.
..server("display", b"bin/display", Limits { pages: 5200, regions: 8, ..SMALL })
..server("display", b"bin/display", Limits { pages: 5200, regions: 9, ..SMALL })
},
// Text on the display, lines from the keyboard (it takes input focus).
// Its clients wait on lines and Ctrl+C. It draws its own text now,

View File

@@ -26,9 +26,14 @@ pub(crate) struct User {
name_len: usize,
/// As `types::spawn::role` says.
role: usize,
pub(crate) theme: crate::appearance::Choice,
}
impl User {
pub(crate) fn name(&self) -> &[u8] {
&self.name[..self.name_len]
}
/// The reply to `types::spawn::WHO`.
pub(crate) fn who(&self) -> [usize; 4] {
let [a, b, c] = types::name::pack(&self.name[..self.name_len]);
@@ -121,15 +126,14 @@ pub(crate) fn sign_in(l: &mut Launcher) -> Result<(), &'static str> {
crate::factory::prepare(fs, folder, scratch)?;
let name = core::str::from_utf8(&h.name[..h.name_len]).unwrap_or("someone");
crate::outln!("init: {} is in", name);
if let Some(display) = l.endpoint("display") {
crate::output::post(display, libkos::display::set_user_words(name.as_bytes()));
}
let theme = crate::appearance::load(fs, folder);
let role = match (guest, h.role) {
(true, _) => types::spawn::role::GUEST,
(_, unlocked::OWNER) => types::spawn::role::OWNER,
_ => types::spawn::role::STANDARD,
};
l.user = Some(User { folder, guest, name: h.name, name_len: h.name_len, role });
l.user = Some(User { folder, guest, name: h.name, name_len: h.name_len, role, theme });
crate::appearance::tell_display(l);
start_session(l);
Ok(())
}
@@ -150,10 +154,8 @@ fn start_session(l: &mut Launcher) {
pub(crate) fn sign_out(l: &mut Launcher) {
l.spawner.clear();
l.stop_session();
if let Some(display) = l.endpoint("display") {
crate::output::post(display, libkos::display::set_user_words(b""));
}
let user = l.user.take();
crate::appearance::tell_display(l);
let (fs, _) = l.storage();
let locked = fs.lock().is_ok();
// A guest leaves nothing behind.

View File

@@ -188,6 +188,7 @@ impl Server for Launcher<'_> {
([MALFORMED, 0, 0, 0], None)
}
types::spawn::WHO => (self.user.map_or([MALFORMED, 0, 0, 0], |u| u.who()), None),
types::spawn::SET_THEME if crate::appearance::choose(self, [words[1], words[2], words[3]]) => ([reply::OK, 0, 0, 0], None),
_ => ([MALFORMED, 0, 0, 0], None),
};
}

View File

@@ -324,6 +324,10 @@ impl Launcher<'_> {
..s
});
self.ping(index);
if s.program.name == "display" {
// What it showed went with it.
crate::appearance::tell_display(self);
}
if s.program.restarts_clients {
self.restart_users_of(s.program.name);
}