diff --git a/renaser/CHANGELOG.md b/renaser/CHANGELOG.md index 84b92f6..fd5c765 100644 --- a/renaser/CHANGELOG.md +++ b/renaser/CHANGELOG.md @@ -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. diff --git a/renaser/CLAUDE.md b/renaser/CLAUDE.md index 87d91d4..f616ec0 100644 --- a/renaser/CLAUDE.md +++ b/renaser/CLAUDE.md @@ -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/ # hello_wasm, discola, glotona, cronista, memoriosa, pulso, tonada +cd apps/ # hello_wasm, discola, glotona, cronista, memoriosa, pulso, tonada, bitacora cargo build --target wasm32-unknown-unknown --release cp target/wasm32-unknown-unknown/release/.wasm ../../kernel/assets/.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 diff --git a/renaser/DIARIO.md b/renaser/DIARIO.md index a9c1f40..b8bdc8f 100644 --- a/renaser/DIARIO.md +++ b/renaser/DIARIO.md @@ -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.* diff --git a/renaser/ROADMAP.md b/renaser/ROADMAP.md index c35d4eb..8973ea5 100644 --- a/renaser/ROADMAP.md +++ b/renaser/ROADMAP.md @@ -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. diff --git a/renaser/apps/bitacora/Cargo.lock b/renaser/apps/bitacora/Cargo.lock new file mode 100644 index 0000000..87b6a15 --- /dev/null +++ b/renaser/apps/bitacora/Cargo.lock @@ -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" diff --git a/renaser/apps/bitacora/Cargo.toml b/renaser/apps/bitacora/Cargo.toml new file mode 100644 index 0000000..414d9c6 --- /dev/null +++ b/renaser/apps/bitacora/Cargo.toml @@ -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 diff --git a/renaser/apps/bitacora/src/lib.rs b/renaser/apps/bitacora/src/lib.rs new file mode 100644 index 0000000..f1415f8 --- /dev/null +++ b/renaser/apps/bitacora/src/lib.rs @@ -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, + } +} diff --git a/renaser/boot/src/main.rs b/renaser/boot/src/main.rs index a185fa8..8cb309c 100644 --- a/renaser/boot/src/main.rs +++ b/renaser/boot/src/main.rs @@ -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) }, diff --git a/renaser/kernel/assets/bitacora.wasm b/renaser/kernel/assets/bitacora.wasm new file mode 100755 index 0000000..70387c8 Binary files /dev/null and b/renaser/kernel/assets/bitacora.wasm differ diff --git a/renaser/kernel/src/compositor.rs b/renaser/kernel/src/compositor.rs index 7423a65..db2ad60 100644 --- a/renaser/kernel/src/compositor.rs +++ b/renaser/kernel/src/compositor.rs @@ -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.