- Description:
- TUI para gestionar el contenido de nezzontli.xyz sin CMS.
- Last Change:
- Clone URL:
https://git.nezzontli.xyz/clone/nezzontli-ctl.git
Commit Briefs
flake.nix: agrega packages.default — paquete Nix real, no solo devShell (main)
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.
Branches
Tree
| .gitignore | commits | blame |
| README.md | commits | blame |
| bin/ | |
| flake.lock | commits | blame |
| flake.nix | commits | blame |
| nezzontli_ctl/ | |
| pyproject.toml | commits | blame |
README.md
# nezzontli-ctl TUI (Textual) para crear/editar contenido de [nezzontli.xyz](https://nezzontli.xyz) sin depender de un CMS externo — ninguno soporta git-lfs, que es como se versionan las fotos de los álbumes. Esta herramienta usa git real (el mismo `git lfs install` de siempre), así que no tiene ese problema. Es un proyecto separado del repo del sitio: opera *sobre* `~/projects/website` (u otra ruta, ver `NEZZONTLI_REPO_PATH` abajo), no vive adentro de él. ## Desarrollo ```bash nix develop # crea/activa .venv y hace pip install -e . automáticamente ctl # o: python3 -m nezzontli_ctl ``` `NEZZONTLI_REPO_PATH` (variable de entorno, opcional) apunta al clone del sitio sobre el que trabaja. Default: `~/projects/website`. También se puede fijar de forma persistente en `~/.config/nezzontli-ctl/config.json` (`repo_path`) o cambiarla desde la pantalla de Configuración. ## Instalación (comando `ctl` global, sin `nix develop` a mano) ```bash ln -s ~/projects/nezzontli-ctl/bin/ctl ~/.local/bin/ctl ctl # ya disponible en cualquier terminal ``` `bin/ctl` es un wrapper (`nix develop . --command ctl` parado en el directorio del repo) — no un `pipx install`. La app necesita `libstdc++.so.6` (lo pide numpy, dependencia de matplotlib para el renderizado de LaTeX del preview), y en NixOS esa lib solo está en el `LD_LIBRARY_PATH` que arma el `shellHook` del flake; un `pipx install` plano no lo lleva y crashearía al primer `$...$` en el preview. El wrapper sortea eso reusando siempre el flake real. ## Estado Menú de dos niveles: **Crear nuevo…** (post / página en un proyecto / álbum) y **Editar existente…** (post / página extra de un proyecto / álbum / agregar fotos a un álbum), más Configuración. Cada flujo de edición tiene botón "Eliminar" (confirmación simple, borra del disco y pushea). Validado contra un clone local descartable (bare repo como origin) para los tres borrados: se confirma qué se borra del disco, el commit y el push al origin. El preview del editor, además de Markdown, renderiza fórmulas en bloque (`$$...$$`) como imagen (subset mathtext de matplotlib, fondo transparente) y aproxima el LaTeX inline (`$...$`) a texto Unicode en el lugar (no rompe el párrafo). Muestra imágenes referenciadas —locales del repo (`![]()`, shortcodes `photo()`/`gallery()` de Zola) o remotas por URL (shortcode `image()`)— usando el protocolo de gráficos de la terminal (Kitty, Sixel o half-cell según detección; si no hay terminal real, cae a texto). El scroll del editor y el preview están sincronizados por línea de origen. Probado contra el repo real de nezzontli.xyz en uso diario. Queda a decisión de Ale cuándo retirar `website/scripts/ctl.py` (el script viejo, todavía intacto) y si integrar esta herramienta a su config de NixOS.
