<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Iterative Tangents</title>
  <link href="https://iterativetangents.com/atom.xml" rel="self"/>
  <link href="https://iterativetangents.com/"/>
  <updated>2026-08-10T15:39:40+00:00</updated>
  <id>https://iterativetangents.com/</id>
  <author>
    <name>exupero</name>
  </author>
  <entry>
    <id>https://iterativetangents.com/obsidian-position-marks</id>
    <link href="https://iterativetangents.com/obsidian-position-marks"/>
    <title>Obsidian position marks</title>
    <updated>2026-08-10T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>Here's a quick <a href="https://obsidian.md/">Obsidian</a> tip. I've written quite a bit of lengthy prose in Obsidian then revised it, and when the work is broken across multiple sittings, it's easy to lose my place in a document. All the more so on a mobile device where the scroll sometimes resets unpredictably in large notes. I used to mark my place with some sequence of symbols, such as <code>//</code>, then search for that to find where I left off. Obsidian offers an improvement via <a href="https://obsidian.md/help/links#Link%20to%20a%20block%20in%20a%20note">block links</a>.</p><p>Instead of a special sequence of characters, type a caret and a word, such as <code>^here</code>. That's a block reference that marks the preceding paragraph with a label. You can jump to that mark with any internal link, such as <code>[[Your file#^here]]</code>, but I use it in the file itself by creating a frontmatter field called <code>marks</code> with the value <code>[[#^here]]</code>. That gets rendered as a link which when clicked jumps to the <code>^here</code> mark.</p><p>I put <code>^here</code> on a blank line between paragraphs. Jumping to it briefly highlights the preceding paragraph, but I know the mark is a separator and resume editing below that point.</p><p>If you make the <code>marks</code> field a list, you can include multiple links in one frontmatter field. You can also create a template that works with both the Templates core plugin or the <a href="https://silentvoid13.github.io/Templater/">Templater</a> community plugin:</p><pre><code>---
marks:
  - '[[#^here]]'
---
^here
</code></pre><p>That will add the link to the <code>marks</code> field, creating the field if it doesn't exist, and add <code>^here</code> wherever your cursor is.</p><p>Be warned that not all Markdown renderers recognize Obsidian's <code>^</code> syntax. While exporting from Obsidian won't render the in-document marks, other Markdown formatters might, and I've occasionally ended up with stray <code>^here</code> paragraphs.</p>]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/obsidian-web-apps-plugin</id>
    <link href="https://iterativetangents.com/obsidian-web-apps-plugin"/>
    <title>Obsidian Web Apps plugin</title>
    <updated>2026-08-07T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>I wrote about some <a href="/local-only-web-apps">local-only web apps</a> I made and how they differ from <a href="/local-only-versus-local-first-software">local-first software</a> that synchronizes data across devices. Often, local-first software is about documents. That's certainly the case for the specialized diagramming tools I've been creating. They allow exporting and importing YAML files, but it's cumbersome for those files to be the primary means of managing documents, since every download produces a new file rather than updating an existing file. Chrome has a file system API that allows direct access to local files, but Firefox is my default browser and it's <a href="https://mozilla.github.io/standards-positions/#native-file-system">opposed</a> to adding similar capability. However, since I use <a href="https://obsidian.md/sync">Obsidian Sync</a>, I already have a local-first app that synchronizes across devices. My initial attempt to leverage Obsidian in my web apps was to add a button that copies the app's YAML data to the system clipboard, which can then be pasted into the frontmatter of a note. While that worked, it still wasn't ergonomic enough to do frequently. A better workflow required integrating the apps directly into Obsidian using a plugin.</p><p>Obsidian has a huge number of community plugins. Plugins support custom HTML and logic, so I could have rebuilt the web apps as dedicated plugins, but I didn't want to make a custom plugin for each tool, just one plugin that bridged between Obsidian and my web apps. I've dabbled a little in making Obsidian plugins, but since I could define the solution clearly and was more interested in the end product than in the process of building, I tried my hand at vibe-coding and asked Claude to make a plugin. You can find it <a href="https://github.com/exupero/obsidian-web-apps">here</a>. I reviewed the code enough to be familiar with it, so if you have suggestions for improvements, feel free to open an issue or pull request.</p><p></p><figure class="figure-large"> <img src="/images/obsidian-web-apps-file.png" alt="Conflict resolution diagram from YAML frontmatter"> <figcaption>A conflict resolution diagram populated from an Obsidian note's YAML frontmatter.</figcaption> </figure><p></p><p>At heart, all the plugin does is transmit data back and forth between the Obsidian note and the web app. It loads the web app in an iframe and passes data to it in a message. Web apps pass messages back with data that the plugin saves to the file. The web app can be the whole tab, in which case the note's YAML frontmatter is passed to the app as JSON, or the web app can be sourced from a code block, and only the code block's YAML content is given. More details on the message-passing protocol are in the <a href="https://github.com/exupero/obsidian-web-apps/blob/main/README.md">README</a>.</p><p>The plugin supports multiple web apps. Configure names and URLs in the plugin's settings, then specify which web app to load by adding a <code>webapp</code> key whose value is the name you gave the web app. There is a toolbar with a &quot;Save&quot; button to ask the web app for the current state. When the web app is sourced from a file, you can switch to a Markdown view. When sourced from a code block, you can specify the height of the iframe.</p><p></p><figure class="figure-large"> <img src="/images/obsidian-web-apps-code-block.png" alt="Conflict resolution diagram from a code block"> <figcaption>A conflict resolution diagram populated from a fenced code block in an Obsidian note.</figcaption> </figure><p></p><p>It can also be used in the mobile version of Obsidian.</p><p>I've been using the plugin here and there for about a month, so it's far from battle-tested, but in my experience the weak point is my web apps, not the plugin. I'm not 100% convinced this is a good idea. The plugin allows web apps to read data from and write data to the Obsidian notes you use it in, so only use web apps you trust. You should also have Obsidian Sync, Git, or some other version control system that backs up files you use with the plugin, since a web app that returns empty values will remove data from the file. If you have security concerns, definitely <a href="mailto:eric@iterativetangents.com?subject=Obsidian Web Apps plugin">email me</a>.</p><p>Do you have other techniques for managing specialized documents across devices? I'm all ears.</p>]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/on-llm-generated-fiction</id>
    <link href="https://iterativetangents.com/on-llm-generated-fiction"/>
    <title>On LLM-generated fiction</title>
    <updated>2026-08-05T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>From <a href="https://mccormick.cx/news/entries/why-i-won-t-read-llm-authored-fiction">Why I Won't Read LLM Authored Fiction</a> by <a href="https://mccormick.cx/">Chris McCormick</a>:</p><blockquote><p> I don't want to read a novel where most of the words are written by an LLM, even if a human designed the novel at a high level. It feels gross. I think this is the reason. The statistical profile of LLM writing is close to the normal. That's literally how LLMs work. They sample from a distribution that captures a median way of writing. I simply don't want my mind pushed towards the statistically normal when I am reading fiction. That's the exact opposite of what I want from the experience. </p></blockquote><p>I may finally read <i><a href="https://www.amazon.com/End-Average-Unlocking-Potential-Embracing/dp/0062358375">The End of Average</a></i>, now that LLMs have given us <i>The Revenge of Average</i>. Average output is acceptable for computer code, for which the purpose is to get a computer to operate in a specific way, but the human central processor works differently. We tend to ignore sameness and remember novelty. That's the root of a lot of our <a href="https://en.wikipedia.org/wiki/List_of_cognitive_biases">cognitive biases</a>.</p><p>I've used LLMs as research assistants for fiction, and sometimes they can break me out of my self-imposed mental boxes, but any time I've accepted their offer to write a sample for a story, I've been profoundly bored by the result. Possibly my prompts could be better. I learned a little about writing from asking an LLM to write a simple scene in the styles of various authors, in order to learn more about pacing narrative prose, and while the scenes were readable, they weren't interesting. Same for a couple vignettes I've generated to entertain my kids with a scene of a Muppet exploring space or under the ocean. LLMs are competent language calculators, but they can't intrigue. They have no personality. They can nerd out about anything, and thus can't obsess over something to the exclusion of everything else.</p><p>Chris's post gives me a much better appreciation for how much writing is, at heart, about connecting with another human. We want to know that a person thought it was worth putting these words together, was willing to spend the time creating this story, sharing these characters, composing this sentence, and that when challenged about what they've written will defend it and maybe, in so doing, teach us something valuable. Reading fiction isn't about assembling information; it's about gaining understanding. Sometimes wisdom comes from juxtaposing information, but more often it comes from the enigmatic other, the person across the table or at the other end of the fiber optic cable. It comes from someone who's had skin in the game and had to make difficult choices.</p><p>My hope in the era of LLMs is that we as humans learn to find each other more fascinating.</p>]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/local-only-versus-local-first-software</id>
    <link href="https://iterativetangents.com/local-only-versus-local-first-software"/>
    <title>Local-only versus local-first software</title>
    <updated>2026-08-04T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>Last week I wrote about a few <a href="/local-only-web-apps">local-only web apps</a> I've made recently. Since I don't have a server to persist data in the cloud, they're all static sites that can export and import YAML data. A downloaded YAML file can be reloaded later to restore the app's state, or you can transfer a file to another device and load it up there. Exploring this local-centric model of state management led me to the term <a href="https://www.inkandswitch.com/local-first-software/">Local-first Software</a>.</p><p>Local-first software is distinct from local-only software. My local-only apps require the user to transfer state between devices, but local-first software is local first, replication second. As far as I've learned so far, that typically means having a server that can relay state changes, often using <a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type">CRDTs</a>. I've started listening to the <a href="https://www.localfirst.fm/">localfirst.fm podcast</a> to learn more. There's also a <a href="https://www.localfirstconf.com/">Local-First Conf</a> and the website <a href="https://lofi.so/">lofi.so</a>. A related term I like is &quot;<a href="https://www.jeremiahlee.com/posts/leaf-computing/">LEAF computing</a>&quot;, which stands for Local Execution, Autonomous, and Federated (I've also heard Local, Encrypted, Autonomous, and Federated). The term &quot;LEAF computing&quot; stands in contrast with &quot;cloudy computing&quot;.</p><p>I treasure the values of the local-first software movement, specifically giving users ownership of their data, the ability to run software fully offline, much lower UI latency, and using local compute resources rather than servers in a data center. It's not the right architecture for many types of software, among them e-commerce and social media platforms. The former needs stronger authorization controls than local-first software can guarantee, and the latter operates at larger scales than can easily run locally (though there is <a href="https://scuttlebutt.nz/">Scuttlebutt</a>).</p><p>Given those constraints, much of the local-first software I see advertised appears to be note-taking software. <a href="https://obsidian.md/">Obsidian</a> is the clearest example, used for managing images and Markdown notes. <a href="https://brainframe.tech/#philosophy">BrainFrame</a> looks similar. <a href="https://allume.com/">Allume</a> captures whiteboard-style notes. Somewhere I saw a recipe tracker. Those uses are great, but I'm more interested in how local-first software can go beyond the zettelkasten. The best examples I know are <a href="https://actualbudget.org/">Actual Budget</a>, which tracks home finances, and some music players.</p><p>Do you use local-first software? Anything you'd recommend, either for using directly or for learning from? If so, <a href="mailto:eric@iterativetangents.com?subject=Local-only versus local-first software">email me</a>!</p>]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/two-safari-bugs</id>
    <link href="https://iterativetangents.com/two-safari-bugs"/>
    <title>Two Safari bugs</title>
    <updated>2026-08-03T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>Doing <a href="/local-only-web-apps">front-end development</a> over the last couple of months, I've been reminded to test web apps in Safari. It exhibits some surprising bugs. Here are two I encountered recently.</p><h2><code>foreignObjects</code></h2><p>The first was in <a href="https://tools.iterativetangents.com/causation/">Causation</a>, my simple cause-and-effect diagrammer. It renders everything in an SVG, and makes the text in nodes editable by embedding HTML using <code>foreignObject</code>. Turns out Safari has multiple <code>foreignObject</code> bugs. I ran into one which rendered every <code>foreignObject</code> in the wrong location, and another that rendered the elements on top of the elements that cover the SVG.</p><p></p><figure class="figure-small"> <img src="/images/safari-foreignObject.png" alt="Safari overlays foreignObject on elements covering the SVG"> <figcaption>Safari overlays foreignObject on elements covering the SVG</figcaption> </figure><p></p><p>I fixed the positioning problem by calculating view transforms myself. I haven't yet fixed the overlay problem with <code>foreignObjects</code>, but I think I'll move the HTML content into a purely HTML layer over the SVG, then position elements absolutely. Most of the UI is an infinite canvas, so some glitches over the toolbar at the top are minor.</p><h2><code>grid-template-columns</code> and <code>grid-template-rows</code></h2><p>On another project, I used CSS's <code>grid-template-columns</code> and <code>grid-template-rows</code> to create a square grid:</p><pre><code class="css">.grid {
  display: grid;
  grid-template-columns: repeat(var(--column-count), 1fr);
  grid-template-rows: repeat(var(--row-count), 1fr);
}
</code></pre><p>The <code>.grid</code> DOM element can then specify its size by setting an element-level <code>style</code> attribute with the value <code>--column-count: 5; --row-count: 5</code>.</p><p>But on Safari, switching from a grid of fewer tiles to one with more tiles in the same width caused the grid to overflow its bounds:</p><p></p><figure class="figure-medium"> <img src="/images/safari-grid.png" alt="Safari doesn't reflow grid rows and columns"> <figcaption>Safari doesn't reflow grid rows and columns.</figcaption> </figure><p></p><p>The layout gets fixed on the next reflow. A couple suggested workarounds didn't help, so I ended up removing the CSS grid properties and instead set a percentage width for the tiles.</p>]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/local-only-web-apps</id>
    <link href="https://iterativetangents.com/local-only-web-apps"/>
    <title>Local-only web apps</title>
    <updated>2026-07-29T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>Over the last few months I've created a handful of local-only web apps for my own use and for family members. They're designed for somewhat specific needs that only occasionally arise, but those needs supplied an excuse to play more seriously with <a href="https://github.com/cjohansen">Christian Johansen</a>'s suite of ClojureScript libraries, namely <a href="https://github.com/cjohansen">Replicant</a>, <a href="https://github.com/cjohansen/portfolio">Portfolio</a>, <a href="https://github.com/cjohansen/nexus">Nexus</a>, <a href="https://github.com/cjohansen/m1p">m1p</a>, <a href="https://github.com/cjohansen/lookup">Lookup</a>, and <a href="https://github.com/cjohansen/dataspex">Dataspex</a>, which I appreciate for their focus on data-driven frontend development. Each of the apps runs entirely in the browser. Chrome has a file system API, but not everyone uses Chrome (including me), so to persist data the apps save to and load from the browser's local storage, as well as providing buttons to download a file and copy data to the system clipboard, both of which export YAML. To load the data, you can import a YAML file.</p><p>None of the apps are polished, nor bulletproof. I made them to learn, not just about a set of ClojureScript libraries but also about new developments in web technologies. Consider them <a href="https://www.youtube.com/watch?v=MJzV0CX0q8o">scrappy fiddles</a>. If you find any of them useful, <a href="mailto:eric@iterativetangents.com?subject=Local-only web apps">let me know</a>, and certainly report any bugs.</p><p>Here's a quick rundown of each app.</p><h2><a href="https://tools.iterativetangents.com/tinycal/">Tinycal</a></h2><p>This is the fourth iteration of this tiny calendar. The first implementation is lost to time, but the second version (which I covered <a href="/tiny-annual-calendar">a few years ago</a>) recreated it and just showed one year from beginning to end with buttons to export it to SVG or PNG. <a href="/tiny-calendar-redux">The third iteration</a> let you configure the start date, end date, what day of the week to start on, which dates to show, and provided a print option. Those versions now redirect to the latest take.</p><p></p><figure class="figure-medium"> <img src="/images/tinycal-4.png" alt="Tinycal"> <figcaption>The current iteration of Tinycal.</figcaption> </figure><p></p><p>The new version I built because my brother uses the exported PNG in an image editor to outline his plans for the year ahead, and I realized I could make that at least a little easier. Now Tinycal allows selecting dates and highlighting them in one of several colors. You can also add a note that labels events to the right of the calendar.</p><p>It's still a tiny calendar, so it gets crowded fast if you try to use it for scheduling, but I like it less for seeing the day-to-day obligations and more for mapping the broad strokes of the coming months, especially for seeing what weekends I have free. To help with that, the app can be saved to your phone's home screen and run as a progressive web app.</p><h2><a href="https://tools.iterativetangents.com/conflict/#null">Conflict resolution diagrammer</a></h2><p>Years ago I made a web-based diagrammer for <a href="https://en.wikipedia.org/wiki/Evaporating_cloud">conflict resolution diagrams</a>, but I was never totally happy with it. Revisiting it recently to explore some tensions at work, I was pleasantly surprised to learn that CSS now has a grid layout that you can <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/grid-template-areas">template using an ASCII diagram</a>. That made it a lot easier to handle a more two-dimensional layout.</p><p></p><figure class="figure-medium"> <img src="/images/conflict.png" alt="Conflict resolution diagrammer"> <figcaption>A conflict resolution diagram exploring whether a company should prioritize remote-first work or in-person work.</figcaption> </figure><p></p><p>Most people aren't familiar with a conflict resolution diagram, so I added some documentation below the app to explain how to use it. It's easy to miss when you land on the page, so if you have design tips to improve it, <a href="mailto:eric@iterativetangents.com?subject=Local-only web apps">send them my way</a>.</p><h2><a href="https://tools.iterativetangents.com/quizboard/">QuizBoard</a></h2><p>The very first real software I made was a pared-down version of <i>Jeopardy!</i> for a kids' study group. I implemented it in Visual Basic in junior high school and called it &quot;Answers &amp; Questions&quot;. It loaded categories and questions from a text file. The layout was hard-coded to an 800x600 screen resolution, so every time it was used on a higher resolution monitor, the computer's screen size had to be changed for the game to fill the screen. Eventually I made a version that scaled to screen resolution, but by then I didn't need the game and the rewrite was mostly a learning exercise.</p><p>A couple months ago I was asked for a new version, which I dubbed &quot;QuizBoard&quot;. While Answers &amp; Questions followed a strict <i>Jeopardy!</i> layout of six categories with five questions each, QuizBoard allows any number of categories, each category can have any number of questions, and each question can have multiple clues. At first I required a game to be made by editing the downloaded YAML file and re-uploading it, but that proved both cumbersome and error-prone, so I added a simple game editor.</p><p></p><figure class="figure-medium"> <img src="/images/quizboard.png" alt="QuizBoard"> <figcaption>QuizBoard at the beginning of a game.</figcaption> </figure><p></p><p>I also added a presentation mode. Launching it opens a new window that can be shown on an external monitor and controlled from the original window. That gives the operator a chance to see all the clues on a question, as well as the answer, so they can decide if a team got the right answer.</p><h2><a href="https://tools.iterativetangents.com/causation/">Causation</a></h2><p>In high school, I had to make a cause and effect diagram of the American Civil War, and I got carried away, spreading it onto multiple sheets of printer paper that I taped together into a poster. In college, I made diagrams for some of the European periods covered in freshman history, and when my classmates saw them, they asked for copies to study from. Probably if I had learned to make such diagrams sooner, I would have been interested in history sooner. I've also drawn cause and effect diagrams to explore <a href="https://en.wikipedia.org/wiki/Systems_thinking">complex systems</a>.</p><p>Since college, I've tried a few different graphing tools for making cause and effect diagrams, but I haven't found any I've liked. <a href="https://graphviz.org/">Graphviz</a> and <a href="https://github.com/mermaid-js/mermaid">Mermaid</a> are text-based and easy to lose my place in. <a href="https://excalidraw.com/">Excalidraw</a> is more visual and manual, and arrows stay connected to boxes, but when drawing a <a href="https://en.wikipedia.org/wiki/Current_reality_tree_(theory_of_constraints">current reality tree</a>) I want to link edges together to denote multiple conditions that need to be true to produce an effect, and Excalidraw doesn't offer that without jumping through hoops like intermediate nodes.</p><p>I finally sat down to make something suited to my usage. It snaps nodes to a coarse grid, doesn't require drawing arrows manually, allows inserting nodes in the middle of existing links, and most importantly, lets you link edges to each other. My favorite feature is being able to move whole subgraphs, dragging all of a node's descendants or ancestors with it to make more room.</p><p></p><figure class="figure-medium"> <img src="/images/causation.png" alt="Causation"> <figcaption>Cause and effect diagram of World War I.</figcaption> </figure><p></p><h2><a href="https://tools.iterativetangents.com/electric/">Home Electric Cost Optimizer</a></h2><p>My father has a hybrid vehicle and was curious if he got solar panels whether he should charge the car with surplus solar power during the day or use cheaper rates from the grid at night and sell surplus solar power back to the grid. I made this calculator to explore possibilities.</p><p></p><figure class="figure-medium"> <img src="/images/electric.png" alt="Home Electric Cost Optimizer"> <figcaption>A calculator to optimize home electricity costs, given several variables.</figcaption> </figure><p></p><p>There are a lot of factors in the conversion of solar irradiance to actual power output. Latitude is the main one, but it's also affected by your local climate's tendency toward cloudiness, the efficiency of the panels, the direction the panels face, and their angle. I simplified all of that by asking for max power output on the summer solstice, then scaling solar irradiance to produce that.</p><p>Making it for someone other than myself meant more back and forth communication with a &quot;customer&quot;. The physicist in me desperately wanted to reduce units like kilowatt-hours per hour to just kilowatts, but to a layman looking at his electric bills that was less intuitive than kWh/h.</p><h2><a href="https://tools.iterativetangents.com/timeliner/">Timeliner</a></h2><p>Another history-related project is Timeliner, which I started because I frequently find myself either surprised by how far apart different events were or how unrelated events happened at the same time. For example, I tend to forget Mozart lived during the American Revolution. To gain a better perspective, I wanted a timeline visualization tool that let me add arbitrary historical events and zoom and scroll horizontally. Probably there's a good tool for this, but I forgot to look.</p><p></p><figure class="figure-medium"> <img src="/images/timeliner.png" alt="Timeliner"> <figcaption>A timeline of some TV shows I watched as a kid.</figcaption> </figure><p></p><p>Making it, I discovered there's a native HTML color element, and while the Mac color picker lets you define swatches, it pops up a new window that can lose focus to the original window, which for me caused some odd behavior.</p>]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/encoding-knowledge-with-automation-scripts</id>
    <link href="https://iterativetangents.com/encoding-knowledge-with-automation-scripts"/>
    <title>Encoding knowledge with automation scripts</title>
    <updated>2026-06-22T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>In the <a href="/two-git-scripts-to-encourage-good-development-habits">previous post</a> I shared a couple small scripts I use with Git. I automate a lot of the repetition in my development workflow to make tasks not just easier but also more reliable. Automation encodes knowledge. Scripts give me a place to capture things I learn.</p><p>Below are descriptions of several scripts I use on a daily basis.</p><p>Most of my scripts aren't portable. They're tailored to the specific tools I use, <a href="/tmux-as-development-environment">such as</a> <a href="https://github.com/tmux/tmux/wiki">Tmux</a> and <a href="https://www.alfredapp.com/">Alfred</a>, they often require particular libraries and environment variables, and while some are Bash, most of them favor <a href="https://clojure.org/">Clojure</a> (<a href="https://babashka.org/">Babashka</a>). Rather than sharing the scripts themselves, this post focuses on the problems that prompted them and what I automated.</p><h2><code>ws</code> &amp; friends</h2><p>I use <a href="https://git-scm.com/docs/git-worktree">Git worktrees</a> extensively, checking out work-in-progress branches and <a href="/local-pr-review">code to review</a> in separate folders and Tmux sessions. <a href="/tmux-worktree-scripts">Here</a>'s one version of the script. My current iteration is called <code>ws</code> and takes two or three parameters:</p><pre><code class="bash">ws &lt;type&gt; &lt;name&gt; [&lt;ticket-number&gt;]
