Tomáš Janoušek, Blog2024-02-15T11:09:08+00:00https://work.lisk.in/Tomáš Janoušektomi@nomi.czSide by side git-range-diff2023-10-19T00:00:00+00:00https://work.lisk.in/2023/10/19/side-by-side-git-range-diff<p><a href="https://git-scm.com/docs/git-range-diff"><code class="language-plaintext highlighter-rouge">git range-diff</code></a> compares two commit ranges (two versions of
a branch, e.g. before and after a rebase). Its output is difficult to
comprehend, though. It’s a diff between diffs, presented in one dimension
using two columns of pluses/minuses/spaces. Wouldn’t it be better
if we used two dimensions and some nice colors?</p>
<details id="toc">
<summary>Table of Contents</summary>
<ul id="markdown-toc">
<li><a href="#motivationexample" id="markdown-toc-motivationexample">Motivation/Example</a></li>
<li><a href="#implementation" id="markdown-toc-implementation">Implementation</a></li>
<li><a href="#next-steps" id="markdown-toc-next-steps">Next steps</a></li>
</ul>
</details>
<h3 id="motivationexample">Motivation/Example</h3>
<p>To illustrate my point, let’s take a work-in-progress<sup id="fnref:at-the-time-of-writing" role="doc-noteref"><a href="#fn:at-the-time-of-writing" class="footnote" rel="footnote">1</a></sup>
<a href="https://github.com/xmonad/xmonad-contrib/pull/836">pull request from
xmonad-contrib</a>, <code class="language-plaintext highlighter-rouge">git
rebase</code> it to autosquash the fixup commits, and then use <a href="https://git-scm.com/docs/git-range-diff"><code class="language-plaintext highlighter-rouge">git
range-diff</code></a> to see what the rebase actually did:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>gh <span class="nb">pr </span>checkout 836
<span class="gp">$</span><span class="w"> </span>git checkout <span class="nt">-b</span> wx-partial-rebase
<span class="gp">$</span><span class="w"> </span>git rebase <span class="nt">-i</span> <span class="nt">--keep-base</span> <span class="nt">--autosquash</span> origin/master
<span class="gp">$</span><span class="w"> </span>git range-diff wx-partial...wx-partial-rebase
</code></pre></div></div>
<figure>
<figcaption>git range-diff</figcaption>
<!--
$ pipx install ansi2html
$ git range-diff --color=always wx-partial... | ansi2html >/tmp/rangediff1.html
-->
<pre class="ansi2html-content body_foreground body_background x-smaller"><code><!--
--><span class="ansi1 ansi31">1: 05a5291e </span><span class="ansi1 ansi37">!</span><span class="ansi1 ansi36"> 1: d5c45f98</span><span class="ansi1 ansi37"> X.Prelude: Add infinite stream type</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> XMonad/Prelude.hs: multimediaKeys = filter ((/= noSymbol) . snd) . map (id &&& s
<span class="ansi1 ansi36"> + type (Item (Stream a)) = a</span>
<span class="ansi1 ansi36"> +</span>
<span class="ansi1 ansi36"> + fromList :: [a] -> Stream a</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ fromList [] = errorWithoutStackTrace "TODO"</span>
<span class="ansi1 ansi36"> + fromList (x : xs) = x :~ fromList xs</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ fromList [] = errorWithoutStackTrace "XMonad.Prelude.Stream.fromList: Can't create stream out of finite list."</span>
<span class="ansi1 ansi36"> +</span>
<span class="ansi1 ansi36"> + toList :: Stream a -> [a]</span>
<span class="ansi1 ansi36"> + toList (x :~ xs) = x : toList xs</span>
<span class="ansi1 ansi37">2: 9dfe5594 = 2: a11625d9 X.L.Groups: Rewrite gen using infinite streams</span>
<span class="ansi1 ansi37">3: c8a08103 = 3: 06855306 Import X.Prelude unqualified if necessary</span>
<span class="ansi1 ansi37">4: 90f16434 = 4: 2de422fe Reduce head usage</span>
<span class="ansi1 ansi31">5: 00a457fd < -: -------- fixup! X.Prelude: Add infinite stream type</span>
</code></pre>
</figure>
<p>This is a simple example—neither the old (05a5291e) nor new (d5c45f98) commits
remove any lines, so the inner column is always <code class="language-plaintext highlighter-rouge">+</code>. Some of you can therefore
figure out that the difference is that the old commit adds a line with “TODO”
in it, while the new adds a line (somewhere else!) with a meaningful error
message instead.</p>
<p>Now let’s look at it side by side (in 2 dimensions):</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>git si-range-diff wx-partial...wx-partial-rebase
</code></pre></div></div>
<p>(note: si-range-diff is my alias; don’t try this at home—just yet)</p>
<figure>
<figcaption>git si-range-diff</figcaption>
<!--
# resize terminal to 108 columns
$ pipx install ansi2html
$ DELTA_PAGER=cat ttyrec -e 'git si-range-diff wx-partial...' /tmp/rangediff2
$ ttyrec2ansi </tmp/rangediff2 | ansi2html >/tmp/rangediff2.html
-->
<pre class="ansi2html-content body_foreground body_background x-smaller"><code><!--
--><span class="ansi38-39"></span><span class="ansi38-39 ansi48-235">────────────────────────────────────────────────────────────────────────────────────────────────────────────</span>
<span class="ansi1 ansi31">1: 05a5291e </span><span class="ansi1 ansi37">!</span><span class="ansi1 ansi36"> 1: d5c45f98</span><span class="ansi1 ansi37"> X.Prelude: Add infinite stream type</span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> XMonad/Prelude.hs: multimediaKeys = filter ((/= noSymbol) . snd) . map (id &&& s </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+ type (Item (Stream a)) = a</span> <span class="ansi38-245">│ </span><span class="ansi38-149">+ type (Item (Stream a)) = a</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+</span> <span class="ansi38-245">│ </span><span class="ansi38-149">+</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+ fromList :: [a] -> Stream a</span> <span class="ansi38-245">│ </span><span class="ansi38-149">+ fromList :: [a] -> Stream a</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ fromList [] = errorWithoutStackTrace "TODO"</span><span class="ansi48-52"> </span><span class="ansi38-245">│ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+ fromList (x : xs) = x :~ fromList xs</span> <span class="ansi38-245">│ </span><span class="ansi38-149">+ fromList (x : xs) = x :~ fromList xs</span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245">│ </span><span class="ansi38-149 ansi48-22">+ fromList [] = errorWithoutStackTrace "XMon</span><span class="ansi34 ansi48-22">↵</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245">│ </span><span class="ansi38-149 ansi48-22">ad.Prelude.Stream.fromList: Can't create stream out</span><span class="ansi34 ansi48-22">↵</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245">│ </span><span class="ansi38-149 ansi48-22"> of finite list."</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+</span> <span class="ansi38-245">│ </span><span class="ansi38-149">+</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+ toList :: Stream a -> [a]</span> <span class="ansi38-245">│ </span><span class="ansi38-149">+ toList :: Stream a -> [a]</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+ toList (x :~ xs) = x : toList xs</span> <span class="ansi38-245">│ </span><span class="ansi38-149">+ toList (x :~ xs) = x : toList xs</span>
<span class="ansi38-39"></span><span class="ansi38-39 ansi48-235">────────────────────────────────────────────────────────────────────────────────────────────────────────────</span>
<span class="ansi1 ansi37">2: 9dfe5594 = 2: a11625d9 X.L.Groups: Rewrite gen using infinite streams</span>
<span class="ansi38-39"></span><span class="ansi38-39 ansi48-235">────────────────────────────────────────────────────────────────────────────────────────────────────────────</span>
<span class="ansi1 ansi37">3: c8a08103 = 3: 06855306 Import X.Prelude unqualified if necessary</span>
<span class="ansi38-39"></span><span class="ansi38-39 ansi48-235">────────────────────────────────────────────────────────────────────────────────────────────────────────────</span>
<span class="ansi1 ansi37">4: 90f16434 = 4: 2de422fe Reduce head usage</span>
<span class="ansi38-39"></span><span class="ansi38-39 ansi48-235">────────────────────────────────────────────────────────────────────────────────────────────────────────────</span>
<span class="ansi1 ansi31">5: 00a457fd < -: -------- fixup! X.Prelude: Add infinite stream type</span>
</code></pre>
</figure>
<p>Much better. It’s easier to see what’s going on here. Left side is the old
diff, right side is the new one. Both are syntax-highlighted using foreground
(text) colours. The inter-diff diff is syntax-highlighted using background
colours—the removed line on the left side is dark red, the added one on the
right side is darkish green.</p>
<p>Here’s a longer example from the Linux kernel, which I believe requires
superhuman abilities to understand (made more difficult by the <code class="language-plaintext highlighter-rouge">##</code> and <code class="language-plaintext highlighter-rouge">@@</code>
lines appearing in the context of neighbouring diff hunks):</p>
<details>
<summary>Expand/Collapse</summary>
<figure>
<figcaption>git range-diff</figcaption>
<!--
$ pipx install ansi2html
$ git range-diff --color=always lorenzo-v2...lorenzo-v3 | ansi2html >/tmp/rangediff3.html
-->
<pre class="ansi2html-content body_foreground body_background xx-smaller"><code><!--
--><span class="ansi1 ansi31">1: 81f8e8008c9a </span><span class="ansi1 ansi37">!</span><span class="ansi1 ansi36"> 1: 05fce90455a2</span><span class="ansi1 ansi37"> mm: abstract the vma_merge()/split_vma() pattern for mprotect() et al.</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> Commit message
parameters, create inline wrapper functions for each of the modify
operations, parameterised only by what is required to perform the action.
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> We can also significantly simplify the logic - by returning the VMA if we</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> split (or merged VMA if we do not) we no longer need specific handling for</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> merge/split cases in any of the call sites.</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span>
Note that the userfaultfd_release() case works even though it does not
split VMAs - since start is set to vma->vm_start and end is set to
vma->vm_end, the split logic does not trigger.
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> Commit message
- vma->vm_start) >> PAGE_SHIFT, and start - vma->vm_start will be 0 in this
instance, this invocation will remain unchanged.
<span class="inv_background inv_foreground"></span><span class="ansi1 inv38-166 inv_foreground">-</span><span class="ansi2"> Signed-off-by: Lorenzo Stoakes <lstoakes@gmail.com></span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> We eliminate a VM_WARN_ON() in mprotect_fixup() as this simply asserts that</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> vma_merge() correctly ensures that flags remain the same, something that is</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> already checked in is_mergeable_vma() and elsewhere, and in any case is not</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> specific to mprotect().</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span>
Reviewed-by: Vlastimil Babka <vbabka@suse.cz>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv38-157 inv_foreground">+</span><span class="ansi1"> Signed-off-by: Lorenzo Stoakes <lstoakes@gmail.com></span>
## fs/userfaultfd.c ##
<span class="ansi1 ansi33"> @@ fs/userfaultfd.c: static int userfaultfd_release(struct inode *inode, struct file *file)</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> fs/userfaultfd.c: static int userfaultfd_release(struct inode *inode, struct fil
<span class="ansi1 ansi31"> - vma->vm_file, vma->vm_pgoff,</span>
<span class="ansi1 ansi31"> - vma_policy(vma),</span>
<span class="ansi1 ansi31"> - NULL_VM_UFFD_CTX, anon_vma_name(vma));</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ prev = vma_modify_flags_uffd(&vmi, prev, vma, vma->vm_start,</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ vma->vm_end, new_flags,</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ NULL_VM_UFFD_CTX);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi31">- if (prev) {</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi31">- vma = prev;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi31">- } else {</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi31">- prev = vma;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi31">- }</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ vma = vma_modify_flags_uffd(&vmi, prev, vma, vma->vm_start,</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ vma->vm_end, new_flags,</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ NULL_VM_UFFD_CTX);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> </span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> vma_start_write(vma);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> userfaultfd_set_vm_flags(vma, new_flags);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> vma->vm_userfaultfd_ctx = NULL_VM_UFFD_CTX;</span>
<span class="ansi1 ansi36"> +</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2"> if (prev) {</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2"> vma = prev;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2"> } else {</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ prev = vma;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> }</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> mmap_write_unlock(mm);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> mmput(mm);</span>
<span class="ansi1 ansi33"> @@ fs/userfaultfd.c: static int userfaultfd_register(struct userfaultfd_ctx *ctx,</span>
unsigned long start, end, vma_end;
struct vma_iterator vmi;
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> fs/userfaultfd.c: static int userfaultfd_register(struct userfaultfd_ctx *ctx,
<span class="ansi1 ansi31"> - /* vma_merge() invalidated the mas */</span>
<span class="ansi1 ansi31"> - vma = prev;</span>
<span class="ansi1 ansi31"> - goto next;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ prev = vma_modify_flags_uffd(&vmi, prev, vma, start, vma_end,</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ new_flags,</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ (struct vm_userfaultfd_ctx){ctx});</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ if (IS_ERR(prev)) {</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ ret = PTR_ERR(prev);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ vma = vma_modify_flags_uffd(&vmi, prev, vma, start, vma_end,</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ new_flags,</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ (struct vm_userfaultfd_ctx){ctx});</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ if (IS_ERR(vma)) {</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ ret = PTR_ERR(vma);</span>
<span class="ansi1 ansi36"> + break;</span>
}
<span class="ansi1 ansi31"> - if (vma->vm_start < start) {</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> fs/userfaultfd.c: static int userfaultfd_register(struct userfaultfd_ctx *ctx,
<span class="ansi1 ansi31"> - break;</span>
<span class="ansi1 ansi31"> - }</span>
<span class="ansi1 ansi31"> - next:</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ if (prev)</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ vma = prev; /* vma_merge() invalidated the mas */</span>
<span class="ansi1 ansi36"> +</span>
/*
* In the vma_merge() successful mprotect-like case 8:
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> fs/userfaultfd.c: static int userfaultfd_unregister(struct userfaultfd_ctx *ctx,
<span class="ansi1 ansi31"> - vma_policy(vma),</span>
<span class="ansi1 ansi31"> - NULL_VM_UFFD_CTX, anon_vma_name(vma));</span>
<span class="ansi1 ansi31"> - if (prev) {</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ prev = vma_modify_flags_uffd(&vmi, prev, vma, start, vma_end,</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ new_flags, NULL_VM_UFFD_CTX);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ if (IS_ERR(prev)) {</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi31">- vma = prev;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi31">- goto next;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ vma = vma_modify_flags_uffd(&vmi, prev, vma, start, vma_end,</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ new_flags, NULL_VM_UFFD_CTX);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ if (IS_ERR(vma)) {</span>
<span class="ansi1 ansi36"> + ret = PTR_ERR(prev);</span>
<span class="ansi1 ansi36"> + break;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ }</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ if (prev)</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2"> vma = prev;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi31">- goto next;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi31">- }</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> }</span>
<span class="ansi1 ansi31"> - if (vma->vm_start < start) {</span>
<span class="ansi1 ansi31"> - ret = split_vma(&vmi, vma, start, 1);</span>
<span class="ansi1 ansi31"> - if (ret)</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> fs/userfaultfd.c: static int userfaultfd_unregister(struct userfaultfd_ctx *ctx,
<span class="ansi1 ansi31"> - break;</span>
<span class="ansi1 ansi31"> - }</span>
<span class="ansi1 ansi31"> - next:</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+</span>
/*
* In the vma_merge() successful mprotect-like case 8:
* the next vma was merged into the current one and
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> mm/madvise.c: static int madvise_update_vma(struct vm_area_struct *vma,
struct mm_struct *mm = vma->vm_mm;
int error;
<span class="ansi1 ansi31"> - pgoff_t pgoff;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ struct vm_area_struct *merged;</span>
VMA_ITERATOR(vmi, mm, start);
if (new_flags == vma->vm_flags && anon_vma_name_eq(anon_vma_name(vma), anon_name)) {
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> mm/madvise.c: static int madvise_update_vma(struct vm_area_struct *vma,
<span class="ansi1 ansi31"> - vma = *prev;</span>
<span class="ansi1 ansi31"> - goto success;</span>
<span class="ansi1 ansi31"> - }</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ merged = vma_modify_flags_name(&vmi, *prev, vma, start, end, new_flags,</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ anon_name);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ if (IS_ERR(merged))</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ return PTR_ERR(merged);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ vma = vma_modify_flags_name(&vmi, *prev, vma, start, end, new_flags,</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ anon_name);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ if (IS_ERR(vma))</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ return PTR_ERR(vma);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi31">- *prev = vma;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ if (merged)</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ vma = *prev = merged;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ else</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ *prev = vma;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> *prev = vma;</span>
<span class="ansi1 ansi31"> - if (start != vma->vm_start) {</span>
<span class="ansi1 ansi31"> - error = split_vma(&vmi, vma, start, 1);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> mm/madvise.c: static int madvise_update_vma(struct vm_area_struct *vma,
## mm/mempolicy.c ##
<span class="ansi1 ansi33"> @@ mm/mempolicy.c: static int mbind_range(struct vma_iterator *vmi, struct vm_area_struct *vma,</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> struct vm_area_struct **prev, unsigned long start,</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> unsigned long end, struct mempolicy *new_pol)</span>
{
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2"> struct vm_area_struct *merged;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi31">- struct vm_area_struct *merged;</span>
unsigned long vmstart, vmend;
<span class="ansi1 ansi31"> - pgoff_t pgoff;</span>
<span class="ansi1 ansi31"> - int err;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> mm/mempolicy.c: static int mbind_range(struct vma_iterator *vmi, struct vm_area_
<span class="ansi1 ansi31"> - merged = vma_merge(vmi, vma->vm_mm, *prev, vmstart, vmend, vma->vm_flags,</span>
<span class="ansi1 ansi31"> - vma->anon_vma, vma->vm_file, pgoff, new_pol,</span>
<span class="ansi1 ansi31"> - vma->vm_userfaultfd_ctx, anon_vma_name(vma));</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ merged = vma_modify_policy(vmi, *prev, vma, vmstart, vmend, new_pol);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ if (IS_ERR(merged))</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ return PTR_ERR(merged);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2"> if (merged) {</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2"> *prev = merged;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2"> return vma_replace_policy(merged, new_pol);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2"> }</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2"> </span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi31">- if (merged) {</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi31">- *prev = merged;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi31">- return vma_replace_policy(merged, new_pol);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi31">- }</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi31">-</span>
<span class="ansi1 ansi31"> - if (vma->vm_start != vmstart) {</span>
<span class="ansi1 ansi31"> - err = split_vma(vmi, vma, vmstart, 1);</span>
<span class="ansi1 ansi31"> - if (err)</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> mm/mempolicy.c: static int mbind_range(struct vma_iterator *vmi, struct vm_area_
<span class="ansi1 ansi31"> - if (err)</span>
<span class="ansi1 ansi31"> - return err;</span>
<span class="ansi1 ansi31"> - }</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi31">-</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ vma = vma_modify_policy(vmi, *prev, vma, vmstart, vmend, new_pol);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ if (IS_ERR(vma))</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ return PTR_ERR(vma);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> </span>
*prev = vma;
return vma_replace_policy(vma, new_pol);
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2"> }</span>
## mm/mlock.c ##
<span class="ansi1 ansi33"> @@ mm/mlock.c: static int mlock_fixup(struct vma_iterator *vmi, struct vm_area_struct *vma,</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> mm/mlock.c: static int mlock_fixup(struct vma_iterator *vmi, struct vm_area_stru
int nr_pages;
int ret = 0;
vm_flags_t oldflags = vma->vm_flags;
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ struct vm_area_struct *merged;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2"> </span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2"> if (newflags == oldflags || (oldflags & VM_SPECIAL) ||</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2"> is_vm_hugetlb_page(vma) || vma == get_gate_vma(current->mm) ||</span>
<span class="ansi1 ansi33"> @@ mm/mlock.c: static int mlock_fixup(struct vma_iterator *vmi, struct vm_area_struct *vma,</span>
/* don't set VM_LOCKED or VM_LOCKONFAULT and don't count */
goto out;
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> mm/mlock.c: static int mlock_fixup(struct vma_iterator *vmi, struct vm_area_stru
<span class="ansi1 ansi31"> - if (*prev) {</span>
<span class="ansi1 ansi31"> - vma = *prev;</span>
<span class="ansi1 ansi31"> - goto success;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ merged = vma_modify_flags(vmi, *prev, vma, start, end, newflags);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ if (IS_ERR(merged)) {</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ ret = PTR_ERR(merged);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ vma = vma_modify_flags(vmi, *prev, vma, start, end, newflags);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ if (IS_ERR(vma)) {</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ ret = PTR_ERR(vma);</span>
<span class="ansi1 ansi36"> + goto out;</span>
}
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> mm/mlock.c: static int mlock_fixup(struct vma_iterator *vmi, struct vm_area_stru
<span class="ansi1 ansi31"> - if (ret)</span>
<span class="ansi1 ansi31"> - goto out;</span>
<span class="ansi1 ansi31"> - }</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ if (merged)</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ vma = *prev = merged;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2"> </span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi31">-</span>
<span class="ansi1 ansi31"> - if (end != vma->vm_end) {</span>
<span class="ansi1 ansi31"> - ret = split_vma(vmi, vma, end, 0);</span>
<span class="ansi1 ansi31"> - if (ret)</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> mm/mmap.c: int split_vma(struct vma_iterator *vmi, struct vm_area_struct *vma,
<span class="ansi1 ansi36"> + *</span>
<span class="ansi1 ansi36"> + * If no merge is possible and the range does not span the entirety of the VMA,</span>
<span class="ansi1 ansi36"> + * we then need to split the VMA to accommodate the change.</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ *</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ * The function returns either the merged VMA, the original VMA if a split was</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ * required instead, or an error if the split failed.</span>
<span class="ansi1 ansi36"> + */</span>
<span class="ansi1 ansi36"> +struct vm_area_struct *vma_modify(struct vma_iterator *vmi,</span>
<span class="ansi1 ansi36"> + struct vm_area_struct *prev,</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> mm/mmap.c: int split_vma(struct vma_iterator *vmi, struct vm_area_struct *vma,
<span class="ansi1 ansi36"> + return ERR_PTR(err);</span>
<span class="ansi1 ansi36"> + }</span>
<span class="ansi1 ansi36"> +</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ return NULL;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ return vma;</span>
<span class="ansi1 ansi36"> +}</span>
<span class="ansi1 ansi36"> +</span>
/*
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> mm/mprotect.c: mprotect_fixup(struct vma_iterator *vmi, struct mmu_gather *tlb,
unsigned int mm_cp_flags = 0;
unsigned long charged = 0;
<span class="ansi1 ansi31"> - pgoff_t pgoff;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ struct vm_area_struct *merged;</span>
int error;
if (newflags == oldflags) {
<span class="inv_background inv_foreground"></span><span class="ansi1 inv33 inv_foreground">@@</span> mm/mprotect.c: mprotect_fixup(struct vma_iterator *vmi, struct mmu_gather *tlb,
<span class="ansi1 ansi31"> - vma->vm_userfaultfd_ctx, anon_vma_name(vma));</span>
<span class="ansi1 ansi31"> - if (*pprev) {</span>
<span class="ansi1 ansi31"> - vma = *pprev;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ merged = vma_modify_flags(vmi, *pprev, vma, start, end, newflags);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ if (IS_ERR(merged)) {</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ error = PTR_ERR(merged);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ goto fail;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ }</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ if (merged) {</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ vma = *pprev = merged;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2"> VM_WARN_ON((vma->vm_flags ^ newflags) & ~VM_SOFTDIRTY);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi31">- VM_WARN_ON((vma->vm_flags ^ newflags) & ~VM_SOFTDIRTY);</span>
<span class="ansi1 ansi31"> - goto success;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ } else {</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi32">+ *pprev = vma;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ vma = vma_modify_flags(vmi, *pprev, vma, start, end, newflags);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ if (IS_ERR(vma)) {</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ error = PTR_ERR(vma);</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1 ansi32">+ goto fail;</span>
}
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi31">- *pprev = vma;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv31 inv_foreground">-</span><span class="ansi2 ansi31">-</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> *pprev = vma;</span>
<span class="inv_background inv_foreground"></span><span class="ansi1 inv36 inv_foreground">+</span><span class="ansi1"> </span>
<span class="ansi1 ansi31"> - if (start != vma->vm_start) {</span>
<span class="ansi1 ansi31"> - error = split_vma(vmi, vma, start, 1);</span>
<span class="ansi1 ansi31"> - if (error)</span>
</code></pre>
</figure>
<figure>
<figcaption>git si-range-diff</figcaption>
<!--
# resize terminal to 169 columns
$ pipx install ansi2html
$ DELTA_PAGER=cat ttyrec -e 'git si-range-diff lorenzo-v2...lorenzo-v3' /tmp/rangediff4
$ ttyrec2ansi </tmp/rangediff4 | ansi2html >/tmp/rangediff4.html
-->
<pre class="ansi2html-content body_foreground body_background xx-smaller"><code><!--
--><span class="ansi38-39"></span><span class="ansi38-39 ansi48-235">─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────</span>
<span class="ansi1 ansi31">1: 81f8e8008c9a </span><span class="ansi1 ansi37">!</span><span class="ansi1 ansi36"> 1: 05fce90455a2</span><span class="ansi1 ansi37"> mm: abstract the vma_merge()/split_vma() pattern for mprotect() et al.</span>
<span class="ansi38-245 ansi48-235">────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> Commit message </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> parameters, create inline wrapper functions for each of the modify</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> parameters, create inline wrapper functions for each of the modify</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> operations, parameterised only by what is required to perform the action.</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> operations, parameterised only by what is required to perform the action.</span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-22"> We can also significantly simplify the logic - by returning the VMA if we</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-22"> split (or merged VMA if we do not) we no longer need specific handling for</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-22"> merge/split cases in any of the call sites.</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> Note that the userfaultfd_release() case works even though it does not</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> Note that the userfaultfd_release() case works even though it does not</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> split VMAs - since start is set to vma->vm_start and end is set to</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> split VMAs - since start is set to vma->vm_start and end is set to</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> vma->vm_end, the split logic does not trigger.</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> vma->vm_end, the split logic does not trigger.</span>
<span class="ansi38-245 ansi48-235">────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> Commit message </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> - vma->vm_start) >> PAGE_SHIFT, and start - vma->vm_start will be 0 in this</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> - vma->vm_start) >> PAGE_SHIFT, and start - vma->vm_start will be 0 in this</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> instance, this invocation will remain unchanged.</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> instance, this invocation will remain unchanged.</span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231 ansi48-52"> Signed-off-by: Lorenzo Stoakes <lstoakes@gmail.com></span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-22"> We eliminate a VM_WARN_ON() in mprotect_fixup() as this simply asserts that</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-22"> vma_merge() correctly ensures that flags remain the same, something that is</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-22"> already checked in is_mergeable_vma() and elsewhere, and in any case is not</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-22"> specific to mprotect().</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> Reviewed-by: Vlastimil Babka <vbabka@suse.cz></span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> Reviewed-by: Vlastimil Babka <vbabka@suse.cz></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-22"> Signed-off-by: Lorenzo Stoakes <lstoakes@gmail.com></span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> ## fs/userfaultfd.c ##</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> ## fs/userfaultfd.c ##</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231">@@ fs/userfaultfd.c: static int userfaultfd_release(struct inode *inode, struct f</span><span class="ansi34">↴</span> <span class="ansi38-245"> │ </span><span class="ansi38-231">@@ fs/userfaultfd.c: static int userfaultfd_release(struct inode *inode, struct f</span><span class="ansi34">↴</span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi34">…</span><span class="ansi38-231">ile *file)</span> <span class="ansi38-245"> │ </span> <span class="ansi34">…</span><span class="ansi38-231">ile *file)</span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> fs/userfaultfd.c: static int userfaultfd_release(struct inode *inode, struct fil </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- vma->vm_file, vma->vm_pgoff,</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- vma->vm_file, vma->vm_pgoff,</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- vma_policy(vma),</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- vma_policy(vma),</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- NULL_VM_UFFD_CTX, anon_vma_name(vma));</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- NULL_VM_UFFD_CTX, anon_vma_name(vma));</span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-203 ansi48-22">- if (prev) {</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-203 ansi48-22">- vma = prev;</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-203 ansi48-22">- } else {</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-203 ansi48-22">- prev = vma;</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-203 ansi48-22">- }</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ </span><span class="ansi38-149 ansi48-124">prev</span><span class="ansi38-149 ansi48-52"> = vma_modify_flags_uffd(&vmi, prev, vma, vma->vm_start,</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ </span><span class="ansi38-149 ansi48-28">vma</span><span class="ansi38-149 ansi48-22"> = vma_modify_flags_uffd(&vmi, prev, vma, vma->vm_start,</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ </span><span class="ansi38-149 ansi48-124"> </span><span class="ansi38-149 ansi48-52">vma->vm_end, new_flags,</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ vma->vm_end, new_flags,</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ </span><span class="ansi38-149 ansi48-124"> </span><span class="ansi38-149 ansi48-52">NULL_VM_UFFD_CTX);</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ NULL_VM_UFFD_CTX);</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="inv35 inv_foreground"> </span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-22"> vma_start_write(vma);</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-22"> userfaultfd_set_vm_flags(vma, new_flags);</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-22"> vma->vm_userfaultfd_ctx = NULL_VM_UFFD_CTX;</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+</span> <span class="ansi38-245"> │ </span><span class="ansi38-149">+</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231 ansi48-52"> if (prev) {</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231 ansi48-52"> vma = prev;</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231 ansi48-52"> } else {</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ prev = vma;</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-22"> }</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-22"> mmap_write_unlock(mm);</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-22"> mmput(mm);</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231">@@ fs/userfaultfd.c: static int userfaultfd_register(struct userfaultfd_ctx *ctx,</span> <span class="ansi38-245"> │ </span><span class="ansi38-231">@@ fs/userfaultfd.c: static int userfaultfd_register(struct userfaultfd_ctx *ctx,</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> unsigned long start, end, vma_end;</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> unsigned long start, end, vma_end;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> struct vma_iterator vmi;</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> struct vma_iterator vmi;</span>
<span class="ansi38-245 ansi48-235">────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> fs/userfaultfd.c: static int userfaultfd_register(struct userfaultfd_ctx *ctx, </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- /* vma_merge() invalidated the mas */</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- /* vma_merge() invalidated the mas */</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- vma = prev;</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- vma = prev;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- goto next;</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- goto next;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ </span><span class="ansi38-149 ansi48-124">prev</span><span class="ansi38-149 ansi48-52"> = vma_modify_flags_uffd(&vmi, prev, vma, start, vma_end,</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ </span><span class="ansi38-149 ansi48-28">vma</span><span class="ansi38-149 ansi48-22"> = vma_modify_flags_uffd(&vmi, prev, vma, start, vma_end,</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ </span><span class="ansi38-149 ansi48-124"> </span><span class="ansi38-149 ansi48-52">new_flags,</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ new_flags,</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ </span><span class="ansi38-149 ansi48-124"> </span><span class="ansi38-149 ansi48-52">(struct vm_userfaultfd_ctx){ctx});</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ (struct vm_userfaultfd_ctx){ctx});</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ if (IS_ERR(</span><span class="ansi38-149 ansi48-124">prev</span><span class="ansi38-149 ansi48-52">)) {</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ if (IS_ERR(</span><span class="ansi38-149 ansi48-28">vma</span><span class="ansi38-149 ansi48-22">)) {</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ ret = PTR_ERR(</span><span class="ansi38-149 ansi48-124">prev</span><span class="ansi38-149 ansi48-52">);</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ ret = PTR_ERR(</span><span class="ansi38-149 ansi48-28">vma</span><span class="ansi38-149 ansi48-22">);</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+ break;</span> <span class="ansi38-245"> │ </span><span class="ansi38-149">+ break;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> }</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> }</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- if (vma->vm_start < start) {</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- if (vma->vm_start < start) {</span>
<span class="ansi38-245 ansi48-235">────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> fs/userfaultfd.c: static int userfaultfd_register(struct userfaultfd_ctx *ctx, </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- break;</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- break;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- }</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- }</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- next:</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- next:</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ if (prev)</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ vma = prev; /* vma_merge() invalidated the mas */</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+</span> <span class="ansi38-245"> │ </span><span class="ansi38-149">+</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> /*</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> /*</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> * In the vma_merge() successful mprotect-like case 8:</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> * In the vma_merge() successful mprotect-like case 8:</span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> fs/userfaultfd.c: static int userfaultfd_unregister(struct userfaultfd_ctx *ctx, </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- vma_policy(vma),</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- vma_policy(vma),</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- NULL_VM_UFFD_CTX, anon_vma_name(vma));</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- NULL_VM_UFFD_CTX, anon_vma_name(vma));</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- if (prev) {</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- if (prev) {</span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-203 ansi48-22">- vma = prev;</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-203 ansi48-22">- goto next;</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ </span><span class="ansi38-149 ansi48-124">prev</span><span class="ansi38-149 ansi48-52"> = vma_modify_flags_uffd(&vmi, prev, vma, start, vma_end,</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ </span><span class="ansi38-149 ansi48-28">vma</span><span class="ansi38-149 ansi48-22"> = vma_modify_flags_uffd(&vmi, prev, vma, start, vma_end,</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ </span><span class="ansi38-149 ansi48-124"> </span><span class="ansi38-149 ansi48-52">new_flags, NULL_VM_UFFD_CTX);</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ new_flags, NULL_VM_UFFD_CTX);</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ if (IS_ERR(</span><span class="ansi38-149 ansi48-124">prev</span><span class="ansi38-149 ansi48-52">)) {</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ if (IS_ERR(</span><span class="ansi38-149 ansi48-28">vma</span><span class="ansi38-149 ansi48-22">)) {</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+ ret = PTR_ERR(prev);</span> <span class="ansi38-245"> │ </span><span class="ansi38-149">+ ret = PTR_ERR(prev);</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+ break;</span> <span class="ansi38-245"> │ </span><span class="ansi38-149">+ break;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-124">+</span><span class="ansi38-149 ansi48-52"> }</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-28"> </span><span class="ansi38-231 ansi48-22"> }</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ if (prev)</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231 ansi48-52"> vma = prev;</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203 ansi48-52">- goto next;</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203 ansi48-52">- }</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- if (vma->vm_start < start) {</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- if (vma->vm_start < start) {</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- ret = split_vma(&vmi, vma, start, 1);</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- ret = split_vma(&vmi, vma, start, 1);</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- if (ret)</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- if (ret)</span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> fs/userfaultfd.c: static int userfaultfd_unregister(struct userfaultfd_ctx *ctx, </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- break;</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- break;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- }</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- }</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- next:</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- next:</span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> /*</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> /*</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> * In the vma_merge() successful mprotect-like case 8:</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> * In the vma_merge() successful mprotect-like case 8:</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> * the next vma was merged into the current one and</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> * the next vma was merged into the current one and</span>
<span class="ansi38-245 ansi48-235">─────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> mm/madvise.c: static int madvise_update_vma(struct vm_area_struct *vma, </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">─────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> struct mm_struct *mm = vma->vm_mm;</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> struct mm_struct *mm = vma->vm_mm;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> int error;</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> int error;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- pgoff_t pgoff;</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- pgoff_t pgoff;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ struct vm_area_struct *merged;</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> VMA_ITERATOR(vmi, mm, start);</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> VMA_ITERATOR(vmi, mm, start);</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> </span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> if (new_flags == vma->vm_flags && anon_vma_name_eq(anon_vma_name(vma), anon_</span><span class="ansi34">↴</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> if (new_flags == vma->vm_flags && anon_vma_name_eq(anon_vma_name(vma), anon_</span><span class="ansi34">↴</span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi34">…</span><span class="ansi38-231">name)) {</span> <span class="ansi38-245"> │ </span> <span class="ansi34">…</span><span class="ansi38-231">name)) {</span>
<span class="ansi38-245 ansi48-235">─────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> mm/madvise.c: static int madvise_update_vma(struct vm_area_struct *vma, </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">─────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- vma = *prev;</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- vma = *prev;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- goto success;</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- goto success;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- }</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- }</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ </span><span class="ansi38-149 ansi48-124">merged</span><span class="ansi38-149 ansi48-52"> = vma_modify_flags_name(&vmi, *prev, vma, start, end, new_flags,</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ </span><span class="ansi38-149 ansi48-28">vma</span><span class="ansi38-149 ansi48-22"> = vma_modify_flags_name(&vmi, *prev, vma, start, end, new_flags,</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ </span><span class="ansi38-149 ansi48-124"> </span><span class="ansi38-149 ansi48-52">anon_name);</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ anon_name);</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ if (IS_ERR(</span><span class="ansi38-149 ansi48-124">merged</span><span class="ansi38-149 ansi48-52">))</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ if (IS_ERR(</span><span class="ansi38-149 ansi48-28">vma</span><span class="ansi38-149 ansi48-22">))</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ return PTR_ERR(</span><span class="ansi38-149 ansi48-124">merged</span><span class="ansi38-149 ansi48-52">);</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ return PTR_ERR(</span><span class="ansi38-149 ansi48-28">vma</span><span class="ansi38-149 ansi48-22">);</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> </span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203 ansi48-124">-</span><span class="ansi38-203 ansi48-52"> *prev = vma;</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-28"> </span><span class="ansi38-231 ansi48-22"> *prev = vma;</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ if (merged)</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ vma = *prev = merged;</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ else</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ *prev = vma;</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> </span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- if (start != vma->vm_start) {</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- if (start != vma->vm_start) {</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- error = split_vma(&vmi, vma, start, 1);</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- error = split_vma(&vmi, vma, start, 1);</span>
<span class="ansi38-245 ansi48-235">─────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> mm/madvise.c: static int madvise_update_vma(struct vm_area_struct *vma, </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">─────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> ## mm/mempolicy.c ##</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> ## mm/mempolicy.c ##</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231">@@ mm/mempolicy.c: static int mbind_range(struct vma_iterator *vmi, struct vm_are</span><span class="ansi34">↴</span> <span class="ansi38-245"> │ </span><span class="ansi38-231">@@ mm/mempolicy.c: static int mbind_range(struct vma_iterator *vmi, struct vm_are</span><span class="ansi34">↴</span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi34">…</span><span class="ansi38-231">a_struct *vma,</span> <span class="ansi38-245"> │ </span> <span class="ansi34">…</span><span class="ansi38-231">a_struct *vma,</span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-22"> struct vm_area_struct **prev, unsigned long start,</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-22"> unsigned long end, struct mempolicy *new_pol)</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> {</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> {</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231 ansi48-124"> </span><span class="ansi38-231 ansi48-52"> struct vm_area_struct *merged;</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-203 ansi48-28">-</span><span class="ansi38-203 ansi48-22"> struct vm_area_struct *merged;</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> unsigned long vmstart, vmend;</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> unsigned long vmstart, vmend;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- pgoff_t pgoff;</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- pgoff_t pgoff;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- int err;</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- int err;</span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> mm/mempolicy.c: static int mbind_range(struct vma_iterator *vmi, struct vm_area_ </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- merged = vma_merge(vmi, vma->vm_mm, *prev, vmstart, vmend, vma->vm_flags,</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- merged = vma_merge(vmi, vma->vm_mm, *prev, vmstart, vmend, vma->vm_flags,</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- vma->anon_vma, vma->vm_file, pgoff, new_pol,</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- vma->anon_vma, vma->vm_file, pgoff, new_pol,</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- vma->vm_userfaultfd_ctx, anon_vma_name(vma));</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- vma->vm_userfaultfd_ctx, anon_vma_name(vma));</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ merged = vma_modify_policy(vmi, *prev, vma, vmstart, vmend, new_pol);</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-124">+</span><span class="ansi38-149 ansi48-52"> if (</span><span class="ansi38-149 ansi48-124">IS_ERR(</span><span class="ansi38-149 ansi48-52">merged)</span><span class="ansi38-149 ansi48-124">)</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-203 ansi48-28">-</span><span class="ansi38-203 ansi48-22"> if (merged)</span><span class="ansi38-203 ansi48-28"> {</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-203 ansi48-22">- *prev = merged;</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-124">+</span><span class="ansi38-149 ansi48-52"> return </span><span class="ansi38-149 ansi48-124">PTR_ERR</span><span class="ansi38-149 ansi48-52">(merged);</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-203 ansi48-28">-</span><span class="ansi38-203 ansi48-22"> return</span><span class="ansi38-203 ansi48-28"> vma_replace_policy</span><span class="ansi38-203 ansi48-22">(merged</span><span class="ansi38-203 ansi48-28">, new_pol</span><span class="ansi38-203 ansi48-22">);</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231 ansi48-52"> if (merged) {</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231 ansi48-52"> *prev = merged;</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231 ansi48-52"> return vma_replace_policy(merged, new_pol);</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231 ansi48-124"> </span><span class="ansi38-231 ansi48-52"> }</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-203 ansi48-28">-</span><span class="ansi38-203 ansi48-22"> }</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231 ansi48-52"> </span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-203 ansi48-22">-</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- if (vma->vm_start != vmstart) {</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- if (vma->vm_start != vmstart) {</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- err = split_vma(vmi, vma, vmstart, 1);</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- err = split_vma(vmi, vma, vmstart, 1);</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- if (err)</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- if (err)</span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> mm/mempolicy.c: static int mbind_range(struct vma_iterator *vmi, struct vm_area_ </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- if (err)</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- if (err)</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- return err;</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- return err;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- }</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- }</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203 ansi48-52">-</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ vma = vma_modify_policy(vmi, *prev, vma, vmstart, vmend, new_pol);</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ if (IS_ERR(vma))</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ return PTR_ERR(vma);</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="inv35 inv_foreground"> </span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> *prev = vma;</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> *prev = vma;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> return vma_replace_policy(vma, new_pol);</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> return vma_replace_policy(vma, new_pol);</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231 ansi48-52"> }</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> ## mm/mlock.c ##</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> ## mm/mlock.c ##</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231">@@ mm/mlock.c: static int mlock_fixup(struct vma_iterator *vmi, struct vm_area_st</span><span class="ansi34">↴</span> <span class="ansi38-245"> │ </span><span class="ansi38-231">@@ mm/mlock.c: static int mlock_fixup(struct vma_iterator *vmi, struct vm_area_st</span><span class="ansi34">↴</span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi34">…</span><span class="ansi38-231">ruct *vma,</span> <span class="ansi38-245"> │ </span> <span class="ansi34">…</span><span class="ansi38-231">ruct *vma,</span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> mm/mlock.c: static int mlock_fixup(struct vma_iterator *vmi, struct vm_area_stru </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> int nr_pages;</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> int nr_pages;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> int ret = 0;</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> int ret = 0;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> vm_flags_t oldflags = vma->vm_flags;</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> vm_flags_t oldflags = vma->vm_flags;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ struct vm_area_struct *merged;</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231 ansi48-52"> </span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231 ansi48-52"> if (newflags == oldflags || (oldflags & VM_SPECIAL) ||</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231 ansi48-52"> is_vm_hugetlb_page(vma) || vma == get_gate_vma(current->mm) ||</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231">@@ mm/mlock.c: static int mlock_fixup(struct vma_iterator *vmi, struct vm_area_st</span><span class="ansi34">↴</span> <span class="ansi38-245"> │ </span><span class="ansi38-231">@@ mm/mlock.c: static int mlock_fixup(struct vma_iterator *vmi, struct vm_area_st</span><span class="ansi34">↴</span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi34">…</span><span class="ansi38-231">ruct *vma,</span> <span class="ansi38-245"> │ </span> <span class="ansi34">…</span><span class="ansi38-231">ruct *vma,</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> /* don't set VM_LOCKED or VM_LOCKONFAULT and don't count */</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> /* don't set VM_LOCKED or VM_LOCKONFAULT and don't count */</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> goto out;</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> goto out;</span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> mm/mlock.c: static int mlock_fixup(struct vma_iterator *vmi, struct vm_area_stru </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- if (*prev) {</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- if (*prev) {</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- vma = *prev;</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- vma = *prev;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- goto success;</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- goto success;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ </span><span class="ansi38-149 ansi48-124">merged</span><span class="ansi38-149 ansi48-52"> = vma_modify_flags(vmi, *prev, vma, start, end, newflags);</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ </span><span class="ansi38-149 ansi48-28">vma</span><span class="ansi38-149 ansi48-22"> = vma_modify_flags(vmi, *prev, vma, start, end, newflags);</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ if (IS_ERR(</span><span class="ansi38-149 ansi48-124">merged</span><span class="ansi38-149 ansi48-52">)) {</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ if (IS_ERR(</span><span class="ansi38-149 ansi48-28">vma</span><span class="ansi38-149 ansi48-22">)) {</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ ret = PTR_ERR(</span><span class="ansi38-149 ansi48-124">merged</span><span class="ansi38-149 ansi48-52">);</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ ret = PTR_ERR(</span><span class="ansi38-149 ansi48-28">vma</span><span class="ansi38-149 ansi48-22">);</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+ goto out;</span> <span class="ansi38-245"> │ </span><span class="ansi38-149">+ goto out;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> }</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> }</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> </span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> </span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> mm/mlock.c: static int mlock_fixup(struct vma_iterator *vmi, struct vm_area_stru </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">──────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- if (ret)</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- if (ret)</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- goto out;</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- goto out;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- }</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- }</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ if (merged)</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ vma = *prev = merged;</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231 ansi48-52"> </span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-203 ansi48-22">-</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- if (end != vma->vm_end) {</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- if (end != vma->vm_end) {</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- ret = split_vma(vmi, vma, end, 0);</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- ret = split_vma(vmi, vma, end, 0);</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- if (ret)</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- if (ret)</span>
<span class="ansi38-245 ansi48-235">────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> mm/mmap.c: int split_vma(struct vma_iterator *vmi, struct vm_area_struct *vma, </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+ *</span> <span class="ansi38-245"> │ </span><span class="ansi38-149">+ *</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+ * If no merge is possible and the range does not span the entirety of the VMA,</span> <span class="ansi38-245"> │ </span><span class="ansi38-149">+ * If no merge is possible and the range does not span the entirety of the VMA,</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+ * we then need to split the VMA to accommodate the change.</span> <span class="ansi38-245"> │ </span><span class="ansi38-149">+ * we then need to split the VMA to accommodate the change.</span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ *</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ * The function returns either the merged VMA, the original VMA if a split was</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ * required instead, or an error if the split failed.</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+ */</span> <span class="ansi38-245"> │ </span><span class="ansi38-149">+ */</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+struct vm_area_struct *vma_modify(struct vma_iterator *vmi,</span> <span class="ansi38-245"> │ </span><span class="ansi38-149">+struct vm_area_struct *vma_modify(struct vma_iterator *vmi,</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+ struct vm_area_struct *prev,</span> <span class="ansi38-245"> │ </span><span class="ansi38-149">+ struct vm_area_struct *prev,</span>
<span class="ansi38-245 ansi48-235">────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> mm/mmap.c: int split_vma(struct vma_iterator *vmi, struct vm_area_struct *vma, </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+ return ERR_PTR(err);</span> <span class="ansi38-245"> │ </span><span class="ansi38-149">+ return ERR_PTR(err);</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+ }</span> <span class="ansi38-245"> │ </span><span class="ansi38-149">+ }</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+</span> <span class="ansi38-245"> │ </span><span class="ansi38-149">+</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ return </span><span class="ansi38-149 ansi48-124">NULL</span><span class="ansi38-149 ansi48-52">;</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ return </span><span class="ansi38-149 ansi48-28">vma</span><span class="ansi38-149 ansi48-22">;</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+}</span> <span class="ansi38-245"> │ </span><span class="ansi38-149">+}</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149">+</span> <span class="ansi38-245"> │ </span><span class="ansi38-149">+</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> /*</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> /*</span>
<span class="ansi38-245 ansi48-235">─────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> mm/mprotect.c: mprotect_fixup(struct vma_iterator *vmi, struct mmu_gather *tlb, </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">─────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> unsigned int mm_cp_flags = 0;</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> unsigned int mm_cp_flags = 0;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> unsigned long charged = 0;</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> unsigned long charged = 0;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- pgoff_t pgoff;</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- pgoff_t pgoff;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ struct vm_area_struct *merged;</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> int error;</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> int error;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> </span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> if (newflags == oldflags) {</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> if (newflags == oldflags) {</span>
<span class="ansi38-245 ansi48-235">─────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┐</span>
<span class="ansi38-231"> mm/mprotect.c: mprotect_fixup(struct vma_iterator *vmi, struct mmu_gather *tlb, </span><span class="ansi38-245 ansi48-235">│</span>
<span class="ansi38-245 ansi48-235">─────────────────────────────────────────────────────────────────────────────────</span><span class="ansi38-245 ansi48-235">┘</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- vma->vm_userfaultfd_ctx, anon_vma_name(vma));</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- vma->vm_userfaultfd_ctx, anon_vma_name(vma));</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- if (*pprev) {</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- if (*pprev) {</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- vma = *pprev;</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- vma = *pprev;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ merged = vma_modify_flags(vmi, *pprev, vma, start, end, newflags);</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ if (IS_ERR(merged)) {</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ error = PTR_ERR(merged);</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ goto fail;</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ }</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ if (merged) {</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ vma = *pprev = merged;</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231 ansi48-124"> </span><span class="ansi38-231 ansi48-52"> VM_WARN_ON((vma->vm_flags ^ newflags) & ~VM_SOFTDIRTY);</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-203 ansi48-28">-</span><span class="ansi38-203 ansi48-22"> VM_WARN_ON((vma->vm_flags ^ newflags) & ~VM_SOFTDIRTY);</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- goto success;</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- goto success;</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ } else {</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-149 ansi48-52">+ *pprev = vma;</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ vma = vma_modify_flags(vmi, *pprev, vma, start, end, newflags);</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ if (IS_ERR(vma)) {</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ error = PTR_ERR(vma);</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="ansi38-149 ansi48-22">+ goto fail;</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> }</span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> }</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-231"> </span> <span class="ansi38-245"> │ </span><span class="ansi38-231"> </span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203 ansi48-124">-</span><span class="ansi38-203 ansi48-52"> *pprev = vma;</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span><span class="ansi38-231 ansi48-28"> </span><span class="ansi38-231 ansi48-22"> *pprev = vma;</span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203 ansi48-52">-</span><span class="ansi48-52"> </span><span class="ansi38-245"> │ </span>
<span class="ansi1 ansi38-245 ansi48-233"></span> <span class="ansi38-245"> │ </span><span class="inv35 inv_foreground"> </span><span class="ansi48-22"></span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- if (start != vma->vm_start) {</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- if (start != vma->vm_start) {</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- error = split_vma(vmi, vma, start, 1);</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- error = split_vma(vmi, vma, start, 1);</span>
<span class="ansi1 ansi38-245 ansi48-233"></span><span class="ansi38-203">- if (error)</span> <span class="ansi38-245"> │ </span><span class="ansi38-203">- if (error)</span>
</code></pre>
</figure>
</details>
<h3 id="implementation">Implementation</h3>
<p>Ideally, this side-by-side view of <a href="https://git-scm.com/docs/git-range-diff"><code class="language-plaintext highlighter-rouge">git range-diff</code></a> would
just appear after I configure <code class="language-plaintext highlighter-rouge">pager.range-diff='delta -s'</code> in
<a href="https://git-scm.com/docs/git-config"><code class="language-plaintext highlighter-rouge">.gitconfig</code></a>. Unfortunately, it’s not that easy:</p>
<ul>
<li><a href="https://github.com/dandavison/delta">delta</a> can’t parse the output of <a href="https://git-scm.com/docs/git-range-diff"><code class="language-plaintext highlighter-rouge">git range-diff</code></a></li>
<li>that output <a href="https://git-scm.com/docs/git-range-diff#_output_stability">isn’t even meant to be
machine-readable</a></li>
</ul>
<p>Nevertheless, I hacked together a proof-of-concept preprocessor that takes the
output of <a href="https://git-scm.com/docs/git-range-diff"><code class="language-plaintext highlighter-rouge">git range-diff</code></a> and turns it into something that
<a href="https://github.com/dandavison/delta">delta</a> parses and presents nicely to a human:</p>
<figure>
<figcaption><a href="https://github.com/liskin/dotfiles/blob/b13cc7da57c223a6d2e00acd99234731efaa62fe/bin/git-range-diff-delta-preproc">git-range-diff-delta-preproc</a></figcaption>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>
<span class="c"># preprocessor for "git range-diff" output to be fed into "delta" for side-by-side diff</span>
<span class="c"># see ~/.config/git/config for usage (si-range-diff alias)</span>
<span class="c">#</span>
<span class="c"># depends on ansi2txt from https://github.com/kilobyte/colorized-logs</span>
<span class="c">#</span>
<span class="c"># developed against git 2.40 - git range-diff doesn't have stable output, might need adjustments</span>
<span class="c"># see https://github.com/git/git/blob/ee48e70a829d1fa2da82f14787051ad8e7c45b71/range-diff.c#L376</span>
<span class="nb">set</span> <span class="nt">-eu</span>
coproc ansi2txt <span class="o">{</span>
<span class="nb">stdbuf</span> <span class="nt">-oL</span> ansi2txt
<span class="o">}</span>
<span class="k">while </span><span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> <span class="nt">-r</span> l<span class="p">;</span> <span class="k">do</span>
<span class="c"># decolor line for matching/processing</span>
<span class="nb">printf</span> <span class="s2">"%s</span><span class="se">\n</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$l</span><span class="s2">"</span> <span class="o">></span>&<span class="s2">"</span><span class="k">${</span><span class="nv">ansi2txt</span><span class="p">[1]</span><span class="k">}</span><span class="s2">"</span>
<span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> <span class="nt">-r</span> ll <&<span class="s2">"</span><span class="k">${</span><span class="nv">ansi2txt</span><span class="p">[0]</span><span class="k">}</span><span class="s2">"</span>
<span class="k">if</span> <span class="o">[[</span> <span class="nv">$ll</span> <span class="o">==</span> <span class="s2">" @@ "</span><span class="k">*</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span> <span class="c"># hunk header line</span>
<span class="c"># fake hunk header for delta</span>
<span class="nb">printf</span> <span class="s2">"@@ -0,0 +0,0 @@ %s</span><span class="se">\n</span><span class="s2">"</span> <span class="s2">"</span><span class="k">${</span><span class="nv">ll</span><span class="p"># @@ </span><span class="k">}</span><span class="s2">"</span>
<span class="k">elif</span> <span class="o">[[</span> <span class="nv">$ll</span> <span class="o">==</span> <span class="s2">" "</span><span class="k">*</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span> <span class="c"># hunk diff line</span>
<span class="c"># un-indent diff</span>
<span class="nb">printf</span> <span class="s2">"%s</span><span class="se">\n</span><span class="s2">"</span> <span class="s2">"</span><span class="k">${</span><span class="nv">ll</span><span class="p"># </span><span class="k">}</span><span class="s2">"</span>
<span class="k">else</span> <span class="c"># pair header line</span>
<span class="c"># horizontal separator before the line</span>
tput setaf 39<span class="p">;</span> tput setab 235<span class="p">;</span> <span class="nb">printf</span> %<span class="s2">"</span><span class="si">$(</span>tput cols<span class="si">)</span><span class="s2">"</span>s | <span class="nb">sed</span> <span class="s1">'s/ /─/g'</span><span class="p">;</span> tput sgr0
<span class="nb">printf</span> <span class="s2">"%s</span><span class="se">\n</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$l</span><span class="s2">"</span>
<span class="k">if</span> <span class="o">[[</span> <span class="nv">$ll</span> <span class="o">=</span>~ ^[[:digit:]][[:digit:]]<span class="k">*</span>:<span class="s2">" "</span><span class="o">([[</span>:alnum:]][[:alnum:]]<span class="k">*</span><span class="o">)</span><span class="s2">" ! "</span><span class="o">[[</span>:digit:]][[:digit:]]<span class="k">*</span>:<span class="s2">" "</span><span class="o">([[</span>:alnum:]][[:alnum:]]<span class="k">*</span><span class="o">)</span><span class="s2">" "</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
<span class="c"># fake file header for delta to syntax highlight as patch</span>
<span class="nb">echo</span> <span class="s2">"--- </span><span class="k">${</span><span class="nv">BASH_REMATCH</span><span class="p">[1]</span><span class="k">}</span><span class="s2">.patch"</span>
<span class="nb">echo</span> <span class="s2">"+++ </span><span class="k">${</span><span class="nv">BASH_REMATCH</span><span class="p">[2]</span><span class="k">}</span><span class="s2">.patch"</span>
<span class="k">else</span>
<span class="c"># extra line for unmatched commits</span>
<span class="nb">echo
</span><span class="k">fi
fi
done</span>
</code></pre></div> </div>
</figure>
<p>To use this, override <code class="language-plaintext highlighter-rouge">pager.range-diff</code> when invoking <code class="language-plaintext highlighter-rouge">git range-diff</code>, like
so:</p>
<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>git <span class="nt">-c</span> pager.range-diff<span class="o">=</span><span class="s1">'
</span><span class="go"> git-range-diff-delta-preproc \
| delta \
--side-by-side \
--file-style=omit \
--line-numbers-left-format="" \
--line-numbers-right-format="│ " \
--hunk-header-style=syntax \
' range-diff wx-partial...wx-partial-rebase
</span></code></pre></div></div>
<p>(Or just steal all the delta-related bits from <a href="https://github.com/liskin/dotfiles/blob/b13cc7da57c223a6d2e00acd99234731efaa62fe/.config/git/config#L61">my git
config</a>.)</p>
<p>Yeah, I know, it’s a massive hack. <emoji>😞</emoji></p>
<h3 id="next-steps">Next steps</h3>
<p>To make this easier to use for the general public, we’d need:</p>
<ul>
<li>machine-readable output from <a href="https://git-scm.com/docs/git-range-diff"><code class="language-plaintext highlighter-rouge">git range-diff</code></a> (other git
commands have that, e.g. <code class="language-plaintext highlighter-rouge">git status --porcelain</code>)</li>
<li>implement parsing of that output in <a href="https://github.com/dandavison/delta">delta</a></li>
</ul>
<p>Unfortunately, I can’t volunteer to do either at this point<sup id="fnref:burnout" role="doc-noteref"><a href="#fn:burnout" class="footnote" rel="footnote">2</a></sup>,
apologies.</p>
<hr />
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:at-the-time-of-writing" role="doc-endnote">
<p>At the time of writing. <a href="#fnref:at-the-time-of-writing" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:burnout" role="doc-endnote">
<p>At present, I’m (very slowly) recovering from a mental health crisis. Just
getting this blog post out took me several months.</p>
<p>(This isn’t a call for help, it’s just context for why I can’t be expected
to do anything besides publishing this.) <a href="#fnref:burnout" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>
Technogym Skillbike: smart trainer usage and trying to get useful data out of it2022-08-29T00:00:00+00:00https://work.lisk.in/2022/08/29/technogym-skillbike<p>We recently moved to an apartment building in London that happens to include a
gym as a shared amenity. There’s <a href="https://www.technogym.com/">Technogym</a> equipment, including the
<a href="https://www.technogym.com/int/skillbike.html">Skillbike</a>, a <a href="https://en.wikipedia.org/wiki/Stationary_bicycle">stationary bike</a> somewhat capable of speaking the <a href="https://www.dcrainmaker.com/2016/07/everything-you-ever-wanted-to-know-about-ant-fe-c-and-bike-trainers.html">smart
trainer ANT+/BTLE protocols</a>. As it happens, getting it to
actually do something useful isn’t straightforward, so here’s a summary of my
attempts.</p>
<details id="toc">
<summary>Table of Contents</summary>
<ul id="markdown-toc">
<li><a href="#goal" id="markdown-toc-goal">Goal</a></li>
<li><a href="#skillbike" id="markdown-toc-skillbike">Skillbike</a></li>
<li><a href="#attempts" id="markdown-toc-attempts">Attempts</a> <ul>
<li><a href="#skillbike-itself" id="markdown-toc-skillbike-itself">Skillbike itself</a></li>
<li><a href="#garmin-watch" id="markdown-toc-garmin-watch">Garmin watch</a></li>
<li><a href="#zwift" id="markdown-toc-zwift">Zwift</a></li>
<li><a href="#rouvy" id="markdown-toc-rouvy">Rouvy</a></li>
<li><a href="#trainerroad" id="markdown-toc-trainerroad">TrainerRoad</a></li>
<li><a href="#wahoo-systm" id="markdown-toc-wahoo-systm">Wahoo SYSTM</a></li>
<li><a href="#wahoo-rgt" id="markdown-toc-wahoo-rgt">Wahoo RGT</a></li>
<li><a href="#skillbike-itself-revisited" id="markdown-toc-skillbike-itself-revisited">Skillbike itself revisited</a></li>
</ul>
</li>
<li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
<li><a href="#appendix-a-technogymmywellness-apps-and-logging-into-the-skillbike" id="markdown-toc-appendix-a-technogymmywellness-apps-and-logging-into-the-skillbike">Appendix A: Technogym/mywellness apps and logging into the Skillbike</a></li>
</ul>
</details>
<h3 id="goal">Goal</h3>
<p>What I’m actually trying to do:</p>
<ul>
<li>Get faster on the bike and fitter off the bike.</li>
<li>Do that effectively—structured training based on <a href="https://www.dcrainmaker.com/2009/08/power-primer-cycling-with-power-101.html">power</a> is
known to have results sooner than just riding the bike a lot (and I can’t
spend all my time doing that).</li>
<li>Not need to think too much about it. I’m spending my “thinking budget”
elsewhere, so here ideally something or someone tells me what to do when,
and I just do it.</li>
<li>Get data out of it. Primarily to help with the above (getting workout
suggestions from something/someone), but also because “if it’s not on Strava
it didn’t happen.”</li>
<li>Have fun while doing so.</li>
<li>As a stretch goal, while cycling indoor I can listen to more podcasts,
audiobooks and tech talks (or just random youtube videos).</li>
</ul>
<p>Last but not least, it’s a “new toy”, so I also just want to try it and see if
it’s any good. Many of my cycling friends already have a smart trainer and use
it with one of the apps made for them.</p>
<h3 id="skillbike">Skillbike</h3>
<figure class="half-size">
<p><a href="https://user-images.githubusercontent.com/300342/187048205-bec6edcc-10c4-4811-b11b-1a9f3ade3439.jpg"><img src="https://user-images.githubusercontent.com/300342/187048238-55125c58-87cd-4a88-a7bf-2f99ddc54758.jpg" alt="Technogym Skillbike" /></a></p>
<figcaption>Technogym Skillbike</figcaption>
</figure>
<p>The Skillbike is a predecessor of Technogym Ride—<a href="https://www.dcrainmaker.com/2022/08/technogym-ride-smart-indoor-bike-in-depth-review.html">recently reviewed by DC
Rainmaker</a> (recommended read to get an idea what it is). They both
look very similar from the outside. The display on the Skillbike is much
smaller and runs simpler, less capable software. This, however, makes space
for a phone holder, which is useful to run 3rd party apps like <a href="https://zwift.com/">Zwift</a>,
control podcasts or watch videos. Unfortunately it seems that Skillbike’s
implementation of <a href="https://www.dcrainmaker.com/2018/05/whats-baseline-on-a-smart-trainer-in-2018.html">smart trainer protocols</a> is worse, so not all
3rd party apps work. More about that later.</p>
<h3 id="attempts">Attempts</h3>
<h4 id="skillbike-itself">Skillbike itself</h4>
<p>The simplest thing I could do would be to just use <a href="https://support.technogym.com/post/training-modes-with-skillbike">what the Skillbike itself
offers</a>. That isn’t much, but at least it’s free (gratis)
and I can watch videos on my phone while training. Unfortunately the “not
think” and “get data” goals aren’t satisfied here, as it doesn’t sync into
Garmin Connect.</p>
<p>I’m not sure if that’s because their <a href="https://www.mywellness.com/">mywellness</a> cloud doesn’t even attempt
to sync into GC and just pulls data out of it (weight, daily movements
summary, …), or whether that’s because they generate a malformed TCX. I tried
to <a href="https://github.com/liskin/dotfiles/blob/33590ee7997c1d8326693e3d61504baf2824c097/bin/technogym-tcx-to-garmin-tcx">fix that TCX</a> and upload it manually. That works but
unfortunately doesn’t result in Garmin’s <a href="https://support.garmin.com/en-GB/?faq=EjPECQK58qA0xzJ5X74vm7">Physio TrueUp</a> syncing the data
back into my watch, so I can’t use any of Garmin’s training features.</p>
<p>Pros:</p>
<ul>
<li>No subscription fee.</li>
<li>Larger display than my smartphone, and the smartphone is free to play
videos.</li>
<li>Syncs into Strava.</li>
</ul>
<p>Cons:</p>
<ul>
<li>Doesn’t sync into Garmin Connect, so my FTP and Power Curve isn’t
automatically updated there.</li>
<li>Due to the above, workouts aren’t synced into my Garmin watch, so I don’t
see <a href="https://support.garmin.com/en-GB/?faq=VxKazDQ2mkAmDoQbJriEBA">Training Status</a> and <a href="https://support.garmin.com/en-GB/?faq=oYknGZ910l1pfBNzkDHX6A">Daily Suggested Workouts</a>. This goes against
the goal of not thinking too much.</li>
<li><a href="https://support.technogym.com/post/training-modes-with-skillbike">Very limited selection of workouts</a>.</li>
</ul>
<h4 id="garmin-watch">Garmin watch</h4>
<figure class="half-size">
<p><a href="https://user-images.githubusercontent.com/300342/187266942-50b34e06-0fa0-4241-a478-d330c74add2f.jpg"><img src="https://user-images.githubusercontent.com/300342/187266942-50b34e06-0fa0-4241-a478-d330c74add2f.jpg" alt="Suggested workout on Garmin Fenix 7S" /></a></p>
<figcaption>Suggested workout on Garmin Fenix 7S</figcaption>
</figure>
<p><a href="https://www.dropbox.com/sh/9lcbnhdark1wp90/AAC71ZGS4zg83ubt7IRCuOQIa">Skillbike manual</a> says that it can be connected to Garmin
devices via ANT+ and to Zwift via Bluetooth, so I tried that, in that order.
When the Skillbike isn’t in “Home mode” (which it isn’t, it’s in a gym), its
ANT+ identification changes every time the devices dialog is opened, requiring
new pairing with the Garmin and cluttering the list of sensors. What’s worse,
with my <a href="https://www.garmin.com/en-GB/p/735548">Garmin Fenix 7S</a> (firmware 8.37) the connection gets lost shortly
after I start pedalling. I wasn’t able to figure out what’s wrong, and
eventually gave up.</p>
<p>Good news is that <a href="https://twitter.com/Garmin/status/1561856493837844487">Garmin is interested in resolving this
issue</a>, so this may be
revisited, if/when time allows. There are other potential disadvantages of
having the Skillbike be driven by a watch, though.</p>
<p>Pros:</p>
<ul>
<li>If it worked, I wouldn’t need to think almost at all and just do the <a href="https://support.garmin.com/en-GB/?faq=oYknGZ910l1pfBNzkDHX6A">Daily
Suggested Workouts</a> while watching something on the smartphone.</li>
<li>Data sync would be seamless too.</li>
<li>No subscription fee.</li>
</ul>
<p>Cons:</p>
<ul>
<li>Doesn’t work: disconnects within half a minute.</li>
<li><a href="https://support.garmin.com/en-GB/?faq=oYknGZ910l1pfBNzkDHX6A">Daily Suggested Workouts</a> aren’t very fun (compared to Zwift, Rouvy, or
riding outdoors), it’s just a series of long periods of holding constant
power. Tolerable while watching a video, though.</li>
<li>It’s a watch: small screen, and not in front of eyes. Not ideal if I want to
see/adjust the workout.</li>
</ul>
<h4 id="zwift">Zwift</h4>
<p>Next thing to try is <a href="https://zwift.com/">Zwift</a>. My current smartphone (<a href="https://www.gsmarena.com/samsung_galaxy_s22_5g-11253.php">Samsung Galaxy S22</a>)
doesn’t support ANT+ (the previous one, <a href="https://www.gsmarena.com/samsung_galaxy_s10e-9537.php">S10e</a>, did) so I was worried this
might not work at all. Turns out Zwift connected to the Skillbike over
Bluetooth immediately, and everything just worked perfectly on the first try.
Sync with Garmin and Strava also worked fine, so my Garmin watch started
showing training metrics/features for cycling almost immediately. Good!</p>
<p>When I tried Zwift for the second time, I struggled to connect it with the
Skillbike, but I think this may have been caused by my previous experiments
with ANT+ and Garmin. After connecting/disconnecting the watch once, I haven’t
encountered this issue again. Another time I tried to use Zwift, my free trial
kilometers ran out, and while I was able to complete the workout (FTP ramp
test), the resulting FIT file was corrupted and wouldn’t sync to Garmin. After
trying some online FIT repair tools suggested by Zwift’s support, I was able
to repair the file manually using <a href="https://developer.garmin.com/fit/fitcsvtool/">FitCSVTool</a> from the official <a href="https://developer.garmin.com/fit/download/">Garmin FIT
SDK</a>.</p>
<p>These were hopefully one-off issues, but I still kept searching for a better
alternative, because Zwift is primarily a video game made for larger screens,
and I’d like something more training-focused that doesn’t waste screen
space—there’s just a phone holder on the Skillbike, no place to hold a laptop.
If I carried a fan, a laptop and a laptop stand to our shared gym, I’d look
like a mad man.</p>
<p>Pros:</p>
<ul>
<li>Syncs into both Strava and Garmin. (Except when it produces a malformed FIT
file and doesn’t.)</li>
<li>Garmin officially supports this, so its <a href="https://support.garmin.com/en-GB/?faq=EjPECQK58qA0xzJ5X74vm7">Physio TrueUp</a> feature syncs
workouts back into the watch which shows <a href="https://support.garmin.com/en-GB/?faq=VxKazDQ2mkAmDoQbJriEBA">Training Status</a> and <a href="https://support.garmin.com/en-GB/?faq=oYknGZ910l1pfBNzkDHX6A">Daily
Suggested Workouts</a>.</li>
<li>Workouts target cadence in addition to power.</li>
<li>Lots of workouts to choose from. Training plans, too.</li>
<li>Less boring than just holding a constant power for a set amount of time.</li>
</ul>
<p>Cons:</p>
<ul>
<li>Subscription fee.</li>
<li>Made for large screens, barely usable on a 6” smartphone. Most of the screen
is wasted on the “fun” stuff and only a small part shows the training
elements (target watts, cadence, workout steps, …).</li>
<li>Can’t watch a video, only podcasts/audiobooks.</li>
</ul>
<h4 id="rouvy">Rouvy</h4>
<p>Next on the list is <a href="https://rouvy.com/">Rouvy</a>, a competitor to Zwift developed in my home
country, Czechia. The main difference between the two is that <a href="https://www.dcrainmaker.com/2018/12/rouvy-augmented-reality-training.html">Zwift is
<em>virtual</em> reality while Rouvy is <em>augmented</em> reality</a>. There are
(video recordings of) real-world cycling routes in Rouvy. This slightly less
game-y focus also means the user interface is less distracting, there are no
virtual fans cheering me to “go go go” while I’m listening to a podcast, and
as a bonus the fonts can be enlarged to be readable on a small screen. In
theory, Rouvy should be better (for me) than Zwift in all aspects. In
practice, I ran into several issues that I haven’t been able to resolve.</p>
<p>When I tried to set up sync with Garmin and Strava, in both cases it asked for
permissions to pull data out of those in addition to just uploading rides done
in Rouvy. I disabled those permissions, which resulted in Strava not being
connected at all, and Garmin connected in a weird state where Rouvy thought it
can only receive data and not upload (the opposite of the permissions I
allowed), but there’s still an option to trigger the upload manually. Despite
this weirdness, rides would sync to Garmin Connect just fine, often
automatically. One would expect GC to then sync them to Strava (just as it
does with rides recorded on the watch), but on one occasion that didn’t
happen.</p>
<p>I reported this to them and I’m sure they’ll fix this eventually. Meanwhile, I
could sync manually whenever the auto sync fails. Annoying, but tolerable, I
thought, and enthusiastically changed into lycra and went to the gym to try an
FTP test with Rouvy. “Cannot launch workout — unknown error occurred.” Did I
try turning it off and on again? Yes, sure, I did. Time to contact the support
for the second time. Apparently this is affecting more people and should be
fixed sooner than the sync issues.</p>
<p>Well, okay, but my free trial is running out and I’m not entirely convinced I
want to put it my credit card details… :-/</p>
<p>Pros:</p>
<ul>
<li>Garmin officially supports this, so as with Zwift, I get all the benefits of
having the data synced. That is, provided the data actually syncs…</li>
<li>Lots of workouts to choose from.</li>
<li>Way less boring than just holding a constant power for a set amount of time.</li>
<li>Compared to Zwift, less distracting user interface focused more on training
than trying to be a video game. Also, fonts can be adjusted a bit (not
enough) for a small smartphone screen.</li>
<li>Made by friends from Czechia.</li>
</ul>
<p>Cons:</p>
<ul>
<li>Buggy sync with Garmin/Strava.</li>
<li>FTP tests didn’t work at the time I tried it. (Support says it’s a known
error, should be fixed soon.)</li>
<li>Subscription fee.</li>
<li>Made for large screens. Smartphone works better than Zwift, but still an
afterthought.</li>
<li>Can’t watch a video, only podcasts/audiobooks.</li>
</ul>
<h4 id="trainerroad">TrainerRoad</h4>
<p>After having success connecting the Skillbike with both Zwift and Rouvy, I
thought anything that connects via Bluetooth would just work. <a href="https://www.trainerroad.com/">TrainerRoad</a>
has a reputation of being an excellent option for people who train seriously.
They don’t waste time on pretty virtual cycling features and instead focus on
being the best for effective training. Machine learning and all that. Sounds
good, maybe I’d be able to watch a video in split screen and get the most
out of the time spent pedalling.</p>
<p>Long story short, it doesn’t work with the Skillbike. It took several tries to
connect with it (both Zwift/Rouvy connected almost immediately) and even then,
it was only connected as a power meter, not as a controllable smart trainer,
so it didn’t adjust resistance or set target power in ERG mode.</p>
<p>At this point I’ve already wasted a lot of time so I didn’t bother contacting
support and just asked for a refund. I really do wish this worked, however,
because TrainerRoad would be ideal for my goals. Perhaps I shall revisit this
later.</p>
<p>Pros:</p>
<ul>
<li>Garmin officially supports this, so as with Zwift, I’d get all the benefits of
having the data synced.</li>
<li>Focused on training, not a distracting virtual cycling game.</li>
<li>Lots of workouts to choose from. Training plans, possibly self-adapting to
what I actually end up doing.</li>
<li>Can probably watch a video, listen to podcasts, whatever.</li>
<li>From what I’ve heard, able to estimate FTP continuously, not needing a
periodic FTP test.</li>
</ul>
<p>Cons:</p>
<ul>
<li>Doesn’t see Skillbike as a smart trainer, just as a power meter. Unable to
control resistance (target power).</li>
<li>Subscription fee. Additionally, free trial isn’t free—it’s give us your
credit card details and we’ll refund if you’re not satisfied.</li>
</ul>
<h4 id="wahoo-systm">Wahoo SYSTM</h4>
<p>There’s an alternative to TrainerRoad from Wahoo: formerly called the
Sufferfest, now <a href="https://wahoofitness.com/systm">Wahoo SYSTM</a>. On paper, it should be more or less the
same style as TrainerRoad: no-bullshit training focused app. In practice, it’s
indeed the same—only treats the Skillbike as a power meter, but cannot control
it.</p>
<p>Unlike TrainerRoad, they responded to my feedback with a note saying that
getting this working likely involves some help from Technogym, and that they’d
be glad if I complained to those as well. And that I can get in touch so we
can work together on resolving this. If/when time allows, I might…</p>
<p>Pros:</p>
<ul>
<li>Same as TrainerRoad, except for the FTP estimation.</li>
</ul>
<p>Cons:</p>
<ul>
<li>Same as TrainerRoad.</li>
</ul>
<h4 id="wahoo-rgt">Wahoo RGT</h4>
<p><a href="https://www.rgtcycling.com/">Wahoo RGT</a>, a recent (2022) acquisition of theirs, is a virtual cycling
game like Zwift. I managed to log into the app exactly once, but when I
changed into lycra and made my way down to the gym, it started crashing, and
no amount of force stops and reinstalls would help.</p>
<p>Another half an hour of standing in the gym with a smartphone in my hand,
having others ask me whether I’m waiting for the equipment they’re currently
using… I did end up looking like a mad man after all, it seems. Can’t be
blamed for not following up with Wahoo’s support, can I? :-)</p>
<p>Pros:</p>
<ul>
<li>No idea…</li>
</ul>
<p>Cons:</p>
<ul>
<li>Crashes immediately after sign-in.</li>
<li>I’d be surprised if it supported Skillbike as a controllable smart trainer
when SYSTM doesn’t.</li>
<li>Likely to have similar cons as Zwift: not what I’m looking for with just a
small smartphone screen.</li>
</ul>
<h4 id="skillbike-itself-revisited">Skillbike itself revisited</h4>
<p>I think I’ve tried everything but still don’t have a winner:</p>
<ul>
<li>I could strap a magnifying glass to my phone and pay double the <a href="https://www.cyclinguk.org/membership-types">Cycling UK
Household membership</a> for
Zwift. I might also figure out a way to strap a tablet onto the Skillbike
somehow.</li>
<li>Or I can pay that amount for a Rouvy family membership and share the
struggles of manually syncing rides with my wife. She probably wouldn’t even
mind not being able to take an FTP test.</li>
<li>Or just use the Skillbike itself, but plan and track my workouts without
the assistance of Garmin or some other app.</li>
<li>(Or just buy another smart trainer, or go to a gym that has one, or get a
power meter and ride outside. Valid options but let’s stick with the
Skillbike for this blog post.)</li>
</ul>
<p>Well actually, there’s one thing I haven’t tried yet…</p>
<p>In the short time I had outside of standing in the middle of a gym with a
phone in my hand, I did a little experiment: if <a href="https://support.garmin.com/en-GB/?faq=EjPECQK58qA0xzJ5X74vm7">Physio TrueUp</a> won’t sync
the <a href="https://github.com/liskin/dotfiles/blob/33590ee7997c1d8326693e3d61504baf2824c097/bin/technogym-tcx-to-garmin-tcx">fixed TCX</a> to my watch, what if I upload it there using a
USB cable? Didn’t work either. Actually, not only were training metrics not
updated, the watch didn’t show the activity at all. A-ha! The watch doesn’t
understand TCX, what if I gave it <a href="https://developer.garmin.com/fit/protocol/">FIT</a> instead?</p>
<p>A weekend of hacking later, I had a Python script that loads the not entirely
valid TCX file from Skillbike/mywellness and uses the <a href="https://developer.garmin.com/fit/download/">Garmin FIT SDK</a> (via
<a href="https://github.com/jpype-project/jpype">jpype</a>, a cross-language bridge to allow using the Java library directly
from Python) to generate a FIT file with faked source as Zwift to trick
<a href="https://support.garmin.com/en-GB/?faq=EjPECQK58qA0xzJ5X74vm7">Physio TrueUp</a> into syncing it to the watch:
<a href="https://github.com/liskin/dotfiles/blob/6c3ccf3f0ca8813e71f047c5060644bc08467c24/bin/technogym-tcx-to-garmin-fit">technogym-tcx-to-garmin-fit.py</a>.</p>
<p>And guess what? It worked on the first try! (Except for a little mistake in
timestamp conversion. Syncing a ride 20 years in the future wiped the training
metrics from the watch, so I had to ride some more to get them back.)</p>
<p>Pros:</p>
<ul>
<li>Synced into Strava, and now also Garmin.</li>
<li>Now that I sync this into Garmin as a fake Zwift ride, I get all the data
and suggestions as if it was a Zwift ride.</li>
<li>No subscription fee.</li>
<li>Larger display than my smartphone, and the smartphone is free to play
videos.</li>
</ul>
<p>Cons:</p>
<ul>
<li>Needs manual conversion and sync. (I’ll automate this later and update the
post.)</li>
<li><a href="https://support.technogym.com/post/training-modes-with-skillbike">Very limited selection of workouts</a>. It’s possible to
create a custom workout and also adjust the power mid-ride, but it’s
annoying having to do this manually before every workout. There is an option
to repeat a previous workout (by date, not by name), but I haven’t tested
this yet.</li>
</ul>
<h3 id="conclusion">Conclusion</h3>
<p>After having spent a weekend hacking the TCX→FIT conversion tool, I’m
obviously a victim of the sunken cost fallacy and will prefer riding the
Skillbike workouts for some time. Objectively speaking, however, learning
about <a href="https://developer.garmin.com/fit/protocol/">FIT</a> and <a href="https://github.com/jpype-project/jpype">jpype</a> was worth it anyway, and I will reevaluate some of
the mentioned apps later. Perhaps some of my friends will nudge me into riding
Zwift with them in winter, and I’ll revisit the option of strapping a tablet
onto the Skillbike. Rouvy may well fix all the problems by that time, too, and
we can ride there instead.</p>
<p>Until winter, DIY Python script it is, though!</p>
<p>(Also, now that I know how to generate a FIT file, I can try to sync
<a href="https://www.technogym.com/int/skillrow.html">Skillrow</a> workouts as well. Those are only visible in the <a href="https://www.mywellness.com/">mywellness</a>
cloud, however, so it’ll be more involved than just downloading a TCX from
Strava.)</p>
<h3 id="appendix-a-technogymmywellness-apps-and-logging-into-the-skillbike">Appendix A: Technogym/mywellness apps and logging into the Skillbike</h3>
<p>In order to not disrupt the post with unimportant details, I didn’t mention
one other hurdle when connecting the Skillbike with external devices. Despite
its <a href="https://www.dropbox.com/sh/9lcbnhdark1wp90/AAC71ZGS4zg83ubt7IRCuOQIa">instruction manual</a> claiming otherwise, it’s necessary
to log into the Skillbike using a mywellness account before the devices dialog
can be used.</p>
<p>At first, the Skillbike was offline, and thus useless. Fortunately, the
<a href="https://www.dropbox.com/sh/9lcbnhdark1wp90/AAC71ZGS4zg83ubt7IRCuOQIa">manual</a> documents how to access the hidden configuration
screen and I managed to connect it to WiFi.</p>
<p>Then, there are two apps which can be used for the login:</p>
<ul>
<li>older, <a href="https://play.google.com/store/apps/details?id=com.technogym.mywellness">mywellness app</a>, rated 2.7<emoji>⭐</emoji></li>
<li>newer, <a href="https://play.google.com/store/apps/details?id=com.technogym.tgapp">Technogym app</a>, rated 3.8<emoji>⭐</emoji></li>
</ul>
<p>I didn’t even try the first one. The second one works, although only using the
QR code (a fallback), not using NFC/Bluetooth (which is what the app tells you
to do). A sub-4.0 rating is well deserved.</p>
<p>That being said, there are some cycling training plans in the app, so perhaps
it can be used to load more interesting workouts onto the Skillbike. My wife
successfully managed to let the app launch a treadmill workout (although she
couldn’t really choose what workout…), adjusting the speed and incline
throughout, so it’s not entirely useless. I’ll experiment with this later,
once I no longer feel bad for playing with my phone in the gym. :-)</p>
Even faster bash startup2020-11-20T00:00:00+00:00https://work.lisk.in/2020/11/20/even-faster-bash-startup<p>I sped up bash startup from 165 ms to 40 ms. It’s actually noticeable.
Why and how did I do it?</p>
<details id="toc">
<summary>Table of Contents</summary>
<ul id="markdown-toc">
<li><a href="#motivation" id="markdown-toc-motivation">Motivation</a></li>
<li><a href="#investigation" id="markdown-toc-investigation">Investigation</a> <ul>
<li><a href="#man" id="markdown-toc-man">man</a></li>
<li><a href="#death-by-a-thousand-cuts" id="markdown-toc-death-by-a-thousand-cuts">death by a thousand cuts</a></li>
<li><a href="#completions" id="markdown-toc-completions">completions</a></li>
<li><a href="#fzf" id="markdown-toc-fzf">fzf</a></li>
<li><a href="#are-we-done-yet" id="markdown-toc-are-we-done-yet">are we done yet?</a></li>
<li><a href="#history" id="markdown-toc-history">history</a></li>
</ul>
</li>
<li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
<li><a href="#update-1-why-not-fix-typing-before-the-prompt-instead" id="markdown-toc-update-1-why-not-fix-typing-before-the-prompt-instead">Update 1: Why not fix typing before the prompt instead?</a></li>
</ul>
</details>
<h3 id="motivation">Motivation</h3>
<p>Whenever I need to quickly look something up (or use a calculator), I open a
new terminal (using a keyboard shortcut) and start typing into it. Slow bash
startup disrupts this workflow as I would often type before the shell prompt:</p>
<p><img src="/img/even-faster-bash-startup/mistype.png" alt="messed up prompt" /></p>
<p><a href="https://twitter.com/danpker">Daniel Parker</a> recently wrote an excellent blog post <a href="https://danpker.com/posts/faster-bash-startup/">Faster Bash
Startup</a> detailing his journey from 1.7 seconds to 210 ms. I start at 165 ms
and need to go significantly lower than Daniel, therefore different techniques
will be needed.</p>
<h3 id="investigation">Investigation</h3>
<p><a href="https://github.com/sharkdp/hyperfine">hyperfine</a> is a brilliant command-line tool for benchmarking commands that
I discovered recently (thanks to Daniel!), so let’s see where we are now:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[tomi@notes ~]$ hyperfine 'bash -i'
Benchmark #1: bash -i
Time (mean ± σ): 165.8 ms ± 0.7 ms [User: 156.3 ms, System: 12.8 ms]
Range (min … max): 164.9 ms … 167.1 ms 17 runs
</code></pre></div></div>
<p>Now we need to find out what’s taking so long. <a href="https://stackoverflow.com/questions/5014823/how-to-profile-a-bash-shell-script-slow-startup/20855353">How to profile a bash shell
script slow startup?</a> Most Stack Overflow answers suggest some
variant of <code class="language-plaintext highlighter-rouge">set -x</code>, which will help us find any single command that takes
unusually long.</p>
<h4 id="man">man</h4>
<p>In my case, that command was <code class="language-plaintext highlighter-rouge">man -w</code>, specifically <a href="https://github.com/liskin/dotfiles/blob/7d14190467fe22bf5d4f85a7b202118d2341e3ed/.bashrc.d/10_env.sh#L8-L10">this piece of my
<code class="language-plaintext highlighter-rouge">.bashrc.d/10_env.sh</code></a>:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">MANPATH</span><span class="o">=</span><span class="nv">$HOME</span>/.local/share/man:
<span class="c"># FIXME: workaround for /usr/share/bash-completion/completions/man</span>
<span class="nv">MANPATH</span><span class="o">=</span><span class="si">$(</span>man <span class="nt">-w</span><span class="si">)</span>
</code></pre></div></div>
<p>Turns out none of this is needed any more, <code class="language-plaintext highlighter-rouge">man</code> and <code class="language-plaintext highlighter-rouge">manpath</code> now add
<code class="language-plaintext highlighter-rouge">~/.local/share/man</code> automatically so I can just drop it and save more than
100 ms<sup id="fnref:man-seccomp" role="doc-noteref"><a href="#fn:man-seccomp" class="footnote" rel="footnote">1</a></sup>.</p>
<h4 id="death-by-a-thousand-cuts">death by a thousand cuts</h4>
<p>But that’s it. No other single command stands out, it’s just a lot of small
things that add up. Daniel says “it has to take <em>some</em> time,” and he’s mostly
right, but I still have one trick up my sleeve.</p>
<p>My <code class="language-plaintext highlighter-rouge">.bashrc</code> is split into several smaller parts in <code class="language-plaintext highlighter-rouge">~/.bashrc.d</code>, so I can
profile these and see if anything stands out. My
<a href="https://github.com/liskin/dotfiles/blob/68964611b4b578b646cf5f13a47a4ee77e93e740/.bashrc"><code class="language-plaintext highlighter-rouge">.bashrc</code></a>
thus becomes:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for </span>i <span class="k">in</span> ~/.bashrc.d/<span class="k">*</span>.sh<span class="p">;</span> <span class="k">do
if</span> <span class="o">[[</span> <span class="nv">$__bashrc_bench</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
</span><span class="nv">TIMEFORMAT</span><span class="o">=</span><span class="s2">"</span><span class="nv">$i</span><span class="s2">: %R"</span>
<span class="nb">time</span> <span class="nb">.</span> <span class="s2">"</span><span class="nv">$i</span><span class="s2">"</span>
<span class="nb">unset </span>TIMEFORMAT
<span class="k">else</span>
<span class="nb">.</span> <span class="s2">"</span><span class="nv">$i</span><span class="s2">"</span>
<span class="k">fi
done</span><span class="p">;</span> <span class="nb">unset </span>i
</code></pre></div></div>
<p>Let’s see what happens…</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[tomi@notes ~]$ __bashrc_bench=1 bash -i
/home/tomi/.bashrc.d/10_env.sh: 0,118
/home/tomi/.bashrc.d/20_history.sh: 0,000
/home/tomi/.bashrc.d/20_prompt.sh: 0,002
/home/tomi/.bashrc.d/30_completion_git.sh: 0,000
/home/tomi/.bashrc.d/31_completion.sh: 0,011
/home/tomi/.bashrc.d/50_aliases.sh: 0,002
/home/tomi/.bashrc.d/50_aliases_sudo.sh: 0,000
/home/tomi/.bashrc.d/50_functions.sh: 0,001
/home/tomi/.bashrc.d/50_git_dotfiles.sh: 0,008
/home/tomi/.bashrc.d/50_mc.sh: 0,000
/home/tomi/.bashrc.d/90_fzf.sh: 0,011
</code></pre></div></div>
<p>118 ms in <code class="language-plaintext highlighter-rouge">10_env.sh</code> was caused by <code class="language-plaintext highlighter-rouge">man -w</code> and we know what to do with that.</p>
<h4 id="completions">completions</h4>
<p>11 ms in <code class="language-plaintext highlighter-rouge">31_completion.sh</code> which loads <a href="https://github.com/scop/bash-completion">bash-completion</a>. That’s
certainly better than Daniel’s 235 ms, probably because up-to-date
bash-completion only loads a few necessary completions and defers everything
else to being loaded on demand. I couldn’t live without the completions, so
11 ms is a fair price.</p>
<p>8 ms for <code class="language-plaintext highlighter-rouge">50_git_dotfiles.sh</code>, which defines a few aliases and
sets up git completions for my <code class="language-plaintext highlighter-rouge">git-dotfiles</code> alias, seems too much, though.
Good news is that we don’t need to drop this. We can use bash-completion’s
on-demand loading. Whenever completions for command <code class="language-plaintext highlighter-rouge">cmd</code> are needed for the
first time, bash-completion looks for
<code class="language-plaintext highlighter-rouge">~/.local/share/bash-completion/completions/cmd</code> or
<code class="language-plaintext highlighter-rouge">/usr/share/bash-completion/completions/cmd</code>.</p>
<p>Therefore,
<a href="https://github.com/liskin/dotfiles/blob/68964611b4b578b646cf5f13a47a4ee77e93e740/.local/share/bash-completion/completions/git-dotfiles"><code class="language-plaintext highlighter-rouge">~/.local/share/bash-completion/completions/git-dotfiles</code></a>
becomes:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>. /usr/share/bash-completion/completions/git
complete -F _git git-dotfiles
</code></pre></div></div>
<h4 id="fzf">fzf</h4>
<p><code class="language-plaintext highlighter-rouge">90_fzf.sh</code> loads key bindings and completions code so that <a href="https://github.com/junegunn/fzf">fzf</a> is used
when searching through history, completing <code class="language-plaintext highlighter-rouge">**</code> in filenames, etc. Well worth
the 11 ms it needs to load<sup id="fnref:fzf" role="doc-noteref"><a href="#fn:fzf" class="footnote" rel="footnote">2</a></sup>.</p>
<h4 id="are-we-done-yet">are we done yet?</h4>
<p>After these changes, I got:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[tomi@notes ~]$ __bashrc_bench=1 bash -i
/home/tomi/.bashrc.d/10_env.sh: 0,001
/home/tomi/.bashrc.d/20_history.sh: 0,000
/home/tomi/.bashrc.d/20_prompt.sh: 0,002
/home/tomi/.bashrc.d/30_completion_git.sh: 0,000
/home/tomi/.bashrc.d/31_completion.sh: 0,012
/home/tomi/.bashrc.d/50_aliases.sh: 0,002
/home/tomi/.bashrc.d/50_aliases_sudo.sh: 0,000
/home/tomi/.bashrc.d/50_functions.sh: 0,001
/home/tomi/.bashrc.d/50_git_dotfiles.sh: 0,000
/home/tomi/.bashrc.d/50_mc.sh: 0,000
/home/tomi/.bashrc.d/90_fzf.sh: 0,011
</code></pre></div></div>
<p>That’s 29 ms, brilliant! Or… is it? <emoji>🤔</emoji></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[tomi@notes ~]$ hyperfine 'bash -i'
Benchmark #1: bash -i
Time (mean ± σ): 55.7 ms ± 1.0 ms [User: 47.6 ms, System: 11.1 ms]
Range (min … max): 54.8 ms … 58.9 ms 53 runs
</code></pre></div></div>
<h4 id="history">history</h4>
<p>Some of those additional 26 ms are spent reading my huge
(<code class="language-plaintext highlighter-rouge">HISTSIZE=50000</code>) <code class="language-plaintext highlighter-rouge">.bash_history</code> file. I will skip the details
about how I investigated this, because I didn’t: I stumbled upon this by
chance while testing something else.</p>
<p>We can see that using an empty history file brings us down to a little under
40 ms:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[tomi@notes ~]$ HISTFILE=/tmp/.bash_history_tmp hyperfine 'bash -i'
Benchmark #1: bash -i
Time (mean ± σ): 38.6 ms ± 0.7 ms [User: 34.0 ms, System: 7.8 ms]
Range (min … max): 37.8 ms … 42.3 ms 75 runs
</code></pre></div></div>
<p>Now, cutting 17 ms by sacrificing the shell history is probably not a good
deal for most people. I settled for setting up a systemd
<a href="https://github.com/liskin/dotfiles/blob/f978be7424946afebe56dbe5ecc85c9f36d1e057/.config/systemd/user/liskin-backup-bash-history.timer">timer</a>
to <a href="https://github.com/liskin/dotfiles/blob/f978be7424946afebe56dbe5ecc85c9f36d1e057/bin/liskin-backup-bash-history">back up
<code class="language-plaintext highlighter-rouge">.bash_history</code></a>
to git once a day and lowered <code class="language-plaintext highlighter-rouge">HISTSIZE</code> to 5000<sup id="fnref:history" role="doc-noteref"><a href="#fn:history" class="footnote" rel="footnote">3</a></sup>. This still keeps
my bash startup below 40 ms:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[tomi@notes ~]$ hyperfine 'bash -i'
Benchmark #1: bash -i
Time (mean ± σ): 39.9 ms ± 0.5 ms [User: 36.1 ms, System: 6.8 ms]
Range (min … max): 39.1 ms … 42.1 ms 73 runs
</code></pre></div></div>
<h3 id="conclusion">Conclusion</h3>
<p>By dropping unnecessary invocation of <code class="language-plaintext highlighter-rouge">man -w</code>, deferring loading of git
completions to when they’re needed, and shortening my shell history file, I
managed to speed up bash startup from 165 ms to 40 ms.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Benchmark #1: bash -i
Time (mean ± σ): 165.8 ms ± 0.7 ms [User: 156.3 ms, System: 12.8 ms]
Range (min … max): 164.9 ms … 167.1 ms 17 runs
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Benchmark #1: bash -i
Time (mean ± σ): 39.9 ms ± 0.5 ms [User: 36.1 ms, System: 6.8 ms]
Range (min … max): 39.1 ms … 42.1 ms 73 runs
</code></pre></div></div>
<p>More importantly, I no longer type before the prompt, even if I try!</p>
<p><img src="/img/even-faster-bash-startup/corrtype.png" alt="not messed up prompt" /></p>
<p>And at this point I can finally agree with Daniel that further tweaking will
only have diminishing returns<sup id="fnref:latency" role="doc-noteref"><a href="#fn:latency" class="footnote" rel="footnote">4</a></sup>. <emoji>😊</emoji></p>
<hr />
<h3 id="update-1-why-not-fix-typing-before-the-prompt-instead">Update 1: Why not fix typing before the prompt instead?</h3>
<p>Redditor <em>buttellmewhynot</em> (pun intended)
<a href="https://old.reddit.com/r/linux/comments/jxfm2y/even_faster_bash_startup_165_ms_40_ms/gcxiigg/">comments</a>:</p>
<blockquote>
<p>I feel like it shouldn’t matter that the shell starts with a delay. If you
start a shell, the computer should assume that you want further input
directed there and queue somewhere to send it to the shell when it’s up.</p>
<p>I understand that there’s probably a lot of weird quirks about how terminals
and shells work and how processes get created but surely there’s a way to do
this.</p>
</blockquote>
<p>They’re right on both points. The input is queued somewhere, and there is a
way to fix the messed up prompt. As some might suspect, <a href="https://www.zsh.org/">zsh</a> handles it
fine: try running <code class="language-plaintext highlighter-rouge">sleep 5</code> and type some input in the meantime:</p>
<figure>
<table>
<thead><tr><th>zsh</th><th>bash</th></tr></thead>
<tr>
<td><img src="/img/even-faster-bash-startup/zsh-nolf.png" alt="zsh no lf" /></td>
<td><img src="/img/even-faster-bash-startup/bash-nolf.png" alt="bash no lf" /></td>
</tr>
<tr>
<td><img src="/img/even-faster-bash-startup/zsh-lf.png" alt="zsh lf" /></td>
<td><img src="/img/even-faster-bash-startup/bash-lf.png" alt="bash lf" /></td>
</tr>
</table>
<figcaption>pending input handling without custom prompt</figcaption>
</figure>
<p>We can see that:</p>
<ul>
<li>in all cases, the input appears twice (bit annoying, but tolerable)</li>
<li>zsh prompt is never messed up</li>
<li>bash prompt is messed up if there’s no newline after the input<sup id="fnref:readline-assumes" role="doc-noteref"><a href="#fn:readline-assumes" class="footnote" rel="footnote">5</a></sup></li>
<li>no input is discarded, in contrast to the first image of this post</li>
</ul>
<p>Turns out <a href="https://github.com/liskin/dotfiles/blob/460bdc3c5fa814b874c19d172ce0e3955e278207/.bashrc.d/20_prompt.sh#L13-L27">my PROMPT_COMMAND</a> which was meant to <a href="https://stackoverflow.com/q/19943482/3407728">ensure
the prompt always starts on new line</a> was discarding
the pending input. Zsh uses <a href="https://github.com/zsh-users/zsh/blob/19390a1ba8dc983b0a1379058e90cd51ce156815/Src/utils.c#L1599">a different approach</a>,
printing <code class="language-plaintext highlighter-rouge">$COLUMNS</code> spaces and then a carriage return
(<a href="https://serverfault.com/a/97543">explanation</a>), which I don’t like as it messes
up copy/paste. But I <a href="https://github.com/liskin/dotfiles/blob/9785740f7f97d7da1f847053fb29fc5a3174d075/.bashrc.d/20_prompt.sh#L10-L27">managed to improve my solution</a> to
correctly detect pending input and not discard it.</p>
<p><img src="/img/even-faster-bash-startup/earlytype.png" alt="not messed up prompt after early typing" /></p>
<p>It’s not perfect (so I’ll still try to keep bash startup fast), but it’s
definitely an improvement, and it will be useful whenever I get impatient with
a slow command and start typing the next command before the prompt appears.</p>
<p>Thank you <a href="https://old.reddit.com/user/buttellmewhynot"><em>buttellmewhynot</em></a> for
nudging me in the correct direction.</p>
<hr />
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:man-seccomp" role="doc-endnote">
<p>At the time of publishing this post, <code class="language-plaintext highlighter-rouge">man -w</code> no longer takes 100+ ms
thanks to <a href="https://github.com/seccomp/libseccomp/blob/2366f6380198c7af23d145a153ccaa9ba37f9db1/CHANGELOG#L13-L14">several performance improvements in
libseccomp</a> <a href="#fnref:man-seccomp" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:fzf" role="doc-endnote">
<p>At the time of publishing this post, the latest fzf release
(<a href="https://github.com/junegunn/fzf/releases/tag/0.24.3">0.24.3</a>) loads
twice as long (20+ ms). I fixed this in
<a href="https://github.com/junegunn/fzf/pull/2246">#2246</a> and
<a href="https://github.com/junegunn/fzf/pull/2250">#2250</a>, but it might take a
short while to be released and find its way to distributions. <a href="#fnref:fzf" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:history" role="doc-endnote">
<p>5000 is a bit limiting in practice, as it rolls over in a few weeks. In
2020, you’d expect your shell to keep <a href="https://superuser.com/questions/137438/how-to-unlimited-bash-shell-history">unlimited
history</a>
without slowdown. I will address this in another post soon. <a href="#fnref:history" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:latency" role="doc-endnote">
<p>Some people may be even more sensitive to latency than me, but
measurements by <a href="https://danluu.com/">Dan Luu</a> suggest that at this scale
there are other bottlenecks: <a href="https://danluu.com/input-lag/">Computer
latency</a>, <a href="https://danluu.com/keyboard-latency/">Keyboard
latency</a>. <a href="#fnref:latency" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:readline-assumes" role="doc-endnote">
<p><a href="https://twobithistory.org/2019/08/22/readline.html">GNU Readline</a> assumes the prompt starts in the first column so it gets
more messed up later e.g. when walking through history using
<kbd>↑</kbd>/<kbd>↓</kbd>. <a href="#fnref:readline-assumes" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>
A better xrandr command-line experience2020-10-11T00:00:00+00:00https://work.lisk.in/2020/10/11/xrandr-ux<p>Can we make a small improvement to <a href="https://manpages.debian.org/unstable/x11-xserver-utils/xrandr.1.en.html">xrandr</a> command-line user experience so
that extra tools like <a href="https://christian.amsuess.com/tools/arandr/">arandr</a> (GUI for xrandr) or <a href="https://github.com/phillipberndt/autorandr">autorandr</a> become
unnecessary for some people (like me)? Yes, I think that making the <code class="language-plaintext highlighter-rouge">--output</code>
option a bit more powerful goes a long way, and lets me cover most use-cases
with just four shell functions/aliases.</p>
<figure>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function </span>layout-vertical <span class="o">{</span>
xrandr-smart <span class="nt">--output</span> <span class="s1">'eDP-*'</span> <span class="nt">--auto</span> <span class="se">\</span>
<span class="nt">--output</span> <span class="s1">'!(eDP-*)'</span> <span class="nt">--auto</span> <span class="nt">--above</span> <span class="s1">'eDP-*'</span>
<span class="o">}</span>
<span class="k">function </span>layout-horizontal <span class="o">{</span>
xrandr-smart <span class="nt">--output</span> <span class="s1">'eDP-*'</span> <span class="nt">--auto</span> <span class="se">\</span>
<span class="nt">--output</span> <span class="s1">'!(eDP-*)'</span> <span class="nt">--auto</span> <span class="nt">--right-of</span> <span class="s1">'eDP-*'</span>
<span class="o">}</span>
<span class="k">function </span>layout-clone <span class="o">{</span>
xrandr-smart <span class="nt">--output</span> <span class="s1">'eDP-*'</span> <span class="nt">--auto</span> <span class="se">\</span>
<span class="nt">--output</span> <span class="s1">'!(eDP-*)'</span> <span class="nt">--auto</span> <span class="nt">--same-as</span> <span class="s1">'eDP-*'</span>
<span class="o">}</span>
<span class="k">function </span>layout-extonly <span class="o">{</span>
xrandr-smart <span class="nt">--output</span> <span class="s1">'!(eDP-*)'</span> <span class="nt">--auto</span>
<span class="o">}</span>
</code></pre></div> </div>
<figcaption>functions/aliases I wish I could have</figcaption>
</figure>
<details id="toc">
<summary>Table of Contents</summary>
<ul id="markdown-toc">
<li><a href="#tldr" id="markdown-toc-tldr">TL;DR</a></li>
<li><a href="#what-why" id="markdown-toc-what-why">What? Why?</a></li>
<li><a href="#behind-the-scenes" id="markdown-toc-behind-the-scenes">Behind the scenes</a></li>
<li><a href="#limitations" id="markdown-toc-limitations">Limitations</a></li>
<li><a href="#future-work" id="markdown-toc-future-work">Future work</a></li>
</ul>
</details>
<h3 id="tldr">TL;DR</h3>
<p>Grab it here: <a href="https://github.com/liskin/dotfiles/tree/standalone/xrandr-smart">release
branch</a>,
<a href="https://github.com/liskin/dotfiles/archive/standalone/xrandr-smart.tar.gz">tarball</a>.</p>
<h3 id="what-why">What? Why?</h3>
<p>The way monitor layouts work for most people with mainstream operating systems
or desktop environments (like <a href="https://www.gnome.org/">GNOME</a>) is this: you connect an external
monitor for the first time, your desktop expands to this monitor, then you can
change its position or turn it off in the settings, and then this setup is
remembered so that you don’t need to do it again the next time you connect it.
People with non-mainstream <a href="https://en.wikipedia.org/wiki/X_window_manager">X11 window managers</a> (like <a href="https://xmonad.org/">xmonad</a>,
<a href="https://i3wm.org/">i3</a>, <a href="https://awesomewm.org/">awesomewm</a>, <a href="http://fluxbox.org/">fluxbox</a>) can get a similar experience: <a href="https://christian.amsuess.com/tools/arandr/">arandr</a>
being the “settings UI” and <a href="https://github.com/phillipberndt/autorandr">autorandr</a> handling the initial expansion,
saving and restoring.</p>
<p>Still, many people don’t know about these tools, and just use plain xrandr,
and then look a bit too nerdy when trying to connect to a projector at some
meetup or conference. I’ve got to admit I used to be one of them: I had a
script to handle external monitors at home/work, but connecting anything else
was so unusual that I didn’t bother writing a script, and had to do it
manually, and feel bad about myself afterwards.</p>
<p>This year I finally decided to do something about it. Not a big deal, right?
Just adopt autorandr and be done with it. But the script is massive (1500
lines of code including various workarounds for X11 and driver bugs that are
already fixed) and the format of its state/configuration files is
undocumented, and that’s a bit of a red flag as I like to keep this stuff
cleaned up and in <a href="https://en.wikipedia.org/wiki/Version_control">version control</a>. So I tried to think of something
simpler.</p>
<p>The simple solution I came up with is to extend xrandr to <strong>allow <a href="https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html">shell
globs</a> as output names</strong> and <strong>disable unspecified outputs</strong>.</p>
<p>So instead of having several scripts like</p>
<figure>
<figcaption><a href="https://github.com/liskin/dotfiles/blob/ca010423335ae885ff620e60ed37186b12354cc8/bin/layout-home-cz-hdmi">layout-home-cz-hdmi</a></figcaption>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>xrandr <span class="se">\</span>
<span class="nt">--output</span> HDMI-2 <span class="nt">--mode</span> 1920x1200 <span class="nt">--pos</span> 0x0 <span class="nt">--dpi</span> 96 <span class="se">\</span>
<span class="nt">--output</span> eDP-1 <span class="nt">--mode</span> 1920x1080 <span class="nt">--pos</span> 0x1200 <span class="nt">--dpi</span> 96 <span class="nt">--primary</span>
</code></pre></div> </div>
</figure>
<figure>
<figcaption><a href="https://github.com/liskin/dotfiles/blob/ca010423335ae885ff620e60ed37186b12354cc8/bin/layout-home-uk-dock">layout-home-uk-dock</a></figcaption>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>xrandr <span class="se">\</span>
<span class="nt">--output</span> DP-2-1 <span class="nt">--mode</span> 1920x1080 <span class="nt">--pos</span> 0x0 <span class="nt">--dpi</span> 96 <span class="se">\</span>
<span class="nt">--output</span> eDP-1 <span class="nt">--mode</span> 1920x1080 <span class="nt">--pos</span> 0x1080 <span class="nt">--dpi</span> 96 <span class="nt">--primary</span>
</code></pre></div> </div>
</figure>
<p>now I just have</p>
<figure>
<figcaption><a href="https://github.com/liskin/dotfiles/blob/74fed5fca5c2f414a588d2e02880c968eb224615/bin/layout-vertical">layout-vertical</a></figcaption>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>xrandr-smart <span class="se">\</span>
<span class="nt">--output</span> <span class="s1">'eDP-*'</span> <span class="nt">--auto</span> <span class="nt">--dpi</span> 96 <span class="nt">--primary</span> <span class="se">\</span>
<span class="nt">--output</span> <span class="s1">'!(eDP-*)'</span> <span class="nt">--auto</span> <span class="nt">--above</span> <span class="s1">'eDP-*'</span> <span class="nt">--dpi</span> 96
</code></pre></div> </div>
</figure>
<p>and I think that’s beautiful.</p>
<p>The source for the <code class="language-plaintext highlighter-rouge">xrandr-smart</code> script as described is <a href="https://github.com/liskin/dotfiles/blob/7a713c40892da5ed3eed162ad271ff5f90f76e9c/bin/xrandr-smart">in my dotfiles
monorepo</a>,
but the best way to obtain the most recent version of it is to use the
<a href="https://github.com/liskin/dotfiles/tree/standalone/xrandr-smart">standalone/xrandr-smart
branch</a>,
which can also be downloaded as <a href="https://github.com/liskin/dotfiles/archive/standalone/xrandr-smart.tar.gz">a
tarball</a>.</p>
<h3 id="behind-the-scenes">Behind the scenes</h3>
<p><a href="https://github.com/liskin/dotfiles/blob/7a713c40892da5ed3eed162ad271ff5f90f76e9c/bin/xrandr-smart"><code class="language-plaintext highlighter-rouge">xrandr-smart</code></a>
invokes the <a href="https://github.com/liskin/dotfiles/blob/7a713c40892da5ed3eed162ad271ff5f90f76e9c/bin/xrandr-smart#L76"><code class="language-plaintext highlighter-rouge">xrandr-auto-find</code>
function</a>
which resolves output globs (if the globs match nothing or more than one
output, it fails) and invokes the <a href="https://github.com/liskin/dotfiles/blob/7a713c40892da5ed3eed162ad271ff5f90f76e9c/bin/xrandr-smart#L51"><code class="language-plaintext highlighter-rouge">xrandr-auto-off</code>
function</a>
to disable all other unspecific outputs.</p>
<p>As an example, <code class="language-plaintext highlighter-rouge">layout-vertical</code> might translate to:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>xrandr-auto-off <span class="se">\</span>
<span class="nt">--output</span> eDP-1 <span class="nt">--auto</span> <span class="nt">--dpi</span> 96 <span class="nt">--primary</span> <span class="se">\</span>
<span class="nt">--output</span> HDMI-1 <span class="nt">--auto</span> <span class="nt">--above</span> eDP-1 <span class="nt">--dpi</span> 96
↓
xrandr <span class="se">\</span>
<span class="nt">--output</span> eDP-1 <span class="nt">--auto</span> <span class="nt">--dpi</span> 96 <span class="nt">--primary</span> <span class="se">\</span>
<span class="nt">--output</span> HDMI-1 <span class="nt">--auto</span> <span class="nt">--above</span> eDP-1 <span class="nt">--dpi</span> 96 <span class="se">\</span>
<span class="nt">--output</span> DP-1 <span class="nt">--off</span> <span class="se">\</span>
<span class="nt">--output</span> DP-2 <span class="nt">--off</span> <span class="se">\</span>
<span class="nt">--output</span> HDMI-2 <span class="nt">--off</span>
</code></pre></div></div>
<p>And that’s all there is to it, really.</p>
<h3 id="limitations">Limitations</h3>
<p>There are two significant limitations compared to <a href="https://www.gnome.org/">GNOME</a> and <a href="https://github.com/phillipberndt/autorandr">autorandr</a>:</p>
<ol>
<li>
<p>The generic <code class="language-plaintext highlighter-rouge">layout-horizontal</code>, <code class="language-plaintext highlighter-rouge">layout-vertical</code> scripts can only support
the laptop panel and one external monitor. In reality, this isn’t a problem
as triple head setups usual need <a href="https://github.com/liskin/dotfiles/blob/74fed5fca5c2f414a588d2e02880c968eb224615/bin/layout-work2-dock#L6-L8">fine-tuned
positioning</a>
anyway.</p>
</li>
<li>
<p>We need extra code to support layout saving and (possibly automatic)
restoring. Turns out that’s just a few lines:
<a href="https://github.com/liskin/dotfiles/blob/7a713c40892da5ed3eed162ad271ff5f90f76e9c/bin/layout-auto"><code class="language-plaintext highlighter-rouge">layout-auto</code></a>,
<a href="https://github.com/liskin/dotfiles/blob/7a713c40892da5ed3eed162ad271ff5f90f76e9c/.xmonad/xmonad.hs#L89-L90">keybindings</a><sup id="fnref:not-auto" role="doc-noteref"><a href="#fn:not-auto" class="footnote" rel="footnote">1</a></sup>.</p>
</li>
</ol>
<h3 id="future-work">Future work</h3>
<ul>
<li>We could additionally use <a href="https://manpages.debian.org/unstable/read-edid/parse-edid.1.en.html">parse-edid</a> to get monitor vendor and model and
match against that as well. This could make <code class="language-plaintext highlighter-rouge">xrandr-smart</code> useful even in
triple-head and non-laptop setups.</li>
</ul>
<hr />
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:not-auto" role="doc-endnote">
<p>Yeah, I invoke this manually using a Fn-key combo. There’s an endless
stream of bugs in the kernel, X server and GPU drivers, plus the
occassional security issue in a screensaver, so I feel safer to just
invoke it manually when I think everything is settled down. <a href="#fnref:not-auto" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>
font-weight: 300 considered harmful (and a fontconfig workaround)2020-07-18T00:00:00+00:00https://work.lisk.in/2020/07/18/font-weight-300<p>Many web pages these days set <code class="language-plaintext highlighter-rouge">font-weight: 300</code> in their stylesheet. With
<a href="https://dejavu-fonts.github.io/">DejaVu Sans</a> as my preferred font, this
results in very thin and light text that is hard to read, because for some
reason the “DejaVu Sans ExtraLight” variant (weight 200) is being used for
weights < 360 (in Chrome; in Firefox up to 399). Let’s investigate why this
happens and what can be done about it.</p>
<details id="toc">
<summary>Table of Contents</summary>
<ul id="markdown-toc">
<li><a href="#the-problem" id="markdown-toc-the-problem">The Problem</a></li>
<li><a href="#macos-font-smoothing-css" id="markdown-toc-macos-font-smoothing-css">MacOS font smoothing, CSS</a></li>
<li><a href="#linux-fontconfig-css" id="markdown-toc-linux-fontconfig-css">Linux, fontconfig, CSS</a></li>
<li><a href="#the-solution" id="markdown-toc-the-solution">The Solution</a></li>
<li><a href="#appendix-a-why-glob" id="markdown-toc-appendix-a-why-glob">Appendix A: Why glob?</a></li>
<li><a href="#appendix-b-reactions" id="markdown-toc-appendix-b-reactions">Appendix B: Reactions</a></li>
</ul>
</details>
<h3 id="the-problem">The Problem</h3>
<p>Here’s what <a href="/img/font-weight-300/test.html">a test page</a> looks like on my
laptop (14” 1920x1080):</p>
<figure class="no-resize">
<p><a href="/img/font-weight-300/test-linux-dejavu.png"><img src="/img/font-weight-300/test-linux-dejavu.png" alt="DejaVu Linux test" /></a></p>
<figcaption>DejaVu Sans at different font-weights</figcaption>
</figure>
<p>For comparison, and possibly also as a clue as to why web designers use
<code class="language-plaintext highlighter-rouge">font-weight: 300</code>, here’s a table of various font-weights of DejaVu Sans on
my system and the default sans-serif font on MacOS Catalina and Android
(unfortunately I don’t have any HiDPI laptop or low-DPI smartphone, so the
comparison might be imprecise/unfair):</p>
<figure class="no-resize">
<table style="background-color: #fff; color: #000">
<thead>
<tr>
<th></th>
<th>DejaVu</th>
<th>MacOS</th>
<th>Android</th>
</tr>
</thead>
<tr>
<th>400</th>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-linux-dejavu-400.png" /></td>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-macos-400.png" /></td>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-android-400.png" /></td>
</tr>
<tr>
<th>300</th>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-linux-dejavu-300.png" /></td>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-macos-300.png" /></td>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-android-300.png" /></td>
</tr>
<tr>
<th>200</th>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-linux-dejavu-200.png" /></td>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-macos-200.png" /></td>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-android-200.png" /></td>
</tr>
</table>
<figcaption>Boldness comparison<sup id="fnref:screenshots" role="doc-noteref"><a href="#fn:screenshots" class="footnote" rel="footnote">1</a></sup> (scaled to equal
height)</figcaption>
</figure>
<h3 id="macos-font-smoothing-css">MacOS font smoothing, CSS</h3>
<p>In MacOS, <code class="language-plaintext highlighter-rouge">font-weight: normal</code> looks almost bold, so web designers who use
MacOS/Safari might use <code class="language-plaintext highlighter-rouge">font-weight: 300</code> to <a href="https://news.ycombinator.com/item?id=23553486">compensate for this, ruining it
for everybody else</a>. :-(</p>
<p>Well, actually not everybody, as some desktop users (e.g. a Fedora Live DVD)
won’t have an extra-light variant of sans serif, so the normal (regular, or
book) variant will be used for all weights. But Android users and desktop
users with DejaVu (used to be default on most Linux distributions, not sure
what’s the current status) and possibly also Windows users are affected.</p>
<p><a href="https://tonsky.me/blog/monitors/#turn-off-font-smoothing">Nikita Prokopov suggested that disabling font smoothing in MacOS reduces the
boldness</a>, and my
experiments confirm that. Furthermore, subpixel smoothing
(antialiasing)<sup id="fnref:macos-subpixel" role="doc-noteref"><a href="#fn:macos-subpixel" class="footnote" rel="footnote">2</a></sup> comes somewhere in the middle between the
default and no smoothing (on my display).</p>
<figure class="no-resize">
<table style="background-color: #fff; color: #000">
<thead>
<tr>
<th></th>
<th>default</th>
<th>subpixel</th>
<th>no smooth</th>
<th>DejaVu</th>
</tr>
</thead>
<tr>
<th>400</th>
<td><img src="/img/font-weight-300/test-macos-400.png" /></td>
<td><img src="/img/font-weight-300/test-macos-subpixel-400.png" /></td>
<td><img src="/img/font-weight-300/test-macos-nosmooth-400.png" /></td>
<td><img src="/img/font-weight-300/test-linux-dejavu-400.png" /></td>
</tr>
<tr>
<th>300</th>
<td><img src="/img/font-weight-300/test-macos-300.png" /></td>
<td><img src="/img/font-weight-300/test-macos-subpixel-300.png" /></td>
<td><img src="/img/font-weight-300/test-macos-nosmooth-300.png" /></td>
<td><img src="/img/font-weight-300/test-linux-dejavu-300.png" /></td>
</tr>
<tr>
<th>200</th>
<td><img src="/img/font-weight-300/test-macos-200.png" /></td>
<td><img src="/img/font-weight-300/test-macos-subpixel-200.png" /></td>
<td><img src="/img/font-weight-300/test-macos-nosmooth-200.png" /></td>
<td><img src="/img/font-weight-300/test-linux-dejavu-200.png" /></td>
</tr>
</table>
<figcaption>Effect of disabling font smoothing in MacOS</figcaption>
</figure>
<figure class="no-resize">
<table style="background-color: #fff; color: #000">
<thead>
<tr>
<th></th>
<th>DejaVu</th>
<th>MacOS</th>
<th>Android</th>
</tr>
</thead>
<tr>
<th>400</th>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-linux-dejavu-400.png" /></td>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-macos-nosmooth-400.png" /></td>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-android-400.png" /></td>
</tr>
<tr>
<th>300</th>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-linux-dejavu-300.png" /></td>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-macos-nosmooth-300.png" /></td>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-android-300.png" /></td>
</tr>
<tr>
<th>200</th>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-linux-dejavu-200.png" /></td>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-macos-nosmooth-200.png" /></td>
<td><img style="object-fit: contain; height: 2ex" src="/img/font-weight-300/test-android-200.png" /></td>
</tr>
</table>
<figcaption>Boldness comparison, this time with no smoothing
in MacOS</figcaption>
</figure>
<p><del>Anyway, we can’t put all the blame on web designers. Matching an extra-light
font with <code class="language-plaintext highlighter-rouge">font-weight: 300</code> doesn’t seem to be a good idea, and matching it
with <code class="language-plaintext highlighter-rouge">font-weight: 350</code> is just plain silly (and I’d need to use explicit
language to describe my feelings about Firefox using an extra-light font for
<code class="language-plaintext highlighter-rouge">font-weight: 399</code>).</del></p>
<p>Actually, we can put all the blame on them, as <code class="language-plaintext highlighter-rouge">font-weight: 300</code> has always
(<a href="https://www.w3.org/TR/CSS1/#font-weight">even in CSS Level 1</a>) meant
“lighter than normal, even if the only lighter font is weight 100.” Firefox’s
behaviour of selecting an extra-light font for <code class="language-plaintext highlighter-rouge">font-weight: 399</code> is in fact
<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#fallback_weights">conforming to the most recent draft specification</a>.</p>
<p class="mark">MacOS’ somewhat bolder rendering of normal-weight fonts is therefore a
<em>very</em> weak excuse for using <code class="language-plaintext highlighter-rouge">font-weight: 300</code>, which literally forces the
browser to not use a normal-weight font (or bolder) unless there is no other
font available.</p>
<p>With that out of the way, let’s finally proceed to <del>fix</del> work around the
problem, since persuading thousands of web developers to fix their websites
doesn’t seem feasible at this point.</p>
<h3 id="linux-fontconfig-css">Linux, fontconfig, CSS</h3>
<p>Font selection and appearance in Linux is
<a href="https://wiki.archlinux.org/index.php/Font_configuration">highly</a>
<a href="https://wiki.archlinux.org/index.php/Font_configuration/Examples">configurable</a>
via <a href="https://www.freedesktop.org/software/fontconfig/fontconfig-user.html">fontconfig</a>. That is both a curse and a blessing. In this case, it is
quite advantageous.</p>
<p>There are a few handy command-line utilities which make it really easy to test
the configuration. I’ll use <a href="https://linux.die.net/man/1/fc-list">fc-list</a> and <a href="https://linux.die.net/man/1/fc-match">fc-match</a> here to see what
fonts I have and when DejaVu Sans ExtraLight is used:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ fc-list | grep -F -w 'DejaVu Sans' | sort
/usr/share/fonts/truetype/dejavu/DejaVuSans-BoldOblique.ttf: DejaVu Sans:style=Bold Oblique
/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf: DejaVu Sans:style=Bold
/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed-BoldOblique.ttf: DejaVu Sans,DejaVu Sans Condensed:style=Condensed Bold Oblique,Bold Oblique
/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed-Bold.ttf: DejaVu Sans,DejaVu Sans Condensed:style=Condensed Bold,Bold
/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed-Oblique.ttf: DejaVu Sans,DejaVu Sans Condensed:style=Condensed Oblique,Oblique
/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed.ttf: DejaVu Sans,DejaVu Sans Condensed:style=Condensed,Book
/usr/share/fonts/truetype/dejavu/DejaVuSans-ExtraLight.ttf: DejaVu Sans,DejaVu Sans Light:style=ExtraLight
/usr/share/fonts/truetype/dejavu/DejaVuSansMono-BoldOblique.ttf: DejaVu Sans Mono:style=Bold Oblique
/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf: DejaVu Sans Mono:style=Bold
/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Oblique.ttf: DejaVu Sans Mono:style=Oblique
/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf: DejaVu Sans Mono:style=Book
/usr/share/fonts/truetype/dejavu/DejaVuSans-Oblique.ttf: DejaVu Sans:style=Oblique
/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf: DejaVu Sans:style=Book
/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans-Bold.ttf: DejaVu Sans:style=Bold
/usr/share/fonts/truetype/ttf-dejavu/DejaVuSansMono-Bold.ttf: DejaVu Sans Mono:style=Bold
/usr/share/fonts/truetype/ttf-dejavu/DejaVuSansMono.ttf: DejaVu Sans Mono:style=Book
/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf: DejaVu Sans:style=Book
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ fc-match -v sans \
| grep -F -w -e style: -e weight: -e fullname:
style: "Book"(s)
fullname: "DejaVu Sans"(s)
weight: 80(f)(s)
$ fc-match -v sans:weight=extralight \
| grep -F -w -e style: -e weight: -e fullname:
style: "ExtraLight"(s)
fullname: "DejaVu Sans ExtraLight"(s)
weight: 40(f)(s)
$ fc-match -v sans:weight=60 | grep -F -w -e weight:
weight: 40(f)(s)
$ fc-match -v sans:weight=61 | grep -F -w -e weight:
weight: 80(f)(s)
$ fc-match -v sans:weight=139 | grep -F -w -e weight:
weight: 80(f)(s)
$ fc-match -v sans:weight=140 | grep -F -w -e weight:
weight: 200(f)(s)
</code></pre></div></div>
<p>Fontconfig defines these symbolic font weights:</p>
<figure class="no-resize">
<table>
<thead>
<tr>
<th>constant</th>
<th style="text-align: right">value</th>
</tr>
</thead>
<tbody>
<tr>
<td>thin</td>
<td style="text-align: right">0</td>
</tr>
<tr>
<td>extralight</td>
<td style="text-align: right">40</td>
</tr>
<tr>
<td>ultralight</td>
<td style="text-align: right">40</td>
</tr>
<tr>
<td>light</td>
<td style="text-align: right">50</td>
</tr>
<tr>
<td>demilight</td>
<td style="text-align: right">55</td>
</tr>
<tr>
<td>semilight</td>
<td style="text-align: right">55</td>
</tr>
<tr>
<td>book</td>
<td style="text-align: right">75</td>
</tr>
<tr>
<td>regular</td>
<td style="text-align: right">80</td>
</tr>
<tr>
<td>normal</td>
<td style="text-align: right">80</td>
</tr>
<tr>
<td>medium</td>
<td style="text-align: right">100</td>
</tr>
<tr>
<td>demibold</td>
<td style="text-align: right">180</td>
</tr>
<tr>
<td>semibold</td>
<td style="text-align: right">180</td>
</tr>
<tr>
<td>bold</td>
<td style="text-align: right">200</td>
</tr>
<tr>
<td>extrabold</td>
<td style="text-align: right">205</td>
</tr>
<tr>
<td>black</td>
<td style="text-align: right">210</td>
</tr>
<tr>
<td>heavy</td>
<td style="text-align: right">210</td>
</tr>
</tbody>
</table>
<figcaption>Fontconfig weight constants</figcaption>
</figure>
<p>Apparently fontconfig selects the font with the closest weight requested.
That’s not what <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#fallback_weights">CSS needs</a>, so browsers probably don’t use
fontconfig font patterns and therefore <a href="https://old.reddit.com/r/linuxquestions/comments/a4h90n/using_fontconfig_to_block_a_problematic_font/">the usual fontconfig ways of avoiding
the extra-light font don’t
work.</a></p>
<p>But wait. Actually, some browsers do. The <a href="https://surf.suckless.org/">surf</a> browser, built using
<a href="https://webkitgtk.org/">WebKitGTK</a>, translates <code class="language-plaintext highlighter-rouge">font-weigth: 300</code> to fontconfig weight 50,
<code class="language-plaintext highlighter-rouge">font-weight: 200</code> to fontconfig weight 40 and <code class="language-plaintext highlighter-rouge">font-weight: 100</code> to
fontconfig weight 0, which is a correct mapping, but it won’t result in
correct behaviour if only font weights 0 and 80 are available, as 80 is closer
to 60, but CSS mandates that 0 is chosen. (To find this out, I used
<code class="language-plaintext highlighter-rouge">FC_DEBUG=1 surf</code>.) Indeed, the fontconfig configuration suggested in the link
above is a sufficient workaround for the <a href="https://webkitgtk.org/">WebKitGTK</a> browser:</p>
<figure>
<figcaption>surf before</figcaption>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ FC_DEBUG=1 surf test.html |& grep -F -w -c ExtraLight
7
</code></pre></div> </div>
</figure>
<figure>
<figcaption>~/.config/fontconfig/fonts.conf</figcaption>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><?xml version="1.0"?></span>
<span class="cp"><!DOCTYPE fontconfig SYSTEM "fonts.dtd"></span>
<span class="nt"><fontconfig></span>
<span class="nt"><match</span> <span class="na">target=</span><span class="s">"pattern"</span><span class="nt">></span>
<span class="nt"><test</span> <span class="na">qual=</span><span class="s">"any"</span> <span class="na">name=</span><span class="s">"family"</span><span class="nt">></span>
<span class="nt"><string></span>DejaVu Sans<span class="nt"></string></span>
<span class="nt"></test></span>
<span class="nt"><test</span> <span class="na">name=</span><span class="s">"weight"</span> <span class="na">compare=</span><span class="s">"less"</span><span class="nt">></span>
<span class="nt"><const></span>book<span class="nt"></const></span>
<span class="nt"></test></span>
<span class="nt"><edit</span> <span class="na">name=</span><span class="s">"weight"</span> <span class="na">mode=</span><span class="s">"assign"</span> <span class="na">binding=</span><span class="s">"same"</span><span class="nt">></span>
<span class="nt"><const></span>book<span class="nt"></const></span>
<span class="nt"></edit></span>
<span class="nt"></match></span>
<span class="nt"></fontconfig></span>
</code></pre></div> </div>
</figure>
<figure>
<figcaption>surf after</figcaption>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ FC_DEBUG=1 surf test.html |& grep -F -w -c ExtraLight
0
</code></pre></div> </div>
</figure>
<p>In a real CSS-conforming browser, this won’t work as fontconfig is
presumably only used to list available fonts, and the font matching algorithm
then runs in the browser engine itself. One might also desperately attempt to
use fontconfig’s <code class="language-xml highlight language-xml highlighter-rouge"><span class="nt"><match</span> <span class="na">target=</span><span class="s">"scan"</span><span class="nt">></span></code> to lower
the weight of the font to 0 and hope the browser will select the nearer,
normal variant. Or at least I did desperately try that. That won’t work,
either:</p>
<ol>
<li>
<p>CSS still prefers a weight 0 font for <code class="language-plaintext highlighter-rouge">font-weight: 300</code> when both weight 0
and weight 400 are available.</p>
</li>
<li>
<p><code class="language-xml highlight language-xml highlighter-rouge"><span class="nt"><match</span> <span class="na">target=</span><span class="s">"scan"</span><span class="nt">></span></code> needs to be applied
system-wide and fontconfig caches then need to be regenerated using
<a href="https://linux.die.net/man/1/fc-cache">fc-cache</a> by root, as apparently the system-wide caches are preferred.
Therefore it’s also impossible to apply this rule to a web browser only.</p>
</li>
</ol>
<p>There is still one option left, fortunately: <code class="language-xml highlight language-xml highlighter-rouge"><span class="nt"><selectfont></span></code>, which controls the set of available fonts. Its documentation
is quite high-level and in some aspects downright incorrect, but by reading
<a href="https://gitlab.freedesktop.org/fontconfig/fontconfig/-/blob/437f03299bd1adc9673cd576072f1657be8fd4e0/src/fccfg.c#L461-478">the source</a>
we can conclude that it works like this:</p>
<ol>
<li>
<p>First, check if the filename is explicitly accepted by any <code class="language-xml highlight language-xml highlighter-rouge"><span class="nt"><glob></span></code>. If it isn’t, then check whether it’s rejected,
and only if it’s not accepted but it is explicitly rejected, skip the font.
Otherwise continue.</p>
<p>(The documentation claims that <code class="language-xml highlight language-xml highlighter-rouge"><span class="nt"><glob></span></code> only
filters directories, but this is fortunately not true.)</p>
</li>
<li>
<p>Then, similarly, check if the font matches any accept <code class="language-xml highlight language-xml highlighter-rouge"><span class="nt"><pattern></span></code> (these may test various font properties). If
not, check reject patterns, and skip the font if rejected and not accepted.
Otherwise continue and allow the font to be used.</p>
</li>
<li>
<p>Order of configuration directives doesn’t matter, it’s just being added to
glob/pattern accept/reject lists as the configuration is read.</p>
</li>
</ol>
<h3 id="the-solution">The Solution</h3>
<p>Fontconfig’s <code class="language-xml highlight language-xml highlighter-rouge"><span class="nt"><selectfont></span></code> lets us hide DejaVu
Sans ExtraLight from the browser. If we want to keep the font available for
other applications (if we don’t, then it might be easier to just uninstall
it), let’s create a browser-specific fontconfig conf:</p>
<figure>
<figcaption>~/.config/fontconfig/browser.conf</figcaption>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><?xml version="1.0"?></span>
<span class="cp"><!DOCTYPE fontconfig SYSTEM "fonts.dtd"></span>
<span class="nt"><fontconfig></span>
<span class="nt"><include></span>fonts.conf<span class="nt"></include></span>
<span class="c"><!-- disable DejaVu Sans ExtraLight, it tends to match font-weight: 300 --></span>
<span class="nt"><selectfont></span>
<span class="nt"><rejectfont></span>
<span class="nt"><glob></span>*/DejaVuSans-ExtraLight.ttf<span class="nt"></glob></span>
<span class="nt"></rejectfont></span>
<span class="nt"></selectfont></span>
<span class="nt"></fontconfig></span>
</code></pre></div> </div>
</figure>
<p>When we now set the <code class="language-plaintext highlighter-rouge">FONTCONFIG_FILE=~/.config/fontconfig/browser.conf</code>
environment variable, DejaVu Sans ExtraLight is nowhere to be seen:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ FONTCONFIG_FILE=~/.config/fontconfig/browser.conf \
fc-match -v sans:weight=40 | grep -F -w -e weight:
weight: 80(f)(s)
$ FONTCONFIG_FILE=~/.config/fontconfig/browser.conf \
fc-list | grep -F -w -c ExtraLight
0
</code></pre></div></div>
<p>Setting <code class="language-plaintext highlighter-rouge">FONTCONFIG_FILE=~/.config/fontconfig/browser.conf</code> for the browser is
left as an exercise to the reader.</p>
<h3 id="appendix-a-why-glob">Appendix A: Why glob?</h3>
<p>An observant reader might have noticed that the solution could be made more
robust by using <code class="language-xml highlight language-xml highlighter-rouge"><span class="nt"><pattern></span></code> instead of <code class="language-xml highlight language-xml highlighter-rouge"><span class="nt"><glob></span></code> and matching on the font weight, thus disabling all
light fonts. This is probably correct, but not usable in my case, as I already
use accept patterns to <a href="https://github.com/liskin/dotfiles/blob/3d30f7f25b6a30ec8216ed370efd88fb35f6f080/.config/fontconfig/browser.conf#L6-L62">limit the available fonts to a few reasonable
ones</a> to prevent web designers from selecting hard to read font
faces. With the advent of web fonts, this workaround has become less effective
lately. :-(</p>
<h3 id="appendix-b-reactions">Appendix B: Reactions</h3>
<ul>
<li><a href="https://css-tricks.com/font-weight-300-considered-harmful/">css-tricks.com: “font-weight: 300 considered”
harmful</a> by
<a href="https://twitter.com/chriscoyier">Chris Coyier</a>, author of <a href="https://css-tricks.com/snippets/css/better-helvetica/">“Better
Helvetica”</a></li>
</ul>
<hr />
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:screenshots" role="doc-endnote">
<p>Screenshots:</p>
<p><a href="/img/font-weight-300/test-linux-dejavu.png">DejaVu Sans on my system</a><br />
<a href="/img/font-weight-300/test-macos.png">MacOS Catalina</a><br />
<a href="/img/font-weight-300/test-macos-subpixel.png">MacOS Catalina + subpixel antialiasing</a><br />
<a href="/img/font-weight-300/test-macos-nosmooth.png">MacOS Catalina + disabled font smoothing</a><br />
<a href="/img/font-weight-300/test-android.png">Android Samsung S10e</a><br />
<a href="/img/font-weight-300/test-android-i9300.png">Android Samsung S3</a><br />
<!-- --> <a href="#fnref:screenshots" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:macos-subpixel" role="doc-endnote">
<p>Subpixel antialiasing is <a href="https://apple.stackexchange.com/questions/337870/how-to-turn-subpixel-antialiasing-on-in-macos-10-14">disabled since
Mojave</a>,
possibly because it’s not necessary with HiDPI/Retina displays and
dropping it reduces code complexity considerably. <a href="#fnref:macos-subpixel" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>
Linux, media keys and multiple players (mpd, chromium, mpv, vlc, …)2020-05-06T00:00:00+00:00https://work.lisk.in/2020/05/06/linux-media-control<p>This post explains how to get <a href="https://en.wikipedia.org/wiki/Computer_keyboard#Miscellaneous">media keys</a> (play, pause, …) on <a href="https://en.wikipedia.org/wiki/ThinkPad#/media/File:Lenovo-ThinkPad-Keyboard.JPG">keyboards</a>
and Bluetooth headphones work with a bare <a href="https://en.wikipedia.org/wiki/X_window_manager">X window manager</a> (as opposed to
a full <a href="https://en.wikipedia.org/wiki/Desktop_environment#Desktop_environments_for_the_X_Window_System">desktop environment</a>) and how to make them control multiple media
players including the web browser (YouTube, <a href="https://bandcamp.com/">bandcamp</a>, <a href="https://mynoise.net/">myNoise</a>, etc.)
which is something that even majority <a href="#windows-10">operating systems</a> and
<a href="#gnome-popular-linux-desktop-environment">desktop environments</a> don’t quite get right out of the box.</p>
<figure>
<p><a href="https://user-images.githubusercontent.com/300342/81182740-8663f280-8fae-11ea-9b0d-db91eb6febaf.jpg"><img src="https://user-images.githubusercontent.com/300342/81182729-8237d500-8fae-11ea-89eb-81ca7b3598fe.jpg" alt="media-keys" /></a></p>
<figcaption>ThinkPad 25 media keys</figcaption>
</figure>
<details id="toc">
<summary>Table of Contents</summary>
<ul id="markdown-toc">
<li><a href="#goal" id="markdown-toc-goal">Goal</a></li>
<li><a href="#solution" id="markdown-toc-solution">Solution</a></li>
<li><a href="#state-of-the-art" id="markdown-toc-state-of-the-art">State of the art</a> <ul>
<li><a href="#gnome-popular-linux-desktop-environment" id="markdown-toc-gnome-popular-linux-desktop-environment">GNOME (popular Linux desktop environment)</a> <ul>
<li><a href="#update-2021-03-10-workaround-using-playerctld" id="markdown-toc-update-2021-03-10-workaround-using-playerctld">Update 2021-03-10: workaround using playerctld</a></li>
<li><a href="#update-2023-01-03-works-out-of-the-box-now" id="markdown-toc-update-2023-01-03-works-out-of-the-box-now">Update 2023-01-03: works out of the box now</a></li>
</ul>
</li>
<li><a href="#kde-plasma-5" id="markdown-toc-kde-plasma-5">KDE Plasma 5</a></li>
<li><a href="#windows-10" id="markdown-toc-windows-10">Windows 10</a></li>
<li><a href="#macos-catalina" id="markdown-toc-macos-catalina">macOS Catalina</a></li>
<li><a href="#android-10-samsung-one-ui-21" id="markdown-toc-android-10-samsung-one-ui-21">Android 10 (Samsung One UI 2.1)</a></li>
<li><a href="#summary" id="markdown-toc-summary">Summary</a></li>
</ul>
</li>
</ul>
</details>
<h3 id="goal">Goal</h3>
<p>My use cases:</p>
<ul>
<li>listening to music/myNoise while working (near computer) or reading (away
from computer)</li>
<li>listening to podcasts while cooking/cleaning (away from computer, screen
locked, wet hands)</li>
<li>discovering new music on YouTube, bandcamp, soundcloud, …</li>
<li>listening off-line (that why I use <a href="https://www.musicpd.org/">mpd</a> and buy music on <a href="https://bandcamp.com/">bandcamp</a>)</li>
</ul>
<p>I’d love to use the same play/pause key/button in all of these scenarios, and
this key/button should control the appropriate application (pause the one
that’s playing, play the last paused one, …). It’s annoying, bad <abbr title="user experience">UX</abbr> otherwise.</p>
<p>I don’t want to think how to pause music when someone needs me in the
<abbr title="Deriving from cyberpunk novels, meatspace is the world outside of the 'net-- that is to say, the real world, where you do things with your body rather than with your keyboard.">meatspace</abbr>. Walking back from the kitchen to pause a podcast is just plain
silly. Playing YouTube, bandcamp, soundcloud etc. <a href="https://github.com/liskin/dotfiles/blob/15c2cd83ce7297c38830053a9fd2be2f3678f4b0/bin/youtube-dl-mpd">using mpd is
possible</a> and it’s what I used to do back when my play/pause
buttons were hardwired to mpd, but it’s a hack and likely against the <abbr title="Terms of Service">ToS</abbr>.</p>
<h3 id="solution">Solution</h3>
<p>There is a standard <a href="https://www.freedesktop.org/wiki/Software/dbus/">D-Bus</a> interface for controlling media players on a
modern Linux desktop: <a href="https://www.freedesktop.org/wiki/Specifications/mpris-spec/">MPRIS</a>. It seems to be supported by both
<a href="https://github.com/chromium/chromium/tree/0944c7716afc8b3d8fe2236db79866d4c8a57b6f/components/system_media_controls/linux">Chromium</a> and <a href="https://github.com/mozilla/gecko-dev/blob/5470b66539234e52e76bc2176d9bec12325fc555/widget/gtk/MPRISServiceHandler.cpp">Firefox</a> these days, and there’s
a command line tool <a href="https://github.com/altdesktop/playerctl">playerctl</a> as well, so we just need to write a few
scripts to wire it all together and everything should just work.</p>
<p>After some hacking, my setup looks like this (all the icons and some of the
arrows are clickable):</p>
<figure class="transparent-bg-light">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="597" height="292" viewBox="-0.5 -0.5 597 292"><defs><filter id="dropShadow"><feGaussianBlur in="SourceAlpha" stdDeviation="1.7" result="blur" /><feOffset in="blur" dx="3" dy="3" result="offsetBlur" /><feFlood flood-color="#3D4574" flood-opacity="0.4" result="offsetColor" /><feComposite in="offsetColor" in2="offsetBlur" operator="in" result="offsetBlur" /><feBlend in="SourceGraphic" in2="offsetBlur" /></filter></defs><g filter="url(#dropShadow)"><a xlink:href="https://github.com/liskin/dotfiles/blob/15c2cd83ce7297c38830053a9fd2be2f3678f4b0/bin/liskin-xsecurelock#L11-L20"><path d="M 64.5 180.85 L 84.27 160.42" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke" /><path d="M 88.45 156.11 L 85.76 164.64 L 84.27 160.42 L 80.01 159.07 Z" fill="#000000" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="all" /></a><a xlink:href="https://github.com/liskin/dotfiles/blob/15c2cd83ce7297c38830053a9fd2be2f3678f4b0/.xmonad/xmonad.hs#L73-L81"><path d="M 64.31 97.24 L 84.46 119.41" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke" /><path d="M 88.5 123.85 L 80.15 120.62 L 84.46 119.41 L 86.07 115.24 Z" fill="#000000" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="all" /></a><a xlink:href="https://github.com/liskin/dotfiles/blob/15c2cd83ce7297c38830053a9fd2be2f3678f4b0/bin/liskin-media#L93-L100"><path d="M 148 140 L 181.76 140" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke" /><path d="M 187.76 140 L 179.76 144 L 181.76 140 L 179.76 136 Z" fill="#000000" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="all" /></a><path d="M 256.24 140 L 281.76 140" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke" /><path d="M 250.24 140 L 258.24 136 L 256.24 140 L 258.24 144 Z" fill="#000000" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="all" /><path d="M 287.76 140 L 279.76 144 L 281.76 140 L 279.76 136 Z" fill="#000000" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="all" /><path d="M 414.9 73.46 L 395.1 98.54" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke" /><path d="M 418.61 68.75 L 416.8 77.51 L 414.9 73.46 L 410.52 72.56 Z" fill="#000000" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="all" /><path d="M 391.39 103.25 L 393.2 94.49 L 395.1 98.54 L 399.48 99.44 Z" fill="#000000" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="all" /><path d="M 419.26 205.09 L 395.74 180.91" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke" /><path d="M 423.44 209.4 L 415 206.45 L 419.26 205.09 L 420.73 200.87 Z" fill="#000000" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="all" /><path d="M 391.56 176.6 L 400 179.55 L 395.74 180.91 L 394.27 185.13 Z" fill="#000000" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="all" /><a xlink:href="https://github.com/chromium/chromium/tree/0944c7716afc8b3d8fe2236db79866d4c8a57b6f/components/system_media_controls/linux"><path d="M 457.71 134.5 L 428.24 134.5" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke" /><path d="M 463.71 134.5 L 455.71 138.5 L 457.71 134.5 L 455.71 130.5 Z" fill="#000000" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="all" /><path d="M 422.24 134.5 L 430.24 130.5 L 428.24 134.5 L 430.24 138.5 Z" fill="#000000" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="all" /></a><a xlink:href="https://github.com/liskin/dotfiles/blob/15c2cd83ce7297c38830053a9fd2be2f3678f4b0/bin/liskin-media#L46-L54"><path d="M 219 111 L 219 101 Q 219 91 209 91 L 143.5 91 Q 133.5 91 133.5 96.88 L 133.5 102.76" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke" /><path d="M 133.5 108.76 L 129.5 100.76 L 133.5 102.76 L 137.5 100.76 Z" fill="#000000" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="all" /></a><a xlink:href="https://github.com/liskin/dotfiles/blob/15c2cd83ce7297c38830053a9fd2be2f3678f4b0/bin/liskin-media#L46-L54"><rect x="140" y="70" width="70" height="20" fill="none" stroke="none" pointer-events="all" /><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 80px; margin-left: 175px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: sans-serif; color: #000000; line-height: 1.2; pointer-events: all; font-style: italic; white-space: nowrap; ">daemon</div></div></div></foreignObject><text x="175" y="84" fill="#000000" font-family="sans-serif" font-size="12px" text-anchor="middle" font-style="italic">daemon</text></switch></g></a><a xlink:href="https://www.freedesktop.org/wiki/Specifications/mpris-spec/"><rect x="300" y="195" width="100" height="20" fill="none" stroke="none" pointer-events="all" /><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 205px; margin-left: 350px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: sans-serif; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">dbus (MPRIS)</div></div></div></foreignObject><text x="350" y="209" fill="#000000" font-family="sans-serif" font-size="12px" text-anchor="middle">dbus (MPRIS)</text></switch></g></a><a xlink:href="https://www.freedesktop.org/wiki/Specifications/mpris-spec/"><image x="288.5" y="93.5" width="122" height="92" xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHdpZHRoPSIxMjJweCIgaGVpZ2h0PSI5MnB4IiB2aWV3Qm94PSItMC41IC0wLjUgMTIyIDkyIj48ZGVmcy8+PGc+PHJlY3QgeD0iNDgiIHk9IjEiIHdpZHRoPSIyNiIgaGVpZ2h0PSIxOCIgZmlsbD0iI2ZmZmJjMCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjIiIHBvaW50ZXItZXZlbnRzPSJhbGwiLz48cmVjdCB4PSIxIiB5PSIxOSIgd2lkdGg9IjI2IiBoZWlnaHQ9IjE4IiBmaWxsPSIjZmZmYmMwIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMiIgcG9pbnRlci1ldmVudHM9ImFsbCIvPjxyZWN0IHg9Ijk1IiB5PSIxOSIgd2lkdGg9IjI2IiBoZWlnaHQ9IjE4IiBmaWxsPSIjZmZmYmMwIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMiIgcG9pbnRlci1ldmVudHM9ImFsbCIvPjxyZWN0IHg9IjEiIHk9IjU1IiB3aWR0aD0iMjYiIGhlaWdodD0iMTgiIGZpbGw9IiNmZmZiYzAiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIyIiBwb2ludGVyLWV2ZW50cz0iYWxsIi8+PHJlY3QgeD0iOTUiIHk9IjU1IiB3aWR0aD0iMjYiIGhlaWdodD0iMTgiIGZpbGw9IiNmZmZiYzAiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIyIiBwb2ludGVyLWV2ZW50cz0iYWxsIi8+PHJlY3QgeD0iNDgiIHk9IjczIiB3aWR0aD0iMjYiIGhlaWdodD0iMTgiIGZpbGw9IiNmZmZiYzAiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIyIiBwb2ludGVyLWV2ZW50cz0iYWxsIi8+PHJlY3QgeD0iNDgiIHk9IjM3IiB3aWR0aD0iMjYiIGhlaWdodD0iMTgiIGZpbGw9IiNjMGY1YTkiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLXdpZHRoPSIyIiBwb2ludGVyLWV2ZW50cz0iYWxsIi8+PHBhdGggZD0iTSA2MSAzNyBMIDYxIDE5IiBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBwb2ludGVyLWV2ZW50cz0ic3Ryb2tlIi8+PHBhdGggZD0iTSA0OCA0MS4wMiBMIDI3IDMyLjk4IiBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBwb2ludGVyLWV2ZW50cz0ic3Ryb2tlIi8+PHBhdGggZD0iTSA3NCA0MS4wMiBMIDk1IDMyLjk4IiBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBwb2ludGVyLWV2ZW50cz0ic3Ryb2tlIi8+PHBhdGggZD0iTSA0OCA1MC45OCBMIDI3IDU5LjAyIiBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBwb2ludGVyLWV2ZW50cz0ic3Ryb2tlIi8+PHBhdGggZD0iTSA3NCA1MC45OCBMIDk1IDU5LjAyIiBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBwb2ludGVyLWV2ZW50cz0ic3Ryb2tlIi8+PHBhdGggZD0iTSA2MSA1NSBMIDYxIDczIiBmaWxsPSJub25lIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBwb2ludGVyLWV2ZW50cz0ic3Ryb2tlIi8+PC9nPjwvc3ZnPg==" preserveAspectRatio="none" /></a><a xlink:href="https://github.com/altdesktop/playerctl"><image x="189.5" y="110.5" width="58" height="58" xlink:href="data:image/png;base64,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" preserveAspectRatio="none" /></a><a xlink:href="https://github.com/altdesktop/playerctl"><rect x="190" y="171" width="70" height="20" fill="none" stroke="none" pointer-events="all" /><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 181px; margin-left: 225px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: sans-serif; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">playerctl</div></div></div></foreignObject><text x="225" y="185" fill="#000000" font-family="sans-serif" font-size="12px" text-anchor="middle">playerctl</text></switch></g></a><a xlink:href="https://github.com/liskin/dotfiles/blob/15c2cd83ce7297c38830053a9fd2be2f3678f4b0/bin/liskin-media"><image x="89.5" y="110.5" width="58" height="58" xlink:href="data:image/png;base64,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" preserveAspectRatio="none" /></a><a xlink:href="https://github.com/liskin/dotfiles/blob/15c2cd83ce7297c38830053a9fd2be2f3678f4b0/bin/liskin-media"><rect x="80" y="171" width="90" height="20" fill="none" stroke="none" pointer-events="all" /><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 181px; margin-left: 125px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: sans-serif; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">liskin-media</div></div></div></foreignObject><text x="125" y="185" fill="#000000" font-family="sans-serif" font-size="12px" text-anchor="middle">liskin-media</text></switch></g></a><a xlink:href="https://github.com/google/xsecurelock"><rect x="25.5" y="176" width="39" height="50" fill="none" stroke="none" pointer-events="all" /><path d="M 27.82 226 C 26.69 225.64 25.82 224.74 25.5 223.61 L 25.5 198.51 C 25.77 197.32 26.65 196.36 27.82 195.97 L 30.7 195.97 L 30.7 189.31 C 30.72 185.79 32.2 182.42 34.8 180 C 37.4 177.58 40.9 176.31 44.47 176.49 C 48.22 176 52 177.13 54.84 179.59 C 57.68 182.04 59.31 185.59 59.3 189.31 L 59.3 195.97 L 61.78 195.97 C 63.1 196.24 64.16 197.22 64.5 198.51 L 64.5 223.61 C 64.16 224.78 63.22 225.68 62.03 226 Z M 46.94 221.08 L 46.94 210.69 C 48.79 209.82 49.78 207.8 49.33 205.83 C 48.88 203.86 47.1 202.46 45.05 202.46 C 43 202.46 41.22 203.86 40.77 205.83 C 40.32 207.8 41.31 209.82 43.16 210.69 L 43.16 221.08 Z M 54.61 195.97 L 54.61 189.06 C 54.21 184.2 49.92 180.55 44.97 180.86 C 40.07 180.6 35.84 184.24 35.44 189.06 L 35.44 195.97 Z" fill="#00188d" stroke="none" pointer-events="all" /></a><a xlink:href="https://github.com/google/xsecurelock"><rect x="0" y="236" width="90" height="20" fill="none" stroke="none" pointer-events="all" /><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 246px; margin-left: 45px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: sans-serif; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">xsecurelock</div></div></div></foreignObject><text x="45" y="250" fill="#000000" font-family="sans-serif" font-size="12px" text-anchor="middle">xsecurelock</text></switch></g></a><a xlink:href="https://xmonad.org/"><image x="25.18" y="45.5" width="38.63" height="60" xlink:href="data:image/png;base64,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" preserveAspectRatio="none" /></a><a xlink:href="https://xmonad.org/"><rect x="15" y="116" width="60" height="20" fill="none" stroke="none" pointer-events="all" /><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 126px; margin-left: 45px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: sans-serif; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">xmonad</div></div></div></foreignObject><text x="45" y="130" fill="#000000" font-family="sans-serif" font-size="12px" text-anchor="middle">xmonad</text></switch></g></a><a xlink:href="https://github.com/CDrummond/cantata"><image x="419.5" y="2.5" width="64" height="64" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAANn0lEQVR4Xs1bCVSVZRp+vntBDFBTlHFJETc0SdMxW2xyRS2XnDFyycu9F1wSxUwdKZcUFU09rhg2OMEPiKmklZq5nrQaG5dxKsUUNQG1TBIrRFnvN+f98Rr33+5/4acz7zmcw7nfu3zf+y3v+jPUMlgslk5ms7k3gFDOeQhjrAOAxgB8JaLvAShgjGVzzi8AOOdwOI6mpaWdAcBra5rMaMbh4eFmX1/f/oyxsQAGAmhWQxn5AA5zzt/38fH5NCkpqayG/FzIDVOAxWIJNJvNrwGwAWhu5CSr8MrnnG82mUxrUlJSrhoho8YKiIyMbM45j+Wcj1c41kbMUYlHKec8zeFwLE1PT79SEyHVVsCCBQu8cnNzpwBYBKB+TSZRA9piAMsBvC0IAv3vMVRLAVFRUaEVFRWbAXT1WGLtEFxkjFlSUlKOe8reYwVYrdYIxlgiAD+9wpo2bYqOHTuiVatWoP8DAgLg5+eHunXriiyKi4tRVFSEW7du4caNG8jLy8P58+fF//UC57ycMRYfFBS0KC4uzqGXTrcC6HX38/N7B8AkPcxbtmyJXr16oWfPnmjYsKEeEhlOQUEBTpw4gWPHjuHqVX1vHmNsu7+/f0RCQkKJHqG6FBAeHl7Hz8+Pjny4FlPGGLp06YKhQ4eiXbt2euTrxrl48SL27NmDM2fOgHO3bsGRsrKyFzMyMn5zJ8CtAmJiYnwKCwt3AwjTYta6dWtYLBa0adPGncwajV++fBnp6enIzc11x+ekyWTqn5ycXKi5aVqDCxYsMOXl5b3POX9ZDc/b2xvh4eHo378/TCaTu0kZMu5wOHDo0CFkZmaivLxclSdj7DN/f//nta6D5gmw2+3rOOfT1CQEBgYiOjoaQUFBNV5YUXEZfLzN8DLrVyKdgsTERNy8eVNL/hZBEF5RQ1BVgM1mo/u+XY0wODgYM2bMgL+/f40XTwwGvZGJyz/8gsRpYRjYo7VunmQ91q1bB3oj1IBzPjU1NZUecBkoKsBqtbZljP0HQAMlotDQUEydOhU+Pj66J6qFSLv/8IvrUV5Rab3Ce4fg3ekD0ahepZl0ByUlJUhISEBWVpYaaglj7JmUlJTTUgQlBTCr1fo5Y+xZJW70yM2ePduwxZOMry/fRLdJqS7imjXyw8bpA/HiM/qsSWlpKVauXIlLly4pKoFzftbHx6e7NJiSKcBms1Ewk6LEhe78W2+9JToxRsKBUzniFVACOg0bXwtDQP2H3Iq8c+cOFi1ahPx8CiAVYaYgCKurjrgoYOLEiQ1KS0uzAQRKyb28vDBv3jxDHjwp78yjF/Dy4l2qC2zR2B9psUPQr1srt0rIyclBfHy8mnUoLC8vD9m8efOPTkYuCrBarXMZY0uUpIwdOxZhYZqugNvJqSGkH8xCxPK9mvSMATEjumPlpD6o42XWxN23bx+2bdumhrNaEISZMgVMnDjRt7S0lEJL2e6TkzN//vxas/Mbd3+N6HUHdSnwyU7NkPHmULRt/rAqPvkJdBVUnKUixlhwSkqKeE8enACr1TqFMbZB9koyJh792vTwFqT+C4vSj8kWNHFIVyR98o3s9/q+dfDOtDCMG/CoqhLILC5dulRtPE4QhIUuCrDZbGT2ukspunbtiunTp+vaneoiTVpzQLZQs4mhbP8sxG46ipXbTyiynvfK01hsVzRWIv6qVatw9uxZJdpcQRCCKdconoD7ictzSphz5841PLCRyhm58GPs/JLe3t+hYb26KPgwRvxh1j+OYFXmSUUlLI36C94c85TiWHZ2NpYtW6a2L88JgvCFqACbzUZZnflSzBYtWmDJEsU3sbqbrUjXe8b7+Pzbay5jQX+qj5yMysjbwTnC43bJlERj9CCefjcCnVtTolkO9HZdu+bK+z5WoiAIU0QF2O32Y5zzp6Xko0ePxqBBgwxdrBKz0PEpyMr52WWoS5sm+CaJXJJKKCgsRmhUMn4sKJKxGNWnI7bOG6Y4z71794pBkwJkC4IQwqKjo/3v3r1bAMBbirR69epqJzM80VrT8ET8dNt1Yc+GPoIv1o5xYZPw0WlM23BYxtr/oTrI3zEFdet4ycYoyzRr1izF6TgcjkeYzWYj435AikGpK43748n6NHEpt+Hz/CqUlbtmsYY82QZ74ke60P786z00GSkzVCLO+ZQohLRspCgrNjZWMWKk2gUpgHL5a6WUffv2RUREhGELVWP0y50SNByxXjY8tl8nZMwZKvv9oRfWoLhUngM4mWhBjw5NFcWkpqbiyJEjsjHO+RJSAKmU0tsuQIsnJdQ2XLp+G+2t/5SJeXXY42IMUBXO5d5C56hkGS55ibd2xoAshxIcPnwYmzdTRk8G20gB+++XsFxGKeLr1KlTba8fx7/7EU/FyCcXO/pJvD3+uQfy6aqMWrILFDdIoVu7QJx+16o613PnzomRohQYY/8lBZwC8GfpIBE0bqxsWozUyt7j32PI3B0ylvGRf8GcsZX2nRb/5nufY/lW5bT/9vnDxRyCGlB0SBuqALmkgO8AdJQObtiwwfCwV2kGaQezYFUIhBKm9sfUEZWO6dbPzmNMPOVl5TCiV3vsXDgCdA3UgMLkmJhKp0oCt0gBeQBaSkc2bdoECoFrG9bsOIUZGz+TiUmLfQGWsM7i7xs+Oo0YBfM3oHsQdi/5m6L5q8qQEqcTJkxQWkoJKYDyy7JA+49SwNzkL7B0y79lk9v39ksY1IPcdeDXohKEzd6OkxcqK0Vk7xfbnsWM8B4waW39fa6aCrBarWcYY6FGX4EzV/LFB+tU9g0x2Zlz4zeUlleImd9WgfXRs2MzkK3fd/IK6BpIgbxA8gadQO4wPZh37pWKtA389OcjNa7ATabmBq9YsQJNmvw+AT1XgR6rjMPnxB39Lu+WHhJVnIup49GuRfVKalKmlDYnZ0gKnPNLpICPOefDpYOemkHa8Qmr94u7ZATkbpkknhQjQMMMfkVvwAoAf5cK8sQROvLNVQybt1M8nkbB9W2T0TzAmJqDmiPEOU8hBUQCeE868T59+sBqVXcunPjXf76D0PHJIJfWSLi5YwqaNJD2UVVPgiAIOHr0qNIViCUFPAFAlnLRGwxRLo9yekYCPewln86Et5f+MpmWfLVgCMAQdr/uT8G4LMtIKaVGjZQjLKfA4HFJyLnxq5HrR+DDvvjpA1l4Ui0ZauEwNVSUl5cHODNClJSXZRRGjRqFwYMHawpWiuWrNdMqRIOfCMany16qKRuRXiMhckwQhF6iAtQywnpSYn1nbgU9gkbCmsn9MH2kLDyplgiNlNhCQRDiRAVEREQEmEymHyjFJpUyZ84ctG/fXlU4JSspaWkU+Pp449rWV1VDW0/kaCVFTSZTx+Tk5AsPQgg1f4BaXl5//XVVuVTZfSI6vcaOj1PAO9MGIHp4N0/Wqb456mlx8fgTYVUFDOWcy0Iu6vuh1Hjbtm1VBdEjODA2Exev367RxMe/0AWbZhiThHVTGIkSBEHMrFQNIskkkj3rIl0FdYBQVVirBeZeSTni0o+J+XtnnV+vNqgIQvE/JUGMACqNxcXFie12CnC1qKioXWZmpui1uUTRdrt9FOd8qxLVmDFjMHAg9T5rQ/a12xD2n0X6oSxcy9fsT4JfXW+89FwIZo/qiUeDAtyx1j3upjg6RRAE6nMUwUUB1BSVm5tLJRhZiYxyA3QVqFCqByocXLwS336fL+b8S8oqcK+kstGbihgU6dEfPXpGwpUrV8SaoFLzlBj8MPZY1bZaWR4lMjKyh8PhoABdVoOmBgkyK0b1BRm5cOJVWFiIxYsXqzZIcM6HpqamflJVrlqPUCJjbLLSBKk5ilxLo/qDjFKCjhaZD1JTU2WNnooKsFgsfmazmeIDxfpz586dxRzb/4sSqElq/fr1oLBXCTjn13x8fLolJSW51t+kb0BVYqvV2pkxRkpQDMnoJJB/UK9ePaM2sVp8qE1u7dq1Ws1R5WazuW9ycvKXSgLcNUqSVdgCQDEso4wRNUrqfRirtUINInrwNm7cqNUURdQur76UndteYbvdPplz/sBsSBmQdRg5cqRoIv/IVtkDBw5gx44dmq2yAB50gqjp0a0CiNBut8/jnC/W2kH6FmDcuHGacYMRJ4A8PGqW1tE+nyAIgmqbr6IfoDVBm80WDSBB7To4aamLdNiwYejQgb6OMw4osNm9e7day4tU0HJBEN7QI13XCXAyslqtVK8WGGNuk3UUSjs/mKAvRKoDlMw4fvy4+MHE9evX9bCgvFy008/XQ+CRAohhREREB5PJRE14j+sRQDjkQFGh1fnJDNUcqdvUaUbJjFHu3vnJDLW30SczbrrApeJzGGOjPf1uyGMFkFSbzUZ16HhqpWeM1X79TFvT1Fnxnslkmunu4wiPzaC7HY6MjAypqKjYwBgb4A63lsa/djgc0WlpaV9Vl3+1ToBUWGRk5LMOhyMOQL/qTsRDum8556vu3r2bkZmZWeEhrQu6IQpwcrTb7b0551Gc87/qeSg9nHgxY2wXFTMEQaCmDrdfTunhb6gCnAIplvDy8hrOOaf0DvXZuG/zVp4t1dmodn6wTp06HyYlJRmbf9eKBfRoTy+OxWIJNplMjzHGQjjnHRhjjTnn/owxZ/XzFwB3OOfi5/MOh4P6YLLS0tJc20f1CvQA738mrzGT1ipQ+AAAAABJRU5ErkJggg==" preserveAspectRatio="none" /></a><a xlink:href="https://github.com/CDrummond/cantata"><rect x="422" y="71" width="60" height="20" fill="none" stroke="none" pointer-events="all" /><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 81px; margin-left: 452px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: sans-serif; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">cantata</div></div></div></foreignObject><text x="452" y="85" fill="#000000" font-family="sans-serif" font-size="12px" text-anchor="middle">cantata</text></switch></g></a><a xlink:href="https://www.musicpd.org/"><image x="519.5" y="-0.5" width="60.97" height="70" xlink:href="data:image/png;base64,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" preserveAspectRatio="none" /></a><a xlink:href="https://www.musicpd.org/"><rect x="530.49" y="70" width="40" height="20" fill="none" stroke="none" pointer-events="all" /><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 80px; margin-left: 550px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: sans-serif; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">mpd</div></div></div></foreignObject><text x="550" y="84" fill="#000000" font-family="sans-serif" font-size="12px" text-anchor="middle">mpd</text></switch></g></a><path d="M 492.24 35 L 511.76 35" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke" /><path d="M 486.24 35 L 494.24 31 L 492.24 35 L 494.24 39 Z" fill="#000000" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="all" /><path d="M 517.76 35 L 509.76 39 L 511.76 35 L 509.76 31 Z" fill="#000000" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="all" /><a xlink:href="https://www.chromium.org/"><path d="M 490.78 160.73 C 479.12 160.01 465.95 149.79 465.95 133.96 C 465.95 120.68 475.98 107 493.64 107 C 512.41 108.15 520.51 123.58 519.77 134.97 C 519.29 150.27 505.7 161.96 490.78 160.73 Z" fill="#4cb749" stroke="none" pointer-events="all" /><path d="M 481.35 137.57 L 470.53 118.82 C 475.17 112.22 482.55 107 493.64 107 C 503.56 107.51 512.62 112.75 517.06 121.95 L 492.41 121.97 C 489.2 122.01 485.16 123.7 482.75 127.47 C 481.22 129.82 480.12 133.44 481.35 137.57 Z" fill="#df2227" stroke="none" pointer-events="all" /><path d="M 492.57 145.84 C 486.15 145.84 480.81 140.46 480.81 134.02 C 481.03 125.75 487.71 122.05 492.41 121.97 C 500.95 121.97 504.74 129.05 504.74 134.05 C 504.5 140.54 499.86 145.84 492.57 145.84 Z" fill="#e4e2e2" stroke="none" pointer-events="all" /><path d="M 490.78 160.73 L 502.07 141.52 C 503.57 139.71 504.7 136.83 504.71 134.05 C 504.71 128.61 500.73 121.97 492.41 121.97 L 517.06 121.95 C 519.2 126.3 520.02 131.14 519.77 134.97 C 519.44 148.41 507.51 162 490.78 160.73 Z" fill="#fcd209" stroke="none" pointer-events="all" /><path d="M 492.83 143.66 C 487.59 143.66 483.05 139.38 483.05 134 C 483.05 127.83 488.17 124.16 492.73 124.16 C 497.37 124.16 502.59 127.62 502.59 134.08 C 502.59 138.93 498.5 143.66 492.83 143.66 Z" fill="#236ba2" stroke="none" pointer-events="all" /><path d="M 517.41 122.7 L 502.12 126.52 C 500.16 124.12 497.2 122.14 492.93 121.96 L 517.06 121.95 Z M 471.03 118.11 L 481.76 129.34 C 480.48 132.53 480.67 135.33 481.36 137.59 L 470.53 118.83 Z M 490.78 160.75 L 495.06 145.64 C 497.8 145.09 500.18 143.78 502.09 141.5 Z" fill-opacity="0.1" fill="#000000" stroke="none" pointer-events="all" /></a><a xlink:href="https://www.chromium.org/"><rect x="453.23" y="169" width="80" height="20" fill="none" stroke="none" pointer-events="all" /><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 179px; margin-left: 493px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: sans-serif; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">chromium</div></div></div></foreignObject><text x="493" y="183" fill="#000000" font-family="sans-serif" font-size="12px" text-anchor="middle">chromium</text></switch></g></a><a xlink:href="https://github.com/hoyon/mpv-mpris"><image x="424.5" y="195.5" width="60" height="60" xlink:href="data:image/png;base64,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" preserveAspectRatio="none" /></a><a xlink:href="https://github.com/hoyon/mpv-mpris"><rect x="420" y="262" width="80" height="20" fill="none" stroke="none" pointer-events="all" /><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 272px; margin-left: 460px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: sans-serif; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">mpv-mpris</div></div></div></foreignObject><text x="460" y="276" fill="#000000" font-family="sans-serif" font-size="12px" text-anchor="middle">mpv-mpris</text></switch></g></a><a xlink:href="https://github.com/mpv-player/mpv"><image x="519.5" y="190.5" width="70" height="70" xlink:href="data:image/png;base64,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" preserveAspectRatio="none" /></a><a xlink:href="https://github.com/mpv-player/mpv"><rect x="535" y="262" width="40" height="20" fill="none" stroke="none" pointer-events="all" /><g transform="translate(-0.5 -0.5)"><switch><foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 272px; margin-left: 555px;"><div style="box-sizing: border-box; font-size: 0; text-align: center; "><div style="display: inline-block; font-size: 12px; font-family: sans-serif; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">mpv</div></div></div></foreignObject><text x="555" y="276" fill="#000000" font-family="sans-serif" font-size="12px" text-anchor="middle">mpv</text></switch></g></a><path d="M 493.24 226 L 511.76 226" fill="none" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="stroke" /><path d="M 487.24 226 L 495.24 222 L 493.24 226 L 495.24 230 Z" fill="#000000" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="all" /><path d="M 517.76 226 L 509.76 230 L 511.76 226 L 509.76 222 Z" fill="#000000" stroke="#000000" stroke-width="2" stroke-miterlimit="10" pointer-events="all" /></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" /><a transform="translate(0,-5)" xlink:href="https://desk.draw.io/support/solutions/articles/16000042487" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Viewer does not support full SVG 1.1</text></a></switch></svg>
<figcaption>diagram of my setup</figcaption>
</figure>
<p>The window manager (<a href="https://xmonad.org/">xmonad</a>) and screen locker
(<a href="https://github.com/google/xsecurelock">xsecurelock</a><sup id="fnref:xscreensaver" role="doc-noteref"><a href="#fn:xscreensaver" class="footnote" rel="footnote">1</a></sup>) bind all the keys (and thus also Bluetooth
headphones buttons: they appear as a keyboard in xinput and deliver normal
keypresses via <a href="https://www.kernel.org/doc/html/v5.4/input/uinput.html">uinput</a> and <a href="https://github.com/RadiusNetworks/bluez/blob/e11bfba10cc15cf74f8a657fad018aece4a5bde9/profiles/audio/avctp.c">bluetoothd</a>) and call the
<a href="https://github.com/liskin/dotfiles/blob/15c2cd83ce7297c38830053a9fd2be2f3678f4b0/bin/liskin-media">liskin-media</a> script:</p>
<figure>
<figcaption><a href="https://github.com/liskin/dotfiles/blob/15c2cd83ce7297c38830053a9fd2be2f3678f4b0/.xmonad/xmonad.hs#L73-L81">~/.xmonad/xmonad.hs</a></figcaption>
<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">,</span> <span class="p">((</span><span class="mi">0</span><span class="p">,</span> <span class="n">xF86XK_AudioPlay</span> <span class="p">),</span> <span class="n">spawn</span> <span class="s">"liskin-media play"</span><span class="p">)</span>
<span class="p">,</span> <span class="p">((</span><span class="mi">0</span><span class="p">,</span> <span class="n">xF86XK_AudioPause</span><span class="p">),</span> <span class="n">spawn</span> <span class="s">"liskin-media play"</span><span class="p">)</span>
</code></pre></div> </div>
</figure>
<figure>
<figcaption><a href="https://github.com/liskin/dotfiles/blob/15c2cd83ce7297c38830053a9fd2be2f3678f4b0/bin/liskin-xsecurelock#L11-L20">~/bin/liskin-xsecurelock</a></figcaption>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">XSECURELOCK_KEY_XF86AudioPlay_COMMAND</span><span class="o">=</span><span class="s2">"liskin-media play"</span>
<span class="nb">export </span><span class="nv">XSECURELOCK_KEY_XF86AudioPause_COMMAND</span><span class="o">=</span><span class="s2">"liskin-media play"</span>
</code></pre></div> </div>
</figure>
<p>A <a href="https://github.com/liskin/dotfiles/blob/15c2cd83ce7297c38830053a9fd2be2f3678f4b0/.config/systemd/user/liskin-media-mpris-daemon.service">background service</a> uses <a href="https://github.com/altdesktop/playerctl">playerctl</a> to keep track
of the last active player:</p>
<figure>
<figcaption><a href="https://github.com/liskin/dotfiles/blob/15c2cd83ce7297c38830053a9fd2be2f3678f4b0/bin/liskin-media#L46-L54">~/bin/liskin-media</a></figcaption>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>playerctl <span class="nt">--all-players</span> <span class="nt">--follow</span> <span class="nt">--format</span> <span class="s1">'{{playerName}} {{status}}'</span> status <span class="se">\</span>
| <span class="k">while </span><span class="nb">read</span> <span class="nt">-r</span> player status<span class="p">;</span> <span class="k">do
if</span> <span class="o">[[</span> <span class="nv">$status</span> <span class="o">==</span> @<span class="o">(</span>Paused|Playing<span class="o">)</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
</span><span class="nb">printf</span> <span class="s2">"%s</span><span class="se">\n</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$player</span><span class="s2">"</span> <span class="o">></span><span class="s2">"</span><span class="k">${</span><span class="nv">XDG_RUNTIME_DIR</span><span class="k">}</span><span class="s2">/liskin-media-last"</span>
<span class="k">fi
done</span>
</code></pre></div> </div>
</figure>
<p>The <a href="https://github.com/liskin/dotfiles/blob/15c2cd83ce7297c38830053a9fd2be2f3678f4b0/bin/liskin-media">liskin-media</a> script then selects the appropriate media player (the first
one that’s playing; the one that’s paused, if there’s only one; or the last one
to play/pause) and uses <a href="https://github.com/altdesktop/playerctl">playerctl</a> to send commands to it:</p>
<figure>
<figcaption><a href="https://github.com/liskin/dotfiles/blob/15c2cd83ce7297c38830053a9fd2be2f3678f4b0/bin/liskin-media#L56-L100">~/bin/liskin-media</a></figcaption>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function </span>get-mpris-smart <span class="o">{</span>
get-mpris-playing <span class="o">||</span> get-mpris-one-playing-or-paused <span class="o">||</span> get-mpris-last
<span class="o">}</span>
<span class="k">function </span>action-play <span class="o">{</span> <span class="nv">p</span><span class="o">=</span><span class="si">$(</span>get-mpris-smart<span class="si">)</span><span class="p">;</span> playerctl <span class="nt">-p</span> <span class="s2">"</span><span class="nv">$p</span><span class="s2">"</span> play-pause<span class="p">;</span> <span class="o">}</span>
<span class="k">function </span>action-stop <span class="o">{</span> playerctl <span class="nt">-a</span> stop<span class="p">;</span> <span class="o">}</span>
<span class="k">function </span>action-next <span class="o">{</span> <span class="nv">p</span><span class="o">=</span><span class="si">$(</span>get-mpris-playing<span class="si">)</span><span class="p">;</span> playerctl <span class="nt">-p</span> <span class="s2">"</span><span class="nv">$p</span><span class="s2">"</span> next<span class="p">;</span> <span class="o">}</span>
<span class="k">function </span>action-prev <span class="o">{</span> <span class="nv">p</span><span class="o">=</span><span class="si">$(</span>get-mpris-playing<span class="si">)</span><span class="p">;</span> playerctl <span class="nt">-p</span> <span class="s2">"</span><span class="nv">$p</span><span class="s2">"</span> previous<span class="p">;</span> <span class="o">}</span>
</code></pre></div> </div>
</figure>
<p><i>
Note that similar logic is also implemented by <a href="https://github.com/icasdri/mpris2controller">mpris2controller</a>, which I
unfortunately haven’t found until I started writing this post.
</i></p>
<p>The final component is the media players themselves.</p>
<p><a href="https://www.google.com/chrome/">Chrome</a>/<a href="https://www.chromium.org/Home">Chromium</a> 81 seem to work out of the box, including metadata
(artist, album, track) in websites that use the <a href="https://www.w3.org/TR/mediasession/">Media Session API</a>.
Somewhat surprisingly, play/pause works for any HTML <code class="language-plaintext highlighter-rouge"><audio></code>/<code class="language-plaintext highlighter-rouge"><video></code>, so
websites that don’t use Media Session (bandcamp, …) can be controlled too. It
seems this wasn’t always the case as there are several webextensions that seem
to solve this now non-existent problem: <a href="https://github.com/Snazzah/MediaSessionMaster">Media Session Master</a>, <a href="https://github.com/f1u77y/web-media-controller">Web Media
Controller</a>, …<sup id="fnref:webext" role="doc-noteref"><a href="#fn:webext" class="footnote" rel="footnote">2</a></sup></p>
<p><a href="https://firefox.com/">Firefox</a> 76 works after enabling <code class="language-plaintext highlighter-rouge">media.hardwaremediakeys.enabled</code> in
<code class="language-plaintext highlighter-rouge">about:config</code>. This only enables play/pause/stop, however. To be able to skip
to next/prev and to get metadata (artist, album, track), <a href="https://www.w3.org/TR/mediasession/">Media Session API</a>
needs to be enabled separately via <code class="language-plaintext highlighter-rouge">dom.media.mediasession.enabled</code> (note that
I couldn’t get this to work in Firefox 75, so this is hot new experimental
stuff and may be unstable). Also, not all websites can be controlled
(play/pause): YouTube and bandcamp works, soundcloud and plain HTML5 <code class="language-plaintext highlighter-rouge"><audio></code>
example don’t. Firefox’s emerging support for media controls is <a href="https://docs.google.com/document/d/1c4FivJpvAjjw9Uw-jn7X1UjGOoWkANXOulNyqDWs83w/edit">documented
here</a>; there are some interesting details about
ignoring silence, short clips, and giving up control if paused for more than a
minute (a feature that I find undesirable and unfortunately present in Chrome
on non-Linux platforms, as noted further).</p>
<p><a href="https://mynoise.net/">myNoise</a> can’t be controlled by media keys in either browser as it uses
plain <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API">Web Audio API</a>, so I’ve made <a href="https://github.com/liskin/dotfiles/blob/7c89ed287af7f73411ab0dbb36cf948957a17d71/src-webextensions/myNoise-chrome-improvements.user.js">a userscript</a> as a
workaround (chromium-only) and will try to help get it fixed upstream.</p>
<p><a href="https://www.musicpd.org/">mpd</a> 0.21.22 itself does not support <a href="https://www.freedesktop.org/wiki/Specifications/mpris-spec/">MPRIS</a>, but the frontend I use —
<a href="https://github.com/CDrummond/cantata">Cantata</a> — does.</p>
<p>Likewise, <a href="https://github.com/mpv-player/mpv">mpv</a> 0.32.0 needs a plugin, <a href="https://github.com/hoyon/mpv-mpris">mpv-mpris</a> works well.</p>
<p><a href="https://www.videolan.org/vlc/index.html">vlc</a> 3.0 supports <a href="https://www.freedesktop.org/wiki/Specifications/mpris-spec/">MPRIS</a> out of the box. Reportedly, <a href="https://wiki.archlinux.org/index.php/Spotify#MPRIS">so does
Spotify</a>. (I don’t use
either.)</p>
<figure class="video">
<div class="iframe iframe-16x9">
<iframe src="https://www.youtube-nocookie.com/embed/VYQOnKIvGgA" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>
</div>
<figcaption>demonstration of my setup</figcaption>
</figure>
<h3 id="state-of-the-art">State of the art</h3>
<p>Out of curiosity, I wanted to know if this is a solved problem if one uses a
less weird operating system or desktop environment. Turns out, not really… :-)</p>
<h4 id="gnome-popular-linux-desktop-environment"><a href="https://www.gnome.org/">GNOME</a> (popular Linux desktop environment)</h4>
<p>Media keys (and presumably also headphone buttons, not tested) appear to work
out of the box, including when the desktop is locked. Unfortunately it only
works reliably (predictably) when there’s just one media player application.
When there’s more than one (e.g. YouTube in Chromium and music in
<a href="https://wiki.gnome.org/Apps/Rhythmbox">Rhythmbox</a>), only the one that started first (application launch, not
necessarily start of playing) is controlled, regardless of whether this one
player is stopped/playing/paused, or whether it has any playable media at all.</p>
<p>In practice, this means that a browser that had once visited YouTube blocks
other apps from being controlled by media keys. This is further complicated by
the fact that <a href="https://gitlab.gnome.org/GNOME/gnome-settings-daemon/-/tree/28ce4225535329dee6a9aff8c44bd1671ce9d2de/plugins/media-keys">gnome-settings-daemon</a> has two different APIs for media keys:
<a href="https://www.freedesktop.org/wiki/Specifications/mpris-spec/">MPRIS</a> and <a href="https://gitlab.gnome.org/GNOME/gnome-settings-daemon/-/blob/28ce4225535329dee6a9aff8c44bd1671ce9d2de/plugins/media-keys/README.media-keys-API">GSD media keys API</a> and prefers MPRIS players. Therefore,
Chromium are Rhythmbox are preferred to <a href="https://wiki.gnome.org/Apps/Videos">Totem</a> (GNOME’s movie player) even
when launched later, which means that a user needs to understand all these
complicated bits to have any hope of knowing what player will act upon a
play/pause button press. Oh and Totem does support MPRIS in fact, it’s a
plugin that may be manually enabled in its preferences.</p>
<p>It is <em>a bit</em> of a mess.</p>
<p><i>
(I tested this on a <a href="https://fedoraproject.org/">Fedora</a> 32 live DVD with <a href="https://www.gnome.org/">GNOME</a> 3.36. Recordings of
some of those experiments: <a href="https://youtu.be/1fN6NMDBFNI">https://youtu.be/1fN6NMDBFNI</a>,
<a href="https://youtu.be/FCStseDBwC4">https://youtu.be/FCStseDBwC4</a>)
</i></p>
<h5 id="update-2021-03-10-workaround-using-playerctld">Update 2021-03-10: workaround using <a href="https://github.com/altdesktop/playerctl#selecting-players-to-control">playerctld</a></h5>
<p>There is now a workaround in <a href="https://github.com/altdesktop/playerctl#selecting-players-to-control">playerctl</a>: <code class="language-plaintext highlighter-rouge">playerctl daemon</code>
starts an <a href="https://www.freedesktop.org/wiki/Specifications/mpris-spec/">MPRIS</a> proxy that provides the “last active player” logic to
environments that don’t implement it themselves, like GNOME.</p>
<h5 id="update-2023-01-03-works-out-of-the-box-now">Update 2023-01-03: works out of the box now</h5>
<p>Since <a href="https://gitlab.gnome.org/GNOME/gnome-settings-daemon/-/commit/ae95b871d2ea4409a95c750d8ce63c9e5b29a6b8">this commit to
gnome-settings-daemon</a>,
available in stable GNOME 3.38 (released back in 2020) and later, the “last
active player” logic is builtin and GNOME does the right thing out of the box.</p>
<h4 id="kde-plasma-5"><a href="https://en.wikipedia.org/wiki/KDE_Plasma_5">KDE Plasma 5</a></h4>
<p>KDE is the only environment out of those tested that works really well.
(Almost.)</p>
<p>Media keys work out of the box in all media players I tried (<a href="https://github.com/KDE/dragon">Dragon</a>,
<a href="https://www.videolan.org/vlc/index.html">vlc</a>, <a href="https://github.com/mpv-player/mpv">mpv</a> + <a href="https://github.com/hoyon/mpv-mpris">mpv-mpris</a>, <a href="https://wiki.gnome.org/Apps/Videos">Totem</a> + MPRIS plugin) including lock-screen. When there are
multiple players, the last one is controlled, and when it’s closed, it
automatically switches to another one. Additionally, there’s an applet in the
bottom panel that lets users override this automatic behaviour and force a
selected player to be controlled.</p>
<p>When <a href="https://firefox.com/">Firefox</a> is first launched, KDE prompts the user to install the
<a href="https://github.com/KDE/plasma-browser-integration/">Plasma Browser Integration</a> extension which adds <a href="https://www.freedesktop.org/wiki/Specifications/mpris-spec/">MPRIS</a> and even <a href="https://www.w3.org/TR/mediasession/">Media
Session API</a> support to Firefox, presumably because this extension predates
this support in Firefox itself. The <a href="https://github.com/KDE/plasma-browser-integration/blob/64a63f2b4b96545dd1d4bcb5583dfbec9122722f/extension/content-script.js#L635-L898">implementation</a>
is different to the one in recent Firefox versions, so it’s not entirely
surprising that soundcloud works as well (as opposed to vanilla Firefox). And
it also follows that it doesn’t work when a media file is opened directly; the
extension only works in HTML pages.</p>
<p>Unfortunately, <a href="https://www.chromium.org/Home">Chromium</a> doesn’t work so well. It’s visible in the list of
media players in the panel applet, it shows what’s currently playing, but the
control buttons are grey and media keys don’t do anything either. This is also
the case if there are more players active: whenever Chromium is the active
player, media keys do nothing. This is a <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=1052609">bug in Chromium’s MPRIS
implementation</a> that should be easy to fix. In the
meantime, one can install the <a href="https://github.com/KDE/plasma-browser-integration/">Plasma Browser Integration</a> extension as a
workaround (for HTML audio/video).</p>
<p><i>
(I tested this on a <a href="https://spins.fedoraproject.org/kde/">Fedora 32 KDE</a> live DVD with KDE Plasma 5.18.3.
Recordings of some of those experiments: <a href="https://youtu.be/-vpHDXg5jW8">https://youtu.be/-vpHDXg5jW8</a>,
<a href="https://youtu.be/IybSl2WiNYE">https://youtu.be/IybSl2WiNYE</a>)
</i></p>
<h4 id="windows-10"><a href="https://en.wikipedia.org/wiki/Windows_10">Windows 10</a></h4>
<p>Similarly to GNOME, media keys appear to work fine at first glance, but when
multiple/specific apps are involved, minor problems appear.</p>
<p>Windows 10 have their equivalent of <a href="https://www.freedesktop.org/wiki/Specifications/mpris-spec/">MPRIS</a> called <a href="https://docs.microsoft.com/en-us/windows/uwp/audio-video-camera/integrate-with-systemmediatransportcontrols">System Media Transport
Controls</a> and this is supported by <a href="https://github.com/chromium/chromium/tree/0944c7716afc8b3d8fe2236db79866d4c8a57b6f/components/system_media_controls/linux">Chromium</a>, and
therefore by both <a href="https://www.google.com/chrome/">Chrome</a> and the <a href="https://blogs.windows.com/windowsexperience/2020/01/15/new-year-new-browser-the-new-microsoft-edge-is-out-of-preview-and-now-available-for-download/">new Chromium-based Edge</a>.
It’s not supported by the (deprecated) <a href="https://en.wikipedia.org/wiki/Windows_Media_Player">Windows Media Player</a>, but that’s
probably fine as the modern replacement <a href="https://en.wikipedia.org/wiki/Microsoft_Movies_%26_TV">Movies/Films & TV</a> supports
it very well.</p>
<p>As opposed to GNOME, an application not supporting the <a href="https://docs.microsoft.com/en-us/windows/uwp/audio-video-camera/integrate-with-systemmediatransportcontrols">SMTC API</a> does
not mean it doesn’t react to media keys. Windows Media Player <a href="https://youtu.be/9DN2tcZGsHU">does, quite
well actually</a> (even on lock screen), but it
doesn’t grab the keys so when there’s another app, media keys <a href="https://youtu.be/aPSkMTZcy8w">control both of
them</a>. On the other hand, <a href="https://www.videolan.org/vlc/index.html">vlc</a> only <a href="https://youtu.be/FQAFurnLUVU">handles
the keys when focused</a>. Finally and not
surprisingly, <a href="https://en.wikipedia.org/wiki/Microsoft_Edge#Spartan_(2014%E2%80%932019)">old Edge</a> and <a href="https://en.wikipedia.org/wiki/Internet_Explorer">Internet Explorer</a> don’t <a href="https://youtu.be/uKRqZ3p76Gw">handle
them at all</a>.</p>
<p>Handling of multiple apps that all support SMTC is
<a href="https://youtu.be/1-m0kECqt38">good</a>, but there’s a bug that would make this
completely unusable for my podcast use case: it’s not possible to continue
playing from the lock screen if it’d been paused for more than a few seconds.
This bug does not affect <a href="https://en.wikipedia.org/wiki/Microsoft_Movies_%26_TV">Movies/Films & TV</a>, though.</p>
<p>Were it not for this issue, I’d say it’s perfectly usable, as deprecated
players/browsers can easily be avoided and I wouldn’t mind not being able to
use vlc for background playback.</p>
<p><i>
(I tested this on a clean <a href="https://en.wikipedia.org/wiki/Windows_10">Windows 10</a> Pro version 1909 with no
vendor-specific bloatware. Recordings of the experiments are linked from the
preceding paragraphs, and for completeness also listed here:
<a href="https://youtu.be/9DN2tcZGsHU">https://youtu.be/9DN2tcZGsHU</a>, <a href="https://youtu.be/1-m0kECqt38">https://youtu.be/1-m0kECqt38</a>,
<a href="https://youtu.be/aPSkMTZcy8w">https://youtu.be/aPSkMTZcy8w</a>, <a href="https://youtu.be/FQAFurnLUVU">https://youtu.be/FQAFurnLUVU</a>,
<a href="https://youtu.be/uKRqZ3p76Gw">https://youtu.be/uKRqZ3p76Gw</a>.)
</i></p>
<h4 id="macos-catalina"><a href="https://en.wikipedia.org/wiki/MacOS_Catalina">macOS Catalina</a></h4>
<p>I expected this to work almost flawlessly as Apple is known for their focus on
<abbr title="user experience">UX</abbr>, but it seems worse than Windows 10, unfortunately. Worse than Windows 10
with the <em>optional</em> upgrade to the <a href="https://blogs.windows.com/windowsexperience/2020/01/15/new-year-new-browser-the-new-microsoft-edge-is-out-of-preview-and-now-available-for-download/">new Chromium-based Edge</a>,
that is.</p>
<p>My experience as a user of macOS is very limited, and as a developer
non-existent, but it seems that the macOS equivalent of MPRIS is
<a href="https://developer.apple.com/documentation/mediaplayer/mpremotecommandcenter">MPRemoteCommandCenter</a> in the Media Player framework.</p>
<p>Media keys work in every app I tried (but I haven’t tried any that don’t come
pre-installed, like <a href="https://www.videolan.org/vlc/index.html">vlc</a>), and they work on lock screen as well, regardless
of how long it’s been paused/locked. Unfortunately, they only start controlling
the app after I’ve interacted with the play/pause button at least once, so
when I open a video and press the play/pause key on the keyboard, instead of
pausing, the Music app opens.</p>
<p>When multiple players are open, the last one that I interacted with is
controlled, as it should be. When one of them is closed, however, the control
isn’t transferred to the other one, unless the application is terminated
entirely or I manually interact with the other one. Strangely, it works well
when a music-playing tab in Safari is closed.</p>
<p><i>
(I tested this on a clean <a href="https://en.wikipedia.org/wiki/MacOS_Catalina">macOS Catalina</a> 10.15.4 with no additional
software installed. Recordings of some of those experiments:
<a href="https://youtu.be/VN7-eZsIpOE">https://youtu.be/VN7-eZsIpOE</a>, <a href="https://youtu.be/oIo21HRPfhM">https://youtu.be/oIo21HRPfhM</a>)
</i></p>
<h4 id="android-10-samsung-one-ui-21">Android 10 (Samsung One UI 2.1)</h4>
<p>Had I not been a longtime Android user, I would expect this to work flawlessly
as smartphones are the primary means of media consumption for many (most?)
people. Turns out there are issues, too. There always are.</p>
<p>Android’s API for media control: “A <a href="https://developer.android.com/reference/android/media/session/MediaSession">MediaSession</a>
should be created when an app wants to publish media playback information or
handle media keys.”</p>
<p>My Android device does not have a dedicated play/pause button, but my
Bluetooth headphones do, so that’s what I tested (wired headphones will likely
behave the same). Obviously, most apps (including <a href="https://www.videolan.org/vlc/index.html">vlc</a>) react to play/pause
just fine. Additionally, pressing play in one app pauses any other that is
currently playing, which is something that desktop systems don’t do and that
isn’t implemented (yet) in my setup either. Also, an incoming/outgoing call
pauses any playing media. So far so good.</p>
<p>Interaction between multiple players is a bit weird, though. Like in macOS,
after closing one of them, <a href="https://youtu.be/2vQAbaMpXfM">control is not transferred to the other
one</a>. Unlike in macOS, quitting the application
(force close) doesn’t help either. Like in macOS, closing a browser tab does
transfer control to a music player.</p>
<p>What’s worse, when a media playing in the browser (<a href="https://play.google.com/store/apps/details?id=com.android.chrome">Chrome</a>) is
paused and the device is locked, it <a href="https://youtu.be/UOXvDx6Dvas">disappears after a while and can’t be
continued</a>, similarly to Windows 10. As noted in
<a href="https://docs.google.com/document/d/1c4FivJpvAjjw9Uw-jn7X1UjGOoWkANXOulNyqDWs83w/edit">the notes about Firefox media controls</a>, this might be
intentional, but I don’t like this: it forces me to install an app for
anything that I might need to pause for longer than a few seconds, and there
isn’t always (a good) one.</p>
<p><i>
(I tested this on a not at all clean, but fully updated <a href="https://www.gsmarena.com/samsung_galaxy_s10e-9537.php">Samsung Galaxy
S10e</a>. This is not vanilla Android 10, but Samsung’s <a href="https://en.wikipedia.org/wiki/One_UI">One UI</a> 2.1, so it’s
possible other devices will behave better (or worse). Recordings of the
experiments are linked from the preceding paragraphs, and for completeness
also listed here: <a href="https://youtu.be/2vQAbaMpXfM">https://youtu.be/2vQAbaMpXfM</a>,
<a href="https://youtu.be/UOXvDx6Dvas">https://youtu.be/UOXvDx6Dvas</a>.)
</i></p>
<p><i>
(Completely unrelated, but perhaps worth noting: to ensure high-quality
playback, Android sends audio at 100% volume to Bluetooth headphones<sup id="fnref:btvol" role="doc-noteref"><a href="#fn:btvol" class="footnote" rel="footnote">3</a></sup>
and lets them adjust volume themselves. Without this, 16-bit audio at 25%
volume effectively becomes 14-bit. <a href="https://www.freedesktop.org/wiki/Software/PulseAudio/">pulseaudio</a> doesn’t do this, but
<a href="https://github.com/liskin/dotfiles/blob/15c2cd83ce7297c38830053a9fd2be2f3678f4b0/bin/liskin-media#L8-L41">liskin-media does</a>.)
</i></p>
<h4 id="summary">Summary</h4>
<p>None of the mainstream environments except <a href="#kde-plasma-5">KDE</a> supports media
keys/buttons well enough to cover my use cases. It seems, therefore, that
niche X window managers aren’t at a very big disadvantage — their target
demographic is used to tweaking things to their liking, after all.</p>
<hr />
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:xscreensaver" role="doc-endnote">
<p>I used to use <a href="https://www.jwz.org/xscreensaver/">xscreensaver</a>, but it doesn’t support binding (media)
keys and <a href="https://news.ycombinator.com/item?id=21224179">I don’t trust it any more</a>. <a href="#fnref:xscreensaver" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:webext" role="doc-endnote">
<p>And there are more, presumably from back when there was no <a href="https://www.freedesktop.org/wiki/Specifications/mpris-spec/">MPRIS</a>
support in the browser whatsoever:</p>
<ul>
<li><a href="https://github.com/otommod/browser-mpris2">https://github.com/otommod/browser-mpris2</a></li>
<li><a href="https://github.com/Aaahh/browser-mpris2-firefox">https://github.com/Aaahh/browser-mpris2-firefox</a></li>
<li><a href="https://github.com/KDE/plasma-browser-integration">https://github.com/KDE/plasma-browser-integration</a></li>
</ul>
<p><a href="#fnref:webext" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
<li id="fn:btvol" role="doc-endnote">
<p>Not all of them, however. I tried <a href="https://www.marshallheadphones.com/us/en/monitor-bluetooth.html">Marshall Monitor Bluetooth</a> and <a href="https://www.sony.com/electronics/support/wireless-headphones-bluetooth-headphones/mdr-xb950bt/specifications">Sony
MDR-XB950BT</a> and it’s only done for the former. <a href="#fnref:btvol" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>
Přestavba Agang Sin City na berany2015-04-22T00:00:00+00:00https://work.lisk.in/2015/04/22/prestavba-na-berany<p>Včera/dneska jsem na moje kolo dal místo rovných řídítek silniční
berany.</p>
<p>Před:<br />
<a href="https://store.lisk.in/tmp/perm/berany_puvodni.jpg"><img src="https://store.lisk.in/tmp/perm/berany_puvodni_small.jpg" alt="" /></a></p>
<p>Po:<br />
<a href="https://store.lisk.in/tmp/perm/berany_final.jpg"><img src="https://store.lisk.in/tmp/perm/berany_final_small.jpg" alt="" /></a></p>
<h3 id="proč">Proč?</h3>
<p>Protože mě bolívá za krkem a mám pocit, že by nemuselo, kdybych to moh držet
jinak. A protože to prostě stejně chci mít možnost držet jinak. Ostatně,
fitnessovej rám to asi má, silniční řídítka tomu nemůžou moc uškodit.</p>
<h3 id="jak">Jak?</h3>
<p>Problém je, že silniční brzdy potřebují poloviční tah lanka a skoro všechny
páčky, zejména <a href="https://en.wikipedia.org/wiki/Shimano_Total_Integration">dualy</a>, tak nejdou použít s mými V-brzdami. Silniční brzdy
tam dát nemůžu, protože mám široký pláště a blatníky.</p>
<p><a href="http://www.phred.org/~alex/bikes/brakes.html">Řešení</a> je několik:</p>
<ul>
<li>zahodit V-brzdy a dát tam cantilevery</li>
<li>použít nějaký <a href="https://problemsolversbike.com/products/travel-agent">udělátor</a> na změnu tahu lanka</li>
<li>koupit si páčky <a href="https://gevenalle.com/product-category/shifters/">Gevenalle</a> (časovkářská řadící páčka přidělaná na
Tektro brzdy)</li>
<li>koupit si páčky <a href="http://www.wiggle.com/tektro-rl520-drop-bar-brake-lever/">Tektro RL-520</a> a dolů do beranů strčit
<a href="https://www.jensonusa.com/Shimano-SL-BS77-9-SPEED-Bar-End-Shifters">časovkářské páčky</a> tak, jak to má třeba <a href="https://www.trekbikes.com/us/en/bikes/road/touring/520/">Trek 520</a></li>
<li>koupit si páčky <a href="http://www.wiggle.com/tektro-rl520-drop-bar-brake-lever/">Tektro RL-520</a> a řazení prostě nějak
<a href="https://cyclingabout.com/rohloff-hubs-with-drop-handlebars/">uhackovat</a> z existujícího SRAM Attack gripshiftu</li>
</ul>
<p>Cantilevery mi známí nedoporučovali, Travel Agent nejde v Česku sehnat,
Gevenalle je zbytečně drahý (€150), časovkářský páčky na to, jak jsou (takhle
použitý) neergonomický, taky, takže vyhrálo ponechání gripshiftu, a to ve
víceméně podobné poloze (tzn. vlevo od místa, kde mám běžně pravou ruku), jako
bylo dřív. Ve výsledku to vypadá takhle:</p>
<p><a href="https://store.lisk.in/tmp/perm/berany_final_detail.jpg"><img src="https://store.lisk.in/tmp/perm/berany_final_detail_small.jpg" alt="" /></a></p>
<p>Přehazovat na těžší převod zvládám palcem a prostředníčkem, na lehčí musím
zvednout ruku z brzd. No, asi to mohlo být i horší… :-)</p>
<h3 id="fotky-z-montáže">Fotky z montáže</h3>
<p>Když jsem si ještě neuvědomoval, jak moc velký průšvih je, že ve Feroně neměli
trubku s průměrem 22 mm a vzal jsem jen 20 mm:<br />
<a href="https://store.lisk.in/tmp/perm/berany_packy1.jpg"><img src="https://store.lisk.in/tmp/perm/berany_packy1_small.jpg" alt="" /></a></p>
<p>První “prototyp”:<br />
<a href="https://store.lisk.in/tmp/perm/berany_packy2.jpg"><img src="https://store.lisk.in/tmp/perm/berany_packy2_small.jpg" alt="" /></a></p>
<p>Brzdy už fungují, takže zařadíme natvrdo pětku a můžem jet koupit šrouby:<br />
<a href="https://store.lisk.in/tmp/perm/berany_singlespeed.jpg"><img src="https://store.lisk.in/tmp/perm/berany_singlespeed_small.jpg" alt="" /></a></p>
<p>Už si uvědomuju, že mám užší trubku, a ničím objímku:<br />
<a href="https://store.lisk.in/tmp/perm/berany_uchyt1.jpg"><img src="https://store.lisk.in/tmp/perm/berany_uchyt1_small.jpg" alt="" /></a>
<a href="https://store.lisk.in/tmp/perm/berany_uchyt2.jpg"><img src="https://store.lisk.in/tmp/perm/berany_uchyt2_small.jpg" alt="" /></a></p>
<p>Na první dobrou (skoro):<br />
<a href="https://store.lisk.in/tmp/perm/berany_uchyt3.jpg"><img src="https://store.lisk.in/tmp/perm/berany_uchyt3_small.jpg" alt="" /></a></p>
<p>Bowden mi trochu zkřížil plány, ale nakonec se tam vešel, akorát je třeba být
extrémně opatrný s tím šroubem potom:<br />
<a href="https://store.lisk.in/tmp/perm/berany_uchyt4.jpg"><img src="https://store.lisk.in/tmp/perm/berany_uchyt4_small.jpg" alt="" /></a></p>
<p>Je čas na další broušení a pilování:<br />
<a href="https://store.lisk.in/tmp/perm/berany_uchyt5.jpg"><img src="https://store.lisk.in/tmp/perm/berany_uchyt5_small.jpg" alt="" /></a>
<a href="https://store.lisk.in/tmp/perm/berany_uchyt6.jpg"><img src="https://store.lisk.in/tmp/perm/berany_uchyt6_small.jpg" alt="" /></a></p>
<p>Zkrátíme šroub, aby se nedotýkal bowdenu, a celé to smontujeme:<br />
<a href="https://store.lisk.in/tmp/perm/berany_uchyt7.jpg"><img src="https://store.lisk.in/tmp/perm/berany_uchyt7_small.jpg" alt="" /></a>
<a href="https://store.lisk.in/tmp/perm/berany_uchyt8.jpg"><img src="https://store.lisk.in/tmp/perm/berany_uchyt8_small.jpg" alt="" /></a></p>
<p>Nakonec omotávka a zkušební jízda:<br />
<a href="https://store.lisk.in/tmp/perm/berany_final_detail.jpg"><img src="https://store.lisk.in/tmp/perm/berany_final_detail_small.jpg" alt="" /></a>
<a href="https://store.lisk.in/tmp/perm/berany_final.jpg"><img src="https://store.lisk.in/tmp/perm/berany_final_small.jpg" alt="" /></a></p>
<p>V pátek ještě přišly <a href="http://dily.maxbike.cz/eshop/staveci-doraz-bowdenu-token-cerne-2ks">stavěcí šrouby</a>, ovšem brzdové nejdou sehnat,
takže je třeba ty řadící trochu upravit:<br />
<a href="https://store.lisk.in/tmp/perm/berany_barrel1.jpg"><img src="https://store.lisk.in/tmp/perm/berany_barrel1_small.jpg" alt="" /></a>
<a href="https://store.lisk.in/tmp/perm/berany_barrel2.jpg"><img src="https://store.lisk.in/tmp/perm/berany_barrel2_small.jpg" alt="" /></a></p>
<h3 id="za-kolik">Za kolik?</h3>
<ul>
<li><a href="http://www.wiggle.com/pro-plt-2014-alloy-road-handlebar/">řídítka Pro PLT</a> z <a href="https://www.facebook.com/events/869841896392719/">blešáku</a>: 500 Kč</li>
<li><a href="http://www.wiggle.com/tektro-rl520-drop-bar-brake-lever/">páčky Tektro RL-520</a> + poštovné: 534 Kč</li>
<li>nová lanka, bowdeny, vodítka: 200 Kč</li>
<li><a href="http://dily.maxbike.cz/eshop/staveci-doraz-bowdenu-token-cerne-2ks">stavěcí šrouby na bowdeny</a>: 200 Kč</li>
<li>objímka ze staré brzdové páčky: 100 Kč</li>
<li>omotávka: 160 Kč</li>
<li>hliníková trubička (a jedna navíc pro jistotu): 15 Kč</li>
<li>šroub (a dva navíc pro jistotu): 9 Kč</li>
</ul>
<p>Celkem: <strong>1703 Kč</strong></p>
Vibration bell for PuTTY for Symbian2010-10-08T00:00:00+00:00https://work.lisk.in/2010/10/08/s2putty-vibrabell<p><a href="http://s2putty.sourceforge.net/">PuTTY</a> is probably the most used application on my phone. I use it to
connect to my server where I read/write e-mails in <a href="http://www.mutt.org/">mutt</a> and communicate
with the outer world using <a href="http://www.irssi.org/">irssi</a> and <a href="http://www.bitlbee.org/">bitlbee</a>. I suppose most PuTTY
users start it only whenever they need to fix something at their servers, not
as a primary communication tool. They didn’t miss a function to alert the user
whenever something happens in a terminal.</p>
<p>I did, however. I wanted to start PuTTY, attach a <a href="http://www.gnu.org/software/screen/">screen</a> session and put
the phone in my pocket, knowing that it would vibrate whenever something
interesting happens. I always hated that whenever I had sent a message to
someone, I had to look at the screen regularly to check whether he replied or
not. So that’s my motivation.</p>
<p>The solution was obvious, I implemented the <code class="language-plaintext highlighter-rouge">do_bell</code> function in PuTTY for
Symbian and made it vibrate for a few hundred milliseconds. The source code is
here: <a href="http://github.com/liskin/s2putty">http://github.com/liskin/s2putty</a>.</p>
<!-- A prebuilt package for s60v5 is in
[downloads][]. Let me know if you need a binary for some other version of
Symbian.
[downloads]: http://github.com/liskin/s2putty/downloads
-->
Multiseat on demand: Split your computer into two whenever you want2009-11-22T00:00:00+00:00https://work.lisk.in/2009/11/22/multiseat-on-demand<p>We needed to make three computers out of two yesterday, so multiseat was the
keyword I googled for. Having found a bunch of howtos all starting with “make
these changes to xorg.conf and gdm.conf”, I took the best from all of them and
put together a solution requiring no single restart of X server. What I got
was this:</p>
<p><img src="https://store.lisk.in/tmp/perm/multiseat.jpg" alt="Multiseat photo" /></p>
<p>Well, what’s the problem? Why didn’t I just take the first howto I found? I
wanted to have a sort of “on demand” multiseat, that I can start and stop
whenever I want, without ever touching my main X server and its configuration,
without losing my open windows — even without having to rearrange them.</p>
<p>What did I need?</p>
<ul>
<li>new enough X.org server and Xephyr server with input hotplug support
(I used 1.6.5),</li>
<li>spare USB keyboard and mouse, spare monitor,</li>
<li>clever window manager with per-screen workspaces (that’s the feature that
lets me switch the external monitor to workspace 12 and still be able to
switch workspaces on my laptop’s display) — I used xmonad, of course.</li>
</ul>
<p>The theory is that the other seat will run in Xephyr, using the keyboard and
mouse I tell it to use. Xephyr can do that, nowadays, we only need to solve
three little problems:</p>
<ol>
<li>main X server uses that keyboard and mouse as well, and we need to disable
that in runtime,</li>
<li><code class="language-plaintext highlighter-rouge">/dev/input/eventN</code> is accessible to root only by default,</li>
<li>a lot of keys don’t work in Xephyr at all :-).</li>
</ol>
<p>Feel tree to skip the explanation of these:</p>
<ol>
<li>
<p>The xinput command can be used to set device properties, and the “Device
Enabled” property is what we’re looking for. Given a device id, this is how
we disable the device in the main X server:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> $ xinput list-props id | \
perl -ne 'if (/Device Enabled \((\d+)\):/){ print $1 }' | \
{ read prop; xinput set-int-prop id $prop 8 0; }
</code></pre></div> </div>
</li>
<li>
<p>Being in a hurry, I added Xephyr for my user to <code class="language-plaintext highlighter-rouge">/etc/sudoers</code>. You may
want to create udev rules to set correct permissions instead.</p>
</li>
<li>
<p>This seems to be caused by Xephyr not using the evdev ruleset for XKB
configuration by default. Running <code class="language-plaintext highlighter-rouge">setxkbmap</code> with <code class="language-plaintext highlighter-rouge">-rules evdev</code> seems to
fix this problem (see the script for details). Xephyr still seems to have
problems with autorepeat, and I guess that tweaking it for each keycode
with <code class="language-plaintext highlighter-rouge">xset r</code> might fix it.</p>
</li>
</ol>
<p>Having solved these problems, I wrote a small script that disables the input
devices in your main X server and launches Xephyr that uses them, fixing
keyboard afterwards: <a href="https://store.lisk.in/tmp/perm/multiseatxephyr">https://store.lisk.in/tmp/perm/multiseatxephyr</a></p>
<p>If called without parameters, it prints a short usage instructions. You’ll
have to look into <code class="language-plaintext highlighter-rouge">/proc/bus/input/devices</code> to get event devices, and at the
xinput list output to find input device ids. It isn’t very robust, but in most
cases you’ll be interested in the last two input devices.</p>
<p>Xephyr is running, with access control disabled, all you need to do now is to
su to another user, <code class="language-plaintext highlighter-rouge">export DISPLAY=:1</code> and run <code class="language-plaintext highlighter-rouge">startkde</code> (or anything else).</p>
<p>If in trouble, consult the documentation. There are a few links at the X.org
wiki: <a href="https://www.x.org/wiki/Development/Documentation/Multiseat">https://www.x.org/wiki/Development/Documentation/Multiseat</a></p>
Going tiled2009-10-28T00:00:00+00:00https://work.lisk.in/2009/10/28/going-tiled<p>I had been using <a href="http://fluxbox.org/">Fluxbox</a> for 7 years when I finally decided it’s time for
change last Friday. As my friends expected, I left it for <a href="https://xmonad.org/">xmonad</a>. Seven
years is a long time and for me it meant that I became a Fluxbox developer.
Therefore, I should say some nice goodbye.</p>
<p>It all started in 2002 when a friend of mine switched me to Linux. I installed
it onto my father’s laptop, which only had 32 MB of memory and quickly
realized that GNOME, KDE and Mozilla aren’t for me. Someone gave me a tarball
of fluxbox 0.1.12, I installed it, and liked it. Unfortunately, I have no
screen shots or photos from that time.</p>
<p>Later that year, I installed fluxbox 0.1.14 (the last in 0.1 series) onto my
“workstation” and it turned out that I would stick with that old version for
another 4 years.</p>
<p>The look of my workspace evolved a little over the years. I chose a few shots
that demonstrate that :-).</p>
<p><a href="https://store.lisk.in/tmp/perm/goingtiled_1.png"><img src="https://store.lisk.in/tmp/perm/goingtiled_1_small.jpg" alt="" /></a></p>
<p><a href="https://store.lisk.in/tmp/perm/goingtiled_2.png"><img src="https://store.lisk.in/tmp/perm/goingtiled_2_small.jpg" alt="" /></a></p>
<p><a href="https://store.lisk.in/tmp/perm/goingtiled_3.png"><img src="https://store.lisk.in/tmp/perm/goingtiled_3_small.jpg" alt="" /></a></p>
<p>In the meantime, Fluxbox team was working hard on the 0.9 series, hoping to
get it stable enough for 1.0. Every time I tried it, it seemed buggy and slow,
bringing nothing new other than transparency and eye candy. The one or two
little features I liked were easy to implement in 0.1.14, anyway. So I stayed
for 4 years.</p>
<p>Then, Mark Tiefenbruck jumped in and made things move faster in 2006. Later
that year, seeing a lot of activity in its svn repo, I decided it was time for
me to jump in as well. Took me only a few patches and it was stable enough for
me to use. I even added some little features I wanted, and managed to get some
of them included in official Fluxbox repo.</p>
<p>At the end of 2007, Fluxbox switched from svn to git and I was given push
(“commit” in svn terminology) access. That year X.org became capable of
switching between single- and dual-head without restarts and I added proper
handling of this stuff to Fluxbox. After that, I had a window manager I was
fully satisfied with. Those few features that never made it into the official
repo were waiting for some polishing on my side that I was supposed to do
after Mark does something I don’t really remember what it was. I’m not sure if
he did it, for some time I thought he didn’t, and then I focused on other
things and didn’t want to hack the window manager that worked perfectly for
me. Well, my fluxbox binary is now more than a year old and will remain that
way. Sorry for that, Fluxbox is a great window manager, I’d like to thank the
people around it, I learned a lot thanks to Fluxbox and the team.</p>
<p>The last day I used Fluxbox, it looked more or less like this:</p>
<p><a href="https://store.lisk.in/tmp/perm/goingtiled_4.png"><img src="https://store.lisk.in/tmp/perm/goingtiled_4_small.jpg" alt="" /></a></p>
<p>And one day I switched to xmonad. That day was last Friday. Being a bug-magnet
(as you may have noticed), I’ve already submitted two patches. But that’s
fine, neither was to xmonad core, both were for xmonad-contrib. (I don’t want
to blame Fluxbox either. That old 0.1.14 was nearly perfect and latest
versions are probably quite stable as well.)</p>
<p>I really like the configurability of xmonad and the layout modifiers concept.
Also, xmonad-contrib is a huge and very nice collection of useful stuff.
I feel that I’ll be submitting more patches (hopefully more than bug fixes) in
the future.</p>
<p>My workspace now looks like this:</p>
<p><a href="https://store.lisk.in/tmp/perm/goingtiled_5.png"><img src="https://store.lisk.in/tmp/perm/goingtiled_5_small.jpg" alt="" /></a></p>
<p>So, again, thanks and goodbye to Fluxbox, and I’m looking forward to having
some fun with xmonad :-).</p>