<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ilean Monterrubio Jr</title>
    <description>The latest articles on DEV Community by Ilean Monterrubio Jr (@elcapitan).</description>
    <link>https://dev.to/elcapitan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2648828%2F4d9e8133-29de-4164-b93c-8376ddd05528.png</url>
      <title>DEV Community: Ilean Monterrubio Jr</title>
      <link>https://dev.to/elcapitan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/elcapitan"/>
    <language>en</language>
    <item>
      <title>Patrones embebidos comunes: búfer circular</title>
      <dc:creator>Ilean Monterrubio Jr</dc:creator>
      <pubDate>Fri, 31 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/elcapitan/patrones-embebidos-comunes-bufer-circular-3280</link>
      <guid>https://dev.to/elcapitan/patrones-embebidos-comunes-bufer-circular-3280</guid>
      <description>&lt;p&gt;Esta es la primera parte de una nueva serie donde vuelvo a explorar los patrones embebidos más comunes, empezando por el búfer circular (ring buffer). Es uno de esos patrones que se usan en la comunicación serial para asegurarnos de no perder ni una trama de datos, y además es un bloque de construcción para otros patrones que veremos más adelante.&lt;/p&gt;

&lt;p&gt;El código fuente completo y probado vive en el repositorio complementario: &lt;a href="https://github.com/ileanmjr88/tetzontli" rel="noopener noreferrer"&gt;ileanmjr88/tetzontli&lt;/a&gt;. Cada patrón de esta serie es su propio módulo con una suite completa de pruebas en GoogleTest, así que puedes clonarlo y ejecutar las pruebas tú mismo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concepto básico
&lt;/h2&gt;

&lt;p&gt;El búfer circular usa dos índices, &lt;code&gt;head&lt;/code&gt; y &lt;code&gt;tail&lt;/code&gt;. Para agregar datos hacemos &lt;code&gt;put&lt;/code&gt; de un byte en &lt;code&gt;head&lt;/code&gt; e incrementamos &lt;code&gt;head&lt;/code&gt; en uno. Para quitar datos hacemos &lt;code&gt;get&lt;/code&gt; de un byte en &lt;code&gt;tail&lt;/code&gt; e incrementamos &lt;code&gt;tail&lt;/code&gt; en uno. Ambos índices solo avanzan hacia adelante: &lt;code&gt;tail&lt;/code&gt; persigue a &lt;code&gt;head&lt;/code&gt; por todo el búfer.&lt;/p&gt;

&lt;p&gt;Este patrón se puede implementar con una lista enlazada o con un arreglo (array) de longitud fija. La lista enlazada suena tentadora, pero en un sistema embebido no quieres asignación dinámica de memoria. En una computadora moderna con gigabytes de memoria, llamar a &lt;code&gt;malloc&lt;/code&gt; no es problema. En un microcontrolador tienes kilobytes, y tres problemas más grandes: el heap se fragmenta en un dispositivo que funciona durante meses, el tiempo que tarda una asignación no es determinista, y muchas veces necesitas agregar datos desde dentro de una interrupción, donde no te puedes dar el lujo de ninguna de las dos cosas. Un arreglo fijo evita todo eso, porque el espacio queda garantizado en tiempo de compilación.&lt;/p&gt;

&lt;p&gt;Ese espacio garantizado es justo lo que le da tiempo al consumidor: los bytes que llegan más rápido de lo que los puedes procesar se quedan seguros en el búfer hasta que llegues a ellos, en lugar de perderse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dar la vuelta dentro del arreglo
&lt;/h2&gt;

&lt;p&gt;Un arreglo no da la vuelta. Cuando llegas al final e intentas pasarte, no regresa al inicio. Te sales del espacio asignado, lo que puede provocar un fallo de segmentación, devolverte un puntero NULL o entregarte basura.&lt;/p&gt;

&lt;h3&gt;
  
  
  La intuición
&lt;/h3&gt;

&lt;p&gt;La forma intuitiva de regresar al inicio es el operador módulo &lt;code&gt;%&lt;/code&gt;, que devuelve el residuo de una división.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Array of length/capacity: 10

Formula: index % capacity -&amp;gt; array index

- index 0: 0 % 10 = 0
- index 1: 1 % 10 = 1
- index 4: 4 % 10 = 4
- index 10: 10 % 10 = 0
- index 11: 11 % 10 = 1

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El módulo nos deja tratar el arreglo fijo como un anillo, lo que lo hace ideal para un búfer circular. Funciona bien en una computadora moderna con varios núcleos, frecuencias altas y una unidad de división por hardware. Pero el módulo tiene una desventaja: requiere división, y muchos microcontroladores no tienen división por hardware. El compilador la emula por software, que es lento, y pagamos ese costo con cada byte que pasa por el búfer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optimizando con una máscara de bits
&lt;/h3&gt;

&lt;p&gt;Hay una forma más rápida de hacer exactamente lo mismo que el operador módulo, con una restricción: la longitud del arreglo debe ser una potencia de dos. Esa restricción es la llave que nos permite recrear el módulo con una sola operación a nivel de bits. La fórmula es &lt;code&gt;index = head &amp;amp; mask&lt;/code&gt;, donde &lt;code&gt;mask = capacity - 1&lt;/code&gt;. Repasemos las matemáticas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Array of length/capacity: 8 (power of two)

mask = 8 - 1 = 7 = 0b0111

- index 0: 0b0000 &amp;amp; 0b0111 = 0
- index 1: 0b0001 &amp;amp; 0b0111 = 1
- index 2: 0b0010 &amp;amp; 0b0111 = 2
- index 8: 0b1000 &amp;amp; 0b0111 = 0
- index 10: 0b1010 &amp;amp; 0b0111 = 2
- index 14: 0b1110 &amp;amp; 0b0111 = 6

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fíjate que son las mismas respuestas que daría el módulo: &lt;code&gt;8 % 8 = 0&lt;/code&gt;, &lt;code&gt;10 % 8 = 2&lt;/code&gt;, &lt;code&gt;14 % 8 = 6&lt;/code&gt;. No es coincidencia. La máscara &lt;code&gt;0b0111&lt;/code&gt; conserva solo los tres bits más bajos y limpia todo lo que está arriba, y los tres bits más bajos de cualquier número son exactamente ese número módulo 8. Quedarte con los bits bajos &lt;em&gt;es&lt;/em&gt; sacar el residuo. Como la capacidad es potencia de dos, &lt;code&gt;capacity - 1&lt;/code&gt; es una secuencia limpia de unos, y eso es lo que hace que el AND coincida perfectamente con el módulo. Intenta esto con un número que no sea potencia de dos y se cae todo: &lt;code&gt;11 &amp;amp; 9 = 9&lt;/code&gt;, pero &lt;code&gt;11 % 10 = 1&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementación
&lt;/h2&gt;

&lt;h3&gt;
  
  
  La estructura del búfer circular
&lt;/h3&gt;

&lt;p&gt;Creamos un struct para mantener juntos todos los datos. Esto nos facilita la vida, porque con un solo puntero a la estructura podemos acceder a &lt;code&gt;buffer&lt;/code&gt;, &lt;code&gt;capacity&lt;/code&gt;, &lt;code&gt;mask&lt;/code&gt;, &lt;code&gt;head&lt;/code&gt; y &lt;code&gt;tail&lt;/code&gt;. También hace que los datos sean fáciles de pasar por puntero a cualquier función que necesite agregar o leer de él.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.h&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;volatile&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;volatile&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Como puedes ver, el struct usa varios tipos distintos, y cada uno es deliberado.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;uint8_t&lt;/code&gt;: el búfer guarda bytes crudos. Sea cual sea la fuente, datos seriales de UART, I2C o cualquier otra cosa, llegan como octetos, así que un arreglo de bytes es el almacenamiento natural para la cola.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;size_t&lt;/code&gt;: &lt;code&gt;capacity&lt;/code&gt;, &lt;code&gt;mask&lt;/code&gt;, &lt;code&gt;head&lt;/code&gt; y &lt;code&gt;tail&lt;/code&gt; son todos &lt;code&gt;size_t&lt;/code&gt;. Es un tipo sin signo del tamaño de la plataforma (normalmente un alias de &lt;code&gt;unsigned int&lt;/code&gt; o &lt;code&gt;unsigned long&lt;/code&gt;), lo que mantiene la aritmética de índices portable entre compiladores y evita problemas de desbordamiento con signo. Que sea sin signo importa más adelante, cuando dependemos de que &lt;code&gt;head - tail&lt;/code&gt; dé la vuelta limpiamente.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;volatile&lt;/code&gt;: &lt;code&gt;head&lt;/code&gt; y &lt;code&gt;tail&lt;/code&gt; están marcados como &lt;code&gt;volatile&lt;/code&gt; para que el compilador siempre los lea de memoria en lugar de guardarse una copia vieja en un registro. El productor y el consumidor pueden ejecutarse en contextos distintos (por ejemplo, una ISR llenando el búfer mientras el bucle principal lo vacía), así que un valor guardado en un registro podría perderse la actualización del otro lado. Ojo: &lt;code&gt;volatile&lt;/code&gt; solo garantiza que la lectura ocurra, no hace que el acceso sea atómico ni seguro entre hilos. Veremos por qué este diseño es seguro de todos modos en la sección sobre interrupciones.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Las funciones de inicialización y de potencia de dos
&lt;/h3&gt;

&lt;p&gt;Empecemos con &lt;code&gt;init&lt;/code&gt; y su ayudante &lt;code&gt;is_power_of_two&lt;/code&gt;. La función &lt;code&gt;init&lt;/code&gt; prepara la estructura de datos, y la verificación de potencia de dos es el primer filtro: si quien la llama pide una capacidad que no es potencia de dos, la rechazamos, porque todo el esquema de enmascarado depende de eso. Arranquemos con el ayudante.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;is_power_of_two&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0u&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1u&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esta implementación hace lo siguiente:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;x != 0u&lt;/code&gt;: verifica que &lt;code&gt;x&lt;/code&gt; no sea &lt;code&gt;0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(x &amp;amp; (x - 1u)) == 0u&lt;/code&gt;: esta es la parte que verifica que haya un solo bit encendido. Una potencia de dos tiene exactamente un bit encendido. Restarle uno apaga ese bit y enciende todos los que están debajo, así que &lt;code&gt;x&lt;/code&gt; y &lt;code&gt;x - 1&lt;/code&gt; no comparten ningún bit y el AND da cero. Toma &lt;code&gt;x = 8&lt;/code&gt;: &lt;code&gt;8 - 1 = 7&lt;/code&gt;, y &lt;code&gt;0b1000 &amp;amp; 0b0111 = 0&lt;/code&gt;. Ahora toma un número que no sea potencia de dos, &lt;code&gt;x = 12&lt;/code&gt;: &lt;code&gt;12 - 1 = 11&lt;/code&gt;, y &lt;code&gt;0b1100 &amp;amp; 0b1011 = 0b1000&lt;/code&gt;, que no es cero. Ese bit que sobra delata que &lt;code&gt;x&lt;/code&gt; tenía más de un bit encendido.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si ambas condiciones se cumplen, entonces &lt;code&gt;x&lt;/code&gt; es potencia de dos, así que la función devuelve &lt;code&gt;true&lt;/code&gt; si y solo si &lt;code&gt;x&lt;/code&gt; es potencia de dos. Ahora pasemos a la función &lt;code&gt;ring_buffer_init&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;ring_buffer_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ring_buffer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;is_power_of_two&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; valida primero sus entradas: un puntero nulo al struct, un &lt;code&gt;storage&lt;/code&gt; nulo, o una capacidad que no sea potencia de dos hacen que se salga y devuelva &lt;code&gt;false&lt;/code&gt;. Una vez que pasan las verificaciones, arma el struct, precalcula &lt;code&gt;mask = capacity - 1&lt;/code&gt; para que &lt;code&gt;put&lt;/code&gt; y &lt;code&gt;get&lt;/code&gt; nunca tengan que recalcularlo, y pone ambos índices en cero.&lt;/p&gt;

&lt;p&gt;Fíjate que &lt;code&gt;init&lt;/code&gt; no asigna nada. Quien la llama nos entrega el arreglo &lt;code&gt;storage&lt;/code&gt;, así que el búfer puede vivir en memoria estática o en la pila. Este es el principio de no usar &lt;code&gt;malloc&lt;/code&gt; que mencionamos antes, hecho concreto: no hay ni un &lt;code&gt;malloc&lt;/code&gt; a la vista, y el espacio quedó garantizado en el momento en que el llamador declaró el arreglo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Las funciones put y get
&lt;/h3&gt;

&lt;p&gt;Estas funciones ponen y sacan un solo byte a la vez. Las limitamos a un byte porque a veces un solo byte es todo lo que necesitamos, y porque las funciones &lt;code&gt;write&lt;/code&gt; y &lt;code&gt;read&lt;/code&gt; de la siguiente sección están construidas encima de ellas. Empecemos con &lt;code&gt;ring_buffer_put&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;ring_buffer_put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ring_buffer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_is_full&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La función empieza validando que &lt;code&gt;rb&lt;/code&gt; no sea &lt;code&gt;NULL&lt;/code&gt;, y luego llama al ayudante &lt;code&gt;ring_buffer_is_full&lt;/code&gt;. Si el búfer está lleno salimos temprano y devolvemos &lt;code&gt;false&lt;/code&gt;, avisándole al llamador que no se guardó ningún byte. Veremos cómo funciona &lt;code&gt;is_full&lt;/code&gt; en la siguiente sección. Si hay espacio, hacemos la escritura, y aquí es donde rinde frutos la sección anterior: &lt;code&gt;rb-&amp;gt;head &amp;amp; rb-&amp;gt;mask&lt;/code&gt; calcula el índice del arreglo con el AND a nivel de bits, envolviendo el &lt;code&gt;head&lt;/code&gt; de avance libre de regreso a una ranura válida. Escribimos el byte ahí, y luego incrementamos &lt;code&gt;head&lt;/code&gt; en uno para que el siguiente &lt;code&gt;put&lt;/code&gt; caiga en la ranura que sigue. Fíjate en el orden: primero escribimos la ranura, después avanzamos &lt;code&gt;head&lt;/code&gt;. Ese orden importa para la seguridad con interrupciones, y llegaremos a eso más adelante. Ahora veamos &lt;code&gt;ring_buffer_get&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;ring_buffer_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ring_buffer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_is_empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esta función también empieza validando que &lt;code&gt;rb&lt;/code&gt; no sea &lt;code&gt;NULL&lt;/code&gt;, y luego verifica que &lt;code&gt;out&lt;/code&gt; tampoco lo sea. Ese es el puntero donde se guarda el byte que leemos, que es la forma en que &lt;code&gt;get&lt;/code&gt; le devuelve datos al llamador dejando libre su valor de retorno &lt;code&gt;bool&lt;/code&gt; para reportar éxito o fracaso. Por último llama al ayudante &lt;code&gt;ring_buffer_is_empty&lt;/code&gt;. Si el búfer está vacío salimos temprano y devolvemos &lt;code&gt;false&lt;/code&gt;, avisándole al llamador que no se leyó ningún byte. Veremos cómo funciona &lt;code&gt;is_empty&lt;/code&gt; en la siguiente sección. Si no está vacío, hacemos la lectura, y otra vez la sección anterior rinde frutos: &lt;code&gt;rb-&amp;gt;tail &amp;amp; rb-&amp;gt;mask&lt;/code&gt; calcula el índice del arreglo con el AND a nivel de bits, envolviendo el &lt;code&gt;tail&lt;/code&gt; de avance libre en una ranura válida. Leemos el byte e incrementamos &lt;code&gt;tail&lt;/code&gt; en uno. Fíjate que &lt;code&gt;get&lt;/code&gt; nunca borra nada. Lee el byte y mueve &lt;code&gt;tail&lt;/code&gt; hacia adelante, y esa ranura cuenta como libre en el momento en que &lt;code&gt;tail&lt;/code&gt; pasa por encima de ella. Los índices por sí solos deciden qué son datos y qué es espacio libre.&lt;/p&gt;

&lt;h3&gt;
  
  
  Las funciones is_empty, is_full y count
&lt;/h3&gt;

&lt;p&gt;Estos tres ayudantes, &lt;code&gt;is_empty&lt;/code&gt;, &lt;code&gt;is_full&lt;/code&gt; y &lt;code&gt;count&lt;/code&gt;, son en los que se apoyan &lt;code&gt;put&lt;/code&gt; y &lt;code&gt;get&lt;/code&gt; para decidir si pueden seguir adelante. Aquí es donde los índices de avance libre por fin se ganan su lugar. Empecemos con &lt;code&gt;ring_buffer_is_empty&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;ring_buffer_is_empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El búfer está vacío cuando &lt;code&gt;head&lt;/code&gt; y &lt;code&gt;tail&lt;/code&gt; tienen el mismo valor. Tiene sentido: &lt;code&gt;tail&lt;/code&gt; persigue a &lt;code&gt;head&lt;/code&gt;, y cuando lo alcanza por completo ya no queda nada por leer. Aquí está la parte sutil. En muchos diseños de búfer circular los índices se envuelven de regreso al rango válido, y entonces &lt;code&gt;head == tail&lt;/code&gt; es ambiguo, porque significa &lt;em&gt;tanto&lt;/em&gt; completamente vacío &lt;em&gt;como&lt;/em&gt; completamente lleno, y no puedes distinguir cuál. Como nuestros índices avanzan libres y nunca se envuelven, &lt;code&gt;head == tail&lt;/code&gt; solo puede significar vacío. En un momento veremos cómo se maneja el caso lleno sin esa ambigüedad.&lt;/p&gt;

&lt;p&gt;Sigue &lt;code&gt;ring_buffer_count&lt;/code&gt;, que es el corazón de todo esto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="nf"&gt;ring_buffer_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ambos índices solo aumentan. Cada &lt;code&gt;put&lt;/code&gt; incrementa &lt;code&gt;head&lt;/code&gt;, cada &lt;code&gt;get&lt;/code&gt; incrementa &lt;code&gt;tail&lt;/code&gt;, y ninguno se envuelve. Así que &lt;code&gt;head&lt;/code&gt; siempre va adelante de &lt;code&gt;tail&lt;/code&gt; por exactamente la cantidad de bytes que se han escrito pero todavía no se han leído, que es el nivel de llenado. La diferencia &lt;code&gt;head - tail&lt;/code&gt; &lt;em&gt;es&lt;/em&gt; la cuenta.&lt;/p&gt;

&lt;p&gt;Lo bonito es lo que pasa cuando &lt;code&gt;head&lt;/code&gt; finalmente se desborda. &lt;code&gt;size_t&lt;/code&gt; tiene un máximo, y después de suficientes bytes &lt;code&gt;head&lt;/code&gt; lo rebasa y da la vuelta hacia cero. Podrías esperar que &lt;code&gt;head - tail&lt;/code&gt; se rompa en ese punto, pero no pasa. La resta sin signo da la vuelta igual que la suma sin signo, así que la diferencia sigue siendo correcta incluso a través del desbordamiento. Justo por esto &lt;code&gt;head&lt;/code&gt; y &lt;code&gt;tail&lt;/code&gt; son sin signo, y por esto el búfer puede funcionar indefinidamente sin ningún manejo especial para el desbordamiento.&lt;/p&gt;