</code></pre><p><code>type</code> can be <code>feat</code>, <code>fix</code>, <code>task</code>, or <code>cve</code>. <code>name</code> is the name to use for the branch and Tmux session. If there's a Jira ticket associated with the work, <code>ticket-number</code> captures that.</p><p><code>ws</code> creates a new Git branch using the given information (e.g. <code>feature/some-branch-name-TICKET-5555</code>), then checks out the branch in a new Git worktree and creates a Tmux session for it. It also captures the data in a couple of files local to the worktree. One is a <a href="/drafting-pull-requests">local draft of the PR</a>, which is Markdown but has YAML frontmatter where I specify the title of the PR, any labels, reviewers, and the ticket number. Those will be used when creating the PR (see <code>gh-open</code> below). Some of that information is also captured in a <code>.workspace.yaml</code> file, which can be used to re-launch the the Tmux session. Both <code>.workspace.yaml</code> and the PR file are created by <code>ws</code> and ignored by Git via a global gitignore.</p><p><code>ws</code> creates the workspace files, but opening the workspace is performed by <code>wso</code> (&quot;workspace open&quot;), which <code>ws</code> invokes but which I can also invoke manually on any directory with a <code>.workspace.yaml</code> file. Initializing the workspace is done by <code>wsi</code> (&quot;workspace initialize&quot;), which sends the <code>init</code> command (see below) to the first Tmux pane in the workspace.</p><p>Similar to <code>ws</code> is <code>wsc</code> (&quot;workspace checkout&quot;), useful for branches that exist but for which I don't have a local worktree.</p><p>To review someone else's pull request locally, I use <code>wsr</code> (&quot;workspace review&quot;). It pulls the URL for the PR from the clipboard, fetches the PR from GitHub's API, switches to the local directory for that repo, then invokes <code>wsc</code> for the PR's branch.</p><p>When I'm done with a workspace, I run <code>wsd</code> (&quot;workspace destroy&quot;) to check for pending changes, and if there aren't any, the script switches the Tmux client to a Tmux session that won't be deleted, deletes the Git worktree, and kills the workspace's Tmux session.</p><h2><code>init</code> and friends</h2><p>I have an <code>init-&lt;repo-name&gt;</code> script for each repo I work in. They do all the setup for a new worktree, such as creating a Python virtual env, activating it, and installing dependencies. Most repos have a Makefile or <code>install.sh</code> script which does the heavy-lifting, so these <code>init-*</code> scripts capture what's specific to my workflow, such as using <code>pyenv</code> rather than some other virtual environment tool.</p><p><code>init</code> is just a wrapper script that 1) inspects the Git repo to figure out which <code>init-*</code> script to run, 2) verifies the <code>init-*</code> exists, and 3) runs it. <code>ws</code> launches this automatically in new workspaces, but on occasion I invoke it manually.</p><h2><code>cve</code></h2><p>A few weeks ago I addressed a lengthy backlog of security vulnerabilities. I chose to tackle them in bulk—rather than chip away here and there—so I could focus on developing a system and encoding it in a script, smoothing the process for future fixes. The result was the script <code>cve</code> with several subcommands.</p><p><code>cve check</code> reads the system clipboard to get the URL for a Jira ticket, parses the ticket's description, finds the name of the affected Docker image and the package that needs to be upgraded, then opens a URL to our internal artifact repository that lists the image's current vulnerabilities. Occasionally vulnerabilities have been fixed by other changes and the ticket remains open. When that happens, <code>cve close</code> will comment on the Jira ticket with a link to the vulnerabilities page, assign it to me, and close the ticket. If the vulnerability still exists, <code>cve ws</code> finds the repo that produces the affected Docker image and invokes <code>ws cve</code> with the CVE's ID.</p><p><code>cve repro</code> builds the Docker image, then runs <code>docker scout cves</code> and picks out the sections mentioning the CVE's identifier. This is the most complicated subcommand. Some of the Dockerfiles need tweaks to build locally (for which I keep a set of patches), several of the images depend on other images having been built first, some images are built by specific Makefiles, and building can require particular environment variables. All that information is tracked in a data structure in the script.</p><p><code>cve locations</code> runs <code>docker scout cves</code> with the <code>--format sarif</code> option, which emits JSON, then the script plucks out the buried <code>.physicalLocation</code> fields to get the full paths to the files that introduce the vulnerability. Where the file indicates whether the vulnerability is due to an NPM dependency, a Python dependency, or a package installed in the Docker image itself.</p><h2><code>testit</code></h2><p>Different projects have different ways to run tests, and even a single repo can have separate test runners for backend unit tests, frontend unit tests, and integration tests. When I want to run a single test, I never remember whether the runner wants a line number or the name of the test. Rather than trying to keep it all straight, I use <code>testit</code>.</p><p>The script takes a path and line number. It checks the path against several regex patterns to find the function that can run the test. For test runners that take a path and line number, the function calls <a href="https://github.com/babashka/process">babashka.process/shell</a> with the proper directory, environment variables, and shell command. For test runners that want the name of a test, the script first parses the test file by shelling out to <a href="https://tree-sitter.github.io/tree-sitter/">tree-sitter</a> with <code>tree-sitter parse &lt;path&gt; --xml</code>, then it finds the appropriate concrete syntax tree expression by pattern-matching with <a href="https://github.com/noprompt/meander">Meander</a>. For example, in a Python unit test suite, it looks for methods starting with <code>test_</code> and classes starting with <code>Test</code>, picking the last one that's above or at the given line number, then it constructs the <code>TestClass::test_method</code> identifier that the command line test runner expects.</p><p>Rather than invoking <code>testit</code> directly, I have a <a href="https://neovim.io/">NeoVim</a> keystroke that grabs the current filename and line number and submits a <code>testit</code> command in a separate Tmux pane. That makes it easy to trigger tests from my editor, as well as re-run tests without having to find the same place in the editor.</p><h2><code>gh-open</code> &amp; <code>gh-update</code></h2><p>When a pull request is ready to open, I polish up my local Markdown draft and run <code>gh-open</code>. That parses the YAML frontmatter out of the Markdown, constructs a title, submits the PR via GitHub's API, requests reviewers and adds labels, and opens the PR in my default browser. If the PR needs any screenshots, I edit the description via the browser. I've aliased <code>gh-open</code> to a <a href="https://cli.github.com/">gh</a> subcommand with <code>gh alias set open '!gh-open $@'</code>.</p><p>I also have <code>gh-update</code>, which takes the latest version of the local Markdown file and updates the pull request. If the PR has images, they're be lost; probably I should add a script that copies the current PR description into my local file.</p><h2><code>qlink</code></h2><p>I use <a href="https://espanso.org/">Espanso</a> as a text expander, most often for generating links to PRs and tickets. Those links go in Slack, personal notes, or comments in GitHub and Jira, and depending on context they need different formats. For that I use <code>qlink</code> (&quot;quick link&quot;). <code>qlink</code> takes a small specifier that names the source of the URL and the output format to use. The <code>p.</code> specifier means &quot;get the URL from the clipboard and output a short-form Markdown link&quot;. <code>j5555,</code> means &quot;create a long-form Markdown link for Jira TICKET-5555&quot;. To generate a long-form link, the script fetches the title from the appropriate API.</p><p>Here's a poor-man's version to get a long-form Markdown link from the URL of a pull request:</p><pre><code class="bash">gh pr view &lt;URL&gt; \
  --json title,url,headRepository,number \
  --jq '&quot;[\(.headRepository.name) #\(.number) - \(.title)](\(.url))&quot;'
