Imported from isradev-git/SYNAPSE_ (
AGENTS.md). Install upstream withnpx skills add isradev-git/SYNAPSE_. Copyright stays with the author.
AGENTS.md — SYNAPSE_
Terminal emulator GPU-accelerated en Rust. Platforms: Linux + macOS (no Windows native). Windows via WSL2 only. 13 fases completadas + Fase A (UI Phases).
Fuentes de verdad
docs/desarrollo/SYNAPSE_UI_PHASES.md— plan UI/UX por fases (A-G), redefinición completa de la interfazdocs/superpowers/specs/2026-05-20-synapse_-master-roadmap.md— roadmap maestro fases 11-20docs/desarrollo/— docs de fases completadas
Datos clave que un agente erraría sin ayuda
- Nombre: "SYNAPSE_" (no "Synapse", no "synapse")
- Lenguaje: Rust stable, edition 2021
- Workspace: 5 crates bajo
crates/—SYNAPSE_-app(bin),SYNAPSE_-renderer,SYNAPSE_-ui,SYNAPSE_-config,SYNAPSE_-suggest - Entry point:
crates/SYNAPSE_-app/src/main.rs— winit event loop - Renderer:
crates/SYNAPSE_-renderer/src/renderer.rs— wgpu instance + surface + device + queue - Tab/Pane system:
TabBar { tabs: Vec<Tab>, active: usize }, cadaTabtienePaneTree+active_pane: PaneId. LosPaneviven enVec<Pane>separado, referenciados por ID. - Sidebar vertical (Fase A): La tab bar se movió de horizontal-superior a vertical-izquierda. Ancho por defecto 180px, configurable via
sidebar_widthenconfig.toml. Cada tab ocupa 36px de alto, con scroll vertical. - Single render pass:
draw_frame(cells, ui_rects)rasteriza glyphs y dibuja cells + UI rects en un sologet_current_texture/present. - Mouse position:
AppState.cursor_x/yse actualiza en cadaCursorMoved(÷ scale_factor).MouseInputlee estas coordenadas para detectar clicks en sidebar (x < sidebar_width). - Código existente: Fase 0–4 implementadas. Hay source real (
main.rs,renderer.rs,text.rs,atlas.rs,cell.rs,cell.wgsl,ui.rs,ui.wgsl,pane.rs,splitter.rs,tab_bar.rs,layout.rs,theme.rs) - Nombres de crate con mayúscula (ej.
SYNAPSE_-app) como consta enproyecto.md. El[lib] nameusa snake_case (ej.synapse_rendererpara el crateSYNAPSE_-renderer).
Stack y versiones exactas
Versiones fijadas en [workspace.dependencies] del Cargo.toml raíz:
winit 0.30, wgpu 22, fontdue 0.9, alacritty_terminal 0.24, portable-pty 0.8, rustybuzz 0.14, tokio 1 (full), serde 1 (derive), toml 0.8, arboard 3, clap 4 (derive), tracing 0.1, tracing-subscriber 0.3, pollster 0.3, bytemuck 1 (derive), base64 0.22, png 0.17.
APIs winit 0.30 (no obvias)
EventLoop::new()?→ luegoevent_loop.create_window(WindowAttributes::default().with_title(...))?EventLoop::run()está deprecado en favor derun_app(usamos el deprecated por simplicidad)RedrawRequested+AboutToWaitpara el loop de render- Para refactor futuro: migrar a
ApplicationHandlertrait
APIs wgpu 22 (no obvias)
entry_pointen vertex/fragment state es&str(noOption<&str>)BindGroupLayoutno implementaClone— pasar por referenciacreate_surface(window)requiereArc<Window>(porraw-window-handle)pollster::block_onpara init async (request_adapter, request_device)Backends::DX11no existe en wgpu 22 (fue eliminado). Solo DX12.- Vertex shader: no usar structs como
@location(0), usar parámetros planos (@location(0) pos: vec2<f32>, etc.). Struct-based vertex input causatype does not match the varyingen naga. - Dynamic array indexing en WGSL (
arr[vertex_index]) rechazado por naga en backends estrictos. Usarif/else. Vercorner_for_index()encell.wgsl.
APIs fontdue 0.9 (no obvias)
Font::rasterize(ch, px)→(Metrics, Vec<u8>)— grayscale bitmap, 1 byte por pixelFont::rasterize_indexed(glyph_id, px)— rasterizar por glyph ID para ligadurasMetrics { xmin, ymin, width, height, advance_width, advance_height }- Convertir grayscale → RGBA antes de subir al atlas
Pipeline de instanced rendering
CellInstance { cell_pos, cell_size, uv_rect, fg_color, bg_color }
→ vertex buffer (step_mode: Instance)
→ cell.wgsl: 4 vértices en TriangleStrip por instancia
→ shader convierte pixel coords → NDC (flip Y)
→ fragment: textureSample(atlas, sampler, uv) → mix(bg, fg, alpha)
CellInstance layout: 16 floats = 64 bytes (pos:2, size:2, uv:4, fg:4, bg:4).
Bind groups: group 0 = atlas (texture + sampler), group 1 = screen uniform (vec2).
Frame cache (Fase 11 — GPU optimization)
render_frame() en crates/SYNAPSE_-app/src/render.rs decide si reconstruir cell_data/ui_rects o reusar el caché:
Invalidan el caché:
pty_received: se procesaron datos del PTYany_grid_dirty: algúnGridtienedirty_frame == truefont_changed:cached_font_size != state.font_sizeblink_changed:cached_blink != cursor_blink_ontab_changed:cached_active_tab != tab_bar.activeui_active:state.selecting || state.search.active || state.history_search.activefirst_frame:cached_cell_data.is_empty()
Si ninguna condición se cumple: el caché de instancias GPU se re-usa, no se sube nada a la GPU ni se itera el grid.
Buffers dinámicos: CellRenderer y UIRenderer crean buffers GPU del tamaño justo ×2 (next_power_of_two). Antes: overflow = silent fail. Ahora: resize automático.
Grid.dirty_frame: cada método mutante (set(), advance_cursor(), scroll_up(), new_line(), resize(), etc.) marca dirty_frame = true. clear_frame_dirty() se llama tras renderizar. No confundir con el dirty por-celda (CharCell.dirty) que no está conectado al render.
Arc<Device>: Renderer, CellRenderer y UIRenderer comparten el device via Arc<wgpu::Device> (wgpu 22 no implementa Clone, el Arc permite la compartición).
Convenciones de arquitectura
- Dirty tracking por frame:
Grid.dirty_framemarca si algo cambió desde el último frame. El render salta la reconstrucción de instancias si no hay cambios. - CharCell.dirty: existe pero no está conectado al render. Cada celda tiene el flag pero el render itera todo el viewport.
- Instanced rendering: un solo draw call por frame, una instancia GPU por celda visible.
- PaneTree: árbol binario de splits (
Leaf | Split { direction, ratio, first, second }). - Scrollback: buffer circular, default 100.000 líneas, con
scroll_offsetpara viewport scrolling. - Config: TOML en
~/.config/SYNAPSE_/config.toml(Linux) o~/Library/Application Support/SYNAPSE_/config.toml(macOS). - Shell detection:
$SHELL→/bin/zsh(macOS) →/bin/bash(Linux).
Paleta de colores (identidad visual)
Tema profesional oscuro con acento azul acero. Los colores se definen en Theme::synapse_() (crates/SYNAPSE_-config/src/themes.rs).
Los 4 temas built-in (synapse_, dracula, catppuccin-mocha, tokyo-night) son intercambiables via theme = "..." en config.toml.
#11131a → fondo principal (clear color en render)
#d2d5db → texto del buffer
#7098cc → cursor, borde de panel activo (azul acero)
#181b24 → tab bar bg
#222739 → tab activa, separadores, bordes inactivos
#e5e8ee → texto UI activo
#737a8c → texto UI inactivo
Comandos útiles
cargo build -p SYNAPSE_-app # build binario
cargo build -p SYNAPSE_-renderer # build crate renderer
cargo test -p SYNAPSE_-renderer # tests de renderer
cargo test -p SYNAPSE_-renderer -- --nocapture # test con stdout visible
cargo build --release # release con mold linker
cargo watch -x test # hot-reload tests
WINIT_UNIX_BACKEND=x11 ./target/debug/synapse_ # forzar X11 (WSLg workaround)
Benchmarking
cargo build --release
RUST_LOG=synapse_::bench=info ./target/release/synapse_ # FPS logging
./build/bench.sh release # benchmark script
Referencia rápida Kitty Keyboard (R-022)
crates/SYNAPSE_-app/src/input.rs—from_key_kitty(), CSI u encoding, keycodescrates/SYNAPSE_-app/src/image_protocol.rs—scan_kkp(),KkpScanenum- Pre-scan en PTY reader (
image_protocol.rs) intercepta\e[?u(query),\e[= u(set),\e[> u(push),\e[< u(pop) antes del parser vte - Flags:
KITTY_DISAMBIGUATE=1,KITTY_REPORT_EVENTS=2,KITTY_REPORT_ALL=8 - Sin esto: neovim no distingue Ctrl+[ de Escape, Ctrl+I de Tab, etc.
Métricas objetivo (no negociables)
- Latencia input→render: <5ms
- FPS: 60 estables, ≥30 con output masivo
- Arranque: <200ms
- RAM idle: <50MB
Sin base de datos, sin HTTP
App de escritorio. Sin rutas REST, sin DB. Todo in-memory.