<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<?xml-stylesheet href="/feed.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Nathan Manceaux-Panot</name>
    <uri>https://cykele.ro/</uri>
  </author>
  <id>urn:uuid:64900eb8-2ce5-477c-a4c6-0f9dcd541876</id>
  
  <link rel="self" type="application/atom+xml" href="https://pending.design/feed.atom.xml" hreflang="en-us" />
  
  
  <link rel="alternate" type="text/html" href="https://pending.design/" hreflang="en-us" />
  
  <icon>https://pending.design/images/icon-512.png</icon>
  
  
  <title>Pending Design</title>
  <updated>2026-03-11T16:23:44+00:00</updated>
  
  <entry>
    <author>
      <name>Nathan Manceaux-Panot</name>
      <uri>https://cykele.ro/</uri>
    </author>
    <id>tag:pending.design,2026:/swiftui-memoization/</id>
    <link rel="alternate" href="https://pending.design/swiftui-memoization/" />
    <title>Memoizing computed properties in SwiftUI</title>
    <published>2026-01-19T14:59:02+01:00</published>
    <updated>2026-01-19T14:59:02+01:00</updated>
    <summary type="text">A ready-to-use property wrapper.</summary>
    <content type="html" xml:base="https://pending.design/" xml:lang="en">
      <![CDATA[<p>The SwiftUI view I was writing had gotten too slow. Any time it rendered, it spent a long time calculating one of its computed properties:</p>
<pre><code class="language-swift">struct ContentView: View {
	var input: Int
	var unrelated: Int
	
	var expensive: Int {
		input * 2 // let’s pretend that multiplication is super slow
	}
	
	var body: some View {
		Text(&quot;Expensive: \(expensive)&quot;)
		Text(&quot;Unrelated: \(unrelated)&quot;)
	}
}
</code></pre>
<p>Sure, whenever <code>input</code> changed, that expensive multiplication had to be recalculated; after all, I was using <code>expensive</code> in the view body. But if only <code>unrelated</code> had a new value? The view really ought to skip the <code>expensive</code> computation, and reuse the previous result.</p>
<p>You’d think there was a built-in way to perform this sort of caching, this <em>memoization</em>; or a ready-to-use code snippet online; but I couldn’t find any. So here’s my take!</p>
<h2 id="using-memoize">Using <code>@Memoize</code></h2>
<p>After copying the source (see below) into your codebase, using it only requires two changes:</p>
<ol>
<li>Add a <code>@Memoize</code> property, with the computed value’s type. This stores the cache.</li>
<li>In the expensive getter, wrap the computation with a call to the newly-added property wrapper, passing all the input values as arguments.</li>
</ol>
<pre><code class="language-swift">struct ContentView: View {
	var input: Int
	var unrelated: Int
	
	@Memoize() private var expensiveCache: Int // 1. Add this
	
	var expensive: Int {
		_expensiveCache(input) { // 2. Wrap in that
			input * 2
		}
	}
	
	var body: some View {
		Text(&quot;Expensive: \(expensive)&quot;)
		Text(&quot;Unrelated: \(unrelated)&quot;)
	}
}
</code></pre>
<p>That’s it! Now, whenever the <code>expensive</code> property is accessed, it’ll first check the cache for an up-to-date value, skipping the closure execution if none of the inputs changed. If there are expensive calculations in some of your views, this can make quite the difference.</p>
<p>Do make sure you list all the input values (here, just the <code>input</code> variable, but you can pass as many as needed). Otherwise, the output value won’t update when it should. The only requirement for these inputs is <code>Hashable</code> conformance.</p>
<p>For types that can’t conform to <code>Hashable</code>, or hash too slowly, you can pass a single <code>Equatable</code> value instead. To enable this, you’ll first need to tell the property wrapper about the key type: <code>@Memoize(key: SomeInput.self)</code>. The downside is increased memory usage, as the full input value will be stored as a cache key, instead of just a tiny hash.</p>
<h2 id="the-source">The source</h2>
<p>Download the ready-to-use <a href="Memoize.swift" download>Memoize.swift</a>
 file, or read the code and implementation notes below if you’re curious.</p>
<h3 id="memoizeswift">Memoize.swift</h3>
<pre><code class="language-swift">@propertyWrapper
struct Memoize&lt;Key: Equatable, Value&gt;: DynamicProperty {
	var wrappedValue: Value {
		fatalError(&quot;To use @Memoize, call it as a function, passing the input value(s), and a closure to produce the value&quot;)
	}
	
	@State @Boxed private var cache: (key: Key, value: Value)?
	
	init(key keyType: Key.Type = Int.self) {}
	
	private func getMemoized(key: Key, produceValue: () throws -&gt; Value) rethrows -&gt; Value {
		if let cache, cache.key == key {
			return cache.value
		}
		
		let newValue = try produceValue()
		cache = (key: key, value: newValue)
		return newValue
	}
	
	func callAsFunction(_ key: Key, produceValue: () throws -&gt; Value) rethrows -&gt; Value {
		return try getMemoized(key: key, produceValue: produceValue)
	}
	
	func callAsFunction(
		_ key: any Hashable...,
		produceValue: () throws -&gt; Value
	) rethrows -&gt; Value where Key == Int {
		var keyHasher = Hasher()
		
		for keyPart in key {
			keyHasher.combine(keyPart)
		}
		
		return try getMemoized(key: keyHasher.finalize(), produceValue: produceValue)
	}
}

@propertyWrapper
fileprivate class Boxed&lt;Wrapped&gt; {
	var wrappedValue: Wrapped
	
	init(wrappedValue: Wrapped) {
		self.wrappedValue = wrappedValue
	}
}
</code></pre>
<h3 id="implementation-notes">Implementation notes</h3>
<ul>
<li>Yes, that’s a box in a state. The state is necessary because we want the cache to persist across renders—it’d be pointless otherwise. But, we need to update that state during view updates, which isn’t supported by SwiftUI. To get around this, we wrap the cache in a reference type—that’s the box—enabling us to update it at any time, without ever modifying the actual contents of the state itself.</li>
<li><code>Memoize</code> is a <code>DynamicProperty</code>: this allows it to use SwiftUI property wrappers, such as <code>@State</code>.</li>
<li>The unusually-implemented <code>wrappedValue</code> is only there to help set the value type. It lets us type <code>@Memoize() private var expensiveCache: Int</code> instead of <code>@Memoize(value: Int.type) private var expensiveCache</code>. Arguably nicer.</li>
<li>Those empty parens in the <code>@Memoize()</code> call are necessary: it seems that without them, Swift doesn’t use our explicit init, and therefore skips the default key type.</li>
<li>To make calls to the memoization function more concise, we’re using <code>callAsFunction</code>, and an <code>any Hashable...</code> variadic parameter. Also, the wrapper takes care of hashing these input values, so you can have more than one input without needing to group them yourself, like you would for instance with <code>onChange(of:)</code>.</li>
</ul>
<p>Hopefully you find this bit of code useful. If you do try it, I’d love to hear your feedback!</p>
]]>
    </content>
  </entry>
  
  <entry>
    <author>
      <name>Nathan Manceaux-Panot</name>
      <uri>https://cykele.ro/</uri>
    </author>
    <id>tag:pending.design,2025:/retcon-icon-tahoe-update/</id>
    <link rel="alternate" href="https://pending.design/retcon-icon-tahoe-update/" />
    <title>Updating Retcon’s icon for Tahoe</title>
    <published>2025-12-12T10:12:33+01:00</published>
    <updated>2025-12-12T10:12:33+01:00</updated>
    <summary type="text">New constraints, new inspiration.</summary>
    <content type="html" xml:base="https://pending.design/" xml:lang="en">
      <![CDATA[<p>Icon design is a surprisingly pragmatic process. It’s all about finding concrete solutions, to issues you encounter while trying to reach specific goals. (and also you want to make things very pretty)</p>
<p>Updating <a href="https://retcon.app/">Retcon</a>’s icon for macOS 26 Tahoe involved a lot of trial and error, a lot of resolving (and creating) problems. So I recorded this short tour of some of the versions the icon went through, and the reasoning for everything. Give it a watch!</p>

<div style="position: relative; padding-top: 95.21%;"><iframe title="Updating Retcon’s icon for Tahoe" width="100%" height="100%" src="https://spectra.video/videos/embed/h7k3Kz4U84LqnwLDdLXjFm" style="border: 0px; position: absolute; inset: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe></div>


<p>If you enjoy this walkthrough, you’ll also like reading about <a href="/retcon-icon-process/">the making of the original icon</a>.</p>
]]>
    </content>
  </entry>
  
  <entry>
    <author>
      <name>Nathan Manceaux-Panot</name>
      <uri>https://cykele.ro/</uri>
    </author>
    <id>tag:pending.design,2025:/making-retcon-fast-caches/</id>
    <link rel="alternate" href="https://pending.design/making-retcon-fast-caches/" />
    <title>Making Retcon fast: A cache for every need</title>
    <published>2025-10-15T15:41:54+02:00</published>
    <updated>2025-10-15T15:41:54+02:00</updated>
    <summary type="text">Big speedups through bespoke caches.</summary>
    <content type="html" xml:base="https://pending.design/" xml:lang="en">
      <![CDATA[<p>Retcon 1.0 was decently fast in modestly-sized repositories, but <em>appallingly</em> slow for anything big. You’d have to watch the app freeze for an agonizing 8.82 seconds to merely rename a commit in the Swift repo. <a href="https://indieapps.space/@Retcon/115056221545845136">In Retcon 1.4</a>, that same rename now completes in 448 forgettable milliseconds; a 1868% improvement. What’s the secret there?</p>
<p>Well, there’s no single thing. Months of work resulted in numerous, surprisingly diverse optimizations; some expected, some eyebrow-raising. Despite that variety, however, one kind of optimization ended up the most common by far: caches.</p>
<aside>
	(If you’re unfamiliar with Retcon, it&rsquo;s a new kind of Git client for macOS. It lets you manipulate your commit history with ease, removing all friction to creating meaningful, useful commits. More on <a href="https://retcon.app/">the Retcon website</a>.)
</aside>

<h2 id="many-values-many-caches">Many values, many caches</h2>
<p>Retcon now has numerous in-memory caches. Anything that’s slightly expensive to compute is stored in memory, to be reused the next time it’s needed. To make sure these cached values don’t go out of date, they’re either discarded whenever the input data changes, or are stored alongside a fingerprint of the input data, to allow checking validity on retrieval.</p>
<p>One of Retcon’s most important classes is <code>VirtualizedRepository</code>. <abbr>VR</abbr> objects mediate access to Git repositories, while transparently layering Retcon’s own data on top. They’re core to allowing Retcon to display and manipulate states that Git itself can’t represent, such as a commit histories with a conflict midway through.</p>
<p>Because of this, a <abbr>VR</abbr> exposes many different properties, most derived from on-disk data, that get accessed quite heavily. And so these properties are cached: the head commit list is cached as an array, allowing for random access, instead of requiring walking the history one commit at a time through parent lookups. The repository’s tags are cached in a purpose-made map, <code>cachedTagsBySHA1</code>, that makes it very fast to look up a given commit’s tags, instead of needing to filter through the repository’s complete tag list every time. There’s the precisely-named <code>cachedPhysicalSecondaryHeadAncestorsByMergeCommitLineage</code>—a map of the commits that were merged into the current branch, keyed by the identity<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> of their merge commit. Purpose-made, once again. And there’s even a cache of the behind/ahead counts, that are eventually displayed on the pull/push buttons—if your branch has significantly diverged from its upstream, finding the closest shared parent can take a while.</p>
<p>Many of the <abbr>VR</abbr> caches are regenerated from scratch whenever any repository data changes, so that they’re always up to date. But some are handled with more granularity, for performance.</p>
<p>The head commit list cache is updated, not discarded. When a repo changes, the vast majority of the history usually stays the same; so Retcon holds onto the current cache’s tail of unchanged commits, and prepends new and modified commits<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>.</p>
<p>The merged commit map is never proactively built, but instead gets populated as data is requested. This is because in Retcon, merge commits are initially displayed collapsed: their child list is only needed once the user toggles them open.</p>
<h2 id="rigorously-not-measuring-text">Rigorously not measuring text</h2>
<p>Caches are useful for more than abstract, internal state. Computing the layout of <abbr>UI</abbr> elements can also be expensive, once again inviting caches; Retcon’s best example is the measuring of diff hunk heights.</p>
<aside>
	(In Git diffs, modified files aren’t displayed in their entirety, but are instead shown as a series of <em>hunks:</em> snippets from the file, focused on a few adjacent changed lines, with some surrounding context. A diff may contain just one such hunk, or many of them; each hunk can range from a handful of lines, to thousands or more.)
</aside>



<figure class="no-frame"><picture>
    <source srcset="hunks-in-retcon-dark.png"
      alt="Screenshot from Retcon, the macOS Git client, showing the diff view. Two HTML files are displayed. The first one with a single hunk, with a single change; the second file has two hunks, which have disjoint line numbers." width="832" height="589"media="(prefers-color-scheme: dark)"
    >
    <img src="hunks-in-retcon-light.png"
      alt="Screenshot from Retcon, the macOS Git client, showing the diff view. Two HTML files are displayed. The first one with a single hunk, with a single change; the second file has two hunks, which have disjoint line numbers." width="832" height="589">
  </picture><figcaption>
      <p>Three hunks, from two different files.</p>
    </figcaption>
</figure>

<p>Now, while generating diff hunks is fast, computing their on-screen height is not. Retcon doesn’t let the text scroll horizontally, instead wrapping it at the window edge; this means that to know the total pixel height of a hunk, the layout system must scan through the entirety of its text, determining line wrapping points, and therefore the number of lines the hunk actually spans<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>. This is a very, very slow process for large amounts of text.</p>
<p>Which brings us back to caches. It’s a no-brainer to cache the output of this expensive measuring process, as a given hunk will often stay the exact same when the diff view is refreshed. Here’s the tricky part, though: what does it mean to be the <em>exact same</em>? If the hunk’s text remains unchanged, but the window becomes wider, then we should redo the measurement—more horizontal space means less wrapping. If one of the hunk’s neighbors changes in content, we ostensibly don’t need to reflow the unchanged hunk itself—unless the neighbor’s <em>line numbers</em> are now different, since these dictate the width of the line number gutter, which is shared by all of a file’s hunks. So many variables to take into account, coming from such distinct sources!</p>
<p>So we need to invalidate our cache based on these diverse factors, and do so with precision: while the height must never go out of date, we also want to avoid costly superfluous invalidations. To achieve this granularity, Retcon makes use of a supporting method, that fetches all the relevant inputs for a given hunk, and mixes them into a single integer:</p>
<pre><code class="language-swift">\showLineNumbersStartingFrom: 624
/// A hash of all the properties that have an impact on this hunk's view height.
func heightDependenciesHash(
	forHunkAtIndex index: Int,
	withParentWidth parentWidth: CGFloat
) -&gt; Int? {
	guard let viewHunk = viewHunks[safeIndex: index]
		else { assertionFailure(); return nil }
	
	var hasher = Hasher()
	hasher.combine(parentWidth)
	hasher.combine(contextIsDough)
	hasher.combine(effectiveForceDisplayingHunks)
	viewHunk.hunk.combineText(into: &amp;hasher)
	hasher.combine(hunkGutterWidth)
	return hasher.finalize()
}
</code></pre>
<p>The method essentially captures the complete context in which a hunk is measured, and represents it as a hash. This allows us to store the hash alongside the cached height, and use it as the cache key: whenever the cache is read back, we first check if the stored hash matches the current output of the method. If the two differ, it means the cached height is stale, and we need to take a fresh measurement.</p>
<p>The nice thing about this approach is that it neatly encapsulates all knowledge about hunk height calculation. While this listing of factors is itself duplication (after all, the window width will affect hunk height regardless of what we hash in the method), it does prevent duplicating and scattering that information any further. It’s the one, canonical way of representing hunk cache freshness.</p>
<h3 id="unrigorously-measuring-text">Unrigorously measuring text?</h3>
<p>In a slightly different world, we could actually skip measuring most hunks. If a hunk isn’t in view, all we need is an approximation of its height, so that we can properly size the scroll bar’s thumb.</p>
<p>Retcon has a way of producing such an estimate: it maintains <code>estimatedRowHeightToLineCountRatio</code>, a cache of the average ratio between the pixel height of a hunk, and the number of line break characters it contains, based on hunks measured so far. We always know how many line breaks a hunk has, so it’s extremely cheap to get an idea of its height using this value.</p>
<p>However, this technique turns out to be unusable. AppKit’s <code>NSTableView</code> class, that Retcon uses to display the list of hunks, requires us to provide <em>exact</em> heights, not estimates, when enumerating our hunks<sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>. If the values we provide are off by even a few pixels, the view will constantly stutter and jump when scrolled, making for a terrible experience. Oh well.</p>
<h2 id="the-long-tail">The long tail</h2>
<p>There’s many more caches in Retcon. They all bring welcome performance boosts, but most aren’t especially interesting. Here’s one last story for the road.</p>
<p>The welcome window, with its list of recently-opened repositories, could be slow to appear, sometimes stalling for more than a second. The view code would read the system-provided <code>recentDocumentURLs</code> list many times per refresh, with the assumption that the access would be instant; it was not. One <code>cachedRecentDocumentURLs</code> later, and the window now shows up without delay. A welcome improvement!</p>
<p>There’s a lot more to Retcon 1.4 than caches, though; we’ll look at the many other optimizations in future blog posts. Follow along on <a href="https://mas.to/@Cykelero">Mastodon</a>, <a href="https://bsky.app/profile/cykele.ro">Bluesky</a> or <a href="/feed.atom.xml">Atom</a>, and <a href="https://retcon.app/">get Retcon now</a> to see firsthand how fast it’s become.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>In regular Git, nothing links a rewritten commit to its source commit: they’re entirely unrelated. A commit’s hash completely changes any time it’s even slightly modified. Retcon works around this by assigning each commit a unique identifier called a <em>lineage <abbr>ID</abbr></em>, which it keeps stable across rewrites. This is primarily to allow animating large history changes, such as rebases, to make it much clearer what’s happening.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>Would you know it: updating the head commit cache makes use of <em>another</em> cache, <code>cachedPhysicalHeadCommitIndicesBySHA1</code>, that’s maintained solely for this purpose.</p>
<p>To determine where the unchanged tail of the new history starts, Retcon needs to walk commits one by one, starting with the head, checking whether each is already contained in the cache. Such a lookup would be very slow if done naively (“for each new commit, iterate over the current cache to see if it’s in there”), so instead we’re using this map, which makes the lookup vastly faster.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>The text layout process, handled by Apple’s TextKit 2, also accounts for varying typographic bounds. While lines of only <abbr>ASCII</abbr> characters always have the same height, adding a Chinese character or an emoji will use a different font, often making the line taller. So even if we disabled line wrapping, calculating a hunk’s total height would be more complicated than just <code>hunkLinebreakCount</code> × <code>lineHeight</code>.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:4">
<p>Retcon’s diff view implements the table view delegate method <code>tableView(_:heightOfRow:)</code>, which expects an exact height.<br>
In UIKit, <code>tableView(_:estimatedHeightForRowAt:)</code> exists precisely for communicating estimates like ours, but AppKit has no equivalent.&#160;<a href="#fnref:4" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]>
    </content>
  </entry>
  
  <entry>
    <author>
      <name>Nathan Manceaux-Panot</name>
      <uri>https://cykele.ro/</uri>
    </author>
    <id>tag:pending.design,2025:/clipboard-manager-uses/</id>
    <link rel="alternate" href="https://pending.design/clipboard-manager-uses/" />
    <title>The other uses of a clipboard manager</title>
    <published>2025-09-11T15:36:20+02:00</published>
    <updated>2025-09-11T15:36:20+02:00</updated>
    <summary type="text">Go beyond the obvious, and make your life nicer.</summary>
    <content type="html" xml:base="https://pending.design/" xml:lang="en">
      <![CDATA[<p>You know that clipboard managers are useful tools, but you might not know how useful. They’re for much more than swapping two variable names around—turns out, having the ability to <em>instantly persist any piece of text</em> gives you a lot of new abilities. So here’s a list that covers the obvious, then the creative.</p>
<h2 id="the-obvious">The obvious</h2>
<ul>
<li><strong>Text swapping</strong> becomes easy. Copy A, copy B; paste B over A, then recall A and paste it over B.</li>
<li><strong>Peace of mind.</strong> When your clipboard holds an important item you mustn’t discard, you no longer need to consciously stop yourself from copying anything. It’s OK if you forget and override that item; it’s OK to switch to a quick side-task that requires the clipboard, or to reboot your computer, or let someone else use it. Your clipboard is no longer <em>fragile</em>. To some people, this makes a world of difference; it’s one less thing to actively keep in mind.</li>
</ul>
<h2 id="the-creative">The creative</h2>
<p>That was the expected. Leaning on your clipboard manager, though, you can start using your computer differently.</p>
<ul>
<li><strong>Long recall.</strong> If you can remember manipulating some piece of text, there’s a chance you copied it. So if you can recall just a few unique letters from it, that clip can be instantly recalled, using the clipboard manager’s search feature.<br>
An email address you copied? Often, just type the person’s name. A URL you recently shared on a chat? The website name. That Terminal command to clear a file’s quarantine flag that you keep looking up online<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>? Try “quarantine” and it should be there.<br>
The list goes on! Without any upfront effort of your part, so much of the interesting text you manipulate can later be recalled in just a few keystrokes. The longer your clipboard history, the better, too; I’ve configured my clipboard manager to keep items for <em>months</em>, so if I remember something, it’s in there for sure.</li>
<li><strong>Instant backup.</strong> Ever lost text to a flaky Web form after spending half an hour composing it? Well, you now have the means to <em>instantly backup any piece of text:</em> <kbd>⌘A</kbd>, <kbd>⌘C</kbd>. That’s it! It’s so fast, you can do it all the time. And now you won’t ever again lose anything you’ve written. <kbd>⌘A</kbd>, <kbd>⌘C</kbd>. Your own instant, ubiquitous backup system.</li>
<li><strong>Regret-proof delete.</strong> This builds up on the previous item. Now, whenever I delete any piece of text that’s longer than a couple words, I do so using <kbd>⌘X</kbd>. That goes for code, or unsent chat messages, or Web searches. Most of the time this is pointless; but often enough, I’ll be happy to save some thinking and typing, when I realize that actually, that first draft <em>was</em> in the right direction. Anything you delete by cutting can be undeleted, and there’s no cost to doing it.</li>
<li><strong>Photographic memory.</strong> And it’s not just about text: I’ll often take a screenshot of a settings windows right before I tweak something. <kbd>⇧⌘4</kbd>, Space, then Control-click: I instantly captured a reference of how things were configured, before I went and messed them up. My future self often thanks me.</li>
</ul>
<p>So many uses out of that single simple system! I think that’s really cool. I hope you find these ideas useful; and if you have your own schemes, please do share them with me on <a href="https://mas.to/@Cykelero">Mastodon</a> or <a href="https://bsky.app/profile/cykele.ro">Bluesky</a>. I’ll be happy to use my clipboard manager even more.</p>
<h2 id="appendix-what-clipboard-manager-to-use">Appendix: What clipboard manager to use?</h2>
<p>The app I personally use does the job brilliantly; however, it stores all of its data unencrypted on the file system, and the developers have stated they didn’t intend on fixing the issue, so I’m really not comfortable recommending it.</p>
<p>Fortunately, there’s plenty of clipboard managers out there! Find one that’s got very fast search, and a large maximum history size, and you’ll be set.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p><code>xattr -rds com.apple.quarantine FILE_PATH</code>&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]>
    </content>
  </entry>
  
  <entry>
    <author>
      <name>Nathan Manceaux-Panot</name>
      <uri>https://cykele.ro/</uri>
    </author>
    <id>tag:pending.design,2025:/stories-of-making/</id>
    <link rel="alternate" href="https://pending.design/stories-of-making/" />
    <title>Stories of making</title>
    <published>2025-07-30T16:18:25+02:00</published>
    <updated>2025-07-30T16:18:25+02:00</updated>
    <summary type="text">I finally have a blog!</summary>
    <content type="html" xml:base="https://pending.design/" xml:lang="en">
      <![CDATA[<p>I’ve always loved reading stories of people making things. The clever displays of creativity; the intoxicating sense of <em>possibility</em> that they evoke. They make the world feel more open-ended.</p>
<p>I <em>also</em> really like telling people all about the things I’m making. There’s a lot of pleasure in describing, in great detail, the goals, the obstacles, the breakthroughs, even the vexing compromises.</p>
<p>Up until now, though, it’s mostly been my friends’ ears that I’ve been talking off. With this blog now online, it can finally be yours too!</p>
]]>
    </content>
  </entry>
  
  <entry>
    <author>
      <name>Nathan Manceaux-Panot</name>
      <uri>https://cykele.ro/</uri>
    </author>
    <id>tag:pending.design,2023:/retcon-icon-process/</id>
    <link rel="alternate" href="https://pending.design/retcon-icon-process/" />
    <title>Creating Retcon’s icon</title>
    <published>2023-11-23T12:00:00+02:00</published>
    <updated>2023-11-23T12:00:00+02:00</updated>
    <summary type="text">The process of making a Mac app icon.</summary>
    <content type="html" xml:base="https://pending.design/" xml:lang="en">
      <![CDATA[<aside>
	(This article was originally published on Dribbble. It was imported on this blog on December 12th, 2025.)
</aside>

<figure><img src="/retcon-icon-process/hero.png"
    alt="The app icon of macOS app Retcon. It depicts a small whiteboard, with a git branch symbol drawn on it. A marker is placed on top. The symbol, marker, and the corners of the board are purple." width="800" height="600">
</figure>

<p>This is the icon for <a href="http://retcon.app/">Retcon</a>, the macOS app for rewriting git history at the speed of thought. My first Mac icon! Rendered in Sketch, with reference renders in Blender.</p>
<p>I followed a process described in <a href="https://bjango.com/articles/macappiconworkflow/">an article from Marc Edward</a>: start with concept sketches, create a 3D version of your icon as a reference, then render the final icon in vectors.<br>
The 3D reference allows the icon to have realistic shapes and lighting—as seen on the pen—but the vector re-render means the icon&rsquo;s look can be fully controlled; great for aesthetics, and legibility.</p>
<h2 id="concept">Concept</h2>
<p>Retcon allows you to modify git history very fast, with the same ease you&rsquo;d move a folder in the Finder, or rename a file. That&rsquo;s what motivated many of the concepts: splicing a movie, tearing pages from a book, redacting a page, etc. The app&rsquo;s all about fast editing.</p>
<p>Ultimately, the whiteboard felt like the clearest metaphor. A close second was the blackboard, but while that might have made for a more visually pleasant icon, it also felt less modern, which wasn&rsquo;t the appropriate for the app.</p>
<p>Some of my favorite concepts are the whiteboard titled “Progress”, and the blackboard being erased; but, while they&rsquo;re pretty compelling as rough sketches, these concepts would probably have been much too busy as actual icons.</p>
<figure><img src="/retcon-icon-process/concepts.jpeg"
    alt="A series of rough sketches depicting various potential app icons: whiteboards, books, a calendar, etc, with different tools on top (a pen, a pencil, a marker, various erasers), and a bunch of miscellaneous symbols." width="800" height="450">
</figure>

<h3 id="symbol">Symbol</h3>
<p>The whiteboard features a git “branch” icon, which feels like a rather obvious choice in retrospect. It immediately ties the icon to git, and slightly hints at history and branch manipulation.</p>
<p>I explored many symbols, though, before settling on this one. Most notably, I looked for a less direct depiction of a git history; something more concrete and relatable, like the tree-related symbols. I eventually decided that the absolute clarity of the actual symbol for a git branch worked perfectly, in the context of the icon. In Retcon, you&rsquo;re literally erasing and rewriting branches!</p>
<p>Another very tempting choice was Apple&rsquo;s “A” app symbol. It evokes development very clearly, and has strong (and hopefully positive) associations.<br>
However, it evokes <em>app</em> development, rather than software development in general, which is too specific for Retcon; and it feels owned by Apple, at least in culture, if not as a trademark.</p>
<h2 id="a-vector-previz-and-a-3d-render">A vector previz, and a 3D render</h2>
<p>These really helped nail down the icon&rsquo;s layout, scale, and overall look. When then rendering the final icon, I heavily referenced the pen, for realism, but mostly made up the board, which wasn&rsquo;t modeled very interestingly in the 3D model.</p>
<figure><img src="/retcon-icon-process/previsualization-and-reference-render.png"
    alt="A bunch of unfinished drawings and 3D renders of the Retcon icon." width="800" height="600">
</figure>

<h2 id="final-render">Final render</h2>
<figure><img src="/retcon-icon-process/final-render.png"
    alt="A bunch of unfinished versions of the Retcon icon." width="800" height="600">
</figure>

<h2 id="balancing-viewing-scales">Balancing viewing scales</h2>
<p>Although Mac icon files can contain many variations, that the system then selects from depending on icon display size, I decided to only create a single size. (or perhaps forgot that you could create these variations. events unclear!)</p>
<p>That meant the icon had to work both when displayed large (as a marketing asset, or when inspecting by the user using Quick Look) and small (in the Dock, in the Finder).</p>
<p>Of the two, the smaller size was definitely the more important one, being the icon&rsquo;s usual display scale. That meant sacrificing good-looking small details, if they muddied the icon when viewed small.</p>
<p>You can see this tension in action in the two-up comparison below. A WIP shot is on the left, and the final icon is on the right.<br>
The final icon is coarser, its various borders thicker, its shadows heavier. The end result is a lot less elegant when viewed up close, but vastly more legible when viewed at Dock size. The process of removing subtlety was painful, but ultimately made for a much better icon!</p>
<figure><img src="/retcon-icon-process/scale-comparison.png"
    alt="A comparison of two versions of the Retcon icon; each version is showed in full scale, and at a small scale. The version on the left has much finer features and details; its rendering is more subtle. The version on the right, however, is much nicer to look at from afar: its details remain visible, whereas the left version becomes muddy and hard to interpret when small." width="800" height="600">
</figure>

<h2 id="objectives">Objectives</h2>
<p>Going in, I had objectives in mind; some essential, some optional. The icon definitely doesn&rsquo;t fulfill them all, but compromises are core to design, no?</p>
<p>The essential goals are predictable:</p>
<ul>
<li><strong>Be a functional icon:</strong> Be recognizable, legible at small sizes, and evocative of the app itself.  <span style="font-size: 0.65em; padding: 0 2px 0 3.5px; vertical-align: 2px">◆</span> 
 That one&rsquo;s a go! As the most important goal, it forced quite a few decisions, as described above.</li>
<li><strong>Look at home on macOS:</strong> The system has its own design language. It&rsquo;s a rather strongly-defined one, although there&rsquo;s still a lot of leeway for experimentation.  <span style="font-size: 0.65em; padding: 0 2px 0 3.5px; vertical-align: 2px">◆</span> 
 I think the icon does just fine here. It&rsquo;s realistic but not actual 3D, and respects the round rect while playing with it a little. The pen really helps, too—more on that below.</li>
</ul>
<p>The optional goals were a bit more personal:</p>
<ul>
<li><strong>Use varied materials:</strong> While assembling a board of reference material, I kept encountering app icons that felt nice, but… were somehow just short of great. Eventually, I realized that they often fell into the trap of having every component rendered the same way: the whole icon would look like a plastic model of a thing, instead of looking like the thing itself. To counter that effect, I really wanted the icon to feature different materials, of which the different properties would contrast in the final render.  <span style="font-size: 0.65em; padding: 0 2px 0 3.5px; vertical-align: 2px">◆</span> 
 I don&rsquo;t think I did an excellent job, here. There&rsquo;s certainly diversity: slightly rough metal for the board frame, shiny white plastic for the pen&rsquo;s body, and some variations of plastic and paper. However, especially when viewing the icon at a small scale, the material differences barely stand out; only the metal frame&rsquo;s telltale shine makes it stand out from what otherwise looks all-rubber, or all-plastic.</li>
<li><strong>Include a tool:</strong> Some of my favorite Mac apps have a tool on their icon. It&rsquo;s an old convention, that screams “this app will let you <em>make</em> things”. I love creation apps, and I love this association—so it was important to me for Retcon&rsquo;s icon to feature a tool.  <span style="font-size: 0.65em; padding: 0 2px 0 3.5px; vertical-align: 2px">◆</span> 
 I&rsquo;m happy I got to do this one! Luckily, the best concepts for the icon all made including a tool natural, if not necessary. The most observant will notice that the pen is shifted right from the standard, Big Sur-style tool location, but the appropriate 24° angle is respected!</li>
<li><strong>Show ample color:</strong> It&rsquo;s so nice to look at something colorful. I wanted the icon to have a well-defined dominant color, and feature it prominently, with a large portion of the icon being painted.  <span style="font-size: 0.65em; padding: 0 2px 0 3.5px; vertical-align: 2px">◆</span> 
 Well, that one&rsquo;s a wash—the icon does have a clearly-communicated tint, but it&rsquo;s not used over large areas; instead, the most represented color by far is classic <em>whiteboard white</em>. Gotta pick your battles!</li>
</ul>
]]>
    </content>
  </entry>
  
</feed>
