feat(renaser): Fase 17 — bitácora, el editor que recuerda

memoriosa (Fase 7c) demostró que un app podía persistir su huella.
Esta fase la lleva al gesto natural: un editor de texto. Tecleas,
reinicias renaser, el texto sigue ahí. La huella vive en el grafo
de objetos como todo lo demás.

- Nuevo crate `apps/bitacora/`: lienzo 480×280, tipografía 8×8
  embebida (`font8x8 = "0.3"`) escalada x2 a 16×16, render pixel
  a pixel desde la memoria del propio app. Buffer 512 bytes con
  wrap automático a 28 columnas; `Enter` salta línea, Backspace
  borra; al desbordar el buffer se descartan los 64 primeros para
  amortizar la mudanza. Cada cambio invoca `sys_estado_guardar`;
  al arrancar, `init` llama a `sys_estado_cargar` y reconstruye.
- Mapeo de scancodes US a ASCII (letras, dígitos, puntuación
  básica, espacio). Sin shift ni mayúsculas — minimalismo.
- `GENESIS` crece de 7 a 8 apps; `bitacora` es la PRIMERA — gana
  la celda maestra al arrancar y te invita a teclear.
- `CELDA_TASKBAR_ANCHO` baja de 150 a 130 px para que las ocho
  pestañas + lanzador + reloj quepan holgadas en 1280 px.

