feat: llimphi standalone — framework UI soberano extraído del monorepo
Motor gráfico Llimphi como workspace independiente: bucle Elm (input→update→view→layout→raster→present) sobre wgpu+vello+taffy+parley. Núcleo (hal/raster/layout/text/ui/theme/surface/motion/icons) + ~40 widgets + módulos, sin dependencias al resto del monorepo. cargo check --workspace pasa (64 crates). Puerta de entrada: cargo run -p llimphi-ui --example counter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
//! Smoke test del terminal: spawnea un shell, le tipea `echo hola`,
|
||||
//! drena hasta ver el output, y verifica que el contenido del screen
|
||||
//! contenga "hola". Cierre con SIGTERM se valida por el Drop.
|
||||
//!
|
||||
//! Requiere `/bin/sh` y un sistema Linux real (no corre en sandbox
|
||||
//! puro). Es razonable porque shuma-exec ya lo asume.
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use llimphi_module_shuma_term::{self as term, ShumaTermAction, ShumaTermMsg};
|
||||
|
||||
#[test]
|
||||
fn echo_a_traves_del_pty_aparece_en_el_screen() {
|
||||
let mut state = term::spawn_with(
|
||||
"/tmp".to_string(),
|
||||
"/bin/sh".to_string(),
|
||||
Vec::new(),
|
||||
80,
|
||||
24,
|
||||
);
|
||||
|
||||
// El shell escribe su prompt al arrancar; lo drenamos sin asumir
|
||||
// su contenido (cambia por distro).
|
||||
spin_drain(&mut state, Duration::from_millis(200));
|
||||
|
||||
// Tipeamos el comando. Sin Llimphi alrededor llamamos a write_input
|
||||
// directamente — el módulo permite hacerlo via KeyInput, pero
|
||||
// construir KeyEvents acá es ruido para este test.
|
||||
write_raw(&mut state, b"echo hola_del_test\n");
|
||||
|
||||
// Esperamos hasta 2s a que el output llegue al screen.
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
let mut visto = false;
|
||||
while Instant::now() < deadline {
|
||||
spin_drain(&mut state, Duration::from_millis(50));
|
||||
if state.screen_contents().contains("hola_del_test") {
|
||||
visto = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
visto,
|
||||
"esperaba ver 'hola_del_test' en el screen, contenido actual:\n{}",
|
||||
state.screen_contents()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_shift_w_emite_action_close_sin_pasar_al_pty() {
|
||||
use llimphi_ui::{Key, KeyEvent, KeyState, Modifiers};
|
||||
|
||||
let mut state = term::spawn_with(
|
||||
"/tmp".to_string(),
|
||||
"/bin/sh".to_string(),
|
||||
Vec::new(),
|
||||
80,
|
||||
24,
|
||||
);
|
||||
spin_drain(&mut state, Duration::from_millis(100));
|
||||
|
||||
let ev = KeyEvent {
|
||||
key: Key::Character("w".into()),
|
||||
state: KeyState::Pressed,
|
||||
text: Some("w".into()),
|
||||
modifiers: Modifiers { ctrl: true, shift: true, ..Modifiers::default() },
|
||||
repeat: false,
|
||||
};
|
||||
let action = term::apply(&mut state, ShumaTermMsg::KeyInput(ev));
|
||||
assert_eq!(action, ShumaTermAction::Close);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_to_bytes_mapea_los_casos_canonicos() {
|
||||
use llimphi_ui::{Key, KeyEvent, KeyState, Modifiers, NamedKey};
|
||||
|
||||
let mk = |key: Key, mods: Modifiers, text: Option<&str>| KeyEvent {
|
||||
key,
|
||||
state: KeyState::Pressed,
|
||||
text: text.map(|s| s.to_string()),
|
||||
modifiers: mods,
|
||||
repeat: false,
|
||||
};
|
||||
|
||||
// Enter → CR (no LF — el driver del PTY lo expande).
|
||||
assert_eq!(
|
||||
term::key_to_bytes(&mk(Key::Named(NamedKey::Enter), Modifiers::default(), None)),
|
||||
b"\r"
|
||||
);
|
||||
// Backspace → DEL.
|
||||
assert_eq!(
|
||||
term::key_to_bytes(&mk(
|
||||
Key::Named(NamedKey::Backspace),
|
||||
Modifiers::default(),
|
||||
None
|
||||
)),
|
||||
vec![0x7f]
|
||||
);
|
||||
// ArrowUp → CSI A.
|
||||
assert_eq!(
|
||||
term::key_to_bytes(&mk(
|
||||
Key::Named(NamedKey::ArrowUp),
|
||||
Modifiers::default(),
|
||||
None
|
||||
)),
|
||||
b"\x1b[A"
|
||||
);
|
||||
// Ctrl+C → 0x03.
|
||||
let ctrl = Modifiers { ctrl: true, ..Modifiers::default() };
|
||||
assert_eq!(
|
||||
term::key_to_bytes(&mk(Key::Character("c".into()), ctrl, Some("c"))),
|
||||
vec![0x03]
|
||||
);
|
||||
// Texto plano (con shift aplicado por el backend) → ese mismo texto.
|
||||
assert_eq!(
|
||||
term::key_to_bytes(&mk(Key::Character("A".into()), Modifiers::default(), Some("A"))),
|
||||
b"A"
|
||||
);
|
||||
// Alt+x → ESC + x.
|
||||
let alt = Modifiers { alt: true, ..Modifiers::default() };
|
||||
assert_eq!(
|
||||
term::key_to_bytes(&mk(Key::Character("x".into()), alt, Some("x"))),
|
||||
vec![0x1b, b'x']
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// helpers
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
/// Pequeño polling: dispara Tick varias veces durante `total` para que
|
||||
/// el módulo drene los bytes que el reader thread haya emitido.
|
||||
fn spin_drain(state: &mut llimphi_module_shuma_term::ShumaTermState, total: Duration) {
|
||||
let deadline = Instant::now() + total;
|
||||
while Instant::now() < deadline {
|
||||
term::apply(state, ShumaTermMsg::Tick);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
/// Atajo: enviar bytes crudos al PTY sin construir un KeyEvent. Usa la
|
||||
/// API pública via un truco — convertimos a un KeyEvent "texto" para
|
||||
/// evitar exponer write_input crudo en el contrato.
|
||||
fn write_raw(state: &mut llimphi_module_shuma_term::ShumaTermState, bytes: &[u8]) {
|
||||
use llimphi_ui::{Key, KeyEvent, KeyState, Modifiers};
|
||||
// Texto entero (incluyendo el LF) en un solo KeyInput. apply() lo
|
||||
// copia tal cual al PTY via la rama `text`.
|
||||
let s = std::str::from_utf8(bytes).expect("test usa ascii");
|
||||
let ev = KeyEvent {
|
||||
// Key::Character vacío para que no entremos por la rama ctrl/alt.
|
||||
key: Key::Character("".into()),
|
||||
state: KeyState::Pressed,
|
||||
text: Some(s.to_string()),
|
||||
modifiers: Modifiers::default(),
|
||||
repeat: false,
|
||||
};
|
||||
term::apply(state, ShumaTermMsg::KeyInput(ev));
|
||||
}
|
||||
Reference in New Issue
Block a user