<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>~iany/</title><link>https://blog.iany.me/</link><description>Recent content in ~iany/</description><language>en-US</language><managingEditor>me@iany.me (Ian Yang)</managingEditor><webMaster>me@iany.me (Ian Yang)</webMaster><copyright>CC-BY-SA 4.0</copyright><lastBuildDate>Fri, 20 Feb 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://blog.iany.me/index.xml" rel="self" type="application/rss+xml"/><item><title>Power of Monoid, Beauty of Simplicity</title><link>https://blog.iany.me/2026/02/power-of-monoid-beauty-of-simplicity/</link><pubDate>Fri, 20 Feb 2026 00:00:00 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/02/power-of-monoid-beauty-of-simplicity/</guid><description>&lt;p&gt;A monoid is one of the smallest useful abstractions in algebra: a set closed under an associative binary operation, with an identity element. That simplicity is exactly why it shows up everywhere—from summing numbers and concatenating strings to powering divide-and-conquer algorithms and elegant data structures like finger trees. This post walks through what monoids are, why they give you &amp;ldquo;compute power&amp;rdquo; for free when you can phrase a problem in terms of them, and how to think about choosing the right monoid and predicate when you do.&lt;/p&gt;
&lt;h2 id="what-is-a-monoid"&gt;What is a monoid?&lt;/h2&gt;
&lt;p&gt;A monoid is a set &lt;code&gt;$S$&lt;/code&gt; equipped with a binary operator &lt;code&gt;$\bullet$&lt;/code&gt; and an identity element &lt;code&gt;$e$&lt;/code&gt; (&lt;a href="https://en.wikipedia.org/wiki/Monoid"&gt;Wikipedia&lt;/a&gt;).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The operator is closed on &lt;code&gt;$S$&lt;/code&gt;. For all &lt;code&gt;$a, b \in S$&lt;/code&gt;, the result &lt;code&gt;$a \bullet b$&lt;/code&gt; is also in &lt;code&gt;$S$&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The operator is associative: For all &lt;code&gt;$a,b,c \in S$&lt;/code&gt;, &lt;code&gt;$(a \bullet b) \bullet c = a \bullet (b \bullet c)$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The identity element &lt;code&gt;$e$&lt;/code&gt; satisfies &lt;code&gt;$e \bullet a = a \bullet e = a$&lt;/code&gt; for all &lt;code&gt;$a \in S$&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, The integer numbers with the operator addition (&lt;code&gt;+&lt;/code&gt;) is a monoid, where the identity element is &lt;code&gt;0&lt;/code&gt;. The integer numbers with the operator multiplication (&lt;code&gt;x&lt;/code&gt;) is also a monoid with the identity element &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The set of finite lists with the operator concatenation is a monoid since:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The operator is closed because concatenation of two finite lists is also a finite list.&lt;/li&gt;
&lt;li&gt;The operator is associative, because both &lt;code&gt;$(a \bullet b) \bullet c$&lt;/code&gt; and &lt;code&gt;$a \bullet (b \bullet c)$&lt;/code&gt; result in a new list by placing elements of &lt;code&gt;$a, b, c$&lt;/code&gt; consecutively.&lt;/li&gt;
&lt;li&gt;The identity element is the empty list.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The integer numbers with the operator &lt;code&gt;max&lt;/code&gt; is a counterexample. The operator is closed and associative, but there is no identity element. Given any integer &lt;code&gt;$e$&lt;/code&gt;, there&amp;rsquo;s always a smaller integer &lt;code&gt;$a$&lt;/code&gt; such that &lt;code&gt;$e \bullet a = e \ne a$&lt;/code&gt;. However, &lt;code&gt;max&lt;/code&gt; on the integer set with a lower bound is a monoid, such as the non-negative integers where the identity element is the lower bound &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="divide-and-conquer-why-associativity-matters"&gt;Divide and conquer: why associativity matters&lt;/h2&gt;
&lt;p&gt;At first glance, associativity may seem too trivial to be useful in programming. However, associativity is what enables powerful divide-and-conquer strategies, where problems can be split into parts, solved independently, and then safely recombined.&lt;/p&gt;
&lt;h3 id="exponentiation-by-squaring"&gt;Exponentiation by Squaring&lt;/h3&gt;
&lt;p&gt;Let’s begin with a simple application: repeatedly applying the binary operator to the same element.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
\underbrace{a \bullet a \bullet \cdots \bullet a}_{a \text{ appears } n \text{ times}}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of applying the binary operator &lt;code&gt;$n-1$&lt;/code&gt; times sequentially, we exploit associativity to group every two instances of &lt;code&gt;$a$&lt;/code&gt; together recursively. This gives us a smaller problem when n is even:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
\underbrace{(a \bullet a) \bullet \cdots \bullet (a \bullet a)}_{(a \bullet a) \text{ appears } \frac{n}{2} \text{ times}}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When n is odd:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
a \bullet \underbrace{(a \bullet a) \bullet \cdots \bullet (a \bullet a)}_{(a \bullet a) \text{ appears } \frac{n-1}{2} \text{ times}}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We only need to compute &lt;code&gt;$a \bullet a$&lt;/code&gt; once to turn the problem of size &lt;code&gt;$n$&lt;/code&gt; to size &lt;code&gt;$n/2$&lt;/code&gt;. Repeating the process on &lt;code&gt;$(a \bullet a)$&lt;/code&gt; gives the &lt;a href="https://en.wikipedia.org/wiki/Exponentiation_by_squaring"&gt;Exponentiation by Squaring&lt;/a&gt; algorithm, which requires at most &lt;code&gt;$\displaystyle 2 \lfloor \log _{2}n\rfloor$&lt;/code&gt; computations that is more efficient than &lt;code&gt;$n-1$&lt;/code&gt; when &lt;code&gt;$n$&lt;/code&gt; is greater than 4.&lt;/p&gt;
&lt;p&gt;For integers or real numbers under multiplication (&lt;code&gt;×&lt;/code&gt;), exponentiation by squaring is an efficient algorithm to compute positive integer powers. Since the set of elliptic curve points under point addition forms a monoid, this same method can also be used to compute &lt;a href="https://kb.iany.me/para/lets/c/Cryptography/Elliptic&amp;#43;Curve&amp;#43;Scalar&amp;#43;Multiplication"&gt;Elliptic Curve Scalar Multiplication&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="general-divide-and-conquer-search-algorithm"&gt;General Divide-and-Conquer Search Algorithm&lt;/h3&gt;
&lt;p&gt;We can generalize the divide-and-conquer method to search an element in a sequence based solely on associativity.&lt;/p&gt;
&lt;p&gt;The result of applying the monoid operator to a sequence from left to right serves as a summary of that sequence. If a predicate can determine whether a given element is in the sequence based solely on this summary, we can devise a general divide-and-conquer search algorithm.&lt;/p&gt;
&lt;p&gt;Let’s say we want to search for a target element in the sequence &lt;code&gt;$t_1, \ldots, t_n$&lt;/code&gt;, where each &lt;code&gt;$t_i$&lt;/code&gt; belongs to a monoid &lt;code&gt;$(S, \bullet, e)$&lt;/code&gt;. Here, &lt;code&gt;$S$&lt;/code&gt; is the underlying set, &lt;code&gt;$\bullet$&lt;/code&gt; is the binary operation, and &lt;code&gt;$e$&lt;/code&gt; is the identity element.&lt;/p&gt;
&lt;p&gt;We don&amp;rsquo;t know which kind of predicates works for the search algorithm. Let&amp;rsquo;s give a best guess that the predicate &lt;code&gt;$p$&lt;/code&gt; is a function of the monoid &amp;ldquo;summary&amp;rdquo; that &lt;code&gt;$p(t_1 \bullet \cdots \bullet t_n)$&lt;/code&gt; is true if and only if &lt;code&gt;$t$&lt;/code&gt; is in the sequence &lt;code&gt;$t_1, \dots, t_n$&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Assume that &lt;code&gt;$p(t_1 \bullet \cdots \bullet t_n)$&lt;/code&gt; is true, thus the target element &lt;code&gt;$t$&lt;/code&gt; is in the sequence. We divide the sequence into two halves: &lt;code&gt;$t_1,\ldots,t_k$&lt;/code&gt; and &lt;code&gt;$t_{k+1},\ldots,t_n$&lt;/code&gt;, where &lt;code&gt;$1 \le k \le n$&lt;/code&gt;. We then evaluate &lt;code&gt;$p(t_1 \bullet \cdots \bullet t_k)$&lt;/code&gt; to determine whether the target element lies in the first or second half, and continue the search.&lt;/p&gt;
&lt;p&gt;Based on this observation, we can deduce the following property of the predicate: there exists an index &lt;code&gt;$x$&lt;/code&gt; such that&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
p(t_1 \bullet \cdots \bullet t_k) := \begin{cases}
\text{false} &amp;amp; \text{if } k &amp;lt; x, \\
\text{true} &amp;amp; \text{if } k \ge x.
\end{cases}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;$t_x$&lt;/code&gt; is the target element if such &lt;code&gt;$x$&lt;/code&gt; exists; otherwise, the target element does not exist in the sequence.&lt;/p&gt;
&lt;p&gt;Intuitively, the target element is the turning point at which the predicate on the running summary changes from false to true.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Monoid Search Turning Point.excalidraw"
loading="lazy"
src="https://blog.iany.me/2026/02/power-of-monoid-beauty-of-simplicity/Monoid%20search%20turning%20point.excalidraw.svg" /&gt;
&lt;/figure&gt;
&lt;p&gt;Note that &lt;code&gt;$p$&lt;/code&gt; makes sense only on the summary of any prefix of the sequence. If we need to continue the search in the second half, we must remember the summary of the scanned prefix.&lt;/p&gt;
&lt;p&gt;Now we can define the search algorithm &lt;code&gt;$\mathrm{Search}(p, s, \{t_i,\ldots,t_j\})$&lt;/code&gt; where&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$s$&lt;/code&gt; is the summary of scanned prefix &lt;code&gt;$t_1 \bullet \cdots \bullet t_{i-1}$&lt;/code&gt; when &lt;code&gt;$i &amp;gt; 1$&lt;/code&gt; or the identity element &lt;code&gt;$e$&lt;/code&gt; otherwise.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$t_i, \ldots, t_j$&lt;/code&gt; is the sub-range to search next.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$p$&lt;/code&gt; is the predicate as defined above&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The algorithm proceeds as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;$p(s \bullet t_i \bullet \cdots \bullet t_j$&lt;/code&gt;) is false, the target element does not exist. The algorithm aborts with an error.&lt;/li&gt;
&lt;li&gt;Otherwise, if there&amp;rsquo;s only one element (&lt;code&gt;$i = j$&lt;/code&gt;), &lt;code&gt;$t_i$&lt;/code&gt; is the target element. The algorithm aborts with the found result.&lt;/li&gt;
&lt;li&gt;Otherwise, choose a pivot index &lt;code&gt;$i \le m \lt j$&lt;/code&gt; to split the sequence into two nonempty halves: &lt;code&gt;$t_i, \ldots, t_m$&lt;/code&gt; and &lt;code&gt;$t_{m+1},\ldots,t_j$&lt;/code&gt;. Test &lt;code&gt;$p(s \bullet t_i \bullet \cdots \bullet t_m)$&lt;/code&gt; that
&lt;ul&gt;
&lt;li&gt;If it is true, continue the search in the first half: &lt;code&gt;$\mathrm{Search}(p, s, \{t_i,\ldots,t_m\})$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Otherwise, continue the search in the second half: &lt;code&gt;$\mathrm{Search}(p, s \bullet (t_i \bullet \cdots \bullet t_m),\{t_{m+1},\ldots,t_j\})$&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The algorithm starts with &lt;code&gt;$\mathrm{Search}(p, e, \{t_1, \ldots, t_k\})$&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="application-random-access-sequence"&gt;Application: Random-Access Sequence&lt;/h3&gt;
&lt;p&gt;An application of the search algorithm is accessing the nth element in the sequence.&lt;/p&gt;
&lt;p&gt;We initialize the sequence to all 1s and use the monoid of non-negative integers with addition &lt;code&gt;$(\mathbb{N},+,0)$&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
\underbrace{1, \ldots, 1}_{n \text{ times}}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The predicate to find the i-th (starting from 0) element is:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
p_i(s) := s &amp;gt; i
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It may seem silly to search for the i-th 1 in a sequence of 1s, but we can store any data in the sequence and attach the monoid values as annotations to guide the search algorithm.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Sequence Monoid Annotations.excalidraw"
loading="lazy"
src="https://blog.iany.me/2026/02/power-of-monoid-beauty-of-simplicity/Sequence%20Monoid%20Annotations.excalidraw.svg" /&gt;
&lt;/figure&gt;
&lt;h3 id="application-max-priority-queue"&gt;Application: Max-Priority Queue&lt;/h3&gt;
&lt;p&gt;Another application is finding the element with the max priority.&lt;/p&gt;
&lt;p&gt;We use the monoid of non-negative integers with operator &lt;code&gt;max&lt;/code&gt; &lt;code&gt;$(\mathbb{N},\mathrm{max},0)$&lt;/code&gt; and assume that the maximum value has the maximum priority.&lt;/p&gt;
&lt;p&gt;The predicate to find the element with the max priority is&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
p(s) := s = m
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where &lt;code&gt;$m$&lt;/code&gt; is the monoid summary of the entire sequence—that is, the maximum value in the sequence. The predicate checks whether the summary equals to &lt;code&gt;$m$&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="annotated-search-tree"&gt;Annotated Search Tree&lt;/h3&gt;
&lt;p&gt;A natural way to support the divide-and-conquer search is an &lt;em&gt;annotated binary tree&lt;/em&gt;. Store the sequence elements at the leaves, and at each node store the monoid summary of the subtree—e.g. the sum of lengths or the maximum priority in that subtree. The predicate can then be evaluated on the left subtree’s annotation to decide whether to descend left or right, and the prefix summary is updated when going right by combining it with the left subtree’s summary.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Annotated Binary Tree.excalidraw"
loading="lazy"
src="https://blog.iany.me/2026/02/power-of-monoid-beauty-of-simplicity/Annotated%20binary%20tree.excalidraw.svg" /&gt;
&lt;/figure&gt;
&lt;p&gt;A plain binary tree can degenerate to a list in the worst case, so operations may become linear. A more advanced structure, the &lt;em&gt;finger tree&lt;/em&gt;&lt;sup id="fnref:1"&gt;&lt;span class="footnote-ref" role="doc-noteref"&gt;1&lt;/span&gt;&lt;/sup&gt;, keeps the tree balanced and supports efficient access at both ends and in the middle; each node carries a monoidal “measure” of its subtree, and the same search strategy applies. In Haskell, &lt;a href="https://hackage-content.haskell.org/package/containers-0.8/docs/Data-Sequence.html"&gt;Data.Sequence&lt;/a&gt; from the &lt;code&gt;containers&lt;/code&gt; library implements sequences as finger trees with size (length) as the measure, giving &lt;code&gt;$O(\log n)$&lt;/code&gt; indexing, splitting, and concatenation.&lt;/p&gt;
&lt;h3 id="utility-of-the-identity-element"&gt;Utility of the Identity Element&lt;/h3&gt;
&lt;p&gt;The general divide-and-conquer algorithm does not require a monoid—only a semigroup. A semigroup is a fancy word for a set equipped with a closed, associative binary operator but lacking an identity element. The presence of an identity element makes monoids convenient to work with.&lt;/p&gt;
&lt;p&gt;The identity element serves as a natural default value or starting point for algorithms. For instance, in the search algorithm, the summary of the scanned prefix is initialized to &lt;code&gt;$e$&lt;/code&gt;. Without an identity element, we need an additional flag to indicate whether any prefix has been scanned, and the algorithm would have to branch conditionally based on that flag.&lt;/p&gt;
&lt;h2 id="the-art-of-choosing-monoid-and-predicate"&gt;The art of choosing monoid and predicate&lt;/h2&gt;
&lt;p&gt;In the random-access example we used &lt;code&gt;$(\mathbb{N}, +, 0)$&lt;/code&gt; and annotated each position with &lt;code&gt;$1$&lt;/code&gt;—the summary of a segment is its length, and the predicate &lt;code&gt;$s &amp;gt; i$&lt;/code&gt; tells us whether the &lt;code&gt;$i$&lt;/code&gt;-th element lies in the prefix we have so far. In the max-priority queue we used &lt;code&gt;$(\mathbb{N}, \max, 0)$&lt;/code&gt; (or a bounded variant): the summary is the maximum value in the segment, and the predicate &lt;code&gt;$s = m$&lt;/code&gt; identifies the segment that contains the global maximum. In both cases, the monoid was chosen so that the &lt;em&gt;combined&lt;/em&gt; summary over a range is exactly what the predicate needs to decide where to go next.&lt;/p&gt;
&lt;p&gt;The flip side is that finding both the right monoid and the right predicate can be tricky. At each step the search has access only to the monoid summary of the prefix (or segment) seen so far, so the predicate must be decided from that summary alone. The monoid must be rich enough to supply the information the predicate needs. Sometimes the natural summary (e.g. sum or max) suggests the predicate (e.g. &lt;code&gt;$s &amp;gt; i$&lt;/code&gt; or &lt;code&gt;$s = m$&lt;/code&gt;). Sometimes you must try a different carrier or operation, or encode extra information into the monoid (e.g. pairs or custom types), so that the predicate can be expressed. There is no universal recipe—it is a matter of design and experimentation. Reframe the problem as: “What do I need to know about a segment to decide the next step?” Then choose a monoid that can represent that knowledge and a predicate that uses it.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Hinze, R., &amp;amp; Paterson, R. (2006). Finger trees: A simple general-purpose data structure. &lt;em&gt;Journal of Functional Programming, 16&lt;/em&gt;(2), 197–217. Cambridge University Press. &lt;a href="https://www.cs.ox.ac.uk/ralf.hinze/publications/FingerTrees.pdf"&gt;https://www.cs.ox.ac.uk/ralf.hinze/publications/FingerTrees.pdf&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/algorithm/">Algorithm</category><category domain="https://blog.iany.me/tags/math/">Math</category><category domain="https://blog.iany.me/tags/programming/">Programming</category></item><item><title>Use Bun for Shell Scripts</title><link>https://blog.iany.me/2026/02/use-bun-for-shell-scripts/</link><pubDate>Sat, 07 Feb 2026 00:00:00 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/02/use-bun-for-shell-scripts/</guid><description>&lt;p&gt;I&amp;rsquo;ve moved most of my small scripts to Bun. It sidesteps Windows&amp;rsquo; lack of shebang support and the Git-for-Windows/WSL bash dance, and gives you one runtime for both ad-hoc shell-style commands and full scripts.&lt;/p&gt;
&lt;h2 id="problems-bun-solves"&gt;Problems Bun solves&lt;/h2&gt;
&lt;p&gt;Windows doesn&amp;rsquo;t execute &lt;code&gt;#!/usr/bin/env python3&lt;/code&gt; or &lt;code&gt;#!/bin/bash&lt;/code&gt;, so scripts written for Linux or macOS usually need a separate PowerShell version. Bun compiles JavaScript to native executables, so a single script can run cross-platform without shebangs.&lt;/p&gt;
&lt;p&gt;Mise &lt;a href="https://mise.jdx.dev/tasks/file-tasks.html"&gt;file tasks&lt;/a&gt; have started to support shebangs on Windows in the latest release, but they rely on system &lt;code&gt;bash.exe&lt;/code&gt;, which often points at WSL. Bun does not. You run &lt;code&gt;bun ./script.js&lt;/code&gt; and embed shell commands via &lt;code&gt;Bun.$&lt;/code&gt;; the script uses Bun&amp;rsquo;s bundled bash environment.&lt;/p&gt;
&lt;h2 id="why-bun-for-shell-scripts"&gt;Why Bun for shell scripts&lt;/h2&gt;
&lt;p&gt;The command &lt;code&gt;bun build ./script.ts --compile --outfile script&lt;/code&gt; &lt;a href="https://bun.com/docs/bundler/executables"&gt;produces a single binary&lt;/a&gt;, so no shebang or interpreter is needed on Windows. The &lt;code&gt;Bun.build&lt;/code&gt; API is available too; I use a &lt;code&gt;build.js&lt;/code&gt; script to walk a directory and compile all executables. Pre-built binaries start quickly—Bun&amp;rsquo;s docs cite ~5ms vs Node&amp;rsquo;s ~25ms for a simple script, which helps for small, frequently run scripts.&lt;/p&gt;
&lt;p&gt;Bun has strong support for shell-style scripting. &lt;a href="https://bun.com/docs/runtime/shell"&gt;Bun Shell&lt;/a&gt; runs bash commands; the main entry point is &lt;code&gt;Bun.$&lt;/code&gt;, which constructs shell commands from template literals for safe interpolation, similar to &lt;a href="https://github.com/google/zx"&gt;google/zx&lt;/a&gt;. On Windows, Bun ships with MSYS, so you get a consistent bash environment and common Linux CLI tools without extra setup.&lt;/p&gt;
&lt;p&gt;Other useful features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Bun.secrets&lt;/code&gt;—Securely store and retrieve secrets using the OS keystore (Windows Credential Manager, macOS Keychain, Linux libsecret). &lt;a href="https://bun.com/docs/api/secrets"&gt;Secrets - Bun&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Built-in support for glob patterns, YAML parsing, ANSI colors, and more. &lt;a href="https://bun.com/docs/runtime"&gt;Bun Runtime&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="short-example"&gt;Short example&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;import { $ } from &amp;quot;bun&amp;quot;;
const out = await $`echo hello`.text();
console.log(out); // hello
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it with &lt;code&gt;bun run script.js&lt;/code&gt; or &lt;code&gt;bun ./script.js&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="obsidian-notes"&gt;Obsidian Notes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kb.iany.me/para/lets/d/Development&amp;#43;Environment/JavaScript&amp;#43;Shell&amp;#43;Scripting"&gt;JavaScript Shell Scripting&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/bun/">Bun</category><category domain="https://blog.iany.me/tags/javascript/">JavaScript</category><category domain="https://blog.iany.me/tags/script/">Script</category><category domain="https://blog.iany.me/tags/windows/">Windows</category><category domain="https://blog.iany.me/tags/dev-environment/">Dev Environment</category></item><item><title>Use tmux for PowerShell in Windows Terminal</title><link>https://blog.iany.me/2026/01/use-tmux-for-powershell-in-windows-terminal/</link><pubDate>Thu, 29 Jan 2026 23:30:42 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2026/01/use-tmux-for-powershell-in-windows-terminal/</guid><description>&lt;p&gt;You can get tmux session persistence and multiplexing in Windows Terminal by running tmux inside WSL and setting the default shell to PowerShell. New panes and windows will then start &lt;code&gt;pwsh.exe&lt;/code&gt; instead of a Linux shell. Here’s a minimal setup using small wrappers and the &lt;code&gt;tmux -C attach&lt;/code&gt; trick to configure new sessions.&lt;/p&gt;
&lt;h2 id="why-tmux-under-wsl-with-powershell"&gt;Why tmux under WSL with PowerShell?&lt;/h2&gt;
&lt;p&gt;Since tmux does not run natively on Windows, the standard approach is to use it through WSL. However, if you launch &lt;code&gt;wsl tmux&lt;/code&gt; from Windows Terminal, all panes will default to your WSL shell. When working in a Windows directory, I prefer to use native PowerShell within the Windows environment.&lt;/p&gt;
&lt;p&gt;To ensure every tmux pane runs PowerShell, configure tmux to use &lt;code&gt;pwsh.exe&lt;/code&gt; as its default command. When you launch &lt;code&gt;pwsh.exe&lt;/code&gt; from WSL within a Windows directory, it brings us back to the Windows environment.&lt;/p&gt;
&lt;h2 id="wrappers-so-you-can-call-tmux-from-powershell"&gt;Wrappers so you can call tmux from PowerShell&lt;/h2&gt;
&lt;p&gt;Put these in a directory on your &lt;code&gt;PATH&lt;/code&gt; (e.g. &lt;code&gt;~/Documents/PowerShell/bin&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PowerShell&lt;/strong&gt; — &lt;code&gt;tmux.ps1&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;wsl tmux $args
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Cmd&lt;/strong&gt; — &lt;code&gt;tmux.cmd&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-batch"&gt;@echo off
wsl tmux %*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From PowerShell or Cmd you can then run &lt;code&gt;tmux new&lt;/code&gt;, &lt;code&gt;tmux attach&lt;/code&gt;, etc., and they all go to WSL’s tmux.&lt;/p&gt;
&lt;h2 id="creating-powershell-sessions"&gt;Creating PowerShell Sessions&lt;/h2&gt;
&lt;p&gt;The next piece is a script that creates a new session whose default command is &lt;code&gt;pwsh.exe&lt;/code&gt;, or attaches if that session already exists. Example usage: &lt;code&gt;tmux-pwsh dev&lt;/code&gt; or &lt;code&gt;tmux-pwsh work&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The script sets the default command only for sessions it creates; it does not affect sessions started with a standard &lt;code&gt;tmux new-session&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;tmux-pwsh.ps1&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;param(
[Parameter(Mandatory=$true)]
[string]$SessionName
)
$pwshCommand = &amp;quot;exec pwsh.exe -nologo&amp;quot;
# Check if session exists using exact name match
wsl tmux has-session -t &amp;quot;\=$SessionName&amp;quot; 2&amp;gt;$null
if ($LASTEXITCODE -ne 0) {
wsl tmux new-session -d -s $SessionName $pwshCommand
echo &amp;quot;set-option default-command `&amp;quot;$pwshCommand`&amp;quot;&amp;quot; | `
wsl tmux -C attach -t &amp;quot;\=$SessionName&amp;quot; &amp;gt;$null
}
wsl tmux attach -t &amp;quot;\=$SessionName&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;=&lt;/code&gt; prefix does an exact session name match and must be escaped when sending the command from PowerShell to WSL.&lt;/li&gt;
&lt;li&gt;The new session specifies command &lt;code&gt;pwsh.exe&lt;/code&gt; via the command argument.&lt;/li&gt;
&lt;li&gt;Running &lt;code&gt;wsl tmux set-option&lt;/code&gt; immediately after creating the session does not work because there&amp;rsquo;s a brief window where the session is not available for setting options. Instead, sending &lt;code&gt;set-option&lt;/code&gt; via &lt;code&gt;tmux -C attach&lt;/code&gt; reliably applies the setting.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After this, splitting panes and creating new windows in that session will all start &lt;code&gt;pwsh.exe -nologo&lt;/code&gt; in Windows Terminal.&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Use WSL for tmux and PowerShell as the default command by setting tmux’s default command to &lt;code&gt;exec pwsh.exe -nologo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tmux.ps1&lt;/code&gt;&lt;/strong&gt; / &lt;strong&gt;&lt;code&gt;tmux.cmd&lt;/code&gt;&lt;/strong&gt; — thin wrappers so &lt;code&gt;tmux&lt;/code&gt; from Windows means &lt;code&gt;wsl tmux&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tmux-pwsh.ps1 &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/strong&gt; — create (or attach to) a named session that uses PowerShell as the default command.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once these are on your &lt;code&gt;PATH&lt;/code&gt;, run e.g. &lt;code&gt;tmux-pwsh dev&lt;/code&gt; from Windows Terminal to get a tmux session where every pane is PowerShell.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/tmux/">Tmux</category><category domain="https://blog.iany.me/tags/powershell/">PowerShell</category><category domain="https://blog.iany.me/tags/wsl/">WSL</category><category domain="https://blog.iany.me/tags/windows-terminal/">Windows Terminal</category></item><item><title>Backup Ignored Files with Git Remote Branch</title><link>https://blog.iany.me/2025/12/backup-ignored-files-with-git-remote-branch/</link><pubDate>Fri, 12 Dec 2025 03:22:58 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2025/12/backup-ignored-files-with-git-remote-branch/</guid><description>&lt;p&gt;When working with Git repositories, there are often files that need to be backed up but shouldn&amp;rsquo;t be committed to the main branch. These might include local development settings, IDE configuration files, personal notes, or development scripts that are specific to your workflow. The challenge is finding a way to back up these ignored files without polluting the main repository history.&lt;/p&gt;
&lt;h2 id="the-original-solution"&gt;The Original Solution&lt;/h2&gt;
&lt;p&gt;My original approach was to use a separate repository to store backup files for all repositories and create symbolic links. This worked, but had several drawbacks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Backup files were disconnected from their source repositories, making it harder to track what belongs where&lt;/li&gt;
&lt;li&gt;Backing up new files required moving them to the backup repository and then creating symbolic links&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="a-better-approach-remote-branch"&gt;A Better Approach: Remote Branch&lt;/h2&gt;
&lt;p&gt;Instead of using a separate repository, we can use a remote branch within the same repository. This approach offers several advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Everything stays in one repository&lt;/li&gt;
&lt;li&gt;When you clone the repository, the backup branch comes with it&lt;/li&gt;
&lt;li&gt;Backup files are stored in a separate branch, keeping the main branch clean&lt;/li&gt;
&lt;li&gt;Full Git history for backup files, just like any other branch&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-script"&gt;The Script&lt;/h2&gt;
&lt;p&gt;With AI assistance, I developed &lt;code&gt;git-store-file&lt;/code&gt;, a tool for managing ignored files by storing them in a remote branch (default: &lt;code&gt;origin/_store&lt;/code&gt;). This keeps backup files separate from your working branch without interfering with regular development. The script is available in &lt;a href="https://github.com/doitian/dotfiles-public/blob/master/default/bin/git-store-file"&gt;Python&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="installation"&gt;Installation&lt;/h3&gt;
&lt;p&gt;Download the script and make it executable:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;curl -o ~/bin/git-store-file https://raw.githubusercontent.com/doitian/dotfiles-public/master/default/bin/git-store-file
chmod +x ~/bin/git-store-file
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="usage"&gt;Usage&lt;/h3&gt;
&lt;p&gt;The script has four main commands: &lt;code&gt;store&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;restore&lt;/code&gt;, and &lt;code&gt;ls&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id="store-files"&gt;Store Files&lt;/h4&gt;
&lt;p&gt;Store one or more files to the remote branch, including files that are ignored by Git:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# Store a single file
git-store-file config.local.json
# Store multiple files
git-store-file config.local.json secrets.env
# Use a custom branch name
git-store-file --branch backup config.local.json
# Use a custom remote
git-store-file --remote upstream config.local.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you run the store command, the script will:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a temporary Git index to avoid conflicts with your working directory&lt;/li&gt;
&lt;li&gt;Load the target branch&amp;rsquo;s current state into the temporary index&lt;/li&gt;
&lt;li&gt;Add the specified files using &lt;code&gt;git add -f&lt;/code&gt; to include ignored files&lt;/li&gt;
&lt;li&gt;Create a commit with the changes&lt;/li&gt;
&lt;li&gt;Display commit statistics and prompt for confirmation&lt;/li&gt;
&lt;li&gt;Push the commit to the remote branch&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="check-status"&gt;Check Status&lt;/h4&gt;
&lt;p&gt;Check which files stored in the remote branch have been modified in your local working directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# Check status
git-store-file status
# Show diff for modified files
git-store-file status --diff
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="restore-files"&gt;Restore Files&lt;/h4&gt;
&lt;p&gt;Restore files from the remote branch to your working directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# Restore a specific file
git-store-file restore config.local.json
# Restore all files from the branch
git-store-file restore
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id="list-files"&gt;List Files&lt;/h4&gt;
&lt;p&gt;List all files stored in the remote branch:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;git-store-file ls
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="options"&gt;Options&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-b, --branch BRANCH&lt;/code&gt;: Specify the branch name (default: &lt;code&gt;_store&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-r, --remote REMOTE&lt;/code&gt;: Specify the remote name (default: &lt;code&gt;origin&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-h, --help&lt;/code&gt;: Show help message&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="examples"&gt;Examples&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# Store local configuration
git-store-file .env.local
# Check what's changed
git-store-file status -d
# Restore after cloning
git-store-file restore .env.local
# List all backed up files
git-store-file ls
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="how-it-works"&gt;How It Works&lt;/h2&gt;
&lt;p&gt;The script uses Git&amp;rsquo;s low-level plumbing commands to manipulate the repository without affecting your working directory:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Temporary Index&lt;/strong&gt;: Uses the &lt;code&gt;GIT_INDEX_FILE&lt;/code&gt; environment variable to create a temporary index, completely isolated from your working directory&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read Tree&lt;/strong&gt;: Loads the target branch&amp;rsquo;s tree structure into the temporary index&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Force Add&lt;/strong&gt;: Uses &lt;code&gt;git add -f&lt;/code&gt; to add files even if they&amp;rsquo;re listed in &lt;code&gt;.gitignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Commit Tree&lt;/strong&gt;: Creates a commit object directly using &lt;code&gt;git commit-tree&lt;/code&gt;, bypassing the normal commit workflow&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Direct Push&lt;/strong&gt;: Pushes the commit hash directly to the remote branch reference&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This low-level approach ensures that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your working directory remains unchanged&lt;/li&gt;
&lt;li&gt;The main branch is never affected&lt;/li&gt;
&lt;li&gt;Ignored files can be stored without modifying &lt;code&gt;.gitignore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The backup branch maintains full Git history&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;git-store-file&lt;/code&gt; script solves the problem of backing up ignored files in a clean, Git-native way. By leveraging a remote branch, it keeps everything in one repository while maintaining clear separation between your main codebase and backup files. Whether you&amp;rsquo;re managing local configurations, IDE settings, or personal development scripts, this tool provides a simple and reliable backup solution that integrates seamlessly with your existing Git workflow.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/git/">Git</category><category domain="https://blog.iany.me/tags/automation/">Automation</category><category domain="https://blog.iany.me/tags/backup/">Backup</category></item><item><title>How to Activate mise for Cursor When It Runs Shell Commands</title><link>https://blog.iany.me/2025/11/how-to-activate-mise-for-cursor-when-it-runs-shell-commands/</link><pubDate>Wed, 26 Nov 2025 11:31:37 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2025/11/how-to-activate-mise-for-cursor-when-it-runs-shell-commands/</guid><description>&lt;p&gt;When using &lt;a href="https://mise.jdx.dev/"&gt;mise&lt;/a&gt; with Cursor, you may notice that the mise environment is not activated when Cursor executes shell commands. This occurs because Cursor launches a non-login shell, which initializes differently than an interactive login shell and therefore does not automatically source your usual mise setup.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Cursor runs commands in a non-login shell, which means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For &lt;strong&gt;zsh&lt;/strong&gt;: Only &lt;code&gt;.zshenv&lt;/code&gt; is loaded (not &lt;code&gt;.zshrc&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;bash&lt;/strong&gt;: Only &lt;code&gt;.bash_profile&lt;/code&gt; is loaded (not &lt;code&gt;.bashrc&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;rsquo;ve configured mise activation in &lt;code&gt;.zshrc&lt;/code&gt; or &lt;code&gt;.zprofile&lt;/code&gt;, it won&amp;rsquo;t be available when Cursor executes commands.&lt;/p&gt;
&lt;h2 id="the-solution"&gt;The Solution&lt;/h2&gt;
&lt;p&gt;The solution is to activate mise in &lt;code&gt;.zshenv&lt;/code&gt; (for zsh) or &lt;code&gt;.bash_profile&lt;/code&gt; (for bash) when the file is loaded by Cursor. You can detect Cursor by checking for the &lt;code&gt;CURSOR_AGENT&lt;/code&gt; environment variable.&lt;/p&gt;
&lt;p&gt;Add this to your &lt;code&gt;~/.zshenv&lt;/code&gt; or &lt;code&gt;~/.bash_profile&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;# Activate mise when running in Cursor
if [[ -n &amp;quot;$CURSOR_AGENT&amp;quot; ]]; then
eval &amp;quot;$(mise activate)&amp;quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="why-this-works"&gt;Why This Works&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.zshenv&lt;/code&gt; (or &lt;code&gt;.bash_profile&lt;/code&gt;) is always sourced for non-login shells&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;CURSOR_AGENT&lt;/code&gt; environment variable is set when Cursor runs commands&lt;/li&gt;
&lt;li&gt;By conditionally activating mise only when &lt;code&gt;CURSOR_AGENT&lt;/code&gt; is present, you avoid potential conflicts or slowdowns in other non-login shell contexts&lt;/li&gt;
&lt;li&gt;This ensures mise and all its configured tools are available when Cursor executes terminal commands&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After adding this configuration, rerun Cursor chats, and mise should be available in all Cursor shell commands.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/mise/">Mise</category><category domain="https://blog.iany.me/tags/cursor/">Cursor</category><category domain="https://blog.iany.me/tags/shell/">Shell</category><category domain="https://blog.iany.me/tags/zsh/">Zsh</category><category domain="https://blog.iany.me/tags/vibe-coding/">Vibe Coding</category></item><item><title>Temporary Vi Mode in PowerShell</title><link>https://blog.iany.me/2025/11/temporary-vi-mode-in-powershell/</link><pubDate>Sat, 22 Nov 2025 23:17:05 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2025/11/temporary-vi-mode-in-powershell/</guid><description>&lt;p&gt;I miss the feature in readline (Bash/Zsh) where &lt;code&gt;Ctrl+x, Ctrl+v&lt;/code&gt; switches to Vi command mode temporarily. In that workflow, entering Insert mode switches back to Emacs mode. &lt;code&gt;PSReadLine&lt;/code&gt; has a command &lt;code&gt;ViCommandMode&lt;/code&gt;, but binding it directly in Emacs mode will report errors on every key input.&lt;/p&gt;
&lt;p&gt;The solution requires handling the mode change event to toggle the global &lt;code&gt;EditMode&lt;/code&gt; between &lt;code&gt;Emacs&lt;/code&gt; and &lt;code&gt;Vi&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When entering &amp;ldquo;Insert&amp;rdquo; mode (the default for Emacs users), we force the mode to &lt;code&gt;Emacs&lt;/code&gt; and set up the trigger for the temporary Vi mode. When that trigger (&lt;code&gt;Ctrl+x, Ctrl+v&lt;/code&gt;) is fired, we switch the edit mode to &lt;code&gt;Vi&lt;/code&gt; and jump to command mode.&lt;/p&gt;
&lt;p&gt;Here is the configuration for your &lt;code&gt;$PROFILE&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;function OnViModeChange {
if ($args[0] -eq 'Insert') {
Set-PSReadLineOption -EditMode Emacs
Set-PSReadlineKeyHandler -Chord &amp;quot;Ctrl+x,Ctrl+v&amp;quot; -ScriptBlock {
Set-PSReadLineOption -EditMode Vi
[Microsoft.PowerShell.PSConsoleReadLine]::ViCommandMode()
}
# Add other Emacs key bindings here.
# Switching `EditMode` resets key bindings to their defaults.
# Remember to reconfigure them.
# Set-PSReadlineKeyHandler -Chord &amp;quot;Ctrl+w&amp;quot; -Function BackwardKillWord
}
}
Set-PSReadLineOption -ViModeIndicator Script
Set-PSReadLineOption -ViModeChangeHandler $Function:OnViModeChange
Set-PSReadLineOption -EditMode Emacs
# Reuse the function to start Emacs mode
OnViModeChange Insert
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Switching &lt;code&gt;EditMode&lt;/code&gt; resets key bindings to their defaults. Remember to reconfigure them.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/powershell/">PowerShell</category><category domain="https://blog.iany.me/tags/vim/">Vim</category><category domain="https://blog.iany.me/tags/emacs/">Emacs</category><category domain="https://blog.iany.me/tags/console/">Console</category></item><item><title>Study on Quotient Spaces</title><link>https://blog.iany.me/2025/11/study-on-quotient-spaces/</link><pubDate>Tue, 18 Nov 2025 21:23:25 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2025/11/study-on-quotient-spaces/</guid><description>&lt;p&gt;I&amp;rsquo;m reading &lt;em&gt;Linear Algebra Done Right&lt;/em&gt; by Axler and found the section on quotient spaces difficult to understand, so I researched and took these notes.&lt;/p&gt;
&lt;h2 id="definitions"&gt;Definitions&lt;/h2&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.95 notion: $v + U$
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;p&gt;Suppose &lt;code&gt;$v \in V$&lt;/code&gt; and &lt;code&gt;$U \subseteq V$&lt;/code&gt;. Then &lt;code&gt;$v + U$&lt;/code&gt; is the subset of &lt;code&gt;$V$&lt;/code&gt; defined by&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[v + U = \{v + u : u \in U\}.\]
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;Also called a translate. &lt;strong&gt;Attention&lt;/strong&gt; that a translate is a set.&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.97 definition: &lt;em&gt;translate&lt;/em&gt;
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
Suppose &lt;code&gt;$v \in V$&lt;/code&gt; and &lt;code&gt;$U \subseteq V$&lt;/code&gt;, the set &lt;code&gt;$v + U$&lt;/code&gt; is said to be a &lt;em&gt;translate&lt;/em&gt; of &lt;code&gt;$U$&lt;/code&gt;.
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;Quotient space is a set of all translates (set of sets):&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.99 definition: &lt;em&gt;quotient space&lt;/em&gt;, $V/U$
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;p&gt;Suppose &lt;code&gt;$U$&lt;/code&gt; is a subspace of &lt;code&gt;$V$&lt;/code&gt;. Then the &lt;em&gt;quotient space&lt;/em&gt; &lt;code&gt;$V/U$&lt;/code&gt; is the set of all translates of &lt;code&gt;$U$&lt;/code&gt;. Thus&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[V/U = \{v + U : v \in V\}.\]
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;Quotient space is a set of sets. There are duplicates for each &lt;code&gt;$v \in V$&lt;/code&gt; because for some &lt;code&gt;$v_1, v_2 \in V$&lt;/code&gt;, &lt;code&gt;$v_1 + U$&lt;/code&gt; and &lt;code&gt;$v_2 + U$&lt;/code&gt; can be identical set.&lt;/p&gt;
&lt;p&gt;A quotient space &lt;code&gt;$V/U$&lt;/code&gt; is formed by &amp;ldquo;collapsing&amp;rdquo; a subspace &lt;code&gt;$U$&lt;/code&gt; to zero within a larger vector space &lt;code&gt;$V$&lt;/code&gt;. This construction is based on an equivalence relation where two vectors &lt;code&gt;$x, y \in V$&lt;/code&gt; are considered equivalent if their difference lies in &lt;code&gt;$U$&lt;/code&gt;—that is, &lt;code&gt;$x \sim y$&lt;/code&gt; if and only if &lt;code&gt;$x - y \in U$&lt;/code&gt;. &lt;a href="https://en.wikipedia.org/wiki/Quotient_space_%28linear_algebra%29"&gt;wikipedia&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="lemmas"&gt;Lemmas&lt;/h2&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.101 &lt;em&gt;two translates of a subspace are equal or disjoint&lt;/em&gt;
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;p&gt;Suppose &lt;code&gt;$U$&lt;/code&gt; is a subspace of &lt;code&gt;$V$&lt;/code&gt; and &lt;code&gt;$v, w \in V$&lt;/code&gt;. Then&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
v - w \in U \iff v + U = w + U \iff (v + U) \cap (w + U) \neq \emptyset
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;If two translates are not disjoint (the union set is not empty), they must be equal. So they are equal or disjoint.&lt;/p&gt;
&lt;p&gt;All distinct translates of a subspace are disjoint. Given any &lt;code&gt;$v \in V$&lt;/code&gt;, it belongs to only one translate.&lt;/p&gt;
&lt;p&gt;Since the quotient space &lt;code&gt;$V/U$&lt;/code&gt; is a set of translates of a subspace, it is like a disjoint partition of values in &lt;code&gt;$V$&lt;/code&gt;. By using the definition of quotient map&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.104 definition: &lt;em&gt;quotient map&lt;/em&gt;, $\pi$
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;p&gt;Suppose &lt;code&gt;$U$&lt;/code&gt; is a subspace of &lt;code&gt;$V$&lt;/code&gt;. The &lt;em&gt;quotient map&lt;/em&gt; &lt;code&gt;$\pi : V \to V/U$&lt;/code&gt; is the linear map defined by&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[\pi(v) = v + U\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;for each &lt;code&gt;$v \in V$&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;We can write that&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
\pi(v_1) = \pi(v_2) \iff v_1 - v_2 \in U
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The quotient map has two essential properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;null space&lt;/strong&gt; of &lt;code&gt;$\pi$&lt;/code&gt; is exactly the subspace &lt;code&gt;$U$&lt;/code&gt;, because &lt;code&gt;$v+U=0+U \iff v-0 \in U \iff v \in U$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;range&lt;/strong&gt; of &lt;code&gt;$\pi$&lt;/code&gt; is the entire quotient space &lt;code&gt;$V/U$&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="quotient-space-is-a-vector-space"&gt;Quotient Space Is a Vector Space&lt;/h2&gt;
&lt;p&gt;First define the addition and scalar multiplication operations:&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.102 definition: &lt;em&gt;addition and scalar multiplication on&lt;/em&gt; $V/U$
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;p&gt;Suppose &lt;code&gt;$U$&lt;/code&gt; is a subspace of &lt;code&gt;$V$&lt;/code&gt;. Then addition and scalar multiplication are defined on &lt;code&gt;$V/U$&lt;/code&gt; by&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[\begin{align*}
(v + U) + (w + U) &amp;amp;= (v + w) + U \\
\lambda(v + U) &amp;amp;= (\lambda v) + U
\end{align*}\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;for all &lt;code&gt;$v, w \in V$&lt;/code&gt; and &lt;code&gt;$\lambda \in \mathbf{F}$&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;&lt;code&gt;$v+U$&lt;/code&gt; is not the unique way to represent a member in &lt;code&gt;$V/U$&lt;/code&gt;, because there may exist &lt;code&gt;$v'\ne v$&lt;/code&gt; that &lt;code&gt;$u + U = v' + U$&lt;/code&gt;. The operations make sense only when the choice of &lt;code&gt;$v$&lt;/code&gt; to represent a translate makes no differences.&lt;/p&gt;
&lt;p&gt;Specifically, suppose &lt;code&gt;$v_1, v_2, w_1, w_2 \in V$&lt;/code&gt; such that&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
v_1 + U = v_2 + U \quad\textrm{and}\quad w_1 + U = w_2 + U
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From the addition definition:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
\begin{align*}
(v_1+U) + (w_1+U) &amp;amp;= (v_1 + w_1) + U \\
(v_2+U) + (w_2+U) &amp;amp;= (v_2 + w_2) + U
\end{align*}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The left side of the two equations indeed are the different representation of the same equation, so we must show that the right side equal: &lt;code&gt;$(v_1 + w_1)+U=(v2+w2)+U$&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This applies to scalar multiplication as well:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
\begin{align*}
\lambda(v_1 + U) &amp;amp;= (\lambda v_1) + U \\
\lambda(v_2 + U) &amp;amp;= (\lambda v_2) + U
\end{align*}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We must show that &lt;code&gt;$(\lambda v_1) + U = (\lambda v_2) + U$&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="dimension"&gt;Dimension&lt;/h2&gt;
&lt;p&gt;The dimension of the quotient space is given by a simple subtraction, relating the dimension of &lt;code&gt;$V/U$&lt;/code&gt; to the &amp;ldquo;lost&amp;rdquo; dimension of &lt;code&gt;$U$&lt;/code&gt;:&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.105 &lt;em&gt;dimension of quotient space&lt;/em&gt;
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;p&gt;Suppose &lt;code&gt;$V$&lt;/code&gt; is finite-dimensional and &lt;code&gt;$U$&lt;/code&gt; is a subspace of V. Then&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[\text{dim } V/U = \text{dim }V - \text{dim }U.\]
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="linear-map-from-vnull-t-to-w"&gt;Linear Map from V/(null T) to W&lt;/h2&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.106 notation: $\widetilde{T}$
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;p&gt;Suppose &lt;code&gt;$T \in \mathcal{L}(V, W)$&lt;/code&gt;. Define &lt;code&gt;$\widetilde{T}: V/(\text{null } T) \to W$&lt;/code&gt; by&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[\widetilde{T}(v + \text{null } T) = Tv.\]
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;Think of merging inputs having the same output. These inputs will be the same input in the quotient space &lt;code&gt;$V/(\text{null } T)$&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For any &lt;code&gt;$v_1, v_2 \in V$&lt;/code&gt; that &lt;code&gt;$Tv_1 = Tv_2$&lt;/code&gt;, &lt;code&gt;$v_1 + \mathrm{null}\, T$&lt;/code&gt; and &lt;code&gt;$v_2 + \mathrm{null}\, T$&lt;/code&gt; are the same value in &lt;code&gt;$V/(\mathrm{null}\, T)$&lt;/code&gt;. This makes &lt;code&gt;$\widetilde{T}$&lt;/code&gt; injective. Because &lt;code&gt;$\mathrm{range}\,\widetilde{T}=\mathrm{range}\, T$&lt;/code&gt;, &lt;code&gt;$\widetilde{T}$&lt;/code&gt; is also surjective on to &lt;code&gt;$\mathrm{range}\, T$&lt;/code&gt;.&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.63 &lt;em&gt;invertibility&lt;/em&gt; $\iff$ &lt;em&gt;injectivity and surjectivity&lt;/em&gt;
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
A linear map is invertible if and only if it is injective and surjective.
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;3.63 shows us that &lt;code&gt;$\widetilde{T}$&lt;/code&gt; is invertible, and according to the definition of isomorphic, &lt;code&gt;$V/(\mathrm{null}\, T)$&lt;/code&gt; and &lt;code&gt;$\mathrm{range}\,T$&lt;/code&gt; are isomorphic vector spaces and &lt;code&gt;$\widetilde{T}$&lt;/code&gt; is their isomorphism.&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-definition" data-callout-type="definition"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-book"&gt;&lt;/i&gt;
3.69 definition: &lt;em&gt;isomorphism, isomorphic&lt;/em&gt;
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;ul&gt;
&lt;li&gt;An &lt;em&gt;isomorphism&lt;/em&gt; is an invertible linear map.&lt;/li&gt;
&lt;li&gt;Two vector spaces are called isomorphic if there is an isomorphism from one vector space onto the other one.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;One of the key uses of &lt;code&gt;$\widetilde{T}$&lt;/code&gt; is demonstrating a canonical isomorphism. For any linear map &lt;code&gt;$T \in \mathcal{L}(V, W)$&lt;/code&gt;, the quotient space &lt;code&gt;$V/(\text{null } T)$&lt;/code&gt; is isomorphic to the image space &lt;code&gt;$\text{range } T$&lt;/code&gt;. This shows that the quotient space &lt;code&gt;$V/(\text{null } T)$&lt;/code&gt; serves as a way to &amp;ldquo;mod out&amp;rdquo; the non-injective part of &lt;code&gt;$T$&lt;/code&gt;.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/math/">Math</category><category domain="https://blog.iany.me/tags/linear-algebra/">Linear Algebra</category></item><item><title>Hosting Static Site on Cloudflare R2</title><link>https://blog.iany.me/2025/10/hosting-static-site-on-cloudflare-r2/</link><pubDate>Wed, 22 Oct 2025 18:46:38 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2025/10/hosting-static-site-on-cloudflare-r2/</guid><description>&lt;p&gt;This post explains how to set up a static site on Cloudflare R2.&lt;/p&gt;
&lt;p&gt;I recently migrated my blog to Cloudflare R2, and the process went smoothly until I encountered R2&amp;rsquo;s lack of native support for rewriting URLs to &lt;code&gt;index.html&lt;/code&gt; files. This post explains how I resolved this issue.&lt;/p&gt;
&lt;h2 id="setting-up"&gt;Setting Up&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Create the bucket.&lt;/li&gt;
&lt;li&gt;Add the custom domain &lt;code&gt;blog.iany.me&lt;/code&gt;. To redirect &lt;code&gt;iany.me&lt;/code&gt; and &lt;code&gt;www.iany.me&lt;/code&gt; to the blog, add these as custom domains as well. Since I host the domain in Cloudflare, the DNS record is automatically configured.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="publishing"&gt;Publishing&lt;/h2&gt;
&lt;p&gt;I use &lt;code&gt;rclone&lt;/code&gt; to deploy the site to R2. Below is the GitHub workflow I employ. Noting that the &lt;code&gt;fetch-depth&lt;/code&gt; option for &lt;code&gt;actions/checkout&lt;/code&gt; retrieves the complete repository history. This works with Hugo&amp;rsquo;s &lt;code&gt;--enableGitInfo&lt;/code&gt; flag to accurately determine article creation dates.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;name: Deploy Hugo Site to Cloudflare R2
on:
push:
branches:
- master
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install latest Hugo
run: |
TAG=$(curl -s https://api.github.com/repos/gohugoio/hugo/releases/latest \
| grep '&amp;quot;tag_name&amp;quot;:' \
| head -1 \
| sed -E 's/.*&amp;quot;([^&amp;quot;]+)&amp;quot;.*/\1/')
curl -L &amp;quot;https://github.com/gohugoio/hugo/releases/download/${TAG}/hugo_${TAG#v}_Linux-64bit.tar.gz&amp;quot; \
-o hugo.tar.gz
tar -xzf hugo.tar.gz hugo
sudo mv hugo /usr/local/bin/
hugo version
- name: Install rclone
run: |
curl https://rclone.org/install.sh | sudo bash
- name: Build site with Hugo
run: hugo --minify --enableGitInfo
- name: Configure rclone for Cloudflare R2
env:
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
run: |
rclone config create r2 s3 \
provider Cloudflare \
access_key_id &amp;quot;${R2_ACCESS_KEY_ID}&amp;quot; \
secret_access_key &amp;quot;${R2_SECRET_ACCESS_KEY}&amp;quot; \
endpoint &amp;quot;${R2_ENDPOINT}&amp;quot; \
--quiet
- name: Deploy to Cloudflare R2
env:
R2_BUCKET: ${{ secrets.R2_BUCKET }}
run: |
rclone copy public/ r2:&amp;quot;${R2_BUCKET}&amp;quot; \
--checksum \
--no-traverse \
--verbose
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="rules"&gt;Rules&lt;/h2&gt;
&lt;p&gt;Go to the dashboard of the domain &lt;code&gt;iany.me&lt;/code&gt; and go to the section &lt;code&gt;Rules&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="redirect-rules"&gt;Redirect Rules&lt;/h3&gt;
&lt;p&gt;I have added 3 redirect rules:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When incoming requests have a hostname &lt;code&gt;iany.me&lt;/code&gt; or &lt;code&gt;www.iany.me&lt;/code&gt;, redirect to &lt;code&gt;concat(&amp;quot;https://blog.iany.me&amp;quot;, http.request.uri.path)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Redirect &lt;code&gt;https://blog.iany.me/*/index.html&lt;/code&gt; to &lt;code&gt;https://blog.iany.me/${1}/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Redirect &lt;code&gt;http://*&lt;/code&gt; to &lt;code&gt;https://${1}&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Although the Redirect Rules manual says if multiple rules make the same modification, the last executed rule wins, I have to put rule 2 before 3.&lt;/p&gt;
&lt;h3 id="url-rewrite-rules"&gt;URL Rewrite Rules&lt;/h3&gt;
&lt;p&gt;I added 2 rewrite rules for &lt;code&gt;index.html&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When the request URL is &lt;code&gt;https://blog.iany.me&lt;/code&gt; or &lt;code&gt;https://blog.iany.me/&lt;/code&gt;, rewrite the URL path to &lt;code&gt;/index.html&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When the request URL matches &lt;code&gt;https://blog.iany.me/*/&lt;/code&gt;, rewrite the path to &lt;code&gt;${1}/index.html&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="cache-rules"&gt;Cache Rules&lt;/h3&gt;
&lt;p&gt;I added 2 cache rules for images and static content:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Set cache TTL to 1 month for &lt;code&gt;png&lt;/code&gt;, &lt;code&gt;jpg&lt;/code&gt;, &lt;code&gt;jpeg&lt;/code&gt;, and &lt;code&gt;svg&lt;/code&gt; files.&lt;/li&gt;
&lt;li&gt;Set cache TTL to 1 year for files in &lt;code&gt;/js&lt;/code&gt;, &lt;code&gt;/fonts&lt;/code&gt;, &lt;code&gt;/css&lt;/code&gt;, and &lt;code&gt;/uploads&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/ci/">CI</category><category domain="https://blog.iany.me/tags/automation/">Automation</category></item><item><title>Explain Atomic Cross-Chain Swaps by Herlihy</title><link>https://blog.iany.me/2025/09/explain-atomic-cross-chain-swaps-by-herlihy/</link><pubDate>Fri, 19 Sep 2025 22:26:46 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2025/09/explain-atomic-cross-chain-swaps-by-herlihy/</guid><description>&lt;p&gt;This article explains the paper [Herlihy, 2018]&lt;sup id="fnref:1"&gt;&lt;span class="footnote-ref" role="doc-noteref"&gt;1&lt;/span&gt;&lt;/sup&gt; in simple words. In the paper, Herlihy has introduced an effective atomic cross-chain swap protocol in order to exchange assets across multiple blockchains among multiple parties.&lt;/p&gt;
&lt;h2 id="model"&gt;Model&lt;/h2&gt;
&lt;p&gt;A swap can be modeled using a directed graph &lt;code&gt;$(V, A)$&lt;/code&gt;. &lt;code&gt;$V$&lt;/code&gt; is a set of involved users, and &lt;code&gt;$A$&lt;/code&gt; is a set of payments. Each payment is annotated as &lt;code&gt;$(u, v)$&lt;/code&gt; where &lt;code&gt;$u$&lt;/code&gt; is the payer and &lt;code&gt;$v$&lt;/code&gt; is the payee. The Atomic Swap Protocol ensures that when all users follow the protocol, if one payment succeeds, all other payments must succeed.&lt;/p&gt;
&lt;p&gt;For example, following model represents a swap that Alice sends a payment to Bob, and Bob sends a payment to Carol.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
\begin{array}{rl}
V &amp;amp;= \{\mathrm{Alice}, \mathrm{Bob}, \mathrm{Carol}\} \\
A &amp;amp;= \{(\mathrm{Alice},\mathrm{Bob}), (\mathrm{Bob}, \mathrm{Carol})\}
\end{array}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Atomic-Swap-Simple-Model.excalidraw"
loading="lazy"
src="https://blog.iany.me/2025/09/explain-atomic-cross-chain-swaps-by-herlihy/atomic-swap-simple-model.excalidraw.svg" /&gt;
&lt;/figure&gt;
&lt;h2 id="simple-protocol"&gt;Simple Protocol&lt;/h2&gt;
&lt;p&gt;When all payments can be connected into a path, like the example above, the protocol is simple and is similar to the payment hops in lightning network.&lt;/p&gt;
&lt;p&gt;A path is annotated as &lt;code&gt;$(u_1, \ldots, u_l)$&lt;/code&gt;, which is consisted of &lt;code&gt;$l-1$&lt;/code&gt; payments: &lt;code&gt;$(u_1, u_2),(u_2, u_3),\ldots,(u_{l-1}, u_l)$&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The final payee &lt;code&gt;$u_l$&lt;/code&gt; generates a secret &lt;code&gt;$s$&lt;/code&gt; and sends its hash &lt;code&gt;$H(s)$&lt;/code&gt; to the payer &lt;code&gt;$u_1$&lt;/code&gt;. The payer &lt;code&gt;$u_1$&lt;/code&gt; creates a Hash Time Locked Contract &lt;code&gt;$(H(s), 2(l-1)\Delta)$&lt;/code&gt; that either &lt;code&gt;$u_2$&lt;/code&gt; can get the fund by providing the secret &lt;code&gt;$s$&lt;/code&gt;, or &lt;code&gt;$u_1$&lt;/code&gt; can get the refund when time has elapsed &lt;code&gt;$2(l-1)\Delta$&lt;/code&gt; units. The users &lt;code&gt;$u_i$&lt;/code&gt; from &lt;code&gt;$u_2$&lt;/code&gt; to &lt;code&gt;$u_{l-2}$&lt;/code&gt; creates a Hash Time Locked Contract &lt;code&gt;$(H(s), 2(l-i)\Delta)$&lt;/code&gt; when they confirm that they have received the inbound contract. The payee &lt;code&gt;$u_l$&lt;/code&gt; can settle the deal by providing the secret &lt;code&gt;$s$&lt;/code&gt; to &lt;code&gt;$u_{l-1}$&lt;/code&gt;, and each user uses the received secret &lt;code&gt;$s$&lt;/code&gt; to settle their inbound contract.&lt;/p&gt;
&lt;p&gt;The simple protocol also works when payee and payer are the same user (&lt;code&gt;$u_1 = u_l$&lt;/code&gt;). From engineering points, it should work for most scenarios.&lt;/p&gt;
&lt;h2 id="arbitrary-connected-directed-graph"&gt;Arbitrary Connected Directed Graph&lt;/h2&gt;
&lt;p&gt;The paper also extends the swap protocol to arbitrary connected directed graph. A connected directed graph means for any two user &lt;code&gt;$u, v$&lt;/code&gt;, there are a payment path from &lt;code&gt;$u$&lt;/code&gt; to &lt;code&gt;$v$&lt;/code&gt;, and a payment path from &lt;code&gt;$v$&lt;/code&gt; to &lt;code&gt;$u$&lt;/code&gt;. Such directed graphs contain loops, as in the example below:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-katex"&gt;\[
\begin{array}{rl}
V = \{&amp;amp;A, B, C, D, E, F\} \\
A = \{ \\
&amp;amp;(A, B), (B, C), (C, A), \\
&amp;amp;(C, D), (D, B), \\
&amp;amp;(D, E), (E, F), (F, D) \\
\} \\
\end{array}
\]
&lt;/code&gt;&lt;/pre&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Atomic-Swap-Complex-Connected-Graph.excalidraw"
loading="lazy"
src="https://blog.iany.me/2025/09/explain-atomic-cross-chain-swaps-by-herlihy/atomic-swap-complex-connected-graph.excalidraw.svg" /&gt;
&lt;/figure&gt;
&lt;h3 id="leaders-selection"&gt;Leaders Selection&lt;/h3&gt;
&lt;p&gt;The protocol must select a Feedback Vertex Set of the directed graph as leaders. A feedback vertex set is a subset of &lt;code&gt;$V$&lt;/code&gt; that once removed from the graph, there will be no loops in the graph. For example, &lt;code&gt;$(B, D)$&lt;/code&gt; is a feedback vertex set of the example above. When there are multiple candidates, choose an arbitrary set.&lt;/p&gt;
&lt;p&gt;The leaders generate secrets and publish their hashes. If &lt;code&gt;$(B, D)$&lt;/code&gt; are selected, they should publish &lt;code&gt;$(H(s_B), H(s_D))$&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The protocol then move forward in two phases.&lt;/p&gt;
&lt;h3 id="phase-one"&gt;Phase One&lt;/h3&gt;
&lt;p&gt;In the first phase, users offer outbound contracts to counterparty for each payment.&lt;/p&gt;
&lt;p&gt;Leaders must offer contracts first:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Publish a contract on every outbound payments, then&lt;/li&gt;
&lt;li&gt;wait until all inbound contracts have been published.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Followers must confirm inbound contracts first:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Wait until correct inbound contracts have been published, then&lt;/li&gt;
&lt;li&gt;publish a contract on every outbound payments.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The contracts are Hash Time Locked Contracts &lt;code&gt;$(h, t)$&lt;/code&gt; that payee can get the fund using the proof to unlock &lt;code&gt;$h$&lt;/code&gt;, or the payer gets the refund after timeout &lt;code&gt;$t$&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For a payment &lt;code&gt;$(u, v)$&lt;/code&gt;, the proof of &lt;code&gt;$h$&lt;/code&gt; is a triple &lt;code&gt;$(s, p, \sigma)$&lt;/code&gt; where&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$p$&lt;/code&gt; is any payment path &lt;code&gt;$(u_0, \ldots, u_k)$&lt;/code&gt; starting from the payee &lt;code&gt;$v$&lt;/code&gt; (&lt;code&gt;$u_0 = v$&lt;/code&gt;) to a leader &lt;code&gt;$u_k$&lt;/code&gt;. The path &lt;code&gt;$p$&lt;/code&gt; can have a length of 0 if &lt;code&gt;$v$&lt;/code&gt; itself is a leader.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$s$&lt;/code&gt; is the secret generated by the ending leader of the path &lt;code&gt;$p$&lt;/code&gt;, and &lt;code&gt;$h = H(s)$&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$\sigma$&lt;/code&gt; is a nested signatures of &lt;code&gt;$s$&lt;/code&gt; signed by users in the path &lt;code&gt;$p$&lt;/code&gt;.
&lt;ul&gt;
&lt;li&gt;The innest signature is &lt;code&gt;$\mathrm{sig}_k = \mathit{sig}(s, u_k)$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;For user &lt;code&gt;$u_i$&lt;/code&gt; (&lt;code&gt;$0 \leq i \leq k - 1$&lt;/code&gt;), the signature is &lt;code&gt;$\mathrm{sig}_i = \mathit{sig}(\mathrm{sig}_{i+1}, u_i)$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$\sigma = \mathrm{sig}_0$&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The hash lock &lt;code&gt;$h$&lt;/code&gt; can be set to &lt;code&gt;$H(s)$&lt;/code&gt; for the shortest &lt;code&gt;$p$&lt;/code&gt;, or just the hashes of all secrets to all unlocking using arbitrary ending leader of the path &lt;code&gt;$p$&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The time lock &lt;code&gt;$t$&lt;/code&gt; is &lt;code&gt;$(\mathit{diam}(\mathcal{D}) + |p|)\Delta$&lt;/code&gt;, where&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$\mathit{diam}(\mathcal{D})$&lt;/code&gt; is the longest length of a payment path without duplicated users in the directed graph.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$|p|$&lt;/code&gt; is the length of the path &lt;code&gt;$p$&lt;/code&gt; in the hash lock.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$\Delta$&lt;/code&gt; is a configured time unit constant for the time out.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&amp;rsquo;s see two examples.&lt;/p&gt;
&lt;p&gt;The first example is the payment from a leader: &lt;code&gt;$(B, C)$&lt;/code&gt;.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Atomic-Swap-Complex-Connected-Graph-B-C-Contract.excalidraw"
loading="lazy"
src="https://blog.iany.me/2025/09/explain-atomic-cross-chain-swaps-by-herlihy/atomic-swap-complex-connected-graph-b-c-contract.excalidraw.svg" /&gt;
&lt;/figure&gt;
&lt;p&gt;There are three candidate path &lt;code&gt;$p$&lt;/code&gt; from the payee &lt;code&gt;$C$&lt;/code&gt; to a leader either &lt;code&gt;$B$&lt;/code&gt; or &lt;code&gt;$D$&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;$(C, A, B)$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$(C, D)$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$(C, D, B)$&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;$B$&lt;/code&gt; can choose any one, but the shortest one is recommended. Let&amp;rsquo;s use &lt;code&gt;$(C, D)$&lt;/code&gt;. &lt;code&gt;$B$&lt;/code&gt; can publish the contract because he/she is the leader.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The hash lock for &lt;code&gt;$(B, C)$&lt;/code&gt; is &lt;code&gt;$H(s_D)$&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The time lock is &lt;code&gt;$6\Delta$&lt;/code&gt;, because the length of the longest path &lt;code&gt;$(A, B, C, D, E, F)$&lt;/code&gt; is 5, and the length of &lt;code&gt;$(C, D)$&lt;/code&gt; is 1.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The second example is the payment from a follower: &lt;code&gt;$(C, A)$&lt;/code&gt;.&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img class="kg-image"
alt="Atomic-Swap-Complex-Connected-Graph-C-a-Contract.excalidraw"
loading="lazy"
src="https://blog.iany.me/2025/09/explain-atomic-cross-chain-swaps-by-herlihy/atomic-swap-complex-connected-graph-c-a-contract.excalidraw.svg" /&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;code&gt;$C$&lt;/code&gt; can choose the path &lt;code&gt;$(A, B)$&lt;/code&gt; and publish the contract:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The hash lock is &lt;code&gt;$H(s_B)$&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The time lock is &lt;code&gt;$6\Delta$&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="phase-two"&gt;Phase Two&lt;/h3&gt;
&lt;p&gt;In phase two, leaders can unlock the inbound contracts because they know all secrets. For example, the leader &lt;code&gt;$B$&lt;/code&gt; can unlock the inbound payment &lt;code&gt;$(A, B)$&lt;/code&gt; by offering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The path &lt;code&gt;$p = (B)$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The secret &lt;code&gt;$s_B$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The signature &lt;code&gt;$sig(s_B, B)$&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Followers must trace outbound contracts unlocking events. For example, &lt;code&gt;$A$&lt;/code&gt; can unlock the contract for &lt;code&gt;$(C, A)$&lt;/code&gt; after receiving the unlocking event above:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The path &lt;code&gt;$p = (A, B)$&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The secret &lt;code&gt;$s_B$&lt;/code&gt; since B has published it when unlocking &lt;code&gt;$(A, B)$&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The signature &lt;code&gt;$sig(sig(s_B, B), A)$&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Herlihy, M. (2018). Atomic Cross-Chain Swaps (No. arXiv:1801.09515). arXiv. &lt;a href="https://doi.org/10.48550/arXiv.1801.09515"&gt;https://doi.org/10.48550/arXiv.1801.09515&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/blockchain/">Blockchain</category><category domain="https://blog.iany.me/tags/cryptography/">Cryptography</category><category domain="https://blog.iany.me/tags/distributed-system/">Distributed System</category></item><item><title>Envrc Alternative for PowerShell in Windows</title><link>https://blog.iany.me/2024/04/envrc-alternative-for-powershell-in-windows/</link><pubDate>Sat, 06 Apr 2024 08:55:34 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2024/04/envrc-alternative-for-powershell-in-windows/</guid><description>&lt;p&gt;This post introduces a solution for automatically setting up and tearing down shell environments for PowerShell in Windows. It is proposed as a potential alternative to the bash-based tool &lt;a href="https://github.com/direnv/direnv"&gt;direnv&lt;/a&gt;, which, while effective at loading &lt;code&gt;.envrc&lt;/code&gt; files in the current or nearest ancestor directory, has limited compatibility with PowerShell in Windows.&lt;/p&gt;
&lt;h2 id="envrcps1"&gt;.envrc.ps1&lt;/h2&gt;
&lt;p&gt;The basic idea is creating a file &lt;code&gt;.envrc.ps1&lt;/code&gt; with two entries: setup and teardown. The setup entry is responsible for setting up the environment up entering the directory, and the teardown entry is used when leaving it.&lt;/p&gt;
&lt;p&gt;PowerShell allows code to be executed from a file using the dot command. This means that the entire file can be used as the setup entry. For the teardown entry, a function can be defined within the same file, and I use the function name &lt;code&gt;down&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The layout of the file &lt;code&gt;.envrc.ps1&lt;/code&gt; looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;# setup
$env:GH_TOKEN = &amp;quot;secret&amp;quot;
# teardown
function global:down {
$env:GH_TOKEN = &amp;quot;&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;global:&lt;/code&gt; scope before the function name is necessary when this file is loaded in the hook, as I will do later.&lt;/p&gt;
&lt;p&gt;The corresponding commands for setup and teardown entries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Setup: &lt;code&gt;. .envrc.ps1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Teardown: &lt;code&gt;down&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="hook"&gt;Hook&lt;/h2&gt;
&lt;p&gt;To hook the setup and teardown entries, I reference &lt;a href="https://github.com/PowerShell/PowerShell/issues/14484#issuecomment-1731647083"&gt;this example&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;using namespace System;
using namespace System.Management.Automation;
$hook = [EventHandler[LocationChangedEventArgs]] {
param([object] $source, [LocationChangedEventArgs] $eventArgs)
end {
# 1. `down` for $eventArgs.OldPath
# 2. `. .envrc.ps1` for $eventArgs.NewPath
}
};
$currentAction = $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction;
if ($currentAction) {
$ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = [Delegate]::Combine($currentAction, $hook);
} else {
$ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = $hook;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To run the code snippet, PowerShell 7 or later is required. You can install it from the &lt;a href="https://github.com/PowerShell/PowerShell"&gt;repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here is the algorithm for identifying setup and teardown entries:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Find the nearest &lt;code&gt;.envrc.ps1&lt;/code&gt; for the old path.&lt;/li&gt;
&lt;li&gt;Find the nearest &lt;code&gt;.envrc.ps1&lt;/code&gt; for the new path.&lt;/li&gt;
&lt;li&gt;If the two paths are the same, skip the next step.&lt;/li&gt;
&lt;li&gt;If they differ, invoke the &lt;code&gt;down&lt;/code&gt; function and remove it if it exists. Then, if found, load the &lt;code&gt;.envrc.ps1&lt;/code&gt; file for the new path.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The full code example to be added into PowerShell &lt;code&gt;$PROFILE&lt;/code&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;using namespace System;
using namespace System.Management.Automation;
# existing PowerShell profile contents
# skip when version is less than 7
if ($PSVersionTable.PSVersion.Major -lt 7) {
return
}
# find the .envrc.ps1 in the currenct directory or the nearest ancestor directory.
function Find-NearestEnvrc {
param (
[Parameter(Mandatory=$true)]
[string]$StartDir
)
$currentDir = Resolve-Path $StartDir
while ($currentDir -ne &amp;quot;&amp;quot;) {
$envrcPath = Join-Path $currentDir &amp;quot;.envrc.ps1&amp;quot;
if (Test-Path $envrcPath) {
return $envrcPath
}
$currentDir = Split-Path $currentDir -Parent
}
return &amp;quot;&amp;quot;
}
# hook the setup and teardown entries
$hook = [EventHandler[LocationChangedEventArgs]] {
param([object] $source, [LocationChangedEventArgs] $eventArgs)
end {
$oldEnvrc = Find-NearestEnvrc $eventArgs.OldPath
$newEnvrc = Find-NearestEnvrc $eventArgs.NewPath
if ($oldEnvrc -ne $newEnvrc) {
Get-Command down -ErrorAction SilentlyContinu
if (Get-Command down -ErrorAction SilentlyContinu) {
down
Remove-Item Function:down
}
if ($newEnvrc -ne &amp;quot;&amp;quot;) {
. $newEnvrc
}
}
}
};
$currentAction = $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction;
if ($currentAction) {
$ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = [Delegate]::Combine($currentAction, $hook);
} else {
$ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = $hook;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="example-usage"&gt;Example Usage&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-powershell"&gt;# setup
# set environment variable
$env:GH_TOKEN = &amp;quot;secret&amp;quot;
# load python virtual env
. .venv\Scripts\Activate.ps1
# add a helper function
function global:build {
cargo build
}
# teardown
function global:down {
# unset environment variable
$env:GH_TOKEN = &amp;quot;&amp;quot;
# unload python virtual env
deactivate
# remove the helper function
Remove-Item Function:build
}
&lt;/code&gt;&lt;/pre&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/environment-variables/">Environment Variables</category><category domain="https://blog.iany.me/tags/powershell/">PowerShell</category><category domain="https://blog.iany.me/tags/windows/">Windows</category></item><item><title>How to Verify JoyID WebAuthn Signature</title><link>https://blog.iany.me/2023/12/how-to-verify-joyid-webauthn-signature/</link><pubDate>Sun, 17 Dec 2023 17:35:33 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2023/12/how-to-verify-joyid-webauthn-signature/</guid><description>&lt;p&gt;&lt;a href="https://docs.joy.id/guide"&gt;JoyID&lt;/a&gt; is a multichain, cross-platform, passwordless and mnemonic-free wallet solution based on FIDO WebAuthn protocol and Nervos CKB.&lt;/p&gt;
&lt;p&gt;This post shows how to verify the signature from the method &lt;a href="https://docs.joy.id/guide/ckb/sign-message"&gt;signChallenge&lt;/a&gt; of the &lt;code&gt;@joyid/ckb&lt;/code&gt; package. The method reference page has a demo. I use the demo to obtain an example response then verify the response using the OpenSSL command line and the Python library &lt;a href="https://pycryptodome.readthedocs.io/en/latest/src/introduction.html"&gt;PyCryptodome&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The JoyID follows the WebAuthn specification and employs secp256r1 for signing. Although the guide references &lt;a href="https://www.w3.org/TR/webauthn-2/#sctn-op-get-assertion"&gt;section 6.3.3&lt;/a&gt; of the WebAuthn specification, titled &amp;ldquo;The authenticatorGetAssertion Operation&amp;rdquo;, I discovered that the example in &lt;a href="https://github.com/duo-labs/py_webauthn/blob/master/webauthn/authentication/verify_authentication_response.py"&gt;this repository&lt;/a&gt; provided me much more helps.&lt;/p&gt;
&lt;h2 id="the-response-parsing"&gt;The Response Parsing&lt;/h2&gt;
&lt;p&gt;This is the example I obtained from the demo.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
&amp;quot;signature&amp;quot;: &amp;quot;MEUCICF25qdO6nLreEoBHnyaw-9R6XFHbIu-NwsAI53t016qAiEAgmhlwTEMxoWxKj79R1rUkB_6nrhJfws82DqHkY_HnqQ&amp;quot;,
&amp;quot;message&amp;quot;: &amp;quot;K4sF4fAwPvuJj-TW3mARmMenuGSrvmohxzsueH4YfFIFAAAAAHsidHlwZSI6IndlYmF1dGhuLmdldCIsImNoYWxsZW5nZSI6IlUybG5iaUIwYUdseklHWnZjaUJ0WlEiLCJvcmlnaW4iOiJodHRwczovL3Rlc3RuZXQuam95aWQuZGV2IiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0&amp;quot;,
&amp;quot;challenge&amp;quot;: &amp;quot;Sign this for me&amp;quot;,
&amp;quot;alg&amp;quot;: -7,
&amp;quot;pubkey&amp;quot;: &amp;quot;3538dfd53ad93d2e0a6e7f470295dcd71057d825e1f87229e5afe2a906aa7cfc099fdfa04442dac33548b6988af8af58d2052529088f7b73ef00800f7fbcddb3&amp;quot;,
&amp;quot;keyType&amp;quot;: &amp;quot;main_key&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="pubkey"&gt;pubkey&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;pubkey&lt;/code&gt; field represents the uncompressed public key concatenating two 32-byte integers in hex. PyCryptodome can import the key by prepending the flag &lt;code&gt;0x04&lt;/code&gt;. OpenSSL uses PEM to encode keys, and PyCryptodome can help here to export the key in PEM format.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;from Crypto.PublicKey import ECC
pubkey_raw_hex = &amp;quot;3538dfd53ad93d2e0a6e7f470295dcd71057d825e1f87229e5afe2a906aa7cfc099fdfa04442dac33548b6988af8af58d2052529088f7b73ef00800f7fbcddb3&amp;quot;
pubkey = ECC.import_key(bytes.fromhex(&amp;quot;04&amp;quot; + pubkey_raw_hex), curve_name=&amp;quot;secp256r1&amp;quot;)
with open(&amp;quot;pubkey.pem&amp;quot;, &amp;quot;wt&amp;quot;) as pemfile:
pemfile.write(pubkey.export_key(format=&amp;quot;PEM&amp;quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Double check the key using OpenSSL:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell-session"&gt;$ openssl ec -text -inform PEM -in pubkey.pem -pubin
...
Public-Key: (256 bit)
pub:
04:35:38:df:d5:3a:d9:3d:2e:0a:6e:7f:47:02:95:
dc:d7:10:57:d8:25:e1:f8:72:29:e5:af:e2:a9:06:
aa:7c:fc:09:9f:df:a0:44:42:da:c3:35:48:b6:98:
8a:f8:af:58:d2:05:25:29:08:8f:7b:73:ef:00:80:
0f:7f:bc:dd:b3
ASN1 OID: prime256v1
NIST CURVE: P-256
...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="message"&gt;message&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;message&lt;/code&gt; is a binary encoded by base64 &lt;a href="https://datatracker.ietf.org/doc/html/rfc4648#section-5"&gt;RFC 4648 §5&lt;/a&gt; without the equal sign (&lt;code&gt;=&lt;/code&gt;) paddings. Many base64 tools and libraries require padding equal sign (&lt;code&gt;=&lt;/code&gt;) in the end of the string to make the length multiple of 4. The &lt;code&gt;message&lt;/code&gt; in the example response has a length 351, which requires one &lt;code&gt;=&lt;/code&gt; padding. A trick is always padding two equals at the end of the string before decoding.&lt;/p&gt;
&lt;p&gt;The first 37 bytes in &lt;code&gt;message&lt;/code&gt; are authenticator data, and the following bytes are client data in JSON.&lt;/p&gt;
&lt;p&gt;The section &lt;a href="https://www.w3.org/TR/webauthn-2/#sctn-authenticator-data"&gt;section 6.1&lt;/a&gt; in the WebAuthn specification defines the layout of the authenticator data.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rpIdHash&lt;/code&gt;, 32 bytes: the sha256 checksum of the text &lt;code&gt;testnet.joyid.dev&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flags&lt;/code&gt;, 1 byte: &lt;code&gt;0x05&lt;/code&gt; in JoyID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;signCount&lt;/code&gt;, 4 bytes: all zeros&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;base64 -d &amp;lt;&amp;lt;&amp;lt;'
K4sF4fAwPvuJj-TW3mARmMenuGSrvmohxzsueH4YfFIFAAAAAHsidHlwZSI6Indl
YmF1dGhuLmdldCIsImNoYWxsZW5nZSI6IlUybG5iaUIwYUdseklHWnZjaUJ0WlEi
LCJvcmlnaW4iOiJodHRwczovL3Rlc3RuZXQuam95aWQuZGV2IiwiY3Jvc3NPcmln
aW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90
IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUg
aHR0cHM6Ly9nb28uZ2wveWFiUGV4In0=' |
dd bs=1 count=37 2&amp;gt;/dev/null |
xxd
#=&amp;gt; 00000000: 2b8b 05e1 f030 3efb 898f e4d6 de60 1198
#=&amp;gt; 00000010: c7a7 b864 abbe 6a21 c73b 2e78 7e18 7c52
#=&amp;gt; 00000020: 0500 0000 00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check the first two lines with the sha256 checksum:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;echo -n 'testnet.joyid.dev' | sha256sum
#=&amp;gt; 2b8b05e1f0303efb898fe4d6de601198c7a7b864abbe6a21c73b2e787e187c52 -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The client data JSON looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;base64 -d &amp;lt;&amp;lt;&amp;lt;'
K4sF4fAwPvuJj-TW3mARmMenuGSrvmohxzsueH4YfFIFAAAAAHsidHlwZSI6Indl
YmF1dGhuLmdldCIsImNoYWxsZW5nZSI6IlUybG5iaUIwYUdseklHWnZjaUJ0WlEi
LCJvcmlnaW4iOiJodHRwczovL3Rlc3RuZXQuam95aWQuZGV2IiwiY3Jvc3NPcmln
aW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90
IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUg
aHR0cHM6Ly9nb28uZ2wveWFiUGV4In0=' |
dd bs=1 skip=37 2&amp;gt;/dev/null |
jq
{
&amp;quot;type&amp;quot;: &amp;quot;webauthn.get&amp;quot;,
&amp;quot;challenge&amp;quot;: &amp;quot;U2lnbiB0aGlzIGZvciBtZQ&amp;quot;,
&amp;quot;origin&amp;quot;: &amp;quot;https://testnet.joyid.dev&amp;quot;,
&amp;quot;crossOrigin&amp;quot;: false,
...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the &lt;code&gt;challenge&lt;/code&gt; field. It is the parameter passed to &lt;code&gt;signChallenge&lt;/code&gt;, in base64.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;base64 -d &amp;lt;&amp;lt;&amp;lt;'U2lnbiB0aGlzIGZvciBtZQ=='
#=&amp;gt; Sign this for me
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Attention that message is not the binary to be signed. According to the Figure 4, Generating an assertion signature, in &lt;a href="https://www.w3.org/TR/webauthn-2/#sctn-authenticator-data"&gt;the WebAuthn specification&lt;/a&gt;, the binary to be signed is a concatenation of the authenticator data and the sha256 checksum of the client data JSON.&lt;/p&gt;
&lt;p&gt;The following code shows how to prepare the message to sign and save it into the file &lt;code&gt;message.bin&lt;/code&gt;. Attention that base64 must use the alternative keys &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt; to replace &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt; respectively.&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
Attention
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
To decode base64 &amp;ldquo;RFC 4648 §5&amp;rdquo; in python, use either &lt;code&gt;base64.b64decode(s, altchars=&amp;quot;-_&amp;quot;)&lt;/code&gt; or &lt;code&gt;binascii.urlsafe_b64decode(s)&lt;/code&gt;.
&lt;/div&gt;
&lt;/details&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import base64
from Crypto.Hash import SHA256
message_bin = base64.urlsafe_b64decode(
&amp;quot;K4sF4fAwPvuJj-TW3mARmMenuGSrvmohxzsueH4YfFIFAAAAAHsidHlwZSI6Indl&amp;quot;
&amp;quot;YmF1dGhuLmdldCIsImNoYWxsZW5nZSI6IlUybG5iaUIwYUdseklHWnZjaUJ0WlEi&amp;quot;
&amp;quot;LCJvcmlnaW4iOiJodHRwczovL3Rlc3RuZXQuam95aWQuZGV2IiwiY3Jvc3NPcmln&amp;quot;
&amp;quot;aW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90&amp;quot;
&amp;quot;IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUg&amp;quot;
&amp;quot;aHR0cHM6Ly9nb28uZ2wveWFiUGV4In0==&amp;quot;,
)
authenticator_data = message_bin[:37]
client_data = message_bin[37:]
message_to_sign = authenticator_data + SHA256.new(client_data).digest()
with open(&amp;quot;message.bin&amp;quot;, &amp;quot;wb&amp;quot;) as fout:
fout.write(message_to_sign)
&lt;/code&gt;&lt;/pre&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-attention" data-callout-type="attention"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-exclamation-triangle"&gt;&lt;/i&gt;
Attention
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
The &lt;code&gt;message&lt;/code&gt; in the response is not the binary to be signed. Instead, the binary to be signed is a concatenation of the authenticator data and the sha256 checksum of the client data JSON.
&lt;/div&gt;
&lt;/details&gt;
&lt;h3 id="signature"&gt;signature&lt;/h3&gt;
&lt;p&gt;The field signature are two 32-byte integers first encoded in &lt;a href="https://wiki.openssl.org/index.php/DER"&gt;DER&lt;/a&gt;, then base64 &lt;a href="https://datatracker.ietf.org/doc/html/rfc4648#section-5"&gt;RFC 4648 §5&lt;/a&gt; without the equal sign (&lt;code&gt;=&lt;/code&gt;) paddings.&lt;/p&gt;
&lt;p&gt;Many base64 tools and libraries require padding equal sign (&lt;code&gt;=&lt;/code&gt;) in the end of the string to make the length multiple of 4. The signature in the example response has a length 95, which requires one &lt;code&gt;=&lt;/code&gt; padding.&lt;/p&gt;
&lt;p&gt;OpenSSL also stores signature in DER, let&amp;rsquo;s save one in the file &lt;code&gt;signature.der&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;base64 -d &amp;lt;&amp;lt;&amp;lt;&amp;quot;MEUCICF25qdO6nLreEoBHnyaw-9R6XFHbIu-NwsAI53t016qAiEAgmhlwTEMxoWxKj79R1rUkB_6nrhJfws82DqHkY_HnqQ=&amp;quot; &amp;gt; signature.der
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The command &lt;code&gt;openssl asn1parse&lt;/code&gt; can parse the file &lt;code&gt;signature.der&lt;/code&gt; in the DER format.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;openssl asn1parse -dump -inform DER -in signature.der
# Output =&amp;gt;
# 0:d=0 hl=2 l= 69 cons: SEQUENCE
# 2:d=1 hl=2 l= 32 prim: INTEGER :2176E6A74EEA72EB784A011E7C9AC3EF51E971476C8BBE370B00239DEDD35EAA
# 36:d=1 hl=2 l= 33 prim: INTEGER :826865C1310CC685B12A3EFD475AD4901FFA9EB8497F0B3CD83A87918FC79EA4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PyCryptodome expects the signature of 64 bytes for two 32-byte integers. Following code uses the &lt;code&gt;asn1&lt;/code&gt; module to extract the raw signature from the DER binary.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import base64
from Crypto.Util.asn1 import DerSequence
signature_der = base64.urlsafe_b64decode(
&amp;quot;MEUCICF25qdO6nLreEoBHnyaw-9R6XFHbIu-NwsAI53t016qAiEAgmhlwTEMxoWx&amp;quot;
&amp;quot;Kj79R1rUkB_6nrhJfws82DqHkY_HnqQ=&amp;quot;,
)
signature_seq = DerSequence()
signature_seq.decode(signature_der)
print(signature_seq[0].to_bytes(32).hex())
# =&amp;gt; 2176e6a74eea72eb784a011e7c9ac3ef51e971476c8bbe370b00239dedd35eaa
print(signature_seq[1].to_bytes(32).hex())
# =&amp;gt; 826865c1310cc685b12a3efd475ad4901ffa9eb8497f0b3cd83a87918fc79ea4
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="verifying"&gt;Verifying&lt;/h2&gt;
&lt;p&gt;PyCryptodome:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;from Crypto.Hash import SHA256
from Crypto.Signature import DSS
DSS.new(pubkey, &amp;quot;fips-186-3&amp;quot;).verify(SHA256.new(message_to_sign), signature)
print(&amp;quot;Verified OK&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OpenSSL:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;openssl dgst -sha256 -verify pubkey.pem -signature signature.der message.bin
&lt;/code&gt;&lt;/pre&gt;
&lt;details class="kg-card kg-callout kg-callout-code" data-callout-type="code"&gt;
&lt;summary class="kg-callout-title"&gt;
&lt;i class="fas fa-chevron-right"&gt;&lt;/i&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-code"&gt;&lt;/i&gt;
Full Python code (&lt;a href="https://gist.github.com/doitian/b1f5c60203e9dbaffccff7d0920d9529"&gt;Gist&lt;/a&gt;)
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import base64
from Crypto.Hash import SHA256
from Crypto.PublicKey import ECC
from Crypto.Signature import DSS
from Crypto.Util.asn1 import DerSequence
response = {
&amp;quot;signature&amp;quot;: &amp;quot;MEUCICF25qdO6nLreEoBHnyaw-9R6XFHbIu-NwsAI53t016qAiEAgmhlwTEMxoWx&amp;quot;
&amp;quot;Kj79R1rUkB_6nrhJfws82DqHkY_HnqQ&amp;quot;,
&amp;quot;message&amp;quot;: &amp;quot;K4sF4fAwPvuJj-TW3mARmMenuGSrvmohxzsueH4YfFIFAAAAAHsidHlwZSI6IndlYmF1dGhuLmdldCIsImNoYWxsZW5nZSI6IlUybG5iaUIwYUdseklHWnZjaUJ0WlEiLCJvcmlnaW4iOiJodHRwczovL3Rlc3RuZXQuam95aWQuZGV2IiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0&amp;quot;,
&amp;quot;challenge&amp;quot;: &amp;quot;Sign this for me&amp;quot;,
&amp;quot;alg&amp;quot;: -7,
&amp;quot;pubkey&amp;quot;: &amp;quot;3538dfd53ad93d2e0a6e7f470295dcd71057d825e1f87229e5afe2a906aa7cfc099fdfa04442dac33548b6988af8af58d2052529088f7b73ef00800f7fbcddb3&amp;quot;,
&amp;quot;keyType&amp;quot;: &amp;quot;main_key&amp;quot;,
}
pubkey = ECC.import_key(
bytes.fromhex(&amp;quot;04&amp;quot; + response[&amp;quot;pubkey&amp;quot;]),
curve_name=&amp;quot;secp256r1&amp;quot;,
)
with open(&amp;quot;pubkey.pem&amp;quot;, &amp;quot;wt&amp;quot;) as fout:
fout.write(pubkey.export_key(format=&amp;quot;PEM&amp;quot;))
message_bin = base64.urlsafe_b64decode(response[&amp;quot;message&amp;quot;] + &amp;quot;==&amp;quot;)
authenticator_data = message_bin[:37]
client_data = message_bin[37:]
# https://github.com/duo-labs/py_webauthn/blob/master/webauthn/authentication/verify_authentication_response.py
message_to_sign = authenticator_data + SHA256.new(client_data).digest()
with open(&amp;quot;message.bin&amp;quot;, &amp;quot;wb&amp;quot;) as fout:
fout.write(message_to_sign)
signature_der = base64.urlsafe_b64decode(response[&amp;quot;signature&amp;quot;] + &amp;quot;==&amp;quot;)
with open(&amp;quot;signature.der&amp;quot;, &amp;quot;wb&amp;quot;) as fout:
fout.write(signature_der)
signature_seq = DerSequence()
signature_seq.decode(signature_der)
signature = signature_seq[0].to_bytes(32) + signature_seq[1].to_bytes(32)
DSS.new(pubkey, &amp;quot;fips-186-3&amp;quot;).verify(SHA256.new(message_to_sign), signature)
print(&amp;quot;Verified OK&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/cryptography/">Cryptography</category><category domain="https://blog.iany.me/tags/javascript/">JavaScript</category><category domain="https://blog.iany.me/tags/webauthn/">Webauthn</category></item><item><title>Renaming Browser Tab Names</title><link>https://blog.iany.me/2023/07/renaming-browser-tab-names/</link><pubDate>Fri, 21 Jul 2023 20:31:59 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2023/07/renaming-browser-tab-names/</guid><description>&lt;p&gt;Renaming browser tab names may seem like a simple task, but it can actually be quite challenging.&lt;/p&gt;
&lt;p&gt;Most browsers sync the tab name with the web page title. Therefore, it seems simple to set tab name by setting &lt;code&gt;document.title&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;document.title = &amp;quot;Custom Tab Name&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, many web pages use JavaScript to alter the page title, which would override the custom name. While &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty"&gt;&lt;code&gt;Object.defineProperty&lt;/code&gt;&lt;/a&gt; in JavaScript can override the property setter function, it&amp;rsquo;s not possible to access the original setter function for &lt;code&gt;document.title&lt;/code&gt;. Fortunately, browsers sync page titles with the first &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; tag, so setting its content acts as a workaround to set the page title.&lt;/p&gt;
&lt;p&gt;Here is the bookmark for renaming a tab. I also add &lt;code&gt;%t&lt;/code&gt; as the token for the original page tab title. This is handy such as adding a custom prefix with &lt;code&gt;Work: %t&lt;/code&gt;, or restoring the original title without reloading the page by using &lt;code&gt;%t&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;// Rename the document title using a bookmarklet.
//
// As a user,
// - Once I have renamed the tab, it should not be overwritten.
// - I can include the token %t as placeholder for the real title.
// - To restore, I can rename the tab to %t
//
// Install: Copy the code to
//
// https://caiorss.github.io/bookmarklet-maker/
//
// and generate the bookmarklet.
// Init only once
if (!(&amp;quot;_renameTitle&amp;quot; in document)) {
// Force creating the title tag.
document.title = document.title || &amp;quot;&amp;quot;;
const titleEl = document.getElementsByTagName(&amp;quot;title&amp;quot;)[0];
const titleTokenRegex = /%t/g;
// Remembers the real title
let titleWithoutRenaming = document.title;
// User set title
let titleWithRenaming = &amp;quot;&amp;quot;;
// Rename the document title to v.
// If v contains the token %t, replace all occurences to the
// real title.
document._renameTitle = (v) =&amp;gt; {
titleWithRenaming = v;
titleEl.innerText = v.replace(titleTokenRegex, titleWithoutRenaming);
};
Object.defineProperty(document, &amp;quot;title&amp;quot;, {
// Other code will use document.title setter to change the title.
//
// Remember the new value as the real title but still use the
// title set by user.
set: (v) =&amp;gt; {
titleWithoutRenaming = v;
// Once document has defined the title property, its value is
// not synchronized to the tab name.
//
// Here uses a workaround to set the title tag content.
titleEl.innerText = titleWithRenaming.replace(titleTokenRegex, v);
},
get: () =&amp;gt; titleEl.innerText,
});
}
const title = prompt(
&amp;quot;Rename tab (Use token %t for original title)&amp;quot;,
document.title
);
// title is null when user cancel the dialog.
if (title !== null) {
document._renameTitle(title);
}
&lt;/code&gt;&lt;/pre&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/browser/">Browser</category><category domain="https://blog.iany.me/tags/javascript/">JavaScript</category><category domain="https://blog.iany.me/tags/productivity/">Productivity</category></item><item><title>Transforming Markdown to Attractive PDFs: A Guide to Using Pandoc with Xelatex</title><link>https://blog.iany.me/2023/04/transforming-markdown-to-attractive-pdfs-a-guide-to-using-pandoc-with-xelatex/</link><pubDate>Sat, 29 Apr 2023 21:43:02 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2023/04/transforming-markdown-to-attractive-pdfs-a-guide-to-using-pandoc-with-xelatex/</guid><description>&lt;p&gt;I used to read lengthy and complex articles in the PDF format. Despite the abundance of PDF exporting options, the resulting files often appear unappealing. However, there is a solution. With the pandoc tool and the xelatex backend, you can transform Markdown files into aesthetically pleasing PDFs. In this tutorial, I will guide you through the steps of using pandoc with xelatex and share the lessons I&amp;rsquo;ve learned.&lt;/p&gt;
&lt;h2 id="installation"&gt;Installation&lt;/h2&gt;
&lt;p&gt;Ubuntu users can install the package &lt;code&gt;texlive-xetex&lt;/code&gt; and &lt;code&gt;pandoc&lt;/code&gt;, In macOS, I recommend a minimal installation:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;brew install librsvg pandoc
brew install --cask basictex
sudo tlmgr update --self
sudo tlmgr update --all
for pkg in texliveonfly xelatex adjustbox tcolorbox collectbox ucs environ \
trimspaces titling enumitem rsfs xecjk fvextra svg transparent; do
sudo tlmgr install $pkg
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The library &lt;code&gt;librsvg&lt;/code&gt; is required to support SVG images.&lt;/p&gt;
&lt;p&gt;Verify the installation by converting a simple markdown file.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;echo &amp;quot;# Hello World&amp;quot; &amp;gt; test.md
pandoc --pdf-engine=xelatex -i test.md -o test.pdf
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="tips"&gt;Tips&lt;/h2&gt;
&lt;p&gt;In LaTeX, the document class serves as the template for the basic page styles.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m a fan of &lt;a href="https://ctan.org/pkg/koma-script?lang=en"&gt;koma-script&lt;/a&gt;, and I will choose between &lt;em&gt;scrreprt&lt;/em&gt; and &lt;em&gt;scrbook&lt;/em&gt; depending whether the file to create is more like a report or a book. I recommend try them and the standard classes like &lt;em&gt;report&lt;/em&gt; and &lt;em&gt;book&lt;/em&gt;, then choose the one that best suites your preferences.&lt;/p&gt;
&lt;p&gt;To customize the document class in pandoc, simply set the variable &lt;code&gt;documentclass&lt;/code&gt;. Other &lt;a href="https://pandoc.org/MANUAL.html#variables-for-latex"&gt;variables&lt;/a&gt; are available for controlling the LaTeX style. I use some of them to define my default style.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;DOCUMENTCLASS=&amp;quot;${DOCUMENTCLASS:-scrreprt}&amp;quot;
MAINFONT=&amp;quot;${MAINFONT:-Helvetica}&amp;quot;
MONOFONT=&amp;quot;${MONOFONT:-Cartograph CF}&amp;quot;
pandoc --pdf-engine=xelatex \
--variable fontsize=12pt \
--variable linestretch=1.5 \
--variable geometry=a4paper \
--variable documentclass=&amp;quot;${DOCUMENTCLASS}&amp;quot; \
--variable mainfont=&amp;quot;${MAINFONT}&amp;quot; \
--variable monofont=&amp;quot;${MONOFONT}&amp;quot; \
-i test.md -o test.pdf
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="gotchas"&gt;Gotchas&lt;/h2&gt;
&lt;h3 id="cjk"&gt;CJK&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;CJK&lt;/strong&gt; is a collective term for the Chinese, Japanese, and Korean languages. When &lt;code&gt;CJKmainfont&lt;/code&gt; is set, pandoc handles CJK characters with the &lt;code&gt;xecjk&lt;/code&gt; package.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;pandoc --variable CJKmainfont=&amp;quot;Noto Serif CJK SC&amp;quot; ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My recommendation is to only set &lt;code&gt;CJKmainfont&lt;/code&gt; when needed to avoid messing up the quotation marks display. Where the article contains a mixture of English and Asian characters resulting in strange looking quotation marks &lt;code&gt;'&amp;quot;“”‘’&lt;/code&gt;, consider trying the following solution.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a file &lt;code&gt;cjk.latex&lt;/code&gt; to force English quotation marks.
&lt;pre&gt;&lt;code class="language-latex"&gt;% cjk.tex
\AtBeginDocument{%
\XeTeXcharclass`^^^^2019=0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Include this file via the pandoc argument &lt;code&gt;-H&lt;/code&gt; such as &lt;code&gt;pandoc -H cjk.tex&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The figure below shows the results from the same markdown file on my MacBook. However, when I test this in Ubuntu, it seems there are no such issues.&lt;/p&gt;
&lt;figure class="kg-image-card kg-width-fit"&gt;
&lt;img alt="Fix CJK Quotation Marks" class="kg-image" loading="lazy" src="https://blog.iany.me/2023/04/transforming-markdown-to-attractive-pdfs-a-guide-to-using-pandoc-with-xelatex/pandoc-pdf-quotation-marks_hu_3473858a480b8700.png" srcset="https://blog.iany.me/2023/04/transforming-markdown-to-attractive-pdfs-a-guide-to-using-pandoc-with-xelatex/pandoc-pdf-quotation-marks_hu_e860b9b6beebbcbe.png 400w, https://blog.iany.me/2023/04/transforming-markdown-to-attractive-pdfs-a-guide-to-using-pandoc-with-xelatex/pandoc-pdf-quotation-marks_hu_7824d52cc0ee7640.png 800w, https://blog.iany.me/2023/04/transforming-markdown-to-attractive-pdfs-a-guide-to-using-pandoc-with-xelatex/pandoc-pdf-quotation-marks_hu_3473858a480b8700.png 1061w" sizes="(max-width: 800px) 100vw, 1061px" /&gt;
&lt;figcaption &gt;Fix CJK Quotation Marks&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;This is the input markdown file used in the example.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-markdown"&gt;English's &amp;quot;straight quotations&amp;quot; and this’s “curly quotations”.
中文的&amp;quot;直引号&amp;quot;和“弯引号”。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="code-block-syntax-highlight"&gt;Code Block Syntax Highlight&lt;/h3&gt;
&lt;p&gt;Pandoc does not wrap lines in code blocks by default. It truncated the lines which are too long to fit on the page.&lt;/p&gt;
&lt;p&gt;To solve the problem mentioned above, I followed a solution on &lt;a href="https://github.com/jgm/pandoc/issues/4302#issuecomment-360799891"&gt;GitHub&lt;/a&gt; suggested by jannick0. It involves the creation of a file that contains the LaTeX snippets below. These snippets can then be included by using the pandoc argument &lt;code&gt;-H highlighting.tex&lt;/code&gt;. Additionally, I added a border around the code block.&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-file" data-callout-type="file"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-file"&gt;&lt;/i&gt;
&lt;code&gt;highlighting.tex&lt;/code&gt;
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;pre&gt;&lt;code class="language-latex"&gt;% use this file via pandoc -H highlighting.tex
\usepackage{fvextra}
\DefineVerbatimEnvironment{Highlighting}{Verbatim}{breaklines,breaknonspaceingroup,breakanywhere,frame=single,framesep=8pt,rulecolor=\color[HTML]{aaaaaa},commandchars=\\\{\}}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;As I &lt;a href="https://github.com/jgm/pandoc/issues/4302#issuecomment-1508595755"&gt;replied in the thread&lt;/a&gt;, this solution does not work for code blocks without setting a language. I use a Lua filter to set the language of these code blocks to &amp;ldquo;text&amp;rdquo;. To run the filter, save it in a file, say &lt;code&gt;highlighting.lua&lt;/code&gt;, and invoke pandoc with &lt;code&gt;--lua-filter=highlighting.lua&lt;/code&gt;&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-file" data-callout-type="file"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-file"&gt;&lt;/i&gt;
&lt;code&gt;highlighting.lua&lt;/code&gt;
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;pre&gt;&lt;code class="language-lua"&gt;function CodeBlock(el)
if #el.classes == 0 then
el.classes[1] = 'text'
end
return el
end
return {{CodeBlock = CodeBlock}}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;h3 id="disable-figures-floating"&gt;Disable Figures Floating&lt;/h3&gt;
&lt;p&gt;The last issue to address is the floating of figures in LaTeX, which moves the images to avoid large blank space in pages. While LaTeX attempts to place figures in appropriate locations, authors may refer to them as &amp;ldquo;above&amp;rdquo; or &amp;ldquo;below&amp;rdquo; in the markdown files. This can be confusing for readers who may find that the referenced chart or diagram is actually on the following page in the PDF file.&lt;/p&gt;
&lt;p&gt;To solve the issue, simply disable floating figures. Create a file &lt;code&gt;disable-floating.tex&lt;/code&gt; and invoke pandoc with arguments &lt;code&gt;-H disable-floating.tex&lt;/code&gt;.&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-file" data-callout-type="file"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-file"&gt;&lt;/i&gt;
&lt;code&gt;disable-floating.tex&lt;/code&gt;
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;pre&gt;&lt;code class="language-latex"&gt;% https://stackoverflow.com/a/58840456/667158
\usepackage{float}
\let\origfigure\figure
\let\endorigfigure\endfigure
\renewenvironment{figure}[1][2] {
\expandafter\origfigure\expandafter[H]
} {
\endorigfigure
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id="putting-it-all-together"&gt;Putting It All Together&lt;/h2&gt;
&lt;p&gt;See the repo &lt;a href="https://github.com/doitian/pandoc-ide"&gt;doitian/pandoc-ide&lt;/a&gt;.&lt;/p&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/latex/">LaTeX</category><category domain="https://blog.iany.me/tags/markdown/">Markdown</category><category domain="https://blog.iany.me/tags/pdf/">PDF</category></item><item><title>The Ultimate Guide to Customizing Obsidian Vim Mode via QuickAdd</title><link>https://blog.iany.me/2023/04/the-ultimate-guide-to-customizing-obsidian-vim-mode-via-quickadd/</link><pubDate>Thu, 27 Apr 2023 21:22:30 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2023/04/the-ultimate-guide-to-customizing-obsidian-vim-mode-via-quickadd/</guid><description>&lt;p&gt;Obsidian is my go-to note-taking app because of its availability on all desktop and mobile platforms with the bonus of Vim mode. In this guide, I&amp;rsquo;ll show you how I customize the Vim mode in Obsidian to maximize my note-taking efficiency.&lt;/p&gt;
&lt;h2 id="setup-user-launch-scripts-via-quickadd"&gt;Setup User Launch Scripts via QuickAdd&lt;/h2&gt;
&lt;p&gt;To customize the Vim mode in Obsidian, JavaScript is the only solution. Obsidian allows running user scripts by implementing an plugin. I use an easy alternative to run a JavaScript file on launch via QuickAdd.&lt;/p&gt;
&lt;p&gt;First, let’s create the JavaScript file in the vault. Save the skeleton below as the file &lt;code&gt;vimrc.js&lt;/code&gt; in any folder in the vault. Keep in mind that Obsidian does not support editing JavaScript files, so use an external editor.&lt;/p&gt;
&lt;details open disabled class="kg-card kg-callout kg-callout-file" data-callout-type="file"&gt;
&lt;summary class="kg-callout-title" tabindex="-1"&gt;
&lt;i aria-hidden="true" class="kg-callout-type fas fa-file"&gt;&lt;/i&gt;
vimrc.js
&lt;/summary&gt;
&lt;div class="kg-callout-content"&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;function notice(text) {
new Notice(text);
return text;
}
module.exports = async function (context) {
const vim = window.CodeMirrorAdapter?.Vim;
if (vim === undefined) {
new Notice(`🔴error: vim mode is disabled`);
return;
}
// Add customizations here.
console.log(`🔵info: vimrc loaded`);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;Then, install QuickAdd by browsing the community plugins in Settings and follow the instructions on the Obsidian help page &lt;a href="https://help.obsidian.md/Extending&amp;#43;Obsidian/Community&amp;#43;plugins"&gt;Community Plugins&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, include the script in a QuickAdd macro and activate the option to execute the macro upon the plugin load.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open the tab QuickAdd in Settings.&lt;/li&gt;
&lt;li&gt;Click the button “Manage Macros”. In the pop-up dialog, fill in the macro name “vimrc” and click the button “Add macro“ to add a new macro vimrc.&lt;/li&gt;
&lt;li&gt;Toggle the option “Run on plugin load” on under the newly created macro.&lt;/li&gt;
&lt;li&gt;Configure the macro vimrc. Find the script file &lt;code&gt;vimrc&lt;/code&gt; in the “User Scripts” section and add the script as the only step in the macro.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Attention that the script runs once on launch or reload. Running “Reload app without saving” via Command Palette will reload the file after changes,&lt;/p&gt;
&lt;h2 id="vim-mode-customization-techniques"&gt;Vim Mode Customization Techniques&lt;/h2&gt;
&lt;p&gt;For the best tutorial on customizing Vim mode and what methods the &lt;code&gt;vim&lt;/code&gt; object provides, refer to &lt;a href="https://github.com/replit/codemirror-vim/blob/master/src/vim.js"&gt;how CodeMirror establishes the Vim mode&lt;/a&gt;. I will only introduce the methods that I use in &lt;a href="https://github.com/doitian/quickadd-settings/blob/main/shore/quickadd/vimrc.js"&gt;my own customizations&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="vimdefineex"&gt;&lt;code&gt;vim.defineEx&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;defineEx&lt;/code&gt; method enables users to execute a new ex command in Vim by typing &lt;code&gt;:&lt;/code&gt; in normal mode. For instance, the ex command &lt;code&gt;:w&lt;/code&gt; saves the file.&lt;/p&gt;
&lt;p&gt;I introduce &lt;code&gt;defineEx&lt;/code&gt; first because I need an ex command to run any Obsidian command so that I can use it to add key bindings later. I name this command &lt;code&gt;obr&lt;/code&gt;, which is the abbreviation of &lt;strong&gt;OB&lt;/strong&gt;sidian &lt;strong&gt;R&lt;/strong&gt;un. The command requires the Obsidian command identifier as the argument, for example, &lt;code&gt;:obr app:open-settings&lt;/code&gt; will open the Settings dialog.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;vim.defineEx(&amp;quot;obr&amp;quot;, &amp;quot;&amp;quot;, function (cm, params) {
if (params?.args?.length !== 1) {
throw new Error(notice(&amp;quot;🔴error: obr requires exactly 1 parameter&amp;quot;));
}
const command = params.args[0];
context.app.commands.executeCommandById(command);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To find the command identifier, I add another ex command &lt;code&gt;:obl&lt;/code&gt; to display a list of identifiers and copy the selected one. It accepts an argument to filter the result. For example, &lt;code&gt;obl open&lt;/code&gt; will list identifiers that contain the word &amp;ldquo;open&amp;rdquo;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;vim.defineEx(&amp;quot;obl&amp;quot;, &amp;quot;&amp;quot;, async function (cm, params) {
let commands = Object.keys(context.app.commands.commands);
for (const keyword of params?.args ?? []) {
commands = commands.filter((command) =&amp;gt; command.includes(keyword));
}
const choice = await context.quickAddApi.suggester(commands, commands);
if (choice !== null) {
await context.quickAddApi.utility.setClipboard(choice);
new Notice(`🔵info: copied ${choice} to the clipboard`);
}
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="vimmap"&gt;&lt;code&gt;vim.map&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;vim.map&lt;/code&gt; method takes two arguments, &lt;code&gt;lhs&lt;/code&gt; and &lt;code&gt;rhs&lt;/code&gt;, which can both be either a key sequence or an ex command. The meaning of the method varies depending on the arguments passed.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When both &lt;code&gt;lhs&lt;/code&gt; and &lt;code&gt;rhs&lt;/code&gt; are key sequence, it maps the key sequence &lt;code&gt;lhs&lt;/code&gt; to &lt;code&gt;rhs&lt;/code&gt;. For example, &lt;code&gt;vim.map('D', 'dd')&lt;/code&gt; will redefine &lt;code&gt;D&lt;/code&gt; to delete the whole line.&lt;/li&gt;
&lt;li&gt;When both &lt;code&gt;lhs&lt;/code&gt; and &lt;code&gt;rhs&lt;/code&gt; are ex commands, it creates a new ex command alias. For example, &lt;code&gt;vim.map(':Reload', ':obr app:reload')&lt;/code&gt; adds the alias &lt;code&gt;:Reload&lt;/code&gt; to run the Obsidian command &lt;code&gt;app:reload&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When &lt;code&gt;lhs&lt;/code&gt; is a key sequence, and &lt;code&gt;rhs&lt;/code&gt; is an ex command, it maps the key sequence to run the ex command. For example &lt;code&gt;vim.map('ZZ', ':obr app:reload')&lt;/code&gt; allows reloading Obsidian by typing ZZ.&lt;/li&gt;
&lt;li&gt;When &lt;code&gt;lhs&lt;/code&gt; is an ex command, and &lt;code&gt;rhs&lt;/code&gt; is a key sequence, executing the ex command has the same effect as typing the key sequence.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I have a bunch of mappings from key sequences to &lt;code&gt;:obr&lt;/code&gt;, for example, the z-family to operate on folds:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;vim.map(&amp;quot;zo&amp;quot;, &amp;quot;:obr editor:toggle-fold&amp;quot;);
vim.map(&amp;quot;zc&amp;quot;, &amp;quot;:obr editor:toggle-fold&amp;quot;);
vim.map(&amp;quot;za&amp;quot;, &amp;quot;:obr editor:toggle-fold&amp;quot;);
vim.map(&amp;quot;zR&amp;quot;, &amp;quot;:obr editor:unfold-all&amp;quot;);
vim.map(&amp;quot;zM&amp;quot;, &amp;quot;:obr editor:fold-all&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To use the space key as a key mapping prefix, it must first be &lt;code&gt;unmap&lt;/code&gt;ped.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;vim.unmap(&amp;quot;&amp;lt;Space&amp;gt;&amp;quot;);
vim.map(&amp;quot;&amp;lt;Space&amp;gt;&amp;lt;Space&amp;gt;&amp;quot;, &amp;quot;:obr switcher:open&amp;quot;);
vim.map(&amp;quot;&amp;lt;Space&amp;gt;n&amp;quot;, &amp;quot;:nohl&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="vimdefineaction-and--vimdefineoperator"&gt;&lt;code&gt;vim.defineAction&lt;/code&gt; and &lt;code&gt;vim.defineOperator&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;We can use both action and operator in &lt;code&gt;vim.mapCommand&lt;/code&gt;. The difference is that an operator performs upon text objects. See how CodeMirror defines the built-in &lt;a href="https://github.com/replit/codemirror-vim/blob/7e70ff7d321f9aa6600616a4d2ee81327394533a/src/vim.js#L2450"&gt;actions&lt;/a&gt; and &lt;a href="https://github.com/replit/codemirror-vim/blob/7e70ff7d321f9aa6600616a4d2ee81327394533a/src/vim.js#L2290"&gt;operators&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I have an action &lt;code&gt;swapLine&lt;/code&gt; to move lines around. It&amp;rsquo;s not perfect, since undo will reverse one step only.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;vim.defineAction(&amp;quot;swapLine&amp;quot;, function(_cm, { repeat, down }) {
const command = down ? &amp;quot;editor:swap-line-down&amp;quot; : &amp;quot;editor:swap-line-up&amp;quot;;
for (let i = 0; i &amp;lt; repeat; i++) {
context.app.commands.executeCommandById(command);
}
});
// undo after 5]e will only swap one line up.
vim.mapCommand(&amp;quot;]e&amp;quot;, &amp;quot;action&amp;quot;, &amp;quot;swapLine&amp;quot;, { down: true });
vim.mapCommand(&amp;quot;[e&amp;quot;, &amp;quot;action&amp;quot;, &amp;quot;swapLine&amp;quot;, { down: false });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The example for &lt;code&gt;vim.defineOperator&lt;/code&gt; is &lt;code&gt;titleCase&lt;/code&gt;, which formats the text in the &lt;a href="https://kb.iany.me/para/lets/w/Writing/Format&amp;#43;the&amp;#43;Title&amp;#43;Using&amp;#43;APA&amp;#43;Title&amp;#43;Case&amp;#43;Capitalization"&gt;APA Title Case Capitalization&lt;/a&gt;. For instance, &lt;code&gt;gzap&lt;/code&gt; formats the current paragraph in the title case.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-javascript"&gt;function titleCase(str, options) {
// See https://github.com/words/ap-style-title-case/blob/master/index.js how to format str to apa title case
}
// Following helper funnctions are borrowed from https://codemirror.net/5/keymap/vim.js
function cursorIsBefore(cur1, cur2) {
if (cur1.line &amp;lt; cur2.line) {
return true;
}
if (cur1.line == cur2.line &amp;amp;&amp;amp; cur1.ch &amp;lt; cur2.ch) {
return true;
}
return false;
}
function cursorMin(cur1, cur2) {
return cursorIsBefore(cur1, cur2) ? cur1 : cur2;
}
function findFirstNonWhiteSpaceCharacter(text) {
if (!text) {
return 0;
}
var firstNonWS = text.search(/\S/);
return firstNonWS == -1 ? text.length : firstNonWS;
}
vim.defineOperator(&amp;quot;titleCase&amp;quot;, function(cm, args, ranges, oldAnchor, newHead) {
const selections = cm.getSelections();
const newSelections = selections.map((s) =&amp;gt;
titleCase(s, { keepSpaces: true })
);
cm.replaceSelections(newSelections);
if (args.shouldMoveCursor) {
return newHead;
} else if (
!cm.state.vim.visualMode &amp;amp;&amp;amp;
args.linewise &amp;amp;&amp;amp;
ranges[0].anchor.line + 1 == ranges[0].head.line
) {
return {
line: oldAnchor.line,
ch: findFirstNonWhiteSpaceCharacter(cm.getLine(oldAnchor.line)),
};
} else if (args.linewise) {
return oldAnchor;
} else {
return cursorMin(ranges[0].anchor, ranges[0].head);
}
return newHead;
});
vim.mapCommand(&amp;quot;gz&amp;quot;, &amp;quot;operator&amp;quot;, &amp;quot;titleCase&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/obsidian/">Obsidian</category><category domain="https://blog.iany.me/tags/vim/">Vim</category></item><item><title>Practice Autofocus Method in Todoist</title><link>https://blog.iany.me/2022/10/practice-autofocus-method-in-todoist/</link><pubDate>Mon, 03 Oct 2022 19:26:38 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/2022/10/practice-autofocus-method-in-todoist/</guid><description>&lt;p&gt;I read the article &lt;em&gt;Autofocus: The Productivity System That Treats Your to-Do List Like a River&lt;/em&gt;&lt;sup id="fnxref:1"&gt;&lt;a href="https://blog.iany.me/2022/10/practice-autofocus-method-in-todoist/#fnx:1"&gt;1&lt;/a&gt;&lt;/sup&gt; recently. I used to fill the time slots with tasks daily, but I rarely completed all the planned tasks. Even for the completed ones, I often missed the scheduled time slot. I felt stressed and guilty. So I decide to give the Autofocus Method a try.&lt;/p&gt;
&lt;h2 id="the-river-filter"&gt;The River Filter&lt;/h2&gt;
&lt;p&gt;I will continue using Todoist. I created a filter named &lt;em&gt;River&lt;/em&gt;. The tasks are sorted by added date. I added the filter to the sidebar and set it as my home view for easy access.&lt;/p&gt;
&lt;p&gt;The filter has three sections:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Overdue tasks: &lt;code&gt;overdue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Tasks I am doing: &lt;code&gt;today | @now💧&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Tasks in River: &lt;code&gt;!##⏰ Events &amp;amp; !overdue &amp;amp; !today &amp;amp; !@now💧 &amp;amp; !@someday🪨&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Todoist supports filter sections by separating queries with comma (&lt;code&gt;,&lt;/code&gt;). So here is the final filter query:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;overdue, today | @now💧, !##⏰ Events &amp;amp; !overdue &amp;amp; !today &amp;amp; !@now💧 &amp;amp; !@someday🪨
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="section-1-overdue-tasks"&gt;Section 1: Overdue Tasks&lt;/h3&gt;
&lt;p&gt;This section allows me to re-schedule the overdue tasks.&lt;/p&gt;
&lt;p&gt;I decided to not totally abandon the due dates. However, I use them with great moderation. I set a due date if I must complete the task on that date, and before the day comes, I can forget the task.&lt;/p&gt;
&lt;p&gt;I add such tasks to the project &lt;code&gt;⏰ Events&lt;/code&gt;. They will appear in the River filter when they are due or overdue.&lt;/p&gt;
&lt;h3 id="section-2-tasks-i-am-doing"&gt;Section 2: Tasks I Am Doing&lt;/h3&gt;
&lt;p&gt;This section lists tasks that I am doing now.&lt;/p&gt;
&lt;p&gt;There are two kinds of such tasks.&lt;/p&gt;
&lt;p&gt;The first is the scheduled tasks that are due today.&lt;/p&gt;
&lt;p&gt;The second is the selected tasks from the river. I add the tag &lt;code&gt;@now💧&lt;/code&gt; to the task if I feel like doing it.&lt;/p&gt;
&lt;h3 id="section-3-tasks-in-river"&gt;Section 3: Tasks in River&lt;/h3&gt;
&lt;p&gt;This section contains the tasks in the river. It excludes tasks from the project &lt;code&gt;⏰ Events&lt;/code&gt;, and the dismissed tasks. I tag dismissed tasks with &lt;code&gt;@someday🪨&lt;/code&gt; .&lt;/p&gt;
&lt;h2 id="the-workflow"&gt;The Workflow&lt;/h2&gt;
&lt;p&gt;I add new tasks to the inbox without triaging the projects, tags, due dates, and priorities. Since the filter&lt;/p&gt;
&lt;p&gt;When I have time to do something, I open the River filter and scan the tasks from top to bottom without scrolling down.&lt;/p&gt;
&lt;p&gt;First, I re-schedule the overdue tasks.&lt;/p&gt;
&lt;p&gt;Then, I do a quick scan on the second section—the tasks that I planned before. If I feel like doing a task in the list, I start working on it.&lt;/p&gt;
&lt;p&gt;If I failed to find a task to work on in the previous step, I continue the scan to the last section—the tasks in the river. I add tag &lt;code&gt;@now💧&lt;/code&gt; to pop the task to the top of the list.&lt;/p&gt;
&lt;p&gt;If there are no items stands out for me, I have three choices to remove tasks from the first screen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Defer the scheduled tasks into future.&lt;/li&gt;
&lt;li&gt;Remove the tag &lt;code&gt;@now💧&lt;/code&gt; from the task.&lt;/li&gt;
&lt;li&gt;Dismiss a task by adding the tag &lt;code&gt;@someday🪨&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Autofocus Method suggests to cross the item off the list and re-enter it at the end of the list if I haven&amp;rsquo;t finished it yet. In Todoist, I duplicate the task and cross the original item off.&lt;/p&gt;
&lt;p&gt;Here is my setup to practice the Autofocus Method in Todoist. I&amp;rsquo;m still experimenting to see whether it can improve my productivity.&lt;/p&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a name="fnx:1"&gt;&lt;/a&gt; McKay, B., &amp;amp; McKay, K. (2022, September 20). &lt;em&gt;Autofocus: The Productivity System That Treats Your To-Do List Like a River&lt;/em&gt;. The Art of Manliness. &lt;a href="https://www.artofmanliness.com/character/behavior/autofocus-the-productivity-system-that-treats-your-to-do-list-like-a-river/"&gt;https://www.artofmanliness.com/character/behavior/autofocus-the-productivity-system-that-treats-your-to-do-list-like-a-river/&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description><category domain="https://blog.iany.me/post/">Posts</category><category domain="https://blog.iany.me/tags/productivity/">Productivity</category><category domain="https://blog.iany.me/tags/todoist/">Todoist</category></item></channel></rss>