</code></pre><p></p><h2><code>gh-pr-list</code></h2><p>This script I trigger from Alfred. I have a workflow that runs it for a handful of different keywords, one for each repo I work in. Besides taking the repo name, the script takes a shorthand search query, constructs a URL for a GitHub pull request or list of pull requests, then opens the URL in my browser. The supported shorthands are:</p><ul><li>no query: open the list of all PRs in the repo</li><li>digits: open the PR with the given number</li><li>starts with <code>#</code>: search for PRs with the given label</li><li>alphanumeric characters: search for PRs by the given author</li></ul><h2><code>grase</code></h2><p>I prefer rebasing my branches to merging in the mainline branch. Rebasing keeps my commits together, and only rarely have teammates complained about the change in branch reference. I always use interactive rebasing so I can verify what commits will be moved. Originally <code>grase</code> was a Bash alias for <code>git rebase -i &lt;mainline-branch&gt;</code>, but since <code>.workspace.yaml</code> captures the target branch, <code>grase</code> is now a script that reads the <code>.workspace.yaml</code> field and rebases on the correct branch. This is especially useful when I have stacked PRs, where changing one branch requires rebasing downstream branches.</p><p><code>grase</code> is an example of how these automation scripts feed off each other. The <code>.workspace.yaml</code> file I originally created for <code>wso</code> has useful information for other scripts.</p><h2><code>checkin</code></h2><p>These scripts cross repo boundaries, so none of them live in any of the projects I use them for. Instead I keep them in a <a href="https://github.com/exupero/hammerspoon">few</a> <a href="https://github.com/exupero/scripts">external</a> <a href="https://github.com/exupero/vim">repos</a> (plus some private repos). After tweaking things here and there through the day, it's tough to remember what changed where, so <a href="https://github.com/exupero/scripts/blob/main/checkin"><code>checkin</code></a> reads directory paths from a <code>CHECKIN_PATHS</code> env var, then goes through the directories one by one, spawning a subshell in the given directory. Then I stage code in the Git index. When I'm done, I exit the subshell with Ctrl-D, and <code>checkin</code> commits the changes with a generic message and pushes to the remote repository.</p><p>Running <code>checkin</code> is an easy routine to follow at the end of the day, and it reminds me of some fun I had.</p><p>The script takes an optional argument to read from a different environment variable, allowing me to check in specific groups of repos. <code>checkin</code> I run daily; <code>checkin weekly</code> I run on Friday to capture changes in less frequently touched repos.</p><p>If you have scripts you use to automate your workflow or make it more fun, I'd be happy to <a href="mailto:eric@iterativetangents.com?subject=Encoding knowledge with automation scripts">hear about them</a>.</p>]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/two-git-scripts-to-encourage-good-development-habits</id>
    <link href="https://iterativetangents.com/two-git-scripts-to-encourage-good-development-habits"/>
    <title>Two Git scripts to encourage good development habits</title>
    <updated>2026-05-29T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>I've written about various practices I have when writing or reviewing code, from <a href="/splitting-branches-using-diff-files">splitting branches using diff files</a> and <a href="/drafting-pull-requests">drafting pull requests locally</a> to <a href="/local-pr-review">reviewing code locally</a> and <a href="/exploratory-testing-on-pull-requests">documenting exploratory testing</a>. It's not always easy going those extra miles, so I try to make it as simple, reliable, and fun as possible by automating whatever I can. Here are two scripts I often use, both of which use <code>git</code> and <a href="https://github.com/junegunn/fzf"><code>fzf</code></a>. One helps keep commits clean, the other I use for testing tests.</p><h2>git-fixup</h2><p>I make a lot of small, frequent commits, checking in every time the code works and has a meaningful change I wouldn't want to lose. Then, if new work gets the code in a tangle or breaks something I don't understand, I can clear the mess by resetting the working directory to latest commit. Tiny and always-working commits also helps <code>git bisect</code>.</p><p>Occasionally commits need amending. Some commit in a long list of commits broke a feature or introduced a bug that didn't get noticed. Rather than just add the fix to that long list, I prefer to update the commit that introduced the problem. To do that, I add a commit whose message starts with &quot;fixup&quot;, then do an interactive rebase, which allows re-ordering commits and indicating that the fixup commit should be squashed into the commit before it. Then the original commit is cleaner.</p><p>While branches are often short-lived enough to rebase the whole branch, it's tricker to rebase when developing directly off the main trunk. One has to find the commit hash for the commit that needs to be amended then run <code>git rebase -i &lt;hash&gt;^</code>. To turn the process of finding a commit hash and rebasing into one step, I use this script:</p><pre><code class="bash">git log --oneline -50 \
    | fzf \
    | cut -d' ' -f1 \
    | xargs -I {} git rebase -i {}^
