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:
90
crates/init/src/appearance.rs
Normal file
90
crates/init/src/appearance.rs
Normal 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]);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
mod appearance;
|
||||
mod devices;
|
||||
mod factory;
|
||||
mod launch;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user