Recently Written · git

recentlywritten

my writings (done recently)

git clone https://github.com/equwal/recentlywritten

Log | Files | Refs


commit 72c0516b068b0b417d33a4af30806402ca6ed8b6
equwal <13551856+equwal@users.noreply.github.com>
2026-10-04 23:16:13 -0700

git: add the sh mirror pages

 git/index.html                                     |    2 +-
 .../309b45f58cb6a212844799171f165f4e67016abe.html  |  116 ++
 .../453cd03c4a64a0b00bce0bd6e1f38e5f31600c06.html  |   58 +
 .../64809ce9eb3bc72801d5392a38eb3f8bf8da8bd5.html  |   52 +
 .../751d46dcf747b5681d8ab58ccf1e36a8b8e67669.html  |   17 +
 .../7afdbe4c30ec35643f04270d4cb366e9f5f21f2d.html  | 1418 ++++++++++++++
 .../95c74ac780a3179b0054566d06b31ee82a4cb4eb.html  |  110 ++
 .../a787c8cbc697cf65a27ba4a3001315185a097b7f.html  | 1989 ++++++++++++++++++++
 .../aa5d2a061006b27afe8ef2cfc513a8ac8ce13635.html  |   58 +
 .../b2851a1027eddd1dd13e0e4d12152fbb7efeebbe.html  |   61 +
 .../c472417bc57e21f732c70298e6ab85ee80a2a764.html  |   43 +
 .../d5b9be51e70238703777a23bc0e224c5672fe1dc.html  |  125 ++
 .../dc554d506824c642f5f2cba7f48112ea53c6091a.html  |   59 +
 git/sh/file/git-update-repos.html                  |   33 +
 git/sh/file/git-update-worker.sh.html              |   38 +
 git/sh/file/group4ify-aux.html                     |   13 +
 git/sh/file/group4ify.html                         |   23 +
 git/sh/file/http-collect.html                      |  205 ++
 git/sh/file/jpegify-aux.html                       |   13 +
 git/sh/file/jpegify.html                           |   23 +
 git/sh/file/macho.html                             |   38 +
 git/sh/file/pashare.html                           |   26 +
 git/sh/file/pomodoro.html                          |   98 +
 git/sh/file/slock-usb-key.html                     |   39 +
 git/sh/file/tag-music.html                         |   62 +
 git/sh/files.html                                  |   11 +
 git/sh/index.html                                  |   11 +
 git/sh/refs.html                                   |   11 +
 28 files changed, 4751 insertions(+), 1 deletion(-)
diff --git a/git/index.html b/git/index.html
index c166b58..ff68329 100644
--- a/git/index.html
+++ b/git/index.html
@@ -7,5 +7,5 @@
 <div id="top"><a href="../index.html">Recently Written</a> &middot; <a href="index.html">git</a></div>
 
 <div id="content">
-<h1>ye olde code shoppe</h1><p>Mirror of <a href="https://github.com/equwal">github.com/equwal</a> (public, non-fork repos).</p><table><thead><tr><td>Name</td><td>Description</td><td>Owner</td><td>Last commit</td></tr></thead><tbody><tr><td><a href="alt-hack0/index.html">alt-hack0</a></td><td>The Hack font family with a non-weird zero, and other variants.</td><td>Spenser Truex</td><td>2023-08-10 13:15</td></tr><tr><td><a href="asdf-registration/index.html">asdf-registration</a></td><td>Register your asdf projects with the ASDF loader.</td><td>Spenser Truex</td><td>2019-11-09 20:40</td></tr><tr><td><a href="assistkey/index.html">assistkey</a></td><td>Hardware key remapper for the Viwoods AiPaper Reader: AI key, volume keys and power, with multi-tap, hold and chords</td><td>Spenser Truex</td><td>2026-09-21 15:52</td></tr><tr><td><a href="awesome-rebind/index.html">awesome-rebind</a></td><td>Extensions and companions for Rebind, the button remapper for e-ink readers and Android.</td><td>Spenser Truex</td><td>2026-09-21 12:40</td></tr><tr><td><a href="blaher/index.html">blaher</a></td><td>Gilbert Baumann&#x27;s blaher</td><td>Spenser Truex</td><td>2023-05-27 18:44</td></tr><tr><td><a href="cl-yag/index.html">cl-yag</a></td><td>Mirror and rewrite of Solène Rapenne&#x27;s cl-yag SSG of renown</td><td>Spenser Truex</td><td>2025-09-01 17:14</td></tr><tr><td><a href="clapt/index.html">clapt</a></td><td>Archived: generate SBCL cores for lots of libraries.</td><td>Spenser Truex</td><td>2019-11-09 20:50</td></tr><tr><td><a href="clic/index.html">clic</a></td><td>MIRROR ONLY Gopher client with pretty colours in Common Lisp</td><td>Spenser Truex</td><td>2020-06-03 11:41</td></tr><tr><td><a href="coleslaw/index.html">coleslaw</a></td><td>Emacs mode for the &quot;Coleslaw&quot; site generator written in Common Lisp.</td><td>Spenser Truex</td><td>2024-05-11 03:08</td></tr><tr><td><a href="coleslaw-mode-docs/index.html">coleslaw-mode-docs</a></td><td>Documentation to coleslaw-mode for Emacs.</td><td>Spenser Truex</td><td>2019-12-07 18:19</td></tr><tr><td><a href="coleslaw-snippets/index.html">coleslaw-snippets</a></td><td>Boilerplate for Coleslaw markdown.</td><td>Spenser Truex</td><td>2019-06-23 00:12</td></tr><tr><td><a href="common-lisp-utils/index.html">common-lisp-utils</a></td><td>My utilities for Common Lisp programming.</td><td>Spenser Truex</td><td>2019-07-02 01:51</td></tr><tr><td><a href="cores/index.html">cores</a></td><td>Save your custom core file after re-installing SBCL. Load libraries compiled into the core. Works but very rough around the edges.</td><td>Spenser Truex</td><td>2025-08-29 14:08</td></tr><tr><td><a href="CSV/index.html">CSV</a></td><td>Convert CSV (in a reader macro, or in a file) in to Lisp lists.</td><td>Spenser Truex</td><td>2019-01-07 14:05</td></tr><tr><td><a href="defsl/index.html">defsl</a></td><td>Dependencies from the lisp macro system, not the reader.</td><td>Spenser Truex</td><td>2023-06-25 20:44</td></tr><tr><td><a href="deploy-handoff/index.html">deploy-handoff</a></td><td>Claude Code plugin: the agent drives the browser to the last deploy, billing, or pull request step. You make the final click.</td><td>Spenser Truex</td><td>2026-09-21 13:27</td></tr><tr><td><a href="desktop-subtitle-replay/index.html">desktop-subtitle-replay</a></td><td></td><td>Spenser Truex</td><td>2026-08-26 19:46</td></tr><tr><td><a href="dickt.store/index.html">dickt.store</a></td><td>BYOK yomitan dictionary converter for yomidick</td><td>Spenser Truex</td><td>2026-05-09 08:51</td></tr><tr><td><a href="dmenu-scripts/index.html">dmenu-scripts</a></td><td>Ask for things</td><td>Spenser Truex</td><td>2025-09-10 23:02</td></tr><tr><td><a href="dmodurl/index.html">dmodurl</a></td><td>Convenient URL v</td><td>Spenser Truex</td><td>2025-09-01 18:48</td></tr><tr><td><a href="dpatchmail/index.html">dpatchmail</a></td><td>Patch programs with a dmenu interface</td><td>Spenser Truex</td><td>2025-09-01 18:37</td></tr><tr><td><a href="dsad/index.html">dsad</a></td><td>Dmenu interface for Simple Audio Daemon</td><td>Spenser Truex</td><td>2025-09-01 18:27</td></tr><tr><td><a href="ediff-spacemacs/index.html">ediff-spacemacs</a></td><td>Bindings fo ediff in a spacemacs layer.</td><td>Spenser Truex</td><td>2019-11-13 22:36</td></tr><tr><td><a href="encrypted-home-backups/index.html">encrypted-home-backups</a></td><td>Automated home backups, onto an encrypted partition</td><td>Spenser Truex</td><td>2019-04-20 16:47</td></tr><tr><td><a href="equwal/index.html">equwal</a></td><td></td><td>Spenser Truex</td><td>2026-09-21 20:56</td></tr><tr><td><a href="esperanto/index.html">esperanto</a></td><td>Compress natural language in Esperanto</td><td>Spenser Truex</td><td>2019-11-25 13:19</td></tr><tr><td><a href="Forthe/index.html">Forthe</a></td><td>Forth-like DSL.</td><td>Spenser Truex</td><td>2019-06-24 12:01</td></tr><tr><td><a href="gendocs/index.html">gendocs</a></td><td>Simple document generation library.</td><td>Spenser Truex</td><td>2019-04-29 19:59</td></tr><tr><td><a href="goldfish/index.html">goldfish</a></td><td>My fish init.</td><td>Spenser Truex</td><td>2020-03-07 08:31</td></tr><tr><td><a href="ideamine/index.html">ideamine</a></td><td>An idea inbox for Claude Code: /idea saves ideas at zero tokens; Claude triages them and routes each to the cheapest model that can build it.</td><td>Spenser Truex</td><td>2026-09-21 19:33</td></tr><tr><td><a href="ii-lchat-metapackage/index.html">ii-lchat-metapackage</a></td><td>Get suckless irc working for YOU</td><td>Spenser Truex</td><td>2025-09-07 11:05</td></tr><tr><td><a href="ink-dim/index.html">ink-dim</a></td><td>Ink Dim: tap to set the frontlight of a Viwoods AiPaper reader below the lowest system level. Tap again to go back.</td><td>Spenser Truex</td><td>2026-09-21 04:11</td></tr><tr><td><a href="ink-recents/index.html">ink-recents</a></td><td>Ink Recents: a recent-apps switcher drawn for e-ink readers. No animation, swipe up to close, swipe down to close the others.</td><td>Spenser Truex</td><td>2026-09-21 04:11</td></tr><tr><td><a href="ink-update/index.html">ink-update</a></td><td>Ink Update: tells you when Rebind, Ink Recents or Ink Dim has a new version. F-Droid, then Google Play, then GitHub.</td><td>Spenser Truex</td><td>2026-09-21 12:36</td></tr><tr><td><a href="len-cmp/index.html">len-cmp</a></td><td>Efficient comparsion of length in lists.</td><td>Spenser Truex</td><td>2019-09-14 14:10</td></tr><tr><td><a href="librivox/index.html">librivox</a></td><td>Automated youtube uploading of audiobooks</td><td>Spenser Truex</td><td>2020-03-10 18:51</td></tr><tr><td><a href="lisp-multithreaded-internet/index.html">lisp-multithreaded-internet</a></td><td>Multithreaded internet for Common Lisp</td><td>Spenser Truex</td><td>2019-10-27 14:44</td></tr><tr><td><a href="LispBrain/index.html">LispBrain</a></td><td>The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp&#x27;s ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</td><td>Spenser Truex</td><td>2019-09-07 13:45</td></tr><tr><td><a href="minimak-tty-xkb/index.html">minimak-tty-xkb</a></td><td>&quot;Check out my awesome website dude.&quot;</td><td>Spenser Truex</td><td>2021-01-15 17:23</td></tr><tr><td><a href="minimalist-template/index.html">minimalist-template</a></td><td>Another minimalist template for quickproject</td><td>Spenser Truex</td><td>2020-01-06 12:37</td></tr><tr><td><a href="nixtoo-genux/index.html">nixtoo-genux</a></td><td>Declarative Nix package manager setup. A new linux distro.</td><td>Spenser Truex</td><td>2025-09-04 16:14</td></tr><tr><td><a href="pass-simple/index.html">pass-simple</a></td><td>fork of zerous&#x27; minimal pass rewrite with bugfixes</td><td>Spenser Truex</td><td>2025-09-02 13:59</td></tr><tr><td><a href="posix-pipes/index.html">posix-pipes</a></td><td>portable pipes straight from CLOCC</td><td>Spenser Truex</td><td>2025-09-03 15:39</td></tr><tr><td><a href="prompt-mail/index.html">prompt-mail</a></td><td>Ask mail-related questions and filter mail-related files with dmenu</td><td>Spenser Truex</td><td>2025-09-05 01:58</td></tr><tr><td><a href="proxyverif/index.html">proxyverif</a></td><td>Check socks proxies whether they are alive or non-responsive  (ping through)</td><td>Spenser Truex</td><td>2019-03-31 23:21</td></tr><tr><td><a href="quickurl/index.html">quickurl</a></td><td></td><td>Spenser Truex</td><td>2019-12-20 22:18</td></tr><tr><td><a href="r6rs-complete/index.html">r6rs-complete</a></td><td>R6RS single document.</td><td>Spenser Truex</td><td>2019-11-24 14:44</td></tr><tr><td><a href="rebind/index.html">rebind</a></td><td>Rebind: remap the hardware buttons of e-ink readers and any Android 12+ device. Free beta APK.</td><td>Spenser Truex</td><td>2026-09-21 12:39</td></tr><tr><td><a href="recentlywritten/index.html">recentlywritten</a></td><td>my writings (done recently)</td><td>Spenser Truex</td><td>2026-09-21 19:15</td></tr><tr><td><a href="rectangle/index.html">rectangle</a></td><td>Spacemacs layer for binding rectangle interaction commands.</td><td>Spenser Truex</td><td>2020-07-24 20:43</td></tr><tr><td><a href="resume-template/index.html">resume-template</a></td><td>Resume template</td><td>Spenser Truex</td><td>2020-09-14 03:30</td></tr><tr><td><a href="sacc/index.html">sacc</a></td><td>Mirror of git://bitreich.org/sacc, the sacc gopher client from bitreich.org.</td><td>Spenser Truex</td><td>2025-03-31 17:55</td></tr><tr><td><a href="sacc-ebuild/index.html">sacc-ebuild</a></td><td>Mirrored on Github for normies.</td><td>Spenser Truex</td><td>2020-04-07 00:17</td></tr><tr><td><a href="sbm/index.html">sbm</a></td><td>dmenu bookmarks. Instantly fuzzy search thousands of bookmarks and plumb them. LOOKING FOR SUCKLESS SOFTWARE EDITION? GO TO sbm-suckless INSTEAD</td><td>Spenser Truex</td><td>2026-09-21 14:06</td></tr><tr><td><a href="sbm-android/index.html">sbm-android</a></td><td>sbm bookmarks for Android: fuzzy search, share to add, sync with bm and the browser add-on</td><td>Spenser Truex</td><td>2026-09-21 05:09</td></tr><tr><td><a href="sbm-extension/index.html">sbm-extension</a></td><td>sbm bookmarks for Firefox and Chrome: fuzzy search, add the page, sync with bm and the sbm app</td><td>Spenser Truex</td><td>2026-09-21 05:09</td></tr><tr><td><a href="sbm-suckless/index.html">sbm-suckless</a></td><td>sbm before the rewrite: bookmarks in a plain file, picked with dmenu. Suckless style.</td><td>Spenser Truex</td><td>2026-09-21 14:05</td></tr><tr><td><a href="sbm-sync/index.html">sbm-sync</a></td><td>Sync server for sbm bookmark files: one small Go program, plain files, AGPL</td><td>Spenser Truex</td><td>2026-09-21 19:02</td></tr><tr><td><a href="sigil/index.html">sigil</a></td><td>A documentation preprocessor for Common Lisp docs</td><td>Spenser Truex</td><td>2020-03-26 16:25</td></tr><tr><td><a href="sl/index.html">sl</a></td><td>SL: Dependency language for Common Lisp</td><td>Spenser Truex</td><td>2019-09-11 17:08</td></tr><tr><td><a href="slurp/index.html">slurp</a></td><td>Spacemacs layer for slurping, inserting, copying, etc. of buffers (keybind layer).</td><td>Spenser Truex</td><td>2019-07-03 13:42</td></tr><tr><td><a href="stumpwm-init/index.html">stumpwm-init</a></td><td>I use dwm now. Archived.</td><td>Spenser Truex</td><td>2020-03-31 15:46</td></tr><tr><td><a href="subplz-web/index.html">subplz-web</a></td><td></td><td>Spenser Truex</td><td>2026-09-21 19:02</td></tr><tr><td><a href="subread-android/index.html">subread-android</a></td><td>SubRead for Android: times an audiobook against its ebook on the device</td><td>Spenser Truex</td><td>2026-09-21 17:55</td></tr><tr><td><a href="subread-dictionary/index.html">subread-dictionary</a></td><td>Pop-up dictionary for Android that reads Yomitan dictionaries, with local audio</td><td>Spenser Truex</td><td>2026-09-21 20:59</td></tr><tr><td><a href="subread-extension/index.html">subread-extension</a></td><td>Show your own .srt over a YouTube video, in time with it. Chrome and Firefox.</td><td>Spenser Truex</td><td>2026-09-21 03:50</td></tr><tr><td><a href="subread-overlay/index.html">subread-overlay</a></td><td>Shows the lines of an .srt over any Android media player, in time with it. Tap a word to look it up.</td><td>Spenser Truex</td><td>2026-09-21 22:12</td></tr><tr><td><a href="subread.koplugin/index.html">subread.koplugin</a></td><td>KOReader plugin: the book follows the narration of an audiobook, from an .srt made by subread.space</td><td>Spenser Truex</td><td>2026-09-21 16:49</td></tr><tr><td><a href="subread.space/index.html">subread.space</a></td><td>read subs in space</td><td>Spenser Truex</td><td>2026-05-07 08:11</td></tr><tr><td><a href="tag-gen/index.html">tag-gen</a></td><td>Generate tags for each directory and say what programming languages to look for</td><td>Spenser Truex</td><td>2025-09-05 00:09</td></tr><tr><td><a href="timeclock/index.html">timeclock</a></td><td>Spacemacs layer for tracking your time.</td><td>Spenser Truex</td><td>2020-07-24 20:46</td></tr><tr><td><a href="tiny-runlevels/index.html">tiny-runlevels</a></td><td>Using an init system for many instances of simple programs . https://therealtruex.com/posts/Using-an-init-system-for-many-instances-of-simple-programs</td><td>Spenser Truex</td><td>2025-09-07 12:16</td></tr><tr><td><a href="tmpl/index.html">tmpl</a></td><td>Insanely simple templates </td><td>Spenser Truex</td><td>2025-09-12 07:59</td></tr><tr><td><a href="tnot/index.html">tnot</a></td><td>like tmux but not</td><td>Spenser Truex</td><td>2025-09-01 22:53</td></tr><tr><td><a href="vibeslop-dickt/index.html">vibeslop-dickt</a></td><td>make your own vibeslop dickt in just a few minutes</td><td>Spenser Truex</td><td>2026-05-22 00:48</td></tr><tr><td><a href="xinput-persistence/index.html">xinput-persistence</a></td><td>xinput for mouse or keyboard configurations that work when disconnected or on boot.</td><td>Spenser Truex</td><td>2019-07-02 01:52</td></tr></tbody></table>
+<h1>ye olde code shoppe</h1><p>Mirror of <a href="https://github.com/equwal">github.com/equwal</a> (public, non-fork repos).</p><table><thead><tr><td>Name</td><td>Description</td><td>Owner</td><td>Last commit</td></tr></thead><tbody><tr><td><a href="9p-store/index.html">9p-store</a></td><td>Small disk-backed 9P file server (diod) for a private network</td><td>Spenser Truex</td><td>2026-10-04 03:38</td></tr><tr><td><a href="agent-share/index.html">agent-share</a></td><td>Shared 9P file store layout and rules for AI coding agents</td><td>Spenser Truex</td><td>2026-10-04 03:27</td></tr><tr><td><a href="ai-config/index.html">ai-config</a></td><td>One canonical AI agent config (rules, skills, MCP servers) rendered into Claude Code, Codex, Copilot CLI and Antigravity by acsync</td><td>Spenser Truex</td><td>2026-10-04 21:01</td></tr><tr><td><a href="ai-config-render/index.html">ai-config-render</a></td><td>Render one canonical AI-agent config (rules, skills, MCP servers) into Claude Code, Codex, Copilot CLI, Gemini and more</td><td>Spenser Truex</td><td>2026-10-04 01:37</td></tr><tr><td><a href="alt-hack0/index.html">alt-hack0</a></td><td>The Hack font family with a non-weird zero, and other variants.</td><td>Spenser Truex</td><td>2021-01-15 16:37</td></tr><tr><td><a href="asdf-registration/index.html">asdf-registration</a></td><td>Register your asdf projects with the ASDF loader.</td><td>Spenser Truex</td><td>2019-11-09 20:40</td></tr><tr><td><a href="assistkey/index.html">assistkey</a></td><td>Hardware key remapper for the Viwoods AiPaper Reader: AI key, volume keys and power, with multi-tap, hold and chords</td><td>Spenser Truex</td><td>2026-09-26 15:00</td></tr><tr><td><a href="awesome-rebind/index.html">awesome-rebind</a></td><td>Extensions and companions for Rebind, the button remapper for e-ink readers and Android.</td><td>Spenser Truex</td><td>2026-09-26 12:50</td></tr><tr><td><a href="bd-plugin-profiler/index.html">bd-plugin-profiler</a></td><td>BetterDiscord plugin that measures the main-thread time of each plugin</td><td>Spenser Truex</td><td>2026-09-26 20:45</td></tr><tr><td><a href="blaher/index.html">blaher</a></td><td>Gilbert Baumann&#x27;s blaher</td><td>Spenser Truex</td><td>2023-05-27 18:44</td></tr><tr><td><a href="cl-yag/index.html">cl-yag</a></td><td>Mirror and rewrite of Solène Rapenne&#x27;s cl-yag SSG of renown</td><td>Spenser Truex</td><td>2025-09-01 17:14</td></tr><tr><td><a href="clapt/index.html">clapt</a></td><td>Archived: generate SBCL cores for lots of libraries.</td><td>Spenser Truex</td><td>2019-11-09 20:50</td></tr><tr><td><a href="clic/index.html">clic</a></td><td>MIRROR ONLY Gopher client with pretty colours in Common Lisp</td><td>Spenser Truex</td><td>2020-06-03 11:41</td></tr><tr><td><a href="coleslaw/index.html">coleslaw</a></td><td>Emacs mode for the &quot;Coleslaw&quot; site generator written in Common Lisp.</td><td>Spenser Truex</td><td>2024-05-11 03:08</td></tr><tr><td><a href="coleslaw-mode-docs/index.html">coleslaw-mode-docs</a></td><td>Documentation to coleslaw-mode for Emacs.</td><td>Spenser Truex</td><td>2019-12-07 18:19</td></tr><tr><td><a href="coleslaw-snippets/index.html">coleslaw-snippets</a></td><td>Boilerplate for Coleslaw markdown.</td><td>Spenser Truex</td><td>2019-06-23 00:12</td></tr><tr><td><a href="common-lisp-utils/index.html">common-lisp-utils</a></td><td>My utilities for Common Lisp programming.</td><td>Spenser Truex</td><td>2019-07-02 01:51</td></tr><tr><td><a href="cores/index.html">cores</a></td><td>Save your custom core file after re-installing SBCL. Load libraries compiled into the core. Works but very rough around the edges.</td><td>Spenser Truex</td><td>2025-08-29 14:08</td></tr><tr><td><a href="CSV/index.html">CSV</a></td><td>Convert CSV (in a reader macro, or in a file) in to Lisp lists.</td><td>Spenser Truex</td><td>2019-01-07 14:05</td></tr><tr><td><a href="defsl/index.html">defsl</a></td><td>Dependencies from the lisp macro system, not the reader.</td><td>Spenser Truex</td><td>2026-10-03 17:33</td></tr><tr><td><a href="deploy-handoff/index.html">deploy-handoff</a></td><td>Claude Code plugin: the agent drives the browser to the last deploy, billing, or pull request step. You make the final click.</td><td>Spenser Truex</td><td>2026-10-04 21:00</td></tr><tr><td><a href="desktop-subtitle-replay/index.html">desktop-subtitle-replay</a></td><td></td><td>Spenser Truex</td><td>2026-09-26 12:50</td></tr><tr><td><a href="dickt.store/index.html">dickt.store</a></td><td>BYOK yomitan dictionary converter for yomidick</td><td>Spenser Truex</td><td>2026-05-09 08:51</td></tr><tr><td><a href="dmenu-scripts/index.html">dmenu-scripts</a></td><td>Ask for things</td><td>Spenser Truex</td><td>2025-09-10 23:02</td></tr><tr><td><a href="dmodurl/index.html">dmodurl</a></td><td>Convenient URL v</td><td>Spenser Truex</td><td>2025-09-01 18:48</td></tr><tr><td><a href="dpatchmail/index.html">dpatchmail</a></td><td>Patch programs with a dmenu interface</td><td>Spenser Truex</td><td>2025-09-01 18:37</td></tr><tr><td><a href="dsad/index.html">dsad</a></td><td>Dmenu interface for Simple Audio Daemon</td><td>Spenser Truex</td><td>2025-09-01 18:27</td></tr><tr><td><a href="dsl-agent/index.html">dsl-agent</a></td><td>Claude Code plugin: an agent that speaks only DSL, and the MCP server that stores and runs the DSLs</td><td>Spenser Truex</td><td>2026-10-04 04:32</td></tr><tr><td><a href="ediff-spacemacs/index.html">ediff-spacemacs</a></td><td>Bindings fo ediff in a spacemacs layer.</td><td>Spenser Truex</td><td>2019-11-13 22:36</td></tr><tr><td><a href="encrypted-home-backups/index.html">encrypted-home-backups</a></td><td>Automated home backups, onto an encrypted partition</td><td>Spenser Truex</td><td>2019-04-20 16:47</td></tr><tr><td><a href="equwal/index.html">equwal</a></td><td></td><td>Spenser Truex</td><td>2026-09-26 12:50</td></tr><tr><td><a href="esperanto/index.html">esperanto</a></td><td>Compress natural language in Esperanto</td><td>Spenser Truex</td><td>2019-11-25 13:19</td></tr><tr><td><a href="Forthe/index.html">Forthe</a></td><td>Forth-like DSL.</td><td>Spenser Truex</td><td>2019-06-24 12:01</td></tr><tr><td><a href="forthlisp/index.html">forthlisp</a></td><td>Scheme on a from-scratch Forth on from-scratch machine code: core (host Lisp, Scheme in Forth, R7RS harness)</td><td>Spenser Truex</td><td>2026-10-04 17:09</td></tr><tr><td><a href="forthlisp-stm32/index.html">forthlisp-stm32</a></td><td>STM32F446 target for forthlisp: Thumb-2 Forth kernel built by an SBCL assembler, QEMU netduinoplus2</td><td>Spenser Truex</td><td>2026-10-04 17:09</td></tr><tr><td><a href="forthlisp-z80/index.html">forthlisp-z80</a></td><td>Z80 target for forthlisp: Z80 Forth kernel built by an SBCL assembler, z80pack cpmsim</td><td>Spenser Truex</td><td>2026-10-04 09:12</td></tr><tr><td><a href="gendocs/index.html">gendocs</a></td><td>Simple document generation library.</td><td>Spenser Truex</td><td>2019-04-29 19:59</td></tr><tr><td><a href="goldfish/index.html">goldfish</a></td><td>My fish init.</td><td>Spenser Truex</td><td>2020-03-07 08:31</td></tr><tr><td><a href="ideamine/index.html">ideamine</a></td><td>An idea inbox for Claude Code: /idea saves ideas at zero tokens; Claude triages them and routes each to the cheapest model that can build it.</td><td>Spenser Truex</td><td>2026-09-26 12:50</td></tr><tr><td><a href="ii-lchat-metapackage/index.html">ii-lchat-metapackage</a></td><td>Get suckless irc working for YOU</td><td>Spenser Truex</td><td>2025-09-07 11:05</td></tr><tr><td><a href="ink-dim/index.html">ink-dim</a></td><td>Ink Dim: tap to set the frontlight of a Viwoods AiPaper reader below the lowest system level. Tap again to go back.</td><td>Spenser Truex</td><td>2026-10-04 22:46</td></tr><tr><td><a href="ink-recents/index.html">ink-recents</a></td><td>Ink Recents: a recent-apps switcher drawn for e-ink readers. No animation, swipe up to close, swipe down to close the others.</td><td>Spenser Truex</td><td>2026-10-04 21:03</td></tr><tr><td><a href="ink-update/index.html">ink-update</a></td><td>Ink Update: tells you when Rebind, Ink Recents or Ink Dim has a new version. F-Droid, then Google Play, then GitHub.</td><td>Spenser Truex</td><td>2026-10-04 22:34</td></tr><tr><td><a href="len-cmp/index.html">len-cmp</a></td><td>Efficient comparsion of length in lists.</td><td>Spenser Truex</td><td>2019-09-14 14:10</td></tr><tr><td><a href="librivox/index.html">librivox</a></td><td>Automated youtube uploading of audiobooks</td><td>Spenser Truex</td><td>2020-03-10 18:51</td></tr><tr><td><a href="lisp-multithreaded-internet/index.html">lisp-multithreaded-internet</a></td><td>Multithreaded internet for Common Lisp</td><td>Spenser Truex</td><td>2019-10-27 14:44</td></tr><tr><td><a href="LispBrain/index.html">LispBrain</a></td><td>The best Brainfuck debugger available, if you know how the Common Lisp debugger works. Uses Common Lisp&#x27;s ease of debugging to make debugging Brainfuck code easy. Almost entirely useless.</td><td>Spenser Truex</td><td>2019-09-07 13:45</td></tr><tr><td><a href="minimak-tty-xkb/index.html">minimak-tty-xkb</a></td><td>&quot;Check out my awesome website dude.&quot;</td><td>Spenser Truex</td><td>2021-01-15 17:23</td></tr><tr><td><a href="minimalist-template/index.html">minimalist-template</a></td><td>Another minimalist template for quickproject</td><td>Spenser Truex</td><td>2020-01-06 12:37</td></tr><tr><td><a href="nixtoo-genux/index.html">nixtoo-genux</a></td><td>Declarative Nix package manager setup. A new linux distro.</td><td>Spenser Truex</td><td>2025-09-04 16:14</td></tr><tr><td><a href="pass-simple/index.html">pass-simple</a></td><td>fork of zerous&#x27; minimal pass rewrite with bugfixes</td><td>Spenser Truex</td><td>2025-09-02 13:59</td></tr><tr><td><a href="plan9port-cluster/index.html">plan9port-cluster</a></td><td>Shared 9P storage and CPU offload for AI agent swarms, built on plan9port fossil + venti</td><td>Spenser Truex</td><td>2026-10-04 05:13</td></tr><tr><td><a href="posix-pipes/index.html">posix-pipes</a></td><td>portable pipes straight from CLOCC</td><td>Spenser Truex</td><td>2025-09-03 15:39</td></tr><tr><td><a href="prompt-mail/index.html">prompt-mail</a></td><td>Ask mail-related questions and filter mail-related files with dmenu</td><td>Spenser Truex</td><td>2025-09-05 01:58</td></tr><tr><td><a href="proxyverif/index.html">proxyverif</a></td><td>Check socks proxies whether they are alive or non-responsive  (ping through)</td><td>Spenser Truex</td><td>2019-03-31 23:21</td></tr><tr><td><a href="quickurl/index.html">quickurl</a></td><td></td><td>Spenser Truex</td><td>2019-12-20 22:18</td></tr><tr><td><a href="r6rs-complete/index.html">r6rs-complete</a></td><td>R6RS single document.</td><td>Spenser Truex</td><td>2019-11-24 14:44</td></tr><tr><td><a href="rebind/index.html">rebind</a></td><td>Rebind: remap the hardware buttons of e-ink readers and any Android 12+ device. Free beta APK.</td><td>Spenser Truex</td><td>2026-09-26 12:50</td></tr><tr><td><a href="recentlywritten/index.html">recentlywritten</a></td><td>my writings (done recently)</td><td>Spenser Truex</td><td>2026-10-04 23:04</td></tr><tr><td><a href="rectangle/index.html">rectangle</a></td><td>Spacemacs layer for binding rectangle interaction commands.</td><td>Spenser Truex</td><td>2020-07-24 20:43</td></tr><tr><td><a href="resume-template/index.html">resume-template</a></td><td>Resume template</td><td>Spenser Truex</td><td>2020-09-14 03:30</td></tr><tr><td><a href="sacc/index.html">sacc</a></td><td>Mirror of git://bitreich.org/sacc, the sacc gopher client from bitreich.org.</td><td>Spenser Truex</td><td>2025-03-31 17:55</td></tr><tr><td><a href="sacc-ebuild/index.html">sacc-ebuild</a></td><td>Mirrored on Github for normies.</td><td>Spenser Truex</td><td>2020-04-07 00:17</td></tr><tr><td><a href="sad/index.html">sad</a></td><td>sad (2f30.org simple audio daemon) with a Cygwin/OSS port on branch cygwin</td><td>Spenser Truex</td><td>2026-10-04 05:08</td></tr><tr><td><a href="sbm/index.html">sbm</a></td><td>dmenu bookmarks. Instantly fuzzy search thousands of bookmarks and plumb them. LOOKING FOR SUCKLESS SOFTWARE EDITION? GO TO sbm-suckless INSTEAD</td><td>Spenser Truex</td><td>2026-09-26 13:24</td></tr><tr><td><a href="sbm-android/index.html">sbm-android</a></td><td>sbm bookmarks for Android: fuzzy search, share to add, sync with bm and the browser add-on</td><td>Spenser Truex</td><td>2026-09-26 14:36</td></tr><tr><td><a href="sbm-extension/index.html">sbm-extension</a></td><td>sbm bookmarks for Firefox and Chrome: fuzzy search, add the page, sync with bm and the sbm app</td><td>Spenser Truex</td><td>2026-09-26 12:55</td></tr><tr><td><a href="sbm-suckless/index.html">sbm-suckless</a></td><td>sbm before the rewrite: bookmarks in a plain file, picked with dmenu. Suckless style.</td><td>Spenser Truex</td><td>2026-09-20 21:25</td></tr><tr><td><a href="sbm-sync/index.html">sbm-sync</a></td><td>Sync server for sbm bookmark files: one small Go program, plain files, AGPL</td><td>Spenser Truex</td><td>2026-10-01 23:22</td></tr><tr><td><a href="sbm-webpublish/index.html">sbm-webpublish</a></td><td>Publish an sbm bookmark file as a list of links on a web site</td><td>Spenser Truex</td><td>2026-09-26 12:50</td></tr><tr><td><a href="scan-subread/index.html">scan-subread</a></td><td>PDF reader that follows an audiobook with the SubRead suite</td><td>Spenser Truex</td><td>2026-10-04 21:17</td></tr><tr><td><a href="set-autocompact/index.html">set-autocompact</a></td><td>Set a 500k-token auto-compact threshold for Claude Code and Codex</td><td>Spenser Truex</td><td>2026-10-03 22:16</td></tr><tr><td><a href="sh/index.html">sh</a></td><td>Small shell and Python scripts</td><td>Spenser Truex</td><td>2026-10-04 23:03</td></tr><tr><td><a href="sigil/index.html">sigil</a></td><td>A documentation preprocessor for Common Lisp docs</td><td>Spenser Truex</td><td>2020-03-26 16:25</td></tr><tr><td><a href="skill-learning/index.html">skill-learning</a></td><td>Hermes-style self-improvement for AI coding agents: learn reusable skills from hard tasks</td><td>Spenser Truex</td><td>2026-10-03 21:49</td></tr><tr><td><a href="sl/index.html">sl</a></td><td>SL: Dependency language for Common Lisp</td><td>Spenser Truex</td><td>2019-09-11 17:08</td></tr><tr><td><a href="slurp/index.html">slurp</a></td><td>Spacemacs layer for slurping, inserting, copying, etc. of buffers (keybind layer).</td><td>Spenser Truex</td><td>2019-07-03 13:42</td></tr><tr><td><a href="sshp-ai/index.html">sshp-ai</a></td><td>Agent skill + read-only MCP server for parallel SSH fleet checks with bahamas10/sshp</td><td>Spenser Truex</td><td>2026-10-03 21:53</td></tr><tr><td><a href="stumpwm-init/index.html">stumpwm-init</a></td><td>I use dwm now. Archived.</td><td>Spenser Truex</td><td>2020-03-31 15:46</td></tr><tr><td><a href="subplz-web/index.html">subplz-web</a></td><td></td><td>Spenser Truex</td><td>2026-09-27 00:06</td></tr><tr><td><a href="subread-android/index.html">subread-android</a></td><td>SubRead for Android: times an audiobook against its ebook on the device</td><td>Spenser Truex</td><td>2026-09-26 19:13</td></tr><tr><td><a href="subread-anki/index.html">subread-anki</a></td><td>One tap makes an Anki card on Android: word, definition, sentence, screenshot, audio. Works with any app, with SubRead Dictionary and SubRead Overlay.</td><td>Spenser Truex</td><td>2026-09-26 20:53</td></tr><tr><td><a href="subread-dictionary/index.html">subread-dictionary</a></td><td>Pop-up dictionary for Android that reads Yomitan dictionaries, with local audio</td><td>Spenser Truex</td><td>2026-09-26 14:41</td></tr><tr><td><a href="subread-extension/index.html">subread-extension</a></td><td>Show your own .srt over a YouTube video, in time with it. Chrome and Firefox.</td><td>Spenser Truex</td><td>2026-09-26 12:52</td></tr><tr><td><a href="subread-overlay/index.html">subread-overlay</a></td><td>Shows the lines of an .srt over any Android media player, in time with it. Tap a word to look it up.</td><td>Spenser Truex</td><td>2026-09-26 13:45</td></tr><tr><td><a href="subread.koplugin/index.html">subread.koplugin</a></td><td>KOReader plugin: the book follows the narration of an audiobook, from an .srt made by subread.space</td><td>Spenser Truex</td><td>2026-09-26 12:58</td></tr><tr><td><a href="subread.space/index.html">subread.space</a></td><td>read subs in space</td><td>Spenser Truex</td><td>2026-09-26 12:50</td></tr><tr><td><a href="subrep-android/index.html">subrep-android</a></td><td>Subrep on Android: live captions of the sound of the phone, made on the phone by Whisper, shared by link.</td><td>Spenser Truex</td><td>2026-10-04 22:57</td></tr><tr><td><a href="tag-gen/index.html">tag-gen</a></td><td>Generate tags for each directory and say what programming languages to look for</td><td>Spenser Truex</td><td>2025-09-05 00:09</td></tr><tr><td><a href="timeclock/index.html">timeclock</a></td><td>Spacemacs layer for tracking your time.</td><td>Spenser Truex</td><td>2020-07-24 20:46</td></tr><tr><td><a href="tiny-runlevels/index.html">tiny-runlevels</a></td><td>Using an init system for many instances of simple programs . https://therealtruex.com/posts/Using-an-init-system-for-many-instances-of-simple-programs</td><td>Spenser Truex</td><td>2025-09-07 12:16</td></tr><tr><td><a href="tmpl/index.html">tmpl</a></td><td>Insanely simple templates </td><td>Spenser Truex</td><td>2026-09-26 23:42</td></tr><tr><td><a href="tnot/index.html">tnot</a></td><td>like tmux but not</td><td>Spenser Truex</td><td>2025-09-01 22:53</td></tr><tr><td><a href="vibeslop-dickt/index.html">vibeslop-dickt</a></td><td>make your own vibeslop dickt in just a few minutes</td><td>Spenser Truex</td><td>2026-05-22 00:48</td></tr><tr><td><a href="waybase/index.html">waybase</a></td><td>Bootable installer: KISS, Gentoo, Arch or Alpine + Nix + a Wayland compositor (sway, river, labwc, dwl)</td><td>Spenser Truex</td><td>2026-10-04 19:36</td></tr><tr><td><a href="xinput-persistence/index.html">xinput-persistence</a></td><td>xinput for mouse or keyboard configurations that work when disconnected or on boot.</td><td>Spenser Truex</td><td>2019-07-02 01:52</td></tr></tbody></table>
 </div></body></html>
