Nepo's blog 2026-03-10T22:07:18Z Juan Antonio Nepormoseno Rosales urn:uuid:d1ef2b92-d2f5-468c-9b12-9104d4e28b5b *nix trick: desactivar auto-updates de Discord urn:uuid:f65b4de3-0a81-4182-a081-089c493ebdb6 2026-03-10T00:00:00Z 2026-03-10T11:00:00Z Un truco chulo (“neat trick”, “*nix-trick…“) para evitar los updates automáticos de Discord en Linux. <p>Si te mueves en el mundo gamedev o de gaming es bastante probable que tengas que usar la aplicación de Discord. Puede gustarte más o menos, pero al final es donde se encuentra la mayor parte de la comunidad. El problema es que en Linux, cada vez que detecta una nueva versión, pide que te descargues un archivo <code>.deb</code> de su web y que lo instales manualmente. ¡Como si estuviéramos en la prehistoria (es decir, en Windows)!</p> <p>Si sólo te interesa cómo desactivar eso, salta el <a href="#desactivar%20auto-update">apartado de desactivar auto-update</a>.</p> <h2 id="por-qué-es-un-problema-opinión">¿Por qué es un problema? (opinión)</h2> <p>Esto es super molesto porque la filosofía de Linux respecto a instalar el software y actualizarlo es completamente distinta a lo que quieren las empresas. Para empezar, no descargas cosas de su web directamente, sinó de un repositorio de programas gestionado y mantenido (normalmente) por los desarrolladores de tu <a href="https://es.wikipedia.org/wiki/Distribuci%C3%B3n_Linux">distro</a>. Nada de descargar un ejecutable de una web random que hace el setup, eso es super inseguro y por eso Windows tiene mala fama por tener muchos virus. En Linux usamos gestores de paquetes como <code>apt</code> o <code>pacman</code>.</p> <p>Pero la parte más hiriente es que en Linux <strong>tú decides</strong> cuándo instalas y actualizas. ¿Sabéis cuando queréis encender o apagar un PC con Windows/Mac y a veces te toca esperar hasta 30 minutos (¡si no más!) porque Microsoft/Apple decidieron que hoy tocaba update? Eso en Linux no existe. El sistema de avisa de que hay actualizaciones y tú, desde la terminal o un botón visual, decides cuándo vas a actualizarlo.</p> <p>Eso es así en la mayoría de programas, pero eso a Discord no le parece bien, porque es una empresa que quiere tener el mayor control posible de cómo se comporta tu máquina. Cuando abres Discord, este se conecta a un servidor para verificar si hay actualizaciones nuevas. Y si las hay, te avisa de que tienes que actualizar la versión realizando un proceso super manual. Esto es inseguro, igual que en Windows, además de muy molesto. La gente que usamos Linux lo hacemos porque huímos de estas situaciones en las que perdemos el control de nuestro propio hardware. Por eso os comparto cómo desactivar este auto-update.</p> <h2 id="desactivar-auto-update">Desactivar auto-update</h2> <ol type="1"> <li>Localizar la carpeta de configuración de Discord. Normalmente está en <code>~/.config/discord/</code></li> <li>Dentro, hay un fichero <code>settings.json</code>. Ábrelo con tu editor de texto favorito. Si no existe créalo.</li> <li>Añade la línea <code>"SKIP_HOST_UPDATE": true,</code>. Ten en cuenta que es un <a href="https://es.wikipedia.org/wiki/JSON">fichero JSON</a>, así que tiene que estar entre un <code>{</code> y un <code>}</code> que debería estar ya en el fichero.</li> <li>Cierra Discord y vuelve a abrirlo.</li> </ol> <p>Este es mi fichero <code>settings.json</code>, de referencia:</p> <details> <summary> settings.json </summary> <div class="sourceCode" id="cb1"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb1-1"><span class="fu">{</span></span> <span id="cb1-2"> <span class="dt">&quot;BACKGROUND_COLOR&quot;</span><span class="fu">:</span> <span class="st">&quot;#2c2d32&quot;</span><span class="fu">,</span></span> <span id="cb1-3"> <span class="dt">&quot;IS_MAXIMIZED&quot;</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span> <span id="cb1-4"> <span class="dt">&quot;IS_MINIMIZED&quot;</span><span class="fu">:</span> <span class="kw">false</span><span class="fu">,</span></span> <span id="cb1-5"> <span class="dt">&quot;SKIP_HOST_UPDATE&quot;</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span> <span id="cb1-6"> <span class="dt">&quot;WINDOW_BOUNDS&quot;</span><span class="fu">:</span> <span class="fu">{</span></span> <span id="cb1-7"> <span class="dt">&quot;x&quot;</span><span class="fu">:</span> <span class="dv">1880</span><span class="fu">,</span></span> <span id="cb1-8"> <span class="dt">&quot;y&quot;</span><span class="fu">:</span> <span class="dv">0</span><span class="fu">,</span></span> <span id="cb1-9"> <span class="dt">&quot;width&quot;</span><span class="fu">:</span> <span class="dv">1796</span><span class="fu">,</span></span> <span id="cb1-10"> <span class="dt">&quot;height&quot;</span><span class="fu">:</span> <span class="dv">1035</span></span> <span id="cb1-11"> <span class="fu">},</span></span> <span id="cb1-12"> <span class="dt">&quot;chromiumSwitches&quot;</span><span class="fu">:</span> <span class="fu">{},</span></span> <span id="cb1-13"> <span class="dt">&quot;offloadAdmControls&quot;</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span> <span id="cb1-14"> <span class="dt">&quot;enableHardwareAcceleration&quot;</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span> <span id="cb1-15"> <span class="dt">&quot;asyncVideoInputDeviceInit&quot;</span><span class="fu">:</span> <span class="kw">false</span><span class="fu">,</span></span> <span id="cb1-16"> <span class="dt">&quot;enableLibOpenH264Electron&quot;</span><span class="fu">:</span> <span class="kw">false</span></span> <span id="cb1-17"><span class="fu">}</span></span></code></pre></div> </details> <p>Ahora Discord debería de actualizarse sólo cuando lo actualices con el gestor de paquetes de tu distro. Y si no quieres hacer esto, siempre te queda entrar desde el navegador 😛</p> Caso de estudio: Espresso Framework urn:uuid:1040853a-1971-4f74-9a78-d4a7ccd1b0b8 2026-01-21T00:00:00Z 2026-01-21T11:00:00Z Analizamos algunos conceptos que maneja Espresso, un framework de testing para apps Android, para aprender qué herramientas usa. Así podemos implementarlas y utilizarlas en nuestros proyectos. <p>En este artículo analizaremos algunos conceptos que maneja <a href="https://developer.android.com/training/testing/espresso">Espresso</a>, un framework de testing para aplicaciones Android, para escribir y optimizar tests en Android.</p> <p>El objetivo es entender la idea general de estos conceptos, para poder recrearlos en nuestros proyectos. Así podemos añadirlos a nuestra caja de herramientas y usarlos cuando lo necesitemos. ¡Vamos a ello!</p> <blockquote> <p><strong>Nota: </strong> todos los ejemplos de código de este artículo están escritos en Kotlin.</p> </blockquote> <h2 id="qué-es-espresso">¿Qué es Espresso?</h2> <p>Antes de entrar con los conceptos, definamos Espresso. Como dije antes, es un framework de testing para Android. Está pensado para hacer <strong>UI testing</strong>, un tipo de testing de alto nivel en el que interactuamos con la aplicación realizando las acciones que haría el usuario final en la aplicación: tocar botones, leer textos, deslizar la pantalla.</p> <p>Un detalle importante que separa a Espresso de otro tipo de frameworks es que sus tests son de <strong>caja blanca/gris</strong> (al contrario de Selenium/Appium, por ejemplo, que son de caja negra). Esto significa que tenemos acceso al código de la aplicación, por lo que podemos hacer optimizaciones interesantes que nos <strong>facilitan escribir tests</strong> y mejoran su <strong>fiabilidad</strong>.</p> <h3 id="matchers-actions-assertions---bdd">Matchers, Actions, Assertions -&gt; BDD</h3> <p>Son la versión de BDD de Espresso. Match-Act-Assert son lo mismo que Given-When-Then, pero están integrados en el propio framework, por lo que el propio framework te hace organizar tus interacciones con la aplicación como escenarios de BDD.</p> <p>Un detalle chulo de los ViewMatchers y ViewAssertions de Espresso es que usan los <a href="https://hamcrest.org/">matchers de Hamcrest</a>, lo cual nos garantiza el acceso a un lenguaje (<a href="https://en.wikipedia.org/wiki/Domain-specific_language">DSL</a>) super rico y flexible. En los ViewMatchers se usan para encontrar elementos de forma flexible y en las ViewAssertions para hacer comprobaciones sobre ellos. Estos matchers permiten combinarse entre sí para formar expresiones complejas y que dejan clara la intención. Por ejemplo, podemos buscar un elemento por su ID, porque no hay nada más claro que eso, queremos <em>ese</em> elemento en concreto:</p> <div class="sourceCode" id="cb1"><pre class="sourceCode kotlin"><code class="sourceCode kotlin"><span id="cb1-1">onView<span class="op">(</span>withId<span class="op">(</span>R<span class="op">.</span>id<span class="op">.</span>login_button<span class="op">))</span></span></code></pre></div> <p>También podemos encontrarlos basándonos, por ejemplo, en su posición en el DOMTree o su contenido y propiedades. Imaginad que tenemos un mensaje de error que es hijo del botón de login y no tiene ID. Podríamos encontrarlo de esta manera:</p> <div class="sourceCode" id="cb2"><pre class="sourceCode kotlin"><code class="sourceCode kotlin"><span id="cb2-1">allOf<span class="op">(</span></span> <span id="cb2-2"> isDescendantOfA<span class="op">(</span>R<span class="op">.</span>id<span class="op">.</span>login_button<span class="op">),</span></span> <span id="cb2-3"> withText<span class="op">(</span>containsString<span class="op">(</span>R<span class="op">.</span>text<span class="op">.</span>login_error_message<span class="op">))</span></span> <span id="cb2-4"><span class="op">)</span></span></code></pre></div> <p>Fijaos que tenemos acceso a los <em>resource files</em> (<code>R</code>) de Android. ¡Es genial para el testing poder reutilizar las partes que forman la aplicación! Si un developer cambia el texto del error, el test ni se enteraría y seguiría funcionando. Lo cual es genial si no estás verificando ese texto en concreto.</p> <p>Si combinamos esto con un <a href="https://martinfowler.com/bliki/PageObject.html">patrón Page Object</a>, podemos organizar la interacción con la aplicación en una capa abstracta y reutilizable. De esta manera, conseguimos también que nuestros tests no tengan conceptos del framework de testing y no estén acoplados a él:</p> <div class="sourceCode" id="cb3"><pre class="sourceCode kotlin"><code class="sourceCode kotlin"><span id="cb3-1"><span class="kw">class</span> LoginPage <span class="op">{</span></span> <span id="cb3-2"></span> <span id="cb3-3"> <span class="kw">val</span> <span class="va">loginButton</span> <span class="op">=</span> onView<span class="op">(</span>withId<span class="op">(</span>R<span class="op">.</span>id<span class="op">.</span>login_button<span class="op">))</span></span> <span id="cb3-4"> <span class="kw">val</span> <span class="va">errorMessage</span> <span class="op">=</span> allOf<span class="op">(</span></span> <span id="cb3-5"> isDescendantOfA<span class="op">(</span>R<span class="op">.</span>id<span class="op">.</span>login_button<span class="op">,</span></span> <span id="cb3-6"> withText<span class="op">(</span>containsString<span class="op">(</span><span class="st">&quot;error&quot;</span><span class="op">))</span></span> <span id="cb3-7"> <span class="op">)</span></span> <span id="cb3-8"></span> <span id="cb3-9"> <span class="kw">fun</span> <span class="fu">login</span><span class="op">()</span> <span class="op">{</span></span> <span id="cb3-10"> loginButton<span class="op">.</span>click<span class="op">()</span></span> <span id="cb3-11"> <span class="op">}</span></span> <span id="cb3-12"></span> <span id="cb3-13"> <span class="kw">fun</span> <span class="fu">checkHasError</span><span class="op">(</span><span class="va">errorText</span><span class="op">:</span> <span class="dt">String</span><span class="op">)</span> <span class="op">{</span></span> <span id="cb3-14"> errorMessage<span class="op">.</span>check<span class="op">(</span></span> <span id="cb3-15"> allOf<span class="op">(</span></span> <span id="cb3-16"> isDisplayed<span class="op">(),</span></span> <span id="cb3-17"> withText<span class="op">(</span>containsString<span class="op">(</span>errorText<span class="op">))</span></span> <span id="cb3-18"> <span class="op">)</span></span> <span id="cb3-19"> <span class="op">)</span></span> <span id="cb3-20"> <span class="op">}</span></span> <span id="cb3-21"></span> <span id="cb3-22"><span class="op">}</span></span></code></pre></div> <div class="sourceCode" id="cb4"><pre class="sourceCode kotlin"><code class="sourceCode kotlin"><span id="cb4-1"><span class="kw">class</span> LoginTest <span class="op">{</span></span> <span id="cb4-2"></span> <span id="cb4-3"> <span class="kw">val</span> <span class="va">loginPage</span> <span class="op">=</span> LoginPage<span class="op">()</span></span> <span id="cb4-4"></span> <span id="cb4-5"> <span class="kw">fun</span> <span class="fu">testLoginFailsWithoutCredentials</span><span class="op">()</span> <span class="op">{</span></span> <span id="cb4-6"> loginPage<span class="op">.</span>login<span class="op">()</span></span> <span id="cb4-7"> loginPage<span class="op">.</span>checkHasError<span class="op">(</span><span class="st">&quot;enter credentials&quot;</span><span class="op">)</span></span> <span id="cb4-8"> <span class="op">}</span></span> <span id="cb4-9"><span class="op">}</span></span></code></pre></div> <h3 id="idling-resources">Idling Resources</h3> <p>Los <a href="https://developer.android.com/training/testing/espresso/idling-resource">Idling Resources</a> son un mecanismo que tiene Espresso para esperar a que terminen procesos lentos.</p> <p>Siempre digo que hay que evitar <code>wait</code> y <code>sleep</code> con una unidad de tiempo hardcodeada. Esto acaba siendo un <code>magic number</code> que <strong>no explica la intención</strong> que tenemos detrás de esa espera, así que <strong>está condenado a quedar desactualizado</strong> y sin explicación. En lugar de eso, deberíamos esperar de forma explícita a que cambien las condiciones necesarias para seguir ejecutando el test. Por ejemplo, si acabamos de rellenar un formulario de login, no deberíamos esperar 3 o 7 segundos, deberíamos esperar a que cargue la página de bienvenida.</p> <p>Los Idling Resources integran un patrón de multithreading directamente con el framework de test: un <a href="https://en.wikipedia.org/wiki/Semaphore_(programming)">semáforo</a>. Nos dice cuándo la ejecución debe esperar (luz roja 🔴) y cuándo puede continuar (luz verde 🟢).</p> <p>Dicho de otra forma, es un chivato que le dice a Espresso cuándo debe esperarse antes de seguir ejecutando el test. Vamos a ver esto con un ejemplo.</p> <h4 id="ejemplo-cliente-http">Ejemplo: Cliente HTTP</h4> <p>Si nuestra aplicación usa un cliente HTTP, como por ejemplo <a href="https://github.com/square/okhttp">OkHttp</a>, podemos querer detener la ejecución del test mientras se están mandando mensajes al backend (o, mejor aún, a un servidor HTTP mock).</p> <p>En el caso de OkHttp podemos usar el concepto de los <code>Interceptor</code>, una clase que escucha a cada petición-respuesta que hagamos y que nos permite reaccionar a ellas. Esto se vería así cuando creamos la instancia del cliente:</p> <div class="sourceCode" id="cb5"><pre class="sourceCode kotlin"><code class="sourceCode kotlin"><span id="cb5-1"><span class="kw">val</span> <span class="va">httpClient</span><span class="op">:</span> OkHttpClient <span class="op">=</span> OkHttpClient<span class="op">.</span>Builder<span class="op">()</span></span> <span id="cb5-2"> <span class="op">.</span>addInterceptor<span class="op">(</span>IdlingResourceInterceptor<span class="op">())</span></span> <span id="cb5-3"> <span class="op">.</span>build<span class="op">()</span></span></code></pre></div> <p>Y esta sería la clase, ya con su <code>IdlingResource</code> que contará cuántas peticiones faltan por resolver:</p> <div class="sourceCode" id="cb6"><pre class="sourceCode kotlin"><code class="sourceCode kotlin"><span id="cb6-1"><span class="kw">class</span> IdlingResourceInterceptor<span class="op">:</span> <span class="dt">Interceptor</span> <span class="op">{</span></span> <span id="cb6-2"></span> <span id="cb6-3"> <span class="at">@Override</span></span> <span id="cb6-4"> <span class="at">@Throws</span><span class="op">(</span>IOException<span class="op">::</span><span class="kw">class</span>)</span> <span id="cb6-5"> <span class="kw">fun</span> intercept<span class="op">(</span><span class="va">Interceptor</span>.<span class="va">Chain</span> <span class="va">chain</span><span class="op">):</span> <span class="dt">Response</span> <span class="op">{</span></span> <span id="cb6-6"> <span class="co">// A request is received, therefore we increase the count</span></span> <span id="cb6-7"> request<span class="op">:</span> Request <span class="op">=</span> chain<span class="op">.</span>request<span class="op">()</span></span> <span id="cb6-8"> countingIdlingResource<span class="op">.</span>increment<span class="op">()</span></span> <span id="cb6-9"> </span> <span id="cb6-10"> <span class="co">// A request is resolved, therefore we decrease the count</span></span> <span id="cb6-11"> response<span class="op">:</span> Response <span class="op">=</span> chain<span class="op">.</span>proceed<span class="op">(</span>request<span class="op">)</span></span> <span id="cb6-12"> countingIdlingResource<span class="op">.</span>decrement<span class="op">()</span></span> <span id="cb6-13"> </span> <span id="cb6-14"> <span class="kw">return</span> response</span> <span id="cb6-15"> <span class="op">}</span></span> <span id="cb6-16"></span> <span id="cb6-17"> <span class="kw">companion</span> <span class="kw">object</span> <span class="op">{</span></span> <span id="cb6-18"> <span class="kw">val</span> <span class="va">countingIdlingResource</span><span class="op">:</span> CountingIdlingResource <span class="op">=</span> CountingIdlingResource<span class="op">()</span></span> <span id="cb6-19"> <span class="op">}</span></span> <span id="cb6-20"><span class="op">}</span></span></code></pre></div> <p>Fijaos que hemos hecho que el <code>IdlingResource</code> sea estático (en Kotlin esto se hace con el <code>companion object</code>). Esto es porque aún no hemos acabado: para que Espresso sepa a qué <code>IdlingResource</code> hay que esperar, hace falta registrarlos desde la clase del test. Esto lo podemos hacer de esta manera:</p> <div class="sourceCode" id="cb7"><pre class="sourceCode kotlin"><code class="sourceCode kotlin"><span id="cb7-1"><span class="kw">fun</span> <span class="fu">setUp</span><span class="op">()</span> <span class="op">{</span></span> <span id="cb7-2"> Espresso<span class="op">.</span>registerIdlingResource<span class="op">(</span></span> <span id="cb7-3"> IdlingResourceInterceptor<span class="op">.</span>countingIdlingResource</span> <span id="cb7-4"> <span class="op">)</span></span> <span id="cb7-5"><span class="op">}</span></span></code></pre></div> <blockquote> <p><strong>Nota:</strong> Si no queréis estar copiando código de un test a otro, recomiendo usar una clase de Test base que se encargue de registrar todos los <code>IdlingResource</code> necesarios de nuestra aplicación.</p> </blockquote> <p>¡Y bam! Así de fácil los tests esperan a que la aplicación deje de estar ocupada para continuar. En cuanto la aplicación empieza a mandar peticiones por internet, el número se incrementa, deteniendo la ejecución. Espresso sólo seguirá ejecutando las instrucciones del test cuando este número vuelva a bajar a 0.</p> <h3 id="intents-para-lanzar-view-específica">Intents para lanzar View específica</h3> <p>En muchos progamas y juegos nos encontramos con que hay un único punto de entrada. La aplicación empieza en la pantalla principal y hay que seguir un recorrido de pasos para llegar a la vista que queremos probar.</p> <p>Esto es bastante problemático para los tests, porque implica que cuando más profundo en la aplicación esté una vista, más dependencias tiene el test que la verifica. Si tenemos que hacer una prueba de unas opciones escondidas en un menú al que sólo se puede acceder después de un login, el test fallará si cambia el login o la forma de acceder al menú.</p> <p>Espresso utiliza dos conceptos para evitar eso:</p> <ul> <li>Android configuran las vistas de las aplicaciones en <a href="https://developer.android.com/guide/components/activities/intro-activities">Activities</a>. Cada una puede lanzarse por separado y podemos inyectarle los datos necesarios para popularla (por ejemplo, el usuario que ha hecho login).</li> <li>Las <a href="https://junit.org/junit4/javadoc/4.12/org/junit/Rule.html">Rules</a> son una herramienta de JUnit (versión 4, se renombraron a <code>ExtendWith</code> y <code>RegisterExtension</code> en versiones posteriores). Usan reflection para ejecutar código antes y después del test. En concreto, Espresso tiene la <a href="https://developer.android.com/guide/components/activities/testing">ActivityScenarioRule</a>, que lanza una Activity al empezar el test y la cierra al terminarlo.</li> </ul> <p>Aprovechando estas dos ideas, nos queda una sintaxis muy recogida:</p> <div class="sourceCode" id="cb8"><pre class="sourceCode kotlin"><code class="sourceCode kotlin"><span id="cb8-1"><span class="at">@RunWith</span><span class="op">(</span>AndroidJUnit4<span class="op">::</span><span class="kw">class</span>)</span> <span id="cb8-2"><span class="kw">class</span> InstrumentedTest <span class="op">{</span></span> <span id="cb8-3"></span> <span id="cb8-4"> <span class="at">@get</span><span class="op">:</span><span class="at">Rule</span></span> <span id="cb8-5"> <span class="kw">val</span> <span class="va">activityScenarioRule</span> <span class="op">=</span> ActivityScenarioRule<span class="op">(</span>LoginActivity<span class="op">::</span><span class="kw">class</span>.java)</span> <span id="cb8-6"></span> <span id="cb8-7"> @Test</span> <span id="cb8-8"> <span class="kw">fun</span> myTestMethod<span class="op">()</span> <span class="op">{</span></span> <span id="cb8-9"> <span class="co">// ...</span></span> <span id="cb8-10"> <span class="op">}</span></span> <span id="cb8-11"><span class="op">}</span></span></code></pre></div> <p>Declaramos la Rule como campo de clase y así no “ensuciamos” el código de setup/teardown con código para abrir la aplicación. Como norma general, siempre que podáis separar la lógica del test (ej. “hacer login”, “saltar”, “abrir un menú”…) de la lógica del framework (ej. conceptos como “label” o métodos como “sendKeys” en <code>getUserLabel().sendKeys(username)</code>). Igual que con el <a href="https://martinfowler.com/bliki/PageObject.html">Page Object Pattern</a> del que hablábamos antes.</p> <p>Además, al abrir la vista directamente sin tener que pasar por todas las anteriores nos libramos de las dependencias que podrían romper nuestro test. Así conseguimos que el test sea más rápido y menos flaky.</p> <h2 id="cierre">Cierre</h2> <p>No tengo ninguna conclusión, pero quería dejar claro que el post termina aquí y así.</p> <p>Llevaba mucho tiempo queriendo escribir este post, porque trabajé con Espresso durante años y quería compartir mi apreciación por algunas ideas que me parecen buenas. Espero que podáis reutilizar estas ideas en los frameworks de testing de vuestros proyectos.</p> <p>Me planteé escribir otros posts antes para presentar los conceptos poco a poco, pero no tenía ganas de escribir esas guías y por eso lo fui dejando pasar 😄</p> Escribir sobre escribir urn:uuid:7486d88a-746a-4ed1-bc78-144cba26e79e 2026-01-12T00:00:00Z 2026-01-12T11:00:00Z Exploro la escritura y mi relación con ella a través de varios ensayos, videoensayos y obras que me han llegado recientemente. <p>Tengo una relación extraña con escribir y con hacer arte en general. Me gusta mucho pensar en hacerlo, me gusta mucho haberlo hecho y tenerlo publicado, pero lo paso mal durante el proceso. Tengo muchas dudas sobre cómo lo va a interpretar la gente, si van a pensar que soy infantil o que se me da muy mal, me repito que debería ser capaz de hacerlo mejor, llego tarde a las entregas casi siempre y me toca recortar cosas por lo que siento que va a gustar menos y que la culpa es mía por no saber organizarme…</p> <p>Todo eso hace que me cueste mucho empezar a escribir (por eso tengo el blog tan abandonado), pero en las últimas semanas me han llegado varias obras que me han hecho pensar en escribir más. Escribir para resistir, escribir para aprender y escribir para compartir.</p> <h3 id="escribir-como-forma-de-resistencia">Escribir como forma de resistencia</h3> <p>El Internet está muy mal. Ya conocemos la máxima de que cuando algo es gratis es porque nosotras somos el producto. Sumando eso a la centralización masiva del tráfico de internet en las plataformas privadas, tenemos mercados monopolísticos en los que no te queda otra opción que “pasar por el aro” y hacerte cuenta en las redes principales para tener visibilidad.</p> <p>En su ensayo <a href="https://minishinternet.com/2025/10/18/contra-el-algoritmo-cultivar-ideas-en-vez-de-likes/">“Contra el algoritmo: cultivar ideas en vez de likes”</a>, Alba Lafarga nos anima a crear un espacio propio para luchar contra la <a href="https://es.wikipedia.org/wiki/Decadencia_de_plataformas">mierdificación</a> del Internet. Combatir el consumo instantáneo de la FYP y el tweet de “aquí te pillo, aquí te mando” con una comunicación más pausada y exploratoria.</p> <blockquote> <p>Pero, ¿por qué un jardín? Porque implica tiempo, ciclos, transformación. Porque no hay un producto final, sino un proceso de crecimiento. Porque hay que cuidar lo que plantamos, regarlo, podarlo, dejar que algunas ideas mueran y que otras florezcan.</p> <p>— Alba Lafarga (minishinternet.com), <a href="https://minishinternet.com/2025/10/18/contra-el-algoritmo-cultivar-ideas-en-vez-de-likes/">“Contra el algoritmo: cultivar ideas en vez de likes”</a></p> </blockquote> <p>Por ejemplo, para este blog yo tengo unos ficheros en mi ordenador con el contenido de los posts y otros que definen cómo se ve el blog. Puedo dedicarle tiempo a ponerlo bonito, a escribir algo nuevo, a corregir algo antiguo con lo que he aprendido, a reordenar los párrafos para ver si se entiende mejor… mientras que en las redes principales sólo puedes postear y borrar. ¿Cómo podemos pensar en ese “contenido” como nuestro si no tenemos control para modificarlo, ni mucho menos para escoger cómo se presenta?</p> <p>En <a href="https://henry.codes/writing/a-website-to-destroy-all-websites/">“A website to destroy all websites”</a>, Henry Desroches apunta a las herramientas que no están pensadas para escalar, y que sí están diseñadas para mejorar la autonomía y creatividad de sus usuarias, como la forma de resistir a las grandes plataformas. De nuevo, los espacios personales:</p> <blockquote> <p>Schumacher’s concept of “intermediate technology” introduced in his 1973 book <em>Small Is Beautiful: A Study of Economics As If People Mattered</em>, convivial tools are sustainable, energy-efficient (though often labor intensive), local-first, and designed primarily to enhance the autonomy and creativity of their users. Illich cites specifically hand tools, bicycles, and telephones as examples, but with its enormous capacity for interoperability and extensibility, the Internet is the perfect workshed in which to design our own Tools For Conviviality.</p> <p>(…)</p> <p>Hand-coded, syndicated, and above all <em>personal</em> websites are exemplary: They let users of the internet to be autonomous, experiment, have ownership, learn, share, find god, find love, find purpose. Bespoke, endlessly tweaked, eternally redesigned, built-in-public, surprising UI and delightful UX. The personal website is a staunch undying answer to everything the corporate and industrial web has taken from us.</p> <p>— Henry Desroches (henry.codes), <a href="https://henry.codes/writing/a-website-to-destroy-all-websites/">“A website to destroy all websites”</a></p> </blockquote> <p>Yo entendía el concepto de jardín digital como una especie de <em>“patio trasero”</em> privado en el que cuidar de tus plantas y pasar el rato con tus pensamientos. Pero estos artículos me hicieron pensar en ello como en un jardín público, como los jardines de Montjuïc o El Retiro. Lugares diseñados para ser transitados y compartidos. Saliendo de la metáfora, una página en la que mostrar lo que te interesa y te preocupa, y donde hacer comunidad también.</p> <p>Quizá lo que me faltaba entender era la parte de “personal” del concepto “web personal”.</p> <h3 id="escribir-para-aprender">Escribir para aprender</h3> <p>Seguramente conoces esa cita (falsamente atribuída a Einstein) que dice que “si no eres capaz de explicárselo a una criatura de 6 años, no lo entiendes”. Y es que lo vemos en técnicas como el <a href="https://es.wikipedia.org/wiki/M%C3%A9todo_de_depuraci%C3%B3n_del_patito_de_goma">rubber-duck debugging</a>: explicar algo nos permite verlo desde perspectivas distintas y rellenar los huecos que faltan en nuestra comprensión. Cory Doctorow (que acuñó el término “enshitification” del que hablamos antes) escribe sobre escribir:</p> <blockquote> <p>It’s revelatory. It teaches you what you know. It lets you know what you know. It lets you know <em>more</em> than you know. It’s alchemical. It creates new knowledge, and dispels superstition. It sharpens how you think. It sharpens how you talk. And obviously, it sharpens how you write.</p> <p>— Cory Doctorow (pluralistic.net), <a href="https://pluralistic.net/2026/01/07/delicious-pizza/">“Writing vs AI”</a></p> </blockquote> <p>Reiterando en la escritura como una forma de aprender, el youtuber Odysseas habla sobre cómo los ensayos pueden ser una herramienta cuando queremos aprender algo (en el video da consejos sobre cómo empezar a escribir ensayos para vosotras mismas, por si os interesa, pero recomiendo escribir y ver qué os funciona y qué no):</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/DO5MflX_eik?clip=UgkxWVSFIQZiXNaC4sgj5LpoZehPF_80F4yP&amp;clipt=ENjjChjo4Aw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen> </iframe> <p><a href="https://www.youtube.com/clip/UgkxWVSFIQZiXNaC4sgj5LpoZehPF_80F4yP">https://www.youtube.com/clip/UgkxWVSFIQZiXNaC4sgj5LpoZehPF_80F4yP</a></p> <p>Transcripción:</p> <blockquote> <p>Essay writing should be seen more as a problem solving method. Where the process, the means, is more important than the end. You are not writing so you can have a finished essay. (…)</p> <p>The point of writing an essay is to give yourself the chance to think. Because the ideas in you head, they are scattered. They’re unclear. They’re vague. You don’t really know what you know. But as soon as you put them on the paper they become real, they become expressed into the world. And only then you have the perspective to see them and to really judge them properly.</p> <p>— <span class="citation" data-cites="odysseas">@odysseas</span>__ (https://www.youtube.com/<span class="citation" data-cites="odysseas__">@odysseas__</span>), <a href="https://www.youtube.com/watch?v=DO5MflX_eik">“I’m begging you to write essays”</a></p> </blockquote> <p>Creo que en ambos casos es importante mencionar la diferencia entre “aprender por necesidad” y “aprender a tu ritmo/por gusto”. El primero podría ser un ámbito académico, un aprendizaje industrializado en el que hay que cumplir unas cuotas y en el que posiblemente tengas prisas y otras cosas que aprender. ¿Sería útil en este contexto? Quizá, no lo he probado, pero es que ese no me parece que sea un buen contexto para aprender nada.</p> <p>Pero si nos mantenemos con curiosidad y seguimos aprendiendo a lo largo de nuestra vida, escribir sobre lo que aprendemos puede hacernos construir nuestra comprensión para que sea sólida, nos resulte fácil aprender otras cosas en el futuro y que sea lo suficientemente lúcida como para poder compartirla con otra persona.</p> <h3 id="escribir-para-expresarse">Escribir para expresarse</h3> <p>Me ha gustado la escritura desde que era adolescente. Recuerdo que empecé a escribir una historia de fantasía en 2º de la ESO (era horrible), pero un comentario desafortunado de mi madre hizo que parara de escribir y que no lo considerara como una inversión válida de mi tiempo. Acabé pensando que no era algo que necesitara y que era mejor descansar jugando un videojuego o leyendo que tener que justificar porqué quiero dedicarle tiempo a eso.</p> <p>Aún así, la escritura ha sido algo que me ha acompañado toda la vida. De adolescente, unos amigos crearon un foro y empezamos a escribir historias ahí (horribles también). Cuando repetí 2º de bachiller, quedándome en una clase en la que no conocía a nadie, conocí a un grupo maravilloso por un foro de internet que me acompañó durante ese año y muchos más. Acabamos haciendo un club de escritura en el que escribíamos un relato corto de 1-2 páginas cada semana (¡algo que me encantaría recuperar!). Rol por foro, blog posts, narrativa de videojuegos, documentación en el trabajo… ¡parece que sí necesito escribir después de todo!</p> <p>La mayoría de lo que he escrito se ha quedado en privado, en grupos pequeños. Creo que porque la fantasía y escribir para entretener me parecen menos válidos que algo más “académico” como los tutoriales o la divulgación de herramientas y técnicas que vengo haciendo en el blog. ¡Ojo! No es algo que defienda y animo a cualquier persona a que escriba lo que quiera escribir, pero personalmente tengo que luchar contra este juicio cada vez que quiero escribir algo.</p> <p>Por suerte, encontré este video ensayo sobre escritura creativa de <a href="https://www.youtube.com/@WritingwithAndrew"><span class="citation" data-cites="WritingwithAndrew">@WritingwithAndrew</span></a> que me abrió los ojos a otro tipo de escritura:</p> <iframe width="560" height="315" src="https://www.youtube.com/embed/WIP_hLaLnLo?clip=UgkxmziJH2h_UhuaFZdkATCfjMN1nWSQylnf&amp;clipt=ELSCDRi3gBA" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen> </iframe> <p><a href="https://www.youtube.com/clip/UgkxmziJH2h_UhuaFZdkATCfjMN1nWSQylnf">https://www.youtube.com/clip/UgkxmziJH2h_UhuaFZdkATCfjMN1nWSQylnf</a></p> <p>Transcripción:</p> <blockquote> <p>Just as fiction shows us what imagined humans can do in imagined high-stakes scenarios, and poetry shows us how humans experience and feel in response to life, creative non-fiction shows us how another person encounters and processes their experience.</p> <p>Creative non-fiction is what happens when we get an essay about a father digging a hole in the backyard with his son a year into the pandemic. When we get a single devastating paragraph about a man taking his daughter to the pediatrician on the morning of what he calls his last hangover. Or when we get a playful meditation on the migratory habits of the blackpoll warbler.</p> <p>In each case, a lived experience or phenomenon or fact gets filtered through a unique and compelling human perspective. Yeah, the warblers are interesting, but the real show is looking at those warblers through Amy Leach’s eyes.</p> <p>— <span class="citation" data-cites="WritingwithAndrew">@WritingwithAndrew</span> (https://www.youtube.com/<span class="citation" data-cites="WritingwithAndrew">@WritingwithAndrew</span>), <a href="https://www.youtube.com/watch?v=WIP_hLaLnLo">“I’m Politely Begging You to Write Nonfiction”</a></p> </blockquote> <p>Porque a veces <em>qué</em> se cuenta no es tan importante, válido, hermoso y valioso como el <em>cómo</em>. Me sirvió de ejemplo de este tipo de escritura descubrir a <a href="https://xotdoctubre.substack.com/">Xot d’octubre</a>, que escribe cosas preciosas y evocadoras en catalán, y conectar con Juarrín, de <a href="https://lacuevadelosecos.es/">La cueva de los ecos</a>.</p> <p>Todo esto me ha despertado las ganas de escribir de nuevo, lo que ha resultado en este artículo que estás leyendo (¡gracias! ❤️) y en <a href="https://alicia-redmarr.itch.io/pedacitos">un pequeño juego narrativo sobre finales y <em>burnout</em></a>.</p> <p>No quería acabar con ninguna conclusión ni nada. Sólo la promesa (¿o amenaza?) de que seguiré escribiendo. ¡Nos leemos! 👋</p> *nix trick: command wrappers urn:uuid:152ebb82-63f6-4099-87ad-58e717610ca1 2025-11-04T00:00:00Z 2025-11-04T11:00:00Z Un truco chulo (“neat trick”, “*nix-trick…“) para shells de tipo UNIX que permite sobreescribir/extender el comportamiento de comandos existentes sin modificar su código fuente. <p>¡Hola! Lo que me encanta de la terminal es que no hace falta que recuerdes cómo funciona cada comando. Si hay algo que vas a usar 2 veces al mes, lo guardas en un script y usas eso directamente. No tienes que recordar en qué submenú, en qué botón con un icono estaba esa funcionalida. Y lo mejor es que no te van a cambiar con un update, porque son scripts que tienes en tu PC. ¡Es como escribir notas que pueden ejecutarse y validar que siguen funcionando! ❤️‍🔥</p> <p>Hoy quería traeros un truco super sencillo pero super útil. Es una manera de <strong>sobreescribir/extender el comportamiento de comandos existentes en shells de tipo UNIX</strong> (o sea, os funcionará en Linux, Mac y WSL de Windows). Al finalizar el post habremos extendido <code>git diff</code> para poder llamarlo con un parámetro “godot” (es decir, <code>git diff godot</code>), que nos mostrará un diff únicamente de ficheros de escenas y código. Nada de binarios ni ficheros de configuración de importación de assets.</p> <p>Pues si os interesa esto, lo primero que necesitamos es conocer el comando alias.</p> <h3 id="comando-alias">Comando alias</h3> <p>Existe un comando <code>alias</code> con el que puedes decir “cada vez que te diga esto, llama a este comando”. Por ejemplo:</p> <div class="sourceCode" id="cb1"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><span class="bu">alias</span> some=<span class="st">&quot;echo &#39;body once told me the world is gonna roll me&#39;&quot;</span></span></code></pre></div> <p>Con eso, cada vez que escribamos <code>some</code> en la terminal, ejecutará el <code>echo</code> que le hemos puesto después:</p> <pre><code>$ some body once told me the world is gonna roll me</code></pre> <p>Normalmente, los cambios que hacemos con este programa sólo duran mientras la sesión de la shell esté abierta. Eso quiere decir que si cerramos la terminal y la volvemos a abrir <strong>perderemos ese alias</strong> que hemos creado.</p> <p>Para hacer que este cambio sea <strong>permanente</strong> podemos añadir la línea del alias a nuestro fichero <code>.rc</code>. Suele depender de la shell que estemos usando: si es bash estará en <code>~/.bashrc</code>, si es zsh estará en <code>~/.zshrc</code>…</p> <p>Por ejemplo, yo tengo una sección en mi <code>.zshrc</code> para configurar estos aliases:</p> <div class="sourceCode" id="cb3"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb3-1"><span class="co"># ...</span></span> <span id="cb3-2"></span> <span id="cb3-3"><span class="co">####</span></span> <span id="cb3-4"><span class="co"># Aliases</span></span> <span id="cb3-5"><span class="co">####</span></span> <span id="cb3-6"></span> <span id="cb3-7"><span class="bu">alias</span> please=<span class="st">&quot;sudo&quot;</span></span> <span id="cb3-8"><span class="bu">alias</span> open=<span class="st">&quot;xdg-open&quot;</span></span> <span id="cb3-9"><span class="bu">alias</span> code=<span class="st">&quot;codium&quot;</span></span> <span id="cb3-10"></span> <span id="cb3-11"><span class="co"># ...</span></span></code></pre></div> <p>Ahora que ya conocemos qué hace este comando y cómo usarlo, podemos empezar a extender otros programas.</p> <h3 id="explicación-del-caso-de-ejemplo">Explicación del caso de ejemplo</h3> <p>Godot genera algunos archivos como configuraciones de importación de imágenes, audio y modelos 3D (<code>"*.import"</code>) o ficheros binarios de materiales y recursos (<code>"*.res"</code>). La mayoría del tiempo me interesa saber si ha habido cambios en esos ficheros, pero hay momentos en los que sólo quiero ver los cambios que se han hecho en los ficheros de escenas y código (<code>*.tscn</code> y <code>*.gd</code>), por lo que ver los cambios de los ficheros de importación y recursos me molesta.</p> <p>Lo primero que pensé es que <code>git</code> debe tener alguna forma de filtrar los archivos que no me interesan, y así es. El problema es que <strong>es muy largo</strong> como para tener que reescribirlo (¡y acordarme!) cada vez que quiera usarlo:</p> <div class="sourceCode" id="cb4"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb4-1"><span class="fu">git</span> diff <span class="at">--</span> . <span class="st">&#39;:!**/*.import&#39;</span> <span class="st">&#39;:!**/*.res&#39;</span></span></code></pre></div> <p>Podemos añadir un alias para ejecutar esta línea. Por ejemplo, un <code>alias godot-diff</code> o <code>alias git-diff-godot</code> en nuestro achivo <code>.rc</code>. Pero podemos hacerlo aún más bonito con un script que nos haga de wrapper.</p> <h3 id="wrapper-scripts">Wrapper scripts</h3> <p>¿Qué es esto de un script que hace de wrapper? La idea principal es que en lugar de llamar al script como <code>git-diff-godot</code>, podríamos llamarlo <strong>siguiendo la API de git</strong> y hacer un: <code>git diff godot</code>.</p> <p>Esto también nos permite <strong>definir varios casos</strong>. Quizá tenemos un diff distinto para Unity, otro para Unreal, otro para un engine custom… Si simplemente usáramos aliases, deberíamos tener varios (<code>git-diff-unity</code>, <code>git-diff-unreal</code>…), mientras que si hacemos el wrapper script estos serían casos de un switch-case. ¿Y si queremos sobreescribir la funcionalidad de <code>git status</code> o <code>git commit</code>? ¡También podríamos usar nuestro script para detectar los casos en los que tiene que hacer algo distinto!</p> <p>La idea principal es que este script se encargue de 2 cosas: 1. Saber <strong>cuándo</strong> tiene que llamar al <strong>programa original</strong> y cuándo hacer <strong>algo distinto</strong>. 2. <strong>Ejecutar el caso distinto</strong> para el que lo estamos construyendo. En este caso, el diff complicado.</p> <p>Podemos complicarlo mucho, pero para empezar con algo sencillo deberíamos aprender a usar el comando <code>if</code> y el comando <code>case</code> (docs de <a href="https://www.gnu.org/software/bash/manual/html_node/Bash-Conditional-Expressions.html">condiciones</a> e <a href="https://www.gnu.org/software/bash/manual/html_node/Conditional-Constructs.html">ifs y cases</a>). Por ejemplo, con este script podríamos hacer que al llamar a <code>git diff godot</code> se lance el comando que encontramos en la sesión anterior:</p> <div class="sourceCode" id="cb5"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb5-1"><span class="co">#!/usr/bin/env bash</span></span> <span id="cb5-2"></span> <span id="cb5-3"><span class="va">GIT</span><span class="op">=</span><span class="st">&quot;/usr/bin/git&quot;</span> <span class="co"># el comando de git original</span></span> <span id="cb5-4"><span class="va">ARGS</span><span class="op">=</span><span class="va">(</span><span class="st">&quot;</span><span class="va">$@</span><span class="st">&quot;</span><span class="va">)</span> <span class="co"># los parámetros originales</span></span> <span id="cb5-5"></span> <span id="cb5-6"><span class="kw">function</span><span class="fu"> forward_to_git</span> <span class="kw">{</span></span> <span id="cb5-7"> <span class="va">$GIT</span> <span class="st">&quot;</span><span class="va">${ARGS</span><span class="op">[@]</span><span class="va">}</span><span class="st">&quot;</span></span> <span id="cb5-8"><span class="kw">}</span></span> <span id="cb5-9"></span> <span id="cb5-10"><span class="cf">if</span> <span class="bu">[</span> <span class="va">$#</span> <span class="ot">-gt</span> 0 <span class="bu">]</span> <span class="kw">&amp;&amp;</span> <span class="bu">[</span> <span class="st">&quot;</span><span class="va">$1</span><span class="st">&quot;</span> <span class="ot">=</span> <span class="st">&quot;diff&quot;</span> <span class="bu">]</span><span class="kw">;</span> <span class="cf">then</span></span> <span id="cb5-11"> <span class="cf">if</span> <span class="bu">[</span> <span class="va">$2</span> <span class="ot">=</span> <span class="st">&quot;godot&quot;</span> <span class="bu">]</span><span class="kw">;</span> <span class="cf">then</span></span> <span id="cb5-12"> <span class="va">$GIT</span> diff <span class="at">--</span> . <span class="st">&#39;:!**/*.import&#39;</span> <span class="st">&#39;:!**/*.res&#39;</span></span> <span id="cb5-13"> <span class="cf">else</span></span> <span id="cb5-14"> <span class="ex">forward_to_git</span></span> <span id="cb5-15"> <span class="cf">fi</span></span> <span id="cb5-16"><span class="cf">else</span></span> <span id="cb5-17"> <span class="ex">forward_to_git</span></span> <span id="cb5-18"><span class="cf">fi</span></span></code></pre></div> <p>Al ejecutarse este script, pueden darse 3 posibilidades distintas:</p> <ol type="1"> <li>Es el comando git, pero no tiene parámetros (es decir, <code>git</code> a secas; no cumple <code>$# -gt 0</code>) o el primer parámetro no es “diff” (por ejemplo, <code>git status</code>; no cumple <code>"$1" = "diff"</code>). En este caso, llamamos al comando original <code>git</code> y le pasamos los parámetros recibidos.</li> <li>Es el comando git, su primer parámetro es “diff” y su segundo parámetro es la opción que nos hemos inventado, “godot” (es decir, <code>git diff godot</code>). En este caso, llamamos al comando que queremos llamar.</li> <li>Es el comant git, su primer parámetro es “diff”, pero su segundo parámetro no es la opción “godot” (por ejemplo, <code>git diff --staged</code>). En este caso, llamamos al comando <code>git</code> con los parámetros que hemos recibido.</li> </ol> <p><strong>¡Recordad una cosa!</strong> Este script por sí mismo <strong>no hace nada</strong>. Tenemos que ponerle el <code>alias git=$(path hasta el script)</code> en nuestro fichero <code>.rc</code> para que se llame a este script en lugar de al comando de <code>git</code> original.</p> <h3 id="el-wrapper-script-que-uso-yo">El wrapper script que uso yo</h3> <p>El script que uso no es exactamente el que os he compartido. Ese está simplificado para que sea más fácil de seguir. Os dejo aquí el mío por si os da más ideas o por si queréis usarlo de base para empezar a extender git a vuestra manera.</p> <p>¿Qué ideas se os ocurren? ¿Hacer un pretty print para ver el árbol de commits con <code>git log</code>? ¿Comprobar que los archivos están nombrados como queremos antes de hacer un commit? ¿Lanzar los tests antes de cada push? ¿Usarlo con otro comando que os cuesta aprender? ¡Compartidme las ideas que se os ocurran por el Fedi o Discord! 😄</p> <div class="sourceCode" id="cb6"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb6-1"><span class="co">#!/usr/bin/env bash</span></span> <span id="cb6-2"></span> <span id="cb6-3"><span class="co"># This script works by setting up an alias in you .rc file (&quot;~/.bashrc&quot;, &quot;~/.zshrc&quot;...).</span></span> <span id="cb6-4"><span class="co">#</span></span> <span id="cb6-5"><span class="co"># ```bash</span></span> <span id="cb6-6"><span class="co"># alias git $(path_to_this_script)</span></span> <span id="cb6-7"><span class="co"># ```</span></span> <span id="cb6-8"><span class="co">#</span></span> <span id="cb6-9"><span class="co"># That way you can override git&#39;s call to use it like:</span></span> <span id="cb6-10"><span class="co">#</span></span> <span id="cb6-11"><span class="co"># ```bash</span></span> <span id="cb6-12"><span class="co"># git diff godot</span></span> <span id="cb6-13"><span class="co"># ```</span></span> <span id="cb6-14"></span> <span id="cb6-15"><span class="va">GIT</span><span class="op">=</span><span class="st">&quot;/usr/bin/git&quot;</span></span> <span id="cb6-16"><span class="va">ARGS</span><span class="op">=</span><span class="va">(</span><span class="st">&quot;</span><span class="va">$@</span><span class="st">&quot;</span><span class="va">)</span></span> <span id="cb6-17"></span> <span id="cb6-18"><span class="kw">function</span><span class="fu"> forward_to_git</span> <span class="kw">{</span></span> <span id="cb6-19"> <span class="va">$GIT</span> <span class="st">&quot;</span><span class="va">${ARGS</span><span class="op">[@]</span><span class="va">}</span><span class="st">&quot;</span></span> <span id="cb6-20"><span class="kw">}</span></span> <span id="cb6-21"></span> <span id="cb6-22"><span class="kw">function</span><span class="fu"> diff</span> <span class="kw">{</span></span> <span id="cb6-23"> <span class="cf">case</span> <span class="va">$2</span> <span class="kw">in</span></span> <span id="cb6-24"> <span class="st">&quot;godot&quot;</span><span class="kw">)</span></span> <span id="cb6-25"> <span class="va">$GIT</span> diff <span class="at">--</span> . <span class="st">&#39;:!**/*.import&#39;</span> <span class="st">&#39;:!**/*.res&#39;</span></span> <span id="cb6-26"> <span class="cf">;;</span></span> <span id="cb6-27"> <span class="pp">*</span><span class="kw">)</span></span> <span id="cb6-28"> <span class="ex">forward_to_git</span></span> <span id="cb6-29"> <span class="cf">;;</span></span> <span id="cb6-30"> <span class="cf">esac</span></span> <span id="cb6-31"><span class="kw">}</span></span> <span id="cb6-32"></span> <span id="cb6-33"><span class="kw">function</span><span class="fu"> main</span> <span class="kw">{</span></span> <span id="cb6-34"> <span class="cf">if</span> <span class="bu">[</span> <span class="va">$#</span> <span class="ot">-gt</span> 0 <span class="bu">]</span><span class="kw">;</span> <span class="cf">then</span> </span> <span id="cb6-35"> <span class="cf">case</span> <span class="st">&quot;</span><span class="va">$1</span><span class="st">&quot;</span> <span class="kw">in</span></span> <span id="cb6-36"> <span class="st">&quot;diff&quot;</span><span class="kw">)</span></span> <span id="cb6-37"> <span class="fu">diff</span> <span class="va">$*</span></span> <span id="cb6-38"> <span class="cf">;;</span></span> <span id="cb6-39"> <span class="st">&quot;nuke&quot;</span><span class="kw">)</span></span> <span id="cb6-40"> <span class="va">$GIT</span> reset <span class="at">--hard</span> <span class="kw">&amp;&amp;</span> <span class="va">$GIT</span> clean <span class="at">-fd</span></span> <span id="cb6-41"> <span class="cf">;;</span></span> <span id="cb6-42"> <span class="pp">*</span><span class="kw">)</span></span> <span id="cb6-43"> <span class="ex">forward_to_git</span></span> <span id="cb6-44"> <span class="cf">;;</span></span> <span id="cb6-45"> <span class="cf">esac</span></span> <span id="cb6-46"> <span class="cf">else</span></span> <span id="cb6-47"> <span class="ex">forward_to_git</span></span> <span id="cb6-48"> <span class="cf">fi</span></span> <span id="cb6-49"></span> <span id="cb6-50"><span class="kw">}</span></span> <span id="cb6-51"></span> <span id="cb6-52"><span class="ex">main</span> <span class="va">${ARGS</span><span class="op">[@]</span><span class="va">}</span></span></code></pre></div> Tarrey Town en la OST de Breath of the Wild urn:uuid:9c72e7fb-2d94-460a-9488-28603826c8e2 2024-02-04T00:00:00Z 2024-02-04T11:00:00Z Una carta de amor al tema de Tarrey Town en Breath of the Wild y a las bandas sonoras adaptativas. <p>Voy a tener un momento de apreciación muy random con este tema. Quizá ya lo conocéis y sabéis todo lo que voy a decir (se ha hablado mucho de él y llego <em>muy</em> tarde). Pero es que llevo unos días que no dejo de escucharlo y tengo que sacármelo de la cabeza.</p> <iframe class="center youtube-video-size" src="https://www.youtube.com/embed/OdcTAC5LHN4?si=DA1PpLbGkyOBjCpE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen> </iframe> <p>Si habéis jugado a Breath of the Wild y habéis llegado a este pueblo, sabréis que se llama pueblo Tarrey (Tarrey Town a partir de ahora). Al principio suena esa melodía, tranquila pero muy vacía. Hay un tipo construyendo casas y tal. Un poco aburrido, la verdad.</p> <p>El tema es agradable y simpático, y a priori podríamos quedarnos ahí. Pero la gracia de este pueblo es que vas “reclutando” a gente para que se vaya a vivir ahí. Y dependiendo de a quién invites, aparecen nuevos instrumentos.</p> <p>Por ejemplo, el primer personaje que reclutamos es el Goron Greyson, que es un bicho grande y que come piedras (y van siempre desnudos o con taparrabos, por alguna razon). Como es lo más <strong>bruto</strong> que te puedas imaginar, añade un <strong>trombón super estridente</strong> al tema (1:36).</p> <p><img src="$BASE_URL$/imgs/tarrey-town-ost/ocarina-of-time-goron.gif" class="center" /></p> <h2 id="narrativa">Narrativa</h2> <p>Lo más interesante es que, al añadir instrumentos, el propio tema cambia. Al principio la base y el bajo los lleva un piano. Es el mismo instrumento que usan cuando vas paseando por el campo o en caballo. Es majestuoso y al sostener las notas da la sensación de espacio abierto y vacío. Además, no hay ningún otro pueblo (salvo el <a href="https://www.youtube.com/watch?v=cHcLOLJHlMY">pueblo Zora</a>) que incluya un piano (<a href="https://www.youtube.com/watch?v=Uj07-YU5cTk">Hateno</a>, <a href="https://www.youtube.com/watch?v=0Oxz-LmklV4">Kakariko</a>, <a href="https://www.youtube.com/watch?v=oad-1DT5z9I">Gerudo</a>, <a href="https://www.youtube.com/watch?v=8J7dNNPxU4w">Goron City</a>, <a href="https://www.youtube.com/watch?v=2H84NHErkHE">Lurelin</a>), lo cual refuerza que el piano sea el <strong>instrumento que caracteriza el campo abierto</strong>, la naturaleza de Hyrule, o a la propia Hyrule en sí.</p> <p>Pero en cuanto llega el Zora al pueblo, se empieza a llenar. Ese piano desaparece y lo sustituye una <strong>guitarra</strong> con mucha menos reverb (fijaos en el cambio en los bajos alrededor del 6:20). Hace que el tema se vuelva más cálido y cercano. Deja de ser majestuoso y con eco. Ya no es gente reunida en cuatro chozas en el campo, ahora es una <strong>plaza de pueblo bulliciosa</strong>, con mucha gente distinta y de diferentes culturas. Y cada una aporta algo a la canción.</p> <h2 id="voces">Voces</h2> <p>Otra cosa interesante que hace este tema es que los acentos de cada personaje son como una <strong>conversación</strong>. Abre uno y responde otro. Luego sale un tercero. Luego suenan el primero y segundo a la vez, y después les responde el tercero. Es literalmente un “buen día, doña Encarni”, “buen día don Miguel”, “¿pasó usted por mi casa?”…</p> <p><a href="https://www.zeldadungeon.net/musical-musings-building-the-themes-of-tarrey-town-from-the-ground-up/">Este post de Matt Pederberg</a> me ayudó mucho a distinguir las “voces” de cada personaje en la canción, los instrumentos que los representan. Ya hablamos del Goron y el trombón antes, pero hagamos esa relación ahora con el resto de personajes:</p> <ul> <li>El <strong>clarinete</strong> (00:00) que lleva la melodía es la esencia del propio pueblo, del pobre Hudson que está construyendo todo solo.</li> <li>El <strong>trombón</strong> (1:26) es el Goron Greyson, pero también trae un vibráfono.</li> <li>El <strong>dulcimer</strong> (3:12) representa a la Gerudo Rhondson. Es mucho más sutil que los otros instrumentos, representando el carácter aislado de las Gerudo.</li> <li>Hay otro <strong>clarinete</strong> que hace las notas más altas (4:48). Este representa a Fryson, el Rito.</li> <li>La <strong>guitarra</strong> Zora de Kapson reemplaza al piano (6:24) y entran también las <strong>gaitas</strong> de Hateno traidas por Bolson.</li> </ul> <p>Los únicos instrumentos importantes que suenan debajo del resto son:</p> <ul> <li>El <strong>clarinete</strong> que toca la base, la melodía del pueblo.</li> <li>El <strong>piano</strong> que toca el bajo con tanto reverb y que más tarde desaparece.</li> <li>La <strong>guitarra</strong> que reemplaza al piano en el bajo.</li> </ul> <p>Lo cual me parece un poco injusto por el Zora que trae la guitarra, que no tiene su momento de poder hablar y que le hagan caso 😆</p> <h2 id="reactividad">Reactividad</h2> <p>Todo esto se puede percibir sin tener idea de leitmotivs ni <em>movidas chungas</em> de teoría musical, simplemente fijándose en los instrumentos que suenan. Me parece maravilloso y es una de las cosas que más me gustan de todo el juego.</p> <p>La jugadora tiene muchas cosas a hacer en este Hyrule tan grande. Por lo que seguir esta misión acaba siendo <strong>su decisión</strong>, no la del game designer. Quizá en este caso, la reactividad del entorno, estos cambios tan sensoriales como la música y las nuevas casas, se suman con el mundo abierto para reforzar <strong>la agencia de la jugadora</strong>.</p> <p>El videojuego es un medio interactivo y pareciera que a veces nos olvidamos de eso. Hay opciones obligadas que no son realmente una opción, u opciones falsas que no llevan a ningún cambio. O peor, cambios en números, pero ningún cambio real en la experiencia de juego. Es algo completamente normal y hasta deseable. Si tuviéramos que implementar todas las posibilidades que se nos ocurren, el desarrollo de los videojuegos tendría costes inasumibles. Y opino que no serían nada interesantes.</p> <p>Pero por eso me gusta encontrarme y dar reconocimiento a experiencias como esta. Experiencias que deciden dedicarle recursos y tiempo a premiar la agencia de quien las juega. Y que haciendo eso, acaban funcionando tan bien y cuentan una historia tan <em>wholesome</em>.</p> <p>Aunque esto es sólo una excusa para hablar de un tema sonoro tan super bonico como este ❤️</p> Configuración de CI para jams urn:uuid:52ecd52f-1bef-4429-a23f-3fda1aec5855 2024-02-01T00:00:00Z 2024-02-01T11:00:00Z Una explicación de cómo configurar una build y deploys automáticos basada en la que uso en jams, con GitHub Actions y Godot <p>Hace un par de días <a href="https://twitter.com/antimundo21/status/1752454023565705710">liberamos el código</a> de dos juegos que hemos hecho en equipo:</p> <ul> <li><a href="https://github.com/antimundo/rat-and-furrius">Rat and Furrius</a>, para la <a href="https://itch.io/jam/mermelada-jam">Mermelada Jam</a></li> <li><a href="https://github.com/nepo-dev/falda-montana">La Falda de la Montaña</a>, para la <a href="https://itch.io/jam/malagajam-weekend-17">MálagaJam Weekend 17</a></li> </ul> <p>Tiene todas las ñapas que podáis esperar de una jam, pero también hay cosas que os pueden resultar útiles. Entre ellas, un archivo para <strong>exportar</strong> un proyecto de Godot a <strong>web</strong> y <strong>subirlo a itch.io</strong> automáticamente.</p> <p>¿Por qué querrías hacer eso en una jam? Pues porque se hace en 5 minutos y te permite:</p> <ul> <li><strong>No bloquear</strong> a une programadore cada vez que quieras hacer una build.</li> <li><strong>No depender</strong> de une programadore para poder lanzar el juego.</li> <li>Hacer builds <strong>más frecuentes</strong> para hacer <strong>playtesting</strong> de los prototipos más rápido.</li> <li>Asegurarte de que <strong>la build siempre será la misma</strong>. Que no te habrás equivocado al exportar/subir el proyecto.</li> <li>En los últimos 5 minutos de jam, no tienes que estar comprimiendo archivos, yendo a una web y subiéndo archivos. Sólo haces <strong>click en un botón</strong> y esperas relajadamente 🍹</li> </ul> <h2 id="configuración">Configuración</h2> <p>Lo que sigue es sólo una lista de instrucciones rápidas si ya sabes qué hay que hacer. Si no entiendes algo, en la <a href="#guía-de-configuración-en-detalle">guía de configuración que hay más abajo</a> estará explicado 🙂</p> <ol type="1"> <li>Copiar <code>.github/workflows/main.yml</code> a tu repositorio de GitHub.</li> <li>Cambiar los valores de <a href="https://github.com/nepo-dev/falda-montana/blob/07955a0dd83e74703359850c7f6ba298838d4354/.github/workflows/main.yml#L5-L8">estas variables</a>. Si actualizas la versión de Godot, recuerda actualizar <a href="https://github.com/nepo-dev/falda-montana/blob/07955a0dd83e74703359850c7f6ba298838d4354/.github/workflows/main.yml#L15">la versión de la imagen de Docker</a>.</li> <li>Genera una <a href="https://itch.io/user/settings/api-keys">API Key de Itch</a> y añádela como secreto en el repositorio (<code>Settings &gt; Secrets and variables &gt; Repository secrets</code>).</li> <li>Abre el proyecto en Godot y genera la configuración para exportar el proyecto a web (<code>Project &gt; Export... &gt; Add... &gt; Web</code>). Como nombre de esa configuración deja <code>Web</code>, y como export path ponle <code>build/index.html</code>.</li> </ol> <p>Sube los cambios al repositorio y ya está listo para usar.</p> <h2 id="probando-que-funciona">Probando que funciona</h2> <p>Esto puede hacerlo cualquier persona con acceso al repositorio, no sólo les programadores:</p> <ol type="1"> <li>Entra en Actions.</li> <li>Selecciona el workflow <code>Build + Deploy</code>.</li> <li>Haz click en <code>Run workflow &gt; Run workflow</code>.</li> </ol> <video muted autoplay controls loop> <source src="$BASE_URL$/vids/ci-config-para-jams/lanzar_builds.mp4" type="video/mp4"/> </video> <p>Si todo sale bien, en unos minutos os saldrá la ejecución en verde. Y si váis a vuestra página de Itch, os debería aparecer vuestro juego.</p> <p><img src="$BASE_URL$/imgs/ci-config-para-jams/successful_run.png" /></p> <h2 id="y-esto-es-gratis">¿Y esto es gratis?</h2> <p>Hasta cierto límite. Tenéis <a href="https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#included-storage-and-minutes">2.000 minutos de ejecución gratuitos</a> por mes en el plan gratis. Para Godot y para jams, es difícil de alcanzar. Y la ventaja de no depender de/bloquear a une programadore y que siempre se suba el proyecto de la misma manera es considerable.</p> <p><br /></p> <hr /> <p><br /></p> <h2 id="guía-de-configuración-en-detalle">Guía de configuración en detalle</h2> <p>En esta guía se explica con pelos y señales cómo funciona esta automatización, por si tienes alguna duda o por si tienes curiosidad y quieres aprender más 😊</p> <h3 id="copiar-main.yml">Copiar main.yml</h3> <p>Lo primero que tienes que hacer es copiar el fichero <code>.github/workflows/main.yml</code> a tu repositorio en GitHub, dentro de esas mismas carpetas “.github” y “workflows”.</p> <video muted autoplay loop> <source src="$BASE_URL$/vids/ci-config-para-jams/copy-file.mp4" type="video/mp4"/> </video> <p>Esos son unos directorios especiales que GitHub interpreta como una configuración de su <strong>sistema de CI</strong> (Integración Continua), las <strong>GitHub Actions</strong>. Este permite automatizar todo tipo de procesos: desde las builds y subirlo (deploy) a Itch, hasta conversiones de ficheros, ejecución de pruebas, escribir mensajes en Discord/redes…</p> <p>No puedo explicar en este post cómo funciona en detalle. Pero si te interesa aprender más, tienes <a href="https://docs.github.com/en/actions">la documentación oficial</a>. Recomiendo <a href="https://docs.docker.com/guides/get-started/">aprender algo de Docker</a> de antemano para entender cómo se configuran las máquinas en las que se ejecutan estos procesos. Para esta guía sólo nos hace falta saber que una “imagen de Docker” es algo parecido a un ordenador que ya viene con cosas instaladas.</p> <p>¿Y qué es lo que hace este fichero? Pues define los pasos a seguir para generar esa build y subirla a Itch. Si lo abres, verás que tiene unos “steps” definidos. A grandes rasgos, esto es lo que hacen:</p> <ul> <li><strong>Checkout:</strong> se descarga el código del repositorio.</li> <li><strong>Setup:</strong> prepara las export templates de Godot. La imagen de Docker ya incluye esas templates, que a veces pesan ~1GB o más, dependiendo de la plataforma.</li> <li><strong>Web Build:</strong> usa Godot desde la línea de comandos para exportar el proyecto para web.</li> <li><strong>Itch.io Deploy:</strong> sube los ficheros exportados a Itch para que se puedan jugar.</li> </ul> <h3 id="editar-las-variables">Editar las variables</h3> <h4 id="variables-en-main.yml">Variables en main.yml</h4> <p>Una vez copiado, hará falta modificar los valores de <a href="https://github.com/nepo-dev/falda-montana/blob/07955a0dd83e74703359850c7f6ba298838d4354/.github/workflows/main.yml#L5-L8">estas variables</a> en <code>main.yml</code> para que sean los de tu juego:</p> <ul> <li><code>ITCHIO_USERNAME</code> y <code>ITCHIO_GAME</code> son tu nombre y el de tu juego que aparece en la url. Por ejemplo, para <code>https://edearth.itch.io/falda-montana</code> serían <code>edearth</code> y <code>falda-montana</code> respectivamente.</li> <li><code>GODOT_VERSION</code> es la versión de Godot que estés usando. Si la actualizas, tendrás que actualizar también la versión en <a href="https://github.com/nepo-dev/falda-montana/blob/07955a0dd83e74703359850c7f6ba298838d4354/.github/workflows/main.yml#L15">la línea que define la imagen de Docker</a>. Puedes consultar las versiones disponibles en <a href="https://hub.docker.com/r/barichello/godot-ci/tags">este enlace</a>.</li> <li>La variable <code>BUTLER_API_KEY</code> es especial y está definida en otro lugar. No hace falta modificarla aquí. Te lo explico a continuación.</li> </ul> <h4 id="api-key">API Key</h4> <p>Una API Key es una <strong>especie de “contraseña”</strong> que permitirá a esta GitHub Action <strong>usar vuestra cuenta de Itch</strong>. Si te parece peligroso, es porque puede serlo. Por eso no podemos guardarla con el resto de variables, porque tiene que mantenerse en secreto.</p> <p>Para configurarla, accede a <code>Settings &gt; Secrets and variables &gt; Actions &gt; Repository secrets</code> y cread un nuevo secreto. Dentro, copia la <a href="https://itch.io/user/settings/api-keys">API key que generes desde itch</a>.</p> <video muted autoplay controls loop> <source src="$BASE_URL$/vids/ci-config-para-jams/create_secret.mp4" type="video/mp4"/> </video> <p>Esta API Key se puede usar con <a href="https://itch.io/docs/butler/">Butler</a>, la herramienta para línea de comandos de Itch. Permite subir proyectos a Itch de maner super rápida, subiendo sólo los ficheros que se modificaron. Y es justo lo que nuestra automatización hará.</p> <blockquote> <p>Es <strong>MUY importante</strong> que mantengas esta <strong>clave bien segura</strong>. No la pases por Discord, ni WhatsApp, ni ningún sitio. Y si sospechas que se ha filtrado, tienes que ir al <a href="https://itch.io/user/settings/api-keys">dashboard</a> y darle a “revoke” lo antes posible. Si alguien se hace con ella, puede llegar a acceder a tu cuenta de Itch y borrarte los juegos.</p> </blockquote> <h3 id="export-en-godot">Export en Godot</h3> <p>El último paso es decirle a Godot cómo y dónde tiene que generar esa build del juego. Para eso, abre el proyecto en Godot y navega hasta <code>Project &gt; Export... &gt; Add... &gt; Web</code>. Si es la primera vez que vas a exportar tu juego, esta ventana estará vacía como en el video. Al darle al botón de <code>Add...</code> y seleccionar <code>Web</code> se generará la configuración para exportar el proyecto a web.</p> <video muted autoplay controls loop> <source src="$BASE_URL$/vids/ci-config-para-jams/config_export.mp4" type="video/mp4"/> </video> <p>El fichero <code>main.yml</code> está configurado para que la build se exporte a una carpeta específica, por lo que tendrás que configurar el “export path” para que sea <code>build/index.html</code>. Ahí es donde se creará la build. No es por nada que necesite GitHub ni Godot. Es que usé ese directorio en <code>main.yml</code> y es más fácil dejarlo como está que modificarlo cada vez que crees un nuevo proyecto.</p> <p>Una vez hecho esto, ya habrás terminado. ¡Lo único que te queda es <a href="#probando-que-funciona">probar que funciona</a>!</p> Setting up shared folders in Linux urn:uuid:07b39aed-c9e4-41f8-8cb2-79c70579267c 2023-09-11T22:22:00Z 2023-09-11T22:22:00Z Recently I found an article that advocated having separate work and personal users in your laptop to avoid this case. Here’s how to set up shared folders for those users. <p>I find it difficult to concentrate on personal projects when I’m working on my PC. I go to search some documentation online and I see social networks tabs, videos and articles with interesting topics I saved for later… and I lose my focus.</p> <p>Recently I found an article that advocated having separate work and personal users in your laptop to avoid this case. That sounds like a great idea, but I have my Obsidian second brain in my personal user folder and I have Syncthing to share it with other devices. So I need to update it to be accessible for both users.</p> <h2 id="design">Design</h2> <p>I want to create a folder that Syncthing can read and write to. Both personal and work users must be able to read and write to that folder as well. For that, we will use a <strong>user group</strong> shared by Syncthing and the other users.</p> <p><img src="$BASE_URL$/imgs/multi-user-shared-folders/syncthing_shared_folder.png" /></p> <p>Existing files and folders will need to have its <strong>group and permissions updated</strong> to be readable and writable for that user group.</p> <p>We will also need <strong>new files and folders</strong> to be created with specific permissions: - They must be added to the <code>syncthing</code> group. - They must have the read and write permissions for the group.</p> <p>Are there easier ways to do this? Probably, but this is the way I wanted to do this.</p> <h2 id="setting-it-up">Setting it up</h2> <p>Just FYI, you will probably need to use <code>sudo</code> for most of the commands. I removed it to make the code less verbose and redundant. Just use your common sense: if you’re updating permissions for a file/folder not owned by you or you’re creating and assigning groups to users, use <code>sudo</code>.</p> <h3 id="create-group">Create group</h3> <p>With these commands you create a user group called <code>syncthing</code> and add <code>personal</code> and <code>work</code> users to it.</p> <div class="sourceCode" id="cb1"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><span class="ex">groupadd</span> syncthing</span> <span id="cb1-2"><span class="ex">usermod</span> <span class="at">-a</span> <span class="at">-G</span> syncthing personal</span> <span id="cb1-3"><span class="ex">usermod</span> <span class="at">-a</span> <span class="at">-G</span> syncthing work</span></code></pre></div> <p>At this point, you must reboot your computer for these changes to apply. You can verify your user was added to the group when you login with these commands:</p> <div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><span class="fu">groups</span></span> <span id="cb2-2"><span class="fu">id</span></span></code></pre></div> <h3 id="permission-for-existing-files">Permission for existing files</h3> <p>Make the entire folder be owned by the group you just created with:</p> <div class="sourceCode" id="cb3"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb3-1"><span class="fu">chgrp</span> <span class="at">-R</span> syncthing /srv/syncthing</span></code></pre></div> <p>Then update the existing files and folders to have read and write permissions for the group:</p> <div class="sourceCode" id="cb4"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb4-1"><span class="fu">find</span> /srv/syncthing <span class="at">-exec</span> chmod 775 {} +</span></code></pre></div> <h3 id="default-group-permissions-for-new-files-and-folders">Default group permissions for new files and folders</h3> <p>By default, your system might create new files and folders with the write permission for the group disabled. In order to change that, you need to use Access Control Lists (ACLs). They seem complicated and</p> <div class="sourceCode" id="cb5"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb5-1"><span class="co"># Force new files and folders to have the same group as the parent folder</span></span> <span id="cb5-2"><span class="fu">chmod</span> g+s /srv/syncthing</span> <span id="cb5-3"><span class="fu">find</span> /var/www <span class="at">-type</span> d <span class="at">-exec</span> chmod g+s {} + <span class="co"># for subdirectories</span></span> <span id="cb5-4"></span> <span id="cb5-5"><span class="co"># Force new files and folders to have the group permissions set to read and write</span></span> <span id="cb5-6"><span class="ex">setfacl</span> <span class="at">-m</span> <span class="st">&quot;default:group::rw&quot;</span> /srv/syncthing</span> <span id="cb5-7"><span class="fu">find</span> /srv/syncthing <span class="at">-type</span> d <span class="at">-exec</span> setfacl <span class="at">-m</span> d:g::rw {} + <span class="co"># for subdirectories</span></span></code></pre></div> <p>And that should be it.</p> <h2 id="testing-it">Testing it</h2> <p>In my case I just removed the original folder from Syncthing and re-synced again in the new location: <code>/srv/syncthing/Obsidian</code>.</p> <p>After that, I logged in with my personal user, set up Obsidian to use that folder and I can open and write in files. Do the same for the other user and it works too ✅</p> Librarian tools for a more civilized age urn:uuid:07b39aed-c9e4-41f8-8cb2-79c70579267c 2021-06-26T21:42:00Z 2021-06-26T21:42:00Z A thought about digital tools. You wouldn’t trust a plumber that doesn’t know how to use a wrench. As a programmer, do you know your tools? <p>Do you remember how, for a brief moment in human history, you could fix machines by using violence? No university degrees or user manuals required, just a good ole slap o’ the hand to the side or top of the idiot box. And there you go, it works again! This could’ve been caused by a connection error, some faulty wires that didn’t properly transmit the video signal. But the point is you didn’t have to know about that, it used to be a magic solution that sometimes fixed stuff.</p> <p>We don’t have wires anymore, for the most part. Most of our electronics use a PCB with each component attached to it (usually soldered or screwed). They’re so pervasive and easy to produce you could even use them for garage projects! It’s as easy as printing an image, transfering the ink to the board, applying some chemicals and drilling some holes. And they look <em>waaay better</em> than a mess of cables connecting everything.</p> <figure> <img src="$BASE_URL$/imgs/librarian-tools-for-more-civilized-age/cow_tools.jpg" class="center" alt="“Cow tools”, a Far side comic by Gary Larson." /> <figcaption aria-hidden="true">“Cow tools”, a <em>Far side</em> comic by Gary Larson.</figcaption> </figure> <p>While listening to <a href="https://cyberpunklibrarian.com/podcast/cyberpunk-librarian-episode-05-charlie-buckets-dad/">episode 5 (“Charlie Bucket’s dad”)</a> from Daniel Messer’s (<a href="https://twitter.com/bibrarian">@bibrarian</a>) “Cyberpunk Librarian” podcast, I realized our problems are now mostly digital. We’ve figured if we remove the wires we won’t have faulty wires anymore. But that doesn’t mean we fixed the “connection error” problem. In the digital world we could have an API or program failing because we entered the wrong parameters. Or, if we have a long chain of different pieces (which sounds like a generic definition of what a computer or the Internet is), it could be any of those pieces failing.</p> <p>What this means is most of the times we can’t apply a magic solution and hope for the best. Well, turning it off and on again works sometimes, but it won’t fix all problems. We need to really understand the problem in order to fix it. This is captured very well on Julia Evans’ (<a href="https://twitter.com/b0rk">@b0rk</a>) <a href="https://mysteries.wizardzines.com/">debugging adventure games</a>. In these choose-your-own-adventure games you investigate a problem, finding clues like if it were a Phoenix Wright game, until you find out the root cause and fix it. Finding bits of knowledge about a digital problem, that we solve with digital tools.</p> <p>And that’s what concerns this post: tools. Even though “tool” calls up a very physical mental image (like a wrench, hammer or a voltmeter with a display, switch knob and probes), digital problems in digital systems require digital tools to solve. And to use those tools we sometimes need to read documentation, experiment, fix and replace them when they stop working. Just like physical tools!</p> <p>Daniel’s point about familiarizing yourself with your tools is as valid in software development as it is with librarian tools (or physical tools in a physical job 😛). Yet, perhaps because of the amount of tools or their “locality” (eg. Apple ecosystem used just for Apple development), this is a difficult task. Nevertheless, you would be doing yourself a disservice if you don’t. Learning about your tools is a good way to find ways to make your job easier. You stop depending on others to fix things, you grow your knowledge base, you do a more rewarding job and with your own individual background you could find an obvious solution that isn’t obvious to anybody else. Like in the example at the beginning of the post: by understanding the wires were causing the connection problem, we can ask ourselves: can we fix them? Can we replace them with something cheaper that doens’t have that issue?</p> <p>That’s all. This post was mostly an excuse to share work by some people I like. I hope you found it useful or interesting in some way. Be sure to check the work done by Daniel Messer (<a href="https://hackers.town/@CyberpunkLibrarian">@[email protected]</a>) Julia Evans (<a href="https://social.jvns.ca/@b0rk">@[email protected]</a>), it’s really good! 😄</p> On insecurity and self-narrative urn:uuid:7ec3e802-134e-42e9-baf3-9359382f11c7 2021-05-03T00:00:00Z 2021-05-03T11:00:00Z A personal anecdote and a reflection on how our own narrative affects how we feel <p><strong>Be warned:</strong> this post will be quite personal. Back away now if you’d like to avoid cringing 😄</p> <p>Last year I made a small exercise. I was dealing with feelings of inadequacy, since I have a tendency to those. In other words: I often feel insecure. I remembered how a friend of mine told me that I don’t value my successes and I delve too much on my failures. So I decided to get external input and I asked some friends and colleagues for feedback. The questions were:</p> <ul> <li>When was the last time I <em>helped you</em>?</li> <li>In which way / doing what?</li> <li>What are the 3 things <em>you value</em> most from me?</li> </ul> <p>I was surprised by the result: what these people were seeing was nothing like how I saw myself!</p> <p>Where they saw a fun, loyal, unjudging friend that cared about them, I saw a guy with boring conversation, that couldn’t return the affection others showed him. Where they saw honesty, I was painfully aware of all the things I didn’t say in the past to avoid hurting someone or because of fear of judgment. Where they saw a critical-thinking engineer and growth potential, I thought back on all the times I was slow to finish a task or that I had to ask for help/learn something new to do my work (as in “I don’t have the knowledge/level I need to do this”). How could it be <em>they</em> were so wrong? And most importantly: what will they do when they <em>find out</em>?</p> <p>Of course, I knew they all couldn’t be wrong, and that maybe (just maybe) my own self-image was <em>skewed</em>. Since then, I started noticing when I was judging myself and applying a generous serving of <strong>doubt</strong> to those thoughts.</p> <p>This allowed me to clear my mind of those intrusive judgements and focus on the real cause of my insecurity. I stopped accepting those thoughts just as they popped up. Instead I questioned them and thought “what would I tell <strong>someone else</strong> if they were in this position?”. For instance:</p> <ul> <li>When I’m talking with friends and I think “they look bored, I should leave and let them do something they enjoy”, I remind myself they can leave whenever they want, they chose to come and they’re choosing to stay. Maybe I’m wrong to doubt them. I’m sure I’m wrong for trying to decide for them.</li> <li>When at work I don’t understand something and I lose 2h reading documentation, I remind myself this is my first time doing this. No one was born knowing. It isn’t my fault and it’s perfectly fine. I will be better when I’m done reading, and next time I won’t need so much time.</li> <li>When I see successful people and think I haven’t done anything with my life, I remind myself of all the people that keep in touch with me, of my career and how much it has made me grow, of all those little projects after work, of all the things I <em>do</em> know instead of the ones I don’t, of this little blog… all those things <em>I</em> care about.</li> </ul> <p>It might seem like I’m constantly defusing bombs in my head, but far from it. Refuting the negative judgements slowly improved my self-image. I was more kind to myself and so the thoughts appeared less frequently. In time it got easier. My fears became smaller and easier to manage. I still have them, but at least I’m not struggling with them. And lately gratitude has been replacing them. Gratitude for friends spending time with me, or for being fortunate enough to learn at work.</p> <p>If you’re struggling with insecurities yourself, try avoiding judgement and find out where that fear comes from. Then you can figure out (on your own or with other people’s help) if those things are true or not. The story we tell ourselves has a <em>huge</em> impact on how we feel and what we do. Don’t let your fears dictate the narrative. You can tell a much better story! 😉</p> <p>I saved the notes from when I asked friends and colleagues for feedback. I just saw them today and they brought a smile to my face. If you do something similar, they might bring one to yours in the future 🙂</p> Painless responsive CSS: water, flexboxes and grids urn:uuid:cac2f2f8-8116-4d7d-bbb5-566c178bd7a8 2021-01-31T00:00:00Z 2021-01-31T00:00:00Z A quickstart guide on how to style the layout of a website while keeping it simple. <p>When I started this blog I had no idea of how to write modern CSS, nor did I know the slightest thing about how to make a website responsive. I started making a stylesheet with lots of minute details, like setting multiple margins in pixels for all classes, organizing the layout based on those margins, using weird tricks to center divs, etc.</p> <p>Only when I tried making the site responsive I realized I had a huge problem. I tried to fix it, but in the end <a href="https://github.com/nepo-dev/nepo.dev/blob/b88d46ff96841cf4d30a7b84bca2f09c33bc6cb9/edearth.css">this untenable mess</a> had to go and I had to start from the ground up.</p> <p>In this post I share some of the lessons I learned and a recommendation to use some tools to save time.</p> <h2 id="desing-for-mobile-first">Desing for mobile first</h2> <p>One of the simple but important lessons I learned is to design the site for mobile first, rather than desktop. Since mobile is more restrictive, due to its limited horizontal space, it lends itself to linear layouts. This forces you to keep your layout simple, so you’ll be making decisions about what to show or what to keep, instead of focusing on how you want to display it.</p> <p>This step takes place before you even start writing the first line of HTML or CSS, so it should be quick to iterate until you find a design you’re comfortable with.</p> <h2 id="responsive-elements-water.css">Responsive elements: water.css</h2> <p>A friend introduced me to this wonderful tool. It’s a ready-made collection of styles that make HTML elements look good while keeping them responsive. To put it simply: instead of painstakingly going over every element on your site and writing CSS to make sure they scale well, you can just re-use someone else’s CSS!</p> <p>You only need to do a couple of things to start using it. You want to make your website aware of the device’s resolution with the <code>viewport</code> tag and import the <code>water.css</code> stylesheet. Like this:</p> <div class="sourceCode" id="cb1"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb1-1"><span class="dt">&lt;</span><span class="kw">head</span><span class="dt">&gt;</span></span> <span id="cb1-2"> <span class="dt">&lt;</span><span class="kw">meta</span><span class="ot"> name</span><span class="op">=</span><span class="st">&quot;viewport&quot;</span><span class="ot"> content</span><span class="op">=</span><span class="st">&quot;width=device-width, initial-scale=1.0&quot;</span><span class="dt">&gt;</span></span> <span id="cb1-3"> <span class="dt">&lt;</span><span class="kw">link</span><span class="ot"> rel</span><span class="op">=</span><span class="st">&quot;stylesheet&quot;</span><span class="ot"> href</span><span class="op">=</span><span class="st">&quot;https://cdn.jsdelivr.net/npm/water.css@2/out/water.css&quot;</span><span class="dt">&gt;</span></span> <span id="cb1-4"><span class="dt">&lt;/</span><span class="kw">head</span><span class="dt">&gt;</span></span></code></pre></div> <p>This is just an example. There are other tools similar to this one out there. Tailwind, Materialize or Foundation for Apps might be the right thing for you. But these are frameworks and they require some learning. If what you want is <a href="https://youtu.be/q2gN6_alzVQ">taking it easy</a>, just use <code>water.css</code>.</p> <h2 id="responsive-layout-flexbox-and-grid-layout">Responsive layout: flexbox and grid layout</h2> <p>In my initial search for modern responsive techniques, I came across this video by Una Kravets (<a href="https://twitter.com/una">@una</a>) where she talks about flexbox and grid layouts.</p> <iframe class="center youtube-video-size" src="https://www.youtube-nocookie.com/embed/qm0IfG1GyZU" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen> </iframe> <p>Watch it from beginning ’til end, it’s worth your time. There is also <a href="https://1linelayouts.glitch.me/">this website version</a> in case you want to play around with the result or you want a convenient cheat sheet.</p> <p>As a quick takeaway: if you need to position two elements in a row and you need them to scale, you can create a parent element with <code>display: flex;</code>. Then you define what percentage those elements will take with <code>flex: 50%</code>. The flexbox will then take care of scaling them for you, and if you add <code>flex-wrap: wrap;</code> to the parent, it will position them in a new row when they can’t fit in the screen.</p> <div class="sourceCode" id="cb2"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb2-1"><span class="dt">&lt;</span><span class="kw">div</span><span class="ot"> style</span><span class="op">=</span><span class="st">&quot;display: flex;&quot;</span><span class="dt">&gt;</span></span> <span id="cb2-2"> <span class="dt">&lt;</span><span class="kw">p</span><span class="ot"> style</span><span class="op">=</span><span class="st">&quot;flex: 60%;&quot;</span><span class="dt">&gt;</span></span> <span id="cb2-3"> This column takes 60% of the available space.</span> <span id="cb2-4"> <span class="dt">&lt;/</span><span class="kw">p</span><span class="dt">&gt;</span></span> <span id="cb2-5"> <span class="dt">&lt;</span><span class="kw">p</span><span class="ot"> style</span><span class="op">=</span><span class="st">&quot;flex: 30%;&quot;</span><span class="dt">&gt;</span></span> <span id="cb2-6"> This one just 30%.</span> <span id="cb2-7"> <span class="dt">&lt;/</span><span class="kw">p</span><span class="dt">&gt;</span></span> <span id="cb2-8"><span class="dt">&lt;/</span><span class="kw">div</span><span class="dt">&gt;</span></span></code></pre></div> <div style="display: flex;"> <p style="flex: 70%; background-color: khaki;"> This column takes 70% of the available space. </p> <p style="flex: 30%; background-color: palevioletred;"> This one just 30%. </p> </div> <h2 id="conditions-media-queries">Conditions: media queries</h2> <p>There are some cases where you want to show something just on desktop. Or maybe you need to move an element to the next row when browsing on mobile. You can do that with media queries.</p> <p>The idea behind them is easy to understand. You can think of them as simple conditions. Is the screen bigger than 500px? Is the site being browsed in a mobile device in portrait mode? Then apply these CSS rules.</p> <details> <summary> Example </summary> <div class="sourceCode" id="cb3"><pre class="sourceCode css"><code class="sourceCode css"><span id="cb3-1"><span class="im">@media</span> <span class="fu">(</span><span class="kw">min-width</span><span class="ch">:</span> <span class="dv">500</span><span class="dt">px</span><span class="fu">)</span> {</span> <span id="cb3-2"> <span class="co">/*the style for this class will only be applied when the screen&#39;s width is 500px or more*/</span></span> <span id="cb3-3"> <span class="fu">.normal-css</span> {</span> <span id="cb3-4"> <span class="kw">background-color</span><span class="ch">:</span> <span class="cn">white</span><span class="op">;</span></span> <span id="cb3-5"> <span class="kw">color</span><span class="ch">:</span> <span class="cn">black</span><span class="op">;</span></span> <span id="cb3-6"> }</span> <span id="cb3-7">}</span> <span id="cb3-8"></span> <span id="cb3-9"><span class="im">@media</span> <span class="fu">(</span><span class="kw">orientation</span><span class="ch">:</span> <span class="dv">portrait</span><span class="fu">)</span> {</span> <span id="cb3-10"> <span class="co">/*the style for this class will only be applied when the device is in portrait mode*/</span></span> <span id="cb3-11"> <span class="fu">.normal-css</span> {</span> <span id="cb3-12"> <span class="kw">background-color</span><span class="ch">:</span> <span class="cn">white</span><span class="op">;</span></span> <span id="cb3-13"> <span class="kw">color</span><span class="ch">:</span> <span class="cn">black</span><span class="op">;</span></span> <span id="cb3-14"> }</span> <span id="cb3-15">}</span></code></pre></div> </details> <p>An example of how to use it in combination with the flexbox layout is to position a horizontal line of elements vertically. In the following example the boxes will be positioned vertically, since they’re set to fill 100% of the flexbox with <code>flex: 100%;</code>. But if the viewport is bigger than 500px they’ll be positioned side by side, since they’ll have the property <code>flex: 50%;</code>.</p> <p>Check the following example in a mobile device and in a desktop:</p> <div> <style> .example { flex: 100%; } @media (min-width: 500px) { .example { flex: 50%; } } </style> <div style="background-color: lightblue; width: 100%;"> <div style="display: flex; flex-wrap: wrap;"> <p class="example" style="background-color: khaki;"> This column takes all horizontal space when the container is too small. </p> <p class="example" style="background-color: palevioletred;"> So this one is being pushed to the next row. </p> </div> </div> </div> <details> <summary> Example </summary> <p>CSS:</p> <div class="sourceCode" id="cb4"><pre class="sourceCode css"><code class="sourceCode css"><span id="cb4-1"><span class="fu">.example</span> {</span> <span id="cb4-2"> <span class="kw">flex</span><span class="ch">:</span> <span class="dv">100</span><span class="dt">%</span><span class="op">;</span></span> <span id="cb4-3">}</span> <span id="cb4-4"></span> <span id="cb4-5"><span class="im">@media</span> <span class="fu">(</span><span class="kw">min-width</span><span class="ch">:</span> <span class="dv">500</span><span class="dt">px</span><span class="fu">)</span> {</span> <span id="cb4-6"> <span class="fu">.example</span> {</span> <span id="cb4-7"> <span class="kw">flex</span><span class="ch">:</span> <span class="dv">50</span><span class="dt">%</span><span class="op">;</span></span> <span id="cb4-8"> }</span> <span id="cb4-9">}</span></code></pre></div> <p>HTML:</p> <div class="sourceCode" id="cb5"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb5-1"><span class="dt">&lt;</span><span class="kw">div</span><span class="ot"> style</span><span class="op">=</span><span class="st">&quot;display: flex; flex-wrap: wrap;&quot;</span><span class="dt">&gt;</span></span> <span id="cb5-2"> <span class="dt">&lt;</span><span class="kw">p</span><span class="ot"> class</span><span class="op">=</span><span class="st">&quot;example&quot;</span><span class="dt">&gt;</span></span> <span id="cb5-3"> This column takes all horizontal space when the container is too small.</span> <span id="cb5-4"> <span class="dt">&lt;/</span><span class="kw">p</span><span class="dt">&gt;</span></span> <span id="cb5-5"> <span class="dt">&lt;</span><span class="kw">p</span><span class="ot"> class</span><span class="op">=</span><span class="st">&quot;example&quot;</span><span class="dt">&gt;</span></span> <span id="cb5-6"> So this one is being pushed to the next row.</span> <span id="cb5-7"> <span class="dt">&lt;/</span><span class="kw">p</span><span class="dt">&gt;</span></span> <span id="cb5-8"><span class="dt">&lt;/</span><span class="kw">div</span><span class="dt">&gt;</span></span></code></pre></div> </details> <hr /> <p>That’s all for now. I hope you found some of this useful!</p> API discovery in Android Apps and task automation urn:uuid:dcab5a71-98f5-45df-93ba-1db36cdead74 2020-11-06T00:00:00Z 2020-11-06T00:00:00Z On how to use a Man-in-the-Middle proxy to reverse engineer an otherwise hidden Android App API, and what to do with that. This is not a post about app development. <p>Have you ever felt frustrated when using a mobile app because of any of the following reasons?</p> <ul> <li>It lacks simple quality of life features like filtering, searching, automating actions, etc.</li> <li>It is poorly optimized and it is extremely laggy. Or maybe it takes too long to load.</li> <li>It doesn’t show a lot of information on screen, since images and buttons have to be BIG for a mobile app. Or the complete opposite, the interactable elements are too small or don’t work well. The UX is terrible in both cases.</li> <li>It takes up too much space on the phone’s storage.</li> </ul> <p>If you can relate with any of those reasons, I’m right there with you. You could ditch that application, but if you really want to use it, I’ve got good news: there is a way around it. It involves learning about cyber-security, reverse engineering an API and automating nearly anything that matters in an app.</p> <h3 id="overview">Overview</h3> <blockquote> <p><strong>Summary:</strong> We will perform a MITM attack by installing a custom CA in an Android emulator and scripting the API requests we intercept. If you understand that, go to the <a href="#setting-up-emulator-and-app">next section</a> already.</p> </blockquote> <p>Normally we can’t see the traffic coming out of an App. It’s <strong>encrypted</strong>, and therefore it looks like random garbage to us. This keeps your information safe, at least during the transaction (what each ends does with the information and how they protect it is a different matter).</p> <p>You might have heard about <strong>digital certificates</strong>. They are what make this encryption possible, and in order to know what certificates you can trust or not your devices depend on a <strong>Certificate Authority</strong> (CA). These are actors that can create digital certificates</p> <p>But anyone can <em>be</em> a CA, or they can create one at least, so how do we know which CAs can we trust or not? Usually, your devices/applications come with a hardcoded set of CAs they trust by default. This is a <em>good enough</em> measure that <em>mostly works</em>, although these CAs then become very high value targets for hackers and you can believe some have been compromised before.</p> <p>So, back to our little project. We want to be able to snoop into what our target app and their backend are talking about. In order to do that, we are going to force an Android emulator into sending all its network traffic through us and then make it think it can trust us. How we will do that is by installing our own certificate as one of those default CAs. This way, we will be able to understand the encrypted HTTPS messages any app sends and receives. This is what is known as a <strong>“Man in the Middle attack”</strong> (MITM) in cyber-security.</p> <figure> <img src="$BASE_URL$/imgs/reverse-engineer-android-app-api/mitm_diagram.jpg" alt="Drawing of man in the middle attack" /> <figcaption aria-hidden="true">Drawing of man in the middle attack</figcaption> </figure> <p>When we are able to understand what app and backend are saying to each other, we will begin investigating the now exposed API. If we’re lucky, we might even have the chance to automate our daily app usage into a script. That way we won’t even have to open the app again.</p> <p>This post will be split in 3 parts:</p> <ul> <li><a href="#setting-up-emulator-and-app">Setting up the emulator</a></li> <li><a href="#setting-up-the-proxy">Configuring MITM proxy</a></li> <li><a href="#exploitation">Investigating the API and automating</a></li> </ul> <h2 id="setting-up-emulator-and-app">Setting up emulator and app</h2> <h3 id="install-the-tools">Install the tools</h3> <p>If you don’t have the Android platform tools, we will install them now. I’m using Arch, so I will use these AUR packages:</p> <ul> <li><a href="https://aur.archlinux.org/android-sdk.git">https://aur.archlinux.org/android-sdk.git</a></li> <li><a href="https://aur.archlinux.org/android-sdk-platform-tools.git">https://aur.archlinux.org/android-sdk-platform-tools.git</a></li> </ul> <p>You can install them like:</p> <div class="sourceCode" id="cb1"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb1-1"><span class="fu">git</span> clone https://aur.archlinux.org/android-sdk.git</span> <span id="cb1-2"><span class="bu">cd</span> android-sdk</span> <span id="cb1-3"><span class="ex">makepkg</span> <span class="at">-si</span></span> <span id="cb1-4"><span class="bu">cd</span> ..</span> <span id="cb1-5"><span class="fu">git</span> clone https://aur.archlinux.org/android-sdk-platform-tools.git</span> <span id="cb1-6"><span class="bu">cd</span> android-sdk-platform-tools</span> <span id="cb1-7"><span class="ex">makepkg</span> <span class="at">-si</span></span></code></pre></div> <p>It shouldn’t be too difficult to find out how to install them on another platform. For instance, if you’re on Ubuntu, you can just do:</p> <div class="sourceCode" id="cb2"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb2-1"><span class="fu">sudo</span> apt update <span class="kw">&amp;&amp;</span> <span class="fu">sudo</span> apt install android-sdk</span></code></pre></div> <p>On Mac (assuming you have <a href="https://brew.sh/">Brew</a> installed):</p> <div class="sourceCode" id="cb3"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb3-1"><span class="ex">brew</span> tap caskroom/cask</span> <span id="cb3-2"><span class="ex">brew</span> cask install android-sdk</span></code></pre></div> <h3 id="install-the-system-image">Install the system image</h3> <p>Once we have the tools we need, we can download the system image. Since we want to download an app from the Play Store, we will need that the image we use includes the Google API. We want to find a an image containing “google_apis”, but not “google_apis_playstore”. Why not get the one that comes with the Play Store pre-installed, you ask? Well, those are production builds and we won’t be able to root them. We will need rooting our emulator later.</p> <p>I chose the “android-25” one, but you can choose another one if you want.</p> <div class="sourceCode" id="cb4"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb4-1"><span class="ex">sdkmanager</span> <span class="at">--list</span> <span class="kw">|</span> <span class="fu">grep</span> <span class="st">&quot;google_apis&quot;</span> <span class="co">#select another image from this list if you want</span></span> <span id="cb4-2"><span class="fu">sudo</span> sdkmanager <span class="at">--install</span> <span class="st">&quot;system-images;android-25;google_apis;x86_64&quot;</span></span></code></pre></div> <h3 id="create-the-avd">Create the AVD</h3> <p>After accepting the license and waiting for the download to finish, we can finally create our AVD.</p> <div class="sourceCode" id="cb5"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb5-1"><span class="ex">avdmanager</span> create avd <span class="at">--name</span> <span class="st">&quot;mitm-emulator&quot;</span> <span class="at">--package</span> <span class="st">&quot;system-images;android-25;google_apis;x86_64&quot;</span></span></code></pre></div> <p>And open it with:</p> <div class="sourceCode" id="cb6"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb6-1"><span class="ex">emulator</span> <span class="at">-avd</span> mitm-emulator <span class="kw">&amp;</span></span></code></pre></div> <h3 id="installing-google-play">Installing Google Play</h3> <p>Download Open GApps for your system image from <a href="https://opengapps.org/">https://opengapps.org/</a>. We will need them to download the target app from the Play Store. We can extract it with:</p> <div class="sourceCode" id="cb7"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><span class="fu">unzip</span> open_gapps-<span class="pp">*</span>.zip <span class="st">&#39;Core/*&#39;</span></span> <span id="cb7-2"><span class="fu">rm</span> Core/setup<span class="pp">*</span></span> <span id="cb7-3"><span class="ex">lzip</span> <span class="at">-d</span> Core/<span class="pp">*</span>.lz</span> <span id="cb7-4"><span class="cf">for</span> f <span class="kw">in</span> <span class="va">$(</span><span class="fu">ls</span> Core/<span class="pp">*</span>.tar<span class="va">)</span><span class="kw">;</span> <span class="cf">do</span></span> <span id="cb7-5"> <span class="fu">tar</span> <span class="at">-x</span> <span class="at">--strip-components</span> 2 <span class="at">-f</span> <span class="va">$f</span></span> <span id="cb7-6"><span class="cf">done</span></span></code></pre></div> <p>Then, we run our emulator and install the packages:</p> <div class="sourceCode" id="cb8"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb8-1"><span class="ex">emulator</span> <span class="at">-avd</span> <span class="st">&quot;mitm-emulator&quot;</span> <span class="at">-writable-system</span> <span class="kw">&amp;</span></span></code></pre></div> <p>Wait for the loading to finish. As soon as the home screen is shown, copy the Open GApps folders to your system.</p> <div class="sourceCode" id="cb9"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb9-1"><span class="ex">adb</span> root</span> <span id="cb9-2"><span class="ex">adb</span> remount</span> <span id="cb9-3"><span class="ex">adb</span> push etc /system</span> <span id="cb9-4"><span class="ex">adb</span> push framework /system</span> <span id="cb9-5"><span class="ex">adb</span> push app /system</span> <span id="cb9-6"><span class="ex">adb</span> push priv-app /system</span></code></pre></div> <p>We can now restart the emulator.</p> <div class="sourceCode" id="cb10"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb10-1"><span class="ex">adb</span> shell stop</span> <span id="cb10-2"><span class="ex">adb</span> shell start</span></code></pre></div> <p>After the loading finishes, you will see the Play Store in your home screen.</p> <h3 id="install-the-target-app">Install the target app</h3> <p>This should be the easiest step. Just log into the Play Store and download the target app.</p> <p>Make sure you can open it before proceeding.</p> <h2 id="setting-up-the-proxy">Setting up the proxy</h2> <h3 id="install-mitm-proxy">Install MITM proxy</h3> <p>Now we will install and configure our proxy. We will be using <strong><a href="https://mitmproxy.org">mitmproxy</a></strong>, an MIT licensed open source tool built just for MITM attacks.</p> <p>It can easily be installed from Arch’s official repositories with:</p> <div class="sourceCode" id="cb11"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb11-1"><span class="ex">pacman</span> <span class="at">-Sy</span> mitmproxy</span></code></pre></div> <p>It seems on Ubuntu you will need to install pip and then install mitmproxy using it.</p> <div class="sourceCode" id="cb12"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb12-1"><span class="fu">sudo</span> apt install python3-pip</span> <span id="cb12-2"><span class="fu">sudo</span> pip3 install mitmproxy</span></code></pre></div> <p>On Mac, just use Brew again:</p> <div class="sourceCode" id="cb13"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb13-1"><span class="ex">brew</span> install mitmproxy</span></code></pre></div> <p>To test it out, let’s start the emulator and open its settings by clicking on the 3 dots button. Then, navigate to the <strong>settings</strong> section, select the <strong>Proxy tab</strong> and configure it to use <code>http://127.0.0.1:8080</code> as a proxy, like in the image below.</p> <figure> <img src="$BASE_URL$/imgs/reverse-engineer-android-app-api/emulator_proxy_config.png" alt="a screenshot shows the emulator proxy configured to use http://127.0.0.1:8080" /> <figcaption aria-hidden="true">a screenshot shows the emulator proxy configured to use http://127.0.0.1:8080</figcaption> </figure> <p>Run mitmproxy listening in 127.0.0.1:8080 in a terminal, like this:</p> <div class="sourceCode" id="cb14"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb14-1"><span class="ex">mitmproxy</span> <span class="at">--listen-host</span> 127.0.0.1 <span class="at">--listen-port</span> 8080</span></code></pre></div> <p>We can now go back to the emulator and navigate anywhere. You will see it’s almost as if your device lost connection, and if you try to use a web browser, a warning about your connection not being private will appear.</p> <figure> <img src="$BASE_URL$/imgs/reverse-engineer-android-app-api/mitm_no_cert00.png" class="center" style="height:400px;" alt="a screenshot shows chrome browser warning the user its connection is not private" /> <figcaption aria-hidden="true">a screenshot shows chrome browser warning the user its connection is not private</figcaption> </figure> <p>This happens because mitmproxy signs HTTPS traffic with its own certificate. Since the emulator doesn’t trust that certificate (yet), it won’t even accept that response! If you go to the terminal where you launched mitmproxy, you should see something like this. Notice all the traffic is HTTP, there are no HTTPS messages.</p> <figure> <img src="$BASE_URL$/imgs/reverse-engineer-android-app-api/mitm_no_cert01.png" alt="a screenshot shows a get request to http google, redirected to https" /> <figcaption aria-hidden="true">a screenshot shows a get request to http google, redirected to https</figcaption> </figure> <figure> <img src="$BASE_URL$/imgs/reverse-engineer-android-app-api/mitm_no_cert02.png" alt="a screenshot shows more detail on the get request to http google" /> <figcaption aria-hidden="true">a screenshot shows more detail on the get request to http google</figcaption> </figure> <p>To continue, we will need to make the emulator trust mitmproxy’s Certificate Authority.</p> <h3 id="install-the-certificate-authority">Install the Certificate Authority</h3> <p>The way mitmproxy works is it has its own Certificate Authority. It uses it to generate certificates on the fly for whatever external resource the emulator asks for. It then uses those certificates to encrypt the messages sent to it, making it seem like the emulator is talking with the real server. In reality, though, it is decrypting and encrypting all the messages with its own certificates, so it knows <strong>everything</strong> the client and server are talking about. And neither of them knows it is spying on them.</p> <p>If you read the documentation for mitmproxy you will see there are official instructions on how to install the certificate on mobile devices: you visit “<code>mitm.it</code>” in the browser, download the certificate and install it. Then you can see decrypted HTTPS traffic in mitmproxy… but just the traffic generated by the browser.</p> <p>If we want to read HTTPS traffic from native apps, we need to install it as a <strong>system certificate</strong>. We will need root privileges for that, so we will launch the emulator specifying:</p> <div class="sourceCode" id="cb15"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb15-1"><span class="ex">emulator</span> <span class="at">-avd</span> mitm-emulator <span class="at">-writable-system</span> <span class="kw">&amp;</span></span> <span id="cb15-2"><span class="ex">adb</span> root</span> <span id="cb15-3"><span class="ex">adb</span> remount</span></code></pre></div> <p>Now we need to find our certificate file. If you are using Linux, it will be in the <code>~/.mitmproxy/</code> folder. Let’s save it to a variable named “CA”:</p> <div class="sourceCode" id="cb16"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb16-1"><span class="va">CA</span><span class="op">=</span>~/.mitmproxy/mitmproxy-ca-cert.pem</span></code></pre></div> <p>We just need to copy that file into the emulator’s trusted CAs folder, but it needs to be named with a special value. The filename must be the hash of the certificate itself. You can calculate it with the following command:</p> <div class="sourceCode" id="cb17"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb17-1"><span class="va">HASH</span><span class="op">=</span><span class="va">$(</span><span class="ex">openssl</span> x509 <span class="at">-noout</span> <span class="at">-subject_hash_old</span> <span class="at">-in</span> <span class="st">&quot;</span><span class="va">$CA</span><span class="st">&quot;</span><span class="va">)</span></span></code></pre></div> <p>But since mitmproxy uses the same default certificate everywhere, we know the value should be <code>c8750f0d</code>, so you can skip this step and just use that value. You can always come back and use the previous line to recalculate the value if it doesn’t work 😛</p> <p>So you can just do:</p> <div class="sourceCode" id="cb18"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb18-1"><span class="ex">adb</span> push <span class="st">&quot;</span><span class="va">$CA</span><span class="st">&quot;</span> /system/etc/security/cacerts/c8750f0d.0</span></code></pre></div> <p>If you try to load a website now, it should load correctly and you should see traffic being detected by mitmproxy. Moreover, if you try to open the target app, you should see its traffic too! Congratulations!</p> <p>Don’t forget to unroot the device before you continue.</p> <div class="sourceCode" id="cb19"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb19-1"><span class="ex">adb</span> unroot</span></code></pre></div> <h2 id="exploitation">Exploitation</h2> <h3 id="investigating-the-api">Investigating the API</h3> <p>To begin, let’s just use the app normally. Create an account with username and password (avoid authenticating with Google or Facebook for now, you don’t want to have to deal with OAuth). Log in, search and browse some items… We just want to generate some traffic so that we can inspect it later.</p> <p>Now we can go back to our terminal and check the messages mitmproxy caught. We have to locate the traffic from the target app. To do so, just browse the captured requests list and see if any URL matches the app’s domain. Alternatively, you could look for endpoints that match actions you performed (like a login, search, etc.).</p> <p>If you’re lucky, your target API will use a human-readable document format like JSON or XML, instead of protobuf. We are lucky and our target uses JSON.</p> <figure> <img src="$BASE_URL$/imgs/reverse-engineer-android-app-api/response_censored.png" alt="a screenshot shows how an intercepted response looks in mitmproxy" /> <figcaption aria-hidden="true">a screenshot shows how an intercepted response looks in mitmproxy</figcaption> </figure> <p>This means we can easily replicate that request in the terminal.</p> <p>In the terminal running mitmproxy, enter <code>w</code>. You will enter export mode. If you then type <code>export.clip curl @focus</code>, your request will be replicated as a curl command and it will be copied to your clipboard. You can then paste it on your terminal to see if it works.</p> <p>For my particular case I had to make 2 changes for it to work:</p> <ul> <li>Remove the <code>:authority</code> pseudo-header that looked like <code>-H ':authority: domainname.com'</code>.</li> <li>Change the IP address for it’s domain name in the URL (<code>https://1.2.3.4/v1/endpoint</code> =&gt; <code>https://domainname.com/v1/endpoint</code>).</li> </ul> <h3 id="automating-queries">Automating queries</h3> <p>From this point on, it’s just a matter of exploring and seeing what can you do with the endpoints you discover. It’s just like learning any regular new API.</p> <p>For example, I wanted to automate a search for a restaurant. It has to be near me (&lt;1km), it cannot be a bakery and I just want to know the name, price and pickup time.</p> <div class="sourceCode" id="cb20"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb20-1"><span class="kw">function</span><span class="fu"> get_store_list()</span> <span class="kw">{</span></span> <span id="cb20-2"> <span class="ex">curl</span> https://x.x.x.x/get_store_list<span class="pp">?</span>...</span> <span id="cb20-3"><span class="kw">}</span></span> <span id="cb20-4"></span> <span id="cb20-5"><span class="va">result</span><span class="op">=</span><span class="va">$(</span><span class="ex">get_store_list</span><span class="dt">\</span></span> <span id="cb20-6"><span class="kw">|</span> <span class="ex">jq</span> <span class="st">&#39;.groupings[].items[]&#39;</span> <span class="dt">\\</span> <span class="co"># get all stores</span></span> <span id="cb20-7"><span class="kw">|</span> <span class="ex">jq</span> <span class="st">&#39;select(.distance &lt; 1)&#39;</span> <span class="dt">\\</span> <span class="co"># filter out stores further than 1 km away</span></span> <span id="cb20-8"><span class="kw">|</span> <span class="ex">jq</span> <span class="st">&#39;select(.item.item_category != &quot;BAKED_GOODS&quot;)&#39;</span> <span class="dt">\\</span> <span class="co"># filter out unwanted store categories</span></span> <span id="cb20-9"><span class="kw">|</span> <span class="ex">jq</span> <span class="st">&#39;(.store.store_name + &quot;, &quot;</span></span> <span id="cb20-10"><span class="st">+ (.item.price.minor_units/100 | tostring) + &quot;€, &quot;</span></span> <span id="cb20-11"><span class="st">+ .pickup_interval.start)&#39;</span> <span class="dt">\\</span> <span class="co"># print just the wanted data: store name, price and pickup time</span></span> <span id="cb20-12"><span class="kw">|</span> <span class="fu">sort</span> <span class="kw">|</span> <span class="fu">uniq</span><span class="va">)</span> <span class="co"># sort and show only unique results</span></span> <span id="cb20-13"></span> <span id="cb20-14"><span class="ex">notify-send</span> <span class="at">-t</span> 30000 <span class="st">&quot;</span><span class="va">$result</span><span class="st">&quot;</span> <span class="co"># send a notification in Linux desktop</span></span> <span id="cb20-15"><span class="co"># or</span></span> <span id="cb20-16"><span class="ex">termux-notification</span> <span class="at">--content</span> <span class="st">&quot;</span><span class="va">$result</span><span class="st">&quot;</span> <span class="co"># send a notification in Android&#39;s Termux</span></span></code></pre></div> <p>This shows a simple list like this one as a notification:</p> <pre class="csv"><code>&quot;Store name 1, 3.99€, 2020-11-04T19:00:00Z&quot; &quot;Store name 2, 4.99€, 2020-11-04T15:00:00Z&quot; &quot;Store name 3, 4.99€, 2020-11-04T15:00:00Z&quot; &quot;Store name 4, 2.99€, 2020-11-04T19:00:00Z&quot; ...</code></pre> <p>It can be then saved as a script and run as a cron job everyday at certain time (lunchtime?). This will notify me about available stores to get food from.</p> <p>Do once. Run forever. Ok, run until the API changes or something breaks, but still, it’s less worrisome than opening the app and searching manually.</p> <h2 id="links-of-interest">Links of interest</h2> <p>If you want to know more about this topic, I encourage you to follow these links and search for more information.</p> <ul> <li>Setting up mitmproxy for Android emulator, Jonathan Lipps (2019 Apr 3): <a href="https://appiumpro.com/editions/63-capturing-android-emulator-network-traffic-with-appium">link</a></li> <li>Installing Open GApps, Daishi Kato (2017 Mar 6): <a href="https://medium.com/@dai_shi/installing-google-play-services-on-an-android-studio-emulator-fffceb2c28a1">link</a></li> <li>Why you need a Google API image, “oenpelli” on StackOverflow (2014 Jul 18): <a href="https://stackoverflow.com/a/24817495">link</a></li> <li>mitmproxy docs: <a href="https://docs.mitmproxy.org/stable/">link</a></li> </ul> A universal document language urn:uuid:708e86b3-a0ce-4bab-b9d7-312c602478f8 2020-08-12T00:00:00Z 2020-08-12T00:00:00Z On how to use Markdown and Pandoc to generate documents in multiple formats (HTML, PDF, DOC, PPT, ODT…) <p>I love Markdown. It’s a simple to use, minimalist markup language. It’s completely text based, so it’s very lightweight and it works extremely well when used with a VCS like Git. Here is an example:</p> <div class="sourceCode" id="cb1"><pre class="sourceCode md"><code class="sourceCode markdown"><span id="cb1-1"><span class="co">---</span></span> <span id="cb1-2"><span class="an">title:</span><span class="co"> &#39;My document&#39;</span></span> <span id="cb1-3"><span class="an">author:</span></span> <span id="cb1-4"><span class="co">* John Doe</span></span> <span id="cb1-5"><span class="co">---</span></span> <span id="cb1-6"></span> <span id="cb1-7">Paragraph</span> <span id="cb1-8"></span> <span id="cb1-9"><span class="ss">* </span>List item 1</span> <span id="cb1-10"><span class="ss">* </span>List item 2</span> <span id="cb1-11"></span> <span id="cb1-12"><span class="fu">## Section 2</span></span> <span id="cb1-13"></span> <span id="cb1-14">Another paragraph</span></code></pre></div> <p>And not only can you use it for text files, you can use it to generate other types of documents as well.</p> <h2 id="converting-documents">Converting documents</h2> <p>There are multiple tools to achieve this, but today I want to talk about <strong><a href="https://pandoc.org/">Pandoc</a></strong>. Pandoc is a <strong>universal document converter</strong>. It allows converting from and to different formats, like from Open/Libre Office’s ODT to Microsoft Word’s DOCX or viceversa. From word processor formats like the previous 2, to HTML, PDF, Slides, Wiki, TeX… and the best of all: it allows converting to and from <strong>Markdown</strong>.</p> <p>Why is this good? Well, you can write a document in Markdown and maintain a local git repository for it, so you can keep a <strong>history</strong> of the changes you made. And then you can <strong>convert</strong> it to a <strong>PDF/Word</strong> format, so a colleague can read your notes in visual form. With the same file, you can even convert it to a <strong>slideshow</strong> format, so you can present it in front of people. Need to add this info in a web page? You just convert it to <strong>HTML</strong>. It is <em>extremely</em> flexible. And if you already take notes in Markdown, the process to adapt your notes to another format/document is trivially easy and quick.</p> <div class="sourceCode" id="cb2"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb2-1"><span class="co"># Markdown to ODT</span></span> <span id="cb2-2"><span class="ex">pandoc</span> document.md <span class="at">-o</span> </span> <span id="cb2-3"></span> <span id="cb2-4"><span class="co"># Markdown to PDF (using xelatex)</span></span> <span id="cb2-5"><span class="ex">pandoc</span> document.md <span class="at">--pdf-engine</span><span class="op">=</span>xelatex <span class="at">-o</span> document.pdf</span> <span id="cb2-6"></span> <span id="cb2-7"><span class="co"># Markdown to HTML</span></span> <span id="cb2-8"><span class="co"># -s (for standalone) is optional, used to generate tags </span></span> <span id="cb2-9"><span class="co"># like the headers, body, etc. and make a whole HTML file</span></span> <span id="cb2-10"><span class="ex">pandoc</span> document.md <span class="at">-s</span> <span class="at">-o</span> document.html</span> <span id="cb2-11"></span> <span id="cb2-12"><span class="co"># Markdown to slideshow (using Beamer)</span></span> <span id="cb2-13"><span class="ex">pandoc</span> document.md <span class="at">-t</span> beamer <span class="at">-o</span> slides.pdf</span></code></pre></div> <p>Not convinced yet? What if I told you you could automate the styling of the document? That would allow you to <strong>focus on the content</strong> and leave the formatting to Pandoc. That can be easily achieved with a custom template.</p> <h1 id="writing-your-own-template">Writing your own template</h1> <p>Writing your own template is easy to do. Let’s take an HTML template as an example. We start by making a dummy HTML:</p> <div class="sourceCode" id="cb3"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb3-1"><span class="dt">&lt;</span><span class="kw">html</span><span class="dt">&gt;</span></span> <span id="cb3-2"> <span class="dt">&lt;</span><span class="kw">head</span><span class="dt">&gt;</span></span> <span id="cb3-3"> <span class="dt">&lt;</span><span class="kw">link</span><span class="ot"> rel</span><span class="op">=</span><span class="st">&quot;stylesheet&quot;</span><span class="ot"> type</span><span class="op">=</span><span class="st">&quot;text/css&quot;</span><span class="er">...</span></span> <span id="cb3-4"><span class="ot"> </span><span class="er">...</span></span> <span id="cb3-5"><span class="ot"> </span><span class="er">&lt;/</span><span class="ot">head</span><span class="dt">&gt;</span></span> <span id="cb3-6"> <span class="dt">&lt;</span><span class="kw">body</span><span class="dt">&gt;</span></span> <span id="cb3-7"> <span class="dt">&lt;</span><span class="kw">h1</span><span class="dt">&gt;</span>Title<span class="dt">&lt;/</span><span class="kw">h1</span><span class="dt">&gt;</span></span> <span id="cb3-8"> <span class="dt">&lt;</span><span class="kw">p</span><span class="dt">&gt;</span>Lorem ipsum...<span class="dt">&lt;/</span><span class="kw">p</span><span class="dt">&gt;</span></span> <span id="cb3-9"> <span class="dt">&lt;/</span><span class="kw">body</span><span class="dt">&gt;</span></span> <span id="cb3-10"><span class="dt">&lt;/</span><span class="kw">html</span><span class="dt">&gt;</span></span></code></pre></div> <p>Then we can use the default template as a reference to start building our own. You can use the <code>-D</code> option to get the standard template for a format, like this:</p> <div class="sourceCode" id="cb4"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb4-1"><span class="ex">pandoc</span> <span class="at">-D</span> html <span class="op">&gt;</span> template.html</span></code></pre></div> <p>From the get go, we see properties like “author-meta”, “date-meta” or “keywords”. To keep this post short I’ll just stick to the “title” and “body”, but you can <a href="https://pandoc.org/MANUAL.html#variables">read the official documentation</a> if you want to see all you can do with it. You can also use conditionals, loops and embed templates on other templates, if you need to do something more complex. But let’s go back to our example.</p> <p>To use these properties in our document, we can use either of two formats:</p> <ul> <li><code>$prop$</code></li> <li><code>${prop}</code></li> </ul> <p>So to add the title and the content of our document to our HTML template, we just need to do:</p> <div class="sourceCode" id="cb5"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb5-1"><span class="dt">&lt;</span><span class="kw">html</span><span class="dt">&gt;</span></span> <span id="cb5-2"> <span class="dt">&lt;</span><span class="kw">head</span><span class="dt">&gt;</span></span> <span id="cb5-3"> <span class="dt">&lt;</span><span class="kw">link</span><span class="ot"> rel</span><span class="op">=</span><span class="st">&quot;stylesheet&quot;</span><span class="ot"> type</span><span class="op">=</span><span class="st">&quot;text/css&quot;</span><span class="er">...</span></span> <span id="cb5-4"><span class="ot"> </span><span class="er">...</span></span> <span id="cb5-5"><span class="ot"> </span><span class="er">&lt;/</span><span class="ot">head</span><span class="dt">&gt;</span></span> <span id="cb5-6"> <span class="dt">&lt;</span><span class="kw">body</span><span class="dt">&gt;</span></span> <span id="cb5-7"> <span class="dt">&lt;</span><span class="kw">h1</span><span class="dt">&gt;</span>${title}<span class="dt">&lt;/</span><span class="kw">h1</span><span class="dt">&gt;</span></span> <span id="cb5-8"> <span class="dt">&lt;</span><span class="kw">p</span><span class="dt">&gt;</span>${body}<span class="dt">&lt;/</span><span class="kw">p</span><span class="dt">&gt;</span></span> <span id="cb5-9"> <span class="dt">&lt;/</span><span class="kw">body</span><span class="dt">&gt;</span></span> <span id="cb5-10"><span class="dt">&lt;/</span><span class="kw">html</span><span class="dt">&gt;</span></span></code></pre></div> <p>And, using the example Markdown document at the beginning of the article, the resulting HTML would look like:</p> <div class="sourceCode" id="cb6"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb6-1"><span class="dt">&lt;</span><span class="kw">html</span><span class="dt">&gt;</span></span> <span id="cb6-2"> <span class="dt">&lt;</span><span class="kw">head</span><span class="dt">&gt;</span></span> <span id="cb6-3"> <span class="dt">&lt;</span><span class="kw">link</span><span class="ot"> rel</span><span class="op">=</span><span class="st">&quot;stylesheet&quot;</span><span class="ot"> type</span><span class="op">=</span><span class="st">&quot;text/css&quot;</span><span class="er">...</span></span> <span id="cb6-4"><span class="ot"> </span><span class="er">...</span></span> <span id="cb6-5"><span class="ot"> </span><span class="er">&lt;/</span><span class="ot">head</span><span class="dt">&gt;</span></span> <span id="cb6-6"> <span class="dt">&lt;</span><span class="kw">body</span><span class="dt">&gt;</span></span> <span id="cb6-7"> <span class="dt">&lt;</span><span class="kw">h1</span><span class="dt">&gt;</span>My document<span class="dt">&lt;/</span><span class="kw">h1</span><span class="dt">&gt;</span></span> <span id="cb6-8"> <span class="dt">&lt;</span><span class="kw">p</span><span class="dt">&gt;</span>Paragraph<span class="dt">&lt;/</span><span class="kw">p</span><span class="dt">&gt;</span></span> <span id="cb6-9"> <span class="dt">&lt;</span><span class="kw">ul</span><span class="dt">&gt;</span></span> <span id="cb6-10"> <span class="dt">&lt;</span><span class="kw">li</span><span class="dt">&gt;</span>List item 1<span class="dt">&lt;/</span><span class="kw">li</span><span class="dt">&gt;</span></span> <span id="cb6-11"> <span class="dt">&lt;</span><span class="kw">li</span><span class="dt">&gt;</span>List item 2<span class="dt">&lt;/</span><span class="kw">li</span><span class="dt">&gt;</span></span> <span id="cb6-12"> <span class="dt">&lt;/</span><span class="kw">ul</span><span class="dt">&gt;</span></span> <span id="cb6-13"> <span class="dt">&lt;</span><span class="kw">h2</span><span class="dt">&gt;</span>Section 2<span class="dt">&lt;/</span><span class="kw">h2</span><span class="dt">&gt;</span></span> <span id="cb6-14"> <span class="dt">&lt;</span><span class="kw">p</span><span class="dt">&gt;</span>Another paragraph<span class="dt">&lt;/</span><span class="kw">p</span><span class="dt">&gt;</span></span> <span id="cb6-15"> <span class="dt">&lt;/</span><span class="kw">body</span><span class="dt">&gt;</span></span> <span id="cb6-16"><span class="dt">&lt;/</span><span class="kw">html</span><span class="dt">&gt;</span></span></code></pre></div> <p>The best part is that you don’t even need to write your own template. You can just search for more on the Internet. There is a selection of them in <a href="https://github.com/jgm/pandoc/wiki/User-contributed-templates">Pandoc’s official repository</a>.</p> <h1 id="conclusion">Conclusion</h1> <p>The key takeaway from this post is this: Markdown emphasizes focusing on the content, not the style. Pandoc allows us to keep these two tasks separated. By using it, we can write our documents in Markdown and rely on our template to apply the style in the end, automatically.</p>