&lt;p&gt;Por último, &lt;code&gt;ring_buffer_is_full&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;ring_buffer_is_full&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una vez que tenemos &lt;code&gt;count&lt;/code&gt;, lo de lleno es trivial: el búfer está lleno cuando contiene &lt;code&gt;capacity&lt;/code&gt; bytes. Sin desperdiciar una ranura, sin un contador aparte que mantener sincronizado, sin ambigüedad con el caso vacío. Los índices de avance libre nos dan vacío (&lt;code&gt;head == tail&lt;/code&gt;), lleno (&lt;code&gt;count == capacity&lt;/code&gt;) y todo lo que hay en medio, todo a partir de dos números que solo cuentan hacia arriba.&lt;/p&gt;

&lt;h3&gt;
  
  
  Las funciones write y read
&lt;/h3&gt;

&lt;p&gt;Estas funciones escriben y leen más de un byte a la vez. Empecemos con &lt;code&gt;ring_buffer_write&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="nf"&gt;ring_buffer_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ring_buffer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esta función devuelve la cantidad de bytes que realmente se escribieron, que puede ser menor a la que se pidió. Devuelve &lt;code&gt;0&lt;/code&gt; en la salida temprana, cuando el búfer circular &lt;code&gt;rb&lt;/code&gt; o el puntero de origen &lt;code&gt;data&lt;/code&gt; son &lt;code&gt;NULL&lt;/code&gt;. Si no, inicializamos &lt;code&gt;written&lt;/code&gt; para llevar la cuenta de cuántos bytes hemos copiado de &lt;code&gt;data&lt;/code&gt; al búfer. El ciclo se ejecuta con dos condiciones unidas por &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;: &lt;code&gt;written &amp;lt; len&lt;/code&gt; nos impide leer más allá del final de la entrada, y &lt;code&gt;ring_buffer_put&lt;/code&gt; devuelve &lt;code&gt;false&lt;/code&gt; en cuanto el búfer se llena. Mientras ambas se cumplan, seguimos copiando bytes e incrementando &lt;code&gt;written&lt;/code&gt;. El ciclo se detiene en cuanto una de las dos falla, ya sea porque se nos acabó la entrada o porque se nos acabó el espacio. La cuenta que devolvemos le dice al llamador exactamente cuánto entró: todo, una parte, o cero si el búfer ya estaba lleno.&lt;/p&gt;

&lt;p&gt;Sigue &lt;code&gt;ring_buffer_read&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="nf"&gt;ring_buffer_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ring_buffer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esta función también devuelve la cantidad de bytes que realmente se leyeron, que puede ser menor a la que se pidió. Devuelve &lt;code&gt;0&lt;/code&gt; en la salida temprana, cuando el búfer circular &lt;code&gt;rb&lt;/code&gt; o el puntero de destino &lt;code&gt;out&lt;/code&gt; son &lt;code&gt;NULL&lt;/code&gt;. Si no, inicializamos &lt;code&gt;count&lt;/code&gt; para llevar la cuenta de cuántos bytes hemos leído del búfer hacia &lt;code&gt;out&lt;/code&gt;. El ciclo se ejecuta con dos condiciones unidas por &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;: &lt;code&gt;count &amp;lt; len&lt;/code&gt; nos impide escribir más allá del final de la salida, y &lt;code&gt;ring_buffer_get&lt;/code&gt; devuelve &lt;code&gt;false&lt;/code&gt; en cuanto el búfer se vacía. Mientras ambas se cumplan, seguimos leyendo bytes e incrementando &lt;code&gt;count&lt;/code&gt;. El ciclo se detiene en cuanto una de las dos falla, ya sea porque el búfer del llamador se llenó o porque el búfer circular se quedó seco. La cuenta que devolvemos le dice al llamador exactamente cuántos bytes recibió.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seguridad con la rutina de servicio de interrupción (ISR)
&lt;/h2&gt;

&lt;p&gt;Para quienes van empezando en embebidos, puede que no sea obvio por qué se considera seguro este diseño, ni cuáles son sus aplicaciones típicas. El uso principal de un búfer circular, como mencionamos antes, es la comunicación serial. Toma el ejemplo de dos microcontroladores hablando entre sí, cada uno ejecutando su propio firmware hecho a la medida de su tarea. No sabemos cuándo va a necesitar enviar datos ninguno de los dos, así que para asegurarnos de no perder un solo byte, montamos la ruta de recepción de datos (RX) usando la rutina de servicio de interrupción (Interrupt Service Routine, ISR) del microcontrolador. En cuanto llega el inicio de una transmisión, la ISR toma el control y captura cada byte.&lt;/p&gt;

&lt;p&gt;Este es el lado productor del búfer. La ISR llama a &lt;code&gt;put&lt;/code&gt; por cada byte que recibe. En otro lado, el bucle principal es el consumidor, llamando a &lt;code&gt;get&lt;/code&gt; cuando tiene chance de procesar lo que llegó. Los dos se ejecutan en contextos distintos, y aquí está el detalle: la ISR puede dispararse en cualquier momento, entre cualesquiera dos instrucciones que esté ejecutando el bucle principal. Así que la pregunta justa es: si la interrupción cae a la mitad de un &lt;code&gt;get&lt;/code&gt;, ¿pueden pisarse entre sí y corromper el búfer?&lt;/p&gt;

&lt;p&gt;La respuesta es no, y la razón es que cada índice tiene exactamente un escritor. &lt;code&gt;put&lt;/code&gt; es la única función que escribe &lt;code&gt;head&lt;/code&gt;, y solo se ejecuta en la ISR. &lt;code&gt;get&lt;/code&gt; es la única función que escribe &lt;code&gt;tail&lt;/code&gt;, y solo se ejecuta en el bucle principal. Ninguno de los dos lados escribe el índice del otro. El productor lee &lt;code&gt;tail&lt;/code&gt; para saber si el búfer está lleno, y el consumidor lee &lt;code&gt;head&lt;/code&gt; para saber si está vacío, pero leer es seguro. Como ningún índice tiene dos escritores compitiendo por cambiarlo, no hay nada que corromper, y no necesitamos deshabilitar interrupciones ni recurrir a un candado. A esto se refiere la gente cuando habla de un solo productor y un solo consumidor, o SPSC por sus siglas en inglés.&lt;/p&gt;

&lt;p&gt;Esa regla de un solo escritor también es el límite. En el momento en que tengas dos cosas llamando a &lt;code&gt;put&lt;/code&gt;, o dos cosas llamando a &lt;code&gt;get&lt;/code&gt;, la garantía se acaba y necesitas sincronización de verdad. Este búfer es seguro para exactamente un productor y un consumidor, ni uno más.&lt;/p&gt;

&lt;p&gt;Hay un detalle más que lo hace funcionar, y es el orden que señalamos allá en &lt;code&gt;put&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1. write the data&lt;/span&gt;
&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 2. then publish it&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Primero escribimos el byte en la ranura, y después incrementamos &lt;code&gt;head&lt;/code&gt;. Ese orden no es accidente. &lt;code&gt;head&lt;/code&gt; es la señal que le dice al consumidor que hay un byte nuevo disponible. Si incrementáramos &lt;code&gt;head&lt;/code&gt; primero y el tiempo de la interrupción se acomodara mal, el consumidor podría ver el nuevo &lt;code&gt;head&lt;/code&gt;, ir a leer esa ranura y sacar un byte que en realidad todavía no habíamos escrito. Al escribir la ranura antes de avanzar &lt;code&gt;head&lt;/code&gt;, garantizamos que en el instante en que el consumidor puede ver el byte, el byte de verdad ya está ahí. La actualización del índice publica los datos, así que va al final. &lt;code&gt;get&lt;/code&gt; hace el espejo: primero leemos el byte y después avanzamos &lt;code&gt;tail&lt;/code&gt;, así nunca marcamos una ranura como libre antes de habernos llevado su contenido.&lt;/p&gt;

&lt;p&gt;Un par de advertencias honestas para quien las quiera. Primero, &lt;code&gt;volatile&lt;/code&gt; nos da visibilidad, no atomicidad. Este diseño asume que leer o escribir &lt;code&gt;head&lt;/code&gt; y &lt;code&gt;tail&lt;/code&gt; sucede en una sola operación de máquina ininterrumpible, lo cual es cierto cuando son del tamaño de palabra en el objetivo. En un MCU de 8 bits, donde &lt;code&gt;size_t&lt;/code&gt; abarca varios bytes, la actualización de un índice podría partirse en varias instrucciones y quedar rota por una interrupción, y tendrías que tomarlo en cuenta. Segundo, en un microcontrolador de un solo núcleo con una ISR, &lt;code&gt;volatile&lt;/code&gt; más el orden de escribir y luego publicar es suficiente. En un sistema multinúcleo con ordenamiento de memoria débil recurrirías a operaciones atómicas con semántica explícita de adquisición y liberación, para garantizar que la escritura de la ranura se vea antes que la del índice. Para el caso clásico de ISR y bucle principal, que es para el que está hecho este búfer, estamos en terreno firme.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cierre
&lt;/h2&gt;

&lt;p&gt;Eso es un búfer circular completo y funcional en un par de cientos de líneas de C. No hace asignación dinámica, da la vuelta con un solo AND a nivel de bits en lugar de una división, distingue lleno de vacío sin desperdiciar una ranura ni llevar un contador aparte, y es seguro llenarlo desde una interrupción mientras lo vaciamos desde el bucle principal. Para una estructura tan pequeña, trae metida una cantidad sorprendente de sutilezas.&lt;/p&gt;

&lt;p&gt;Vale la pena ser honesto sobre lo que este diseño no hace, porque cada una de estas cosas es un intercambio deliberado, no un descuido:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;La capacidad debe ser potencia de dos.&lt;/strong&gt; Este es el precio de cambiar el módulo por una máscara. Si necesitas guardar exactamente 100 bytes, redondeas a 128 y desperdicias un poco de espacio. En un sistema embebido casi siempre es un intercambio que conviene.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solo un productor y un consumidor.&lt;/strong&gt; Un escritor por índice es lo que lo hace libre de candados. Dos ISR escribiendo, o dos contextos leyendo, y necesitas sincronización de verdad.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No hay modo de sobrescribir lo más viejo.&lt;/strong&gt; Cuando el búfer está lleno, &lt;code&gt;put&lt;/code&gt; rechaza el byte nuevo en lugar de descartar el más viejo. Algunas aplicaciones quieren lo contrario, conservar las muestras más recientes y tirar las viejas. Esa es una variante razonable, solo que no es esta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solo bytes.&lt;/strong&gt; Este búfer guarda &lt;code&gt;uint8_t&lt;/code&gt;. Guardar elementos más anchos, o structs arbitrarios, implica generalizar el tamaño del elemento, lo que cambia la aritmética de índices y el almacenamiento.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ninguna de estas limitaciones es difícil de levantar. Son las preguntas que salen naturalmente, y varias de ellas se convierten en sus propios patrones más adelante en esta serie.&lt;/p&gt;

&lt;p&gt;Y hablando de eso, el búfer circular es un cimiento, no un destino. Aparece por debajo de varios de los patrones que faltan: una cola de eventos es un búfer circular de mensajes en lugar de bytes, un filtro de promedio móvil es un búfer circular de muestras, y el despachador de comandos que construiremos lee su entrada directamente de uno. Lo que sigue en la serie es el pool de memoria. El búfer circular nos dio almacenamiento fijo para bytes; el pool de memoria nos da almacenamiento fijo para objetos, y es lo que permite que una cola de eventos guarde structs en lugar de datos crudos. Nos vemos ahí.&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>embeddedsystems</category>
      <category>datastructures</category>
      <category>c</category>
    </item>
    <item>
      <title>Common Embedded Patterns: Ring Buffer</title>
      <dc:creator>Ilean Monterrubio Jr</dc:creator>
      <pubDate>Fri, 31 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/elcapitan/common-embedded-patterns-ring-buffer-36jc</link>
      <guid>https://dev.to/elcapitan/common-embedded-patterns-ring-buffer-36jc</guid>
      <description>&lt;p&gt;This is the first part of a new series where I'll re-explore common embedded patterns, starting with the ring buffer. It's one of those patterns used in serial communication to make sure no frame of data is lost, and it's also a building block for other patterns we'll cover later.&lt;/p&gt;

&lt;p&gt;The complete, tested source lives in the companion repo: &lt;a href="https://github.com/ileanmjr88/tetzontli" rel="noopener noreferrer"&gt;ileanmjr88/tetzontli&lt;/a&gt;. Each pattern in this series is its own module with a full GoogleTest suite, so you can clone it and run the tests yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic Concept
&lt;/h2&gt;

&lt;p&gt;The ring buffer uses two indices, &lt;code&gt;head&lt;/code&gt; and &lt;code&gt;tail&lt;/code&gt;. To add data we &lt;code&gt;put&lt;/code&gt; a byte at &lt;code&gt;head&lt;/code&gt; and increment &lt;code&gt;head&lt;/code&gt; by one. To remove data we &lt;code&gt;get&lt;/code&gt; a byte from &lt;code&gt;tail&lt;/code&gt; and increment &lt;code&gt;tail&lt;/code&gt; by one. Both indices only move forward, the tail chases the head around the buffer.&lt;/p&gt;

&lt;p&gt;This pattern can be implemented with a linked list or a fixed-length array. A linked list might be tempting, but on an embedded system you don't want dynamic allocation. On a modern computer with gigabytes of memory, calling &lt;code&gt;malloc&lt;/code&gt; is a non-issue. On a microcontroller you have kilobytes, and three bigger problems: the heap fragments over a device that runs for months, allocation timing is non-deterministic, and you often need to add data from inside an interrupt where you can't afford either. A fixed array sidesteps all of it, the space is guaranteed at compile time.&lt;/p&gt;

&lt;p&gt;That guaranteed space is what buys the consumer time: bytes arriving faster than you can process them sit safely in the buffer until you get to them, instead of being lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping around the array
&lt;/h2&gt;

&lt;p&gt;An array does not loop. When you reach the end and try to go past it, it doesn't wrap around. You run beyond the allocated space, which can cause a segmentation fault, return a NULL pointer, or hand you garbage data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Intuition
&lt;/h3&gt;

&lt;p&gt;The intuitive way to loop back around is the modulo operator &lt;code&gt;%&lt;/code&gt;, which returns the remainder after division.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Array of length/capacity: 10

Formula: index % capacity -&amp;gt; array index

- index 0: 0 % 10 = 0
- index 1: 1 % 10 = 1
- index 4: 4 % 10 = 4
- index 10: 10 % 10 = 0
- index 11: 11 % 10 = 1

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modulo lets us treat the fixed array as a ring, which makes it a great fit for a ring buffer. It works fine on a modern computer with multiple cores, high clock speeds, and a hardware divide unit. But modulo has a drawback. It requires division, and many microcontrollers have no hardware divide. The compiler emulates it in software, which is slow, and we pay that cost on every byte we move through the buffer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optimizing with a bitmask
&lt;/h3&gt;

&lt;p&gt;There's a faster way to do exactly what the modulo operator does, with one restriction: the array length must be a power of two. That restriction is the key that lets us recreate modulo with a single bitwise operation. The formula is &lt;code&gt;index = head &amp;amp; mask&lt;/code&gt;, where &lt;code&gt;mask = capacity - 1&lt;/code&gt;. Let's walk through the math.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Array of length/capacity: 8 (power of two)

mask = 8 - 1 = 7 = 0b0111

- index 0: 0b0000 &amp;amp; 0b0111 = 0
- index 1: 0b0001 &amp;amp; 0b0111 = 1
- index 2: 0b0010 &amp;amp; 0b0111 = 2
- index 8: 0b1000 &amp;amp; 0b0111 = 0
- index 10: 0b1010 &amp;amp; 0b0111 = 2
- index 14: 0b1110 &amp;amp; 0b0111 = 6

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice these are the same answers modulo would give: &lt;code&gt;8 % 8 = 0&lt;/code&gt;, &lt;code&gt;10 % 8 = 2&lt;/code&gt;, &lt;code&gt;14 % 8 = 6&lt;/code&gt;. That's not a coincidence. The mask &lt;code&gt;0b0111&lt;/code&gt; keeps only the lowest three bits and clears everything above, and the lowest three bits of any number are exactly that number modulo 8. Keeping the low bits &lt;em&gt;is&lt;/em&gt; taking the remainder. Because the capacity is a power of two, &lt;code&gt;capacity - 1&lt;/code&gt; is a clean run of ones, which is what makes the AND line up perfectly with modulo. Try this with a non power of two and it falls apart: &lt;code&gt;11 &amp;amp; 9 = 9&lt;/code&gt;, but &lt;code&gt;11 % 10 = 1&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Ring buffer structure
&lt;/h3&gt;

&lt;p&gt;We create a struct to keep all the data types together. This makes life easy, because with a single pointer to the data structure we can access: &lt;code&gt;buffer&lt;/code&gt;, &lt;code&gt;capacity&lt;/code&gt;, &lt;code&gt;mask&lt;/code&gt;, &lt;code&gt;head&lt;/code&gt;, and &lt;code&gt;tail&lt;/code&gt;. It also makes the data easy to pass by pointer for any function that needs to add and read from it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.h&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;volatile&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;volatile&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see the struct uses a few different types, and each one is deliberate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;uint8_t&lt;/code&gt;: the buffer stores raw bytes. Whatever the source is, serial data from UART, I2C, or anything else, it arrives as octets, so a byte array is the natural storage for the queue.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;size_t&lt;/code&gt;: &lt;code&gt;capacity&lt;/code&gt;, &lt;code&gt;mask&lt;/code&gt;, &lt;code&gt;head&lt;/code&gt;, and &lt;code&gt;tail&lt;/code&gt; are all &lt;code&gt;size_t&lt;/code&gt;. It's an unsigned type sized to the platform (usually an alias for &lt;code&gt;unsigned int&lt;/code&gt; or &lt;code&gt;unsigned long&lt;/code&gt;), which keeps the index math portable across compilers and avoids signed-overflow issues. The unsignedness matters later, when we rely on &lt;code&gt;head - tail&lt;/code&gt; wrapping cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;volatile&lt;/code&gt;: &lt;code&gt;head&lt;/code&gt; and &lt;code&gt;tail&lt;/code&gt; are marked &lt;code&gt;volatile&lt;/code&gt; so the compiler always reads them from memory instead of caching a stale copy in a register. The producer and consumer can run in different execution contexts (for example, an ISR filling the buffer while the main loop drains it), so a value cached in a register could miss the other side's update. Note that &lt;code&gt;volatile&lt;/code&gt; only guarantees the read happens, it does not make the access atomic or thread-safe. We'll cover why this design is safe anyway in the section on interrupts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Initialization and power of two functions
&lt;/h3&gt;

