Commit Briefs
Autor por defecto de entradas y páginas nuevas: «Ale B. E.»
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
flake.nix: agrega packages.default — paquete Nix real, no solo devShell
Hasta ahora solo había devShells.default (venv + pip, pensado para desarrollar/debuggear). Para poder declararlo en la config de NixOS como un paquete normal (mismo patrón que usa Ale para sus otros proyectos propios, ej. inputs.psysonic.packages.${system}.default en /nixdots) hace falta un output packages.${system}.default de verdad. Se arma con python313Packages.buildPythonApplication usando las deps de nixpkgs directamente (textual, pyfiglet, matplotlib, textual-image y flatlatex ya estaban empaquetadas ahí) — solo cowsay (el paquete de Python) no está, así que se empaqueta a mano (wheel puro de PyPI, sin deps propias, build corto). Ventaja sobre el devShell: numpy/matplotlib de nixpkgs ya vienen enlazados correctamente contra libstdc++, así que este paquete NO necesita el fix de LD_LIBRARY_PATH que sí hace falta en el devShell (venv + pip instala wheels compilados que esperan rutas FHS que no existen en NixOS). Confirmado con 'nix build .#default' + prueba de imports (matplotlib, textual_image, flatlatex, cowsay, nezzontli_ctl) sin exportar nada a mano. El devShell queda intacto para seguir desarrollando/debuggeando igual que hasta ahora.
Agrega wrapper bin/ctl para comando global (evita el bug de pipx en nixpkgs)
pipx no está en el cache binario de nixpkgs para este snapshot — su propio test suite tiene un bug (parametrize mal armado en test_inject.py) que rompe el build desde cero. Y aunque se lo evite, un pipx install plano tampoco alcanzaría: la app necesita libstdc++.so.6 (numpy, para el renderizado de LaTeX del preview) que en NixOS solo está en el LD_LIBRARY_PATH que arma el shellHook del flake. bin/ctl es un wrapper chico que resuelve su propia ubicación (para andar symlinkeado desde ~/.local/bin), se para en el directorio del repo, y delega todo a 'nix develop . --command ctl' — reusa el flake real en vez de duplicar su lógica, así que no se puede desincronizar. Actualiza el README con la instalación (symlink en vez de pipx) y el estado real del proyecto (menú de dos niveles, eliminar, preview LaTeX/imágenes).
Confirmación de eliminar: una sola pregunta, diálogo centrado; centra el mensaje de cowsay
ConfirmDeleteScreen pasa de doble confirmación a una sola (el usuario pidió revertir eso), y en vez de reusar .form-container (pensado para formularios que ocupan toda la pantalla) ahora es un diálogo compacto de ancho fijo (64 cols) centrado con align: center middle sobre la propia screen. #success-container (el cowsay del final) le faltaba width: 100% — sin eso 'align: center middle' no tenía espacio extra para centrar contra, así que quedaba pegado a la izquierda. Se agrega, más content-align: center middle en el Static del arte ASCII para que el bloque de texto multilínea también se centre horizontalmente dentro de su caja.
Menú de dos niveles (Crear/Editar) + eliminar entradas con doble confirmación
El menú principal pasaba de 4 a 9 opciones con post/page/album duplicados entre crear y editar — más difícil de escanear. Ahora es: Crear nuevo…, Editar existente…, Configuración, Salir; cada uno abre un submenú con las 3-4 opciones específicas y un '← Volver'. 'Agregar fotos a un álbum existente' se movió a Editar (opera sobre un álbum ya creado). Se agrega la opción de editar páginas extra de un proyecto (ej. content/blog/cortometraje/rama1.md) — antes solo se podía editar el index.md de cada proyecto, no las páginas extra generadas con 'page'. list_extra_pages() las lista, NewPageScreen ahora acepta existing_file igual que NewPostScreen (oculta proyecto/slug, precarga título/descripción/ tags/páginas relacionadas, preserva la fecha original). Se agrega un botón 'Eliminar' en los tres formularios de edición (post, página, álbum) con confirmación DOBLE (ConfirmDeleteScreen, mismo modal pregunta dos veces antes de devolver True) antes de borrar y pushear: - post: borra content/blog/<slug>/ completo (incluye sus páginas extra) - página: borra solo ese .md, no toca el resto del proyecto - álbum: borra content/photos/<slug>/ Y static/images/<slug>/ (las fotos en sí, para no dejar basura LFS huérfana) Probado de punta a punta contra un fixture git descartable (bare repo como origin) para los tres flujos de borrado: confirma que el path correcto se borra del disco, se commitea y se pushea al origin.
Editor: posiciona el cursor al inicio del documento al abrir, no al final
Tenía sentido para posts nuevos (arrancar escribiendo al final de un frontmatter ya armado), pero al editar un archivo existente el usuario quiere empezar viendo el principio del documento, no el final.
Preview: no parte los bloques de código ``` en varios widgets
split_paragraphs_with_offsets (agregado para el sync por párrafo) cortaba por cualquier línea en blanco, sin distinguir si estaba dentro de un bloque ```código``` — muy común tener líneas en blanco entre funciones/ comentarios en código real. Cada pedazo se montaba como su propio widget Markdown con su propio margen, así que el bloque se veía hecho pedazos con huecos enormes en vez de un solo bloque contiguo. Ahora un bloque ```...``` se detecta primero y se trata como un párrafo atómico — nunca se parte por las líneas en blanco que tenga adentro. El texto antes/después del bloque sigue partiéndose en párrafos finos para el sync, como antes.
Sync del scroll: un anchor por párrafo (no solo por imagen/fórmula) y arregla el salto de imágenes
Dos bugs quedaban con el sync por línea-ancla: 1. Se anclaba CADA widget de un grupo (imagen + su caption, o cada foto de una gallery en la misma línea de origen), y el algoritmo de selección se queda con el ÚLTIMO anchor que empata esa línea — al llegar ahí el scroll saltaba directo a la caption (o a la última foto), dejando la imagen en sí arriba, fuera de vista. Ahora es un solo anchor por línea de origen distinta, apuntando siempre al primer widget ahí. 2. Un tramo largo de texto plano sin imágenes/fórmulas en el medio se montaba como un único widget Markdown gigante con un solo anchor — así que después de la última fórmula/imagen del documento, no había más anchors y el preview se quedaba pegado ahí por el resto del documento por más que se siguiera bajando en el editor. Ahora cada párrafo (separado por línea en blanco) es su propio widget con su propio anchor, repartidos parejo en todo el texto y no solo donde hay medios. _render_segment ahora devuelve (widget, línea_o_None) en vez de solo widgets — None para los que no deben ser su propio punto de anclaje (la caption de una imagen).
Sync del scroll: usa la línea lógica real, no la fila visual (rompía con wrap)
scroll_y de TextArea está en filas VISUALES, no en líneas lógicas del documento — con soft_wrap (el default, y lo normal para escribir párrafos de blog como una sola línea larga) una línea lógica ocupa varias filas visuales. Los anchors del preview se calculan en líneas lógicas (text.count('\n', ...)), así que compararlos directo contra scroll_y (filas visuales) se desalineaba cada vez más division a medida que había párrafos largos arriba en el documento — exactamente el desfase que se seguía viendo. TextArea.wrapped_document.offset_to_location() traduce la fila visual de vuelta a la línea lógica real (confirmado con params: fila=45 → línea 16, no fila=45 → línea 45), que es la misma unidad de los anchors.
LaTeX inline: aproxima a texto Unicode en vez de romper el párrafo en imágenes
El problema real no era el tamaño de la imagen sino que CADA $...$ inline generaba un widget de imagen separado, partiendo una oración en varios bloques apilados (texto/imagen/texto/imagen) — se perdía la lectura fluida sin importar qué tan chica fuera la imagen. Ahora el $...$ inline se reemplaza en el lugar por su aproximación en texto Unicode (griego, sub/superíndices, ·, ×, →, √, fracciones simples, etc.) vía flatlatex, así el párrafo sigue siendo un solo bloque de Markdown que fluye normal. Se agrega preprocesamiento para los casos que flatlatex no cubre (\sin/\cos/\log/..., \text{}/\mathrm{}, \left/\right, \to). El bloque $$...$$ (que ya vive aislado en su propio párrafo en el Markdown fuente) sigue renderizándose como imagen real vía matplotlib — ahí sí tiene sentido, es una ecuación destacada, no texto corrido. tokenize() ahora devuelve (texto_mostrado, segmentos) en vez de solo segmentos, porque los offsets de línea para el sync del scroll deben calcularse sobre el texto YA sustituido, no el original (la sustitución cambia longitudes de línea pero nunca la cantidad de saltos de línea).
LaTeX del preview: renderiza con fondo transparente en vez de cartel blanco
matplotlib.mathtext.math_to_image() no expone transparent=True, así que savefig() terminaba pintando un fondo blanco sólido — se veía como un recuadro pegado encima del preview oscuro, con el texto crema casi ilegible contra el blanco. Se reimplementa el mismo procedimiento (parser + Figure.text + savefig) que usa esa función internamente, pasando transparent=True, para que la fórmula se vea como texto renderizado flotando sobre el fondo del tema en vez de una imagen con marco. Se versiona la clave de caché (v2) para invalidar los renders viejos con fondo blanco que ya estaban en disco.
Preview: detecta el protocolo de imágenes antes de que Textual tome la terminal, sync por línea-ancla, indicador de carga
Causa real del renderizado pixelado/borroso: textual_image negocia Kitty TGP/Sixel mandando escape sequences y leyendo la respuesta del terminal, pero esa negociación solo funciona ANTES de que Textual ponga la terminal en raw mode + alt screen. Como el import era lazy (recién al abrir el editor), la negociación fallaba y caía al renderer de half-cell (bloques de color de baja resolución — exactamente lo que se veía). Se fuerza el import arriba de todo en app.py, antes de NezzontliApp().run(). El sync de scroll entre el TextArea y el preview era por proporción de alturas, que se rompe en cuanto el preview tiene bloques de altura fija (imágenes, fórmulas) distintos a la del texto — quedaban desfazados y al volver a editar no se veía la línea correspondiente. Ahora se ancla por línea de origen real: TextArea es un widget de líneas monoespaciadas así que scroll_y ya es el número de línea top, y se busca qué widget del preview corresponde a esa línea (tokenize ahora devuelve el offset de cada segmento) en vez de escalar por altura. También: indicador '⏳ Renderizando...' mientras se resuelven imágenes/ fórmulas, y el render de cada segmento corre en paralelo (asyncio.gather) en vez de secuencial, para que abrir el editor no se sienta tan lento.
Preview del editor: renderiza LaTeX e imágenes (locales/remotas), fija libs de NixOS
El preview de Markdown ahora tokeniza fórmulas $...$/$$...$$ (renderizadas a PNG vía matplotlib.mathtext) e imágenes referenciadas por markdown estándar o por los shortcodes de Zola (photo/gallery/image), mostrándolas con textual-image (Kitty/Sixel/Unicode). El rebuild del preview corre debounced y en un worker para no bloquear la UI con el render/descarga. flake.nix agrega libstdc++/zlib al devShell — los wheels compilados de numpy (dependencia de matplotlib) los buscan en rutas FHS que no existen en NixOS.
Editar entradas existentes (post/página/álbum) + fixes de UI
- screens/post.py: NewPostScreen acepta existing_file, precarga campos y cuerpo real, escribe sobre el mismo archivo en vez de crear uno nuevo (oculta el campo slug, no aplica al editar) - screens/edit_album.py: título/descripción/tags/cuerpo de un álbum existente (las fotos se siguen agregando por "Agregar fotos") - screens/select_existing.py: picker genérico reutilizado por ambos - menu.py: dos opciones nuevas ("Editar un post..." / "...álbum") - content.py: parse_frontmatter() (tomllib) para leer el TOML existente al editar Bugs encontrados al probar edición real: - La fecha se pisaba con la de hoy en cualquier edición (dt=None por default, nunca se pasaba la fecha original) — agregado _toml_date() que además tolera fechas guardadas como string entre comillas (pasó con un post tocado antes por Sveltia CMS; Zola lo acepta igual, tomllib lo lee como str en vez de date) - El preview de Markdown en EditorScreen no scrolleaba con la rueda del mouse: el widget Markdown no es scrolleable por si solo (extiende Widget, no ScrollableContainer). Envuelto en un VerticalScroll, y agregado sync proporcional real vía self.watch(text_area, "scroll_y", ...) — el "eco" de scroll_y no tiene el mismo problema de timing que tuvo el de slug porque acá se usa watch() en vez de comparar valores manualmente - Sacado el "— Ctrl+S para guardar, Esc para cancelar" del título del editor (ya está en el Footer de abajo, quedaba duplicado)
Arregla el autocompletado de slug (se pegaba en la primera letra)
El "eco" del Input.Changed que dispara asignar slug-input.value programáticamente se procesa async, así que una bandera que se prendía y apagaba en el mismo handler síncrono no llegaba a tiempo: para cuando se procesaba el evento de eco, la bandera ya estaba en False de nuevo, y quedaba marcado como "tocado a mano" tras la primera letra. Ahora compara el valor contra el último slug que puse yo mismo, en vez de una bandera transitoria.
App Textual completa: menú, editor con preview, 4 flujos, cowsay
- app.py + theme.py: tema Gruvbox (misma paleta que nezzontli.xyz) - screens/menu.py: banner Figlet + OptionList - screens/editor.py: TextArea(markdown) + Markdown preview en vivo, reemplaza abrir $EDITOR externo - screens/post.py, page.py, album.py, add_photos.py: los 4 flujos, con Select/SelectionList/DirectoryTree en vez de texto libre donde aplica - screens/confirm.py, success.py: diff+preview+commit/push real, pantalla final con cowsay (animal configurable) - screens/settings.py, folder_picker.py Validado de punta a punta contra un clone local descartable (bare + working copy, no el repo real): los 4 flujos completos, con commit y push reales, y confirmado con `git show :ruta` que las fotos quedan como punteros LFS reales, no blobs crudos. Encontrado y arreglado en el camino: commit_and_push no manejaba con gracia un `git commit` sin nada para comitear (podía tirar abajo el worker); el botón de confirmar ahora se deshabilita al primer click para evitar envíos duplicados. `pipx install -e .` no se pudo validar tal cual: pipx falla al compilarse desde este snapshot de nixpkgs (bug de su propio test suite, no relacionado a este proyecto). El entry point `ctl` sí se probó directo vía `.venv/bin/ctl` y funciona.
Estructura inicial: pyproject.toml, config/content/git_ops, flake.nix
Proyecto separado de website — opera sobre ese repo (NEZZONTLI_REPO_PATH) en vez de vivir adentro. En desarrollo, todavía no reemplaza a website/scripts/ctl.py.