</code></pre><p>That lists the last 50 commits with their short-form SHA and commit message, and pipes them into <code>fzf</code> where you can search for the commit message of the commit you want to rebase onto. Select a commit by moving up and down and/or searching and hitting Enter. Then <code>git rebase -i &lt;hash&gt;^</code> will run with the commit you selected, which opens your default text editor where you can move the fixup commit to where it belongs in the list, mark it with an <code>f</code> for &quot;fixup&quot;, then exit and apply the rebase.</p><h2>git-regress</h2><p>When reviewing code that includes tests, I run the tests on my local machine to surface any missing environment variables, potential OS incompatibilities, test flakiness, and sometimes even tests that aren't picked up by the test runner.</p><p>I also want to see that the tests can fail. Sometimes the tests run but the assertions are broken, such as the case of the <code>.assertTrue</code> that forgot the <code>()</code> after it, preventing the assertion from actually being invoked. The easiest way to see new tests fail is to undo the application changes the tests are checking.</p><p>To undo a branch's logical changes but not its tests, use <code>git diff &lt;some-branch&gt;... -R -- &lt;directories&gt; | patch -p1</code>. That creates a reverse diff of specific files, then applies the changes.</p><p>To simplify finding the code to undo then patching, I use this script that gives <code>fzf</code> all the changed files on a branch, which lets me select the files to revert then applies the reverse diff of those files:</p><pre><code class="bash">mainBranch=$(git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)
toRevert=$(git diff $mainBranch... --name-only \
  | sort -u \
  | fzf --ansi \
      --disabled \
      --bind 'j:down,k:up,q:abort' \
      --preview=&quot;git diff $mainBranch... -- {}&quot; \
      --preview-window=right:60%)