&lt;p&gt;Let's start with &lt;code&gt;init&lt;/code&gt; and its helper &lt;code&gt;is_power_of_two&lt;/code&gt;. The &lt;code&gt;init&lt;/code&gt; function sets up the data structure, and the power-of-two check is the first gate: if the caller asks for a capacity that isn't a power of two, we refuse, because the whole masking scheme depends on it. We'll start with the helper.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;is_power_of_two&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0u&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1u&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This implementation does the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;x != 0u&lt;/code&gt;: checks if &lt;code&gt;x&lt;/code&gt; is not &lt;code&gt;0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(x &amp;amp; (x - 1u)) == 0u&lt;/code&gt;: this is the part that checks for a single set bit. A power of two has exactly one bit set. Subtracting one flips that bit off and turns every bit below it on, so &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;x - 1&lt;/code&gt; share no bits and the AND comes out zero. Take &lt;code&gt;x = 8&lt;/code&gt;: &lt;code&gt;8 - 1 = 7&lt;/code&gt;, and &lt;code&gt;0b1000 &amp;amp; 0b0111 = 0&lt;/code&gt;. Now take a number that isn't a power of two, &lt;code&gt;x = 12&lt;/code&gt;: &lt;code&gt;12 - 1 = 11&lt;/code&gt;, and &lt;code&gt;0b1100 &amp;amp; 0b1011 = 0b1000&lt;/code&gt;, which is not zero. The leftover bit is the tell that &lt;code&gt;x&lt;/code&gt; had more than one bit set. If both conditions are true then &lt;code&gt;x&lt;/code&gt; is a power of two, so the function returns &lt;code&gt;true&lt;/code&gt; if and only if &lt;code&gt;x&lt;/code&gt; is a power of two. Now let's turn to the &lt;code&gt;ring_buffer_init&lt;/code&gt; function.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;ring_buffer_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ring_buffer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;is_power_of_two&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; validates its inputs first: a null struct pointer, null storage, or a capacity that isn't a power of two all cause it to bail out and return &lt;code&gt;false&lt;/code&gt;. Once the checks pass, it wires up the struct, precomputes &lt;code&gt;mask = capacity - 1&lt;/code&gt; so &lt;code&gt;put&lt;/code&gt; and &lt;code&gt;get&lt;/code&gt; never have to recompute it, and zeroes both indices.&lt;/p&gt;

&lt;p&gt;Notice &lt;code&gt;init&lt;/code&gt; doesn't allocate anything. The caller hands us the &lt;code&gt;storage&lt;/code&gt; array, so the buffer can live in static or stack memory. This is the no-malloc principle from earlier, made concrete: there's no &lt;code&gt;malloc&lt;/code&gt; in sight, and the space was guaranteed the moment the caller declared the array.&lt;/p&gt;

&lt;h3&gt;
  
  
  Put and get functions
&lt;/h3&gt;

&lt;p&gt;These functions put and get a single byte at a time. We scope them to one byte because sometimes a single byte is all we need, and because the next section's &lt;code&gt;write&lt;/code&gt; and &lt;code&gt;read&lt;/code&gt; functions are built on top of them. Let's start with &lt;code&gt;ring_buffer_put&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;ring_buffer_put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ring_buffer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_is_full&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function starts by validating that &lt;code&gt;rb&lt;/code&gt; isn't &lt;code&gt;NULL&lt;/code&gt;, then calls the helper &lt;code&gt;ring_buffer_is_full&lt;/code&gt;. If the buffer is full we exit early and return &lt;code&gt;false&lt;/code&gt;, telling the caller no byte was stored. We'll see how &lt;code&gt;is_full&lt;/code&gt; works in the next section. If there's room, we do the actual write, and this is where the previous section pays off: &lt;code&gt;rb-&amp;gt;head &amp;amp; rb-&amp;gt;mask&lt;/code&gt; computes the array index using the bitwise AND, wrapping the free-running &lt;code&gt;head&lt;/code&gt; back into a valid slot. We write the byte there, then increment &lt;code&gt;head&lt;/code&gt; by one so the next &lt;code&gt;put&lt;/code&gt; lands in the following slot. Notice the order: we write the slot first, then advance &lt;code&gt;head&lt;/code&gt;. That ordering matters for interrupt safety, which we'll get to later. Now let's take a look at &lt;code&gt;ring_buffer_get&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;ring_buffer_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ring_buffer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_is_empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function also starts by validating that &lt;code&gt;rb&lt;/code&gt; isn't &lt;code&gt;NULL&lt;/code&gt;, then checks that &lt;code&gt;out&lt;/code&gt; isn't &lt;code&gt;NULL&lt;/code&gt;. This is the pointer where the byte we read gets stored, which is how &lt;code&gt;get&lt;/code&gt; hands data back to the caller while keeping its &lt;code&gt;bool&lt;/code&gt; return free to report success or failure. Finally it calls the helper &lt;code&gt;ring_buffer_is_empty&lt;/code&gt;. If the buffer is empty we exit early and return &lt;code&gt;false&lt;/code&gt;, telling the caller no byte was read. We'll see how &lt;code&gt;is_empty&lt;/code&gt; works in the next section. If it's not empty, we do the read, and again the previous section pays off: &lt;code&gt;rb-&amp;gt;tail &amp;amp; rb-&amp;gt;mask&lt;/code&gt; computes the array index with the bitwise AND, wrapping the free-running &lt;code&gt;tail&lt;/code&gt; into a valid slot. We read the byte, then increment &lt;code&gt;tail&lt;/code&gt; by one. Notice &lt;code&gt;get&lt;/code&gt; never erases anything. It reads the byte and moves &lt;code&gt;tail&lt;/code&gt; forward, and that slot counts as free again the moment &lt;code&gt;tail&lt;/code&gt; passes it. The indices alone decide what's data and what's free space.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is empty, is full, and count
&lt;/h3&gt;

&lt;p&gt;These three helpers, &lt;code&gt;is_empty&lt;/code&gt;, &lt;code&gt;is_full&lt;/code&gt;, and &lt;code&gt;count&lt;/code&gt;, are what &lt;code&gt;put&lt;/code&gt; and &lt;code&gt;get&lt;/code&gt; lean on to decide whether they can proceed. This is where the free-running indices finally earn their keep. Let's start with &lt;code&gt;ring_buffer_is_empty&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;ring_buffer_is_empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The buffer is empty when &lt;code&gt;head&lt;/code&gt; and &lt;code&gt;tail&lt;/code&gt; hold the same value. That makes sense: &lt;code&gt;tail&lt;/code&gt; chases &lt;code&gt;head&lt;/code&gt;, and when it catches all the way up there's nothing left to read. Here's the subtle part. In a lot of ring buffer designs the indices get wrapped back into range, and then &lt;code&gt;head == tail&lt;/code&gt; is ambiguous, it means &lt;em&gt;both&lt;/em&gt; completely empty and completely full, and you can't tell which. Because our indices run free and never wrap, &lt;code&gt;head == tail&lt;/code&gt; can only ever mean empty. We'll see in a moment how full is handled without that ambiguity.&lt;/p&gt;

&lt;p&gt;Next, &lt;code&gt;ring_buffer_count&lt;/code&gt;, which is the heart of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="nf"&gt;ring_buffer_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both indices only ever increase. Every &lt;code&gt;put&lt;/code&gt; bumps &lt;code&gt;head&lt;/code&gt;, every &lt;code&gt;get&lt;/code&gt; bumps &lt;code&gt;tail&lt;/code&gt;, and neither wraps. So &lt;code&gt;head&lt;/code&gt; is always ahead of &lt;code&gt;tail&lt;/code&gt; by exactly the number of bytes that have been written but not yet read, which is the fill level. The difference &lt;code&gt;head - tail&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; the count.&lt;/p&gt;

&lt;p&gt;The nice part is what happens when &lt;code&gt;head&lt;/code&gt; eventually overflows. &lt;code&gt;size_t&lt;/code&gt; has a maximum, and after enough bytes &lt;code&gt;head&lt;/code&gt; wraps past it back toward zero. You might expect &lt;code&gt;head - tail&lt;/code&gt; to break at that point, but it doesn't. Unsigned subtraction wraps the same way unsigned addition does, so the difference stays correct even across the overflow. This is exactly why &lt;code&gt;head&lt;/code&gt; and &lt;code&gt;tail&lt;/code&gt; are unsigned, and it's why the buffer can run indefinitely without any special handling for the wraparound.&lt;/p&gt;

&lt;p&gt;Finally, &lt;code&gt;ring_buffer_is_full&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;ring_buffer_is_full&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once we have &lt;code&gt;count&lt;/code&gt;, full is trivial: the buffer is full when it holds &lt;code&gt;capacity&lt;/code&gt; bytes. No wasted slot, no separate counter to keep in sync, no ambiguity with the empty case. The free-running indices give us empty (&lt;code&gt;head == tail&lt;/code&gt;), full (&lt;code&gt;count == capacity&lt;/code&gt;), and everything in between, all from two numbers that only count upward.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write and read functions
&lt;/h3&gt;

&lt;p&gt;These functions write and read more than a single byte of data at a time. Let's start with &lt;code&gt;ring_buffer_write&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="nf"&gt;ring_buffer_write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ring_buffer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function returns the number of bytes actually written, which may be fewer than requested. It returns &lt;code&gt;0&lt;/code&gt; on the early exit when either the ring buffer &lt;code&gt;rb&lt;/code&gt; or the source pointer &lt;code&gt;data&lt;/code&gt; is &lt;code&gt;NULL&lt;/code&gt;. Otherwise we initialize &lt;code&gt;written&lt;/code&gt; to track how many bytes we've copied from &lt;code&gt;data&lt;/code&gt; into the buffer. The loop runs on two conditions joined by &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;: &lt;code&gt;written &amp;lt; len&lt;/code&gt; keeps us from reading past the end of the input, and &lt;code&gt;ring_buffer_put&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt; the moment the buffer is full. As long as both hold, we keep copying bytes and incrementing &lt;code&gt;written&lt;/code&gt;. The loop stops as soon as either one fails, whether we ran out of input or ran out of room. The count we return tells the caller exactly how much made it in: all of it, part of it, or zero if the buffer was already full.&lt;/p&gt;

&lt;p&gt;Next, &lt;code&gt;ring_buffer_read&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// modules/ring_buffer/ring_buffer.c&lt;/span&gt;
&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="nf"&gt;ring_buffer_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ring_buffer_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;ring_buffer_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function also returns the number of bytes actually read, which may be fewer than requested. It returns &lt;code&gt;0&lt;/code&gt; on the early exit when either the ring buffer &lt;code&gt;rb&lt;/code&gt; or the destination pointer &lt;code&gt;out&lt;/code&gt; is &lt;code&gt;NULL&lt;/code&gt;. Otherwise we initialize &lt;code&gt;count&lt;/code&gt; to track how many bytes we've read from the buffer into &lt;code&gt;out&lt;/code&gt;. The loop runs on two conditions joined by &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;: &lt;code&gt;count &amp;lt; len&lt;/code&gt; keeps us from writing past the end of the output, and &lt;code&gt;ring_buffer_get&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt; the moment the buffer is empty. As long as both hold, we keep reading bytes and incrementing &lt;code&gt;count&lt;/code&gt;. The loop stops as soon as either one fails, whether the caller's buffer is full or the ring buffer ran dry. The count we return tells the caller exactly how many bytes they got.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safety with the Interrupt Service Routine (ISR)
&lt;/h2&gt;

&lt;p&gt;For those new to embedded, it might not be obvious why this design is considered safe, or what its typical applications are. The primary use for a ring buffer, as we mentioned earlier, is serial communication. Take the example of two microcontrollers talking to each other, each running its own firmware tailored to its own task. We don't know when either one will need to send data, so to make sure we don't miss a single byte, we set up the data receive (RX) path using the microcontroller's Interrupt Service Routine (ISR). As soon as the start of a transmission arrives, the ISR takes over and captures each byte.&lt;/p&gt;

&lt;p&gt;This is the producer side of the buffer. The ISR calls &lt;code&gt;put&lt;/code&gt; for every byte it receives. Somewhere else, the main loop is the consumer, calling &lt;code&gt;get&lt;/code&gt; when it gets around to processing what came in. The two run in different execution contexts, and here's the catch: the ISR can fire at any moment, between any two instructions the main loop is running. So the fair question is, if the interrupt lands in the middle of a &lt;code&gt;get&lt;/code&gt;, can the two step on each other and corrupt the buffer?&lt;/p&gt;

&lt;p&gt;The answer is no, and the reason is that each index has exactly one writer. &lt;code&gt;put&lt;/code&gt; is the only function that writes &lt;code&gt;head&lt;/code&gt;, and it only ever runs in the ISR. &lt;code&gt;get&lt;/code&gt; is the only function that writes &lt;code&gt;tail&lt;/code&gt;, and it only ever runs in the main loop. Neither side writes the other's index. The producer reads &lt;code&gt;tail&lt;/code&gt; to check whether the buffer is full, and the consumer reads &lt;code&gt;head&lt;/code&gt; to check whether it's empty, but reading is safe. Since no index has two writers racing to change it, there's nothing to corrupt, and we don't need to disable interrupts or reach for a lock. This is what people mean by single-producer, single-consumer, or SPSC.&lt;/p&gt;

&lt;p&gt;That single-writer rule is also the boundary. The moment you have two things calling &lt;code&gt;put&lt;/code&gt;, or two things calling &lt;code&gt;get&lt;/code&gt;, the guarantee is gone and you need real synchronization. This buffer is safe for exactly one producer and one consumer, no more.&lt;/p&gt;

&lt;p&gt;One more detail makes it work, and it's the ordering we flagged back in &lt;code&gt;put&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1. write the data&lt;/span&gt;
&lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 2. then publish it&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We write the byte into the slot first, then increment &lt;code&gt;head&lt;/code&gt;. That order is not an accident. &lt;code&gt;head&lt;/code&gt; is the signal that tells the consumer a new byte is available. If we bumped &lt;code&gt;head&lt;/code&gt; first and the interrupt timing worked out badly, the consumer could see the new &lt;code&gt;head&lt;/code&gt;, go read that slot, and pull out a byte we hadn't actually written yet. By writing the slot before advancing &lt;code&gt;head&lt;/code&gt;, we guarantee that the instant the consumer can see the byte, the byte is really there. The index update publishes the data, so it comes last. &lt;code&gt;get&lt;/code&gt; mirrors this: we read the byte out first, then advance &lt;code&gt;tail&lt;/code&gt;, so we never mark a slot free before we've taken its contents.&lt;/p&gt;

&lt;p&gt;A couple of honest caveats for the readers who want them. First, &lt;code&gt;volatile&lt;/code&gt; gets us visibility, not atomicity. This design assumes reading or writing &lt;code&gt;head&lt;/code&gt; and &lt;code&gt;tail&lt;/code&gt; happens in a single, uninterruptible machine operation, which is true when they're word-sized on the target. On an 8-bit MCU where &lt;code&gt;size_t&lt;/code&gt; spans multiple bytes, an index update could be split across instructions and torn by an interrupt, and you'd need to account for that. Second, on a single-core microcontroller with an ISR, &lt;code&gt;volatile&lt;/code&gt; plus the write-then-publish ordering is enough. On a multi-core system with weak memory ordering you'd reach for atomics with explicit acquire and release semantics to guarantee the slot write is seen before the index write. For the classic ISR-and-main-loop case this buffer is built for, we're on solid ground.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;That's a complete, working ring buffer in a couple hundred lines of C. It does no dynamic allocation, wraps with a single bitwise AND instead of a division, tells full from empty without wasting a slot or keeping a separate counter, and it's safe to fill from an interrupt while draining it from the main loop. For a structure this small, there's a surprising amount of nuance packed in.&lt;/p&gt;

&lt;p&gt;It's worth being honest about what this design does not do, because every one of these is a deliberate trade, not an oversight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capacity must be a power of two.&lt;/strong&gt; This is the price of replacing modulo with a mask. If you need to hold exactly 100 bytes you round up to 128 and waste a little space. On an embedded system that's almost always a trade worth making.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single producer, single consumer only.&lt;/strong&gt; One writer per index is what makes it lock-free. Two ISRs writing, or two contexts reading, and you need real synchronization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No overwrite-oldest mode.&lt;/strong&gt; When the buffer is full, &lt;code&gt;put&lt;/code&gt; refuses the new byte rather than discarding the oldest one. Some applications want the opposite (keep the newest samples, drop the stale ones). That's a reasonable variant, just not this one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bytes only.&lt;/strong&gt; This buffer stores &lt;code&gt;uint8_t&lt;/code&gt;. Storing wider elements, or arbitrary structs, means generalizing the element size, which changes the index math and the storage. None of these are hard to lift. They're the natural next questions, and a few of them turn into their own patterns later in this series.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Speaking of which, the ring buffer is a foundation, not a destination. It shows up underneath several of the patterns still to come: an event queue is a ring buffer of messages instead of bytes, a moving-average filter is a ring buffer of samples, and the command dispatcher we'll build reads its input straight out of one. Next in the series we'll tackle the memory pool. The ring buffer gave us fixed storage for bytes; the memory pool gives us fixed storage for objects, and it's what lets an event queue hold structs instead of raw data. See you there.&lt;/p&gt;

</description>
      <category>embeddedsystems</category>
      <category>datastructures</category>
      <category>c</category>
      <category>microcontrollers</category>
    </item>
    <item>
      <title>Compendium: Reproducible Developer Environments in One Config</title>
      <dc:creator>Ilean Monterrubio Jr</dc:creator>
      <pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/elcapitan/compendium-reproducible-developer-environments-in-one-config-3aon</link>
      <guid>https://dev.to/elcapitan/compendium-reproducible-developer-environments-in-one-config-3aon</guid>
      <description>&lt;h2&gt;
  
  
  Why I Built It
&lt;/h2&gt;

&lt;p&gt;Earlier this year I kept coming back to a project that had been on the back burner for a while. The original idea was an IDE with a built-in way to configure development environments, but the more I researched, the clearer it got that I was looking at a multi-year build. Not wanting to wait that long to ship something useful, I narrowed in on the feature I cared about most: the ability to configure and set up a development environment easily, without the IDE wrapped around it.&lt;/p&gt;

&lt;p&gt;Throughout my career, setting up toolchains and development environments has always been a painful experience: embedded cross-compilers that only build cleanly on the maintainer's exact OS version, READMEs that promise five minutes and take three days. I've also spent weeks building containers to wall off the development environment, only to watch the image balloon every time another tool got added. The documentation goes stale almost as fast as it gets written, because the tools moved on and nobody updated it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why C and C++ First
&lt;/h2&gt;

&lt;p&gt;For Compendium's first iteration I wanted to focus on the language where the setup pain runs deepest: C and C++. Every scripting language already has a version manager that works well enough. nvm handles Node, pyenv handles Python, asdf and mise cover most of the rest. None of them seriously handle GCC, clang, or arm-none-eabi-gcc. Embedded toolchains in particular are where I lose the most time, between USB drivers, OpenOCD configs, and compiler versions that have to match the linker script the project was tested against. Starting there meant solving the hardest case first. Adding Go, Python, and Node on top of that architecture turned out to be the easy part.&lt;/p&gt;

