Nepo's blog2026-03-10T22:07:18ZJuan Antonio Nepormoseno Rosalesurn:uuid:d1ef2b92-d2f5-468c-9b12-9104d4e28b5b*nix trick: desactivar auto-updates de Discordurn:uuid:f65b4de3-0a81-4182-a081-089c493ebdb62026-03-10T00:00:00Z2026-03-10T11:00:00ZUn 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">"BACKGROUND_COLOR"</span><span class="fu">:</span> <span class="st">"#2c2d32"</span><span class="fu">,</span></span>
<span id="cb1-3"> <span class="dt">"IS_MAXIMIZED"</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span>
<span id="cb1-4"> <span class="dt">"IS_MINIMIZED"</span><span class="fu">:</span> <span class="kw">false</span><span class="fu">,</span></span>
<span id="cb1-5"> <span class="dt">"SKIP_HOST_UPDATE"</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span>
<span id="cb1-6"> <span class="dt">"WINDOW_BOUNDS"</span><span class="fu">:</span> <span class="fu">{</span></span>
<span id="cb1-7"> <span class="dt">"x"</span><span class="fu">:</span> <span class="dv">1880</span><span class="fu">,</span></span>
<span id="cb1-8"> <span class="dt">"y"</span><span class="fu">:</span> <span class="dv">0</span><span class="fu">,</span></span>
<span id="cb1-9"> <span class="dt">"width"</span><span class="fu">:</span> <span class="dv">1796</span><span class="fu">,</span></span>
<span id="cb1-10"> <span class="dt">"height"</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">"chromiumSwitches"</span><span class="fu">:</span> <span class="fu">{},</span></span>
<span id="cb1-13"> <span class="dt">"offloadAdmControls"</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span>
<span id="cb1-14"> <span class="dt">"enableHardwareAcceleration"</span><span class="fu">:</span> <span class="kw">true</span><span class="fu">,</span></span>
<span id="cb1-15"> <span class="dt">"asyncVideoInputDeviceInit"</span><span class="fu">:</span> <span class="kw">false</span><span class="fu">,</span></span>
<span id="cb1-16"> <span class="dt">"enableLibOpenH264Electron"</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 Frameworkurn:uuid:1040853a-1971-4f74-9a78-d4a7ccd1b0b82026-01-21T00:00:00Z2026-01-21T11:00:00ZAnalizamos 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
-> 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">"error"</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">"enter credentials"</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 escribirurn:uuid:7486d88a-746a-4ed1-bc78-144cba26e79e2026-01-12T00:00:00Z2026-01-12T11:00:00ZExploro 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&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&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 wrappersurn:uuid:152ebb82-63f6-4099-87ad-58e717610ca12025-11-04T00:00:00Z2025-11-04T11:00:00ZUn 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">"echo 'body once told me the world is gonna roll me'"</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">"sudo"</span></span>
<span id="cb3-8"><span class="bu">alias</span> open=<span class="st">"xdg-open"</span></span>
<span id="cb3-9"><span class="bu">alias</span> code=<span class="st">"codium"</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">':!**/*.import'</span> <span class="st">':!**/*.res'</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">"/usr/bin/git"</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">"</span><span class="va">$@</span><span class="st">"</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">"</span><span class="va">${ARGS</span><span class="op">[@]</span><span class="va">}</span><span class="st">"</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">&&</span> <span class="bu">[</span> <span class="st">"</span><span class="va">$1</span><span class="st">"</span> <span class="ot">=</span> <span class="st">"diff"</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">"godot"</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">':!**/*.import'</span> <span class="st">':!**/*.res'</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 ("~/.bashrc", "~/.zshrc"...).</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'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">"/usr/bin/git"</span></span>
<span id="cb6-16"><span class="va">ARGS</span><span class="op">=</span><span class="va">(</span><span class="st">"</span><span class="va">$@</span><span class="st">"</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">"</span><span class="va">${ARGS</span><span class="op">[@]</span><span class="va">}</span><span class="st">"</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">"godot"</span><span class="kw">)</span></span>
<span id="cb6-25"> <span class="va">$GIT</span> diff <span class="at">--</span> . <span class="st">':!**/*.import'</span> <span class="st">':!**/*.res'</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">"</span><span class="va">$1</span><span class="st">"</span> <span class="kw">in</span></span>
<span id="cb6-36"> <span class="st">"diff"</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">"nuke"</span><span class="kw">)</span></span>
<span id="cb6-40"> <span class="va">$GIT</span> reset <span class="at">--hard</span> <span class="kw">&&</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 Wildurn:uuid:9c72e7fb-2d94-460a-9488-28603826c8e22024-02-04T00:00:00Z2024-02-04T11:00:00ZUna 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 jamsurn:uuid:52ecd52f-1bef-4429-a23f-3fda1aec58552024-02-01T00:00:00Z2024-02-01T11:00:00ZUna 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 > Secrets and variables > Repository secrets</code>).</li>
<li>Abre el proyecto en Godot y genera la configuración para exportar el
proyecto a web
(<code>Project > Export... > Add... > 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 > 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 > Secrets and variables > Actions > 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 > Export... > Add... > 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 Linuxurn:uuid:07b39aed-c9e4-41f8-8cb2-79c70579267c2023-09-11T22:22:00Z2023-09-11T22:22:00ZRecently 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">"default:group::rw"</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 ageurn:uuid:07b39aed-c9e4-41f8-8cb2-79c70579267c2021-06-26T21:42:00Z2021-06-26T21:42:00ZA 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-narrativeurn:uuid:7ec3e802-134e-42e9-baf3-9359382f11c72021-05-03T00:00:00Z2021-05-03T11:00:00ZA 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 gridsurn:uuid:cac2f2f8-8116-4d7d-bbb5-566c178bd7a82021-01-31T00:00:00Z2021-01-31T00:00:00ZA 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"><</span><span class="kw">head</span><span class="dt">></span></span>
<span id="cb1-2"> <span class="dt"><</span><span class="kw">meta</span><span class="ot"> name</span><span class="op">=</span><span class="st">"viewport"</span><span class="ot"> content</span><span class="op">=</span><span class="st">"width=device-width, initial-scale=1.0"</span><span class="dt">></span></span>
<span id="cb1-3"> <span class="dt"><</span><span class="kw">link</span><span class="ot"> rel</span><span class="op">=</span><span class="st">"stylesheet"</span><span class="ot"> href</span><span class="op">=</span><span class="st">"https://cdn.jsdelivr.net/npm/water.css@2/out/water.css"</span><span class="dt">></span></span>
<span id="cb1-4"><span class="dt"></</span><span class="kw">head</span><span class="dt">></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"><</span><span class="kw">div</span><span class="ot"> style</span><span class="op">=</span><span class="st">"display: flex;"</span><span class="dt">></span></span>
<span id="cb2-2"> <span class="dt"><</span><span class="kw">p</span><span class="ot"> style</span><span class="op">=</span><span class="st">"flex: 60%;"</span><span class="dt">></span></span>
<span id="cb2-3"> This column takes 60% of the available space.</span>
<span id="cb2-4"> <span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span>
<span id="cb2-5"> <span class="dt"><</span><span class="kw">p</span><span class="ot"> style</span><span class="op">=</span><span class="st">"flex: 30%;"</span><span class="dt">></span></span>
<span id="cb2-6"> This one just 30%.</span>
<span id="cb2-7"> <span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span>
<span id="cb2-8"><span class="dt"></</span><span class="kw">div</span><span class="dt">></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'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"><</span><span class="kw">div</span><span class="ot"> style</span><span class="op">=</span><span class="st">"display: flex; flex-wrap: wrap;"</span><span class="dt">></span></span>
<span id="cb5-2"> <span class="dt"><</span><span class="kw">p</span><span class="ot"> class</span><span class="op">=</span><span class="st">"example"</span><span class="dt">></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"></</span><span class="kw">p</span><span class="dt">></span></span>
<span id="cb5-5"> <span class="dt"><</span><span class="kw">p</span><span class="ot"> class</span><span class="op">=</span><span class="st">"example"</span><span class="dt">></span></span>
<span id="cb5-6"> So this one is being pushed to the next row.</span>
<span id="cb5-7"> <span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span>
<span id="cb5-8"><span class="dt"></</span><span class="kw">div</span><span class="dt">></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 automationurn:uuid:dcab5a71-98f5-45df-93ba-1db36cdead742020-11-06T00:00:00Z2020-11-06T00:00:00ZOn 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">&&</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">"google_apis"</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">"system-images;android-25;google_apis;x86_64"</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">"mitm-emulator"</span> <span class="at">--package</span> <span class="st">"system-images;android-25;google_apis;x86_64"</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">&</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">'Core/*'</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">"mitm-emulator"</span> <span class="at">-writable-system</span> <span class="kw">&</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">&</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">"</span><span class="va">$CA</span><span class="st">"</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">"</span><span class="va">$CA</span><span class="st">"</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> =>
<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 (<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">'.groupings[].items[]'</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">'select(.distance < 1)'</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">'select(.item.item_category != "BAKED_GOODS")'</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">'(.store.store_name + ", "</span></span>
<span id="cb20-10"><span class="st">+ (.item.price.minor_units/100 | tostring) + "€, "</span></span>
<span id="cb20-11"><span class="st">+ .pickup_interval.start)'</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">"</span><span class="va">$result</span><span class="st">"</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">"</span><span class="va">$result</span><span class="st">"</span> <span class="co"># send a notification in Android's Termux</span></span></code></pre></div>
<p>This shows a simple list like this one as a notification:</p>
<pre class="csv"><code>"Store name 1, 3.99€, 2020-11-04T19:00:00Z"
"Store name 2, 4.99€, 2020-11-04T15:00:00Z"
"Store name 3, 4.99€, 2020-11-04T15:00:00Z"
"Store name 4, 2.99€, 2020-11-04T19:00:00Z"
...</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 languageurn:uuid:708e86b3-a0ce-4bab-b9d7-312c602478f82020-08-12T00:00:00Z2020-08-12T00:00:00ZOn 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"> 'My document'</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"><</span><span class="kw">html</span><span class="dt">></span></span>
<span id="cb3-2"> <span class="dt"><</span><span class="kw">head</span><span class="dt">></span></span>
<span id="cb3-3"> <span class="dt"><</span><span class="kw">link</span><span class="ot"> rel</span><span class="op">=</span><span class="st">"stylesheet"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"text/css"</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"></</span><span class="ot">head</span><span class="dt">></span></span>
<span id="cb3-6"> <span class="dt"><</span><span class="kw">body</span><span class="dt">></span></span>
<span id="cb3-7"> <span class="dt"><</span><span class="kw">h1</span><span class="dt">></span>Title<span class="dt"></</span><span class="kw">h1</span><span class="dt">></span></span>
<span id="cb3-8"> <span class="dt"><</span><span class="kw">p</span><span class="dt">></span>Lorem ipsum...<span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span>
<span id="cb3-9"> <span class="dt"></</span><span class="kw">body</span><span class="dt">></span></span>
<span id="cb3-10"><span class="dt"></</span><span class="kw">html</span><span class="dt">></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">></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"><</span><span class="kw">html</span><span class="dt">></span></span>
<span id="cb5-2"> <span class="dt"><</span><span class="kw">head</span><span class="dt">></span></span>
<span id="cb5-3"> <span class="dt"><</span><span class="kw">link</span><span class="ot"> rel</span><span class="op">=</span><span class="st">"stylesheet"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"text/css"</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"></</span><span class="ot">head</span><span class="dt">></span></span>
<span id="cb5-6"> <span class="dt"><</span><span class="kw">body</span><span class="dt">></span></span>
<span id="cb5-7"> <span class="dt"><</span><span class="kw">h1</span><span class="dt">></span>${title}<span class="dt"></</span><span class="kw">h1</span><span class="dt">></span></span>
<span id="cb5-8"> <span class="dt"><</span><span class="kw">p</span><span class="dt">></span>${body}<span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span>
<span id="cb5-9"> <span class="dt"></</span><span class="kw">body</span><span class="dt">></span></span>
<span id="cb5-10"><span class="dt"></</span><span class="kw">html</span><span class="dt">></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"><</span><span class="kw">html</span><span class="dt">></span></span>
<span id="cb6-2"> <span class="dt"><</span><span class="kw">head</span><span class="dt">></span></span>
<span id="cb6-3"> <span class="dt"><</span><span class="kw">link</span><span class="ot"> rel</span><span class="op">=</span><span class="st">"stylesheet"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"text/css"</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"></</span><span class="ot">head</span><span class="dt">></span></span>
<span id="cb6-6"> <span class="dt"><</span><span class="kw">body</span><span class="dt">></span></span>
<span id="cb6-7"> <span class="dt"><</span><span class="kw">h1</span><span class="dt">></span>My document<span class="dt"></</span><span class="kw">h1</span><span class="dt">></span></span>
<span id="cb6-8"> <span class="dt"><</span><span class="kw">p</span><span class="dt">></span>Paragraph<span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span>
<span id="cb6-9"> <span class="dt"><</span><span class="kw">ul</span><span class="dt">></span></span>
<span id="cb6-10"> <span class="dt"><</span><span class="kw">li</span><span class="dt">></span>List item 1<span class="dt"></</span><span class="kw">li</span><span class="dt">></span></span>
<span id="cb6-11"> <span class="dt"><</span><span class="kw">li</span><span class="dt">></span>List item 2<span class="dt"></</span><span class="kw">li</span><span class="dt">></span></span>
<span id="cb6-12"> <span class="dt"></</span><span class="kw">ul</span><span class="dt">></span></span>
<span id="cb6-13"> <span class="dt"><</span><span class="kw">h2</span><span class="dt">></span>Section 2<span class="dt"></</span><span class="kw">h2</span><span class="dt">></span></span>
<span id="cb6-14"> <span class="dt"><</span><span class="kw">p</span><span class="dt">></span>Another paragraph<span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span>
<span id="cb6-15"> <span class="dt"></</span><span class="kw">body</span><span class="dt">></span></span>
<span id="cb6-16"><span class="dt"></</span><span class="kw">html</span><span class="dt">></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>