feat(k-ui): first slice: App trait + run loop, builders (column/row/text/button), flex layout, theme tokens (Whimsy Dark), tiny-skia + fontdue render (Noto Sans Latin subset), keyboard focus ring; tally demo; manifest memory key (pm spawns with it)

This commit is contained in:
2026-10-28 03:20:41 +01:00
parent c256f4ecf3
commit fe73b89525
27 changed files with 1428 additions and 6 deletions

View File

@@ -144,3 +144,32 @@ impl Accent {
u32::from_str_radix(hex, 16).ok().map(Self)
}
}
/// How much memory an app may use, in 4 KiB pages: written `"512 KiB"` or
/// `"3 MiB"`, from 64 KiB to 16 MiB.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Memory(u16);
impl Memory {
/// What an app gets unless its manifest says more: enough for a small
/// command-line program.
pub const DEFAULT: Self = Self(128);
const MIN_PAGES: u32 = 16;
const MAX_PAGES: u32 = 4096;
pub fn parse(s: &str) -> Option<Self> {
let (n, unit) = s.split_once(' ')?;
let n: u32 = n.parse().ok()?;
let kib = match unit {
"KiB" => n,
"MiB" => n.checked_mul(1024)?,
_ => return None,
};
let pages = kib.div_ceil(4);
(kib % 4 == 0 && (Self::MIN_PAGES..=Self::MAX_PAGES).contains(&pages)).then_some(Self(pages as u16))
}
pub fn pages(&self) -> usize {
self.0 as usize
}
}

View File

@@ -16,7 +16,7 @@ mod tests;
pub use capability::{Capabilities, Capability};
pub use decision::{Decision, MAX_TEXT as MAX_DECISION_TEXT};
pub use fields::{Accent, AppId, Command, FileName, Text, Version};
pub use fields::{Accent, AppId, Command, FileName, Memory, Text, Version};
pub use manifest::Manifest;
/// Why a manifest or decision was refused; `line` is 1-based.

View File

@@ -2,7 +2,7 @@
use crate::Error;
use crate::capability::{Capabilities, Capability};
use crate::fields::{Accent, AppId, Command, FileName, Text, Version};
use crate::fields::{Accent, AppId, Command, FileName, Memory, Text, Version};
use crate::toml::{self, Entry, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -15,6 +15,8 @@ pub struct Manifest {
pub command: Command,
pub author: Option<Text<64>>,
pub icon: Option<FileName>,
/// How much memory it may use.
pub memory: Memory,
/// Denied = not installed.
pub required: Capabilities,
/// The owner decides each.
@@ -32,6 +34,7 @@ struct Draft {
command: Option<Command>,
author: Option<Text<64>>,
icon: Option<FileName>,
memory: Option<Memory>,
required: Option<Capabilities>,
optional: Option<Capabilities>,
accent: Option<Accent>,
@@ -81,6 +84,7 @@ impl Draft {
("app", "command") => set(&mut self.command, s.and_then(Command::new), l),
("app", "author") => set(&mut self.author, s.and_then(Text::new), l),
("app", "icon") => set(&mut self.icon, s.and_then(FileName::new), l),
("app", "memory") => set(&mut self.memory, s.and_then(Memory::parse), l),
("capabilities", "required") => {
set(&mut self.required, Some(capabilities(e.value, l)?), l)
}
@@ -116,6 +120,7 @@ impl Manifest {
command,
author: d.author,
icon: d.icon,
memory: d.memory.unwrap_or(Memory::DEFAULT),
required,
optional,
accent: d.accent,

View File

@@ -239,3 +239,13 @@ fn every_capability_name_round_trips_and_fits_the_set() {
all
);
}
#[test]
fn memory_is_in_kib_or_mib_within_bounds() {
use crate::Memory;
assert_eq!(Memory::parse("3 MiB").map(|m| m.pages()), Some(768));
assert_eq!(Memory::parse("512 KiB").map(|m| m.pages()), Some(128));
for bad in ["3MiB", "3 GiB", "17 MiB", "32 KiB", "6 KiB", "-1 MiB", "x MiB"] {
assert_eq!(Memory::parse(bad), None, "{bad}");
}
}