&lt;h2&gt;
  
  
  v0.1.1 Ships Today
&lt;/h2&gt;

&lt;p&gt;v0.1.1 ships today. The CLI is a single Go binary, the config is a single TOML file, and everything installs under &lt;code&gt;~/.local/compendium/&lt;/code&gt; without sudo. Rather than recap the docs here, I'll point you at them: &lt;a href="https://compendium.ilean.me/" rel="noopener noreferrer"&gt;compendium.ilean.me&lt;/a&gt; has the quick start, the full tool list, the config reference, and per-language guides for C/C++, Go, Python, Node, and embedded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CLI&lt;/strong&gt; : Go (single static binary, no runtime dependencies)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration&lt;/strong&gt; : TOML&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registry&lt;/strong&gt; : JSON manifests, hosted on GitHub&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platforms&lt;/strong&gt; : macOS (Apple Silicon, Intel), Linux (x86_64, arm64)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt; : GPL v3 (CLI), CC0 (registry)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Roadmap
&lt;/h2&gt;

&lt;p&gt;v0.2 broadens C/C++ build system support to Autotools, adds Java and Rust, completes the embedded cross-compiler set with Xtensa for ESP32, and wires up the custom git hook runner. v0.3 introduces a Compendium-native C/C++ package manager. The full roadmap lives in the docs. If you want to follow along, contribute, or request a tool for the registry, GitHub issues and discussions are open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://compendium.ilean.me/" rel="noopener noreferrer"&gt;Compendium Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ileanmjr88/compendium" rel="noopener noreferrer"&gt;Compendium Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ileanmjr88/compendium-registry" rel="noopener noreferrer"&gt;Compendium Registry&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>compendium</category>
      <category>devtools</category>
      <category>go</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Compendium: entornos de desarrollo reproducibles en una sola configuración</title>
      <dc:creator>Ilean Monterrubio Jr</dc:creator>
      <pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/elcapitan/compendium-entornos-de-desarrollo-reproducibles-en-una-sola-configuracion-47i2</link>
      <guid>https://dev.to/elcapitan/compendium-entornos-de-desarrollo-reproducibles-en-una-sola-configuracion-47i2</guid>
      <description>&lt;h2&gt;
  
  
  Por qué lo construí
&lt;/h2&gt;

&lt;p&gt;A principios de este año seguía volviendo a un proyecto que llevaba un buen rato en pausa. La idea original era un IDE con una forma integrada de configurar entornos de desarrollo, pero entre más investigaba, más claro me quedaba que estaba frente a un desarrollo de varios años. Como no quería esperar tanto para lanzar algo útil, me enfoqué en la funcionalidad que más me importaba: poder configurar y preparar un entorno de desarrollo con facilidad, sin el IDE alrededor.&lt;/p&gt;

&lt;p&gt;A lo largo de mi carrera, configurar toolchains y entornos de desarrollo siempre ha sido una experiencia dolorosa: compiladores cruzados de embebidos que solo compilan limpiamente en la versión exacta del sistema operativo del mantenedor, READMEs que prometen cinco minutos y toman tres días. También he pasado semanas construyendo contenedores para aislar el entorno de desarrollo, solo para ver cómo la imagen se inflaba cada vez que se agregaba otra herramienta. La documentación queda obsoleta casi tan rápido como se escribe, porque las herramientas avanzaron y nadie la actualizó.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué primero C y C++
&lt;/h2&gt;

&lt;p&gt;Para la primera iteración de Compendium quise enfocarme en el lenguaje donde el dolor de la configuración es más profundo: C y C++. Todos los lenguajes de scripting ya tienen un gestor de versiones que funciona lo suficientemente bien. nvm maneja Node, pyenv maneja Python, asdf y mise cubren casi todo lo demás. Ninguno de ellos maneja en serio GCC, clang o arm-none-eabi-gcc. Los toolchains de embebidos en particular son donde más tiempo pierdo, entre drivers USB, configuraciones de OpenOCD y versiones de compilador que tienen que coincidir con el linker script contra el que se probó el proyecto. Empezar ahí significaba resolver primero el caso más difícil. Agregar Go, Python y Node sobre esa arquitectura resultó ser la parte fácil.&lt;/p&gt;

&lt;h2&gt;
  
  
  La v0.1.1 se lanza hoy
&lt;/h2&gt;

&lt;p&gt;La v0.1.1 se lanza hoy. La CLI es un único binario de Go, la configuración es un único archivo TOML y todo se instala en &lt;code&gt;~/.local/compendium/&lt;/code&gt; sin sudo. En lugar de repetir la documentación aquí, te dirijo a ella: &lt;a href="https://compendium.ilean.me/" rel="noopener noreferrer"&gt;compendium.ilean.me&lt;/a&gt; tiene la guía de inicio rápido, la lista completa de herramientas, la referencia de configuración y guías por lenguaje para C/C++, Go, Python, Node y embebidos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack tecnológico
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CLI&lt;/strong&gt; : Go (un único binario estático, sin dependencias en tiempo de ejecución)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuración&lt;/strong&gt; : TOML&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registro&lt;/strong&gt; : manifiestos JSON, alojados en GitHub&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plataformas&lt;/strong&gt; : macOS (Apple Silicon, Intel), Linux (x86_64, arm64)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Licencia&lt;/strong&gt; : GPL v3 (CLI), CC0 (registro)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Hoja de ruta
&lt;/h2&gt;

&lt;p&gt;La v0.2 amplía el soporte de sistemas de build de C/C++ a Autotools, agrega Java y Rust, completa el conjunto de compiladores cruzados para embebidos con Xtensa para el ESP32 y conecta el ejecutor de git hooks personalizados. La v0.3 introduce un gestor de paquetes de C/C++ nativo de Compendium. La hoja de ruta completa vive en la documentación. Si quieres seguir el avance, contribuir o solicitar una herramienta para el registro, los issues y las discusiones de GitHub están abiertos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enlaces del proyecto
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://compendium.ilean.me/" rel="noopener noreferrer"&gt;Documentación de Compendium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ileanmjr88/compendium" rel="noopener noreferrer"&gt;Repositorio de Compendium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ileanmjr88/compendium-registry" rel="noopener noreferrer"&gt;Registro de Compendium&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>spanish</category>
      <category>devtools</category>
      <category>go</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I Built a Low-Friction Blog Pipeline</title>
      <dc:creator>Ilean Monterrubio Jr</dc:creator>
      <pubDate>Mon, 27 Apr 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/elcapitan/how-i-built-a-low-friction-blog-pipeline-1lof</link>
      <guid>https://dev.to/elcapitan/how-i-built-a-low-friction-blog-pipeline-1lof</guid>
      <description>&lt;h2&gt;
  
  
  Why Build a Pipeline?
&lt;/h2&gt;

&lt;p&gt;I wanted to grow my online presence, a dedicated corner of the internet to showcase my projects and skills, point people to my GitHub, LinkedIn, and socials, and share my thoughts on things I was building or learning. Simple enough goal. The execution, though, turned out to be a learning process.&lt;/p&gt;

&lt;p&gt;My first attempt at blogging was manual and messy. Write a post, publish it somewhere, copy it somewhere else, and hope people find it. I had set up the Dev.to RSS feed fetching early on, but that was about it. There was no real visibility into whether anyone was reading, and no reason to keep going. Unsurprisingly, I took a year-long gap between posts, not because I stopped having things to say, but because the friction was too high and life got in the way. When I started a new job, I focused on learning my new responsibilities and let the blog quietly go dormant.&lt;/p&gt;

&lt;p&gt;When I came back to it, I wanted to do things differently. The goal was simple: write once, publish everywhere, no copy-pasting across platforms, no manual distribution, and some way to know if the work was actually reaching anyone. The RSS feed was already there but the imports were messy. The frontmatter in my blog posts didn't match the RSS schema, so every import came in inconsistent and making changes or adding new frontmatter fields meant manually fixing every post. What I did this time was get my Astro frontmatter to match the RSS schema properly, so imports were clean and uniform from the start. Now it was about connecting the remaining pieces into a proper pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Astro&lt;/strong&gt; Before launching my personal site, I had tried to build it with Next.js a year earlier. I didn't fully understand it at the time and kept running into hydration issues. At a local Code and Coffee meetup, I asked one of the attendees for advice and he pointed me to Astro. Within days of going through a tutorial, I had my site live for the first time. I paired it with Tailwind CSS to make theming easy to maintain and update down the road.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pexels&lt;/strong&gt; Every post needs a cover image. Pexels is my go-to for finding images, royalty-free stock photos and videos shared by creators. High quality, free to use, and no licensing headaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dev.to&lt;/strong&gt; This came much later in the process. After some digging I discovered that Astro can generate an &lt;code&gt;rss.xml&lt;/code&gt; feed if set up correctly, and Dev.to can periodically pull from that feed to automatically import new posts. All I need to do is keep my frontmatter in sync with what Dev.to expects from the feed, no manual copying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Umami&lt;/strong&gt; I wanted visibility into who was actually reading. After some research I landed on Umami: privacy-friendly, no cookie banners, and a free hobby tier that covers up to 3 websites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mastodon&lt;/strong&gt; After Twitter was acquired I decided to leave and delete my profile. Mastodon turned out to be a great fit, especially for the tech community, and the fediverse has a different energy that I appreciate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt; Where I manually share each post to take advantage of how LinkedIn's algorithm circulates content to your network and extended network for a few days after publishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Astro
&lt;/h2&gt;

&lt;p&gt;Each blog post is written in Markdown format, which Astro handles exceptionally well out of the box. One of the keys to making everything work smoothly is Astro's content collections configuration, this is how you tell Astro the shape of your content so it can validate and query it consistently. For this project I have Markdown files for blog entries and JSON files to define my projects. The &lt;a href="https://docs.astro.build/en/guides/content-collections/" rel="noopener noreferrer"&gt;Astro content collections documentation&lt;/a&gt; is worth reading carefully to understand how to define your schema.&lt;/p&gt;

&lt;p&gt;Before looking at the frontmatter, it helps to see the schema that enforces it. This lives in &lt;code&gt;src/content.config.ts&lt;/code&gt; and uses Astro's content collections with Zod validation. If a frontmatter field is missing or the wrong type, Astro will throw an error at build time rather than silently generating a broken RSS feed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineCollection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro:content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;glob&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro/loaders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineCollection&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/*.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/content/blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;pubDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;collections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;blog&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each blog post starts with a frontmatter block that drives everything downstream, from the RSS feed to Dev.to imports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Building&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Terminal&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Text&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Editor:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;The&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;View&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(Part&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3)"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;In&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Part&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;building&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;wordNebula,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cover&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;View&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;layer,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;why&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;chose&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;FTXUI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;over&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ncurses..."&lt;/span&gt;
&lt;span class="na"&gt;pubDate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-04-05&lt;/span&gt;
&lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ilean&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Monterrubio&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Jr"&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/src/content/images/pexels-markus-winkler-1430818-4065400.jpg'&lt;/span&gt;
&lt;span class="na"&gt;categories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cpp'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;terminal'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;architecture'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;programming'&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping this frontmatter consistent and aligned with the RSS schema is what makes the rest of the pipeline work without any manual cleanup. This also makes the &lt;code&gt;rss.xml.ts&lt;/code&gt; configuration straightforward to set up. All it needs to do is read the frontmatter content and it will generate the &lt;code&gt;rss.xml&lt;/code&gt; file at build time.&lt;/p&gt;

&lt;p&gt;Here is what a basic &lt;code&gt;rss.xml.ts&lt;/code&gt; file looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;rss&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@astrojs/rss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getCollection&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro:content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getCollection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;rss&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your Blog Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your blog description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;site&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;pubDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pubDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fields in the &lt;code&gt;items&lt;/code&gt; map directly to the frontmatter fields you define. As long as the frontmatter is consistent, the RSS output will be clean and Dev.to will be able to import it without any issues. For additional information visit the &lt;a href="https://docs.astro.build/en/recipes/rss/" rel="noopener noreferrer"&gt;Astro RSS documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto-Importing to Dev.to via RSS
&lt;/h2&gt;

&lt;p&gt;For this part you will need a Dev.to account. Once your website is live and the RSS feed URL is active, navigate to your Dev.to dashboard. On the left-hand sidebar you will find &lt;strong&gt;RSS Import Feeds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fca2t9mhnrayjf5rd9muk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fca2t9mhnrayjf5rd9muk.webp" alt="Dev.to RSS feed import setup screen" width="800" height="643"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From there, click &lt;strong&gt;+ Add a Feed Source&lt;/strong&gt; to expand the form. Enter your &lt;code&gt;rss.xml&lt;/code&gt; URL in the RSS Feed URL field. A couple of settings worth paying attention to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mark the RSS source as canonical URL by default:&lt;/strong&gt; keep this checked. This is one of the most important settings in the whole pipeline. It tells Dev.to that your personal site is the original source of the content, which means search engines will credit your site and not the Dev.to version. Get this wrong and Dev.to can end up outranking your own site for your own writing. Always leave this on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace self-referential links with DEV Community-specific links:&lt;/strong&gt; leave this unchecked unless you are migrating your entire blog to Dev.to permanently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you click &lt;strong&gt;Add Feed Source&lt;/strong&gt; , Dev.to will periodically check your feed and pull in any new posts. Imported posts land as &lt;strong&gt;drafts&lt;/strong&gt; , so before publishing you will want to review each one and add a &lt;strong&gt;series name&lt;/strong&gt; if it is part of a multi-part post, pick &lt;strong&gt;tags&lt;/strong&gt; that match popular Dev.to tags for discoverability (for example: &lt;code&gt;cpp&lt;/code&gt;, &lt;code&gt;terminal&lt;/code&gt;, &lt;code&gt;architecture&lt;/code&gt;, &lt;code&gt;programming&lt;/code&gt;), and add a &lt;strong&gt;cover image&lt;/strong&gt; sourced from Pexels. When you are ready to go live, open the post editor and flip &lt;code&gt;published&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; in the Dev.to frontmatter at the top of the post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mastodon Verification and Author Attribution
&lt;/h2&gt;

&lt;p&gt;I wanted to establish a presence on Mastodon, and it turns out Astro makes it easy to connect your blog to the fediverse with just two small additions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verification with rel="me"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mastodon uses the &lt;code&gt;rel="me"&lt;/code&gt; link standard to verify that you own a website. Adding it to your Astro layout head tag gives you the green checkmark on your Mastodon profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"me"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://mastodon.social/@yourusername"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Author Attribution with fediverse:creator&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;fediverse:creator&lt;/code&gt; meta tag is a newer addition that connects your blog posts to your Mastodon identity. When someone shares one of your posts on Mastodon, it automatically credits your account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"fediverse:creator"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"@yourusername@mastodon.social"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this to your blog post layout so it only appears on post pages rather than every page on the site. Together these two tags make your blog a proper fediverse citizen, verified, attributable, and discoverable by the tech community that has made Mastodon their home.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Privacy-Friendly Analytics with Umami
&lt;/h2&gt;

&lt;p&gt;When I started getting posts out I wanted to know if anyone was actually reading them. Google Analytics was the obvious choice but it felt like overkill, heavy, cookie-dependent, and requiring a consent banner just to get started. After some digging I found Umami, a lightweight privacy-friendly alternative that ticked every box. The free hobby tier covers up to 3 websites, 100K events per month, and 6 months of data retention. No cookies, no consent banners, no tracking across sites.&lt;/p&gt;

&lt;p&gt;Setup is straightforward. Once you create your Umami account and add your site, you get a script tag to drop into your Astro layout file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script
  &lt;/span&gt;&lt;span class="na"&gt;defer&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cloud.umami.is/script.js"&lt;/span&gt;
  &lt;span class="na"&gt;data-website-id=&lt;/span&gt;&lt;span class="s"&gt;"your-website-id"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this to your main &lt;code&gt;Layout.astro&lt;/code&gt; file and it will be included on every page automatically. I also added a small notice in the footer to let visitors know the site uses privacy-friendly analytics with no cookies or personal data collected, a small but honest touch that sets the right expectations.&lt;/p&gt;

&lt;p&gt;The Umami dashboard covers all the basics: visitors, page views, referrers, locations, devices, and bounce rate. It is everything you need to understand how your content is performing without compromising your readers' privacy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Publishing Workflow
&lt;/h2&gt;

&lt;p&gt;I typically write about something I worked on professionally, a personal project, or a topic I consider myself an expert in. Here is the end-to-end process I follow from idea to published post:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Write the draft in Notion&lt;/strong&gt; I started out using Google Docs but since returning to the blog I migrated to Notion. It handles code blocks and tables properly and exports to Markdown in a way that actually translates correctly, which makes the next step much smoother.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Generate the Astro-ready Markdown file with frontmatter&lt;/strong&gt; Once the draft is ready I convert it into a Markdown file with the correct frontmatter fields — title, description, pubDate, author, image, and categories — making sure everything aligns with the RSS schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Deploy to ilean.me on Sunday morning&lt;/strong&gt; Sunday is my publishing day. I push the new post and deploy the site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Dev.to picks up via RSS&lt;/strong&gt; Dev.to periodically polls the RSS feed URL, so the post will appear as a draft in my Dev.to dashboard within a few hours depending on when it last checked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Post on Mastodon the same day&lt;/strong&gt; I share the post on Mastodon with hashtags, typically the same ones used in the post frontmatter for consistency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Schedule LinkedIn for Monday morning&lt;/strong&gt; LinkedIn has a post scheduling feature which has been a game changer. I schedule it to go live Monday morning at 8-9 AM, when recruiters and professionals are most active, letting LinkedIn's algorithm circulate it to my network and extended network over the following days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Review and publish the Dev.to draft&lt;/strong&gt; Once it lands in Dev.to I review it, add a series name if it is part of a multi-part post, set the tags, add a cover image from Pexels, and flip &lt;code&gt;published&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results So Far
&lt;/h2&gt;

&lt;p&gt;I revived the blog in February and March of 2026. On Dev.to, in February the most views I received in a single day was 16. Since then that number has climbed to 36 views in a single day. As of April 25th 2026 Dev.to shows 435 total views, though down 24% compared to the previous 7 days, a good reminder that consistency matters.&lt;/p&gt;

&lt;p&gt;On ilean.me, Umami shows 48 unique visitors so far. What surprised me was the geographic spread. A large portion of visitors are coming from Asia, which makes sense given that embedded systems and C++ are a common stack there. The majority are from the US, with California showing up frequently, which tracks given the tech industry concentration there.&lt;/p&gt;