diff --git a/git/sh/commit/309b45f58cb6a212844799171f165f4e67016abe.html b/git/sh/commit/309b45f58cb6a212844799171f165f4e67016abe.html
new file mode 100644
index 0000000..d0109ad
--- /dev/null
+++ b/git/sh/commit/309b45f58cb6a212844799171f165f4e67016abe.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<html lang="en"><head><meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1" />
+<title>sh 309b45f5 - Recently Written</title>
+<link rel="stylesheet" href="../../stagit.css" />
+</head><body>
+<div id="top"><a href="../../../index.html">Recently Written</a> &middot; <a href="../../index.html">git</a></div>
+<h1>sh</h1><span class="desc">Small shell and Python scripts</span><p class="url">git clone https://github.com/equwal/sh</p><p><a href="../../sh/index.html">Log</a> | <a href="../../sh/files.html">Files</a> | <a href="../../sh/refs.html">Refs</a></p><hr/>
+<div id="content">
+<pre>commit 309b45f58cb6a212844799171f165f4e67016abe
+equwal &lt;13551856+equwal@users.noreply.github.com&gt;
+2026-10-04 23:02:59 -0700
+
+http-collect: fix &amp;amp; escape, empty filename, socket cleanup, missing ip, non-tty QR, UTF-8 text
+
+</pre><pre> http-collect | 30 ++++++++++++++++--------------
+ 1 file changed, 16 insertions(+), 14 deletions(-)
+</pre><pre>diff --git a/http-collect b/http-collect
+index 6ca88e0..a6e586e 100755
+--- a/http-collect
++++ b/http-collect
+<span class="h">@@ -1,14 +1,15 @@</span>
+ #!/usr/bin/env python3
+<span class="a">+# Serve a secret URL (and QR code) where a phone uploads files/text to cwd.</span>
+<span class="a">+# Usage: http-collect [--host IP] [--port N] [--secret S]  Needs: flask, qrcode</span>
+ 
+ 
+ from argparse import ArgumentParser
+<span class="d">-from os import environ</span>
+<span class="d">-from os.path import isfile</span>
+<span class="a">+from os.path import lexists</span>
+ from random import randint
+ from re import match
+ from socket import socket, AF_INET, SOCK_STREAM
+ from subprocess import check_output
+<span class="d">-from sys import stderr</span>
+<span class="a">+from sys import stderr, stdout</span>
+ from time import sleep
+ from uuid import uuid4
+ 
+<span class="h">@@ -55,11 +56,11 @@ input, textarea {</span>
+ 
+ 
+ def sanitize_fs(s):
+<span class="d">-    return s.strip().lstrip(&#x27;.&#x27;).replace(&#x27;/&#x27;, &#x27;_&#x27;)</span>
+<span class="a">+    return s.strip().lstrip(&#x27;.&#x27;).replace(&#x27;/&#x27;, &#x27;_&#x27;) or &#x27;unnamed&#x27;</span>
+ 
+ 
+ def sanitize_html(s):
+<span class="d">-    return s.replace(&#x27;&amp;&#x27;, &#x27;&amp;amp&#x27;).replace(&#x27;&lt;&#x27;, &#x27;&amp;lt;&#x27;).replace(&#x27;&gt;&#x27;, &#x27;&amp;gt;&#x27;)</span>
+<span class="a">+    return s.replace(&#x27;&amp;&#x27;, &#x27;&amp;amp;&#x27;).replace(&#x27;&lt;&#x27;, &#x27;&amp;lt;&#x27;).replace(&#x27;&gt;&#x27;, &#x27;&amp;gt;&#x27;)</span>
+ 
+ 
+ def suffixify(basename, suff):
+<span class="h">@@ -73,7 +74,7 @@ def suffixify(basename, suff):</span>
+ def get_final_name(original_name):
+     candidate = base_name = sanitize_fs(original_name)
+     suff = 1
+<span class="d">-    while isfile(candidate):</span>
+<span class="a">+    while lexists(candidate):</span>
+         candidate = suffixify(base_name, suff)
+         suff += 1
+     return candidate
+<span class="h">@@ -89,7 +90,10 @@ def determine_host_and_port(args):</span>
+         port = args.port
+ 
+     if host is None:
+<span class="d">-        rt = check_output([&#x27;ip&#x27;, &#x27;-4&#x27;, &#x27;route&#x27;]).decode(&#x27;UTF-8&#x27;)</span>
+<span class="a">+        try:</span>
+<span class="a">+            rt = check_output([&#x27;ip&#x27;, &#x27;-4&#x27;, &#x27;route&#x27;]).decode(&#x27;UTF-8&#x27;)</span>
+<span class="a">+        except OSError:</span>
+<span class="a">+            rt = &#x27;&#x27;</span>
+         for line in rt.splitlines():
+             m = match(r&#x27;default.* src ([0-9.]+)&#x27;, line)
+             if m:
+<span class="h">@@ -107,9 +111,9 @@ def determine_host_and_port(args):</span>
+             try_port = port
+ 
+         try:
+<span class="d">-            s = socket(AF_INET, SOCK_STREAM)</span>
+<span class="d">-            s.bind((host, try_port))</span>
+<span class="d">-            s.listen()</span>
+<span class="a">+            with socket(AF_INET, SOCK_STREAM) as s:</span>
+<span class="a">+                s.bind((host, try_port))</span>
+<span class="a">+                s.listen()</span>
+         except Exception as e:
+             print(f&#x27;Failed to use {host}:{try_port}: {repr(e)}&#x27;)
+             sleep(1)
+<span class="h">@@ -117,8 +121,6 @@ def determine_host_and_port(args):</span>
+             port = try_port
+             success = True
+             break
+<span class="d">-        finally:</span>
+<span class="d">-            s.close()</span>
+ 
+     if not success:
+         raise Exception(&#x27;Could not find free port&#x27;)
+<span class="h">@@ -127,7 +129,7 @@ def determine_host_and_port(args):</span>
+ 
+     qr = QRCode()
+     qr.add_data(f&#x27;http://{host}:{port}/{secret}&#x27;)
+<span class="d">-    qr.print_ascii(tty=True)</span>
+<span class="a">+    qr.print_ascii(tty=stdout.isatty())</span>
+ 
+     print()
+     print(url)
+<span class="h">@@ -165,7 +167,7 @@ def save(secret_from_url):</span>
+ 
+     if request.form.get(&#x27;textarea&#x27;):
+         final_name = get_final_name(&#x27;plain.txt&#x27;)
+<span class="d">-        with open(final_name, &#x27;w&#x27;) as fp:</span>
+<span class="a">+        with open(final_name, &#x27;w&#x27;, encoding=&#x27;UTF-8&#x27;) as fp:</span>
+             fp.write(request.form.get(&#x27;textarea&#x27;))
+         saved.append(f&#x27;&lt;li&gt;{sanitize_html(final_name)}&lt;/li&gt;&#x27;)
+ </pre>
+</div></body></html>
diff --git a/git/sh/commit/453cd03c4a64a0b00bce0bd6e1f38e5f31600c06.html b/git/sh/commit/453cd03c4a64a0b00bce0bd6e1f38e5f31600c06.html
new file mode 100644
index 0000000..74cb132
--- /dev/null
+++ b/git/sh/commit/453cd03c4a64a0b00bce0bd6e1f38e5f31600c06.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html lang="en"><head><meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1" />
+<title>sh 453cd03c - Recently Written</title>
+<link rel="stylesheet" href="../../stagit.css" />
+</head><body>
+<div id="top"><a href="../../../index.html">Recently Written</a> &middot; <a href="../../index.html">git</a></div>
+<h1>sh</h1><span class="desc">Small shell and Python scripts</span><p class="url">git clone https://github.com/equwal/sh</p><p><a href="../../sh/index.html">Log</a> | <a href="../../sh/files.html">Files</a> | <a href="../../sh/refs.html">Refs</a></p><hr/>
+<div id="content">
+<pre>commit 453cd03c4a64a0b00bce0bd6e1f38e5f31600c06
+equwal &lt;13551856+equwal@users.noreply.github.com&gt;
+2026-10-04 23:02:59 -0700
+
+jpegify: POSIX sh, mktemp -d with trap cleanup, any input path, numeric page order
+
+</pre><pre> jpegify     | 24 +++++++++++-------------
+ jpegify-aux |  1 +
+ 2 files changed, 12 insertions(+), 13 deletions(-)
+</pre><pre>diff --git a/jpegify b/jpegify
+index 5768d5c..00d4b12 100755
+--- a/jpegify
++++ b/jpegify
+<span class="h">@@ -1,15 +1,13 @@</span>
+<span class="d">-#!/bin/bash</span>
+<span class="d">-TEMP=&quot;$(mktemp)&quot;</span>
+<span class="d">-[ -e &quot;$TEMP&quot; ] &amp;&amp; rm &quot;$TEMP&quot;</span>
+<span class="d">-mkdir &quot;$TEMP&quot;</span>
+<span class="d">-BASE=&quot;$(basename &quot;$1&quot;)&quot;</span>
+<span class="d">-cp &quot;$1&quot; &quot;$TEMP&quot;</span>
+<span class="a">+#!/bin/sh</span>
+<span class="a">+# Shrink a PDF by re-rendering every page as a JPEG (via jpegify-aux).</span>
+<span class="a">+# Usage: jpegify FILE.pdf   (writes ./FILE.pdf-jpeg.pdf)</span>
+<span class="a">+set -e</span>
+<span class="a">+TEMP=&quot;$(mktemp -d)&quot;</span>
+<span class="a">+trap &#x27;rm -r &quot;$TEMP&quot;&#x27; EXIT</span>
+<span class="a">+OUT=&quot;$PWD/$(basename &quot;$1&quot;)-jpeg.pdf&quot;</span>
+<span class="a">+pdfseparate &quot;$1&quot; &quot;$TEMP&quot;/%d.pdf</span>
+ 
+ cd &quot;$TEMP&quot;
+<span class="d">-pdfseparate &quot;$PWD&quot;/&quot;$1&quot; %d.pdf</span>
+<span class="d">-rm &quot;$BASE&quot;</span>
+<span class="d">-find . -name &#x27;*.pdf&#x27; -print0 | grep -z -v jpeg | xargs -0 -L1 jpegify-aux</span>
+<span class="d">-cat &lt;(find . -name &#x27;*-jpeg.pdf&#x27; | sed &#x27;s:^./::&#x27; | sort -h | sed &#x27;s:^\|$:&quot;:g&#x27;) &lt;(echo &quot;$BASE&quot;-jpeg.pdf) | xargs pdfunite</span>
+<span class="d">-cd -</span>
+<span class="d">-mv &quot;$TEMP&quot;/&quot;$BASE&quot;-jpeg.pdf .</span>
+<span class="d">-rm -r &quot;$TEMP&quot;</span>
+<span class="a">+for p in *.pdf; do jpegify-aux &quot;$p&quot;; done</span>
+<span class="a">+# shellcheck disable=SC2046 # page files are N.pdf-jpeg.pdf: no spaces</span>
+<span class="a">+pdfunite $(printf &#x27;%s\n&#x27; *-jpeg.pdf | sort -n) &quot;$OUT&quot;</span>
+diff --git a/jpegify-aux b/jpegify-aux
+index 2690656..0a8ff86 100755
+--- a/jpegify-aux
++++ b/jpegify-aux
+<span class="h">@@ -1,2 +1,3 @@</span>
+ #!/bin/sh
+<span class="a">+# Re-render one PDF as 100 dpi JPEG pages. Usage: jpegify-aux file.pdf -&gt; file.pdf-jpeg.pdf</span>
+ convert -density 100 -compress JPEG  -quality 70 &quot;$1&quot; &quot;$1&quot;-jpeg.pdf</pre>
+</div></body></html>
diff --git a/git/sh/commit/64809ce9eb3bc72801d5392a38eb3f8bf8da8bd5.html b/git/sh/commit/64809ce9eb3bc72801d5392a38eb3f8bf8da8bd5.html
new file mode 100644
index 0000000..d63dec4
--- /dev/null
+++ b/git/sh/commit/64809ce9eb3bc72801d5392a38eb3f8bf8da8bd5.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html lang="en"><head><meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1" />
+<title>sh 64809ce9 - Recently Written</title>
+<link rel="stylesheet" href="../../stagit.css" />
+</head><body>
+<div id="top"><a href="../../../index.html">Recently Written</a> &middot; <a href="../../index.html">git</a></div>
+<h1>sh</h1><span class="desc">Small shell and Python scripts</span><p class="url">git clone https://github.com/equwal/sh</p><p><a href="../../sh/index.html">Log</a> | <a href="../../sh/files.html">Files</a> | <a href="../../sh/refs.html">Refs</a></p><hr/>
+<div id="content">
+<pre>commit 64809ce9eb3bc72801d5392a38eb3f8bf8da8bd5
+equwal &lt;13551856+equwal@users.noreply.github.com&gt;
+2026-10-04 23:03:00 -0700
+
+slock-usb-key: clear error when the key file is missing, one slock per disk event, killall from PATH
+
+</pre><pre> slock-usb-key | 13 +++++++++----
+ 1 file changed, 9 insertions(+), 4 deletions(-)
+</pre><pre>diff --git a/slock-usb-key b/slock-usb-key
+index 6aed366..bc85af4 100755
+--- a/slock-usb-key
++++ b/slock-usb-key
+<span class="h">@@ -1,4 +1,6 @@</span>
+ #!/usr/bin/env python3
+<span class="a">+# Lock the screen with slock when a USB key is unplugged; unlock when replugged.</span>
+<span class="a">+# Usage: put the key&#x27;s ID_SERIAL in ~/.slock-usb-key; allow NOPASSWD sudo killall.</span>
+ 
+ 
+ from os.path import expanduser
+<span class="h">@@ -10,15 +12,18 @@ import pyudev</span>
+ 
+ signal.signal(signal.SIGCHLD, signal.SIG_IGN)
+ 
+<span class="d">-with open(expanduser(&#x27;~/.slock-usb-key&#x27;), &#x27;r&#x27;) as fp:</span>
+<span class="d">-    serial = fp.read().strip()</span>
+<span class="a">+try:</span>
+<span class="a">+    with open(expanduser(&#x27;~/.slock-usb-key&#x27;), &#x27;r&#x27;) as fp:</span>
+<span class="a">+        serial = fp.read().strip()</span>
+<span class="a">+except OSError as e:</span>
+<span class="a">+    raise SystemExit(f&#x27;slock-usb-key: {e}: put the key ID_SERIAL there&#x27;)</span>
+ 
+ context = pyudev.Context()
+ monitor = pyudev.Monitor.from_netlink(context)
+<span class="d">-monitor.filter_by(&#x27;block&#x27;)</span>
+<span class="a">+monitor.filter_by(&#x27;block&#x27;, &#x27;disk&#x27;)</span>
+ for device in iter(monitor.poll, None):
+     if device.get(&#x27;ID_SERIAL&#x27;) == serial:
+         if device.action == &#x27;add&#x27;:
+<span class="d">-            Popen([&#x27;sudo&#x27;, &#x27;/usr/bin/killall&#x27;, &#x27;slock&#x27;])</span>
+<span class="a">+            Popen([&#x27;sudo&#x27;, &#x27;killall&#x27;, &#x27;slock&#x27;])</span>
+         elif device.action == &#x27;remove&#x27;:
+             Popen([&#x27;slock&#x27;])</pre>
+</div></body></html>
diff --git a/git/sh/commit/751d46dcf747b5681d8ab58ccf1e36a8b8e67669.html b/git/sh/commit/751d46dcf747b5681d8ab58ccf1e36a8b8e67669.html
new file mode 100644
index 0000000..78fbf64
--- /dev/null
+++ b/git/sh/commit/751d46dcf747b5681d8ab58ccf1e36a8b8e67669.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en"><head><meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1" />
+<title>sh 751d46dc - Recently Written</title>
+<link rel="stylesheet" href="../../stagit.css" />
+</head><body>
+<div id="top"><a href="../../../index.html">Recently Written</a> &middot; <a href="../../index.html">git</a></div>
+<h1>sh</h1><span class="desc">Small shell and Python scripts</span><p class="url">git clone https://github.com/equwal/sh</p><p><a href="../../sh/index.html">Log</a> | <a href="../../sh/files.html">Files</a> | <a href="../../sh/refs.html">Refs</a></p><hr/>
+<div id="content">
+<pre>commit 751d46dcf747b5681d8ab58ccf1e36a8b8e67669
+equwal &lt;13551856+equwal@users.noreply.github.com&gt;
+2026-10-04 22:43:16 -0700
+
+Start public scripts repo
+
+</pre><pre></pre><pre></pre>
+</div></body></html>
diff --git a/git/sh/commit/7afdbe4c30ec35643f04270d4cb366e9f5f21f2d.html b/git/sh/commit/7afdbe4c30ec35643f04270d4cb366e9f5f21f2d.html
new file mode 100644
index 0000000..65d7246
--- /dev/null
+++ b/git/sh/commit/7afdbe4c30ec35643f04270d4cb366e9f5f21f2d.html
@@ -0,0 +1,1418 @@
+<!DOCTYPE html>
+<html lang="en"><head><meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1" />
+<title>sh 7afdbe4c - Recently Written</title>
+<link rel="stylesheet" href="../../stagit.css" />
+</head><body>
+<div id="top"><a href="../../../index.html">Recently Written</a> &middot; <a href="../../index.html">git</a></div>
+<h1>sh</h1><span class="desc">Small shell and Python scripts</span><p class="url">git clone https://github.com/equwal/sh</p><p><a href="../../sh/index.html">Log</a> | <a href="../../sh/files.html">Files</a> | <a href="../../sh/refs.html">Refs</a></p><hr/>
+<div id="content">
+<pre>commit 7afdbe4c30ec35643f04270d4cb366e9f5f21f2d
+equwal &lt;13551856+equwal@users.noreply.github.com&gt;
+2026-10-04 23:02:26 -0700
+
+Remove bm, sbm, mpdmenu, windervolage: not this repo&#x27;s code (equwal/sbm copies, Chris Down&#x27;s mpdmenu, markus schnalke&#x27;s wv)
+
+</pre><pre> bm               | 686 -------------------------------------------------------
+ git-update-repos |   0
+ mpdmenu          |  76 ------
+ sbm/bm-commit    |  36 ---
+ sbm/bm-page      | 212 -----------------
+ sbm/bm-sync      | 248 --------------------
+ sbm/bm-title     |  33 ---
+ windervolage     |  57 -----
+ 8 files changed, 1348 deletions(-)
+</pre><pre>diff --git a/bm b/bm
+deleted file mode 100755
+index 5b5f090..0000000
+--- a/bm
++++ /dev/null
+<span class="h">@@ -1,686 +0,0 @@</span>
+<span class="d">-#!/bin/sh</span>
+<span class="d">-</span>
+<span class="d">-# bm: bookmarks in a plain file, picked with dmenu (or fzf).</span>
+<span class="d">-#</span>
+<span class="d">-#     URL&lt;tab&gt;description&lt;tab&gt;tag tag tag</span>
+<span class="d">-#</span>
+<span class="d">-# bm owns that file and nothing else. Importing, link checking, the web page</span>
+<span class="d">-# and so on are separate bm-* filters; see README.</span>
+<span class="d">-</span>
+<span class="d">-DMENULINES=${DMENULINES:-15}</span>
+<span class="d">-DATADIR=${XDG_DATA_HOME:-$HOME/.local/share}/sbm</span>
+<span class="d">-BOOKMARKS=${BOOKMARKS:-$DATADIR/bookmarks}</span>
+<span class="d">-USERTAGS=${USERTAGS:-$DATADIR/usertags}</span>
+<span class="d">-ENGINES=${SBM_ENGINES:-$DATADIR/engines}</span>
+<span class="d">-SEARCH=${SBM_SEARCH:-https://duckduckgo.com/?q=%s}</span>
+<span class="d">-USAGE=$BOOKMARKS.usage</span>
+<span class="d">-WLOCK=$BOOKMARKS.lock</span>
+<span class="d">-locked=</span>
+<span class="d">-TAB=$(printf &#x27;\t&#x27;)</span>
+<span class="d">-</span>
+<span class="d">-# Two URLs are the same bookmark when they differ only in scheme, a leading</span>
+<span class="d">-# www., trailing slashes or the case of the host.</span>
+<span class="d">-AWK_NORM=&#x27;</span>
+<span class="d">-function norm(u,    host, rest, i) {</span>
+<span class="d">-    sub(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//, &quot;&quot;, u)</span>
+<span class="d">-    i = index(u, &quot;/&quot;)</span>
+<span class="d">-    if (i) { host = substr(u, 1, i - 1); rest = substr(u, i) }</span>
+<span class="d">-    else   { host = u; rest = &quot;&quot; }</span>
+<span class="d">-    host = tolower(host)</span>
+<span class="d">-    sub(/^www\./, &quot;&quot;, host)</span>
+<span class="d">-    sub(/\/+$/, &quot;&quot;, rest)</span>
+<span class="d">-    return host rest</span>
+<span class="d">-}&#x27;</span>
+<span class="d">-</span>
+<span class="d">-die () {</span>
+<span class="d">-    printf &#x27;bm: %s\n&#x27; &quot;$*&quot; &gt;&amp;2</span>
+<span class="d">-    exit 1</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-have () {</span>
+<span class="d">-    command -v &quot;$1&quot; &gt;/dev/null 2&gt;&amp;1</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-usage () {</span>
+<span class="d">-    cat &lt;&lt;&#x27;EOF&#x27;</span>
+<span class="d">-usage: bm [-t tag] [-S order] [action]</span>
+<span class="d">-Without an action: browse. Pick a bookmark, then what to do with it. Text</span>
+<span class="d">-that is no bookmark is opened as an address, or searched for on the web.</span>
+<span class="d">-  -o, --open        open a bookmark, an address or a web search</span>
+<span class="d">-                    (-p, --plumb is the same)</span>
+<span class="d">-  -c, --copy        copy a bookmark&#x27;s URL to the clipboard</span>
+<span class="d">-      --print       print a bookmark&#x27;s URL</span>
+<span class="d">-  -a, --add [url]   add a bookmark; the url defaults to the selection</span>
+<span class="d">-  -e, --edit        open the bookmark file in $EDITOR at a bookmark</span>
+<span class="d">-  -d, --delete      delete a bookmark</span>
+<span class="d">-  -l, --list        print the bookmarks</span>
+<span class="d">-  -m, --merge       add the bookmarks on stdin, skipping the known ones</span>
+<span class="d">-  -t, --tag &lt;tag&gt;   only consider bookmarks with this tag</span>
+<span class="d">-  -S, --sort &lt;o&gt;    used (default), recent, url, desc or tag</span>
+<span class="d">-  -h, --help</span>
+<span class="d">-EOF</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# ---- external programs: menu, clipboard, opener ----</span>
+<span class="d">-</span>
+<span class="d">-# dmenu when there is a display to draw it on and bm does not run in a</span>
+<span class="d">-# terminal, for example from a key of the window manager. In a terminal fzf</span>
+<span class="d">-# is the menu, because its preview shows the page of the bookmark under the</span>
+<span class="d">-# cursor. fzf also stands in when dmenu is not installed.</span>
+<span class="d">-pickmenu () {</span>
+<span class="d">-    [ -n &quot;$SBM_MENU&quot; ] &amp;&amp; return</span>
+<span class="d">-    if [ -n &quot;$DISPLAY$WAYLAND_DISPLAY&quot; ] &amp;&amp; ! { [ -t 0 ] &amp;&amp; [ -t 2 ]; }; then</span>
+<span class="d">-        order=&#x27;dmenu fzf&#x27;</span>
+<span class="d">-    else</span>
+<span class="d">-        order=&#x27;fzf dmenu&#x27;</span>
+<span class="d">-    fi</span>
+<span class="d">-    for menu in $order; do</span>
+<span class="d">-        if have &quot;$menu&quot;; then</span>
+<span class="d">-            SBM_MENU=$menu</span>
+<span class="d">-            return</span>
+<span class="d">-        fi</span>
+<span class="d">-    done</span>
+<span class="d">-    # shellcheck disable=SC2016</span>
+<span class="d">-    die &#x27;no menu program found: install dmenu or fzf, or set $SBM_MENU&#x27;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# $SBM_MENU may carry arguments, so it is split on purpose.</span>
+<span class="d">-custommenu () {</span>
+<span class="d">-    # shellcheck disable=SC2086</span>
+<span class="d">-    $SBM_MENU &quot;$@&quot;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-fzfcmd () {</span>
+<span class="d">-    # shellcheck disable=SC2086</span>
+<span class="d">-    fzf -i --reverse --height=40% $SBM_FZF_OPTS &quot;$@&quot;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# fzfpick &lt;fzf options&gt;: the chosen line, or else what was typed, which is</span>
+<span class="d">-# what dmenu gives by itself.</span>
+<span class="d">-fzfpick () {</span>
+<span class="d">-    out=$(fzfcmd --print-query &quot;$@&quot;)</span>
+<span class="d">-    chosen=$(printf &#x27;%s\n&#x27; &quot;$out&quot; | sed -n 2p)</span>
+<span class="d">-    printf &#x27;%s\n&#x27; &quot;${chosen:-$(printf &#x27;%s\n&#x27; &quot;$out&quot; | sed -n 1p)}&quot;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# pick &lt;prompt&gt;: choices on stdin; the chosen line, or the typed text.</span>
+<span class="d">-pick () {</span>
+<span class="d">-    case $SBM_MENU in</span>
+<span class="d">-        fzf)   fzfpick --prompt=&quot;$1 &quot; ;;</span>
+<span class="d">-        dmenu) dmenu -i -l &quot;$DMENULINES&quot; -p &quot;$1&quot; ;;</span>
+<span class="d">-        *)     custommenu pick &quot;$1&quot; ;;</span>
+<span class="d">-    esac</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# pickmulti &lt;prompt&gt;: like pick, for any number of lines.</span>
+<span class="d">-pickmulti () {</span>
+<span class="d">-    choices=$(cat)</span>
+<span class="d">-    case $SBM_MENU in</span>
+<span class="d">-        fzf)</span>
+<span class="d">-            while :; do</span>
+<span class="d">-                out=$(printf &#x27;%s\n&#x27; &quot;$choices&quot; | fzfcmd --multi --print-query \</span>
+<span class="d">-                      --prompt=&quot;$1 &quot; --header=&#x27;TAB marks, ENTER confirms. No match: ENTER creates the typed tag, ESC finishes.&#x27;)</span>
+<span class="d">-                chosen=$(printf &#x27;%s\n&#x27; &quot;$out&quot; | sed 1d)</span>
+<span class="d">-                if [ -n &quot;$chosen&quot; ]; then</span>
+<span class="d">-                    printf &#x27;%s\n&#x27; &quot;$chosen&quot;</span>
+<span class="d">-                    break</span>
+<span class="d">-                fi</span>
+<span class="d">-                typed=$(printf &#x27;%s\n&#x27; &quot;$out&quot; | sed -n 1p)</span>
+<span class="d">-                [ -n &quot;$typed&quot; ] || break</span>
+<span class="d">-                printf &#x27;%s\n&#x27; &quot;$typed&quot;</span>
+<span class="d">-            done</span>
+<span class="d">-            ;;</span>
+<span class="d">-        dmenu)</span>
+<span class="d">-            while :; do</span>
+<span class="d">-                one=$(printf &#x27;done | (choose when done)\n%s\n&#x27; &quot;$choices&quot; \</span>
+<span class="d">-                      | dmenu -i -l &quot;$DMENULINES&quot; -p &quot;$1&quot;)</span>
+<span class="d">-                # An empty answer means the menu was closed.</span>
+<span class="d">-                [ -z &quot;$one&quot; ] || [ &quot;${one%% *}&quot; = &#x27;done&#x27; ] &amp;&amp; break</span>
+<span class="d">-                printf &#x27;%s\n&#x27; &quot;$one&quot;</span>
+<span class="d">-            done</span>
+<span class="d">-            ;;</span>
+<span class="d">-        *)  printf &#x27;%s\n&#x27; &quot;$choices&quot; | custommenu multi &quot;$1&quot; ;;</span>
+<span class="d">-    esac</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# ask &lt;prompt&gt; [default]: free text on stdout.</span>
+<span class="d">-ask () {</span>
+<span class="d">-    case $SBM_MENU in</span>
+<span class="d">-        fzf)</span>
+<span class="d">-            printf &#x27;%s%s: &#x27; &quot;$1&quot; &quot;${2:+ [$2]}&quot; &gt;&amp;2</span>
+<span class="d">-            IFS= read -r reply || reply=</span>
+<span class="d">-            printf &#x27;%s\n&#x27; &quot;${reply:-$2}&quot;</span>
+<span class="d">-            ;;</span>
+<span class="d">-        dmenu)</span>
+<span class="d">-            if [ -n &quot;$2&quot; ]; then printf &#x27;%s\n&#x27; &quot;$2&quot;; fi | dmenu -p &quot;$1:&quot;</span>
+<span class="d">-            ;;</span>
+<span class="d">-        *)</span>
+<span class="d">-            if [ -n &quot;$2&quot; ]; then printf &#x27;%s\n&#x27; &quot;$2&quot;; fi | custommenu ask &quot;$1&quot;</span>
+<span class="d">-            ;;</span>
+<span class="d">-    esac</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-clip_out () {</span>
+<span class="d">-    # shellcheck disable=SC2086,SC2016</span>
+<span class="d">-    if   [ -n &quot;$SBM_COPY&quot; ]; then $SBM_COPY</span>
+<span class="d">-    elif [ -n &quot;$WAYLAND_DISPLAY&quot; ] &amp;&amp; have wl-copy; then wl-copy</span>
+<span class="d">-    elif have xclip; then xclip -i -selection clipboard</span>
+<span class="d">-    elif have xsel; then xsel -ib</span>
+<span class="d">-    else die &#x27;no clipboard program found: set $SBM_COPY&#x27;</span>
+<span class="d">-    fi</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# The primary selection first, then the clipboard. Never fatal: without</span>
+<span class="d">-# either there is just no URL to suggest.</span>
+<span class="d">-clip_in () {</span>
+<span class="d">-    # shellcheck disable=SC2086</span>
+<span class="d">-    if   [ -n &quot;$SBM_PASTE&quot; ]; then $SBM_PASTE</span>
+<span class="d">-    elif [ -n &quot;$WAYLAND_DISPLAY&quot; ] &amp;&amp; have wl-paste; then</span>
+<span class="d">-        wl-paste -n -p; echo; wl-paste -n</span>
+<span class="d">-    elif have xclip; then</span>
+<span class="d">-        xclip -o; echo; xclip -o -selection clipboard</span>
+<span class="d">-    elif have xsel; then</span>
+<span class="d">-        xsel -op; echo; xsel -ob</span>
+<span class="d">-    fi 2&gt;/dev/null</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-opener () {</span>
+<span class="d">-    # shellcheck disable=SC2086,SC2016</span>
+<span class="d">-    if   [ -n &quot;$SBM_OPEN&quot; ]; then $SBM_OPEN &quot;$1&quot;</span>
+<span class="d">-    elif have xdg-open; then xdg-open &quot;$1&quot;</span>
+<span class="d">-    else die &#x27;no opener found: set $SBM_OPEN&#x27;</span>
+<span class="d">-    fi</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# ---- the bookmark file ----</span>
+<span class="d">-</span>
+<span class="d">-# Bookmarks restricted to $tagfilter and ordered by $sortorder, each followed</span>
+<span class="d">-# by a fourth field: how often it was used.</span>
+<span class="d">-rows () {</span>
+<span class="d">-    SBM_TAG=$tagfilter SBM_ORDER=$sortorder SBM_USAGE=$USAGE awk -F&#x27;\t&#x27; &#x27;</span>
+<span class="d">-        BEGIN {</span>
+<span class="d">-            while ((getline line &lt; ENVIRON[&quot;SBM_USAGE&quot;]) &gt; 0) {</span>
+<span class="d">-                split(line, f, &quot;\t&quot;)</span>
+<span class="d">-                used[f[1]] = f[2]</span>
+<span class="d">-            }</span>
+<span class="d">-            order = ENVIRON[&quot;SBM_ORDER&quot;]</span>
+<span class="d">-        }</span>
+<span class="d">-        NF == 0 || /^#/ { next }</span>
+<span class="d">-        ENVIRON[&quot;SBM_TAG&quot;] != &quot;&quot; {</span>
+<span class="d">-            hit = 0</span>
+<span class="d">-            n = split($3, tags, &quot; &quot;)</span>
+<span class="d">-            for (i = 1; i &lt;= n; i++)</span>
+<span class="d">-                if (tags[i] == ENVIRON[&quot;SBM_TAG&quot;]) hit = 1</span>
+<span class="d">-            if (!hit) next</span>
+<span class="d">-        }</span>
+<span class="d">-        {</span>
+<span class="d">-            if (order == &quot;used&quot;)        key = sprintf(&quot;%09d&quot;, 999999999 - used[$1])</span>
+<span class="d">-            else if (order == &quot;recent&quot;) key = sprintf(&quot;%09d&quot;, 999999999 - NR)</span>
+<span class="d">-            else if (order == &quot;desc&quot;)   key = tolower($2)</span>
+<span class="d">-            else if (order == &quot;tag&quot;)    key = tolower($3)</span>
+<span class="d">-            else                        key = &quot;&quot;</span>
+<span class="d">-            printf &quot;%s\t%s\t%s\t%s\t%s\t%d\n&quot;, key, tolower($1), $1, $2, $3, used[$1]</span>
+<span class="d">-        }&#x27; &quot;$BOOKMARKS&quot; | LC_ALL=C sort -t&quot;$TAB&quot; -k1,1 -k2,2 | cut -f3-</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# details &lt;url&gt;: a bookmark in full, one field per line.</span>
+<span class="d">-details () {</span>
+<span class="d">-    rows | SBM_URL=$1 awk -F&#x27;\t&#x27; &#x27;$1 == ENVIRON[&quot;SBM_URL&quot;] {</span>
+<span class="d">-        printf &quot;%s\n%s\ntags: %s\nused: %d\n&quot;, $1, $2, $3, $4</span>
+<span class="d">-        exit</span>
+<span class="d">-    }&#x27;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-is_bookmark () {</span>
+<span class="d">-    SBM_URL=$1 awk -F&#x27;\t&#x27; &#x27;$1 == ENVIRON[&quot;SBM_URL&quot;] { found = 1; exit }</span>
+<span class="d">-        END { exit !found }&#x27; &quot;$BOOKMARKS&quot;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# For fzf a fifth field is added, and it is all that is listed. The first</span>
+<span class="d">-# four feed the preview, which follows the cursor: the bookmark in full, then</span>
+<span class="d">-# the text of its page when bm-page is installed. fzf stops the preview when</span>
+<span class="d">-# the cursor moves on, so the short sleep keeps a fast scroll from fetching</span>
+<span class="d">-# each page on the way. A sleep that knows no fractions fails and skips it.</span>
+<span class="d">-PREVIEW=&quot;printf &#x27;%s\n%s\ntags: %s\nused: %s\n\n&#x27; {1} {2} {3} {4}; sleep 0.3 2&gt;/dev/null; bm-page {1} 2&gt;/dev/null&quot;</span>
+<span class="d">-fzfrows () {</span>
+<span class="d">-    awk -F&#x27;\t&#x27; &#x27;NF {</span>
+<span class="d">-        desc = $2</span>
+<span class="d">-        if (length(desc) &gt; 44) desc = substr(desc, 1, 42) &quot;..&quot;</span>
+<span class="d">-        printf &quot;%s\t%-44s  %-18s  %s\n&quot;, $0, desc, $3, $1</span>
+<span class="d">-    }&#x27;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# withpreview &lt;fzf command and options&gt;: for the output of fzfrows.</span>
+<span class="d">-withpreview () {</span>
+<span class="d">-    &quot;$@&quot; --delimiter=&quot;$TAB&quot; --with-nth=5.. \</span>
+<span class="d">-         --preview-window=right:50%:wrap --preview=&quot;$PREVIEW&quot;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# choose &lt;prompt&gt;: a bookmark&#x27;s URL, or whatever else the user typed.</span>
+<span class="d">-choose () {</span>
+<span class="d">-    if [ &quot;$SBM_MENU&quot; = fzf ]; then</span>
+<span class="d">-        rows | fzfrows | withpreview fzfpick --prompt=&quot;$1 &quot;</span>
+<span class="d">-    else</span>
+<span class="d">-        rows | cut -f1-3 | pick &quot;$1&quot;</span>
+<span class="d">-    fi | cut -f1</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# choose_url &lt;prompt&gt;: like choose, but only a bookmark will do.</span>
+<span class="d">-choose_url () {</span>
+<span class="d">-    if [ -z &quot;$(rows)&quot; ]; then</span>
+<span class="d">-        [ -n &quot;$tagfilter&quot; ] &amp;&amp; die &quot;no bookmarks tagged &#x27;$tagfilter&#x27;&quot;</span>
+<span class="d">-        die &#x27;no bookmarks yet; add one with: bm --add&#x27;</span>
+<span class="d">-    fi</span>
+<span class="d">-    url=$(choose &quot;$1&quot;)</span>
+<span class="d">-    [ -n &quot;$url&quot; ] || exit 1</span>
+<span class="d">-    is_bookmark &quot;$url&quot; || die &quot;not a bookmark: $url&quot;</span>
+<span class="d">-    printf &#x27;%s\n&#x27; &quot;$url&quot;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# find_dup &lt;url&gt;: print the bookmark that is the same page as &lt;url&gt;, if any.</span>
+<span class="d">-find_dup () {</span>
+<span class="d">-    SBM_URL=$1 awk -F&#x27;\t&#x27; &quot;$AWK_NORM&quot;&#x27;</span>
+<span class="d">-        BEGIN { want = norm(ENVIRON[&quot;SBM_URL&quot;]) }</span>
+<span class="d">-        NF &amp;&amp; !/^#/ &amp;&amp; norm($1) == want { print; exit }&#x27; &quot;$BOOKMARKS&quot;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# tmpfile &lt;file&gt;: create an empty temporary file and print its name. Beside</span>
+<span class="d">-# &lt;file&gt; if that directory can be written to, else in $TMPDIR. noclobber makes</span>
+<span class="d">-# the shell create it with O_EXCL, so a file or symlink planted under that</span>
+<span class="d">-# name in a shared /tmp is refused rather than followed: mktemp, in POSIX.</span>
+<span class="d">-tmpfile () {</span>
+<span class="d">-    for candidate in &quot;$1.tmp.$$&quot; &quot;${TMPDIR:-/tmp}/sbm.$$.${1##*/}&quot;; do</span>
+<span class="d">-        if (umask 077; set -C; : &gt; &quot;$candidate&quot;) 2&gt;/dev/null; then</span>
+<span class="d">-            printf &#x27;%s\n&#x27; &quot;$candidate&quot;</span>
+<span class="d">-            return 0</span>
+<span class="d">-        fi</span>
+<span class="d">-    done</span>
+<span class="d">-    return 1</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# rewrite &lt;file&gt;: replace &lt;file&gt; with stdin. Through cat, not mv, so that a</span>
+<span class="d">-# symlinked file stays a symlink and keeps its mode.</span>
+<span class="d">-rewrite () {</span>
+<span class="d">-    tmp=$(tmpfile &quot;$1&quot;) || die &quot;cannot create a temporary file for $1&quot;</span>
+<span class="d">-    cat &gt; &quot;$tmp&quot; &amp;&amp; cat &quot;$tmp&quot; &gt; &quot;$1&quot;</span>
+<span class="d">-    rc=$?</span>
+<span class="d">-    rm -f &quot;$tmp&quot;</span>
+<span class="d">-    return $rc</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# lock: take the write lock of the bookmark file, a directory beside it. bm</span>
+<span class="d">-# holds it while it changes the file, and bm-sync while it reads or writes</span>
+<span class="d">-# the file, so that neither writes over a change of the other. A lock whose</span>
+<span class="d">-# process is gone is taken over. After $SBM_LOCK_WAIT seconds (default 10)</span>
+<span class="d">-# bm stops.</span>
+<span class="d">-lock () {</span>
+<span class="d">-    waited=0</span>
+<span class="d">-    until mkdir &quot;$WLOCK&quot; 2&gt;/dev/null; do</span>
+<span class="d">-        pid=$(cat &quot;$WLOCK/pid&quot; 2&gt;/dev/null)</span>
+<span class="d">-        if [ -n &quot;$pid&quot; ] &amp;&amp; ! kill -0 &quot;$pid&quot; 2&gt;/dev/null; then</span>
+<span class="d">-            [ &quot;$(cat &quot;$WLOCK/pid&quot; 2&gt;/dev/null)&quot; != &quot;$pid&quot; ] || rm -rf &quot;$WLOCK&quot;</span>
+<span class="d">-            continue</span>
+<span class="d">-        fi</span>
+<span class="d">-        [ &quot;$waited&quot; -lt &quot;${SBM_LOCK_WAIT:-10}&quot; ] \</span>
+<span class="d">-            || die &quot;$BOOKMARKS is in use (bm -e or bm-sync). If neither runs, remove $WLOCK&quot;</span>
+<span class="d">-        sleep 1</span>
+<span class="d">-        waited=$((waited + 1))</span>
+<span class="d">-    done</span>
+<span class="d">-    echo $$ &gt; &quot;$WLOCK/pid&quot;</span>
+<span class="d">-    locked=1</span>
+<span class="d">-    # Also in the subshell of add_inside, which does not keep these traps.</span>
+<span class="d">-    trap unlock EXIT</span>
+<span class="d">-    trap &#x27;exit 1&#x27; HUP INT TERM</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-unlock () {</span>
+<span class="d">-    [ -z &quot;$locked&quot; ] || rm -rf &quot;$WLOCK&quot;</span>
+<span class="d">-    locked=</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# bump &lt;url&gt;: count a use, for the &quot;used&quot; order. The counts live beside the</span>
+<span class="d">-# bookmark file, which stays a plain list: url, count, time of last use.</span>
+<span class="d">-# srand() returns the previous seed, and the first seed is the time of day:</span>
+<span class="d">-# that is &quot;date +%s&quot; for a POSIX that has no %s.</span>
+<span class="d">-bump () {</span>
+<span class="d">-    [ -e &quot;$USAGE&quot; ] || : &gt; &quot;$USAGE&quot; || return 0</span>
+<span class="d">-    SBM_URL=$1 awk -F&#x27;\t&#x27; &#x27;</span>
+<span class="d">-        BEGIN { OFS = &quot;\t&quot;; srand(); now = srand() }</span>
+<span class="d">-        $1 == ENVIRON[&quot;SBM_URL&quot;] { $2 = $2 + 1; $3 = now; seen = 1 }</span>
+<span class="d">-        { print }</span>
+<span class="d">-        END { if (!seen) print ENVIRON[&quot;SBM_URL&quot;], 1, now }</span>
+<span class="d">-    &#x27; &quot;$USAGE&quot; | rewrite &quot;$USAGE&quot;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# learn_tags: tag names on stdin; the ones $USERTAGS lacks are appended to it.</span>
+<span class="d">-learn_tags () {</span>
+<span class="d">-    [ -e &quot;$USERTAGS&quot; ] || : &gt; &quot;$USERTAGS&quot;</span>
+<span class="d">-    while IFS= read -r name; do</span>
+<span class="d">-        [ -n &quot;$name&quot; ] || continue</span>
+<span class="d">-        sed &#x27;s/[ |].*//&#x27; &quot;$USERTAGS&quot; | grep -Fxq -- &quot;$name&quot; \</span>
+<span class="d">-            || printf &#x27;%s | \n&#x27; &quot;$name&quot; &gt;&gt; &quot;$USERTAGS&quot;</span>
+<span class="d">-    done</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# changed &lt;message&gt;: the file was written. bm-commit, where installed, keeps</span>
+<span class="d">-# its history in git. bm-sync, where installed, tells your other machines; it</span>
+<span class="d">-# runs in the background, so that a slow network never holds the menu up.</span>
+<span class="d">-changed () {</span>
+<span class="d">-    if have bm-commit; then bm-commit &quot;$1&quot;; fi</span>
+<span class="d">-    if have bm-sync; then bm-sync -q &gt;/dev/null 2&gt;&amp;1 &amp; fi</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# ---- actions ----</span>
+<span class="d">-</span>
+<span class="d">-copy_url () {</span>
+<span class="d">-    bump &quot;$1&quot;</span>
+<span class="d">-    printf &#x27;%s&#x27; &quot;$1&quot; | clip_out</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-open_url () {</span>
+<span class="d">-    bump &quot;$1&quot;</span>
+<span class="d">-    opener &quot;$1&quot;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# The editor reads the file when it starts and writes it when you save, so</span>
+<span class="d">-# bm holds the lock until the editor ends. bm-sync then waits, and syncs the</span>
+<span class="d">-# edit after it.</span>
+<span class="d">-edit_url () {</span>
+<span class="d">-    lock</span>
+<span class="d">-    line=$(SBM_URL=$1 awk -F&#x27;\t&#x27; \</span>
+<span class="d">-           &#x27;$1 == ENVIRON[&quot;SBM_URL&quot;] { print NR; exit }&#x27; &quot;$BOOKMARKS&quot;)</span>
+<span class="d">-    ${VISUAL:-${EDITOR:-vi}} &quot;+${line:-1}&quot; &quot;$BOOKMARKS&quot;</span>
+<span class="d">-    unlock</span>
+<span class="d">-    changed &#x27;bm: edit&#x27;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-delete_url () {</span>
+<span class="d">-    lock</span>
+<span class="d">-    for file in &quot;$BOOKMARKS&quot; &quot;$USAGE&quot;; do</span>
+<span class="d">-        [ -e &quot;$file&quot; ] || continue</span>
+<span class="d">-        SBM_URL=$1 awk -F&#x27;\t&#x27; &#x27;$1 != ENVIRON[&quot;SBM_URL&quot;]&#x27; &quot;$file&quot; \</span>
+<span class="d">-            | rewrite &quot;$file&quot; || die &quot;could not rewrite $file&quot;</span>
+<span class="d">-    done</span>
+<span class="d">-    unlock</span>
+<span class="d">-    changed &quot;bm: delete $1&quot;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# urlencode &lt;text&gt;: for a query string. awk has no ord(), hence the table.</span>
+<span class="d">-urlencode () {</span>
+<span class="d">-    printf &#x27;%s&#x27; &quot;$1&quot; | LC_ALL=C awk &#x27;</span>
+<span class="d">-        BEGIN { for (i = 1; i &lt; 256; i++) ord[sprintf(&quot;%c&quot;, i)] = i }</span>
+<span class="d">-        {</span>
+<span class="d">-            for (i = 1; i &lt;= length($0); i++) {</span>
+<span class="d">-                c = substr($0, i, 1)</span>
+<span class="d">-                if (c ~ /[A-Za-z0-9._~-]/) out = out c</span>
+<span class="d">-                else if (c == &quot; &quot;)         out = out &quot;+&quot;</span>
+<span class="d">-                else                       out = out sprintf(&quot;%%%02X&quot;, ord[c])</span>
+<span class="d">-            }</span>
+<span class="d">-        }</span>
+<span class="d">-        END { printf &quot;%s&quot;, out }&#x27;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# go &lt;text&gt;: open text that is not a bookmark. One word with &quot;://&quot; or a dot</span>
+<span class="d">-# in it is an address. Anything else is a web search: by the engine whose</span>
+<span class="d">-# keyword is the first word ($ENGINES holds &quot;keyword | url with %s&quot; lines),</span>
+<span class="d">-# or else by $SEARCH.</span>
+<span class="d">-go () {</span>
+<span class="d">-    case $1 in</span>
+<span class="d">-        *&#x27; &#x27;*) ;;</span>
+<span class="d">-        *://*) opener &quot;$1&quot;; return ;;</span>
+<span class="d">-        *.*)   opener &quot;https://$1&quot;; return ;;</span>
+<span class="d">-    esac</span>
+<span class="d">-    engine=$SEARCH</span>
+<span class="d">-    query=$1</span>
+<span class="d">-    if [ &quot;${1%% *}&quot; != &quot;$1&quot; ] &amp;&amp; [ -r &quot;$ENGINES&quot; ]; then</span>
+<span class="d">-        found=$(SBM_KEY=${1%% *} awk -F&#x27; *[|] *&#x27; \</span>
+<span class="d">-                &#x27;$1 == ENVIRON[&quot;SBM_KEY&quot;] { print $2; exit }&#x27; &quot;$ENGINES&quot;)</span>
+<span class="d">-        if [ -n &quot;$found&quot; ]; then</span>
+<span class="d">-            engine=$found</span>
+<span class="d">-            query=${1#* }</span>
+<span class="d">-        fi</span>
+<span class="d">-    fi</span>
+<span class="d">-    opener &quot;$(printf &#x27;%s\n&#x27; &quot;$engine&quot; | sed &quot;s|%s|$(urlencode &quot;$query&quot;)|&quot;)&quot;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-plumb () {</span>
+<span class="d">-    url=$(choose &#x27;open:&#x27;)</span>
+<span class="d">-    [ -n &quot;$url&quot; ] || exit 1</span>
+<span class="d">-    if is_bookmark &quot;$url&quot;; then open_url &quot;$url&quot;; else go &quot;$url&quot;; fi</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# Exit status 3 means &quot;already bookmarked&quot;.</span>
+<span class="d">-add () {</span>
+<span class="d">-    url=$1</span>
+<span class="d">-    if [ -z &quot;$url&quot; ]; then</span>
+<span class="d">-        url=$(ask &#x27;url&#x27; &quot;$(clip_in | grep &#x27;://&#x27; | head -n 1)&quot;)</span>
+<span class="d">-    fi</span>
+<span class="d">-    url=$(printf &#x27;%s&#x27; &quot;$url&quot; | tr -d &#x27;[:space:]&#x27;)</span>
+<span class="d">-    [ -n &quot;$url&quot; ] || exit 1</span>
+<span class="d">-</span>
+<span class="d">-    dup=$(find_dup &quot;$url&quot;)</span>
+<span class="d">-    if [ -n &quot;$dup&quot; ]; then</span>
+<span class="d">-        printf &#x27;bm: already bookmarked:\n%s\n&#x27; &quot;$dup&quot; &gt;&amp;2</span>
+<span class="d">-        exit 3</span>
+<span class="d">-    fi</span>
+<span class="d">-</span>
+<span class="d">-    # bm-title, where installed, fetches the page&#x27;s title.</span>
+<span class="d">-    title=</span>
+<span class="d">-    if have bm-title; then title=$(bm-title &quot;$url&quot;); fi</span>
+<span class="d">-    desc=$(ask &#x27;description&#x27; &quot;$title&quot; | tr &#x27;\t&#x27; &#x27; &#x27;)</span>
+<span class="d">-</span>
+<span class="d">-    [ -e &quot;$USERTAGS&quot; ] || : &gt; &quot;$USERTAGS&quot;</span>
+<span class="d">-    names=$(pickmulti &#x27;tags:&#x27; &lt; &quot;$USERTAGS&quot; | sed &#x27;s/[ |].*//&#x27; | sed &#x27;/^$/d&#x27; \</span>
+<span class="d">-            | sort -u)</span>
+<span class="d">-    printf &#x27;%s\n&#x27; &quot;$names&quot; | learn_tags</span>
+<span class="d">-    tags=$(printf &#x27;%s\n&#x27; &quot;$names&quot; | paste -sd &#x27; &#x27; -)</span>
+<span class="d">-</span>
+<span class="d">-    lock</span>
+<span class="d">-    printf &#x27;%s\t%s\t%s\n&#x27; &quot;$url&quot; &quot;$desc&quot; &quot;$tags&quot; &gt;&gt; &quot;$BOOKMARKS&quot;</span>
+<span class="d">-    unlock</span>
+<span class="d">-    changed &quot;bm: add $url&quot;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# merge: bookmark lines on stdin (a bare URL per line will do); the ones not</span>
+<span class="d">-# known yet are appended. This is how the output of bm-import gets in.</span>
+<span class="d">-merge () {</span>
+<span class="d">-    new=$(tmpfile &quot;$BOOKMARKS&quot;) || die &#x27;cannot create a temporary file&#x27;</span>
+<span class="d">-    lock</span>
+<span class="d">-    SBM_FILE=$BOOKMARKS SBM_NEW=$new awk -F&#x27;\t&#x27; &quot;$AWK_NORM&quot;&#x27;</span>
+<span class="d">-        BEGIN {</span>
+<span class="d">-            while ((getline line &lt; ENVIRON[&quot;SBM_FILE&quot;]) &gt; 0) {</span>
+<span class="d">-                split(line, f, &quot;\t&quot;)</span>
+<span class="d">-                seen[norm(f[1])] = 1</span>
+<span class="d">-            }</span>
+<span class="d">-        }</span>
+<span class="d">-        NF == 0 || /^#/ { next }</span>
+<span class="d">-        seen[norm($1)]++ { skipped++; next }</span>
+<span class="d">-        { printf &quot;%s\t%s\t%s\n&quot;, $1, $2, $3 &gt; ENVIRON[&quot;SBM_NEW&quot;]; added++ }</span>
+<span class="d">-        END { printf &quot;added %d, skipped %d duplicates\n&quot;, added, skipped }&#x27;</span>
+<span class="d">-    cut -f3 &quot;$new&quot; | tr &#x27; &#x27; &#x27;\n&#x27; | sort -u | learn_tags</span>
+<span class="d">-    cat &quot;$new&quot; &gt;&gt; &quot;$BOOKMARKS&quot;</span>
+<span class="d">-    unlock</span>
+<span class="d">-    rm -f &quot;$new&quot;</span>
+<span class="d">-    changed &#x27;bm: merge&#x27;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-nextorder () {</span>
+<span class="d">-    case $sortorder in</span>
+<span class="d">-        used)   sortorder=recent ;;</span>
+<span class="d">-        recent) sortorder=url ;;</span>
+<span class="d">-        url)    sortorder=desc ;;</span>
+<span class="d">-        desc)   sortorder=tag ;;</span>
+<span class="d">-        *)      sortorder=used ;;</span>
+<span class="d">-    esac</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# add exits, which must not end a browsing session; and when it refuses a</span>
+<span class="d">-# duplicate the user has to get to see that before the list comes back.</span>
+<span class="d">-add_inside () {</span>
+<span class="d">-    ( add &#x27;&#x27; )</span>
+<span class="d">-    if [ $? -eq 3 ]; then</span>
+<span class="d">-        ask &#x27;already bookmarked; press ENTER&#x27; &gt;/dev/null</span>
+<span class="d">-    fi</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# browse_fzf: fzf can bind keys, so one list does everything. Opening and</span>
+<span class="d">-# copying leave it, the other keys come back to it.</span>
+<span class="d">-browse_fzf () {</span>
+<span class="d">-    while :; do</span>
+<span class="d">-        header=&quot;ENTER open  ^Y copy  ^A add  ^D delete  ^E edit  ^S sort [$sortorder]&quot;</span>
+<span class="d">-        out=$(rows | fzfrows | withpreview fzfcmd --print-query \</span>
+<span class="d">-              --expect=ctrl-y,ctrl-a,ctrl-d,ctrl-e,ctrl-s \</span>
+<span class="d">-              --header=&quot;$header&quot; --prompt=&#x27;bookmark: &#x27;)</span>
+<span class="d">-        [ -n &quot;$out&quot; ] || exit 1</span>
+<span class="d">-        typed=$(printf &#x27;%s\n&#x27; &quot;$out&quot; | sed -n 1p)</span>
+<span class="d">-        key=$(printf &#x27;%s\n&#x27; &quot;$out&quot; | sed -n 2p)</span>
+<span class="d">-        url=$(printf &#x27;%s\n&#x27; &quot;$out&quot; | sed -n 3p | cut -f1)</span>
+<span class="d">-</span>
+<span class="d">-        case $key in</span>
+<span class="d">-            ctrl-a) add_inside; continue ;;</span>
+<span class="d">-            ctrl-s) nextorder; continue ;;</span>
+<span class="d">-        esac</span>
+<span class="d">-        if [ -z &quot;$url&quot; ]; then</span>
+<span class="d">-            [ -n &quot;$key&quot; ] &amp;&amp; continue</span>
+<span class="d">-            [ -n &quot;$typed&quot; ] || exit 1</span>
+<span class="d">-            go &quot;$typed&quot;</span>
+<span class="d">-            exit 0</span>
+<span class="d">-        fi</span>
+<span class="d">-        case $key in</span>
+<span class="d">-            ctrl-y) copy_url &quot;$url&quot;; exit 0 ;;</span>
+<span class="d">-            ctrl-e) edit_url &quot;$url&quot; ;;</span>
+<span class="d">-            ctrl-d)</span>
+<span class="d">-                case $(ask &quot;delete $url? (y/N)&quot;) in</span>
+<span class="d">-                    y|Y|yes) delete_url &quot;$url&quot; ;;</span>
+<span class="d">-                esac</span>
+<span class="d">-                ;;</span>
+<span class="d">-            *)      open_url &quot;$url&quot;; exit 0 ;;</span>
+<span class="d">-        esac</span>
+<span class="d">-    done</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# browse_menu: dmenu can neither bind keys nor preview, so a second menu does</span>
+<span class="d">-# both: it shows the bookmark in full and asks what to do with it. Closing it</span>
+<span class="d">-# goes back to the list; closing the list leaves.</span>
+<span class="d">-browse_menu () {</span>
+<span class="d">-    while :; do</span>
+<span class="d">-        chosen=$( { printf &#x27;[add]\n&#x27;; rows | cut -f1-3; } | pick &#x27;bookmark:&#x27;)</span>
+<span class="d">-        [ -n &quot;$chosen&quot; ] || exit 1</span>
+<span class="d">-        if [ &quot;$chosen&quot; = &#x27;[add]&#x27; ]; then</span>
+<span class="d">-            add_inside</span>
+<span class="d">-            continue</span>
+<span class="d">-        fi</span>
+<span class="d">-        url=$(printf &#x27;%s\n&#x27; &quot;$chosen&quot; | cut -f1)</span>
+<span class="d">-        if ! is_bookmark &quot;$url&quot;; then</span>
+<span class="d">-            go &quot;$chosen&quot;</span>
+<span class="d">-            exit 0</span>
+<span class="d">-        fi</span>
+<span class="d">-        action=$( { printf &#x27;open\ncopy\nedit\ndelete\n&#x27;</span>
+<span class="d">-                    details &quot;$url&quot; | sed &#x27;s/^/  /&#x27;; } | pick &#x27;do:&#x27;)</span>
+<span class="d">-        case $action in</span>
+<span class="d">-            open)   open_url &quot;$url&quot;; exit 0 ;;</span>
+<span class="d">-            copy)   copy_url &quot;$url&quot;; exit 0 ;;</span>
+<span class="d">-            edit)   edit_url &quot;$url&quot; ;;</span>
+<span class="d">-            delete) delete_url &quot;$url&quot; ;;</span>
+<span class="d">-        esac</span>
+<span class="d">-    done</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# ---- main ----</span>
+<span class="d">-</span>
+<span class="d">-setaction () {</span>
+<span class="d">-    if [ -n &quot;$action&quot; ]; then</span>
+<span class="d">-        printf &#x27;bm: only one action at a time\n&#x27; &gt;&amp;2</span>
+<span class="d">-        exit 2</span>
+<span class="d">-    fi</span>
+<span class="d">-    action=$1</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# needarg &lt;flag&gt; &lt;count of remaining arguments&gt; &lt;value&gt;</span>
+<span class="d">-needarg () {</span>
+<span class="d">-    if [ &quot;$2&quot; -lt 2 ] || [ -z &quot;$3&quot; ]; then</span>
+<span class="d">-        printf &#x27;bm: %s needs an argument\n&#x27; &quot;$1&quot; &gt;&amp;2</span>
+<span class="d">-        exit 2</span>
+<span class="d">-    fi</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-action=</span>
+<span class="d">-addurl=</span>
+<span class="d">-tagfilter=</span>
+<span class="d">-sortorder=${SBM_SORT:-used}</span>
+<span class="d">-while [ $# -gt 0 ]; do</span>
+<span class="d">-    case $1 in</span>
+<span class="d">-        -h|--help)   setaction help ;;</span>
+<span class="d">-        -c|--copy)   setaction copy ;;</span>
+<span class="d">-        --print)     setaction print ;;</span>
+<span class="d">-        -o|--open|-p|--plumb) setaction open ;;</span>
+<span class="d">-        -e|--edit)   setaction edit ;;</span>
+<span class="d">-        -d|--delete) setaction delete ;;</span>
+<span class="d">-        -l|--list)   setaction list ;;</span>
+<span class="d">-        -m|--merge)  setaction merge ;;</span>
+<span class="d">-        -a|--add)</span>
+<span class="d">-            setaction add</span>
+<span class="d">-            case ${2-} in</span>
+<span class="d">-                &#x27;&#x27;|-*) ;;</span>
+<span class="d">-                *) addurl=$2; shift ;;</span>
+<span class="d">-            esac</span>
+<span class="d">-            ;;</span>
+<span class="d">-        -t|--tag)</span>
+<span class="d">-            needarg &quot;$1&quot; $# &quot;${2-}&quot;</span>
+<span class="d">-            tagfilter=$2</span>
+<span class="d">-            shift</span>
+<span class="d">-            ;;</span>
+<span class="d">-        -S|--sort)</span>
+<span class="d">-            needarg &quot;$1&quot; $# &quot;${2-}&quot;</span>
+<span class="d">-            sortorder=$2</span>
+<span class="d">-            shift</span>
+<span class="d">-            ;;</span>
+<span class="d">-        *)</span>
+<span class="d">-            printf &#x27;bm: unknown argument: %s\n&#x27; &quot;$1&quot; &gt;&amp;2</span>
+<span class="d">-            usage &gt;&amp;2</span>
+<span class="d">-            exit 2</span>
+<span class="d">-            ;;</span>
+<span class="d">-    esac</span>
+<span class="d">-    shift</span>
+<span class="d">-done</span>
+<span class="d">-</span>
+<span class="d">-case $sortorder in</span>
+<span class="d">-    used|recent|url|desc|tag) ;;</span>
+<span class="d">-    *)</span>
+<span class="d">-        printf &#x27;bm: unknown sort order: %s\n&#x27; &quot;$sortorder&quot; &gt;&amp;2</span>
+<span class="d">-        exit 2</span>
+<span class="d">-        ;;</span>
+<span class="d">-esac</span>
+<span class="d">-</span>
+<span class="d">-if [ &quot;$action&quot; = help ]; then</span>
+<span class="d">-    usage</span>
+<span class="d">-    exit 0</span>
+<span class="d">-fi</span>
+<span class="d">-</span>
+<span class="d">-mkdir -p &quot;$(dirname &quot;$BOOKMARKS&quot;)&quot; || die &quot;cannot create directory for $BOOKMARKS&quot;</span>
+<span class="d">-[ -e &quot;$BOOKMARKS&quot; ] || : &gt; &quot;$BOOKMARKS&quot; || die &quot;cannot create $BOOKMARKS&quot;</span>
+<span class="d">-</span>
+<span class="d">-if awk &#x27;!/\t/ &amp;&amp; !/^#/ &amp;&amp; NF { found = 1; exit } END { exit !found }&#x27; &quot;$BOOKMARKS&quot;</span>
+<span class="d">-then</span>
+<span class="d">-    printf &#x27;bm: %s has lines in the old format; run: bm-migrate\n&#x27; &quot;$BOOKMARKS&quot; &gt;&amp;2</span>
+<span class="d">-fi</span>
+<span class="d">-</span>
+<span class="d">-case $action in</span>
+<span class="d">-    list|merge) ;;</span>
+<span class="d">-    *) pickmenu ;;</span>
+<span class="d">-esac</span>
+<span class="d">-</span>
+<span class="d">-case $action in</span>
+<span class="d">-    &#x27;&#x27;)</span>
+<span class="d">-        if [ &quot;$SBM_MENU&quot; = fzf ]; then browse_fzf; else browse_menu; fi</span>
+<span class="d">-        ;;</span>
+<span class="d">-    open)   plumb ;;</span>
+<span class="d">-    copy)   url=$(choose_url &#x27;copy:&#x27;)   &amp;&amp; copy_url &quot;$url&quot; ;;</span>
+<span class="d">-    print)  url=$(choose_url &#x27;print:&#x27;)  &amp;&amp; bump &quot;$url&quot; &amp;&amp; printf &#x27;%s\n&#x27; &quot;$url&quot; ;;</span>
+<span class="d">-    edit)   url=$(choose_url &#x27;edit:&#x27;)   &amp;&amp; edit_url &quot;$url&quot; ;;</span>
+<span class="d">-    delete) url=$(choose_url &#x27;delete:&#x27;) &amp;&amp; delete_url &quot;$url&quot; \</span>
+<span class="d">-                &amp;&amp; printf &#x27;deleted %s\n&#x27; &quot;$url&quot; ;;</span>
+<span class="d">-    add)    add &quot;$addurl&quot; ;;</span>
+<span class="d">-    list)   rows | cut -f1-3 ;;</span>
+<span class="d">-    merge)  merge ;;</span>
+<span class="d">-esac</span>
+diff --git a/git-update-repos b/git-update-repos
+old mode 100644
+new mode 100755
+diff --git a/mpdmenu b/mpdmenu
+deleted file mode 100755
+index d32d957..0000000
+--- a/mpdmenu
++++ /dev/null
+<span class="h">@@ -1,76 +0,0 @@</span>
+<span class="d">-#!/bin/bash</span>
+<span class="d">-</span>
+<span class="d">-all_name=&#x27;[ALL]&#x27;</span>
+<span class="d">-mode=library</span>
+<span class="d">-</span>
+<span class="d">-d_artist() {</span>
+<span class="d">-    mpc list artist | sort -f | dmenu -p artist &quot;${dmenu_args[@]}&quot;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-d_album() {</span>
+<span class="d">-    local artist=&quot;$1&quot;</span>
+<span class="d">-    local albums</span>
+<span class="d">-</span>
+<span class="d">-    mapfile -t albums &lt; &lt;(mpc list album artist &quot;$artist&quot;)</span>
+<span class="d">-    if (( ${#albums[@]} &gt; 1 )) ; then</span>
+<span class="d">-        {</span>
+<span class="d">-            printf &#x27;%s\n&#x27; &quot;$all_name&quot;</span>
+<span class="d">-            printf &#x27;%s\n&#x27; &quot;${albums[@]}&quot; | sort -f</span>
+<span class="d">-        } | dmenu -p album &quot;${dmenu_args[@]}&quot;</span>
+<span class="d">-    else</span>
+<span class="d">-        # We only have one album, so just use that.</span>
+<span class="d">-        printf &#x27;%s\n&#x27; &quot;${albums[0]}&quot;</span>
+<span class="d">-    fi</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-d_playlist() {</span>
+<span class="d">-    local format=&quot;%position% %title%&quot;</span>
+<span class="d">-    local extra_format=&quot;(%artist% - %album%)&quot;</span>
+<span class="d">-    local track</span>
+<span class="d">-    local num_extras</span>
+<span class="d">-</span>
+<span class="d">-    # If all tracks are from the same artist and album, no need to display that</span>
+<span class="d">-    num_extras=$(mpc playlist -f &quot;$extra_format&quot; | sort | uniq | wc -l)</span>
+<span class="d">-    (( num_extras == 1 )) || format+=&quot; $extra_format&quot;</span>
+<span class="d">-</span>
+<span class="d">-    track=$(mpc playlist -f &quot;$format&quot; | dmenu -p track &quot;${dmenu_args[@]}&quot;)</span>
+<span class="d">-    printf &#x27;%s&#x27; &quot;${track%% *}&quot;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-i=2</span>
+<span class="d">-</span>
+<span class="d">-for arg do</span>
+<span class="d">-    if [[ $arg == :: ]]; then</span>
+<span class="d">-        dmenu_args=( &quot;${@:$i}&quot; )</span>
+<span class="d">-        break</span>
+<span class="d">-    fi</span>
+<span class="d">-</span>
+<span class="d">-    case &quot;$arg&quot; in</span>
+<span class="d">-        -l) mode=library ;;</span>
+<span class="d">-        -p) mode=playlist ;;</span>
+<span class="d">-    esac</span>
+<span class="d">-</span>
+<span class="d">-    let i++</span>
+<span class="d">-done</span>
+<span class="d">-</span>
+<span class="d">-case &quot;$mode&quot; in</span>
+<span class="d">-    library)</span>
+<span class="d">-        artist=$(d_artist)</span>
+<span class="d">-        [[ $artist ]] || exit 1</span>
+<span class="d">-</span>
+<span class="d">-        album=$(d_album &quot;$artist&quot;)</span>
+<span class="d">-        [[ $album ]] || exit 2</span>
+<span class="d">-</span>
+<span class="d">-        mpc clear</span>
+<span class="d">-        if [[ $album == &quot;$all_name&quot; ]]; then</span>
+<span class="d">-            mpc find artist &quot;$artist&quot; | sort | mpc add</span>
+<span class="d">-        else</span>
+<span class="d">-            mpc find artist &quot;$artist&quot; album &quot;$album&quot; | sort | mpc add</span>
+<span class="d">-        fi</span>
+<span class="d">-</span>
+<span class="d">-        mpc play &gt;/dev/null</span>
+<span class="d">-    ;;</span>
+<span class="d">-    playlist)</span>
+<span class="d">-        mpc play &quot;$(d_playlist)&quot; &gt;/dev/null</span>
+<span class="d">-    ;;</span>
+<span class="d">-esac</span>
+diff --git a/sbm/bm-commit b/sbm/bm-commit
+deleted file mode 100755
+index fbefcca..0000000
+--- a/sbm/bm-commit
++++ /dev/null
+<span class="h">@@ -1,36 +0,0 @@</span>
+<span class="d">-#!/bin/sh</span>
+<span class="d">-</span>
+<span class="d">-# bm-commit [message]: commit the bookmark file, and the tag file beside it,</span>
+<span class="d">-# when their directory is a git repository of its own. bm runs it after every</span>
+<span class="d">-# change when it is installed. It never pushes or pulls.</span>
+<span class="d">-#</span>
+<span class="d">-#     cd ~/.local/share/sbm &amp;&amp; git init</span>
+<span class="d">-#     printf &#x27;%s\n&#x27; &#x27;*.usage&#x27; &#x27;*.bak*&#x27; &gt; .gitignore</span>
+<span class="d">-#</span>
+<span class="d">-# A file that merely sits somewhere inside a bigger repository (your dotfiles,</span>
+<span class="d">-# say) is left alone, unless SBM_GIT=1. SBM_GIT=0 turns commits off.</span>
+<span class="d">-</span>
+<span class="d">-DATADIR=${XDG_DATA_HOME:-$HOME/.local/share}/sbm</span>
+<span class="d">-BOOKMARKS=${BOOKMARKS:-$DATADIR/bookmarks}</span>
+<span class="d">-USERTAGS=${USERTAGS:-$DATADIR/usertags}</span>
+<span class="d">-</span>
+<span class="d">-[ &quot;${SBM_GIT:-auto}&quot; != 0 ] || exit 0</span>
+<span class="d">-dir=$(cd &quot;$(dirname &quot;$BOOKMARKS&quot;)&quot; 2&gt;/dev/null &amp;&amp; pwd -P) || exit 0</span>
+<span class="d">-top=$(git -C &quot;$dir&quot; rev-parse --show-toplevel 2&gt;/dev/null) || exit 0</span>
+<span class="d">-[ &quot;${SBM_GIT:-auto}&quot; = 1 ] || [ &quot;$top&quot; = &quot;$dir&quot; ] || exit 0</span>
+<span class="d">-</span>
+<span class="d">-set -- &quot;${1:-bm: update}&quot; &quot;$BOOKMARKS&quot;</span>
+<span class="d">-if [ -e &quot;$USERTAGS&quot; ] \</span>
+<span class="d">-    &amp;&amp; [ &quot;$(cd &quot;$(dirname &quot;$USERTAGS&quot;)&quot; 2&gt;/dev/null &amp;&amp; pwd -P)&quot; = &quot;$dir&quot; ]</span>
+<span class="d">-then</span>
+<span class="d">-    set -- &quot;$@&quot; &quot;$USERTAGS&quot;</span>
+<span class="d">-fi</span>
+<span class="d">-message=$1</span>
+<span class="d">-shift</span>
+<span class="d">-</span>
+<span class="d">-git -C &quot;$dir&quot; add -- &quot;$@&quot; 2&gt;/dev/null || exit 0</span>
+<span class="d">-git -C &quot;$dir&quot; diff --cached --quiet -- &quot;$@&quot; &amp;&amp; exit 0</span>
+<span class="d">-if ! git -C &quot;$dir&quot; commit -q -m &quot;$message&quot; -- &quot;$@&quot; &gt;/dev/null 2&gt;&amp;1; then</span>
+<span class="d">-    printf &#x27;bm-commit: git commit failed in %s\n&#x27; &quot;$dir&quot; &gt;&amp;2</span>
+<span class="d">-    exit 1</span>
+<span class="d">-fi</span>
+diff --git a/sbm/bm-page b/sbm/bm-page
+deleted file mode 100755
+index 41d73ce..0000000
+--- a/sbm/bm-page
++++ /dev/null
+<span class="h">@@ -1,212 +0,0 @@</span>
+<span class="d">-#!/bin/sh</span>
+<span class="d">-</span>
+<span class="d">-# bm-page: print a web page as plain text: its title, a blank line, then its</span>
+<span class="d">-# text. When it is installed, the fzf list of bm shows the page of the</span>
+<span class="d">-# bookmark under the cursor in the preview, live. SBM_FETCH=0 keeps it off</span>
+<span class="d">-# the network.</span>
+<span class="d">-#</span>
+<span class="d">-#     bm-page https://suckless.org/ | less</span>
+<span class="d">-#</span>
+<span class="d">-# Only http and https pages are fetched. curl stops after 5 seconds, and dd</span>
+<span class="d">-# passes at most 512k of the page. Scripts, styles and comments are left</span>
+<span class="d">-# out. Control characters are removed, so that a page cannot send escape</span>
+<span class="d">-# sequences to the terminal.</span>
+<span class="d">-</span>
+<span class="d">-if [ $# -ne 1 ]; then</span>
+<span class="d">-    printf &#x27;usage: bm-page &lt;url&gt;\n&#x27; &gt;&amp;2</span>
+<span class="d">-    exit 2</span>
+<span class="d">-fi</span>
+<span class="d">-[ &quot;${SBM_FETCH:-1}&quot; != 0 ] || exit 0</span>
+<span class="d">-case $(printf &#x27;%s&#x27; &quot;$1&quot; | tr &#x27;[:upper:]&#x27; &#x27;[:lower:]&#x27;) in</span>
+<span class="d">-    http://*|https://*) ;;</span>
+<span class="d">-    *) exit 0 ;;</span>
+<span class="d">-esac</span>
+<span class="d">-</span>
+<span class="d">-# The first awk reads the page as one record (RS is a control character),</span>
+<span class="d">-# then splits it at each &quot;&lt;&quot;. Each part starts with a tag and ends with the</span>
+<span class="d">-# text after it, so one pass through the parts reads the page in linear</span>
+<span class="d">-# time. The second awk tidies the white space.</span>
+<span class="d">-curl -sL --max-time 5 -A &#x27;Mozilla/5.0&#x27; -- &quot;$1&quot; 2&gt;/dev/null \</span>
+<span class="d">-    | dd bs=1024 count=512 2&gt;/dev/null | LC_ALL=C awk &#x27;</span>
+<span class="d">-    function utf8(c) {</span>
+<span class="d">-        if (c == 160) return &quot; &quot;</span>
+<span class="d">-        if (c &lt; 128) return sprintf(&quot;%c&quot;, c)</span>
+<span class="d">-        if (c &lt; 2048) return sprintf(&quot;%c%c&quot;, 192 + int(c / 64), 128 + c % 64)</span>
+<span class="d">-        if (c &lt; 65536)</span>
+<span class="d">-            return sprintf(&quot;%c%c%c&quot;, 224 + int(c / 4096), 128 + int(c / 64) % 64, 128 + c % 64)</span>
+<span class="d">-        if (c &lt; 1114112)</span>
+<span class="d">-            return sprintf(&quot;%c%c%c%c&quot;, 240 + int(c / 262144), 128 + int(c / 4096) % 64,</span>
+<span class="d">-                           128 + int(c / 64) % 64, 128 + c % 64)</span>
+<span class="d">-        return &quot;&quot;</span>
+<span class="d">-    }</span>
+<span class="d">-    function entity(name,    n, i) {</span>
+<span class="d">-        if (name ~ /^#[0-9]+$/) return utf8(substr(name, 2) + 0)</span>
+<span class="d">-        if (name ~ /^#[xX][0-9A-Fa-f]+$/) {</span>
+<span class="d">-            n = 0</span>
+<span class="d">-            for (i = 3; i &lt;= length(name); i++)</span>
+<span class="d">-                n = n * 16 + index(&quot;0123456789abcdef&quot;, tolower(substr(name, i, 1))) - 1</span>
+<span class="d">-            return utf8(n)</span>
+<span class="d">-        }</span>
+<span class="d">-        if (name in named) return named[name]</span>
+<span class="d">-        return &quot;&amp;&quot; name &quot;;&quot;</span>
+<span class="d">-    }</span>
+<span class="d">-    function decode(s,    out, i, j, name) {</span>
+<span class="d">-        out = &quot;&quot;</span>
+<span class="d">-        while ((i = index(s, &quot;&amp;&quot;)) &gt; 0) {</span>
+<span class="d">-            out = out substr(s, 1, i - 1)</span>
+<span class="d">-            s = substr(s, i + 1)</span>
+<span class="d">-            j = index(s, &quot;;&quot;)</span>
+<span class="d">-            name = substr(s, 1, j - 1)</span>
+<span class="d">-            if (j &gt; 1 &amp;&amp; j &lt;= 12 &amp;&amp; name ~ /^(#[0-9]+|#[xX][0-9A-Fa-f]+|[A-Za-z][A-Za-z0-9]*)$/) {</span>
+<span class="d">-                out = out entity(name)</span>
+<span class="d">-                s = substr(s, j + 1)</span>
+<span class="d">-            } else {</span>
+<span class="d">-                out = out &quot;&amp;&quot;</span>
+<span class="d">-            }</span>
+<span class="d">-        }</span>
+<span class="d">-        return out s</span>
+<span class="d">-    }</span>
+<span class="d">-    # text: the text of the page, or of its title. As in a browser, a line</span>
+<span class="d">-    # break in the text is a space. The line breaks come from the tags: brk</span>
+<span class="d">-    # tells how many are waiting for the next text.</span>
+<span class="d">-    function text(s) {</span>
+<span class="d">-        if (intitle) {</span>
+<span class="d">-            title = title &quot; &quot; s</span>
+<span class="d">-            return</span>
+<span class="d">-        }</span>
+<span class="d">-        s = decode(s)</span>
+<span class="d">-        gsub(/[ \t\r\n]+/, &quot; &quot;, s)</span>
+<span class="d">-        if (s == &quot;&quot; || s == &quot; &quot;) {</span>
+<span class="d">-            if (s == &quot; &quot; &amp;&amp; !brk) printf &quot; &quot;</span>
+<span class="d">-            return</span>
+<span class="d">-        }</span>
+<span class="d">-        if (brk) {</span>
+<span class="d">-            printf &quot;%s&quot;, brk == 2 ? &quot;\n\n&quot; : &quot;\n&quot;</span>
+<span class="d">-            brk = 0</span>
+<span class="d">-            sub(/^ /, &quot;&quot;, s)</span>
+<span class="d">-        }</span>
+<span class="d">-        printf &quot;%s&quot;, s</span>
+<span class="d">-    }</span>
+<span class="d">-    BEGIN {</span>
+<span class="d">-        RS = &quot;\001&quot;</span>
+<span class="d">-        named[&quot;amp&quot;] = &quot;&amp;&quot;; named[&quot;lt&quot;] = &quot;&lt;&quot;; named[&quot;gt&quot;] = &quot;&gt;&quot;; named[&quot;quot&quot;] = &quot;\&quot;&quot;</span>
+<span class="d">-        named[&quot;apos&quot;] = &quot;\047&quot;; named[&quot;nbsp&quot;] = &quot; &quot;</span>
+<span class="d">-        named[&quot;ndash&quot;] = utf8(8211); named[&quot;mdash&quot;] = utf8(8212); named[&quot;hellip&quot;] = utf8(8230)</span>
+<span class="d">-        named[&quot;lsquo&quot;] = utf8(8216); named[&quot;rsquo&quot;] = utf8(8217)</span>
+<span class="d">-        named[&quot;ldquo&quot;] = utf8(8220); named[&quot;rdquo&quot;] = utf8(8221)</span>
+<span class="d">-        named[&quot;laquo&quot;] = utf8(171); named[&quot;raquo&quot;] = utf8(187); named[&quot;middot&quot;] = utf8(183)</span>
+<span class="d">-        named[&quot;bull&quot;] = utf8(8226); named[&quot;copy&quot;] = utf8(169); named[&quot;reg&quot;] = utf8(174)</span>
+<span class="d">-        named[&quot;trade&quot;] = utf8(8482); named[&quot;deg&quot;] = utf8(176); named[&quot;times&quot;] = utf8(215)</span>
+<span class="d">-        named[&quot;euro&quot;] = utf8(8364)</span>
+<span class="d">-        # Tags that start a line, and tags that start a paragraph.</span>
+<span class="d">-        split(&quot;br div li tr dt dd header footer nav main section article aside &quot; \</span>
+<span class="d">-              &quot;figure figcaption form details summary address hr&quot;, b, &quot; &quot;)</span>
+<span class="d">-        for (i in b) line[b[i]] = 1</span>
+<span class="d">-        split(&quot;p h1 h2 h3 h4 h5 h6 ul ol dl table blockquote pre&quot;, b, &quot; &quot;)</span>
+<span class="d">-        for (i in b) para[b[i]] = 1</span>
+<span class="d">-        # Tags whose content is no text of the page.</span>
+<span class="d">-        split(&quot;script style noscript template svg iframe object&quot;, b, &quot; &quot;)</span>
+<span class="d">-        for (i in b) hidden[b[i]] = 1</span>
+<span class="d">-    }</span>
+<span class="d">-    { page = page $0 }</span>
+<span class="d">-    END {</span>
+<span class="d">-        if (substr(page, 1, 5) == &quot;%PDF-&quot;) {</span>
+<span class="d">-            print &quot;(a PDF document)&quot;</span>
+<span class="d">-            exit</span>
+<span class="d">-        }</span>
+<span class="d">-        n = split(page, part, &quot;&lt;&quot;)</span>
+<span class="d">-        text(part[1])</span>
+<span class="d">-        for (i = 2; i &lt;= n; i++) {</span>
+<span class="d">-            p = part[i]</span>
+<span class="d">-            if (comment || substr(p, 1, 3) == &quot;!--&quot;) {</span>
+<span class="d">-                k = index(p, &quot;--&gt;&quot;)</span>
+<span class="d">-                comment = !k</span>
+<span class="d">-                if (k &amp;&amp; skip == &quot;&quot;) text(substr(p, k + 3))</span>
+<span class="d">-                continue</span>
+<span class="d">-            }</span>
+<span class="d">-            k = index(p, &quot;&gt;&quot;)</span>
+<span class="d">-            if (!k) {</span>
+<span class="d">-                # A &quot;&lt;&quot; in the text, or the end of a page that dd cut off.</span>
+<span class="d">-                if (skip == &quot;&quot; &amp;&amp; i &lt; n) text(&quot;&lt;&quot; p)</span>
+<span class="d">-                continue</span>
+<span class="d">-            }</span>
+<span class="d">-            tag = tolower(substr(p, 1, k - 1))</span>
+<span class="d">-            closing = substr(tag, 1, 1) == &quot;/&quot;</span>
+<span class="d">-            name = tag</span>
+<span class="d">-            sub(/^\//, &quot;&quot;, name)</span>
+<span class="d">-            sub(/[^a-z0-9].*/, &quot;&quot;, name)</span>
+<span class="d">-            rest = substr(p, k + 1)</span>
+<span class="d">-            if (skip != &quot;&quot;) {</span>
+<span class="d">-                if (closing &amp;&amp; name == skip) {</span>
+<span class="d">-                    skip = &quot;&quot;</span>
+<span class="d">-                    text(rest)</span>
+<span class="d">-                }</span>
+<span class="d">-                continue</span>
+<span class="d">-            }</span>
+<span class="d">-            if (name in hidden &amp;&amp; !closing &amp;&amp; substr(tag, length(tag)) != &quot;/&quot;) {</span>
+<span class="d">-                skip = name</span>
+<span class="d">-                continue</span>
+<span class="d">-            }</span>
+<span class="d">-            if (name == &quot;title&quot; &amp;&amp; !done) {</span>
+<span class="d">-                if (closing) {</span>
+<span class="d">-                    intitle = 0</span>
+<span class="d">-                    done = 1</span>
+<span class="d">-                    t = decode(title)</span>
+<span class="d">-                    gsub(/[ \t\r\n]+/, &quot; &quot;, t)</span>
+<span class="d">-                    printf &quot;\n\001T%s\n&quot;, t</span>
+<span class="d">-                    brk = 0</span>
+<span class="d">-                } else {</span>
+<span class="d">-                    intitle = 1</span>
+<span class="d">-                }</span>
+<span class="d">-            } else if (name == &quot;main&quot;) {</span>
+<span class="d">-                # Marks for the second awk: the main part of the page.</span>
+<span class="d">-                printf &quot;\n\001%s\n&quot;, closing ? &quot;&gt;&quot; : &quot;&lt;&quot;</span>
+<span class="d">-                brk = 0</span>
+<span class="d">-            } else if (name in para) {</span>
+<span class="d">-                brk = 2</span>
+<span class="d">-            } else if (name in line) {</span>
+<span class="d">-                if (!brk) brk = 1</span>
+<span class="d">-            } else if (name == &quot;td&quot; || name == &quot;th&quot;) {</span>
+<span class="d">-                text(&quot; &quot;)</span>
+<span class="d">-            }</span>
+<span class="d">-            text(rest)</span>
+<span class="d">-        }</span>
+<span class="d">-        print &quot;&quot;</span>
+<span class="d">-    }&#x27; | LC_ALL=C awk &#x27;</span>
+<span class="d">-    {</span>
+<span class="d">-        gsub(/\302\240/, &quot; &quot;)</span>
+<span class="d">-        gsub(/\302[\200-\237]/, &quot;&quot;)</span>
+<span class="d">-        gsub(/[ \t\r]+/, &quot; &quot;)</span>
+<span class="d">-        sub(/^ /, &quot;&quot;)</span>
+<span class="d">-        sub(/ $/, &quot;&quot;)</span>
+<span class="d">-    }</span>
+<span class="d">-    /^\001T/ {</span>
+<span class="d">-        if (title == &quot;&quot;) {</span>
+<span class="d">-            title = substr($0, 3)</span>
+<span class="d">-            sub(/^ /, &quot;&quot;, title)</span>
+<span class="d">-        }</span>
+<span class="d">-        next</span>
+<span class="d">-    }</span>
+<span class="d">-    /^\001&lt;/ { if (!opened) { opened = 1; from = n + 1 }; next }</span>
+<span class="d">-    /^\001&gt;/ { if (opened &amp;&amp; !closed) { closed = 1; to = n }; next }</span>
+<span class="d">-    { line[++n] = $0 }</span>
+<span class="d">-    # The title, then the main part of the page when the page marks one, so</span>
+<span class="d">-    # that menus do not fill the preview. At most one blank line in a row.</span>
+<span class="d">-    END {</span>
+<span class="d">-        if (!opened) from = 1</span>
+<span class="d">-        if (!closed) to = n</span>
+<span class="d">-        if (title != &quot;&quot;) {</span>
+<span class="d">-            print title</span>
+<span class="d">-            seen = blank = 1</span>
+<span class="d">-        }</span>
+<span class="d">-        for (i = from; i &lt;= to; i++) {</span>
+<span class="d">-            if (line[i] == &quot;&quot;) {</span>
+<span class="d">-                blank = seen</span>
+<span class="d">-                continue</span>
+<span class="d">-            }</span>
+<span class="d">-            if (blank) print &quot;&quot;</span>
+<span class="d">-            print line[i]</span>
+<span class="d">-            seen = 1</span>
+<span class="d">-            blank = 0</span>
+<span class="d">-        }</span>
+<span class="d">-    }&#x27; | tr -d &#x27;\000-\010\013-\037\177&#x27;</span>
+diff --git a/sbm/bm-sync b/sbm/bm-sync
+deleted file mode 100755
+index b4e233d..0000000
+--- a/sbm/bm-sync
++++ /dev/null
+<span class="h">@@ -1,248 +0,0 @@</span>
+<span class="d">-#!/bin/sh</span>
+<span class="d">-</span>
+<span class="d">-# bm-sync: keep the bookmark file the same on all your devices, through an</span>
+<span class="d">-# sbm-sync server (https://github.com/equwal/sbm-sync). bm runs it after each</span>
+<span class="d">-# change when it is installed; you can also run it yourself, or from cron.</span>
+<span class="d">-#</span>
+<span class="d">-# The server merges: bookmarks that you add or remove here are added or</span>
+<span class="d">-# removed there, and the other way round. The file stays a plain file.</span>
+<span class="d">-</span>
+<span class="d">-DATADIR=${XDG_DATA_HOME:-$HOME/.local/share}/sbm</span>
+<span class="d">-BOOKMARKS=${BOOKMARKS:-$DATADIR/bookmarks}</span>
+<span class="d">-CONFIG=${SBM_SYNC_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/sbm/sync}</span>
+<span class="d">-SERVER=https://sbmsync.com</span>
+<span class="d">-STATE=$BOOKMARKS.sync   # name of the version of the last sync</span>
+<span class="d">-LOCK=$BOOKMARKS.sync.lock</span>
+<span class="d">-WLOCK=$BOOKMARKS.lock   # the write lock of bm</span>
+<span class="d">-quiet=</span>
+<span class="d">-</span>
+<span class="d">-usage () {</span>
+<span class="d">-    cat &lt;&lt;&#x27;EOF&#x27;</span>
+<span class="d">-usage: bm-sync [-q] [login [server] | logout | status]</span>
+<span class="d">-Without a command: sync the bookmark file now.</span>
+<span class="d">-  login     sign in to the server (default https://sbmsync.com)</span>
+<span class="d">-            and sync; asks for your email and password</span>
+<span class="d">-  logout    sign out on this computer</span>
+<span class="d">-  status    show the server and the time of the last sync</span>
+<span class="d">-  -q        print nothing but errors</span>
+<span class="d">-EOF</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-die () {</span>
+<span class="d">-    printf &#x27;bm-sync: %s\n&#x27; &quot;$*&quot; &gt;&amp;2</span>
+<span class="d">-    exit 1</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-say () {</span>
+<span class="d">-    [ -n &quot;$quiet&quot; ] || printf &#x27;bm-sync: %s\n&#x27; &quot;$*&quot;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# setting &lt;name&gt;: a value from the config file.</span>
+<span class="d">-setting () {</span>
+<span class="d">-    sed -n &quot;s/^$1=//p&quot; &quot;$CONFIG&quot; 2&gt;/dev/null | head -n 1</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# private &lt;file&gt;: make &lt;file&gt; an empty file that only you can read. umask</span>
+<span class="d">-# sets the mode of a new file. chmod sets the mode of a file that exists</span>
+<span class="d">-# already, for example the config file of an earlier login.</span>
+<span class="d">-private () {</span>
+<span class="d">-    (umask 077; : &gt; &quot;$1&quot;) &amp;&amp; chmod 600 &quot;$1&quot;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-cleanup () {</span>
+<span class="d">-    rm -rf &quot;$work&quot;</span>
+<span class="d">-    [ -z &quot;$locked&quot; ] || rm -rf &quot;$LOCK&quot;</span>
+<span class="d">-    wunlock</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# wlock: take the write lock of bm, as bm does. bm holds it while it changes</span>
+<span class="d">-# the file: while bm -e runs, that is until the editor ends. After</span>
+<span class="d">-# $SBM_LOCK_WAIT seconds (default 10) bm-sync stops, and the next change in</span>
+<span class="d">-# bm syncs again.</span>
+<span class="d">-wlock () {</span>
+<span class="d">-    waited=0</span>
+<span class="d">-    until mkdir &quot;$WLOCK&quot; 2&gt;/dev/null; do</span>
+<span class="d">-        pid=$(cat &quot;$WLOCK/pid&quot; 2&gt;/dev/null)</span>
+<span class="d">-        if [ -n &quot;$pid&quot; ] &amp;&amp; ! kill -0 &quot;$pid&quot; 2&gt;/dev/null; then</span>
+<span class="d">-            [ &quot;$(cat &quot;$WLOCK/pid&quot; 2&gt;/dev/null)&quot; != &quot;$pid&quot; ] || rm -rf &quot;$WLOCK&quot;</span>
+<span class="d">-            continue</span>
+<span class="d">-        fi</span>
+<span class="d">-        [ &quot;$waited&quot; -lt &quot;${SBM_LOCK_WAIT:-10}&quot; ] \</span>
+<span class="d">-            || die &quot;$BOOKMARKS is in use (bm -e?): sync again later&quot;</span>
+<span class="d">-        sleep 1</span>
+<span class="d">-        waited=$((waited + 1))</span>
+<span class="d">-    done</span>
+<span class="d">-    echo $$ &gt; &quot;$WLOCK/pid&quot;</span>
+<span class="d">-    wlocked=1</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-wunlock () {</span>
+<span class="d">-    [ -z &quot;$wlocked&quot; ] || rm -rf &quot;$WLOCK&quot;</span>
+<span class="d">-    wlocked=</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-login () {</span>
+<span class="d">-    server=${1:-$SERVER}</span>
+<span class="d">-    server=${server%/}</span>
+<span class="d">-    printf &#x27;email: &#x27; &gt;&amp;2</span>
+<span class="d">-    read -r email || exit 1</span>
+<span class="d">-    printf &#x27;password: &#x27; &gt;&amp;2</span>
+<span class="d">-    stty -echo 2&gt;/dev/null</span>
+<span class="d">-    IFS= read -r password</span>
+<span class="d">-    rc=$?</span>
+<span class="d">-    stty echo 2&gt;/dev/null</span>
+<span class="d">-    printf &#x27;\n&#x27; &gt;&amp;2</span>
+<span class="d">-    [ $rc -eq 0 ] || exit 1</span>
+<span class="d">-</span>
+<span class="d">-    # The password goes through stdin: on the command line, other users</span>
+<span class="d">-    # could see it with ps.</span>
+<span class="d">-    code=$(printf &#x27;%s&#x27; &quot;$password&quot; | curl -sS -o &quot;$work/out&quot; -w &#x27;%{http_code}&#x27; \</span>
+<span class="d">-        --data-urlencode &quot;email=$email&quot; --data-urlencode &#x27;password@-&#x27; \</span>
+<span class="d">-        -- &quot;$server/api/login&quot;) || die &quot;cannot reach $server&quot;</span>
+<span class="d">-    [ &quot;$code&quot; = 200 ] || die &quot;$(cat &quot;$work/out&quot;)&quot;</span>
+<span class="d">-    token=$(tr -d &#x27;\r\n&#x27; &lt; &quot;$work/out&quot;)</span>
+<span class="d">-</span>
+<span class="d">-    mkdir -p &quot;$(dirname &quot;$CONFIG&quot;)&quot; || exit 1</span>
+<span class="d">-    private &quot;$CONFIG&quot; || exit 1</span>
+<span class="d">-    printf &#x27;server=%s\ntoken=%s\n&#x27; &quot;$server&quot; &quot;$token&quot; &gt; &quot;$CONFIG&quot;</span>
+<span class="d">-    rm -f &quot;$STATE&quot;</span>
+<span class="d">-    say &quot;signed in to $server&quot;</span>
+<span class="d">-    sync</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-logout () {</span>
+<span class="d">-    if [ -e &quot;$CONFIG&quot; ]; then</span>
+<span class="d">-        private &quot;$work/auth&quot;</span>
+<span class="d">-        printf &#x27;Authorization: Bearer %s\n&#x27; &quot;$(setting token)&quot; &gt; &quot;$work/auth&quot;</span>
+<span class="d">-        curl -sS -o /dev/null -X POST -H &quot;@$work/auth&quot; -- &quot;$(setting server)/api/logout&quot; \</span>
+<span class="d">-            2&gt;/dev/null</span>
+<span class="d">-        rm -f &quot;$CONFIG&quot; &quot;$STATE&quot;</span>
+<span class="d">-    fi</span>
+<span class="d">-    say &#x27;signed out&#x27;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-status () {</span>
+<span class="d">-    if [ ! -e &quot;$CONFIG&quot; ]; then</span>
+<span class="d">-        echo &#x27;not signed in: run bm-sync login&#x27;</span>
+<span class="d">-        return</span>
+<span class="d">-    fi</span>
+<span class="d">-    echo &quot;server: $(setting server)&quot;</span>
+<span class="d">-    if [ -e &quot;$STATE&quot; ]; then</span>
+<span class="d">-        echo &quot;last sync: $(ls -l &quot;$STATE&quot; | awk &#x27;{ print $6, $7, $8 }&#x27;)&quot;</span>
+<span class="d">-    else</span>
+<span class="d">-        echo &#x27;last sync: never&#x27;</span>
+<span class="d">-    fi</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-# sync: send the file with the name of its last version; the server gives</span>
+<span class="d">-# back the merged file and the name of the new version.</span>
+<span class="d">-sync () {</span>
+<span class="d">-    [ -e &quot;$CONFIG&quot; ] || die &#x27;not signed in: run bm-sync login&#x27;</span>
+<span class="d">-    server=$(setting server)</span>
+<span class="d">-    token=$(setting token)</span>
+<span class="d">-    [ -n &quot;$server&quot; ] &amp;&amp; [ -n &quot;$token&quot; ] || die &quot;$CONFIG is damaged: run bm-sync login&quot;</span>
+<span class="d">-</span>
+<span class="d">-    # One sync at a time. A second bm-sync leaves a note and exits: the</span>
+<span class="d">-    # first one then syncs again, so that no change waits for the next one.</span>
+<span class="d">-    if ! mkdir &quot;$LOCK&quot; 2&gt;/dev/null; then</span>
+<span class="d">-        pid=$(cat &quot;$LOCK/pid&quot; 2&gt;/dev/null)</span>
+<span class="d">-        if [ -n &quot;$pid&quot; ] &amp;&amp; kill -0 &quot;$pid&quot; 2&gt;/dev/null; then</span>
+<span class="d">-            : &gt; &quot;$LOCK/again&quot;</span>
+<span class="d">-            exit 0</span>
+<span class="d">-        fi</span>
+<span class="d">-        rm -rf &quot;$LOCK&quot;   # left by a bm-sync that was killed</span>
+<span class="d">-        mkdir &quot;$LOCK&quot; 2&gt;/dev/null || die &quot;cannot lock $LOCK&quot;</span>
+<span class="d">-    fi</span>
+<span class="d">-    locked=1</span>
+<span class="d">-    echo $$ &gt; &quot;$LOCK/pid&quot;</span>
+<span class="d">-</span>
+<span class="d">-    private &quot;$work/auth&quot;</span>
+<span class="d">-    printf &#x27;Authorization: Bearer %s\n&#x27; &quot;$token&quot; &gt; &quot;$work/auth&quot;</span>
+<span class="d">-    if [ ! -e &quot;$BOOKMARKS&quot; ]; then</span>
+<span class="d">-        mkdir -p &quot;$(dirname &quot;$BOOKMARKS&quot;)&quot; &amp;&amp; : &gt; &quot;$BOOKMARKS&quot; || exit 1</span>
+<span class="d">-    fi</span>
+<span class="d">-</span>
+<span class="d">-    tries=0</span>
+<span class="d">-    while :; do</span>
+<span class="d">-        rm -f &quot;$LOCK/again&quot;</span>
+<span class="d">-        # bm writes a delete as an empty file and then the new text. Without</span>
+<span class="d">-        # the lock, bm-sync could send the half-written file, and the server</span>
+<span class="d">-        # would delete the lines that it lacks.</span>
+<span class="d">-        wlock</span>
+<span class="d">-        cat &quot;$BOOKMARKS&quot; &gt; &quot;$work/sent&quot;</span>
+<span class="d">-        wunlock</span>
+<span class="d">-        # An empty file with a version would delete all bookmarks on the</span>
+<span class="d">-        # server, and then on all devices. Without a version, the server</span>
+<span class="d">-        # deletes nothing, and sends all bookmarks back.</span>
+<span class="d">-        [ -s &quot;$work/sent&quot; ] || rm -f &quot;$STATE&quot;</span>
+<span class="d">-        base=$(cat &quot;$STATE&quot; 2&gt;/dev/null)</span>
+<span class="d">-        code=$(curl -sS --max-time 60 -o &quot;$work/out&quot; -D &quot;$work/head&quot; -w &#x27;%{http_code}&#x27; \</span>
+<span class="d">-            -H &quot;@$work/auth&quot; -H &#x27;Content-Type: text/plain; charset=utf-8&#x27; \</span>
+<span class="d">-            --data-binary &quot;@$work/sent&quot; -- &quot;$server/api/sync?base=$base&quot;) \</span>
+<span class="d">-            || die &quot;cannot reach $server&quot;</span>
+<span class="d">-        case $code in</span>
+<span class="d">-            200) ;;</span>
+<span class="d">-            401) die &#x27;not signed in: run bm-sync login&#x27; ;;</span>
+<span class="d">-            *)   die &quot;$(cat &quot;$work/out&quot;)&quot; ;;</span>
+<span class="d">-        esac</span>
+<span class="d">-        version=$(tr -d &#x27;\r&#x27; &lt; &quot;$work/head&quot; | sed -n &#x27;s/^[Ss][Bb][Mm]-[Vv][Ee][Rr][Ss][Ii][Oo][Nn]: *//p&#x27;)</span>
+<span class="d">-        [ -n &quot;$version&quot; ] || die &#x27;the server gave no version&#x27;</span>
+<span class="d">-</span>
+<span class="d">-        # Hold the lock from the check to the write, so that no change of bm</span>
+<span class="d">-        # comes between them.</span>
+<span class="d">-        wlock</span>
+<span class="d">-        # bm changed the file while it was away: send the new file. The</span>
+<span class="d">-        # answer to the old one is of no use.</span>
+<span class="d">-        if ! cmp -s &quot;$work/sent&quot; &quot;$BOOKMARKS&quot;; then</span>
+<span class="d">-            wunlock</span>
+<span class="d">-            tries=$((tries + 1))</span>
+<span class="d">-            [ $tries -lt 5 ] || die &#x27;the file changes all the time: try again later&#x27;</span>
+<span class="d">-            continue</span>
+<span class="d">-        fi</span>
+<span class="d">-        wrote=</span>
+<span class="d">-        if ! cmp -s &quot;$work/out&quot; &quot;$BOOKMARKS&quot;; then</span>
+<span class="d">-            # Forget the version first, and keep the new one only after the</span>
+<span class="d">-            # write. If the write stops part way (a full disk), the next sync</span>
+<span class="d">-            # sends no version, so the server deletes none of the lost lines.</span>
+<span class="d">-            rm -f &quot;$STATE&quot;</span>
+<span class="d">-            # Through cat, not mv, as in bm: a symlinked file stays a symlink.</span>
+<span class="d">-            cat &quot;$work/out&quot; &gt; &quot;$BOOKMARKS&quot; || die &quot;cannot write $BOOKMARKS&quot;</span>
+<span class="d">-            wrote=1</span>
+<span class="d">-        fi</span>
+<span class="d">-        printf &#x27;%s\n&#x27; &quot;$version&quot; &gt; &quot;$STATE&quot;</span>
+<span class="d">-        wunlock</span>
+<span class="d">-        if [ -n &quot;$wrote&quot; ] &amp;&amp; command -v bm-commit &gt;/dev/null 2&gt;&amp;1; then</span>
+<span class="d">-            bm-commit &#x27;bm-sync: merge&#x27;</span>
+<span class="d">-        fi</span>
+<span class="d">-        [ -e &quot;$LOCK/again&quot; ] || break</span>
+<span class="d">-    done</span>
+<span class="d">-    say &quot;in sync: $(grep -c -v -e &#x27;^#&#x27; -e &#x27;^[[:space:]]*$&#x27; &quot;$BOOKMARKS&quot;) bookmarks&quot;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-while [ $# -gt 0 ]; do</span>
+<span class="d">-    case $1 in</span>
+<span class="d">-        -q) quiet=1 ;;</span>
+<span class="d">-        -h|--help) usage; exit 0 ;;</span>
+<span class="d">-        -*) usage &gt;&amp;2; exit 2 ;;</span>
+<span class="d">-        *)  break ;;</span>
+<span class="d">-    esac</span>
+<span class="d">-    shift</span>
+<span class="d">-done</span>
+<span class="d">-</span>
+<span class="d">-command -v curl &gt;/dev/null 2&gt;&amp;1 || die &#x27;curl is not installed&#x27;</span>
+<span class="d">-# mkdir fails when the name exists: a directory planted in a shared /tmp is</span>
+<span class="d">-# refused, not used.</span>
+<span class="d">-work=${TMPDIR:-/tmp}/bm-sync.$$</span>
+<span class="d">-mkdir -m 700 &quot;$work&quot; || die &quot;cannot create $work&quot;</span>
+<span class="d">-locked=</span>
+<span class="d">-wlocked=</span>
+<span class="d">-trap cleanup EXIT</span>
+<span class="d">-trap &#x27;exit 1&#x27; INT TERM</span>
+<span class="d">-</span>
+<span class="d">-case ${1:-sync} in</span>
+<span class="d">-    login)  shift; login &quot;$@&quot; ;;</span>
+<span class="d">-    logout) logout ;;</span>
+<span class="d">-    status) status ;;</span>
+<span class="d">-    sync)   sync ;;</span>
+<span class="d">-    *)      usage &gt;&amp;2; exit 2 ;;</span>
+<span class="d">-esac</span>
+diff --git a/sbm/bm-title b/sbm/bm-title
+deleted file mode 100755
+index 84a17ba..0000000
+--- a/sbm/bm-title
++++ /dev/null
+<span class="h">@@ -1,33 +0,0 @@</span>
+<span class="d">-#!/bin/sh</span>
+<span class="d">-</span>
+<span class="d">-# bm-title: print the &lt;title&gt; of a web page, or nothing.</span>
+<span class="d">-# When it is installed, &quot;bm --add&quot; offers the title as the description.</span>
+<span class="d">-# SBM_FETCH=0 keeps it off the network.</span>
+<span class="d">-</span>
+<span class="d">-if [ $# -ne 1 ]; then</span>
+<span class="d">-    printf &#x27;usage: bm-title &lt;url&gt;\n&#x27; &gt;&amp;2</span>
+<span class="d">-    exit 2</span>
+<span class="d">-fi</span>
+<span class="d">-[ &quot;${SBM_FETCH:-1}&quot; != 0 ] || exit 0</span>
+<span class="d">-</span>
+<span class="d">-# Lines are gathered until the title has been closed; awk then leaves, and</span>
+<span class="d">-# curl stops downloading. dd caps it at 64k for pages that are one long line</span>
+<span class="d">-# (it may pass a little less when its reads come up short; never more).</span>
+<span class="d">-curl -sL --max-time 3 -A &#x27;Mozilla/5.0&#x27; -- &quot;$1&quot; 2&gt;/dev/null \</span>
+<span class="d">-    | dd bs=1024 count=64 2&gt;/dev/null | LC_ALL=C awk &#x27;</span>
+<span class="d">-    function title(s,    start, end) {</span>
+<span class="d">-        start = index(tolower(s), &quot;&lt;title&quot;)</span>
+<span class="d">-        if (!start) return</span>
+<span class="d">-        s = substr(s, start)</span>
+<span class="d">-        s = substr(s, index(s, &quot;&gt;&quot;) + 1)</span>
+<span class="d">-        end = index(tolower(s), &quot;&lt;/title&quot;)</span>
+<span class="d">-        if (end) s = substr(s, 1, end - 1)</span>
+<span class="d">-        gsub(/&amp;lt;/, &quot;&lt;&quot;, s); gsub(/&amp;gt;/, &quot;&gt;&quot;, s); gsub(/&amp;quot;/, &quot;\&quot;&quot;, s)</span>
+<span class="d">-        gsub(/&amp;#39;|&amp;#x27;|&amp;apos;/, &quot;\047&quot;, s); gsub(/&amp;nbsp;/, &quot; &quot;, s)</span>
+<span class="d">-        gsub(/&amp;amp;/, &quot;\\&amp;&quot;, s)</span>
+<span class="d">-        gsub(/[ \t\r]+/, &quot; &quot;, s); sub(/^ /, &quot;&quot;, s); sub(/ $/, &quot;&quot;, s)</span>
+<span class="d">-        print s</span>
+<span class="d">-    }</span>
+<span class="d">-    { page = page &quot; &quot; $0 }</span>
+<span class="d">-    index(tolower($0), &quot;&lt;/title&quot;) || NR &gt;= 500 { exit }</span>
+<span class="d">-    END { title(page) }&#x27;</span>
+diff --git a/windervolage b/windervolage
+deleted file mode 100755
+index faac9fd..0000000
+--- a/windervolage
++++ /dev/null
+<span class="h">@@ -1,57 +0,0 @@</span>
+<span class="d">-#!/bin/sh</span>
+<span class="d">-#</span>
+<span class="d">-# wv - Wiedervorlage (bring forward file)</span>
+<span class="d">-# schedule a reminder mail to yourself at a given date using at(1)</span>
+<span class="d">-#</span>
+<span class="d">-# markus schnalke &lt;meillo@marmaro.de&gt;</span>
+<span class="d">-</span>
+<span class="d">-queue=w</span>
+<span class="d">-</span>
+<span class="d">-cd  # move to home directory</span>
+<span class="d">-</span>
+<span class="d">-listone() {</span>
+<span class="d">-	atq -q &quot;$queue&quot; | awk &quot;/^$1\t/ {sub(/ $queue .*/,\&quot;\&quot;);print}&quot; </span>
+<span class="d">-	at -c &quot;$1&quot; | sed &#x27;</span>
+<span class="d">-		1,/^# wv/d;</span>
+<span class="d">-		/^mail /{s,[^&quot;]*&quot;,,; s,&quot;.*,,};</span>
+<span class="d">-		s,^,\t,</span>
+<span class="d">-		&#x27;</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-list() {</span>
+<span class="d">-	if [ $# -eq 0 ] ; then</span>
+<span class="d">-		atq -q &quot;$queue&quot; | while read id rest ; do</span>
+<span class="d">-			listone &quot;$id&quot;</span>
+<span class="d">-		done</span>
+<span class="d">-	else</span>
+<span class="d">-		for i in &quot;$@&quot; ; do</span>
+<span class="d">-			listone &quot;$i&quot;</span>
+<span class="d">-		done</span>
+<span class="d">-	fi</span>
+<span class="d">-}</span>
+<span class="d">-</span>
+<span class="d">-</span>
+<span class="d">-if [ $# -eq 0 ] ; then</span>
+<span class="d">-	cat &gt;&amp;2 &lt;&lt;!</span>
+<span class="d">-Usage: ${0##*/} timespec...</span>
+<span class="d">-       ${0##*/} -l [jobid...]</span>
+<span class="d">-!</span>
+<span class="d">-	exit 1</span>
+<span class="d">-fi</span>
+<span class="d">-</span>
+<span class="d">-if [ &quot;$1&quot; = &quot;-l&quot; ] ; then</span>
+<span class="d">-	shift</span>
+<span class="d">-	list &quot;$@&quot;</span>
+<span class="d">-	exit</span>
+<span class="d">-fi</span>
+<span class="d">-</span>
+<span class="d">-echo At: `date +&quot;%Y-%m-%d %H:%M&quot; -d &quot;$*&quot;`</span>
+<span class="d">-printf &quot;Subject: &quot;</span>
+<span class="d">-read sub</span>
+<span class="d">-</span>
+<span class="d">-at -q &quot;$queue&quot; &quot;$@&quot; &lt;&lt;!</span>
+<span class="d">-# wv</span>
+<span class="d">-mail -s &quot;Wv: $sub&quot; `id -un` &lt;&lt;EOF</span>
+<span class="d">-`cat`</span>
+<span class="d">-EOF</span>
+<span class="d">-!</span></pre>
+</div></body></html>
diff --git a/git/sh/commit/95c74ac780a3179b0054566d06b31ee82a4cb4eb.html b/git/sh/commit/95c74ac780a3179b0054566d06b31ee82a4cb4eb.html
new file mode 100644
index 0000000..89ec5ba
--- /dev/null
+++ b/git/sh/commit/95c74ac780a3179b0054566d06b31ee82a4cb4eb.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html lang="en"><head><meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1" />
+<title>sh 95c74ac7 - Recently Written</title>
+<link rel="stylesheet" href="../../stagit.css" />
+</head><body>
+<div id="top"><a href="../../../index.html">Recently Written</a> &middot; <a href="../../index.html">git</a></div>
+<h1>sh</h1><span class="desc">Small shell and Python scripts</span><p class="url">git clone https://github.com/equwal/sh</p><p><a href="../../sh/index.html">Log</a> | <a href="../../sh/files.html">Files</a> | <a href="../../sh/refs.html">Refs</a></p><hr/>
+<div id="content">
+<pre>commit 95c74ac780a3179b0054566d06b31ee82a4cb4eb
+equwal &lt;13551856+equwal@users.noreply.github.com&gt;
+2026-10-04 23:02:58 -0700
+
+git-update-repos: run workers with xargs -P 8 instead of tsp, fix quoting and result parsing
+
+</pre><pre> git-update-repos     | 39 +++++++++++++++------------------------
+ git-update-worker.sh | 14 +++++++-------
+ 2 files changed, 22 insertions(+), 31 deletions(-)
+</pre><pre>diff --git a/git-update-repos b/git-update-repos
+index 1784322..41f3148 100755
+--- a/git-update-repos
++++ b/git-update-repos
+<span class="h">@@ -2,31 +2,22 @@</span>
+ # Update all git repos in directory tree
+ dir=${1:-&quot;.&quot;}
+ results=$(mktemp)
+<span class="d">-log=$(mktemp)</span>
+<span class="d">-trap &quot;tsp -K; rm -f &quot;$results&quot; &quot;$log&quot;&quot; EXIT</span>
+<span class="d">-echo &quot;Updating repos in: &quot;$dir&quot;&quot;</span>
+<span class="d">-tsp -S 8</span>
+<span class="d">-find &quot;&quot;$dir&quot;&quot; -name &quot;.git&quot; -type d 2&gt;/dev/null | while read git_dir; do</span>
+<span class="d">-	tsp -n git-update-worker.sh &quot;$(dirname &quot;&quot;$git_dir&quot;&quot;)&quot; &quot;&quot;$results&quot;&quot; &quot;&quot;$log&quot;&quot;</span>
+<span class="d">-done</span>
+<span class="d">-tail -f &quot;&quot;$log&quot;&quot; &amp;</span>
+<span class="d">-tail_pid=$!</span>
+<span class="d">-tsp -w</span>
+<span class="d">-kill &quot;$tail_pid&quot; 2&gt;/dev/null</span>
+<span class="d">-conflicts=$(grep -c &quot;^REBASE_CONFLICT:&quot; &quot;&quot;$results&quot;&quot; 2&gt;/dev/null || echo 0)</span>
+<span class="a">+trap &#x27;rm -f &quot;$results&quot;&#x27; EXIT</span>
+<span class="a">+echo &quot;Updating repos in: $dir&quot;</span>
+<span class="a">+find &quot;$dir&quot; -name .git -type d -prune -print0 2&gt;/dev/null |</span>
+<span class="a">+	xargs -0 -P 8 -I{} &quot;$(dirname &quot;$0&quot;)/git-update-worker.sh&quot; {} &quot;$results&quot;</span>
+<span class="a">+conflicts=$(grep -c &quot;^REBASE_CONFLICT:&quot; &quot;$results&quot;)</span>
+ echo &quot;CHANGES:&quot;
+<span class="d">-grep &quot;^UPDATED:&quot; &quot;&quot;$results&quot;&quot; | while IFS=&#x27;|&#x27; read prefix repo changelog; do</span>
+<span class="d">-	repo_name=$(echo &quot;&quot;$repo&quot;&quot; | sed &quot;s|^&quot;$dir&quot;/||&quot; | sed &#x27;s|^./||&#x27;)</span>
+<span class="d">-	echo &quot;\033[32m&quot;$repo_name&quot;:\033[0m&quot;</span>
+<span class="d">-	echo &quot;&quot;$changelog&quot;&quot; | tr &#x27;;&#x27; &#x27;\n&#x27; | sed &#x27;s/^/  /&#x27; | sed &#x27;s/^  $//&#x27;</span>
+<span class="a">+sed -n &#x27;s/^UPDATED://p&#x27; &quot;$results&quot; | while IFS=&#x27;|&#x27; read -r repo changelog; do</span>
+<span class="a">+	repo_name=${repo#&quot;$dir&quot;/}</span>
+<span class="a">+	printf &#x27;\033[32m%s:\033[0m\n&#x27; &quot;$repo_name&quot;</span>
+<span class="a">+	printf &#x27;%s\n&#x27; &quot;$changelog&quot; | tr &#x27;;&#x27; &#x27;\n&#x27; | sed &#x27;s/^/  /&#x27; | sed &#x27;s/^  $//&#x27;</span>
+ done
+<span class="d">-if grep -q &quot;^REBASE_CONFLICT:&quot; &quot;&quot;$results&quot;&quot;; then</span>
+<span class="d">-	echo &quot;\033[31mREBASE CONFLICTS:\033[0m&quot;</span>
+<span class="d">-	grep &quot;^REBASE_CONFLICT:&quot; &quot;&quot;$results&quot;&quot; | while IFS=&#x27;|&#x27; read prefix repo _; do</span>
+<span class="d">-		repo_name=$(echo &quot;&quot;$repo&quot;&quot; | sed &quot;s|^&quot;$dir&quot;/||&quot; | sed &#x27;s|^./||&#x27;)</span>
+<span class="d">-		echo &quot;\033[31m&quot;$repo_name&quot;: requires manual resolution\033[0m&quot;</span>
+<span class="a">+if [ &quot;$conflicts&quot; -gt 0 ]; then</span>
+<span class="a">+	printf &#x27;\033[31mREBASE CONFLICTS:\033[0m\n&#x27;</span>
+<span class="a">+	sed -n &#x27;s/^REBASE_CONFLICT://p&#x27; &quot;$results&quot; | while IFS=&#x27;|&#x27; read -r repo _; do</span>
+<span class="a">+		repo_name=${repo#&quot;$dir&quot;/}</span>
+<span class="a">+		printf &#x27;\033[31m%s: requires manual resolution\033[0m\n&#x27; &quot;$repo_name&quot;</span>
+ 	done
+<span class="d">-fi</span>
+<span class="d">-if [ &quot;&quot;$conflicts&quot;&quot; ] &amp;&amp; [ &quot;&quot;$conflicts&quot;&quot; -gt 0 ] 2&gt;/dev/null; then</span>
+<span class="d">-	notify-send &quot;Git Rebase Conflicts&quot; &quot;&quot;$conflicts&quot; repos need manual resolution&quot; -u critical -t 10000</span>
+<span class="a">+	notify-send &quot;Git Rebase Conflicts&quot; &quot;$conflicts repos need manual resolution&quot; -u critical -t 10000</span>
+ fi
+diff --git a/git-update-worker.sh b/git-update-worker.sh
+index 9926866..8a26320 100755
+--- a/git-update-worker.sh
++++ b/git-update-worker.sh
+<span class="h">@@ -1,12 +1,12 @@</span>
+ #!/bin/sh
+<span class="d">-repo=&quot;$1&quot;</span>
+<span class="a">+# Worker for git-update-repos: git-update-worker.sh REPO/.git RESULTS</span>
+<span class="a">+repo=${1%/.git}</span>
+ results=&quot;$2&quot;
+<span class="d">-log=&quot;$3&quot;</span>
+ name=$(basename &quot;$repo&quot;)
+ old=$(cd &quot;$repo&quot; &amp;&amp; git rev-parse HEAD 2&gt;/dev/null)
+ if [ -f &quot;$repo/.git/FETCH_HEAD&quot; ]; then
+ 	if [ &quot;$(find &quot;$repo/.git/FETCH_HEAD&quot; -mmin -5 2&gt;/dev/null)&quot; ]; then
+<span class="d">-		echo &quot;[$name] skipped&quot; &gt;&gt; &quot;$log&quot;</span>
+<span class="a">+		echo &quot;[$name] skipped&quot;</span>
+ 		exit
+ 	fi
+ fi
+<span class="h">@@ -15,14 +15,14 @@ if (cd &quot;$repo&quot; &amp;&amp; git pull --rebase &gt;/dev/null 2&gt;&amp;1); then</span>
+ 	:
+ elif (cd &quot;$repo&quot; &amp;&amp; git rebase --abort &gt;/dev/null 2&gt;&amp;1); then
+ 	echo &quot;REBASE_CONFLICT:$repo|&quot; &gt;&gt; &quot;$results&quot;
+<span class="d">-	echo &quot;[$name] REBASE CONFLICT&quot; &gt;&gt; &quot;$log&quot;</span>
+<span class="a">+	echo &quot;[$name] REBASE CONFLICT&quot;</span>
+ 	exit
+ else
+<span class="d">-	echo &quot;[$name] FAILED&quot; &gt;&gt; &quot;$log&quot;</span>
+<span class="a">+	echo &quot;[$name] FAILED&quot;</span>
+ 	exit
+ fi
+ new=$(cd &quot;$repo&quot; &amp;&amp; git rev-parse HEAD 2&gt;/dev/null)
+<span class="d">-[ &quot;$old&quot; = &quot;$new&quot; ] &amp;&amp; { echo &quot;[$name] up to date&quot; &gt;&gt; &quot;$log&quot;; exit; }</span>
+<span class="d">-echo &quot;[$name] UPDATED&quot; &gt;&gt; &quot;$log&quot;</span>
+<span class="a">+[ &quot;$old&quot; = &quot;$new&quot; ] &amp;&amp; { echo &quot;[$name] up to date&quot;; exit; }</span>
+<span class="a">+echo &quot;[$name] UPDATED&quot;</span>
+ changelog=$(cd &quot;$repo&quot; &amp;&amp; git log --oneline --no-merges -3 &quot;$old..$new&quot; 2&gt;/dev/null | tr &#x27;\n&#x27; &#x27;;&#x27;)
+ echo &quot;UPDATED:$repo|$changelog&quot; &gt;&gt; &quot;$results&quot;
+\ No newline at end of file</pre>
+</div></body></html>
diff --git a/git/sh/commit/a787c8cbc697cf65a27ba4a3001315185a097b7f.html b/git/sh/commit/a787c8cbc697cf65a27ba4a3001315185a097b7f.html
new file mode 100644
index 0000000..85ac8b1
--- /dev/null
+++ b/git/sh/commit/a787c8cbc697cf65a27ba4a3001315185a097b7f.html
@@ -0,0 +1,1989 @@
+<!DOCTYPE html>
+<html lang="en"><head><meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1" />
+<title>sh a787c8cb - Recently Written</title>
+<link rel="stylesheet" href="../../stagit.css" />
+</head><body>
+<div id="top"><a href="../../../index.html">Recently Written</a> &middot; <a href="../../index.html">git</a></div>
+<h1>sh</h1><span class="desc">Small shell and Python scripts</span><p class="url">git clone https://github.com/equwal/sh</p><p><a href="../../sh/index.html">Log</a> | <a href="../../sh/files.html">Files</a> | <a href="../../sh/refs.html">Refs</a></p><hr/>
+<div id="content">
+<pre>commit a787c8cbc697cf65a27ba4a3001315185a097b7f
+equwal &lt;13551856+equwal@users.noreply.github.com&gt;
+2026-10-04 22:51:27 -0700
+
+Import scripts from ~/src/sh unchanged
+
+</pre><pre> bm                        | 686 ++++++++++++++++++++++++++++++++++++++++++++++
+ git-update-repos          |  32 +++
+ git-update-worker.sh      |  28 ++
+ group4ify                 |  15 +
+ group4ify-aux             |   2 +
+ http-collect              | 193 +++++++++++++
+ jpegify                   |  15 +
+ jpegify-aux               |   2 +
+ macho                     |  25 ++
+ mpdmenu                   |  76 +++++
+ pashare                   |  13 +
+ pomodoro                  |  83 ++++++
+ pomodoro-files/config.def |   2 +
+ sbm/bm-commit             |  36 +++
+ sbm/bm-page               | 212 ++++++++++++++
+ sbm/bm-sync               | 248 +++++++++++++++++
+ sbm/bm-title              |  33 +++
+ slock-usb-key             |  24 ++
+ tag-music                 |  49 ++++
+ windervolage              |  57 ++++
+ 20 files changed, 1831 insertions(+)
+</pre><pre>diff --git a/bm b/bm
+new file mode 100755
+index 0000000..5b5f090
+--- /dev/null
++++ b/bm
+<span class="h">@@ -0,0 +1,686 @@</span>
+<span class="a">+#!/bin/sh</span>
+<span class="a">+</span>
+<span class="a">+# bm: bookmarks in a plain file, picked with dmenu (or fzf).</span>
+<span class="a">+#</span>
+<span class="a">+#     URL&lt;tab&gt;description&lt;tab&gt;tag tag tag</span>
+<span class="a">+#</span>
+<span class="a">+# bm owns that file and nothing else. Importing, link checking, the web page</span>
+<span class="a">+# and so on are separate bm-* filters; see README.</span>
+<span class="a">+</span>
+<span class="a">+DMENULINES=${DMENULINES:-15}</span>
+<span class="a">+DATADIR=${XDG_DATA_HOME:-$HOME/.local/share}/sbm</span>
+<span class="a">+BOOKMARKS=${BOOKMARKS:-$DATADIR/bookmarks}</span>
+<span class="a">+USERTAGS=${USERTAGS:-$DATADIR/usertags}</span>
+<span class="a">+ENGINES=${SBM_ENGINES:-$DATADIR/engines}</span>
+<span class="a">+SEARCH=${SBM_SEARCH:-https://duckduckgo.com/?q=%s}</span>
+<span class="a">+USAGE=$BOOKMARKS.usage</span>
+<span class="a">+WLOCK=$BOOKMARKS.lock</span>
+<span class="a">+locked=</span>
+<span class="a">+TAB=$(printf &#x27;\t&#x27;)</span>
+<span class="a">+</span>
+<span class="a">+# Two URLs are the same bookmark when they differ only in scheme, a leading</span>
+<span class="a">+# www., trailing slashes or the case of the host.</span>
+<span class="a">+AWK_NORM=&#x27;</span>
+<span class="a">+function norm(u,    host, rest, i) {</span>
+<span class="a">+    sub(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//, &quot;&quot;, u)</span>
+<span class="a">+    i = index(u, &quot;/&quot;)</span>
+<span class="a">+    if (i) { host = substr(u, 1, i - 1); rest = substr(u, i) }</span>
+<span class="a">+    else   { host = u; rest = &quot;&quot; }</span>
+<span class="a">+    host = tolower(host)</span>
+<span class="a">+    sub(/^www\./, &quot;&quot;, host)</span>
+<span class="a">+    sub(/\/+$/, &quot;&quot;, rest)</span>
+<span class="a">+    return host rest</span>
+<span class="a">+}&#x27;</span>
+<span class="a">+</span>
+<span class="a">+die () {</span>
+<span class="a">+    printf &#x27;bm: %s\n&#x27; &quot;$*&quot; &gt;&amp;2</span>
+<span class="a">+    exit 1</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+have () {</span>
+<span class="a">+    command -v &quot;$1&quot; &gt;/dev/null 2&gt;&amp;1</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+usage () {</span>
+<span class="a">+    cat &lt;&lt;&#x27;EOF&#x27;</span>
+<span class="a">+usage: bm [-t tag] [-S order] [action]</span>
+<span class="a">+Without an action: browse. Pick a bookmark, then what to do with it. Text</span>
+<span class="a">+that is no bookmark is opened as an address, or searched for on the web.</span>
+<span class="a">+  -o, --open        open a bookmark, an address or a web search</span>
+<span class="a">+                    (-p, --plumb is the same)</span>
+<span class="a">+  -c, --copy        copy a bookmark&#x27;s URL to the clipboard</span>
+<span class="a">+      --print       print a bookmark&#x27;s URL</span>
+<span class="a">+  -a, --add [url]   add a bookmark; the url defaults to the selection</span>
+<span class="a">+  -e, --edit        open the bookmark file in $EDITOR at a bookmark</span>
+<span class="a">+  -d, --delete      delete a bookmark</span>
+<span class="a">+  -l, --list        print the bookmarks</span>
+<span class="a">+  -m, --merge       add the bookmarks on stdin, skipping the known ones</span>
+<span class="a">+  -t, --tag &lt;tag&gt;   only consider bookmarks with this tag</span>
+<span class="a">+  -S, --sort &lt;o&gt;    used (default), recent, url, desc or tag</span>
+<span class="a">+  -h, --help</span>
+<span class="a">+EOF</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# ---- external programs: menu, clipboard, opener ----</span>
+<span class="a">+</span>
+<span class="a">+# dmenu when there is a display to draw it on and bm does not run in a</span>
+<span class="a">+# terminal, for example from a key of the window manager. In a terminal fzf</span>
+<span class="a">+# is the menu, because its preview shows the page of the bookmark under the</span>
+<span class="a">+# cursor. fzf also stands in when dmenu is not installed.</span>
+<span class="a">+pickmenu () {</span>
+<span class="a">+    [ -n &quot;$SBM_MENU&quot; ] &amp;&amp; return</span>
+<span class="a">+    if [ -n &quot;$DISPLAY$WAYLAND_DISPLAY&quot; ] &amp;&amp; ! { [ -t 0 ] &amp;&amp; [ -t 2 ]; }; then</span>
+<span class="a">+        order=&#x27;dmenu fzf&#x27;</span>
+<span class="a">+    else</span>
+<span class="a">+        order=&#x27;fzf dmenu&#x27;</span>
+<span class="a">+    fi</span>
+<span class="a">+    for menu in $order; do</span>
+<span class="a">+        if have &quot;$menu&quot;; then</span>
+<span class="a">+            SBM_MENU=$menu</span>
+<span class="a">+            return</span>
+<span class="a">+        fi</span>
+<span class="a">+    done</span>
+<span class="a">+    # shellcheck disable=SC2016</span>
+<span class="a">+    die &#x27;no menu program found: install dmenu or fzf, or set $SBM_MENU&#x27;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# $SBM_MENU may carry arguments, so it is split on purpose.</span>
+<span class="a">+custommenu () {</span>
+<span class="a">+    # shellcheck disable=SC2086</span>
+<span class="a">+    $SBM_MENU &quot;$@&quot;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+fzfcmd () {</span>
+<span class="a">+    # shellcheck disable=SC2086</span>
+<span class="a">+    fzf -i --reverse --height=40% $SBM_FZF_OPTS &quot;$@&quot;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# fzfpick &lt;fzf options&gt;: the chosen line, or else what was typed, which is</span>
+<span class="a">+# what dmenu gives by itself.</span>
+<span class="a">+fzfpick () {</span>
+<span class="a">+    out=$(fzfcmd --print-query &quot;$@&quot;)</span>
+<span class="a">+    chosen=$(printf &#x27;%s\n&#x27; &quot;$out&quot; | sed -n 2p)</span>
+<span class="a">+    printf &#x27;%s\n&#x27; &quot;${chosen:-$(printf &#x27;%s\n&#x27; &quot;$out&quot; | sed -n 1p)}&quot;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# pick &lt;prompt&gt;: choices on stdin; the chosen line, or the typed text.</span>
+<span class="a">+pick () {</span>
+<span class="a">+    case $SBM_MENU in</span>
+<span class="a">+        fzf)   fzfpick --prompt=&quot;$1 &quot; ;;</span>
+<span class="a">+        dmenu) dmenu -i -l &quot;$DMENULINES&quot; -p &quot;$1&quot; ;;</span>
+<span class="a">+        *)     custommenu pick &quot;$1&quot; ;;</span>
+<span class="a">+    esac</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# pickmulti &lt;prompt&gt;: like pick, for any number of lines.</span>
+<span class="a">+pickmulti () {</span>
+<span class="a">+    choices=$(cat)</span>
+<span class="a">+    case $SBM_MENU in</span>
+<span class="a">+        fzf)</span>
+<span class="a">+            while :; do</span>
+<span class="a">+                out=$(printf &#x27;%s\n&#x27; &quot;$choices&quot; | fzfcmd --multi --print-query \</span>
+<span class="a">+                      --prompt=&quot;$1 &quot; --header=&#x27;TAB marks, ENTER confirms. No match: ENTER creates the typed tag, ESC finishes.&#x27;)</span>
+<span class="a">+                chosen=$(printf &#x27;%s\n&#x27; &quot;$out&quot; | sed 1d)</span>
+<span class="a">+                if [ -n &quot;$chosen&quot; ]; then</span>
+<span class="a">+                    printf &#x27;%s\n&#x27; &quot;$chosen&quot;</span>
+<span class="a">+                    break</span>
+<span class="a">+                fi</span>
+<span class="a">+                typed=$(printf &#x27;%s\n&#x27; &quot;$out&quot; | sed -n 1p)</span>
+<span class="a">+                [ -n &quot;$typed&quot; ] || break</span>
+<span class="a">+                printf &#x27;%s\n&#x27; &quot;$typed&quot;</span>
+<span class="a">+            done</span>
+<span class="a">+            ;;</span>
+<span class="a">+        dmenu)</span>
+<span class="a">+            while :; do</span>
+<span class="a">+                one=$(printf &#x27;done | (choose when done)\n%s\n&#x27; &quot;$choices&quot; \</span>
+<span class="a">+                      | dmenu -i -l &quot;$DMENULINES&quot; -p &quot;$1&quot;)</span>
+<span class="a">+                # An empty answer means the menu was closed.</span>
+<span class="a">+                [ -z &quot;$one&quot; ] || [ &quot;${one%% *}&quot; = &#x27;done&#x27; ] &amp;&amp; break</span>
+<span class="a">+                printf &#x27;%s\n&#x27; &quot;$one&quot;</span>
+<span class="a">+            done</span>
+<span class="a">+            ;;</span>
+<span class="a">+        *)  printf &#x27;%s\n&#x27; &quot;$choices&quot; | custommenu multi &quot;$1&quot; ;;</span>
+<span class="a">+    esac</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# ask &lt;prompt&gt; [default]: free text on stdout.</span>
+<span class="a">+ask () {</span>
+<span class="a">+    case $SBM_MENU in</span>
+<span class="a">+        fzf)</span>
+<span class="a">+            printf &#x27;%s%s: &#x27; &quot;$1&quot; &quot;${2:+ [$2]}&quot; &gt;&amp;2</span>
+<span class="a">+            IFS= read -r reply || reply=</span>
+<span class="a">+            printf &#x27;%s\n&#x27; &quot;${reply:-$2}&quot;</span>
+<span class="a">+            ;;</span>
+<span class="a">+        dmenu)</span>
+<span class="a">+            if [ -n &quot;$2&quot; ]; then printf &#x27;%s\n&#x27; &quot;$2&quot;; fi | dmenu -p &quot;$1:&quot;</span>
+<span class="a">+            ;;</span>
+<span class="a">+        *)</span>
+<span class="a">+            if [ -n &quot;$2&quot; ]; then printf &#x27;%s\n&#x27; &quot;$2&quot;; fi | custommenu ask &quot;$1&quot;</span>
+<span class="a">+            ;;</span>
+<span class="a">+    esac</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+clip_out () {</span>
+<span class="a">+    # shellcheck disable=SC2086,SC2016</span>
+<span class="a">+    if   [ -n &quot;$SBM_COPY&quot; ]; then $SBM_COPY</span>
+<span class="a">+    elif [ -n &quot;$WAYLAND_DISPLAY&quot; ] &amp;&amp; have wl-copy; then wl-copy</span>
+<span class="a">+    elif have xclip; then xclip -i -selection clipboard</span>
+<span class="a">+    elif have xsel; then xsel -ib</span>
+<span class="a">+    else die &#x27;no clipboard program found: set $SBM_COPY&#x27;</span>
+<span class="a">+    fi</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# The primary selection first, then the clipboard. Never fatal: without</span>
+<span class="a">+# either there is just no URL to suggest.</span>
+<span class="a">+clip_in () {</span>
+<span class="a">+    # shellcheck disable=SC2086</span>
+<span class="a">+    if   [ -n &quot;$SBM_PASTE&quot; ]; then $SBM_PASTE</span>
+<span class="a">+    elif [ -n &quot;$WAYLAND_DISPLAY&quot; ] &amp;&amp; have wl-paste; then</span>
+<span class="a">+        wl-paste -n -p; echo; wl-paste -n</span>
+<span class="a">+    elif have xclip; then</span>
+<span class="a">+        xclip -o; echo; xclip -o -selection clipboard</span>
+<span class="a">+    elif have xsel; then</span>
+<span class="a">+        xsel -op; echo; xsel -ob</span>
+<span class="a">+    fi 2&gt;/dev/null</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+opener () {</span>
+<span class="a">+    # shellcheck disable=SC2086,SC2016</span>
+<span class="a">+    if   [ -n &quot;$SBM_OPEN&quot; ]; then $SBM_OPEN &quot;$1&quot;</span>
+<span class="a">+    elif have xdg-open; then xdg-open &quot;$1&quot;</span>
+<span class="a">+    else die &#x27;no opener found: set $SBM_OPEN&#x27;</span>
+<span class="a">+    fi</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# ---- the bookmark file ----</span>
+<span class="a">+</span>
+<span class="a">+# Bookmarks restricted to $tagfilter and ordered by $sortorder, each followed</span>
+<span class="a">+# by a fourth field: how often it was used.</span>
+<span class="a">+rows () {</span>
+<span class="a">+    SBM_TAG=$tagfilter SBM_ORDER=$sortorder SBM_USAGE=$USAGE awk -F&#x27;\t&#x27; &#x27;</span>
+<span class="a">+        BEGIN {</span>
+<span class="a">+            while ((getline line &lt; ENVIRON[&quot;SBM_USAGE&quot;]) &gt; 0) {</span>
+<span class="a">+                split(line, f, &quot;\t&quot;)</span>
+<span class="a">+                used[f[1]] = f[2]</span>
+<span class="a">+            }</span>
+<span class="a">+            order = ENVIRON[&quot;SBM_ORDER&quot;]</span>
+<span class="a">+        }</span>
+<span class="a">+        NF == 0 || /^#/ { next }</span>
+<span class="a">+        ENVIRON[&quot;SBM_TAG&quot;] != &quot;&quot; {</span>
+<span class="a">+            hit = 0</span>
+<span class="a">+            n = split($3, tags, &quot; &quot;)</span>
+<span class="a">+            for (i = 1; i &lt;= n; i++)</span>
+<span class="a">+                if (tags[i] == ENVIRON[&quot;SBM_TAG&quot;]) hit = 1</span>
+<span class="a">+            if (!hit) next</span>
+<span class="a">+        }</span>
+<span class="a">+        {</span>
+<span class="a">+            if (order == &quot;used&quot;)        key = sprintf(&quot;%09d&quot;, 999999999 - used[$1])</span>
+<span class="a">+            else if (order == &quot;recent&quot;) key = sprintf(&quot;%09d&quot;, 999999999 - NR)</span>
+<span class="a">+            else if (order == &quot;desc&quot;)   key = tolower($2)</span>
+<span class="a">+            else if (order == &quot;tag&quot;)    key = tolower($3)</span>
+<span class="a">+            else                        key = &quot;&quot;</span>
+<span class="a">+            printf &quot;%s\t%s\t%s\t%s\t%s\t%d\n&quot;, key, tolower($1), $1, $2, $3, used[$1]</span>
+<span class="a">+        }&#x27; &quot;$BOOKMARKS&quot; | LC_ALL=C sort -t&quot;$TAB&quot; -k1,1 -k2,2 | cut -f3-</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# details &lt;url&gt;: a bookmark in full, one field per line.</span>
+<span class="a">+details () {</span>
+<span class="a">+    rows | SBM_URL=$1 awk -F&#x27;\t&#x27; &#x27;$1 == ENVIRON[&quot;SBM_URL&quot;] {</span>
+<span class="a">+        printf &quot;%s\n%s\ntags: %s\nused: %d\n&quot;, $1, $2, $3, $4</span>
+<span class="a">+        exit</span>
+<span class="a">+    }&#x27;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+is_bookmark () {</span>
+<span class="a">+    SBM_URL=$1 awk -F&#x27;\t&#x27; &#x27;$1 == ENVIRON[&quot;SBM_URL&quot;] { found = 1; exit }</span>
+<span class="a">+        END { exit !found }&#x27; &quot;$BOOKMARKS&quot;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# For fzf a fifth field is added, and it is all that is listed. The first</span>
+<span class="a">+# four feed the preview, which follows the cursor: the bookmark in full, then</span>
+<span class="a">+# the text of its page when bm-page is installed. fzf stops the preview when</span>
+<span class="a">+# the cursor moves on, so the short sleep keeps a fast scroll from fetching</span>
+<span class="a">+# each page on the way. A sleep that knows no fractions fails and skips it.</span>
+<span class="a">+PREVIEW=&quot;printf &#x27;%s\n%s\ntags: %s\nused: %s\n\n&#x27; {1} {2} {3} {4}; sleep 0.3 2&gt;/dev/null; bm-page {1} 2&gt;/dev/null&quot;</span>
+<span class="a">+fzfrows () {</span>
+<span class="a">+    awk -F&#x27;\t&#x27; &#x27;NF {</span>
+<span class="a">+        desc = $2</span>
+<span class="a">+        if (length(desc) &gt; 44) desc = substr(desc, 1, 42) &quot;..&quot;</span>
+<span class="a">+        printf &quot;%s\t%-44s  %-18s  %s\n&quot;, $0, desc, $3, $1</span>
+<span class="a">+    }&#x27;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# withpreview &lt;fzf command and options&gt;: for the output of fzfrows.</span>
+<span class="a">+withpreview () {</span>
+<span class="a">+    &quot;$@&quot; --delimiter=&quot;$TAB&quot; --with-nth=5.. \</span>
+<span class="a">+         --preview-window=right:50%:wrap --preview=&quot;$PREVIEW&quot;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# choose &lt;prompt&gt;: a bookmark&#x27;s URL, or whatever else the user typed.</span>
+<span class="a">+choose () {</span>
+<span class="a">+    if [ &quot;$SBM_MENU&quot; = fzf ]; then</span>
+<span class="a">+        rows | fzfrows | withpreview fzfpick --prompt=&quot;$1 &quot;</span>
+<span class="a">+    else</span>
+<span class="a">+        rows | cut -f1-3 | pick &quot;$1&quot;</span>
+<span class="a">+    fi | cut -f1</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# choose_url &lt;prompt&gt;: like choose, but only a bookmark will do.</span>
+<span class="a">+choose_url () {</span>
+<span class="a">+    if [ -z &quot;$(rows)&quot; ]; then</span>
+<span class="a">+        [ -n &quot;$tagfilter&quot; ] &amp;&amp; die &quot;no bookmarks tagged &#x27;$tagfilter&#x27;&quot;</span>
+<span class="a">+        die &#x27;no bookmarks yet; add one with: bm --add&#x27;</span>
+<span class="a">+    fi</span>
+<span class="a">+    url=$(choose &quot;$1&quot;)</span>
+<span class="a">+    [ -n &quot;$url&quot; ] || exit 1</span>
+<span class="a">+    is_bookmark &quot;$url&quot; || die &quot;not a bookmark: $url&quot;</span>
+<span class="a">+    printf &#x27;%s\n&#x27; &quot;$url&quot;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# find_dup &lt;url&gt;: print the bookmark that is the same page as &lt;url&gt;, if any.</span>
+<span class="a">+find_dup () {</span>
+<span class="a">+    SBM_URL=$1 awk -F&#x27;\t&#x27; &quot;$AWK_NORM&quot;&#x27;</span>
+<span class="a">+        BEGIN { want = norm(ENVIRON[&quot;SBM_URL&quot;]) }</span>
+<span class="a">+        NF &amp;&amp; !/^#/ &amp;&amp; norm($1) == want { print; exit }&#x27; &quot;$BOOKMARKS&quot;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# tmpfile &lt;file&gt;: create an empty temporary file and print its name. Beside</span>
+<span class="a">+# &lt;file&gt; if that directory can be written to, else in $TMPDIR. noclobber makes</span>
+<span class="a">+# the shell create it with O_EXCL, so a file or symlink planted under that</span>
+<span class="a">+# name in a shared /tmp is refused rather than followed: mktemp, in POSIX.</span>
+<span class="a">+tmpfile () {</span>
+<span class="a">+    for candidate in &quot;$1.tmp.$$&quot; &quot;${TMPDIR:-/tmp}/sbm.$$.${1##*/}&quot;; do</span>
+<span class="a">+        if (umask 077; set -C; : &gt; &quot;$candidate&quot;) 2&gt;/dev/null; then</span>
+<span class="a">+            printf &#x27;%s\n&#x27; &quot;$candidate&quot;</span>
+<span class="a">+            return 0</span>
+<span class="a">+        fi</span>
+<span class="a">+    done</span>
+<span class="a">+    return 1</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# rewrite &lt;file&gt;: replace &lt;file&gt; with stdin. Through cat, not mv, so that a</span>
+<span class="a">+# symlinked file stays a symlink and keeps its mode.</span>
+<span class="a">+rewrite () {</span>
+<span class="a">+    tmp=$(tmpfile &quot;$1&quot;) || die &quot;cannot create a temporary file for $1&quot;</span>
+<span class="a">+    cat &gt; &quot;$tmp&quot; &amp;&amp; cat &quot;$tmp&quot; &gt; &quot;$1&quot;</span>
+<span class="a">+    rc=$?</span>
+<span class="a">+    rm -f &quot;$tmp&quot;</span>
+<span class="a">+    return $rc</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# lock: take the write lock of the bookmark file, a directory beside it. bm</span>
+<span class="a">+# holds it while it changes the file, and bm-sync while it reads or writes</span>
+<span class="a">+# the file, so that neither writes over a change of the other. A lock whose</span>
+<span class="a">+# process is gone is taken over. After $SBM_LOCK_WAIT seconds (default 10)</span>
+<span class="a">+# bm stops.</span>
+<span class="a">+lock () {</span>
+<span class="a">+    waited=0</span>
+<span class="a">+    until mkdir &quot;$WLOCK&quot; 2&gt;/dev/null; do</span>
+<span class="a">+        pid=$(cat &quot;$WLOCK/pid&quot; 2&gt;/dev/null)</span>
+<span class="a">+        if [ -n &quot;$pid&quot; ] &amp;&amp; ! kill -0 &quot;$pid&quot; 2&gt;/dev/null; then</span>
+<span class="a">+            [ &quot;$(cat &quot;$WLOCK/pid&quot; 2&gt;/dev/null)&quot; != &quot;$pid&quot; ] || rm -rf &quot;$WLOCK&quot;</span>
+<span class="a">+            continue</span>
+<span class="a">+        fi</span>
+<span class="a">+        [ &quot;$waited&quot; -lt &quot;${SBM_LOCK_WAIT:-10}&quot; ] \</span>
+<span class="a">+            || die &quot;$BOOKMARKS is in use (bm -e or bm-sync). If neither runs, remove $WLOCK&quot;</span>
+<span class="a">+        sleep 1</span>
+<span class="a">+        waited=$((waited + 1))</span>
+<span class="a">+    done</span>
+<span class="a">+    echo $$ &gt; &quot;$WLOCK/pid&quot;</span>
+<span class="a">+    locked=1</span>
+<span class="a">+    # Also in the subshell of add_inside, which does not keep these traps.</span>
+<span class="a">+    trap unlock EXIT</span>
+<span class="a">+    trap &#x27;exit 1&#x27; HUP INT TERM</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+unlock () {</span>
+<span class="a">+    [ -z &quot;$locked&quot; ] || rm -rf &quot;$WLOCK&quot;</span>
+<span class="a">+    locked=</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# bump &lt;url&gt;: count a use, for the &quot;used&quot; order. The counts live beside the</span>
+<span class="a">+# bookmark file, which stays a plain list: url, count, time of last use.</span>
+<span class="a">+# srand() returns the previous seed, and the first seed is the time of day:</span>
+<span class="a">+# that is &quot;date +%s&quot; for a POSIX that has no %s.</span>
+<span class="a">+bump () {</span>
+<span class="a">+    [ -e &quot;$USAGE&quot; ] || : &gt; &quot;$USAGE&quot; || return 0</span>
+<span class="a">+    SBM_URL=$1 awk -F&#x27;\t&#x27; &#x27;</span>
+<span class="a">+        BEGIN { OFS = &quot;\t&quot;; srand(); now = srand() }</span>
+<span class="a">+        $1 == ENVIRON[&quot;SBM_URL&quot;] { $2 = $2 + 1; $3 = now; seen = 1 }</span>
+<span class="a">+        { print }</span>
+<span class="a">+        END { if (!seen) print ENVIRON[&quot;SBM_URL&quot;], 1, now }</span>
+<span class="a">+    &#x27; &quot;$USAGE&quot; | rewrite &quot;$USAGE&quot;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# learn_tags: tag names on stdin; the ones $USERTAGS lacks are appended to it.</span>
+<span class="a">+learn_tags () {</span>
+<span class="a">+    [ -e &quot;$USERTAGS&quot; ] || : &gt; &quot;$USERTAGS&quot;</span>
+<span class="a">+    while IFS= read -r name; do</span>
+<span class="a">+        [ -n &quot;$name&quot; ] || continue</span>
+<span class="a">+        sed &#x27;s/[ |].*//&#x27; &quot;$USERTAGS&quot; | grep -Fxq -- &quot;$name&quot; \</span>
+<span class="a">+            || printf &#x27;%s | \n&#x27; &quot;$name&quot; &gt;&gt; &quot;$USERTAGS&quot;</span>
+<span class="a">+    done</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# changed &lt;message&gt;: the file was written. bm-commit, where installed, keeps</span>
+<span class="a">+# its history in git. bm-sync, where installed, tells your other machines; it</span>
+<span class="a">+# runs in the background, so that a slow network never holds the menu up.</span>
+<span class="a">+changed () {</span>
+<span class="a">+    if have bm-commit; then bm-commit &quot;$1&quot;; fi</span>
+<span class="a">+    if have bm-sync; then bm-sync -q &gt;/dev/null 2&gt;&amp;1 &amp; fi</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# ---- actions ----</span>
+<span class="a">+</span>
+<span class="a">+copy_url () {</span>
+<span class="a">+    bump &quot;$1&quot;</span>
+<span class="a">+    printf &#x27;%s&#x27; &quot;$1&quot; | clip_out</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+open_url () {</span>
+<span class="a">+    bump &quot;$1&quot;</span>
+<span class="a">+    opener &quot;$1&quot;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# The editor reads the file when it starts and writes it when you save, so</span>
+<span class="a">+# bm holds the lock until the editor ends. bm-sync then waits, and syncs the</span>
+<span class="a">+# edit after it.</span>
+<span class="a">+edit_url () {</span>
+<span class="a">+    lock</span>
+<span class="a">+    line=$(SBM_URL=$1 awk -F&#x27;\t&#x27; \</span>
+<span class="a">+           &#x27;$1 == ENVIRON[&quot;SBM_URL&quot;] { print NR; exit }&#x27; &quot;$BOOKMARKS&quot;)</span>
+<span class="a">+    ${VISUAL:-${EDITOR:-vi}} &quot;+${line:-1}&quot; &quot;$BOOKMARKS&quot;</span>
+<span class="a">+    unlock</span>
+<span class="a">+    changed &#x27;bm: edit&#x27;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+delete_url () {</span>
+<span class="a">+    lock</span>
+<span class="a">+    for file in &quot;$BOOKMARKS&quot; &quot;$USAGE&quot;; do</span>
+<span class="a">+        [ -e &quot;$file&quot; ] || continue</span>
+<span class="a">+        SBM_URL=$1 awk -F&#x27;\t&#x27; &#x27;$1 != ENVIRON[&quot;SBM_URL&quot;]&#x27; &quot;$file&quot; \</span>
+<span class="a">+            | rewrite &quot;$file&quot; || die &quot;could not rewrite $file&quot;</span>
+<span class="a">+    done</span>
+<span class="a">+    unlock</span>
+<span class="a">+    changed &quot;bm: delete $1&quot;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# urlencode &lt;text&gt;: for a query string. awk has no ord(), hence the table.</span>
+<span class="a">+urlencode () {</span>
+<span class="a">+    printf &#x27;%s&#x27; &quot;$1&quot; | LC_ALL=C awk &#x27;</span>
+<span class="a">+        BEGIN { for (i = 1; i &lt; 256; i++) ord[sprintf(&quot;%c&quot;, i)] = i }</span>
+<span class="a">+        {</span>
+<span class="a">+            for (i = 1; i &lt;= length($0); i++) {</span>
+<span class="a">+                c = substr($0, i, 1)</span>
+<span class="a">+                if (c ~ /[A-Za-z0-9._~-]/) out = out c</span>
+<span class="a">+                else if (c == &quot; &quot;)         out = out &quot;+&quot;</span>
+<span class="a">+                else                       out = out sprintf(&quot;%%%02X&quot;, ord[c])</span>
+<span class="a">+            }</span>
+<span class="a">+        }</span>
+<span class="a">+        END { printf &quot;%s&quot;, out }&#x27;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# go &lt;text&gt;: open text that is not a bookmark. One word with &quot;://&quot; or a dot</span>
+<span class="a">+# in it is an address. Anything else is a web search: by the engine whose</span>
+<span class="a">+# keyword is the first word ($ENGINES holds &quot;keyword | url with %s&quot; lines),</span>
+<span class="a">+# or else by $SEARCH.</span>
+<span class="a">+go () {</span>
+<span class="a">+    case $1 in</span>
+<span class="a">+        *&#x27; &#x27;*) ;;</span>
+<span class="a">+        *://*) opener &quot;$1&quot;; return ;;</span>
+<span class="a">+        *.*)   opener &quot;https://$1&quot;; return ;;</span>
+<span class="a">+    esac</span>
+<span class="a">+    engine=$SEARCH</span>
+<span class="a">+    query=$1</span>
+<span class="a">+    if [ &quot;${1%% *}&quot; != &quot;$1&quot; ] &amp;&amp; [ -r &quot;$ENGINES&quot; ]; then</span>
+<span class="a">+        found=$(SBM_KEY=${1%% *} awk -F&#x27; *[|] *&#x27; \</span>
+<span class="a">+                &#x27;$1 == ENVIRON[&quot;SBM_KEY&quot;] { print $2; exit }&#x27; &quot;$ENGINES&quot;)</span>
+<span class="a">+        if [ -n &quot;$found&quot; ]; then</span>
+<span class="a">+            engine=$found</span>
+<span class="a">+            query=${1#* }</span>
+<span class="a">+        fi</span>
+<span class="a">+    fi</span>
+<span class="a">+    opener &quot;$(printf &#x27;%s\n&#x27; &quot;$engine&quot; | sed &quot;s|%s|$(urlencode &quot;$query&quot;)|&quot;)&quot;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+plumb () {</span>
+<span class="a">+    url=$(choose &#x27;open:&#x27;)</span>
+<span class="a">+    [ -n &quot;$url&quot; ] || exit 1</span>
+<span class="a">+    if is_bookmark &quot;$url&quot;; then open_url &quot;$url&quot;; else go &quot;$url&quot;; fi</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# Exit status 3 means &quot;already bookmarked&quot;.</span>
+<span class="a">+add () {</span>
+<span class="a">+    url=$1</span>
+<span class="a">+    if [ -z &quot;$url&quot; ]; then</span>
+<span class="a">+        url=$(ask &#x27;url&#x27; &quot;$(clip_in | grep &#x27;://&#x27; | head -n 1)&quot;)</span>
+<span class="a">+    fi</span>
+<span class="a">+    url=$(printf &#x27;%s&#x27; &quot;$url&quot; | tr -d &#x27;[:space:]&#x27;)</span>
+<span class="a">+    [ -n &quot;$url&quot; ] || exit 1</span>
+<span class="a">+</span>
+<span class="a">+    dup=$(find_dup &quot;$url&quot;)</span>
+<span class="a">+    if [ -n &quot;$dup&quot; ]; then</span>
+<span class="a">+        printf &#x27;bm: already bookmarked:\n%s\n&#x27; &quot;$dup&quot; &gt;&amp;2</span>
+<span class="a">+        exit 3</span>
+<span class="a">+    fi</span>
+<span class="a">+</span>
+<span class="a">+    # bm-title, where installed, fetches the page&#x27;s title.</span>
+<span class="a">+    title=</span>
+<span class="a">+    if have bm-title; then title=$(bm-title &quot;$url&quot;); fi</span>
+<span class="a">+    desc=$(ask &#x27;description&#x27; &quot;$title&quot; | tr &#x27;\t&#x27; &#x27; &#x27;)</span>
+<span class="a">+</span>
+<span class="a">+    [ -e &quot;$USERTAGS&quot; ] || : &gt; &quot;$USERTAGS&quot;</span>
+<span class="a">+    names=$(pickmulti &#x27;tags:&#x27; &lt; &quot;$USERTAGS&quot; | sed &#x27;s/[ |].*//&#x27; | sed &#x27;/^$/d&#x27; \</span>
+<span class="a">+            | sort -u)</span>
+<span class="a">+    printf &#x27;%s\n&#x27; &quot;$names&quot; | learn_tags</span>
+<span class="a">+    tags=$(printf &#x27;%s\n&#x27; &quot;$names&quot; | paste -sd &#x27; &#x27; -)</span>
+<span class="a">+</span>
+<span class="a">+    lock</span>
+<span class="a">+    printf &#x27;%s\t%s\t%s\n&#x27; &quot;$url&quot; &quot;$desc&quot; &quot;$tags&quot; &gt;&gt; &quot;$BOOKMARKS&quot;</span>
+<span class="a">+    unlock</span>
+<span class="a">+    changed &quot;bm: add $url&quot;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# merge: bookmark lines on stdin (a bare URL per line will do); the ones not</span>
+<span class="a">+# known yet are appended. This is how the output of bm-import gets in.</span>
+<span class="a">+merge () {</span>
+<span class="a">+    new=$(tmpfile &quot;$BOOKMARKS&quot;) || die &#x27;cannot create a temporary file&#x27;</span>
+<span class="a">+    lock</span>
+<span class="a">+    SBM_FILE=$BOOKMARKS SBM_NEW=$new awk -F&#x27;\t&#x27; &quot;$AWK_NORM&quot;&#x27;</span>
+<span class="a">+        BEGIN {</span>
+<span class="a">+            while ((getline line &lt; ENVIRON[&quot;SBM_FILE&quot;]) &gt; 0) {</span>
+<span class="a">+                split(line, f, &quot;\t&quot;)</span>
+<span class="a">+                seen[norm(f[1])] = 1</span>
+<span class="a">+            }</span>
+<span class="a">+        }</span>
+<span class="a">+        NF == 0 || /^#/ { next }</span>
+<span class="a">+        seen[norm($1)]++ { skipped++; next }</span>
+<span class="a">+        { printf &quot;%s\t%s\t%s\n&quot;, $1, $2, $3 &gt; ENVIRON[&quot;SBM_NEW&quot;]; added++ }</span>
+<span class="a">+        END { printf &quot;added %d, skipped %d duplicates\n&quot;, added, skipped }&#x27;</span>
+<span class="a">+    cut -f3 &quot;$new&quot; | tr &#x27; &#x27; &#x27;\n&#x27; | sort -u | learn_tags</span>
+<span class="a">+    cat &quot;$new&quot; &gt;&gt; &quot;$BOOKMARKS&quot;</span>
+<span class="a">+    unlock</span>
+<span class="a">+    rm -f &quot;$new&quot;</span>
+<span class="a">+    changed &#x27;bm: merge&#x27;</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+nextorder () {</span>
+<span class="a">+    case $sortorder in</span>
+<span class="a">+        used)   sortorder=recent ;;</span>
+<span class="a">+        recent) sortorder=url ;;</span>
+<span class="a">+        url)    sortorder=desc ;;</span>
+<span class="a">+        desc)   sortorder=tag ;;</span>
+<span class="a">+        *)      sortorder=used ;;</span>
+<span class="a">+    esac</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# add exits, which must not end a browsing session; and when it refuses a</span>
+<span class="a">+# duplicate the user has to get to see that before the list comes back.</span>
+<span class="a">+add_inside () {</span>
+<span class="a">+    ( add &#x27;&#x27; )</span>
+<span class="a">+    if [ $? -eq 3 ]; then</span>
+<span class="a">+        ask &#x27;already bookmarked; press ENTER&#x27; &gt;/dev/null</span>
+<span class="a">+    fi</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# browse_fzf: fzf can bind keys, so one list does everything. Opening and</span>
+<span class="a">+# copying leave it, the other keys come back to it.</span>
+<span class="a">+browse_fzf () {</span>
+<span class="a">+    while :; do</span>
+<span class="a">+        header=&quot;ENTER open  ^Y copy  ^A add  ^D delete  ^E edit  ^S sort [$sortorder]&quot;</span>
+<span class="a">+        out=$(rows | fzfrows | withpreview fzfcmd --print-query \</span>
+<span class="a">+              --expect=ctrl-y,ctrl-a,ctrl-d,ctrl-e,ctrl-s \</span>
+<span class="a">+              --header=&quot;$header&quot; --prompt=&#x27;bookmark: &#x27;)</span>
+<span class="a">+        [ -n &quot;$out&quot; ] || exit 1</span>
+<span class="a">+        typed=$(printf &#x27;%s\n&#x27; &quot;$out&quot; | sed -n 1p)</span>
+<span class="a">+        key=$(printf &#x27;%s\n&#x27; &quot;$out&quot; | sed -n 2p)</span>
+<span class="a">+        url=$(printf &#x27;%s\n&#x27; &quot;$out&quot; | sed -n 3p | cut -f1)</span>
+<span class="a">+</span>
+<span class="a">+        case $key in</span>
+<span class="a">+            ctrl-a) add_inside; continue ;;</span>
+<span class="a">+            ctrl-s) nextorder; continue ;;</span>
+<span class="a">+        esac</span>
+<span class="a">+        if [ -z &quot;$url&quot; ]; then</span>
+<span class="a">+            [ -n &quot;$key&quot; ] &amp;&amp; continue</span>
+<span class="a">+            [ -n &quot;$typed&quot; ] || exit 1</span>
+<span class="a">+            go &quot;$typed&quot;</span>
+<span class="a">+            exit 0</span>
+<span class="a">+        fi</span>
+<span class="a">+        case $key in</span>
+<span class="a">+            ctrl-y) copy_url &quot;$url&quot;; exit 0 ;;</span>
+<span class="a">+            ctrl-e) edit_url &quot;$url&quot; ;;</span>
+<span class="a">+            ctrl-d)</span>
+<span class="a">+                case $(ask &quot;delete $url? (y/N)&quot;) in</span>
+<span class="a">+                    y|Y|yes) delete_url &quot;$url&quot; ;;</span>
+<span class="a">+                esac</span>
+<span class="a">+                ;;</span>
+<span class="a">+            *)      open_url &quot;$url&quot;; exit 0 ;;</span>
+<span class="a">+        esac</span>
+<span class="a">+    done</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# browse_menu: dmenu can neither bind keys nor preview, so a second menu does</span>
+<span class="a">+# both: it shows the bookmark in full and asks what to do with it. Closing it</span>
+<span class="a">+# goes back to the list; closing the list leaves.</span>
+<span class="a">+browse_menu () {</span>
+<span class="a">+    while :; do</span>
+<span class="a">+        chosen=$( { printf &#x27;[add]\n&#x27;; rows | cut -f1-3; } | pick &#x27;bookmark:&#x27;)</span>
+<span class="a">+        [ -n &quot;$chosen&quot; ] || exit 1</span>
+<span class="a">+        if [ &quot;$chosen&quot; = &#x27;[add]&#x27; ]; then</span>
+<span class="a">+            add_inside</span>
+<span class="a">+            continue</span>
+<span class="a">+        fi</span>
+<span class="a">+        url=$(printf &#x27;%s\n&#x27; &quot;$chosen&quot; | cut -f1)</span>
+<span class="a">+        if ! is_bookmark &quot;$url&quot;; then</span>
+<span class="a">+            go &quot;$chosen&quot;</span>
+<span class="a">+            exit 0</span>
+<span class="a">+        fi</span>
+<span class="a">+        action=$( { printf &#x27;open\ncopy\nedit\ndelete\n&#x27;</span>
+<span class="a">+                    details &quot;$url&quot; | sed &#x27;s/^/  /&#x27;; } | pick &#x27;do:&#x27;)</span>
+<span class="a">+        case $action in</span>
+<span class="a">+            open)   open_url &quot;$url&quot;; exit 0 ;;</span>
+<span class="a">+            copy)   copy_url &quot;$url&quot;; exit 0 ;;</span>
+<span class="a">+            edit)   edit_url &quot;$url&quot; ;;</span>
+<span class="a">+            delete) delete_url &quot;$url&quot; ;;</span>
+<span class="a">+        esac</span>
+<span class="a">+    done</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# ---- main ----</span>
+<span class="a">+</span>
+<span class="a">+setaction () {</span>
+<span class="a">+    if [ -n &quot;$action&quot; ]; then</span>
+<span class="a">+        printf &#x27;bm: only one action at a time\n&#x27; &gt;&amp;2</span>
+<span class="a">+        exit 2</span>
+<span class="a">+    fi</span>
+<span class="a">+    action=$1</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+# needarg &lt;flag&gt; &lt;count of remaining arguments&gt; &lt;value&gt;</span>
+<span class="a">+needarg () {</span>
+<span class="a">+    if [ &quot;$2&quot; -lt 2 ] || [ -z &quot;$3&quot; ]; then</span>
+<span class="a">+        printf &#x27;bm: %s needs an argument\n&#x27; &quot;$1&quot; &gt;&amp;2</span>
+<span class="a">+        exit 2</span>
+<span class="a">+    fi</span>
+<span class="a">+}</span>
+<span class="a">+</span>
+<span class="a">+action=</span>
+<span class="a">+addurl=</span>
+<span class="a">+tagfilter=</span>
+<span class="a">+sortorder=${SBM_SORT:-used}</span>
+<span class="a">+while [ $# -gt 0 ]; do</span>
+<span class="a">+    case $1 in</span>
+<span class="a">+        -h|--help)   setaction help ;;</span>
+<span class="a">+        -c|--copy)   setaction copy ;;</span>
+<span class="a">+        --print)     setaction print ;;</span>
+<span class="a">+        -o|--open|-p|--plumb) setaction open ;;</span>
+<span class="a">+        -e|--edit)   setaction edit ;;</span>
+<span class="a">+        -d|--delete) setaction delete ;;</span>
+<span class="a">+        -l|--list)   setaction list ;;</span>
+<span class="a">+        -m|--merge)  setaction merge ;;</span>
+<span class="a">+        -a|--add)</span>
+<span class="a">+            setaction add</span>
+<span class="a">+            case ${2-} in</span>
+<span class="a">+                &#x27;&#x27;|-*) ;;</span>
+<span class="a">+                *) addurl=$2; shift ;;</span>
+<span class="a">+            esac</span>
+<span class="a">+            ;;</span>
+<span class="a">+        -t|--tag)</span>
+<span class="a">+            needarg &quot;$1&quot; $# &quot;${2-}&quot;</span>
+<span class="a">+            tagfilter=$2</span>
+<span class="a">+            shift</span>
+<span class="a">+            ;;</span>
+<span class="a">+        -S|--sort)</span>
+<span class="a">+            needarg &quot;$1&quot; $# &quot;${2-}&quot;</span>
+<span class="a">+            sortorder=$2</span>
+<span class="a">+            shift</span>
+<span class="a">+            ;;</span>
+<span class="a">+        *)</span>
+<span class="a">+            printf &#x27;bm: unknown argument: %s\n&#x27; &quot;$1&quot; &gt;&amp;2</span>
+<span class="a">+            usage &gt;&amp;2</span>
+<span class="a">+            exit 2</span>
+<span class="a">+            ;;</span>
+<span class="a">+    esac</span>
+<span class="a">+    shift</span>
+<span class="a">+done</span>
+<span class="a">+</span>
+<span class="a">+case $sortorder in</span>
+<span class="a">+    used|recent|url|desc|tag) ;;</span>
+<span class="a">+    *)</span>
+<span class="a">+        printf &#x27;bm: unknown sort order: %s\n&#x27; &quot;$sortorder&quot; &gt;&amp;2</span>
+<span class="a">+        exit 2</span>
+<span class="a">+        ;;</span>
+<span class="a">+esac</span>
+<span class="a">+</span>
+<span class="a">+if [ &quot;$action&quot; = help ]; then</span>
+<span class="a">+    usage</span>
+<span class="a">+    exit 0</span>
+<span class="a">+fi</span>
+<span class="a">+</span>
+<span class="a">+mkdir -p &quot;$(dirname &quot;$BOOKMARKS&quot;)&quot; || die &quot;cannot create directory for $BOOKMARKS&quot;</span>
+<span class="a">+[ -e &quot;$BOOKMARKS&quot; ] || : &gt; &quot;$BOOKMARKS&quot; || die &quot;cannot create $BOOKMARKS&quot;</span>
+<span class="a">+</span>
+<span class="a">+if awk &#x27;!/\t/ &amp;&amp; !/^#/ &amp;&amp; NF { found = 1; exit } END { exit !found }&#x27; &quot;$BOOKMARKS&quot;</span>
+<span class="a">+then</span>
+<span class="a">+    printf &#x27;bm: %s has lines in the old format; run: bm-migrate\n&#x27; &quot;$BOOKMARKS&quot; &gt;&amp;2</span>
+<span class="a">+fi</span>
+<span class="a">+</span>
+<span class="a">+case $action in</span>
+<span class="a">+    list|merge) ;;</span>
+<span class="a">+    *) pickmenu ;;</span>
+<span class="a">+esac</span>
+<span class="a">+</span>
+<span class="a">+case $action in</span>
+<span class="a">+    &#x27;&#x27;)</span>
+<span class="a">+        if [ &quot;$SBM_MENU&quot; = fzf ]; then browse_fzf; else browse_menu; fi</span>
+<span class="a">+        ;;</span>
+<span class="a">+    open)   plumb ;;</span>
+<span class="a">+    copy)   url=$(choose_url &#x27;copy:&#x27;)   &amp;&amp; copy_url &quot;$url&quot; ;;</span>
+<span class="a">+    print)  url=$(choose_url &#x27;print:&#x27;)  &amp;&amp; bump &quot;$url&quot; &amp;&amp; printf &#x27;%s\n&#x27; &quot;$url&quot; ;;</span>
+<span class="a">+    edit)   url=$(choose_url &#x27;edit:&#x27;)   &amp;&amp; edit_url &quot;$url&quot; ;;</span>
+<span class="a">+    delete) url=$(choose_url &#x27;delete:&#x27;) &amp;&amp; delete_url &quot;$url&quot; \</span>
+<span class="a">+                &amp;&amp; printf &#x27;deleted %s\n&#x27; &quot;$url&quot; ;;</span>
+<span class="a">+    add)    add &quot;$addurl&quot; ;;</span>
+<span class="a">+    list)   rows | cut -f1-3 ;;</span>
+<span class="a">+    merge)  merge ;;</span>
+<span class="a">+esac</span>
+diff --git a/git-update-repos b/git-update-repos
+new file mode 100644
+index 0000000..1784322
+--- /dev/null
++++ b/git-update-repos
+<span class="h">@@ -0,0 +1,32 @@</span>
+<span class="a">+#!/bin/sh</span>
+<span class="a">+# Update all git repos in directory tree</span>
+<span class="a">+dir=${1:-&quot;.&quot;}</span>
+<span class="a">+results=$(mktemp)</span>
+<span class="a">+log=$(mktemp)</span>
+<span class="a">+trap &quot;tsp -K; rm -f &quot;$results&quot; &quot;$log&quot;&quot; EXIT</span>
+<span class="a">+echo &quot;Updating repos in: &quot;$dir&quot;&quot;</span>
+<span class="a">+tsp -S 8</span>
+<span class="a">+find &quot;&quot;$dir&quot;&quot; -name &quot;.git&quot; -type d 2&gt;/dev/null | while read git_dir; do</span>
+<span class="a">+	tsp -n git-update-worker.sh &quot;$(dirname &quot;&quot;$git_dir&quot;&quot;)&quot; &quot;&quot;$results&quot;&quot; &quot;&quot;$log&quot;&quot;</span>
+<span class="a">+done</span>
+<span class="a">+tail -f &quot;&quot;$log&quot;&quot; &amp;</span>
+<span class="a">+tail_pid=$!</span>
+<span class="a">+tsp -w</span>
+<span class="a">+kill &quot;$tail_pid&quot; 2&gt;/dev/null</span>
+<span class="a">+conflicts=$(grep -c &quot;^REBASE_CONFLICT:&quot; &quot;&quot;$results&quot;&quot; 2&gt;/dev/null || echo 0)</span>
+<span class="a">+echo &quot;CHANGES:&quot;</span>
+<span class="a">+grep &quot;^UPDATED:&quot; &quot;&quot;$results&quot;&quot; | while IFS=&#x27;|&#x27; read prefix repo changelog; do</span>
+<span class="a">+	repo_name=$(echo &quot;&quot;$repo&quot;&quot; | sed &quot;s|^&quot;$dir&quot;/||&quot; | sed &#x27;s|^./||&#x27;)</span>
+<span class="a">+	echo &quot;\033[32m&quot;$repo_name&quot;:\033[0m&quot;</span>
+<span class="a">+	echo &quot;&quot;$changelog&quot;&quot; | tr &#x27;;&#x27; &#x27;\n&#x27; | sed &#x27;s/^/  /&#x27; | sed &#x27;s/^  $//&#x27;</span>
+<span class="a">+done</span>
+<span class="a">+if grep -q &quot;^REBASE_CONFLICT:&quot; &quot;&quot;$results&quot;&quot;; then</span>
+<span class="a">+	echo &quot;\033[31mREBASE CONFLICTS:\033[0m&quot;</span>
+<span class="a">+	grep &quot;^REBASE_CONFLICT:&quot; &quot;&quot;$results&quot;&quot; | while IFS=&#x27;|&#x27; read prefix repo _; do</span>
+<span class="a">+		repo_name=$(echo &quot;&quot;$repo&quot;&quot; | sed &quot;s|^&quot;$dir&quot;/||&quot; | sed &#x27;s|^./||&#x27;)</span>
+<span class="a">+		echo &quot;\033[31m&quot;$repo_name&quot;: requires manual resolution\033[0m&quot;</span>
+<span class="a">+	done</span>
+<span class="a">+fi</span>
+<span class="a">+if [ &quot;&quot;$conflicts&quot;&quot; ] &amp;&amp; [ &quot;&quot;$conflicts&quot;&quot; -gt 0 ] 2&gt;/dev/null; then</span>
+<span class="a">+	notify-send &quot;Git Rebase Conflicts&quot; &quot;&quot;$conflicts&quot; repos need manual resolution&quot; -u critical -t 10000</span>
+<span class="a">+fi</span>
+diff --git a/git-update-worker.sh b/git-update-worker.sh
+new file mode 100755
+index 0000000..9926866
+--- /dev/null
++++ b/git-update-worker.sh
+<span class="h">@@ -0,0 +1,28 @@</span>
+<span class="a">+#!/bin/sh</span>
+<span class="a">+repo=&quot;$1&quot;</span>
+<span class="a">+results=&quot;$2&quot;</span>
+<span class="a">+log=&quot;$3&quot;</span>
+<span class="a">+name=$(basename &quot;$repo&quot;)</span>
+<span class="a">+old=$(cd &quot;$repo&quot; &amp;&amp; git rev-parse HEAD 2&gt;/dev/null)</span>
+<span class="a">+if [ -f &quot;$repo/.git/FETCH_HEAD&quot; ]; then</span>
+<span class="a">+	if [ &quot;$(find &quot;$repo/.git/FETCH_HEAD&quot; -mmin -5 2&gt;/dev/null)&quot; ]; then</span>
+<span class="a">+		echo &quot;[$name] skipped&quot; &gt;&gt; &quot;$log&quot;</span>
+<span class="a">+		exit</span>
+<span class="a">+	fi</span>
+<span class="a">+fi</span>
+<span class="a">+(cd &quot;$repo&quot; &amp;&amp; git remote update &gt;/dev/null 2&gt;&amp;1)</span>
+<span class="a">+if (cd &quot;$repo&quot; &amp;&amp; git pull --rebase &gt;/dev/null 2&gt;&amp;1); then</span>
+<span class="a">+	:</span>
+<span class="a">+elif (cd &quot;$repo&quot; &amp;&amp; git rebase --abort &gt;/dev/null 2&gt;&amp;1); then</span>
+<span class="a">+	echo &quot;REBASE_CONFLICT:$repo|&quot; &gt;&gt; &quot;$results&quot;</span>
+<span class="a">+	echo &quot;[$name] REBASE CONFLICT&quot; &gt;&gt; &quot;$log&quot;</span>
+<span class="a">+	exit</span>
+<span class="a">+else</span>
+<span class="a">+	echo &quot;[$name] FAILED&quot; &gt;&gt; &quot;$log&quot;</span>
+<span class="a">+	exit</span>
+<span class="a">+fi</span>
+<span class="a">+new=$(cd &quot;$repo&quot; &amp;&amp; git rev-parse HEAD 2&gt;/dev/null)</span>
+<span class="a">+[ &quot;$old&quot; = &quot;$new&quot; ] &amp;&amp; { echo &quot;[$name] up to date&quot; &gt;&gt; &quot;$log&quot;; exit; }</span>
+<span class="a">+echo &quot;[$name] UPDATED&quot; &gt;&gt; &quot;$log&quot;</span>
+<span class="a">+changelog=$(cd &quot;$repo&quot; &amp;&amp; git log --oneline --no-merges -3 &quot;$old..$new&quot; 2&gt;/dev/null | tr &#x27;\n&#x27; &#x27;;&#x27;)</span>
+<span class="a">+echo &quot;UPDATED:$repo|$changelog&quot; &gt;&gt; &quot;$results&quot;</span>
+\ No newline at end of file
+diff --git a/group4ify b/group4ify
+new file mode 100755
+index 0000000..3674ca6
+--- /dev/null
++++ b/group4ify
+<span class="h">@@ -0,0 +1,15 @@</span>
+<span class="a">+#!/bin/bash</span>
+<span class="a">+TEMP=&quot;$(mktemp)&quot;</span>
+<span class="a">+[ -e &quot;$TEMP&quot; ] &amp;&amp; rm &quot;$TEMP&quot;</span>
+<span class="a">+mkdir &quot;$TEMP&quot;</span>
+<span class="a">+BASE=&quot;$(basename &quot;$1&quot;)&quot;</span>
+<span class="a">+cp &quot;$1&quot; &quot;$TEMP&quot;</span>
+<span class="a">+</span>
+<span class="a">+cd &quot;$TEMP&quot;</span>
+<span class="a">+pdfseparate &quot;$PWD&quot;/&quot;$1&quot; %d.pdf</span>
+<span class="a">+rm &quot;$BASE&quot;</span>
+<span class="a">+find . -name &#x27;*.pdf&#x27; -print0 | grep -z -v group4 | xargs -0 -L1 group4ify-aux</span>
+<span class="a">+cat &lt;(find . -name &#x27;*-group4.pdf&#x27; | sed &#x27;s:^./::&#x27; | sort -h | sed &#x27;s:^\|$:&quot;:g&#x27;) &lt;(echo &quot;$BASE&quot;-group4.pdf) | xargs pdfunite</span>
+<span class="a">+cd -</span>
+<span class="a">+mv &quot;$TEMP&quot;/&quot;$BASE&quot;-group4.pdf .</span>
+<span class="a">+rm -r &quot;$TEMP&quot;</span>
+diff --git a/group4ify-aux b/group4ify-aux
+new file mode 100755
+index 0000000..02daa1b
+--- /dev/null
++++ b/group4ify-aux
+<span class="h">@@ -0,0 +1,2 @@</span>
+<span class="a">+#!/bin/sh</span>
+<span class="a">+convert -density 300 -compress Group4 -type bilevel &quot;$1&quot; &quot;$1&quot;-group4.pdf</span>
+diff --git a/http-collect b/http-collect
+new file mode 100755
+index 0000000..6ca88e0
+--- /dev/null
++++ b/http-collect
+<span class="h">@@ -0,0 +1,193 @@</span>
+<span class="a">+#!/usr/bin/env python3</span>
+<span class="a">+</span>
+<span class="a">+</span>
+<span class="a">+from argparse import ArgumentParser</span>
+<span class="a">+from os import environ</span>
+<span class="a">+from os.path import isfile</span>
+<span class="a">+from random import randint</span>
+<span class="a">+from re import match</span>
+<span class="a">+from socket import socket, AF_INET, SOCK_STREAM</span>
+<span class="a">+from subprocess import check_output</span>
+<span class="a">+from sys import stderr</span>
+<span class="a">+from time import sleep</span>
+<span class="a">+from uuid import uuid4</span>
+<span class="a">+</span>
+<span class="a">+from flask import Flask, request</span>
+<span class="a">+from qrcode import QRCode</span>
+<span class="a">+</span>
+<span class="a">+</span>
+<span class="a">+app = Flask(__name__)</span>
+<span class="a">+secret = str(uuid4())</span>
+<span class="a">+</span>
+<span class="a">+</span>
+<span class="a">+def html(body):</span>
+<span class="a">+    out = &#x27;&#x27;&#x27;</span>
+<span class="a">+&lt;!DOCTYPE html&gt;</span>
+<span class="a">+&lt;html&gt;</span>
+<span class="a">+    &lt;head&gt;</span>
+<span class="a">+        &lt;link href=&quot;data:,&quot; rel=&quot;icon&quot;&gt;</span>
+<span class="a">+        &lt;meta charset=&quot;UTF-8&quot;&gt;</span>
+<span class="a">+        &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width,initial-scale=1.0&quot;&gt;</span>
+<span class="a">+        &lt;title&gt;http-collect&lt;/title&gt;</span>
+<span class="a">+        &lt;style&gt;</span>
+<span class="a">+body {</span>
+<span class="a">+    max-width: 400px;</span>
+<span class="a">+    margin: 0px auto;</span>
+<span class="a">+    padding: 3em;</span>
+<span class="a">+    font-family: sans-serif;</span>
+<span class="a">+}</span>
+<span class="a">+input, textarea {</span>
+<span class="a">+    width: 100%;</span>
+<span class="a">+    height: 8em;</span>
+<span class="a">+    margin: 0.5em 0px;</span>
+<span class="a">+}</span>
+<span class="a">+        &lt;/style&gt;</span>
+<span class="a">+    &lt;/head&gt;</span>
+<span class="a">+    &lt;body&gt;</span>
+<span class="a">+    &#x27;&#x27;&#x27;</span>
+<span class="a">+    out += body</span>
+<span class="a">+    out += &#x27;&#x27;&#x27;</span>
+<span class="a">+    &lt;/body&gt;</span>
+<span class="a">+&lt;/html&gt;</span>
+<span class="a">+    &#x27;&#x27;&#x27;</span>
+<span class="a">+</span>
+<span class="a">+    return out</span>
+<span class="a">+</span>
+<span class="a">+</span>
+<span class="a">+def sanitize_fs(s):</span>
+<span class="a">+    return s.strip().lstrip(&#x27;.&#x27;).replace(&#x27;/&#x27;, &#x27;_&#x27;)</span>
+<span class="a">+</span>
+<span class="a">+</span>
+<span class="a">+def sanitize_html(s):</span>
+<span class="a">+    return s.replace(&#x27;&amp;&#x27;, &#x27;&amp;amp&#x27;).replace(&#x27;&lt;&#x27;, &#x27;&amp;lt;&#x27;).replace(&#x27;&gt;&#x27;, &#x27;&amp;gt;&#x27;)</span>
+<span class="a">+</span>
+<span class="a">+</span>
+<span class="a">+def suffixify(basename, suff):</span>
+<span class="a">+    m = match(r&#x27;(.+)\.([^.]+)$&#x27;, basename)</span>
+<span class="a">+    if not m:</span>
+<span class="a">+        return f&#x27;{basename} ({suff:03d})&#x27;</span>
+<span class="a">+</span>
+<span class="a">+    return f&#x27;{m.group(1)} ({suff:03d}).{m.group(2)}&#x27;</span>
+<span class="a">+</span>
+<span class="a">+</span>
+<span class="a">+def get_final_name(original_name):</span>
+<span class="a">+    candidate = base_name = sanitize_fs(original_name)</span>
+<span class="a">+    suff = 1</span>
+<span class="a">+    while isfile(candidate):</span>
+<span class="a">+        candidate = suffixify(base_name, suff)</span>
+<span class="a">+        suff += 1</span>
+<span class="a">+    return candidate</span>
+<span class="a">+</span>
+<span class="a">+</span>
+<span class="a">+def determine_host_and_port(args):</span>
+<span class="a">+    host = None</span>
+<span class="a">+    port = None</span>
+<span class="a">+</span>
+<span class="a">+    if args.host:</span>
+<span class="a">+        host = args.host</span>
+<span class="a">+    if args.port:</span>
+<span class="a">+        port = args.port</span>
+<span class="a">+</span>
+<span class="a">+    if host is None:</span>
+<span class="a">+        rt = check_output([&#x27;ip&#x27;, &#x27;-4&#x27;, &#x27;route&#x27;]).decode(&#x27;UTF-8&#x27;)</span>
+<span class="a">+        for line in rt.splitlines():</span>
+<span class="a">+            m = match(r&#x27;default.* src ([0-9.]+)&#x27;, line)</span>
+<span class="a">+            if m:</span>
+<span class="a">+                host = m.group(1)</span>
+<span class="a">+</span>
+<span class="a">+        if host is None:</span>
+<span class="a">+            print(&#x27;Could not find my IPv4, using 0.0.0.0&#x27;, file=stderr)</span>
+<span class="a">+            host = &#x27;0.0.0.0&#x27;</span>
+<span class="a">+</span>
+<span class="a">+    success = False</span>
+<span class="a">+    for _ in range(5):</span>
+<span class="a">+        if port is None:</span>
+<span class="a">+            try_port = randint(1025, 65535)</span>
+<span class="a">+        else:</span>
+<span class="a">+            try_port = port</span>
+<span class="a">+</span>
+<span class="a">+        try:</span>
+<span class="a">+            s = socket(AF_INET, SOCK_STREAM)</span>
+<span class="a">+            s.bind((host, try_port))</span>
+<span class="a">+            s.listen()</span>
+<span class="a">+        except Exception as e:</span>
+<span class="a">+            print(f&#x27;Failed to use {host}:{try_port}: {repr(e)}&#x27;)</span>
+<span class="a">+            sleep(1)</span>
+<span class="a">+        else:</span>
+<span class="a">+            port = try_port</span>
+<span class="a">+            success = True</span>
+<span class="a">+            break</span>
+<span class="a">+        finally:</span>
+<span class="a">+            s.close()</span>
+<span class="a">+</span>
+<span class="a">+    if not success:</span>
+<span class="a">+        raise Exception(&#x27;Could not find free port&#x27;)</span>
+<span class="a">+</span>
+<span class="a">+    url = f&#x27;http://{host}:{port}/{secret}&#x27;</span>
+<span class="a">+</span>
+<span class="a">+    qr = QRCode()</span>
+<span class="a">+    qr.add_data(f&#x27;http://{host}:{port}/{secret}&#x27;)</span>
+<span class="a">+    qr.print_ascii(tty=True)</span>
+<span class="a">+</span>
+<span class="a">+    print()</span>
+<span class="a">+    print(url)</span>
+<span class="a">+    print()</span>
+<span class="a">+</span>
+<span class="a">+    return host, port</span>
+<span class="a">+</span>
+<span class="a">+</span>
+<span class="a">+@app.route(&#x27;/&lt;secret_from_url&gt;&#x27;, method
[diff truncated]