if [ -z &quot;$toRevert&quot; ]; then
  echo &quot;No directories or files selected.&quot;
  exit 1
fi

git diff $mainBranch... -R -- $toRevert | patch -p1
</code></pre><p>You may want to add a step after <code>git diff ...</code> that takes the files changed on the branch and includes their parent directories, so you can revert the changes in a whole directory without having to select every file under it.</p><p>If you have suggestions for improvements to either of these scripts, or if you have your own scripts that help with your workflow, please <a href="mailto:eric@iterativetangents.com?subject=Two Git scripts to encourage good development habits">email me</a>.</p>]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/nice-words</id>
    <link href="https://iterativetangents.com/nice-words"/>
    <title>Nice words</title>
    <updated>2026-05-20T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>An old oak in my yard fell a few weeks ago, and after I cleared the brush my kids volunteered to help rake up twigs and bark. My six year-old declared the job somewhat difficult, to which my three year-old replied, &quot;It's not hard for me.&quot; I was a bit snappish and couldn't let that braggadocio go uncorrected, so I told him, &quot;It's easy for you because you're not doing anything, you're just telling everyone else what to do.&quot; He paused for a long moment. Then he said, &quot;But you are doing a good job!&quot;</p><p>The little guy is a born manager. My friend Joe has a term for that kind of response from a leader. Years ago I was pulled off a project to work on a more urgent project, and during it I wrote some convoluted code that I should have refactored before publishing. When a reviewer called me out, I complained about the rush. The next morning, the project manager left a comment: &quot;You’ve been doing a lot of awesome work Eric! Your contributions, insights, and suggestions have really helped the [project] effort. Thanks!&quot; When I told Joe that story, he called it &quot;affirmation bombing&quot;.</p><p>Leaders often address complaints with compliments. But when praise is just a tactic used to assuage frustration, it comes across as disingenuous. Worse, it can to backfire if the receiver sees through such praise and interprets it as, &quot;Your negative emotions have given me negative emotions, so I want them to go away. If this bit of positivity doesn't give you positive emotions, you must either hide your negative emotions or go away yourself.&quot; Praise can, counterintuitively, deepen a negative feeling.</p><p>To keep my praise authentic, I tend to be sparing with it, but as a dad I've had to learn to be more generous. Kind words still have to be spontaneous, not calculated, so I've chosen to get sensitive to what's worth praising. Trying an unfamiliar food. Keeping a screwdriver in the head of a screw.</p><p>I've also learned to give myself some kind words. It's easy for me to discount praise, to see it as motivated, to know it's part of a scheduled review cycle, to wish it was more substantive. But rejecting all praise leads to long, dark moods. It's important to recognize compliments from others even if it's sometimes tactical. Rarely is it outright dishonest. Kindness prompted by a complaint is still kindness.</p><p>At one company I worked for, we emailed positive remarks from a client out to the whole staff with a subject line of &quot;Nice words&quot;. One day I decided to adopt that practice for myself. Whenever someone gives a kind word, even if it was affirmation bombing, or a perfunctory farewell when changing jobs, or an annual review where the boss instructed everyone to review gently, I write it down. Tim Ferriss does something similar with a &quot;<a href="https://tim.blog/2018/01/01/the-tim-ferriss-show-transcripts-how-to-optimize-creative-output-jarvis-vs-ferriss/">Jar of Awesome</a>&quot;. I keep a digital version. Every now and then I come across one I'd forgotten. &quot;You have a nice reading voice.&quot; &quot;We should all be using your terminal setup.&quot; &quot;You've been a huge inspiration to me.&quot; &quot;I love the Eric notes.&quot; &quot;What you did is how I want this department to behave.&quot; &quot;You're probably my favorite writer.&quot; I can quote the project lead's original affirmation bomb word for word because I wrote it down in my nice words file.</p><p></p><figure class="figure-small"> <img src="/images/nice-words.svg" alt="Affirmation bombing into a Jar of Awesome full of nice words"> <figcaption>Even affirmation bombs are nice words.</figcaption> </figure><p></p><p>Some nice words were offhand comments, and while I may still discount a lot of compliments as general politeness, I do write down the things I find myself thinking about later. Fading memory doesn't mean the emotional boost of kind words also has to fade.</p><p>When I was a teenager, my father made me read Sean Covey's <i><a href="https://www.amazon.com/s?k=seven+habits+for+teens">The 7 Habits of Highly Effective Teens</a></i>, which used the metaphor of a relationship bank account: you have to make more deposits than withdrawals, and unlike an actual bank account, in relationships a deposit tends to evaporate and a withdrawal turns to stone. The same happens in our relationships with ourselves. For decades we remember an embarrassing moment, a typo in a tweet, a negative comment on a blog post, but we forget the compliments and nice words. Keeping a jar of awesome or a document of nice words is a way to combat that human weakness. Beyond that, documenting the positive things people say about you shows what others perceive to be your strengths, which can provide insight into how you can be your best self.</p><p>Sure there are bad days and low moods, but you are doing a good job!</p>]]></content>
  </entry>
  <entry>
    <id>https://iterativetangents.com/5-tips-for-sketchnoting</id>
    <link href="https://iterativetangents.com/5-tips-for-sketchnoting"/>
    <title>5 Tips for Sketchnoting</title>
    <updated>2026-05-15T08:00:00+00:00</updated>
    <content type="html"><![CDATA[<p>Back in 2013 I learned about <a href="https://en.wikipedia.org/wiki/Sketchnoting">sketchnoting</a> in a workshop at Midwest UX, and as someone who always doodled in my notes, giving meaning to the doodles felt like an obvious innovation I had overlooked, like putting wheels on luggage. After a few months' practice, I captured <a href="https://spin.atomicobject.com/sketchnoting-doodles-with-meaning/">some brief tips</a>. Since then, I've sketchnoted presentations from Agile Conf, local software meetups, company retreats, Clojure/conj, a handful of local one-day conferences, and a commencement speech. My style is mostly the same as it was ten years ago. And that's fine, because the point was never to get better at drawing, it was to capture notes more memorably. If you're interested in visual note-taking, here are five tips after 12 years and 100 sketchnotes.</p><p></p><figure class="figure-large"> <img src="/images/sketchnotes.jpg" alt="Sketchnotes of a talk about sketchnoting"> <figcaption>Sketchnotes about sketchnoting.</figcaption> </figure><p></p><h2>1. Draw to create focus</h2><p>It's easy to get distracted during a 40-minute or longer talk, but drawing creates focus by distracting the part of the brain that generates distractions. Doodling occupies the hand and the brain's visual processing. Even abstract patterns can serve as memory anchors, but infusing your doodles with content from the presentation locks it even deeper into memory, and it's more fun.</p><p>You don't have to be an artist to make sketchnoting fun or memorable. Simple drawings are great. Elaborate illustrations often detract from focus, rather than enhancing it.</p><p></p><figure class="figure-small"> <img src="/images/creation-of-adam-sketchnote.jpg" alt="Creation of Adam, as a sketchnote"> <figcaption><i>The Creation of Adam</i>, with apologies to Michelangelo.</figcaption> </figure><p></p><h2>2. There are many ways to be visual</h2><p>Sketchnotes frequently translate a presentation's visual elements into pictures, but many presentations are mostly text. Notes without a visual language can still be sketchnotes. Just capture text in visually interesting ways: make lists look like lists, underline important works, use an accent color for some words, write some words in ALL CAPS. Wrap sentences and fragments to leave room around them where you can write related concepts.</p><p></p><figure class="figure-small"> <img src="/images/text-focused-sketchnotes.jpg" alt="Text-focused sketchnotes"> <figcaption>Sketchnotes might be mostly text, but they can still have visual structure.</figcaption> </figure><p></p><p>A presentation is given linearly, one word after another, but the ideas have non-linear relationships that benefit from being captured in two dimensions. Paper allows you to de-linearize the content, even if you don't draw any pictures.</p><h2>3. Use pen and paper</h2><p>Using physical pen and paper is freeing. I've sketchnoted on tablets, but switching pens and colors is distracting. If the app allows editing—which most do because of the imprecision of stylus input—then it's tempting to clean up a drawing instead of listening. Paper has no distracting notifications or apps, and the battery never dies. You might not be able to erase, but mistakes can be memorable too.</p><p></p><figure class="figure-small figure-full-width"> <img src="/images/airplane-sketchnote.jpg" alt="An inked sketchnote with a mistake"> <figcaption>An inked airplane that needed a wing extension.</figcaption> </figure><p></p><p>I take sketchnotes on an 8&quot;x5&quot; index card that's blank on both sides. For pens I use basic utensils: a Pilot V5 and an orange felt-tip Paper Mate. Both provide smooth lines. The one fancier tool I use is a Copic N4 marker with a brush tip, to add some color or shadow to drawings, though when it's used up I'll probably look for a lighter shade of gray.</p><h2>4. Sketchnote for yourself, not others</h2><p>Sketchnotes are never detailed enough to reconstruct a talk, so they tend to have little meaning to those outside the audience. Even for those who did hear the presentation, your personal notations are likely to be opaque. You might share your sketchnotes with the speaker or other members of the audience, but don't expect anyone to use them as a reference. They're for you. The act of making them is the valuable part. Your memory of the event will be embedded in your own physical movements of drawing and writing. Share your notes in recognition of the time and energy the speaker put into their talk, and as a token of the time the audience spent together, but no one will get more out of your notes than you do.</p><p></p><figure class="figure-small"> <img src="/images/inverted-pyramid-sketchnote.jpg" alt="An inverted pyramid with some cryptic icons"> <figcaption>A tiny summary of the visuals on several slides, likely not intelligible to others.</figcaption> </figure><p></p><h2>5. Do it!</h2><p>You can start sketchnoting without experience, without tips, and without a grand reason. Do it for fun! Do it to enhance your memory of the occasion. Do it to create connection with others.</p><p>The purpose of sketchnoting isn't to become a master. You don't need artistic ability or perfect penmanship. The goal is to focus your attention, make you think more deeply about others' ideas, and have and share a memorable experience.</p><p>Sketchnoting is a gift to your future self.</p><p></p><figure class="figure-large"> <img src="/images/horizons-sketchnote.jpg" alt="Sketchnote of a ship facing progressively longer time horizons"> <figcaption>The important part is to start the journey.</figcaption> </figure><p></p><p>(For concrete tips on sketchnoting, see my <a href="https://spin.atomicobject.com/sketchnoting-doodles-with-meaning/">earlier post</a> and Mike Rohde's <i><a href="https://www.amazon.com/Sketchnote-Handbook-illustrated-visual-taking/dp/0321857895/ref=sr_1_1?dib=eyJ2IjoiMSJ9.IsC3ii8wWaWwpoD1z_mf3QJJ8-nMU5Sk2uttSUUyPiCqF1rIzJzPb19_lyIVlPgm4rVC3kDZdURdGfw6MPyt30ckefTDfVvJJ8HWuwlRLxI2ana2y2hkPkqaudABccjQwRcW3BgqbugBdYNQXtFGlwZOcxoWOUVn2zYbEfhEKICDa77vypNrvW0blbDvjhavNROGwHDb8F-EAF9jl-9tHtJHt8INGeM0j3tWX0IT2Fs.ZRobYhbV1DBXQ7v-S3QcbjTWik-TdMeDLjoDY-2NQZ4&amp;dib_tag=se&amp;keywords=the+sketchnote+handbook&amp;qid=1778803680&amp;sr=8-1">The Sketchnote Handbook</a></i>.)</p>]]></content>
  </entry>
</feed>