&lt;p&gt;The numbers are small but the trend is encouraging. Search engine traffic compounds over time; every post indexed is another entry point for someone to find your work. The goal was never overnight virality, it was to slowly and consistently build an online presence, and the data shows that is exactly what is happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;A few honest lessons from getting this pipeline up and running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Double check your publication dates.&lt;/strong&gt; One post did not show up on Dev.to on time and after some digging I realized the date in the frontmatter was off by a day. Dev.to pulls from the RSS feed based on the pubDate field, so if that is wrong the post gets skipped or delayed. Always verify the date before deploying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be patient with the RSS fetch timing.&lt;/strong&gt; Dev.to does not poll your feed instantly. Depending on when it last checked, it could take several hours for a new post to appear as a draft. Do not panic and assume something is broken; just give it time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filter out your own visits.&lt;/strong&gt; Early on I got excited seeing a visitor from Houston in Umami before realizing it was just me clicking my own links. The free Umami tier does not have a built-in way to exclude your own IP, so just keep that in mind when reading your early numbers and take them with a grain of salt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find your community.&lt;/strong&gt; One thing I want to do going forward is find Houston based tech Discord servers with software engineers and developers to share my content in. Posting in relevant communities can accelerate growth in a way that passive RSS and social posts alone cannot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not check analytics too often.&lt;/strong&gt; It is tempting at the start but the numbers move slowly and checking constantly just creates unnecessary anxiety. Set a cadence, maybe once a week, and focus on writing the next post instead.&lt;/p&gt;

&lt;p&gt;The whole point of building this pipeline was to remove friction, and it worked. There is no year-long gap waiting to happen again; there is just the next post.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>blogging</category>
      <category>devto</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Cómo construí un pipeline de blog sin fricciones</title>
      <dc:creator>Ilean Monterrubio Jr</dc:creator>
      <pubDate>Mon, 27 Apr 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/elcapitan/como-construi-un-pipeline-de-blog-sin-fricciones-1elk</link>
      <guid>https://dev.to/elcapitan/como-construi-un-pipeline-de-blog-sin-fricciones-1elk</guid>
      <description>&lt;h2&gt;
  
  
  ¿Por qué construir un pipeline?
&lt;/h2&gt;

&lt;p&gt;Quería hacer crecer mi presencia en línea: un rincón propio de internet para mostrar mis proyectos y habilidades, dirigir a la gente a mi GitHub, LinkedIn y redes sociales, y compartir mis ideas sobre lo que estaba construyendo o aprendiendo. Un objetivo bastante sencillo. La ejecución, sin embargo, resultó ser todo un proceso de aprendizaje.&lt;/p&gt;

&lt;p&gt;Mi primer intento de bloguear fue manual y desordenado. Escribir una entrada, publicarla en algún lado, copiarla a otro y esperar que la gente la encontrara. Había configurado la obtención del feed RSS de Dev.to desde el principio, pero hasta ahí llegaba. No había una visibilidad real de si alguien estaba leyendo, ni una razón para seguir. Como era de esperarse, dejé pasar un año entero entre entradas, no porque dejara de tener cosas que decir, sino porque la fricción era demasiado alta y la vida se atravesó. Cuando empecé un nuevo trabajo, me concentré en aprender mis nuevas responsabilidades y dejé que el blog quedara en silencio.&lt;/p&gt;

&lt;p&gt;Cuando volví a retomarlo, quería hacer las cosas de otra manera. El objetivo era simple: escribir una vez, publicar en todas partes, sin copiar y pegar entre plataformas, sin distribución manual, y con alguna forma de saber si el trabajo realmente llegaba a alguien. El feed RSS ya estaba ahí, pero las importaciones eran un desorden. El frontmatter de mis entradas no coincidía con el esquema del RSS, así que cada importación llegaba inconsistente, y hacer cambios o agregar nuevos campos al frontmatter significaba corregir cada entrada a mano. Esta vez lo que hice fue lograr que el frontmatter de Astro coincidiera correctamente con el esquema del RSS, para que las importaciones fueran limpias y uniformes desde el inicio. Ahora se trataba de conectar las piezas restantes en un pipeline en forma.&lt;/p&gt;

&lt;h2&gt;
  
  
  El stack
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Astro&lt;/strong&gt; Antes de lanzar mi sitio personal, había intentado construirlo con Next.js un año antes. En ese momento no lo entendía del todo y me topaba constantemente con problemas de hidratación. En un meetup local de Code and Coffee le pedí consejo a uno de los asistentes y me recomendó Astro. A los pocos días de seguir un tutorial, tenía mi sitio en vivo por primera vez. Lo combiné con Tailwind CSS para que el manejo de temas fuera fácil de mantener y actualizar más adelante.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pexels&lt;/strong&gt; Cada entrada necesita una imagen de portada. Pexels es mi opción de cabecera para encontrar imágenes: fotos y videos de stock libres de regalías compartidos por creadores. De alta calidad, gratis y sin dolores de cabeza por licencias.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dev.to&lt;/strong&gt; Esto llegó mucho después en el proceso. Tras investigar un poco, descubrí que Astro puede generar un feed &lt;code&gt;rss.xml&lt;/code&gt; si se configura correctamente, y que Dev.to puede consultar ese feed de forma periódica para importar automáticamente las nuevas entradas. Lo único que tengo que hacer es mantener mi frontmatter en sincronía con lo que Dev.to espera del feed, sin copiar nada a mano.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Umami&lt;/strong&gt; Quería tener visibilidad de quién estaba leyendo realmente. Después de investigar, me decidí por Umami: respetuoso con la privacidad, sin banners de cookies y con un plan gratuito para aficionados que cubre hasta 3 sitios web.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mastodon&lt;/strong&gt; Después de que adquirieran Twitter, decidí irme y eliminar mi perfil. Mastodon resultó encajar muy bien, sobre todo para la comunidad tech, y el fediverso tiene una energía distinta que aprecio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt; Donde comparto manualmente cada entrada para aprovechar cómo el algoritmo de LinkedIn hace circular el contenido por tu red y tu red extendida durante unos días después de publicar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configurando Astro
&lt;/h2&gt;

&lt;p&gt;Cada entrada del blog está escrita en formato Markdown, que Astro maneja excepcionalmente bien desde el inicio. Una de las claves para que todo funcione sin problemas es la configuración de las content collections de Astro; así es como le dices a Astro la forma de tu contenido para que pueda validarlo y consultarlo de manera consistente. Para este proyecto tengo archivos Markdown para las entradas del blog y archivos JSON para definir mis proyectos. Vale la pena leer con atención la &lt;a href="https://docs.astro.build/en/guides/content-collections/" rel="noopener noreferrer"&gt;documentación de content collections de Astro&lt;/a&gt; para entender cómo definir tu esquema.&lt;/p&gt;