Verificado en QEMU: tras escribir "hola renaser" y reiniciar el
kernel con el mismo disk.img, bitácora muestra el texto donde lo
dejó. El `almacen` reporta 24 objetos en el grafo (frente a 9
antes de escribir) y `raiz presente` — cada `guardar` anexó una
versión al log direccionado por contenido.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-23 03:52:20 +00:00
parent e100c5acff
commit 60553bec44
10 changed files with 422 additions and 11 deletions
+34
View File
@@ -1085,3 +1085,37 @@ a la derecha, y le pone el reloj a latir cada segundo.
reloj `0:17` a la derecha (el tiempo que el kernel lleva vivo al capturar).
Diez segundos después, el reloj marca `0:29` — la barra se ha refrescado
doce veces sin intervención del ratón ni del teclado.
## Fase 17 — `bitacora` :: el editor que recuerda — 2026-05-23
`memoriosa` (Fase 7c) demostró que un app podía persistir un contador a
través de los reinicios. `bitacora` extiende esa demostración a un EDITOR de
texto: tecleas, los caracteres aparecen; reinicias el kernel, el texto
sigue ahí. La huella vive en el grafo de objetos, como todo lo demás.
### Añadido — app `bitacora` (`apps/bitacora/`)
- **Lienzo 480×280** con título "bitacora :: el texto persiste" en índigo
arriba y, debajo, las últimas líneas del buffer. Wrap automático a 28
columnas; `Enter` salta de línea; Backspace borra el último carácter.
- **Tipografía 8×8** embebida (crate `font8x8 = "0.3"`), escalada x2 a 16×16.
El app rasteriza cada glifo pixel a pixel desde su propia memoria lineal.
- **Persistencia automática**: cada cambio invoca `sys_estado_guardar`. Al
arrancar, `init` llama a `sys_estado_cargar` y restaura el buffer.
- Buffer de 512 bytes; al desbordarse descarta los 64 primeros para hacer
hueco (amortiza el coste — no es una mudanza por cada pulsación).
- Mapeo de scancodes US a ASCII para letras, dígitos y puntuación común;
Enter genera `\n`, Backspace borra. Sin mayúsculas ni modificadores.
### Cambiado
- **`GENESIS` crece de 7 a 8 apps** con `bitacora` como la PRIMERA — gana
la celda maestra al arrancar, así que la primera ventana grande del
escritorio te invita a teclear.
- **`CELDA_TASKBAR_ANCHO`** baja de 150 a 130 píxeles para que las ocho
pestañas + el lanzador + el reloj quepan holgadas en 1280 px.
### Verificado
- QEMU (`sendkey` del monitor): tras escribir `hola renaser` y `quit` →
relanzar QEMU con el mismo `disk.img`, la `bitacora` muestra de nuevo el
texto justo donde quedó. El `almacen` reporta 24 objetos en el grafo
(frente a 9 antes de escribir) y `raiz presente`: cada `guardar` anexó
una versión al log direccionado por contenido.
+5 -3
View File
@@ -28,7 +28,7 @@ Reconstruir una app WASM del userspace tras tocarla. Los `.wasm` viven en
modulo `hello_wasm` se copia como `app.wasm`, el resto conserva su nombre:
```sh
cd apps/<app> # hello_wasm, discola, glotona, cronista, memoriosa, pulso, tonada
cd apps/<app> # hello_wasm, discola, glotona, cronista, memoriosa, pulso, tonada, bitacora
cargo build --target wasm32-unknown-unknown --release
cp target/wasm32-unknown-unknown/release/<app>.wasm ../../kernel/assets/<app>.wasm
# (hello_wasm es la excepcion: su destino es kernel/assets/app.wasm)
@@ -87,9 +87,11 @@ infraestructura `memory::mmio` (mapeador propio de regiones MMIO en la tabla
L4), la Fase 14 COMPLETA —nombres en cada ventana y barra de tareas con
clic-para-enfocar—, la Fase 15 COMPLETA —la voz del sistema: acorde al
arrancar, repique al lanzar o cerrar, bajo al desalojar, con prioridad
sobre `sys_tono` y la Fase 16 COMPLETA —la barra viva: botón «+»
sobre `sys_tono`— la Fase 16 COMPLETA —la barra viva: botón «+»
lanzador a la izquierda y reloj `mm:ss` a la derecha que late cada
segundo—. Todo verificado en QEMU. Ver `ROADMAP.md`.
segundo— y la Fase 17 COMPLETA —`bitacora`, editor de texto que persiste
entre arranques en el grafo de objetos (tipografía 8×8 embebida)—.
Todo verificado en QEMU. Ver `ROADMAP.md`.
## Flujo de trabajo
+21
View File
@@ -547,6 +547,27 @@ reloj LATE: cada vez que pasa un segundo nuevo —y sólo entonces, ni una vez
de más—, la casa recompone el zócalo para mostrar la cifra siguiente. El
resto del tiempo, el zócalo descansa.
## La bitácora — escribir y volver a encontrarlo
Hace tiempo que la casa permitía a sus inquilinos guardar pequeños recuerdos
en sus paredes —`memoriosa` lo había estrenado contando teclas a través de
los amaneceres—. Pero un cuaderno de notas, no había. Hoy llegó uno.
«bitácora» se asoma al despertar como el inquilino más importante: ocupa la
celda más grande del escritorio, con un título índigo y un papel limpio
debajo. Donde el cursor apuntaba, va apareciendo lo que se teclea, letra a
letra. Cuando llega al borde derecho del papel, salta de línea solo;
con Enter, también; con Backspace, retrocede y borra. Hasta aquí, nada
nuevo bajo el sol.
Lo nuevo es lo que ocurre al apagar la casa. Habitualmente, lo que se
escribe en un papel desaparece cuando el papel se quema. En la casa de
renaser no: cada letra que la bitácora recibe queda anclada en su mapa
secreto de objetos —el mismo árbol en el que viven los inquilinos—. Al
apagar y volver a encender, el papel vuelve a su sitio con cada palabra
intacta. Apaga, enciende, sigue escribiendo. La casa no olvida lo que se
le confía.
---
*El diario continúa. La próxima página la escribirá la próxima jornada.*
+14
View File
@@ -289,6 +289,20 @@ tiempo: el reloj avanza de `0:17` a `0:29`.
`pintar_taskbar` dibuja la cruz del lanzador como dos rectángulos
cruzados (sin depender de la tipografía) y rotula el reloj.
## Fase 17 — `bitacora`, el editor que recuerda (completada)
`memoriosa` (Fase 7c) demostró que un app podía persistir su huella. La Fase 17
lo lleva al gesto natural: un editor de texto. Tecleas, reinicias renaser, el
texto sigue ahí. Verificada en QEMU.
- Nuevo crate `apps/bitacora/`: lienzo 480×280, tipografía 8×8 (crate `font8x8`)
escalada x2 a 16×16, render pixel a pixel desde la memoria del propio app.
Buffer 512 bytes, wrap a 28 columnas, Enter / Backspace, persiste cada
cambio con `sys_estado_guardar`. Mapeo de scancodes US a ASCII (minúsculas).
- `GENESIS` crece de 7 a 8 apps; `bitacora` es la maestra al arrancar.
- `CELDA_TASKBAR_ANCHO` baja de 150 a 130 px para que las ocho pestañas
quepan holgadas con el lanzador y el reloj.
Líneas abiertas posteriores: reciclado de las ranuras de ventana cerradas;
audio con varias voces (PCM) más allá del tono único de la bocina.
+16
View File
@@ -0,0 +1,16 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bitacora"
version = "0.1.0"
dependencies = [
"font8x8",
]
[[package]]
name = "font8x8"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875488b8711a968268c7cf5d139578713097ca4635a76044e8fe8eedf831d07e"
+31
View File
@@ -0,0 +1,31 @@
# =============================================================================
# renaser :: apps/bitacora — Fase 17 :: un editor de texto que persiste
# -----------------------------------------------------------------------------
# Tecleas, los caracteres se quedan. Y al reiniciar siguen ahi, porque viven en
# el grafo de objetos (Fase 7c, `sys_estado_*`). Renderizado con la tipografia
# 8x8 clasica (font8x8), embebida en el binario WASM — no depende del kernel.
# =============================================================================
[package]
name = "bitacora"
version = "0.1.0"
edition = "2021"
description = "renaser :: app WASM — un editor de texto que persiste entre arranques"
[workspace]
[dependencies]
# Tipografia 8x8 pixel-perfect, public domain. La crate es `no_std` y compila
# limpiamente para `wasm32-unknown-unknown` con `default-features = false`.
font8x8 = { version = "0.3", default-features = false, features = ["unicode"] }
[lib]
crate-type = ["cdylib"]
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
opt-level = "s"
lto = true
+289
View File
@@ -0,0 +1,289 @@
// =============================================================================
// renaser :: apps/bitacora — Fase 17 :: un editor que recuerda
// -----------------------------------------------------------------------------
// La fase 7c le dio a las apps memoria mas alla del arranque: `sys_estado_*`
// ancla la huella de un app en el grafo, y al reiniciar el kernel se la
// devuelve. `memoriosa` lo demostro contando teclas. `bitacora` lo lleva al
// siguiente paso natural: ofrecer un editor de texto.
//
// La pantalla muestra un titulo en indigo y debajo el texto que el usuario va
// tecleando, con salto de linea automatico al llegar al margen y con `Enter`.
// Backspace borra el ultimo. Cada cambio se persiste de inmediato, asi que la
// apagada brusca no pierde nada — la proxima vida del kernel retoma exacto.
//
// Tipografia: la 8x8 clasica (font8x8), escalada x2 a 16x16. Cabe en su propia
// memoria lineal y se renderiza pixel a pixel — el app no toca el lienzo del
// kernel, solo entrega su propio fotograma.
// =============================================================================
#![no_std]
use font8x8::legacy::BASIC_LEGACY;
#[link(wasm_import_module = "renaser")]
extern "C" {
fn sys_render_frame(ptr: u32, len: u32);
fn sys_get_scancode() -> u32;
fn sys_estado_cargar(salida: u32, capacidad: u32) -> i32;
fn sys_estado_guardar(datos: u32, datos_len: u32) -> i32;
}
#[panic_handler]
fn al_fallar(_: &core::panic::PanicInfo) -> ! {
loop {}
}
// --- Geometria ----------------------------------------------------------------
/// Tamaño del lienzo natural — debe coincidir con `region` del manifiesto.
const ANCHO: usize = 480;
const ALTO: usize = 280;
/// Pixeles por celda de glifo (8x8 escalado x2).
const PASO: usize = 16;
/// Margen horizontal para el cuerpo del texto.
const MARGEN_X: usize = 16;
/// Y de la linea base del titulo.
const Y_LABEL: usize = 6;
/// Y de la linea base de la primera fila de texto.
const Y_TEXTO: usize = 38;
/// Cuantas columnas caben.
const COLUMNAS: usize = (ANCHO - 2 * MARGEN_X) / PASO;
/// Cuantas filas caben bajo el titulo.
const FILAS: usize = (ALTO - Y_TEXTO) / PASO;
// --- Estado -------------------------------------------------------------------
/// Capacidad del buffer de texto. Al desbordarse se descarta una porcion del
/// principio (el texto mas viejo) para dejar sitio al nuevo — un cuaderno con
/// memoria finita, no un agujero negro.
const CAPACIDAD: usize = 512;
const FONDO: u32 = 0x0A_18_30;
const TINTA: u32 = 0xE8_EC_F4;
const ETIQUETA: u32 = 0x8B_5C_F6;
static mut BUFFER: [u8; CAPACIDAD] = [0; CAPACIDAD];
static mut LEN: usize = 0;
static mut LIENZO: [u32; ANCHO * ALTO] = [0; ANCHO * ALTO];
// --- ABI del userspace --------------------------------------------------------
#[no_mangle]
pub extern "C" fn init() {
// Cargar el texto persistido — si no hay nada, `n` es 0 y empezamos vacios.
let buffer = unsafe { &mut *core::ptr::addr_of_mut!(BUFFER) };
// SEGURIDAD: `sys_estado_cargar` es una capacidad del host; (ptr, len) cae
// dentro de nuestra propia memoria lineal y el host lo valida sin piedad.
let n = unsafe { sys_estado_cargar(buffer.as_mut_ptr() as u32, CAPACIDAD as u32) };
if n > 0 {
// SEGURIDAD: lectura/escritura escalar; LEN es nuestro propio cursor.
unsafe {
LEN = (n as usize).min(CAPACIDAD);
}
}
pintar();
}
#[no_mangle]
pub extern "C" fn tick() {
let mut cambio = false;
// Drenar TODOS los scancodes acumulados desde el ultimo fotograma. La cola
// es propia de este app — la inscribio la fase 5 en la IRQ1 — asi que
// mirarla aqui no le quita nada a nadie.
loop {
let sc = unsafe { sys_get_scancode() } as u8;
if sc == 0 {
break;
}
if sc & 0x80 != 0 {
// Codigo de KEY-UP (release). Lo ignoramos: tecleamos al pulsar.
continue;
}
match sc {
0x0E => {
// Backspace — borrar el ultimo caracter, si lo hay.
unsafe {
if LEN > 0 {
LEN -= 1;
cambio = true;
}
}
}
0x1C => {
// Enter — salto de linea explicito.
anexar(b'\n');
cambio = true;
}
otro => {
let c = scancode_a_caracter(otro);
if c != 0 {
anexar(c);
cambio = true;
}
}
}
}
if cambio {
guardar();
}
pintar();
}
// --- Estado: buffer -----------------------------------------------------------
/// Anexa un caracter al final del buffer. Si el buffer esta lleno descarta los
/// 64 primeros bytes para hacer hueco (amortiza el coste; no es una mudanza por
/// cada pulsacion).
fn anexar(c: u8) {
unsafe {
if LEN >= CAPACIDAD {
let buffer = &mut *core::ptr::addr_of_mut!(BUFFER);
buffer.copy_within(64.., 0);
LEN = CAPACIDAD - 64;
}
let buffer = &mut *core::ptr::addr_of_mut!(BUFFER);
buffer[LEN] = c;
LEN += 1;
}
}
/// Persiste el buffer en el grafo. La huella sobrevive a la siguiente arrancada.
fn guardar() {
unsafe {
let buffer = &*core::ptr::addr_of!(BUFFER);
// SEGURIDAD: (ptr, len) describe nuestra propia memoria; el host lo
// verifica y nunca lee fuera del rango entregado.
let _ = sys_estado_guardar(buffer.as_ptr() as u32, LEN as u32);
}
}
// --- Renderizado --------------------------------------------------------------
fn pintar() {
let lienzo = unsafe { &mut *core::ptr::addr_of_mut!(LIENZO) };
// Fondo limpio.
for pixel in lienzo.iter_mut() {
*pixel = FONDO;
}
// Titulo.
pintar_texto(lienzo, b"bitacora :: el texto persiste", MARGEN_X, Y_LABEL, ETIQUETA);
// Linea sutil bajo el titulo.
let y_linea = Y_LABEL + PASO + 4;
for x in MARGEN_X..(ANCHO - MARGEN_X) {
lienzo[y_linea * ANCHO + x] = ETIQUETA;
lienzo[(y_linea + 1) * ANCHO + x] = ETIQUETA;
}
// Cuerpo: mostrar las ultimas `FILAS` lineas del buffer, con wrap en
// `COLUMNAS`. Dos pasadas para saltarse las filas viejas con elegancia.
let buffer = unsafe { &*core::ptr::addr_of!(BUFFER) };
let len = unsafe { LEN };
// Pasada 1: contar filas totales (con wrap).
let mut filas_total = 1usize;
let mut col = 0usize;
for i in 0..len {
let c = buffer[i];
if c == b'\n' {
filas_total += 1;
col = 0;
} else {
if col >= COLUMNAS {
filas_total += 1;
col = 0;
}
col += 1;
}
}
let skip = filas_total.saturating_sub(FILAS);
// Pasada 2: renderizar solo a partir de la fila `skip`.
let mut fila_actual = 0usize;
let mut col2 = 0usize;
for i in 0..len {
let c = buffer[i];
if c == b'\n' {
fila_actual += 1;
col2 = 0;
continue;
}
if col2 >= COLUMNAS {
fila_actual += 1;
col2 = 0;
}
if fila_actual >= skip {
let rfila = fila_actual - skip;
if rfila < FILAS {
let x = MARGEN_X + col2 * PASO;
let y = Y_TEXTO + rfila * PASO;
pintar_glifo(lienzo, c, x, y, TINTA);
}
}
col2 += 1;
}
// SEGURIDAD: `sys_render_frame` valida (ptr, len) contra nuestra memoria.
unsafe {
sys_render_frame(lienzo.as_ptr() as u32, (ANCHO * ALTO * 4) as u32);
}
}
/// Pinta una cadena ASCII en (x, y_base), avanzando un PASO por glifo. Se
/// detiene si el siguiente glifo no cabria dentro del lienzo.
fn pintar_texto(lienzo: &mut [u32], texto: &[u8], x: usize, y: usize, color: u32) {
let mut cx = x;
for &c in texto {
if cx + PASO > ANCHO {
break;
}
pintar_glifo(lienzo, c, cx, y, color);
cx += PASO;
}
}
/// Pinta un solo glifo 8x8 escalado a 16x16 en (x, y). Los caracteres no ASCII
/// se renderizan como `?`.
fn pintar_glifo(lienzo: &mut [u32], c: u8, x: usize, y: usize, color: u32) {
let glifo = if (c as usize) < 128 {
BASIC_LEGACY[c as usize]
} else {
BASIC_LEGACY[b'?' as usize]
};
for row in 0..8 {
for col in 0..8 {
if glifo[row] & (1 << col) != 0 {
let px = x + col * 2;
let py = y + row * 2;
if px + 1 >= ANCHO || py + 1 >= ALTO {
continue;
}
lienzo[py * ANCHO + px] = color;
lienzo[py * ANCHO + px + 1] = color;
lienzo[(py + 1) * ANCHO + px] = color;
lienzo[(py + 1) * ANCHO + px + 1] = color;
}
}
}
}
// --- Teclado: scancode -> caracter --------------------------------------------
/// Traduce un MAKE-code del set 1 (US layout) a su caracter ASCII en minuscula.
/// Devuelve 0 para los scancodes que no producen texto — modificadores,
/// extendidos, etc.: el llamante los descarta sin gritar.
fn scancode_a_caracter(sc: u8) -> u8 {
match sc {
0x02 => b'1', 0x03 => b'2', 0x04 => b'3', 0x05 => b'4', 0x06 => b'5',
0x07 => b'6', 0x08 => b'7', 0x09 => b'8', 0x0A => b'9', 0x0B => b'0',
0x10 => b'q', 0x11 => b'w', 0x12 => b'e', 0x13 => b'r', 0x14 => b't',
0x15 => b'y', 0x16 => b'u', 0x17 => b'i', 0x18 => b'o', 0x19 => b'p',
0x1E => b'a', 0x1F => b's', 0x20 => b'd', 0x21 => b'f', 0x22 => b'g',
0x23 => b'h', 0x24 => b'j', 0x25 => b'k', 0x26 => b'l',
0x2C => b'z', 0x2D => b'x', 0x2E => b'c', 0x2F => b'v', 0x30 => b'b',
0x31 => b'n', 0x32 => b'm',
0x33 => b',', 0x34 => b'.', 0x35 => b'/',
0x27 => b';', 0x28 => b'\'',
0x39 => b' ',
_ => 0,
}
}
+8 -6
View File
@@ -107,12 +107,14 @@ struct AppGenesis {
region: (u32, u32, u32, u32),
}
/// El userspace de genesis — las siete aplicaciones que pueblan un disco recien
/// forjado. La melodia visual `tonada` (Fase 12), el compas visual `pulso`
/// (Fase 11), un saludo (`hola`), la `memoriosa` interactiva que recuerda entre
/// sesiones (Fase 7c), y tres demos de los guardarrailes del kernel: `discola`
/// (combustible), `glotona` (memoria) y `cronista` (la cronica de los arranques).
const GENESIS: [AppGenesis; 7] = [
/// El userspace de genesis — las ocho aplicaciones que pueblan un disco recien
/// forjado. La `bitacora` (Fase 17, editor que persiste), la melodia visual
/// `tonada` (Fase 12), el compas visual `pulso` (Fase 11), un saludo (`hola`),
/// la `memoriosa` interactiva que recuerda entre sesiones (Fase 7c), y tres
/// demos de los guardarrailes del kernel: `discola` (combustible), `glotona`
/// (memoria) y `cronista` (la cronica de los arranques).
const GENESIS: [AppGenesis; 8] = [
AppGenesis { nombre: "bitacora", archivo: "bitacora.wasm", region: (100, 120, 480, 280) },
AppGenesis { nombre: "tonada", archivo: "tonada.wasm", region: (100, 120, 360, 120) },
AppGenesis { nombre: "pulso", archivo: "pulso.wasm", region: (100, 120, 360, 120) },
AppGenesis { nombre: "hola", archivo: "app.wasm", region: (100, 120, 480, 560) },
Binary file not shown.
+4 -2
View File
@@ -67,8 +67,10 @@ const FRANJA_CONSOLA: usize = 296;
/// ahi una pestaña con su nombre, que el clic enfoca.
const FRANJA_TASKBAR: usize = 40;
/// Anchura de cada celda de la barra de tareas, en pixeles.
const CELDA_TASKBAR_ANCHO: usize = 150;
/// Anchura de cada celda de la barra de tareas, en pixeles. Dimensionada para
/// que las ocho apps de genesis + el lanzador + el reloj caben holgados en una
/// pantalla de 1280 px.
const CELDA_TASKBAR_ANCHO: usize = 130;
/// Hueco entre celdas adyacentes de la barra.
const CELDA_TASKBAR_HUECO: usize = 6;
/// Margen izquierdo y derecho de la barra de tareas.