From 4103af8cd7216a0cf899d4158012d03a3f2f074e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Mar 2026 05:25:21 +0000 Subject: [PATCH 1/2] [TRACK_B] IMPL: Add riina-ui (SINAR/RUPA/SUSUN) + riina-os (TERAS) crates (+69 tests) riina-ui (Phase 8 - Platform + Rendering): - SINAR: Renderer trait for backend abstraction - RUPA: Type-safe styling with WCAG 2.1 contrast ratio (AA/AAA) - LUKIS: Declarative UI element tree evaluation - SUSUN: Layout engine with row/column positioning and overlap detection - SENTUH: Input event handling with keyboard/mouse support - Terminal renderer (ANSI escape codes) - HTML renderer (div/span with inline CSS flexbox) riina-os (Phase 9 - OS Primitives): - Capability-based security with delegation and linear revocation - System call interface types (TERAS) - IPC message-passing with bounded channels - Memory management with page table map/unmap https://claude.ai/code/session_01A5jiDkqMahHqopV2mcaUt3 --- 03_PROTO/Cargo.toml | 6 + 03_PROTO/crates/riina-os/Cargo.toml | 12 ++ 03_PROTO/crates/riina-os/src/capability.rs | 134 +++++++++++++ 03_PROTO/crates/riina-os/src/ipc.rs | 124 ++++++++++++ 03_PROTO/crates/riina-os/src/lib.rs | 8 + 03_PROTO/crates/riina-os/src/memory.rs | 129 +++++++++++++ 03_PROTO/crates/riina-os/src/syscall.rs | 76 ++++++++ 03_PROTO/crates/riina-ui/Cargo.toml | 13 ++ 03_PROTO/crates/riina-ui/src/html.rs | 112 +++++++++++ 03_PROTO/crates/riina-ui/src/lib.rs | 11 ++ 03_PROTO/crates/riina-ui/src/lukis.rs | 89 +++++++++ 03_PROTO/crates/riina-ui/src/rupa.rs | 213 +++++++++++++++++++++ 03_PROTO/crates/riina-ui/src/sentuh.rs | 85 ++++++++ 03_PROTO/crates/riina-ui/src/sinar.rs | 56 ++++++ 03_PROTO/crates/riina-ui/src/susun.rs | 163 ++++++++++++++++ 03_PROTO/crates/riina-ui/src/terminal.rs | 103 ++++++++++ 16 files changed, 1334 insertions(+) create mode 100644 03_PROTO/crates/riina-os/Cargo.toml create mode 100644 03_PROTO/crates/riina-os/src/capability.rs create mode 100644 03_PROTO/crates/riina-os/src/ipc.rs create mode 100644 03_PROTO/crates/riina-os/src/lib.rs create mode 100644 03_PROTO/crates/riina-os/src/memory.rs create mode 100644 03_PROTO/crates/riina-os/src/syscall.rs create mode 100644 03_PROTO/crates/riina-ui/Cargo.toml create mode 100644 03_PROTO/crates/riina-ui/src/html.rs create mode 100644 03_PROTO/crates/riina-ui/src/lib.rs create mode 100644 03_PROTO/crates/riina-ui/src/lukis.rs create mode 100644 03_PROTO/crates/riina-ui/src/rupa.rs create mode 100644 03_PROTO/crates/riina-ui/src/sentuh.rs create mode 100644 03_PROTO/crates/riina-ui/src/sinar.rs create mode 100644 03_PROTO/crates/riina-ui/src/susun.rs create mode 100644 03_PROTO/crates/riina-ui/src/terminal.rs diff --git a/03_PROTO/Cargo.toml b/03_PROTO/Cargo.toml index a4f72900..784b91b3 100644 --- a/03_PROTO/Cargo.toml +++ b/03_PROTO/Cargo.toml @@ -20,6 +20,8 @@ members = [ "crates/riina-pkg", "crates/riina-wasm", "crates/riina-runtime", + "crates/riina-ui", + "crates/riina-os", "crates/riinac", ] @@ -50,6 +52,10 @@ riina-runtime = { path = "crates/riina-runtime" } riina-typechecker = { path = "crates/riina-typechecker" } riina-wasm = { path = "crates/riina-wasm" } +# Internal dependencies - Platform crates +riina-ui = { path = "crates/riina-ui" } +riina-os = { path = "crates/riina-os" } + # No external dependencies (Law 8) [workspace.lints.rust] diff --git a/03_PROTO/crates/riina-os/Cargo.toml b/03_PROTO/crates/riina-os/Cargo.toml new file mode 100644 index 00000000..b8e2b443 --- /dev/null +++ b/03_PROTO/crates/riina-os/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "riina-os" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +description = "RIINA OS primitives — TERAS kernel abstractions" + +[dependencies] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(test)'] } diff --git a/03_PROTO/crates/riina-os/src/capability.rs b/03_PROTO/crates/riina-os/src/capability.rs new file mode 100644 index 00000000..45c074da --- /dev/null +++ b/03_PROTO/crates/riina-os/src/capability.rs @@ -0,0 +1,134 @@ +//! Capability-based security system. +//! +//! Hardware capabilities with delegation and linear revocation. + +/// Permission flags for a capability. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Permissions { + pub read: bool, + pub write: bool, + pub execute: bool, +} + +impl Permissions { + pub const NONE: Self = Self { read: false, write: false, execute: false }; + pub const READ_ONLY: Self = Self { read: true, write: false, execute: false }; + pub const READ_WRITE: Self = Self { read: true, write: true, execute: false }; + pub const ALL: Self = Self { read: true, write: true, execute: true }; +} + +/// A capability granting access to a resource. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Capability { + pub resource: u64, + pub permissions: Permissions, + pub delegatable: bool, +} + +impl Capability { + pub fn new(resource: u64, permissions: Permissions, delegatable: bool) -> Self { + Self { resource, permissions, delegatable } + } + + pub fn can_read(&self) -> bool { + self.permissions.read + } + + pub fn can_write(&self) -> bool { + self.permissions.write + } + + pub fn can_execute(&self) -> bool { + self.permissions.execute + } + + /// Delegate this capability to another entity. + /// Returns `None` if the capability is not delegatable. + pub fn delegate(&self) -> Option { + if self.delegatable { + Some(self.clone()) + } else { + None + } + } + + /// Revoke this capability by consuming it (linear ownership). + /// The capability is moved into this function and cannot be used afterward. + pub fn revoke(self) { + // Consuming self ensures the capability cannot be used after revocation. + // No explicit drop needed — Rust's ownership system handles it. + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn capability_read_permission() { + let cap = Capability::new(1, Permissions::READ_ONLY, false); + assert!(cap.can_read()); + assert!(!cap.can_write()); + assert!(!cap.can_execute()); + } + + #[test] + fn capability_all_permissions() { + let cap = Capability::new(1, Permissions::ALL, false); + assert!(cap.can_read()); + assert!(cap.can_write()); + assert!(cap.can_execute()); + } + + #[test] + fn capability_no_permissions() { + let cap = Capability::new(1, Permissions::NONE, false); + assert!(!cap.can_read()); + assert!(!cap.can_write()); + assert!(!cap.can_execute()); + } + + #[test] + fn delegatable_capability_delegates() { + let cap = Capability::new(42, Permissions::READ_WRITE, true); + let delegated = cap.delegate(); + assert!(delegated.is_some()); + let d = delegated.unwrap(); + assert_eq!(d.resource, 42); + assert_eq!(d.permissions, Permissions::READ_WRITE); + } + + #[test] + fn non_delegatable_capability_fails() { + let cap = Capability::new(42, Permissions::READ_WRITE, false); + assert!(cap.delegate().is_none()); + } + + #[test] + fn revoke_consumes_capability() { + let cap = Capability::new(1, Permissions::ALL, true); + cap.revoke(); + // cap is moved — cannot use after revoke (enforced by Rust's ownership) + } + + #[test] + fn capability_equality() { + let a = Capability::new(1, Permissions::READ_ONLY, false); + let b = Capability::new(1, Permissions::READ_ONLY, false); + assert_eq!(a, b); + } + + #[test] + fn capability_inequality_resource() { + let a = Capability::new(1, Permissions::READ_ONLY, false); + let b = Capability::new(2, Permissions::READ_ONLY, false); + assert_ne!(a, b); + } + + #[test] + fn capability_inequality_permissions() { + let a = Capability::new(1, Permissions::READ_ONLY, false); + let b = Capability::new(1, Permissions::READ_WRITE, false); + assert_ne!(a, b); + } +} diff --git a/03_PROTO/crates/riina-os/src/ipc.rs b/03_PROTO/crates/riina-os/src/ipc.rs new file mode 100644 index 00000000..8837ecda --- /dev/null +++ b/03_PROTO/crates/riina-os/src/ipc.rs @@ -0,0 +1,124 @@ +//! IPC — Inter-process communication. +//! +//! Simple message-passing between endpoints. + +use std::collections::VecDeque; + +/// An IPC endpoint identifier. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Endpoint { + pub id: u64, +} + +/// A message sent between endpoints. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Message { + pub sender: u64, + pub payload: Vec, +} + +/// IPC error types. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IpcError { + EndpointNotFound, + ChannelFull, + ChannelEmpty, + PermissionDenied, +} + +/// A simple IPC channel backed by a bounded queue. +#[derive(Debug)] +pub struct Channel { + pub endpoint: Endpoint, + buffer: VecDeque, + capacity: usize, +} + +impl Channel { + pub fn new(endpoint: Endpoint, capacity: usize) -> Self { + Self { + endpoint, + buffer: VecDeque::with_capacity(capacity), + capacity, + } + } + + /// Send a message to this channel. + pub fn send(&mut self, msg: Message) -> Result<(), IpcError> { + if self.buffer.len() >= self.capacity { + return Err(IpcError::ChannelFull); + } + self.buffer.push_back(msg); + Ok(()) + } + + /// Receive the next message from this channel. + pub fn recv(&mut self) -> Result { + self.buffer.pop_front().ok_or(IpcError::ChannelEmpty) + } + + /// Check if the channel is empty. + pub fn is_empty(&self) -> bool { + self.buffer.is_empty() + } + + /// Number of pending messages. + pub fn len(&self) -> usize { + self.buffer.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_channel(cap: usize) -> Channel { + Channel::new(Endpoint { id: 1 }, cap) + } + + fn make_msg(sender: u64, data: &[u8]) -> Message { + Message { sender, payload: data.to_vec() } + } + + #[test] + fn send_and_recv() { + let mut ch = make_channel(10); + ch.send(make_msg(1, b"hello")).unwrap(); + let msg = ch.recv().unwrap(); + assert_eq!(msg.sender, 1); + assert_eq!(msg.payload, b"hello"); + } + + #[test] + fn recv_empty_channel() { + let mut ch = make_channel(10); + assert_eq!(ch.recv(), Err(IpcError::ChannelEmpty)); + } + + #[test] + fn send_full_channel() { + let mut ch = make_channel(2); + ch.send(make_msg(1, b"a")).unwrap(); + ch.send(make_msg(1, b"b")).unwrap(); + assert_eq!(ch.send(make_msg(1, b"c")), Err(IpcError::ChannelFull)); + } + + #[test] + fn fifo_ordering() { + let mut ch = make_channel(10); + ch.send(make_msg(1, b"first")).unwrap(); + ch.send(make_msg(2, b"second")).unwrap(); + assert_eq!(ch.recv().unwrap().payload, b"first"); + assert_eq!(ch.recv().unwrap().payload, b"second"); + } + + #[test] + fn len_and_is_empty() { + let mut ch = make_channel(10); + assert!(ch.is_empty()); + assert_eq!(ch.len(), 0); + ch.send(make_msg(1, b"x")).unwrap(); + assert!(!ch.is_empty()); + assert_eq!(ch.len(), 1); + } +} diff --git a/03_PROTO/crates/riina-os/src/lib.rs b/03_PROTO/crates/riina-os/src/lib.rs new file mode 100644 index 00000000..6085fea1 --- /dev/null +++ b/03_PROTO/crates/riina-os/src/lib.rs @@ -0,0 +1,8 @@ +//! RIINA OS primitives — TERAS kernel abstractions +//! +//! Phase 9: OS-level capability system, IPC, memory management. + +pub mod capability; +pub mod syscall; +pub mod ipc; +pub mod memory; diff --git a/03_PROTO/crates/riina-os/src/memory.rs b/03_PROTO/crates/riina-os/src/memory.rs new file mode 100644 index 00000000..01af16a2 --- /dev/null +++ b/03_PROTO/crates/riina-os/src/memory.rs @@ -0,0 +1,129 @@ +//! Memory management — page frame allocation and virtual address mapping. + +use std::collections::HashMap; + +/// A physical page frame. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PageFrame { + pub id: u64, + pub size: usize, +} + +/// A virtual address. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct VirtualAddress(pub u64); + +/// Memory management error types. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MemError { + AlreadyMapped, + NotMapped, + OutOfMemory, + InvalidAddress, +} + +/// A simple page table mapping virtual addresses to page frames. +#[derive(Debug, Default)] +pub struct PageTable { + mappings: HashMap, +} + +impl PageTable { + pub fn new() -> Self { + Self::default() + } + + /// Map a virtual address to a page frame. + pub fn map(&mut self, vaddr: VirtualAddress, frame: PageFrame) -> Result<(), MemError> { + if self.mappings.contains_key(&vaddr.0) { + return Err(MemError::AlreadyMapped); + } + self.mappings.insert(vaddr.0, frame); + Ok(()) + } + + /// Unmap a virtual address, returning the page frame. + pub fn unmap(&mut self, vaddr: VirtualAddress) -> Result { + self.mappings.remove(&vaddr.0).ok_or(MemError::NotMapped) + } + + /// Look up the page frame for a virtual address. + pub fn lookup(&self, vaddr: VirtualAddress) -> Option<&PageFrame> { + self.mappings.get(&vaddr.0) + } + + /// Number of active mappings. + pub fn mapping_count(&self) -> usize { + self.mappings.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn frame(id: u64) -> PageFrame { + PageFrame { id, size: 4096 } + } + + #[test] + fn map_and_lookup() { + let mut pt = PageTable::new(); + pt.map(VirtualAddress(0x1000), frame(1)).unwrap(); + let f = pt.lookup(VirtualAddress(0x1000)).unwrap(); + assert_eq!(f.id, 1); + assert_eq!(f.size, 4096); + } + + #[test] + fn unmap_returns_frame() { + let mut pt = PageTable::new(); + pt.map(VirtualAddress(0x2000), frame(2)).unwrap(); + let f = pt.unmap(VirtualAddress(0x2000)).unwrap(); + assert_eq!(f.id, 2); + } + + #[test] + fn unmap_removes_mapping() { + let mut pt = PageTable::new(); + pt.map(VirtualAddress(0x3000), frame(3)).unwrap(); + pt.unmap(VirtualAddress(0x3000)).unwrap(); + assert!(pt.lookup(VirtualAddress(0x3000)).is_none()); + } + + #[test] + fn double_map_error() { + let mut pt = PageTable::new(); + pt.map(VirtualAddress(0x4000), frame(4)).unwrap(); + assert_eq!( + pt.map(VirtualAddress(0x4000), frame(5)), + Err(MemError::AlreadyMapped) + ); + } + + #[test] + fn unmap_not_mapped_error() { + let mut pt = PageTable::new(); + assert_eq!( + pt.unmap(VirtualAddress(0x5000)), + Err(MemError::NotMapped) + ); + } + + #[test] + fn mapping_count() { + let mut pt = PageTable::new(); + assert_eq!(pt.mapping_count(), 0); + pt.map(VirtualAddress(0x1000), frame(1)).unwrap(); + pt.map(VirtualAddress(0x2000), frame(2)).unwrap(); + assert_eq!(pt.mapping_count(), 2); + pt.unmap(VirtualAddress(0x1000)).unwrap(); + assert_eq!(pt.mapping_count(), 1); + } + + #[test] + fn lookup_nonexistent() { + let pt = PageTable::new(); + assert!(pt.lookup(VirtualAddress(0xDEAD)).is_none()); + } +} diff --git a/03_PROTO/crates/riina-os/src/syscall.rs b/03_PROTO/crates/riina-os/src/syscall.rs new file mode 100644 index 00000000..8906c430 --- /dev/null +++ b/03_PROTO/crates/riina-os/src/syscall.rs @@ -0,0 +1,76 @@ +//! System call interface types. +//! +//! Defines the system call numbers and result types for TERAS. + +/// System call identifiers. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SyscallId { + Read, + Write, + Open, + Close, + Mmap, + Munmap, + Exit, + Fork, + Exec, + Wait, + Send, + Recv, +} + +/// Result of a system call. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SyscallResult { + Success(u64), + Error(SyscallError), +} + +/// System call error codes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SyscallError { + PermissionDenied, + InvalidArgument, + NotFound, + AlreadyExists, + OutOfMemory, + IoError, +} + +/// A system call request with arguments. +#[derive(Debug, Clone)] +pub struct SyscallRequest { + pub id: SyscallId, + pub args: [u64; 4], +} + +impl SyscallRequest { + pub fn new(id: SyscallId, args: [u64; 4]) -> Self { + Self { id, args } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn syscall_request_creation() { + let req = SyscallRequest::new(SyscallId::Read, [1, 1024, 0, 0]); + assert_eq!(req.id, SyscallId::Read); + assert_eq!(req.args[0], 1); + assert_eq!(req.args[1], 1024); + } + + #[test] + fn syscall_result_success() { + let result = SyscallResult::Success(42); + assert_eq!(result, SyscallResult::Success(42)); + } + + #[test] + fn syscall_result_error() { + let result = SyscallResult::Error(SyscallError::PermissionDenied); + assert_eq!(result, SyscallResult::Error(SyscallError::PermissionDenied)); + } +} diff --git a/03_PROTO/crates/riina-ui/Cargo.toml b/03_PROTO/crates/riina-ui/Cargo.toml new file mode 100644 index 00000000..8c8403d3 --- /dev/null +++ b/03_PROTO/crates/riina-ui/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "riina-ui" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +description = "RIINA UI rendering — SINAR/RUPA/LUKIS/SUSUN/SENTUH" + +[dependencies] +riina-types.workspace = true + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(test)'] } diff --git a/03_PROTO/crates/riina-ui/src/html.rs b/03_PROTO/crates/riina-ui/src/html.rs new file mode 100644 index 00000000..8f69f225 --- /dev/null +++ b/03_PROTO/crates/riina-ui/src/html.rs @@ -0,0 +1,112 @@ +//! HTML renderer — generates HTML/CSS output. + +use crate::sinar::Renderer; + +/// A renderer that outputs HTML with inline CSS styling. +pub struct HtmlRenderer { + output: String, +} + +impl HtmlRenderer { + pub fn new() -> Self { + Self { output: String::new() } + } +} + +impl Default for HtmlRenderer { + fn default() -> Self { + Self::new() + } +} + +impl Renderer for HtmlRenderer { + fn render_text(&mut self, text: &str, color: (u8, u8, u8)) { + self.output.push_str(&format!( + "{}", + color.0, color.1, color.2, text + )); + self.output.push_str(""); + } + + fn render_rect(&mut self, x: u32, y: u32, w: u32, h: u32, color: (u8, u8, u8)) { + self.output.push_str(&format!( + "
" + , color.0, color.1, color.2 + )); + self.output.push_str("
"); + } + + fn begin_row(&mut self) { + self.output.push_str("
"); + } + + fn end_row(&mut self) { + self.output.push_str("
"); + } + + fn begin_column(&mut self) { + self.output.push_str("
"); + } + + fn end_column(&mut self) { + self.output.push_str("
"); + } + + fn output(&self) -> &str { + &self.output + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn html_text_span() { + let mut r = HtmlRenderer::new(); + r.render_text("hello", (255, 0, 0)); + let out = r.output(); + assert!(out.contains("")); + } + + #[test] + fn html_rect_div() { + let mut r = HtmlRenderer::new(); + r.render_rect(10, 20, 100, 50, (0, 128, 255)); + let out = r.output(); + assert!(out.contains("")); + } + + #[test] + fn html_row_flexbox() { + let mut r = HtmlRenderer::new(); + r.begin_row(); + r.end_row(); + let out = r.output(); + assert!(out.contains("flex-direction:row")); + } + + #[test] + fn html_column_flexbox() { + let mut r = HtmlRenderer::new(); + r.begin_column(); + r.end_column(); + let out = r.output(); + assert!(out.contains("flex-direction:column")); + } + + #[test] + fn html_default() { + let r = HtmlRenderer::default(); + assert_eq!(r.output(), ""); + } +} diff --git a/03_PROTO/crates/riina-ui/src/lib.rs b/03_PROTO/crates/riina-ui/src/lib.rs new file mode 100644 index 00000000..b7d5ed53 --- /dev/null +++ b/03_PROTO/crates/riina-ui/src/lib.rs @@ -0,0 +1,11 @@ +//! RIINA UI rendering — SINAR/RUPA/LUKIS/SUSUN/SENTUH +//! +//! Phase 8: Platform + Rendering subsystem for the RIINA language. + +pub mod sinar; +pub mod rupa; +pub mod lukis; +pub mod susun; +pub mod sentuh; +pub mod terminal; +pub mod html; diff --git a/03_PROTO/crates/riina-ui/src/lukis.rs b/03_PROTO/crates/riina-ui/src/lukis.rs new file mode 100644 index 00000000..20f450bd --- /dev/null +++ b/03_PROTO/crates/riina-ui/src/lukis.rs @@ -0,0 +1,89 @@ +//! LUKIS — Declarative UI evaluation. +//! +//! Provides a simple declarative UI tree that can be evaluated +//! against any `Renderer` backend. + +use crate::rupa::Style; +use crate::sinar::Renderer; + +/// A declarative UI element. +#[derive(Debug, Clone)] +pub enum Element { + /// Text content with styling. + Text(String, Style), + /// A horizontal row of child elements. + Row(Vec), + /// A vertical column of child elements. + Column(Vec), + /// A styled box containing a child element. + Box(Style, Box), +} + +/// Evaluate a UI element tree against a renderer. +pub fn evaluate(renderer: &mut dyn Renderer, element: &Element) { + match element { + Element::Text(text, style) => { + renderer.render_text(text, style.color); + } + Element::Row(children) => { + renderer.begin_row(); + for child in children { + evaluate(renderer, child); + } + renderer.end_row(); + } + Element::Column(children) => { + renderer.begin_column(); + for child in children { + evaluate(renderer, child); + } + renderer.end_column(); + } + Element::Box(style, child) => { + renderer.render_rect(0, 0, 0, 0, style.background); + evaluate(renderer, child); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::terminal::TerminalRenderer; + + #[test] + fn evaluate_text_element() { + let mut r = TerminalRenderer::new(); + let elem = Element::Text("hello".to_string(), Style::default()); + evaluate(&mut r, &elem); + assert!(r.output().contains("hello")); + } + + #[test] + fn evaluate_row_element() { + let mut r = TerminalRenderer::new(); + let elem = Element::Row(vec![ + Element::Text("a".to_string(), Style::default()), + Element::Text("b".to_string(), Style::default()), + ]); + evaluate(&mut r, &elem); + let out = r.output(); + assert!(out.contains("a")); + assert!(out.contains("b")); + } + + #[test] + fn evaluate_nested_column_in_row() { + let mut r = TerminalRenderer::new(); + let elem = Element::Row(vec![ + Element::Column(vec![ + Element::Text("top".to_string(), Style::default()), + Element::Text("bottom".to_string(), Style::default()), + ]), + ]); + evaluate(&mut r, &elem); + let out = r.output(); + assert!(out.contains("top")); + assert!(out.contains("bottom")); + } +} diff --git a/03_PROTO/crates/riina-ui/src/rupa.rs b/03_PROTO/crates/riina-ui/src/rupa.rs new file mode 100644 index 00000000..8100a4aa --- /dev/null +++ b/03_PROTO/crates/riina-ui/src/rupa.rs @@ -0,0 +1,213 @@ +//! RUPA — Type-safe styling with WCAG 2.1 accessibility checks. + +/// Compute relative luminance of an sRGB color per WCAG 2.1. +fn relative_luminance(color: (u8, u8, u8)) -> f64 { + let linearize = |c: u8| -> f64 { + let s = f64::from(c) / 255.0; + if s <= 0.04045 { + s / 12.92 + } else { + ((s + 0.055) / 1.055).powf(2.4) + } + }; + let r = linearize(color.0); + let g = linearize(color.1); + let b = linearize(color.2); + 0.2126 * r + 0.7152 * g + 0.0722 * b +} + +/// Type-safe styling descriptor. +#[derive(Debug, Clone)] +pub struct Style { + pub padding: u32, + pub margin: u32, + pub font_size: u32, + pub color: (u8, u8, u8), + pub background: (u8, u8, u8), +} + +impl Default for Style { + fn default() -> Self { + Self { + padding: 0, + margin: 0, + font_size: 16, + color: (0, 0, 0), + background: (255, 255, 255), + } + } +} + +impl Style { + /// Compute the WCAG 2.1 contrast ratio between foreground and background. + pub fn contrast_ratio(&self) -> f64 { + let fg_lum = relative_luminance(self.color); + let bg_lum = relative_luminance(self.background); + let (lighter, darker) = if fg_lum > bg_lum { + (fg_lum, bg_lum) + } else { + (bg_lum, fg_lum) + }; + (lighter + 0.05) / (darker + 0.05) + } + + /// Check if the style meets WCAG AA contrast requirement (>= 4.5:1). + pub fn meets_wcag_aa(&self) -> bool { + self.contrast_ratio() >= 4.5 + } + + /// Check if the style meets WCAG AAA contrast requirement (>= 7.0:1). + pub fn meets_wcag_aaa(&self) -> bool { + self.contrast_ratio() >= 7.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn black_on_white_maximum_contrast() { + let style = Style { + color: (0, 0, 0), + background: (255, 255, 255), + ..Style::default() + }; + let ratio = style.contrast_ratio(); + assert!((ratio - 21.0).abs() < 0.1, "Expected ~21:1, got {ratio}"); + } + + #[test] + fn white_on_black_maximum_contrast() { + let style = Style { + color: (255, 255, 255), + background: (0, 0, 0), + ..Style::default() + }; + let ratio = style.contrast_ratio(); + assert!((ratio - 21.0).abs() < 0.1, "Expected ~21:1, got {ratio}"); + } + + #[test] + fn same_color_minimum_contrast() { + let style = Style { + color: (128, 128, 128), + background: (128, 128, 128), + ..Style::default() + }; + let ratio = style.contrast_ratio(); + assert!((ratio - 1.0).abs() < 0.01, "Expected 1:1, got {ratio}"); + } + + #[test] + fn black_on_white_meets_wcag_aa() { + let style = Style::default(); + assert!(style.meets_wcag_aa()); + } + + #[test] + fn black_on_white_meets_wcag_aaa() { + let style = Style::default(); + assert!(style.meets_wcag_aaa()); + } + + #[test] + fn same_color_fails_wcag_aa() { + let style = Style { + color: (100, 100, 100), + background: (100, 100, 100), + ..Style::default() + }; + assert!(!style.meets_wcag_aa()); + } + + #[test] + fn same_color_fails_wcag_aaa() { + let style = Style { + color: (100, 100, 100), + background: (100, 100, 100), + ..Style::default() + }; + assert!(!style.meets_wcag_aaa()); + } + + #[test] + fn low_contrast_fails_aa() { + // Light gray on white — poor contrast + let style = Style { + color: (200, 200, 200), + background: (255, 255, 255), + ..Style::default() + }; + assert!(!style.meets_wcag_aa()); + } + + #[test] + fn medium_contrast_passes_aa_fails_aaa() { + // Dark gray on white — moderate contrast + let style = Style { + color: (100, 100, 100), + background: (255, 255, 255), + ..Style::default() + }; + let ratio = style.contrast_ratio(); + assert!(ratio >= 4.5, "Expected >= 4.5, got {ratio}"); + assert!(style.meets_wcag_aa()); + // This particular combo may or may not pass AAA; test the boundary + } + + #[test] + fn relative_luminance_black() { + let lum = relative_luminance((0, 0, 0)); + assert!((lum - 0.0).abs() < 0.001); + } + + #[test] + fn relative_luminance_white() { + let lum = relative_luminance((255, 255, 255)); + assert!((lum - 1.0).abs() < 0.001); + } + + #[test] + fn relative_luminance_pure_red() { + let lum = relative_luminance((255, 0, 0)); + assert!((lum - 0.2126).abs() < 0.001); + } + + #[test] + fn relative_luminance_pure_green() { + let lum = relative_luminance((0, 255, 0)); + assert!((lum - 0.7152).abs() < 0.001); + } + + #[test] + fn relative_luminance_pure_blue() { + let lum = relative_luminance((0, 0, 255)); + assert!((lum - 0.0722).abs() < 0.001); + } + + #[test] + fn default_style_values() { + let s = Style::default(); + assert_eq!(s.padding, 0); + assert_eq!(s.margin, 0); + assert_eq!(s.font_size, 16); + assert_eq!(s.color, (0, 0, 0)); + assert_eq!(s.background, (255, 255, 255)); + } + + #[test] + fn contrast_ratio_symmetry() { + let s1 = Style { + color: (50, 100, 150), + background: (200, 220, 240), + ..Style::default() + }; + let s2 = Style { + color: (200, 220, 240), + background: (50, 100, 150), + ..Style::default() + }; + assert!((s1.contrast_ratio() - s2.contrast_ratio()).abs() < 0.001); + } +} diff --git a/03_PROTO/crates/riina-ui/src/sentuh.rs b/03_PROTO/crates/riina-ui/src/sentuh.rs new file mode 100644 index 00000000..2f236d4a --- /dev/null +++ b/03_PROTO/crates/riina-ui/src/sentuh.rs @@ -0,0 +1,85 @@ +//! SENTUH — Input handling. +//! +//! Types for representing user input events. + +/// A keyboard key. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Key { + Char(char), + Enter, + Escape, + Tab, + Backspace, + Arrow(Direction), +} + +/// A direction for arrow keys and navigation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + Up, + Down, + Left, + Right, +} + +/// Modifier keys held during an input event. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct Modifiers { + pub ctrl: bool, + pub alt: bool, + pub shift: bool, +} + +/// An input event from the user. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InputEvent { + KeyPress(Key, Modifiers), + MouseClick { x: u32, y: u32 }, + MouseMove { x: u32, y: u32 }, +} + +/// Check if an input event matches a key with modifiers. +pub fn matches_shortcut(event: &InputEvent, key: Key, modifiers: Modifiers) -> bool { + matches!(event, InputEvent::KeyPress(k, m) if *k == key && *m == modifiers) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn shortcut_match() { + let event = InputEvent::KeyPress( + Key::Char('s'), + Modifiers { ctrl: true, alt: false, shift: false }, + ); + assert!(matches_shortcut( + &event, + Key::Char('s'), + Modifiers { ctrl: true, alt: false, shift: false }, + )); + } + + #[test] + fn shortcut_no_match_different_key() { + let event = InputEvent::KeyPress( + Key::Char('s'), + Modifiers { ctrl: true, alt: false, shift: false }, + ); + assert!(!matches_shortcut( + &event, + Key::Char('x'), + Modifiers { ctrl: true, alt: false, shift: false }, + )); + } + + #[test] + fn shortcut_no_match_mouse_event() { + let event = InputEvent::MouseClick { x: 10, y: 20 }; + assert!(!matches_shortcut( + &event, + Key::Char('s'), + Modifiers::default(), + )); + } +} diff --git a/03_PROTO/crates/riina-ui/src/sinar.rs b/03_PROTO/crates/riina-ui/src/sinar.rs new file mode 100644 index 00000000..4d5154a5 --- /dev/null +++ b/03_PROTO/crates/riina-ui/src/sinar.rs @@ -0,0 +1,56 @@ +//! SINAR — Rendering backend abstraction. +//! +//! Defines the `Renderer` trait that all rendering backends must implement. + +/// Trait for rendering backends (terminal, HTML, GPU, etc.). +pub trait Renderer { + /// Render text at the current position with the given foreground color. + fn render_text(&mut self, text: &str, color: (u8, u8, u8)); + + /// Render a filled rectangle at (x, y) with dimensions (w, h). + fn render_rect(&mut self, x: u32, y: u32, w: u32, h: u32, color: (u8, u8, u8)); + + /// Begin a horizontal row layout context. + fn begin_row(&mut self); + + /// End a horizontal row layout context. + fn end_row(&mut self); + + /// Begin a vertical column layout context. + fn begin_column(&mut self); + + /// End a vertical column layout context. + fn end_column(&mut self); + + /// Get the accumulated output (for testing and serialization). + fn output(&self) -> &str; +} + +#[cfg(test)] +mod tests { + use super::*; + + struct NullRenderer; + + impl Renderer for NullRenderer { + fn render_text(&mut self, _text: &str, _color: (u8, u8, u8)) {} + fn render_rect(&mut self, _x: u32, _y: u32, _w: u32, _h: u32, _color: (u8, u8, u8)) {} + fn begin_row(&mut self) {} + fn end_row(&mut self) {} + fn begin_column(&mut self) {} + fn end_column(&mut self) {} + fn output(&self) -> &str { "" } + } + + #[test] + fn null_renderer_implements_trait() { + let mut r = NullRenderer; + r.render_text("hello", (255, 255, 255)); + r.render_rect(0, 0, 100, 50, (0, 0, 0)); + r.begin_row(); + r.end_row(); + r.begin_column(); + r.end_column(); + assert_eq!(r.output(), ""); + } +} diff --git a/03_PROTO/crates/riina-ui/src/susun.rs b/03_PROTO/crates/riina-ui/src/susun.rs new file mode 100644 index 00000000..a5f5f188 --- /dev/null +++ b/03_PROTO/crates/riina-ui/src/susun.rs @@ -0,0 +1,163 @@ +//! SUSUN — Layout engine. +//! +//! Provides simple row/column layout computation with overlap detection. + +/// A positioned box in the layout. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LayoutBox { + pub x: u32, + pub y: u32, + pub width: u32, + pub height: u32, +} + +/// Lay out children in a horizontal row within a container. +/// Children are placed left-to-right starting at x=0, y=0. +pub fn layout_row(children: &[LayoutBox], _container_width: u32) -> Vec { + let mut result = Vec::with_capacity(children.len()); + let mut x_offset = 0u32; + for child in children { + result.push(LayoutBox { + x: x_offset, + y: 0, + width: child.width, + height: child.height, + }); + x_offset = x_offset.saturating_add(child.width); + } + result +} + +/// Lay out children in a vertical column within a container. +/// Children are placed top-to-bottom starting at x=0, y=0. +pub fn layout_column(children: &[LayoutBox], _container_height: u32) -> Vec { + let mut result = Vec::with_capacity(children.len()); + let mut y_offset = 0u32; + for child in children { + result.push(LayoutBox { + x: 0, + y: y_offset, + width: child.width, + height: child.height, + }); + y_offset = y_offset.saturating_add(child.height); + } + result +} + +/// Check that no two layout boxes overlap. +pub fn no_overlap(boxes: &[LayoutBox]) -> bool { + for i in 0..boxes.len() { + for j in (i + 1)..boxes.len() { + let a = &boxes[i]; + let b = &boxes[j]; + // Two rects overlap if they intersect on both axes + let x_overlap = a.x < b.x + b.width && b.x < a.x + a.width; + let y_overlap = a.y < b.y + b.height && b.y < a.y + a.height; + if x_overlap && y_overlap { + return false; + } + } + } + true +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_box(w: u32, h: u32) -> LayoutBox { + LayoutBox { x: 0, y: 0, width: w, height: h } + } + + #[test] + fn row_layout_positions() { + let children = vec![make_box(100, 50), make_box(200, 50), make_box(150, 50)]; + let laid_out = layout_row(&children, 800); + assert_eq!(laid_out[0].x, 0); + assert_eq!(laid_out[1].x, 100); + assert_eq!(laid_out[2].x, 300); + } + + #[test] + fn row_layout_preserves_dimensions() { + let children = vec![make_box(100, 50), make_box(200, 75)]; + let laid_out = layout_row(&children, 800); + assert_eq!(laid_out[0].width, 100); + assert_eq!(laid_out[0].height, 50); + assert_eq!(laid_out[1].width, 200); + assert_eq!(laid_out[1].height, 75); + } + + #[test] + fn column_layout_positions() { + let children = vec![make_box(100, 50), make_box(100, 75), make_box(100, 25)]; + let laid_out = layout_column(&children, 600); + assert_eq!(laid_out[0].y, 0); + assert_eq!(laid_out[1].y, 50); + assert_eq!(laid_out[2].y, 125); + } + + #[test] + fn column_layout_preserves_dimensions() { + let children = vec![make_box(100, 50), make_box(200, 75)]; + let laid_out = layout_column(&children, 600); + assert_eq!(laid_out[0].width, 100); + assert_eq!(laid_out[1].width, 200); + } + + #[test] + fn row_no_overlap() { + let children = vec![make_box(100, 50), make_box(100, 50), make_box(100, 50)]; + let laid_out = layout_row(&children, 800); + assert!(no_overlap(&laid_out)); + } + + #[test] + fn column_no_overlap() { + let children = vec![make_box(100, 50), make_box(100, 50), make_box(100, 50)]; + let laid_out = layout_column(&children, 600); + assert!(no_overlap(&laid_out)); + } + + #[test] + fn overlapping_boxes_detected() { + let boxes = vec![ + LayoutBox { x: 0, y: 0, width: 100, height: 100 }, + LayoutBox { x: 50, y: 50, width: 100, height: 100 }, + ]; + assert!(!no_overlap(&boxes)); + } + + #[test] + fn adjacent_boxes_no_overlap() { + let boxes = vec![ + LayoutBox { x: 0, y: 0, width: 100, height: 100 }, + LayoutBox { x: 100, y: 0, width: 100, height: 100 }, + ]; + assert!(no_overlap(&boxes)); + } + + #[test] + fn empty_layout_no_overlap() { + assert!(no_overlap(&[])); + } + + #[test] + fn single_box_no_overlap() { + let boxes = vec![LayoutBox { x: 0, y: 0, width: 100, height: 100 }]; + assert!(no_overlap(&boxes)); + } + + #[test] + fn empty_row_layout() { + let laid_out = layout_row(&[], 800); + assert!(laid_out.is_empty()); + } + + #[test] + fn empty_column_layout() { + let laid_out = layout_column(&[], 600); + assert!(laid_out.is_empty()); + } +} diff --git a/03_PROTO/crates/riina-ui/src/terminal.rs b/03_PROTO/crates/riina-ui/src/terminal.rs new file mode 100644 index 00000000..a584827a --- /dev/null +++ b/03_PROTO/crates/riina-ui/src/terminal.rs @@ -0,0 +1,103 @@ +//! Terminal renderer — ANSI escape code backend. + +use crate::sinar::Renderer; + +/// A renderer that outputs ANSI-colored terminal text. +pub struct TerminalRenderer { + output: String, +} + +impl TerminalRenderer { + pub fn new() -> Self { + Self { output: String::new() } + } +} + +impl Default for TerminalRenderer { + fn default() -> Self { + Self::new() + } +} + +impl Renderer for TerminalRenderer { + fn render_text(&mut self, text: &str, color: (u8, u8, u8)) { + self.output.push_str(&format!( + "\x1b[38;2;{};{};{}m{}\x1b[0m", + color.0, color.1, color.2, text + )); + } + + fn render_rect(&mut self, x: u32, y: u32, w: u32, h: u32, color: (u8, u8, u8)) { + self.output.push_str(&format!( + "\x1b[48;2;{};{};{}m[rect {x},{y} {w}x{h}]\x1b[0m", + color.0, color.1, color.2 + )); + } + + fn begin_row(&mut self) { + self.output.push_str("[row:"); + } + + fn end_row(&mut self) { + self.output.push(']'); + } + + fn begin_column(&mut self) { + self.output.push_str("[col:"); + } + + fn end_column(&mut self) { + self.output.push(']'); + } + + fn output(&self) -> &str { + &self.output + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn terminal_text_ansi_fg() { + let mut r = TerminalRenderer::new(); + r.render_text("hello", (255, 0, 0)); + assert!(r.output().contains("\x1b[38;2;255;0;0m")); + assert!(r.output().contains("hello")); + assert!(r.output().contains("\x1b[0m")); + } + + #[test] + fn terminal_rect_ansi_bg() { + let mut r = TerminalRenderer::new(); + r.render_rect(10, 20, 100, 50, (0, 255, 0)); + assert!(r.output().contains("\x1b[48;2;0;255;0m")); + } + + #[test] + fn terminal_row_markers() { + let mut r = TerminalRenderer::new(); + r.begin_row(); + r.render_text("item", (255, 255, 255)); + r.end_row(); + assert!(r.output().starts_with("[row:")); + assert!(r.output().ends_with(']')); + } + + #[test] + fn terminal_column_markers() { + let mut r = TerminalRenderer::new(); + r.begin_column(); + r.render_text("item", (255, 255, 255)); + r.end_column(); + assert!(r.output().starts_with("[col:")); + assert!(r.output().ends_with(']')); + } + + #[test] + fn terminal_default() { + let r = TerminalRenderer::default(); + assert_eq!(r.output(), ""); + } +} From f577ac9cac73ca5dd1f8bc8cbb6b2f21478efa30 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Mar 2026 05:25:47 +0000 Subject: [PATCH 2/2] [TRACK_B] CHORE: Update Cargo.lock for riina-ui and riina-os crates https://claude.ai/code/session_01A5jiDkqMahHqopV2mcaUt3 --- 03_PROTO/Cargo.lock | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/03_PROTO/Cargo.lock b/03_PROTO/Cargo.lock index d8c84dff..1515384c 100644 --- a/03_PROTO/Cargo.lock +++ b/03_PROTO/Cargo.lock @@ -55,6 +55,10 @@ dependencies = [ "riina-types", ] +[[package]] +name = "riina-os" +version = "0.3.0" + [[package]] name = "riina-parser" version = "0.3.0" @@ -97,6 +101,13 @@ dependencies = [ "riina-lexer", ] +[[package]] +name = "riina-ui" +version = "0.3.0" +dependencies = [ + "riina-types", +] + [[package]] name = "riina-wasm" version = "0.3.0"