&lt;p&gt;Antes de ver el frontmatter, ayuda conocer el esquema que lo hace cumplir. Vive en &lt;code&gt;src/content.config.ts&lt;/code&gt; y usa las content collections de Astro con validación de Zod. Si falta un campo del frontmatter o es del tipo incorrecto, Astro lanzará un error en tiempo de compilación en lugar de generar en silencio un feed RSS roto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineCollection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro:content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;glob&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro/loaders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineCollection&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/*.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./src/content/blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;pubDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;collections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;blog&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada entrada del blog comienza con un bloque de frontmatter que impulsa todo lo que viene después, desde el feed RSS hasta las importaciones a Dev.to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Building&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Terminal&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Text&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Editor:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;The&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;View&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(Part&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3)"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;In&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Part&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;building&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;wordNebula,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cover&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;View&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;layer,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;why&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;chose&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;FTXUI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;over&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ncurses..."&lt;/span&gt;
&lt;span class="na"&gt;pubDate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-04-05&lt;/span&gt;
&lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ilean&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Monterrubio&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Jr"&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/src/content/images/pexels-markus-winkler-1430818-4065400.jpg'&lt;/span&gt;
&lt;span class="na"&gt;categories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cpp'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;terminal'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;architecture'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;programming'&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mantener este frontmatter consistente y alineado con el esquema del RSS es lo que hace que el resto del pipeline funcione sin ninguna limpieza manual. Esto también hace que la configuración de &lt;code&gt;rss.xml.ts&lt;/code&gt; sea sencilla de armar. Lo único que necesita hacer es leer el contenido del frontmatter y generará el archivo &lt;code&gt;rss.xml&lt;/code&gt; en tiempo de compilación.&lt;/p&gt;

&lt;p&gt;Así se ve un archivo &lt;code&gt;rss.xml.ts&lt;/code&gt; básico:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;rss&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@astrojs/rss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getCollection&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro:content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getCollection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;rss&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your Blog Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your blog description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;site&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;pubDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pubDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Los campos del &lt;code&gt;items&lt;/code&gt; se corresponden directamente con los campos del frontmatter que definas. Mientras el frontmatter sea consistente, la salida del RSS será limpia y Dev.to podrá importarlo sin ningún problema. Para más información, visita la &lt;a href="https://docs.astro.build/en/recipes/rss/" rel="noopener noreferrer"&gt;documentación de RSS de Astro&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Importación automática a Dev.to vía RSS
&lt;/h2&gt;

&lt;p&gt;Para esta parte necesitarás una cuenta de Dev.to. Una vez que tu sitio web esté en vivo y la URL del feed RSS esté activa, ve a tu panel de Dev.to. En la barra lateral izquierda encontrarás &lt;strong&gt;RSS Import Feeds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.ilean.me%2Fes%2Fblog%2Fimages%2Fdevto_rss_setup.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.ilean.me%2Fes%2Fblog%2Fimages%2Fdevto_rss_setup.png" alt="Pantalla de configuración de importación del feed RSS de Dev.to"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Desde ahí, haz clic en &lt;strong&gt;+ Add a Feed Source&lt;/strong&gt; para desplegar el formulario. Ingresa la URL de tu &lt;code&gt;rss.xml&lt;/code&gt; en el campo RSS Feed URL. Un par de ajustes a los que vale la pena prestar atención:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mark the RSS source as canonical URL by default:&lt;/strong&gt; déjalo activado. Este es uno de los ajustes más importantes de todo el pipeline. Le indica a Dev.to que tu sitio personal es la fuente original del contenido, lo que significa que los motores de búsqueda darán crédito a tu sitio y no a la versión de Dev.to. Si te equivocas en esto, Dev.to puede terminar posicionándose por encima de tu propio sitio con tus propios textos. Déjalo siempre activado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace self-referential links with DEV Community-specific links:&lt;/strong&gt; déjalo desactivado, a menos que estés migrando todo tu blog a Dev.to de forma permanente.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Una vez que hagas clic en &lt;strong&gt;Add Feed Source&lt;/strong&gt; , Dev.to revisará tu feed periódicamente e importará cualquier entrada nueva. Las entradas importadas llegan como &lt;strong&gt;borradores&lt;/strong&gt; , así que antes de publicar conviene revisar cada una y agregar un &lt;strong&gt;nombre de serie&lt;/strong&gt; si forma parte de una entrada de varias partes, elegir &lt;strong&gt;etiquetas&lt;/strong&gt; que coincidan con las etiquetas populares de Dev.to para que sea más fácil de descubrir (por ejemplo: &lt;code&gt;cpp&lt;/code&gt;, &lt;code&gt;terminal&lt;/code&gt;, &lt;code&gt;architecture&lt;/code&gt;, &lt;code&gt;programming&lt;/code&gt;) y agregar una &lt;strong&gt;imagen de portada&lt;/strong&gt; tomada de Pexels. Cuando estés listo para publicar, abre el editor de la entrada y cambia &lt;code&gt;published&lt;/code&gt; a &lt;code&gt;true&lt;/code&gt; en el frontmatter de Dev.to al inicio de la entrada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verificación con Mastodon y atribución de autoría
&lt;/h2&gt;

&lt;p&gt;Quería establecer una presencia en Mastodon, y resulta que Astro facilita conectar tu blog al fediverso con solo dos pequeñas adiciones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verificación con rel="me"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mastodon usa el estándar de enlace &lt;code&gt;rel="me"&lt;/code&gt; para verificar que eres dueño de un sitio web. Agregarlo a la etiqueta head de tu layout de Astro te da la palomita verde en tu perfil de Mastodon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"me"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://mastodon.social/@yourusername"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Atribución de autoría con fediverse:creator&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;La meta etiqueta &lt;code&gt;fediverse:creator&lt;/code&gt; es una adición más reciente que conecta las entradas de tu blog con tu identidad de Mastodon. Cuando alguien comparte una de tus entradas en Mastodon, le da crédito a tu cuenta automáticamente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"fediverse:creator"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"@yourusername@mastodon.social"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agrégalo al layout de tus entradas para que solo aparezca en las páginas de las entradas y no en todas las páginas del sitio. Juntas, estas dos etiquetas convierten a tu blog en un ciudadano del fediverso en forma: verificado, atribuible y fácil de descubrir por la comunidad tech que ha hecho de Mastodon su hogar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agregando analíticas respetuosas con la privacidad con Umami
&lt;/h2&gt;

&lt;p&gt;Cuando empecé a sacar entradas, quería saber si alguien las estaba leyendo de verdad. Google Analytics era la opción obvia, pero se sentía excesivo: pesado, dependiente de cookies y que requería un banner de consentimiento solo para empezar. Tras investigar un poco encontré Umami, una alternativa ligera y respetuosa con la privacidad que cumplía con todo. El plan gratuito para aficionados cubre hasta 3 sitios web, 100 mil eventos al mes y 6 meses de retención de datos. Sin cookies, sin banners de consentimiento, sin rastreo entre sitios.&lt;/p&gt;

&lt;p&gt;La configuración es sencilla. Una vez que creas tu cuenta de Umami y agregas tu sitio, obtienes una etiqueta de script para colocar en el archivo de layout de Astro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script
  &lt;/span&gt;&lt;span class="na"&gt;defer&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cloud.umami.is/script.js"&lt;/span&gt;
  &lt;span class="na"&gt;data-website-id=&lt;/span&gt;&lt;span class="s"&gt;"your-website-id"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agrégalo a tu archivo principal &lt;code&gt;Layout.astro&lt;/code&gt; y se incluirá automáticamente en cada página. También agregué un pequeño aviso en el pie de página para que los visitantes sepan que el sitio usa analíticas respetuosas con la privacidad, sin cookies ni recolección de datos personales; un detalle pequeño pero honesto que establece las expectativas correctas.&lt;/p&gt;

&lt;p&gt;El panel de Umami cubre todo lo básico: visitantes, vistas de página, referencias, ubicaciones, dispositivos y tasa de rebote. Es todo lo que necesitas para entender cómo está funcionando tu contenido sin comprometer la privacidad de tus lectores.&lt;/p&gt;

&lt;h2&gt;
  
  
  El flujo de publicación
&lt;/h2&gt;

&lt;p&gt;Normalmente escribo sobre algo en lo que trabajé profesionalmente, un proyecto personal o un tema en el que me considero experto. Este es el proceso de principio a fin que sigo desde la idea hasta la entrada publicada:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Escribir el borrador en Notion&lt;/strong&gt; Empecé usando Google Docs, pero desde que retomé el blog me migré a Notion. Maneja bien los bloques de código y las tablas, y exporta a Markdown de una forma que realmente se traduce correctamente, lo que hace que el siguiente paso sea mucho más fluido.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Generar el archivo Markdown listo para Astro con frontmatter&lt;/strong&gt; Una vez que el borrador está listo, lo convierto en un archivo Markdown con los campos de frontmatter correctos —title, description, pubDate, author, image y categories— asegurándome de que todo se alinee con el esquema del RSS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Desplegar en ilean.me el domingo por la mañana&lt;/strong&gt; El domingo es mi día de publicación. Subo la nueva entrada y despliego el sitio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Dev.to la recoge vía RSS&lt;/strong&gt; Dev.to consulta periódicamente la URL del feed RSS, así que la entrada aparecerá como borrador en mi panel de Dev.to en unas horas, dependiendo de cuándo revisó por última vez.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Publicar en Mastodon el mismo día&lt;/strong&gt; Comparto la entrada en Mastodon con hashtags, normalmente los mismos que uso en el frontmatter de la entrada para mantener la consistencia.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Programar LinkedIn para el lunes por la mañana&lt;/strong&gt; LinkedIn tiene una función para programar publicaciones que ha sido un cambio total. La programo para que salga el lunes por la mañana entre las 8 y 9 AM, cuando los reclutadores y profesionales están más activos, dejando que el algoritmo de LinkedIn la haga circular por mi red y mi red extendida durante los días siguientes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Revisar y publicar el borrador de Dev.to&lt;/strong&gt; Una vez que llega a Dev.to, lo reviso, agrego un nombre de serie si forma parte de una entrada de varias partes, configuro las etiquetas, agrego una imagen de portada de Pexels y cambio &lt;code&gt;published&lt;/code&gt; a &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resultados hasta ahora
&lt;/h2&gt;

&lt;p&gt;Reviví el blog en febrero y marzo de 2026. En Dev.to, en febrero la mayor cantidad de vistas que recibí en un solo día fue 16. Desde entonces ese número subió a 36 vistas en un solo día. Al 25 de abril de 2026, Dev.to muestra 435 vistas totales, aunque con una baja del 24 % respecto a los 7 días anteriores, un buen recordatorio de que la consistencia importa.&lt;/p&gt;

&lt;p&gt;En ilean.me, Umami muestra 48 visitantes únicos hasta ahora. Lo que me sorprendió fue la distribución geográfica. Una buena parte de los visitantes viene de Asia, lo cual tiene sentido dado que los sistemas embebidos y C++ son un stack común allá. La mayoría viene de Estados Unidos, con California apareciendo con frecuencia, lo que cuadra dada la concentración de la industria tecnológica ahí.&lt;/p&gt;

&lt;p&gt;Los números son pequeños, pero la tendencia es alentadora. El tráfico de los motores de búsqueda se acumula con el tiempo; cada entrada indexada es otro punto de entrada para que alguien encuentre tu trabajo. El objetivo nunca fue volverse viral de la noche a la mañana, sino construir una presencia en línea de forma lenta y constante, y los datos muestran que es exactamente lo que está pasando.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué haría diferente
&lt;/h2&gt;

&lt;p&gt;Algunas lecciones honestas de poner este pipeline en marcha.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verifica dos veces tus fechas de publicación.&lt;/strong&gt; Una entrada no apareció a tiempo en Dev.to y, tras investigar un poco, me di cuenta de que la fecha en el frontmatter estaba desfasada por un día. Dev.to toma del feed RSS según el campo pubDate, así que si está mal, la entrada se omite o se retrasa. Verifica siempre la fecha antes de desplegar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ten paciencia con los tiempos de obtención del RSS.&lt;/strong&gt; Dev.to no consulta tu feed al instante. Dependiendo de cuándo revisó por última vez, una entrada nueva puede tardar varias horas en aparecer como borrador. No entres en pánico ni asumas que algo está roto; solo dale tiempo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filtra tus propias visitas.&lt;/strong&gt; Al principio me emocioné al ver un visitante de Houston en Umami, antes de darme cuenta de que era yo haciendo clic en mis propios enlaces. El plan gratuito de Umami no tiene una forma integrada de excluir tu propia IP, así que tenlo en cuenta al leer tus primeros números y tómalos con cautela.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encuentra tu comunidad.&lt;/strong&gt; Algo que quiero hacer de aquí en adelante es encontrar servidores de Discord tech de Houston con ingenieros de software y desarrolladores donde compartir mi contenido. Publicar en comunidades relevantes puede acelerar el crecimiento de una forma que el RSS pasivo y las publicaciones en redes por sí solos no pueden.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No revises las analíticas con demasiada frecuencia.&lt;/strong&gt; Es tentador al principio, pero los números se mueven despacio y revisarlos constantemente solo genera ansiedad innecesaria. Establece una frecuencia, tal vez una vez por semana, y mejor concéntrate en escribir la siguiente entrada.&lt;/p&gt;

&lt;p&gt;Todo el propósito de construir este pipeline era eliminar la fricción, y funcionó. Ya no hay un hueco de un año esperando a repetirse; solo está la siguiente entrada.&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>astro</category>
      <category>blogging</category>
      <category>devto</category>
    </item>
    <item>
      <title>Building a Terminal Text Editor: The View (Part 3)</title>
      <dc:creator>Ilean Monterrubio Jr</dc:creator>
      <pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/elcapitan/building-a-terminal-text-editor-the-view-part-3-12gd</link>
      <guid>https://dev.to/elcapitan/building-a-terminal-text-editor-the-view-part-3-12gd</guid>
      <description>&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://www.ilean.me/blog/building-a-terminal-text-editor-the-model-part-1/" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt;, I talked about the origin of this project, the early architectural decisions, and why I picked the GapBuffer with &lt;code&gt;IBuffer&lt;/code&gt; as the contract for all future buffer implementations. In &lt;a href="https://www.ilean.me/blog/building-a-terminal-text-editor-the-presenter-part-2/" rel="noopener noreferrer"&gt;Part 2&lt;/a&gt;, we got into the details of the Presenter, how it wires the components together, handles user input, manages state, and keeps the Model and View in sync. Now we can move on to the View.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the View?
&lt;/h2&gt;

&lt;p&gt;The View is the component that the user interacts with directly. All user inputs are collected and passed to the Presenter, the View doesn't decide what to do with them. It is also responsible for rendering the text the user types, along with any warnings, statistics like word count, and whether the file is unsaved. The View doesn't calculate any of this, it just displays whatever the Presenter sends it. Another example of how all three parts of the Model-View-Presenter separate concerns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why FTXUI instead of ncurses
&lt;/h2&gt;

&lt;p&gt;In the proof of concept testing, I used ncurses, the de facto standard library for building terminal interfaces. It worked well for the initial testing, and I see a lot of projects still using it; it's tried and true. While ncurses would have been a great choice, I did a bit more research and found FTXUI, a modern C++ library with components to build interactive terminal user interfaces. I wanted to leverage that it uses modern C++, matching the project's early defined specification of targeting C++20.&lt;/p&gt;

&lt;h2&gt;
  
  
  The IView Interface
&lt;/h2&gt;

&lt;p&gt;While implementing the other components (Model and Presenter), I mocked up a thin interface for the View, &lt;code&gt;IView&lt;/code&gt;. At first, it was only taking character input and not accounting for special characters, and it didn't handle rendering any data. The proof of concept View just took the input, sent it to the Presenter, which would send it to the buffer and log it so I could see the data was actually flowing through the system.&lt;/p&gt;

&lt;p&gt;I was thinking of removing it, but after doing more research, I decided to expand the interface instead. Keeping &lt;code&gt;IView&lt;/code&gt; as an abstract class means that if someone wants to write their own View for an OS that isn't supported by FTXUI, they can implement the interface and plug it in without touching the Presenter or Model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;public:&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;IView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;InputEvent&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;onInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ViewState&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;getTerminalSize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;showMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick explanation, &lt;code&gt;run()&lt;/code&gt; starts the event loop and takes a callback for handling input, this is what the Presenter calls in its main loop. &lt;code&gt;render()&lt;/code&gt; takes the &lt;code&gt;ViewState&lt;/code&gt; we saw in Part 2 and draws it to the screen. &lt;code&gt;exit()&lt;/code&gt; and &lt;code&gt;getTerminalSize()&lt;/code&gt; do what they say, and &lt;code&gt;showMessage()&lt;/code&gt; is how the Presenter displays warnings like the "unsaved changes" prompt from the exit flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering the UI
&lt;/h2&gt;

&lt;p&gt;As we showed in Part 2, the &lt;code&gt;ViewState&lt;/code&gt; struct defines the contract for how the Presenter sends data to the View:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;ViewState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Content&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;visibleText&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Text currently visible in viewport&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;cursorPosition&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Linear position of cursor in visibleText&lt;/span&gt;

    &lt;span class="c1"&gt;// Status bar information&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;wordCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Total word count (primary metric for writers)&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Current file name (or "Untitled")&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isDirty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Unsaved changes indicator&lt;/span&gt;

    &lt;span class="c1"&gt;// UI state&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;statusMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Temporary status message (e.g., "Saved", "Error: ...")&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;showHelp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Whether to show help overlay&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are three major types of data in the &lt;code&gt;ViewState&lt;/code&gt; contract: &lt;strong&gt;Content&lt;/strong&gt; , which tells us the &lt;code&gt;cursorPosition&lt;/code&gt; and the &lt;code&gt;visibleText&lt;/code&gt;. &lt;strong&gt;Status bar information&lt;/strong&gt; : &lt;code&gt;wordCount&lt;/code&gt;, &lt;code&gt;filename&lt;/code&gt;, and &lt;code&gt;isDirty&lt;/code&gt; which indicates whether the file is unsaved. Finally, the &lt;strong&gt;UI state&lt;/strong&gt; : &lt;code&gt;statusMessage&lt;/code&gt; and &lt;code&gt;showHelp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One of the biggest challenges was how to deal with the cursor, how to move it, and be able to add more to the buffer when the cursor moves. One of the simplest solutions is to break the &lt;code&gt;visibleText&lt;/code&gt; into three components: beforeCursor, cursor, and afterCursor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Element&lt;/span&gt; &lt;span class="n"&gt;FtxuiView&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;renderEditor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visibleText&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursorPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// Character at cursor with cyan background&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;cursorChar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cursorChar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;bgcolor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Cyan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Black&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;hbox&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cursor is highlighted with a cyan background so the user can see where they are in the text. This challenge existed in both ncurses and FTXUI, neither library gives you a built-in cursor for a custom text editor. You have to simulate it yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Translating Keyboard Input
&lt;/h2&gt;

&lt;p&gt;The View is also responsible for translating raw keyboard events into the &lt;code&gt;InputEvent&lt;/code&gt; structs that the Presenter understands. FTXUI has its own event system, so the View needs a translation layer between what FTXUI gives us and what our Presenter expects. The &lt;code&gt;translateEvent()&lt;/code&gt; method handles this mapping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt; &lt;span class="n"&gt;FtxuiView&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;translateEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ArrowLeft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ARROW_LEFT&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ArrowRight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ARROW_RIGHT&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Backspace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BACKSPACE&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Return&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ENTER&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CtrlQ&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CTRL_Q&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CtrlS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CTRL_S&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// Printable character&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_character&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;InputEvent&lt;/span&gt; &lt;span class="n"&gt;ie&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CHARACTER&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="n"&gt;ie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ie&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;UNKNOWN&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern is straightforward, each FTXUI event maps to one of our &lt;code&gt;InputEvent&lt;/code&gt; types. If it's a printable character, we extract the character value. If we don't recognize the event, it returns &lt;code&gt;UNKNOWN&lt;/code&gt; and gets ignored. This is the same translation layer that would need to be reimplemented if someone wrote a new View using a different library, another benefit of the &lt;code&gt;IView&lt;/code&gt; interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the View Can Do So Far
&lt;/h2&gt;

&lt;p&gt;The View works well for the current state of the project. It renders text as the user types, displays the status bar with the filename, word count, and unsaved changes indicator, captures all input events and routes them to the Presenter, and toggles the help overlay with F1. It is not perfect, newlines don't render yet and the arrow keys can't fully navigate up and down through the text. But for Phase 1, it does its job as the passive layer in the MVP pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up the Series
&lt;/h2&gt;

&lt;p&gt;This wraps up the three-part series on building wordNebula's Phase 1. Across these posts, we went from the origin of the project and why I chose Model-View-Presenter, through the Model with the GapBuffer and IBuffer interface, the Presenter that orchestrates everything and manages state, and finally the View that renders it all to the terminal.&lt;/p&gt;

&lt;p&gt;Building each layer independently and connecting them through contracts like &lt;code&gt;IBuffer&lt;/code&gt; and &lt;code&gt;IView&lt;/code&gt; made the project manageable. I could work on one piece at a time without worrying about breaking the others. That separation is the whole point of MVP, and it paid off.&lt;/p&gt;

&lt;p&gt;Phase 1 is complete but far from finished. There's more to build, but for now I'm stepping away to focus on other projects. If you want to explore the code, check out the project on &lt;a href="https://github.com/ileanmjr88/wordNebula" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>terminal</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
    <item>
      <title>Construyendo un editor de texto de terminal: la Vista (Parte 3)</title>
      <dc:creator>Ilean Monterrubio Jr</dc:creator>
      <pubDate>Sun, 05 Apr 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/elcapitan/construyendo-un-editor-de-texto-de-terminal-la-vista-parte-3-2d1e</link>
      <guid>https://dev.to/elcapitan/construyendo-un-editor-de-texto-de-terminal-la-vista-parte-3-2d1e</guid>
      <description>&lt;h2&gt;
  
  
  Resumen
&lt;/h2&gt;

&lt;p&gt;En la &lt;a href="https://dev.to/es/blog/construyendo-un-editor-de-texto-de-terminal-el-modelo-parte-1/"&gt;Parte 1&lt;/a&gt; hablé del origen de este proyecto, las primeras decisiones de arquitectura y por qué elegí el GapBuffer con &lt;code&gt;IBuffer&lt;/code&gt; como el contrato para todas las futuras implementaciones de búfer. En la &lt;a href="https://dev.to/es/blog/construyendo-un-editor-de-texto-de-terminal-el-presentador-parte-2/"&gt;Parte 2&lt;/a&gt; entramos en los detalles del Presentador, cómo conecta los componentes, maneja la entrada del usuario, gestiona el estado y mantiene el Modelo y la Vista sincronizados. Ahora podemos pasar a la Vista.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué es la Vista?
&lt;/h2&gt;

&lt;p&gt;La Vista es el componente con el que el usuario interactúa directamente. Todas las entradas del usuario se recopilan y se pasan al Presentador; la Vista no decide qué hacer con ellas. También es responsable de renderizar el texto que el usuario escribe, junto con cualquier advertencia, estadísticas como el conteo de palabras y si el archivo está sin guardar. La Vista no calcula nada de esto, solo muestra lo que el Presentador le envía. Otro ejemplo de cómo las tres partes de Modelo-Vista-Presentador separan las responsabilidades.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué FTXUI en lugar de ncurses
&lt;/h2&gt;

&lt;p&gt;En las pruebas de la prueba de concepto usé ncurses, la biblioteca estándar de facto para construir interfaces de terminal. Funcionó bien para las pruebas iniciales, y veo muchos proyectos que todavía la usan; es confiable y probada. Aunque ncurses habría sido una gran elección, investigué un poco más y encontré FTXUI, una biblioteca de C++ moderna con componentes para construir interfaces de usuario de terminal interactivas. Quería aprovechar que usa C++ moderno, en línea con la especificación que definí al inicio del proyecto de apuntar a C++20.&lt;/p&gt;

&lt;h2&gt;
  
  
  La interfaz IView
&lt;/h2&gt;

&lt;p&gt;Mientras implementaba los otros componentes (el Modelo y el Presentador), armé una interfaz ligera para la Vista, &lt;code&gt;IView&lt;/code&gt;. Al principio solo tomaba la entrada de caracteres y no contemplaba los caracteres especiales, ni se encargaba de renderizar dato alguno. La Vista de la prueba de concepto solo tomaba la entrada, la enviaba al Presentador, que la mandaba al búfer y la registraba para que yo pudiera ver que los datos realmente fluían por el sistema.&lt;/p&gt;

&lt;p&gt;Pensaba en eliminarla, pero después de investigar más, decidí más bien expandir la interfaz. Mantener &lt;code&gt;IView&lt;/code&gt; como una clase abstracta significa que, si alguien quiere escribir su propia Vista para un sistema operativo que FTXUI no soporta, puede implementar la interfaz y conectarla sin tocar el Presentador ni el Modelo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;public:&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;IView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;InputEvent&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;onInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ViewState&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;pair&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;getTerminalSize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;showMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una explicación rápida: &lt;code&gt;run()&lt;/code&gt; inicia el bucle de eventos y recibe un callback para manejar la entrada; esto es lo que el Presentador llama en su bucle principal. &lt;code&gt;render()&lt;/code&gt; toma el &lt;code&gt;ViewState&lt;/code&gt; que vimos en la Parte 2 y lo dibuja en la pantalla. &lt;code&gt;exit()&lt;/code&gt; y &lt;code&gt;getTerminalSize()&lt;/code&gt; hacen lo que dicen, y &lt;code&gt;showMessage()&lt;/code&gt; es la forma en que el Presentador muestra advertencias como el aviso de "cambios sin guardar" del flujo de salida.&lt;/p&gt;

&lt;h2&gt;
  
  
  Renderizando la interfaz
&lt;/h2&gt;

&lt;p&gt;Como mostramos en la Parte 2, la estructura &lt;code&gt;ViewState&lt;/code&gt; define el contrato de cómo el Presentador envía datos a la Vista:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;ViewState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Content&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;visibleText&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Text currently visible in viewport&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;cursorPosition&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Linear position of cursor in visibleText&lt;/span&gt;

    &lt;span class="c1"&gt;// Status bar information&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;wordCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Total word count (primary metric for writers)&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Current file name (or "Untitled")&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isDirty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Unsaved changes indicator&lt;/span&gt;

    &lt;span class="c1"&gt;// UI state&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;statusMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Temporary status message (e.g., "Saved", "Error: ...")&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;showHelp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Whether to show help overlay&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hay tres grandes tipos de datos en el contrato de &lt;code&gt;ViewState&lt;/code&gt;: &lt;strong&gt;Contenido&lt;/strong&gt; , que nos dice la &lt;code&gt;cursorPosition&lt;/code&gt; y el &lt;code&gt;visibleText&lt;/code&gt;. &lt;strong&gt;Información de la barra de estado&lt;/strong&gt; : &lt;code&gt;wordCount&lt;/code&gt;, &lt;code&gt;filename&lt;/code&gt; e &lt;code&gt;isDirty&lt;/code&gt;, que indica si el archivo está sin guardar. Por último, el &lt;strong&gt;estado de la interfaz&lt;/strong&gt; : &lt;code&gt;statusMessage&lt;/code&gt; y &lt;code&gt;showHelp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Uno de los mayores retos fue cómo lidiar con el cursor, cómo moverlo y poder agregar más al búfer cuando el cursor se mueve. Una de las soluciones más simples es dividir el &lt;code&gt;visibleText&lt;/code&gt; en tres componentes: beforeCursor, cursor y afterCursor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Element&lt;/span&gt; &lt;span class="n"&gt;FtxuiView&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;renderEditor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visibleText&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursorPosition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// Character at cursor with cyan background&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;cursorChar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cursorChar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;bgcolor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Cyan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Black&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;hbox&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El cursor se resalta con un fondo cian para que el usuario pueda ver dónde está en el texto. Este reto existía tanto en ncurses como en FTXUI; ninguna biblioteca te da un cursor integrado para un editor de texto personalizado. Tienes que simularlo tú mismo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Traduciendo la entrada del teclado
&lt;/h2&gt;

&lt;p&gt;La Vista también es responsable de traducir los eventos crudos del teclado a las estructuras &lt;code&gt;InputEvent&lt;/code&gt; que el Presentador entiende. FTXUI tiene su propio sistema de eventos, así que la Vista necesita una capa de traducción entre lo que FTXUI nos da y lo que nuestro Presentador espera. El método &lt;code&gt;translateEvent()&lt;/code&gt; se encarga de este mapeo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt; &lt;span class="n"&gt;FtxuiView&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;translateEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ArrowLeft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ARROW_LEFT&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ArrowRight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ARROW_RIGHT&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Backspace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BACKSPACE&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Return&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ENTER&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CtrlQ&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CTRL_Q&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ftxui&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CtrlS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CTRL_S&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// Printable character&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_character&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;InputEvent&lt;/span&gt; &lt;span class="n"&gt;ie&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CHARACTER&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="n"&gt;ie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ie&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;UNKNOWN&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El patrón es sencillo: cada evento de FTXUI se mapea a uno de nuestros tipos &lt;code&gt;InputEvent&lt;/code&gt;. Si es un carácter imprimible, extraemos el valor del carácter. Si no reconocemos el evento, devuelve &lt;code&gt;UNKNOWN&lt;/code&gt; y se ignora. Esta es la misma capa de traducción que habría que reimplementar si alguien escribiera una nueva Vista usando una biblioteca distinta, otro beneficio de la interfaz &lt;code&gt;IView&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que la Vista puede hacer hasta ahora
&lt;/h2&gt;

&lt;p&gt;La Vista funciona bien para el estado actual del proyecto. Renderiza el texto conforme el usuario escribe, muestra la barra de estado con el nombre del archivo, el conteo de palabras y el indicador de cambios sin guardar, captura todos los eventos de entrada y los enruta al Presentador, y alterna la superposición de ayuda con F1. No es perfecta: los saltos de línea todavía no se renderizan y las teclas de flecha no pueden navegar del todo hacia arriba y hacia abajo por el texto. Pero para la Fase 1, cumple su trabajo como la capa pasiva del patrón MVP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cerrando la serie
&lt;/h2&gt;

&lt;p&gt;Con esto cerramos la serie de tres partes sobre la construcción de la Fase 1 de wordNebula. A lo largo de estas entradas fuimos desde el origen del proyecto y por qué elegí Modelo-Vista-Presentador, pasando por el Modelo con el GapBuffer y la interfaz IBuffer, el Presentador que orquesta todo y gestiona el estado, y finalmente la Vista que lo renderiza todo en la terminal.&lt;/p&gt;

&lt;p&gt;Construir cada capa de forma independiente y conectarlas mediante contratos como &lt;code&gt;IBuffer&lt;/code&gt; e &lt;code&gt;IView&lt;/code&gt; hizo que el proyecto fuera manejable. Podía trabajar en una pieza a la vez sin preocuparme por romper las demás. Esa separación es justamente el propósito de MVP, y valió la pena.&lt;/p&gt;

&lt;p&gt;La Fase 1 está completa pero lejos de estar terminada. Hay más por construir, pero por ahora me alejo para enfocarme en otros proyectos. Si quieres explorar el código, échale un vistazo al proyecto en &lt;a href="https://github.com/ileanmjr88/wordNebula" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>cpp</category>
      <category>terminal</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Building a Terminal Text Editor: The Presenter (Part 2)</title>
      <dc:creator>Ilean Monterrubio Jr</dc:creator>
      <pubDate>Sun, 22 Mar 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/elcapitan/building-a-terminal-text-editor-the-presenter-part-2-2h08</link>
      <guid>https://dev.to/elcapitan/building-a-terminal-text-editor-the-presenter-part-2-2h08</guid>
      <description>&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://www.ilean.me/blog/building-a-terminal-text-editor-the-model-part-1/" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt;, I talked about the origin of this project and the early architectural decisions. After picking Model-View-Presenter, breaking up the project into Model, View, and Presenter felt like the natural path forward. We tackled the &lt;code&gt;GapBuffer&lt;/code&gt; with &lt;code&gt;IBuffer&lt;/code&gt; as the contract for all future buffer implementations. With the Model standing on its own, we can move on to the Presenter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the Presenter?
&lt;/h2&gt;

&lt;p&gt;We already hinted in Part 1 that the Presenter is the orchestrator — the one that handles all the application's "business" logic. It is responsible for keeping both the Model and the View up to date. The Presenter takes user input from the View and passes it to the Model, then pulls the updated data from the Model and sends it back to the View for rendering. All communication between the two goes through the Presenter.&lt;/p&gt;

&lt;p&gt;Let's take the most common use case of a user pressing a single key stroke. In reality, they will be typing several in succession, and this process gets repeated constantly in the current implementation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;From&lt;/th&gt;
&lt;th&gt;To&lt;/th&gt;
&lt;th&gt;What Happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;User (actor)&lt;/td&gt;
&lt;td&gt;View&lt;/td&gt;
&lt;td&gt;Presses key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;View&lt;/td&gt;
&lt;td&gt;Presenter&lt;/td&gt;
&lt;td&gt;Passes the key event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Presenter&lt;/td&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Calls insertChar()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Presenter&lt;/td&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Requests updated text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Presenter&lt;/td&gt;
&lt;td&gt;View&lt;/td&gt;
&lt;td&gt;Sends text to render&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We see the big role that the Presenter fills and we will explore how we created contracts between the components to ensure that data is passed correctly. For wordNebula, this also means the Presenter will be where future logic lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring It Together
&lt;/h2&gt;

&lt;p&gt;Since this project is using modern C++ I wanted to take advantage of smart pointers and leverage Resource Acquisition Is Initialization (RAII). This way we don't have to explicitly manage memory — this is standard practice in modern C++ for any systems project.&lt;/p&gt;

&lt;p&gt;When starting to wire up the different components, including the mock View from the original testing I did before tackling the full project, I had to make some decisions on ownership. Here is how the three components get created in &lt;code&gt;main&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;presenter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;make_shared&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WNebulaPresenter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;make_shared&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WNebulaModel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;make_shared&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FtxuiView&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;presenter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;presenter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three are created as &lt;code&gt;shared_ptr&lt;/code&gt;, but inside the Presenter they are stored differently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;weak_ptr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IView&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;shared_ptr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WNebulaModel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Model stays as a &lt;code&gt;shared_ptr&lt;/code&gt; because the Presenter needs to own it — it is the core data and must exist as long as the application is running. The View is stored as a &lt;code&gt;weak_ptr&lt;/code&gt; to break a circular dependency. The Presenter needs the View to push updates, and the View needs the Presenter to send input. If both held &lt;code&gt;shared_ptr&lt;/code&gt; to each other, neither would ever get cleaned up.&lt;/p&gt;

&lt;p&gt;The trade-off with &lt;code&gt;weak_ptr&lt;/code&gt; is that every time the Presenter needs to update the View, it has to convert it back to a &lt;code&gt;shared_ptr&lt;/code&gt; temporarily by calling &lt;code&gt;lock()&lt;/code&gt;. If the View still exists, &lt;code&gt;lock()&lt;/code&gt; gives us a valid &lt;code&gt;shared_ptr&lt;/code&gt; to work with. If it doesn't, we know the View is gone and can handle it gracefully. This happens inside &lt;code&gt;updateView()&lt;/code&gt; every time the Presenter needs to render new data to the screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling User Input
&lt;/h2&gt;

&lt;p&gt;In the exploratory testing, I only worried about character user input — it was just to test if I understood the architecture. For the real implementation, I needed to actually account for special keys: &lt;code&gt;CTRL&lt;/code&gt;, &lt;code&gt;BACKSPACE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;ARROW KEYS&lt;/code&gt;, &lt;code&gt;ENTER&lt;/code&gt;, and so on.&lt;/p&gt;

&lt;p&gt;To handle this cleanly, I created an &lt;code&gt;InputEvent&lt;/code&gt; struct that translates raw keyboard input into semantic events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;InputEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CHARACTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ARROW_LEFT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARROW_RIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARROW_UP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARROW_DOWN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CTRL_LEFT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CTRL_RIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CTRL_UP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CTRL_DOWN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;HOME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;BACKSPACE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DELETE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CTRL_S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CTRL_Q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ESCAPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// ... other types&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;character&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sc"&gt;'\0'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Only valid for Type::CHARACTER&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way the Presenter doesn't care about terminal escape codes — it just receives an event type and acts on it. The &lt;code&gt;handleInput&lt;/code&gt; method routes each event to the right operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;WNebulaPresenter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;handleInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;InputEvent&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CHARACTER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;onInsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BACKSPACE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;onDelete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CTRL_LEFT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;onCtrlLeft&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CTRL_Q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;onExit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other cases&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each operation follows the same pattern — delegate to the Model, mark dirty if needed, update the View:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;WNebulaPresenter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;onInsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;insertChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;isDirty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;updateView&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Viewport Management
&lt;/h2&gt;

&lt;p&gt;One of the things I learned along the way, or had to really think about, was the viewport. So far, the initial testing was me writing simple sentences, not full blog entries or longer format text. The area of the terminal is finite, so determining what portion of the buffer to display and how to present it was a challenge.&lt;/p&gt;

&lt;p&gt;The Presenter solves this by building a &lt;code&gt;ViewState&lt;/code&gt; — a struct that packages everything the View needs to render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;ViewState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;visibleText&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;cursorPosition&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;wordCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isDirty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;showHelp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time the Presenter updates the View, it pulls the current text and cursor position from the Model and sends it as a &lt;code&gt;ViewState&lt;/code&gt;. The View doesn't know anything about the buffer — it just renders whatever state it receives. Here is the &lt;code&gt;updateView()&lt;/code&gt; method that builds and sends that state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;WNebulaPresenter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;updateView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ViewState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visibleText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;getText&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursorPosition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;getCursorPosition&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wordCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;getWordCount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currentFilePath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"Untitled"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;currentFilePath&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isDirty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;isDirty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;showHelp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;showHelp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;view.lock()&lt;/code&gt; — this is the &lt;code&gt;weak_ptr&lt;/code&gt; conversion we talked about in the Wiring section. Every time the Presenter needs to update the View, it checks that the View still exists before sending data.&lt;/p&gt;

&lt;h2&gt;
  
  
  State Management
&lt;/h2&gt;

&lt;p&gt;For managing state in this iteration of the project I kept it simple. The Presenter tracks a few boolean flags — &lt;code&gt;isDirty&lt;/code&gt; to know when the file hasn't been saved, &lt;code&gt;isRunning&lt;/code&gt; to control the main loop, &lt;code&gt;showHelp&lt;/code&gt; to toggle the help overlay, and &lt;code&gt;exitWarningShown&lt;/code&gt; for the quit confirmation.&lt;/p&gt;

&lt;p&gt;The most interesting one is the exit flow. If you have unsaved changes and press Ctrl+Q, the Presenter warns you and makes you press it again to actually quit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;WNebulaPresenter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;onExit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isDirty&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;exitWarningShown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;exitWarningShown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;showMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unsaved changes! Press Ctrl+Q again to quit."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;isRunning&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the Presenter Can Do So Far
&lt;/h2&gt;

&lt;p&gt;At this stage, the Presenter takes in all input events and determines if it is a special key or character, fully manages the state of the application and document, and keeps the Model and View in sync. It is the glue between the two, and because of MVP, it can be tested independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;In Part 3, I will talk about why I picked FTXUI instead of ncurses and what it offers out of the box that made the View layer easier to build. You can check out the project on &lt;a href="https://github.com/ileanmjr88/wordNebula" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>terminal</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
    <item>
      <title>Construyendo un editor de texto de terminal: el Presentador (Parte 2)</title>
      <dc:creator>Ilean Monterrubio Jr</dc:creator>
      <pubDate>Sun, 22 Mar 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/elcapitan/construyendo-un-editor-de-texto-de-terminal-el-presentador-parte-2-2lc8</link>
      <guid>https://dev.to/elcapitan/construyendo-un-editor-de-texto-de-terminal-el-presentador-parte-2-2lc8</guid>
      <description>&lt;h2&gt;
  
  
  Resumen
&lt;/h2&gt;

&lt;p&gt;En la &lt;a href="https://dev.to/es/blog/construyendo-un-editor-de-texto-de-terminal-el-modelo-parte-1/"&gt;Parte 1&lt;/a&gt; hablé del origen de este proyecto y de las primeras decisiones de arquitectura. Después de elegir Modelo-Vista-Presentador, dividir el proyecto en Modelo, Vista y Presentador se sintió como el camino natural. Abordamos el &lt;code&gt;GapBuffer&lt;/code&gt; con &lt;code&gt;IBuffer&lt;/code&gt; como el contrato para todas las futuras implementaciones de búfer. Con el Modelo sosteniéndose por sí solo, podemos pasar al Presentador.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué es el Presentador?
&lt;/h2&gt;

&lt;p&gt;Ya insinuamos en la Parte 1 que el Presentador es el orquestador: el que maneja toda la lógica de "negocio" de la aplicación. Es responsable de mantener actualizados tanto el Modelo como la Vista. El Presentador toma la entrada del usuario desde la Vista y se la pasa al Modelo, luego obtiene los datos actualizados del Modelo y los envía de vuelta a la Vista para renderizarlos. Toda la comunicación entre ambos pasa por el Presentador.&lt;/p&gt;

&lt;p&gt;Tomemos el caso de uso más común: un usuario presionando una sola tecla. En realidad estará escribiendo varias en sucesión, y este proceso se repite constantemente en la implementación actual.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Paso&lt;/th&gt;
&lt;th&gt;De&lt;/th&gt;
&lt;th&gt;A&lt;/th&gt;
&lt;th&gt;Qué ocurre&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Usuario (actor)&lt;/td&gt;
&lt;td&gt;Vista&lt;/td&gt;
&lt;td&gt;Presiona una tecla&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Vista&lt;/td&gt;
&lt;td&gt;Presentador&lt;/td&gt;
&lt;td&gt;Pasa el evento de tecla&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Presentador&lt;/td&gt;
&lt;td&gt;Modelo&lt;/td&gt;
&lt;td&gt;Llama a insertChar()&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Presentador&lt;/td&gt;
&lt;td&gt;Modelo&lt;/td&gt;
&lt;td&gt;Solicita el texto actualizado&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Presentador&lt;/td&gt;
&lt;td&gt;Vista&lt;/td&gt;
&lt;td&gt;Envía el texto para renderizar&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Vemos el gran papel que cumple el Presentador y exploraremos cómo creamos contratos entre los componentes para asegurar que los datos se pasen correctamente. Para wordNebula, esto también significa que el Presentador será donde viva la lógica futura.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conectándolo todo
&lt;/h2&gt;

&lt;p&gt;Como este proyecto usa C++ moderno, quise aprovechar los smart pointers y apoyarme en Resource Acquisition Is Initialization (RAII). De esta forma no tenemos que gestionar la memoria de forma explícita; es la práctica estándar en C++ moderno para cualquier proyecto de sistemas.&lt;/p&gt;

&lt;p&gt;Al empezar a conectar los distintos componentes, incluyendo la Vista simulada de las pruebas originales que hice antes de abordar el proyecto completo, tuve que tomar algunas decisiones sobre la propiedad (ownership). Así se crean los tres componentes en &lt;code&gt;main&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;presenter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;make_shared&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WNebulaPresenter&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;make_shared&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WNebulaModel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;make_shared&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FtxuiView&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;presenter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;presenter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Los tres se crean como &lt;code&gt;shared_ptr&lt;/code&gt;, pero dentro del Presentador se almacenan de forma diferente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;weak_ptr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IView&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;shared_ptr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WNebulaModel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El Modelo se queda como &lt;code&gt;shared_ptr&lt;/code&gt; porque el Presentador necesita ser su dueño: son los datos centrales y deben existir mientras la aplicación esté corriendo. La Vista se almacena como &lt;code&gt;weak_ptr&lt;/code&gt; para romper una dependencia circular. El Presentador necesita la Vista para enviar actualizaciones, y la Vista necesita al Presentador para enviar la entrada. Si ambos guardaran un &lt;code&gt;shared_ptr&lt;/code&gt; el uno del otro, ninguno se liberaría jamás.&lt;/p&gt;

&lt;p&gt;La contrapartida de &lt;code&gt;weak_ptr&lt;/code&gt; es que cada vez que el Presentador necesita actualizar la Vista, tiene que convertirla temporalmente de vuelta a un &lt;code&gt;shared_ptr&lt;/code&gt; llamando a &lt;code&gt;lock()&lt;/code&gt;. Si la Vista todavía existe, &lt;code&gt;lock()&lt;/code&gt; nos da un &lt;code&gt;shared_ptr&lt;/code&gt; válido con el cual trabajar. Si no, sabemos que la Vista ya no está y podemos manejarlo de forma elegante. Esto ocurre dentro de &lt;code&gt;updateView()&lt;/code&gt; cada vez que el Presentador necesita renderizar datos nuevos en la pantalla.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manejando la entrada del usuario
&lt;/h2&gt;

&lt;p&gt;En las pruebas exploratorias solo me preocupaba por la entrada de caracteres; era únicamente para comprobar si entendía la arquitectura. Para la implementación real, necesitaba realmente contemplar las teclas especiales: &lt;code&gt;CTRL&lt;/code&gt;, &lt;code&gt;BACKSPACE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;ARROW KEYS&lt;/code&gt;, &lt;code&gt;ENTER&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;Para manejar esto de forma limpia, creé una estructura &lt;code&gt;InputEvent&lt;/code&gt; que traduce la entrada cruda del teclado en eventos semánticos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;InputEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;CHARACTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ARROW_LEFT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARROW_RIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARROW_UP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARROW_DOWN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CTRL_LEFT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CTRL_RIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CTRL_UP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CTRL_DOWN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;HOME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;BACKSPACE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DELETE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CTRL_S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CTRL_Q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ESCAPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// ... other types&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;character&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sc"&gt;'\0'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Only valid for Type::CHARACTER&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;De esta forma al Presentador no le importan los códigos de escape de la terminal: solo recibe un tipo de evento y actúa en consecuencia. El método &lt;code&gt;handleInput&lt;/code&gt; enruta cada evento a la operación correcta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;WNebulaPresenter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;handleInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;InputEvent&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CHARACTER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;onInsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;character&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BACKSPACE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;onDelete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CTRL_LEFT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;onCtrlLeft&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CTRL_Q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;onExit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other cases&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada operación sigue el mismo patrón: delegar en el Modelo, marcar como modificado (dirty) si hace falta y actualizar la Vista:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;WNebulaPresenter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;onInsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;insertChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;isDirty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;updateView&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Gestión del viewport
&lt;/h2&gt;

&lt;p&gt;Una de las cosas que aprendí en el camino, o sobre la que tuve que pensar de verdad, fue el viewport. Hasta ese momento, las pruebas iniciales consistían en que yo escribía frases sencillas, no entradas de blog completas ni texto de formato más largo. El área de la terminal es finita, así que determinar qué porción del búfer mostrar y cómo presentarla fue todo un reto.&lt;/p&gt;

&lt;p&gt;El Presentador resuelve esto construyendo un &lt;code&gt;ViewState&lt;/code&gt;: una estructura que empaqueta todo lo que la Vista necesita para renderizar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;ViewState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;visibleText&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;cursorPosition&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;wordCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isDirty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;showHelp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada vez que el Presentador actualiza la Vista, obtiene el texto actual y la posición del cursor del Modelo y los envía como un &lt;code&gt;ViewState&lt;/code&gt;. La Vista no sabe nada del búfer: solo renderiza cualquier estado que reciba. Aquí está el método &lt;code&gt;updateView()&lt;/code&gt; que construye y envía ese estado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;WNebulaPresenter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;updateView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ViewState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visibleText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;getText&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cursorPosition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;getCursorPosition&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wordCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;getWordCount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currentFilePath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"Untitled"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;currentFilePath&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isDirty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;isDirty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;showHelp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;showHelp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fíjate en el &lt;code&gt;view.lock()&lt;/code&gt;: es la conversión de &lt;code&gt;weak_ptr&lt;/code&gt; de la que hablamos en la sección de conexión. Cada vez que el Presentador necesita actualizar la Vista, comprueba que la Vista todavía exista antes de enviar datos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gestión del estado
&lt;/h2&gt;

&lt;p&gt;Para gestionar el estado en esta iteración del proyecto lo mantuve simple. El Presentador rastrea unas cuantas banderas booleanas: &lt;code&gt;isDirty&lt;/code&gt; para saber cuándo el archivo no se ha guardado, &lt;code&gt;isRunning&lt;/code&gt; para controlar el bucle principal, &lt;code&gt;showHelp&lt;/code&gt; para alternar la superposición de ayuda y &lt;code&gt;exitWarningShown&lt;/code&gt; para la confirmación de salida.&lt;/p&gt;

&lt;p&gt;El más interesante es el flujo de salida. Si tienes cambios sin guardar y presionas Ctrl+Q, el Presentador te avisa y te hace presionarlo de nuevo para salir de verdad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;WNebulaPresenter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;onExit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isDirty&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;exitWarningShown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;exitWarningShown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;showMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unsaved changes! Press Ctrl+Q again to quit."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;isRunning&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lo que el Presentador puede hacer hasta ahora
&lt;/h2&gt;

&lt;p&gt;En esta etapa, el Presentador recibe todos los eventos de entrada y determina si se trata de una tecla especial o un carácter, gestiona por completo el estado de la aplicación y del documento, y mantiene el Modelo y la Vista sincronizados. Es el pegamento entre ambos y, gracias a MVP, se puede probar de forma independiente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que sigue
&lt;/h2&gt;

&lt;p&gt;En la Parte 3 hablaré de por qué elegí FTXUI en lugar de ncurses y de lo que ofrece de fábrica que hizo más fácil construir la capa de la Vista. Puedes ver el proyecto en &lt;a href="https://github.com/ileanmjr88/wordNebula" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>cpp</category>
      <category>datastructures</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Building a Terminal Text Editor: The Model (Part 1)</title>
      <dc:creator>Ilean Monterrubio Jr</dc:creator>
      <pubDate>Sat, 07 Mar 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/elcapitan/building-a-terminal-text-editor-the-model-part-1-gca</link>
      <guid>https://dev.to/elcapitan/building-a-terminal-text-editor-the-model-part-1-gca</guid>
      <description>&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;The idea for wordNebula came from seeing an interview with George R.R. Martin, where he talks about how he writes his books on a really old computer because his preferred word processor can't run on anything new. After finding out that the program is called WordStar, and some minor Googling, I saw it was entirely terminal-based, and thought that idea was novel.&lt;/p&gt;

&lt;p&gt;Growing up in the 90s, I went to an elementary school that gave us weekly computer lab time. I got the chance to use 5.25" floppy disks to play text-based games, and later learned to use the Macintosh computers and play the new versions of The Oregon Trail. So I can see how a writer can feel nostalgic and want to keep using a tool — if it's not broken, why change it?&lt;/p&gt;

&lt;p&gt;I am also a software engineer who has used Vim and Neovim, and I see why they have such great appeal. Those tools keep improving and staying up to date, and they are great at being distraction-free, keeping the user's focus on the task at hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Build Another One?
&lt;/h2&gt;

&lt;p&gt;Starting this project, I did a bit of research. Terminal word processors already exist. The open-source one I found is called &lt;a href="https://github.com/davidgiven/wordgrinder" rel="noopener noreferrer"&gt;WordGrinder&lt;/a&gt; — it has been around for a while and is written in C and Lua. It is a great project; check it out if you have the time. While terminal word processors are no longer popular, many writers still use them for the limited distraction and focus they offer.&lt;/p&gt;

&lt;p&gt;Honestly, I just thought it was something cool to build, and I just wanted something to work on for me. The overall stretch goal is to make wordNebula a full writer's tool — it would format the output for the user based on a config file or similar setup. The idea is that playwrights and screenwriters could fully write without any distraction or worrying about formatting, and novelists could structure their work by chapter. But that is the future; for now, it is a portfolio project that lets me explore terminal UI development, data structures, and architecture patterns in C++.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing an Architecture
&lt;/h2&gt;

&lt;p&gt;For this project, I decided early on to use modern C++ targeting C++20. Before diving into implementation, my first architectural decision was choosing a pattern: Model-View-Controller (MVC), Model-View-Presenter (MVP), or the newer Model-View-ViewModel (MVVM).&lt;/p&gt;

&lt;p&gt;Here is a quick comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Model-View-Controller&lt;/th&gt;
&lt;th&gt;Model-View-Presenter&lt;/th&gt;
&lt;th&gt;Model-View-ViewModel&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mediator&lt;/td&gt;
&lt;td&gt;Controller&lt;/td&gt;
&lt;td&gt;Presenter&lt;/td&gt;
&lt;td&gt;ViewModel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;View Role&lt;/td&gt;
&lt;td&gt;Active, interacts with Model directly&lt;/td&gt;
&lt;td&gt;Passive, passes data to Presenter&lt;/td&gt;
&lt;td&gt;Passive, data is bound to ViewModel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Binding&lt;/td&gt;
&lt;td&gt;Manual updates&lt;/td&gt;
&lt;td&gt;Manual, Presenter sends to Model&lt;/td&gt;
&lt;td&gt;Automatic via data binding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testability&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Best&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Separation&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Better&lt;/td&gt;
&lt;td&gt;Best&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best For&lt;/td&gt;
&lt;td&gt;Small projects&lt;/td&gt;
&lt;td&gt;Medium to complex&lt;/td&gt;
&lt;td&gt;Large, data-driven UIs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After researching these patterns and studying block diagrams, I picked Model-View-Presenter. The main reason is that it completely separates all three concerns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Model&lt;/strong&gt; is the text buffer. Its only job is maintaining the buffer — storing text, tracking the cursor, and supporting insertions and deletions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The View&lt;/strong&gt; handles the UI layout, how the data is displayed, and capturing the user's key inputs, which it passes to the Presenter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Presenter&lt;/strong&gt; is the orchestrator. It receives input from the View, passes text operations to the Model, then requests the updated data from the Model to refresh the View.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All communication flows through the Presenter. This is the main reason the architecture is testable — and while we all know unit tests aren't fun, we understand why they're important.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting My Hands Dirty
&lt;/h2&gt;

&lt;p&gt;With the architecture decision made and the clean separation of concerns giving me the ability to work on different parts of the project independently, I started with a simple proof of concept. I implemented a basic Model text buffer that accepts keystrokes and prints to a log file. It wasn't perfect, but it got me to understand the basic structure and play a bit with MVP before committing to the full implementation.&lt;/p&gt;

&lt;p&gt;For this I set up what I called a TextBuffer, which was just a string where I added things to it via a simple Presenter and View with ncurses. I had Copilot help me test it, and what I learned was that I should really start the project by implementing the Model first — it is such a critical part of the project, and it prompted me to do more research on how word processors handle their buffers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Model: Choosing a Data Structure
&lt;/h2&gt;

&lt;p&gt;I started doing research on text buffers and saw some really appealing ones. One was the Gap Buffer, which I believe Emacs uses. Another was the Piece Table, used by Microsoft Word, and I found a blog from the VS Code team about their usage of it and their findings: &lt;a href="https://code.visualstudio.com/blogs/2018/03/23/text-buffer-reimplementation" rel="noopener noreferrer"&gt;Text Buffer Reimplementation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After observing this and understanding the differences between them, I picked the Gap Buffer for the initial implementation. This decision became clear once I started to define the use cases — since this is a terminal-based UI, users probably won't be highlighting and removing large chunks of text, so undo and redo can be implemented much later. And also a key difference from any modern code editor: we don't need multiple cursors, only a single one where the user can add to the document.&lt;/p&gt;

&lt;p&gt;The idea behind a Gap Buffer is straightforward. You have an array with a "gap" of empty space that follows the cursor around. When you type, you drop characters into the gap. When you move the cursor, the gap moves with it. Here is what that looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before: [H][e][l][l][o][___GAP___][W][o][r][l][d]
                        ^ cursor

Insert ',':
After: [H][e][l][l][o][,][__GAP__][W][o][r][l][d]
                           ^ cursor

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core of the insertion is simple — move the gap to the cursor, make room if needed, and drop the character in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;GapBuffer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;insertChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;moveGapToCursor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getGapSize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;expandGap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;gapStart&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;gapStart&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was also key to use interface abstract classes to make sure I could update the buffer if I ever make a new implementation. This was very important to me because moving into the future we might need flexibility — being able to add undo/redo, or add styling. I was so unsure early on that I thought having that flexibility was very important to have.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;IBuffer&lt;/code&gt; interface defines the full contract that any buffer must fulfill. Here is a trimmed version showing the method signatures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IBuffer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;public:&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;IBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Text Operations&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;insertChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;insertText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;deleteChar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;deleteForward&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Text Access&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;getText&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;getTextRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;getLength&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Cursor Management&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;getCursorPosition&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;setCursorPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;moveCursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Smart Navigation&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;findNextWordBoundary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fromPos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;findPrevWordBoundary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fromPos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;findNextParagraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fromPos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;findPrevParagraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fromPos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Statistics&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;getWordCount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;getParagraphCount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this interface, if I ever need to swap in a Piece Table for advanced undo/redo support down the road, I can do it without rewriting the Presenter or View. The full interface with detailed Doxygen documentation is in the &lt;a href="https://github.com/ileanmjr88/wordNebula" rel="noopener noreferrer"&gt;repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Model Can Do So Far
&lt;/h2&gt;

&lt;p&gt;The Model can fully add text at the cursor position, and the gap can grow dynamically. It supports full cursor navigation, text insertion and deletion, word count, and paragraph count. It also has a comprehensive test suite with Google Test. The Model stands on its own — it was built and tested before the Presenter or View ever existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;In Part 2 I will go over the Presenter, which takes the Model and orchestrates everything — coordinating between the buffer and the UI, and managing application state. The Presenter and View are already implemented; you can check out the project on &lt;a href="https://github.com/ileanmjr88/wordNebula" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>terminal</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
    <item>
      <title>Construyendo un editor de texto de terminal: el Modelo (Parte 1)</title>
      <dc:creator>Ilean Monterrubio Jr</dc:creator>
      <pubDate>Sat, 07 Mar 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/elcapitan/construyendo-un-editor-de-texto-de-terminal-el-modelo-parte-1-4fm4</link>
      <guid>https://dev.to/elcapitan/construyendo-un-editor-de-texto-de-terminal-el-modelo-parte-1-4fm4</guid>
      <description>&lt;h2&gt;
  
  
  La idea
&lt;/h2&gt;

&lt;p&gt;La idea de wordNebula surgió al ver una entrevista con George R.R. Martin, donde cuenta cómo escribe sus libros en una computadora muy vieja porque su procesador de texto preferido no corre en nada nuevo. Después de enterarme de que el programa se llama WordStar, y de googlear un poco, vi que era completamente basado en terminal, y me pareció una idea original.&lt;/p&gt;

&lt;p&gt;Al crecer en los 90, fui a una primaria que nos daba tiempo semanal en el laboratorio de cómputo. Tuve la oportunidad de usar disquetes de 5.25" para jugar juegos basados en texto, y más adelante aprendí a usar las computadoras Macintosh y a jugar las nuevas versiones de The Oregon Trail. Así que entiendo cómo un escritor puede sentirse nostálgico y querer seguir usando una herramienta: si no está roto, ¿para qué cambiarlo?&lt;/p&gt;

&lt;p&gt;También soy ingeniero de software y he usado Vim y Neovim, y entiendo por qué tienen tanto atractivo. Esas herramientas siguen mejorando y manteniéndose al día, y son excelentes para trabajar sin distracciones, manteniendo la atención del usuario en la tarea que tiene enfrente.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Por qué construir otro?
&lt;/h2&gt;

&lt;p&gt;Al empezar este proyecto investigué un poco. Los procesadores de texto de terminal ya existen. El de código abierto que encontré se llama &lt;a href="https://github.com/davidgiven/wordgrinder" rel="noopener noreferrer"&gt;WordGrinder&lt;/a&gt;; lleva un buen tiempo por ahí y está escrito en C y Lua. Es un gran proyecto; échale un vistazo si tienes tiempo. Aunque los procesadores de texto de terminal ya no son populares, muchos escritores todavía los usan por la poca distracción y el enfoque que ofrecen.&lt;/p&gt;

&lt;p&gt;Honestamente, solo me pareció algo padre de construir, y quería algo en lo que trabajar para mí. La meta ambiciosa a largo plazo es convertir a wordNebula en una herramienta completa para escritores: daría formato a la salida según un archivo de configuración o algo similar. La idea es que dramaturgos y guionistas pudieran escribir por completo sin ninguna distracción ni preocuparse por el formato, y que los novelistas pudieran estructurar su trabajo por capítulos. Pero eso es a futuro; por ahora es un proyecto de portafolio que me permite explorar el desarrollo de interfaces de terminal, las estructuras de datos y los patrones de arquitectura en C++.&lt;/p&gt;

&lt;h2&gt;
  
  
  Eligiendo una arquitectura
&lt;/h2&gt;

&lt;p&gt;Para este proyecto decidí desde el principio usar C++ moderno apuntando a C++20. Antes de meterme de lleno en la implementación, mi primera decisión de arquitectura fue elegir un patrón: Modelo-Vista-Controlador (MVC), Modelo-Vista-Presentador (MVP) o el más nuevo Modelo-Vista-ViewModel (MVVM).&lt;/p&gt;

&lt;p&gt;Aquí va una comparación rápida:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspecto&lt;/th&gt;
&lt;th&gt;Modelo-Vista-Controlador&lt;/th&gt;
&lt;th&gt;Modelo-Vista-Presentador&lt;/th&gt;
&lt;th&gt;Modelo-Vista-ViewModel&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mediador&lt;/td&gt;
&lt;td&gt;Controlador&lt;/td&gt;
&lt;td&gt;Presentador&lt;/td&gt;
&lt;td&gt;ViewModel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rol de la Vista&lt;/td&gt;
&lt;td&gt;Activa, interactúa directamente con el Modelo&lt;/td&gt;
&lt;td&gt;Pasiva, pasa los datos al Presentador&lt;/td&gt;
&lt;td&gt;Pasiva, los datos se enlazan al ViewModel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enlace de datos&lt;/td&gt;
&lt;td&gt;Actualizaciones manuales&lt;/td&gt;
&lt;td&gt;Manual, el Presentador envía al Modelo&lt;/td&gt;
&lt;td&gt;Automático mediante data binding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testeabilidad&lt;/td&gt;
&lt;td&gt;Baja&lt;/td&gt;
&lt;td&gt;Buena&lt;/td&gt;
&lt;td&gt;La mejor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Separación&lt;/td&gt;
&lt;td&gt;Básica&lt;/td&gt;
&lt;td&gt;Mejor&lt;/td&gt;
&lt;td&gt;La mejor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ideal para&lt;/td&gt;
&lt;td&gt;Proyectos pequeños&lt;/td&gt;
&lt;td&gt;De medianos a complejos&lt;/td&gt;
&lt;td&gt;Interfaces grandes, orientadas a datos&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Después de investigar estos patrones y estudiar diagramas de bloques, elegí Modelo-Vista-Presentador. La razón principal es que separa por completo las tres responsabilidades:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;El Modelo&lt;/strong&gt; es el búfer de texto. Su único trabajo es mantener el búfer: almacenar el texto, rastrear el cursor y permitir inserciones y eliminaciones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La Vista&lt;/strong&gt; se encarga del diseño de la interfaz, de cómo se muestran los datos y de capturar las teclas que presiona el usuario, las cuales pasa al Presentador.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El Presentador&lt;/strong&gt; es el orquestador. Recibe la entrada de la Vista, pasa las operaciones de texto al Modelo y luego solicita los datos actualizados al Modelo para refrescar la Vista.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Toda la comunicación fluye a través del Presentador. Esta es la razón principal por la que la arquitectura es testeable, y aunque todos sabemos que las pruebas unitarias no son divertidas, entendemos por qué son importantes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manos a la obra
&lt;/h2&gt;

&lt;p&gt;Con la decisión de arquitectura tomada y la separación limpia de responsabilidades dándome la posibilidad de trabajar en distintas partes del proyecto de forma independiente, empecé con una prueba de concepto sencilla. Implementé un búfer de texto básico del Modelo que acepta pulsaciones de teclas y las escribe en un archivo de registro. No era perfecto, pero me sirvió para entender la estructura básica y jugar un poco con MVP antes de comprometerme con la implementación completa.&lt;/p&gt;

&lt;p&gt;Para esto armé lo que llamé un TextBuffer, que era simplemente una cadena a la que le agregaba cosas mediante un Presentador y una Vista sencillos con ncurses. Dejé que Copilot me ayudara a probarlo, y lo que aprendí fue que en realidad debía empezar el proyecto implementando primero el Modelo: es una parte tan crítica del proyecto que me llevó a investigar más sobre cómo los procesadores de texto manejan sus búferes.&lt;/p&gt;

&lt;h2&gt;
  
  
  El Modelo: eligiendo una estructura de datos
&lt;/h2&gt;

&lt;p&gt;Empecé a investigar sobre búferes de texto y vi algunos muy atractivos. Uno era el Gap Buffer, que creo que usa Emacs. Otro era la Piece Table, que usa Microsoft Word, y encontré un blog del equipo de VS Code sobre su uso y sus hallazgos: &lt;a href="https://code.visualstudio.com/blogs/2018/03/23/text-buffer-reimplementation" rel="noopener noreferrer"&gt;Text Buffer Reimplementation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Después de observar esto y entender las diferencias entre ellos, elegí el Gap Buffer para la implementación inicial. Esta decisión se aclaró en cuanto empecé a definir los casos de uso: como se trata de una interfaz basada en terminal, lo más probable es que los usuarios no estén seleccionando y eliminando grandes bloques de texto, así que deshacer y rehacer se pueden implementar mucho después. Y otra diferencia clave respecto a cualquier editor de código moderno: no necesitamos múltiples cursores, solo uno con el que el usuario pueda agregar al documento.&lt;/p&gt;

&lt;p&gt;La idea detrás de un Gap Buffer es sencilla. Tienes un arreglo con un "hueco" (gap) de espacio vacío que sigue al cursor a todos lados. Cuando escribes, vas soltando caracteres en el hueco. Cuando mueves el cursor, el hueco se mueve con él. Así se ve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before: [H][e][l][l][o][___GAP___][W][o][r][l][d]
                        ^ cursor

Insert ',':
After: [H][e][l][l][o][,][__GAP__][W][o][r][l][d]
                           ^ cursor

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El núcleo de la inserción es simple: mueve el hueco hacia el cursor, haz espacio si hace falta y suelta el carácter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;GapBuffer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;insertChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;moveGapToCursor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getGapSize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;expandGap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;gapStart&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;gapStart&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;También fue clave usar clases abstractas de interfaz para asegurarme de poder actualizar el búfer si alguna vez hago una nueva implementación. Esto fue muy importante para mí porque de cara al futuro podríamos necesitar flexibilidad: poder agregar deshacer/rehacer o agregar estilos. Al principio estaba tan inseguro que pensé que tener esa flexibilidad era muy importante.&lt;/p&gt;

&lt;p&gt;La interfaz &lt;code&gt;IBuffer&lt;/code&gt; define el contrato completo que cualquier búfer debe cumplir. Aquí va una versión recortada que muestra las firmas de los métodos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IBuffer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;public:&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;IBuffer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Text Operations&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;insertChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;insertText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;deleteChar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;deleteForward&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Text Access&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;getText&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;getTextRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;getLength&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Cursor Management&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;getCursorPosition&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;setCursorPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;moveCursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Smart Navigation&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;findNextWordBoundary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fromPos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;findPrevWordBoundary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fromPos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;findNextParagraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fromPos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;findPrevParagraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fromPos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Statistics&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;getWordCount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;getParagraphCount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con esta interfaz, si alguna vez necesito cambiar a una Piece Table para soportar deshacer/rehacer avanzado más adelante, puedo hacerlo sin reescribir el Presentador ni la Vista. La interfaz completa con documentación detallada de Doxygen está en el &lt;a href="https://github.com/ileanmjr88/wordNebula" rel="noopener noreferrer"&gt;repositorio&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que el Modelo puede hacer hasta ahora
&lt;/h2&gt;

&lt;p&gt;El Modelo puede agregar texto por completo en la posición del cursor, y el hueco puede crecer de forma dinámica. Soporta navegación completa del cursor, inserción y eliminación de texto, conteo de palabras y conteo de párrafos. También tiene un conjunto de pruebas exhaustivo con Google Test. El Modelo se sostiene por sí solo: se construyó y se probó antes de que el Presentador o la Vista siquiera existieran.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que sigue
&lt;/h2&gt;

&lt;p&gt;En la Parte 2 repasaré el Presentador, que toma el Modelo y orquesta todo: coordina entre el búfer y la interfaz, y gestiona el estado de la aplicación. El Presentador y la Vista ya están implementados; puedes ver el proyecto en &lt;a href="https://github.com/ileanmjr88/wordNebula" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>spanish</category>
      <category>cpp</category>
      <category>datastructures</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
