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> · <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'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'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 "Coleslaw" 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'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>"Check out my awesome website dude."</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' 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'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'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 "Coleslaw" 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'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>"Check out my awesome website dude."</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' 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> · <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 <13551856+equwal@users.noreply.github.com> +2026-10-04 23:02:59 -0700 + +http-collect: fix &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('.').replace('/', '_')</span> +<span class="a">+ return s.strip().lstrip('.').replace('/', '_') or 'unnamed'</span> + + + def sanitize_html(s): +<span class="d">- return s.replace('&', '&amp').replace('<', '&lt;').replace('>', '&gt;')</span> +<span class="a">+ return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')</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(['ip', '-4', 'route']).decode('UTF-8')</span> +<span class="a">+ try:</span> +<span class="a">+ rt = check_output(['ip', '-4', 'route']).decode('UTF-8')</span> +<span class="a">+ except OSError:</span> +<span class="a">+ rt = ''</span> + for line in rt.splitlines(): + m = match(r'default.* src ([0-9.]+)', 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'Failed to use {host}:{try_port}: {repr(e)}') + 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('Could not find free port') +<span class="h">@@ -127,7 +129,7 @@ def determine_host_and_port(args):</span> + + qr = QRCode() + qr.add_data(f'http://{host}:{port}/{secret}') +<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('textarea'): + final_name = get_final_name('plain.txt') +<span class="d">- with open(final_name, 'w') as fp:</span> +<span class="a">+ with open(final_name, 'w', encoding='UTF-8') as fp:</span> + fp.write(request.form.get('textarea')) + saved.append(f'<li>{sanitize_html(final_name)}</li>') + </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> · <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 <13551856+equwal@users.noreply.github.com> +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="$(mktemp)"</span> +<span class="d">-[ -e "$TEMP" ] && rm "$TEMP"</span> +<span class="d">-mkdir "$TEMP"</span> +<span class="d">-BASE="$(basename "$1")"</span> +<span class="d">-cp "$1" "$TEMP"</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="$(mktemp -d)"</span> +<span class="a">+trap 'rm -r "$TEMP"' EXIT</span> +<span class="a">+OUT="$PWD/$(basename "$1")-jpeg.pdf"</span> +<span class="a">+pdfseparate "$1" "$TEMP"/%d.pdf</span> + + cd "$TEMP" +<span class="d">-pdfseparate "$PWD"/"$1" %d.pdf</span> +<span class="d">-rm "$BASE"</span> +<span class="d">-find . -name '*.pdf' -print0 | grep -z -v jpeg | xargs -0 -L1 jpegify-aux</span> +<span class="d">-cat <(find . -name '*-jpeg.pdf' | sed 's:^./::' | sort -h | sed 's:^\|$:":g') <(echo "$BASE"-jpeg.pdf) | xargs pdfunite</span> +<span class="d">-cd -</span> +<span class="d">-mv "$TEMP"/"$BASE"-jpeg.pdf .</span> +<span class="d">-rm -r "$TEMP"</span> +<span class="a">+for p in *.pdf; do jpegify-aux "$p"; done</span> +<span class="a">+# shellcheck disable=SC2046 # page files are N.pdf-jpeg.pdf: no spaces</span> +<span class="a">+pdfunite $(printf '%s\n' *-jpeg.pdf | sort -n) "$OUT"</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 -> file.pdf-jpeg.pdf</span> + convert -density 100 -compress JPEG -quality 70 "$1" "$1"-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> · <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 <13551856+equwal@users.noreply.github.com> +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'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('~/.slock-usb-key'), 'r') as fp:</span> +<span class="d">- serial = fp.read().strip()</span> +<span class="a">+try:</span> +<span class="a">+ with open(expanduser('~/.slock-usb-key'), 'r') 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'slock-usb-key: {e}: put the key ID_SERIAL there')</span> + + context = pyudev.Context() + monitor = pyudev.Monitor.from_netlink(context) +<span class="d">-monitor.filter_by('block')</span> +<span class="a">+monitor.filter_by('block', 'disk')</span> + for device in iter(monitor.poll, None): + if device.get('ID_SERIAL') == serial: + if device.action == 'add': +<span class="d">- Popen(['sudo', '/usr/bin/killall', 'slock'])</span> +<span class="a">+ Popen(['sudo', 'killall', 'slock'])</span> + elif device.action == 'remove': + Popen(['slock'])</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> · <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 <13551856+equwal@users.noreply.github.com> +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> · <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 <13551856+equwal@users.noreply.github.com> +2026-10-04 23:02:26 -0700 + +Remove bm, sbm, mpdmenu, windervolage: not this repo's code (equwal/sbm copies, Chris Down's mpdmenu, markus schnalke'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<tab>description<tab>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 '\t')</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='</span> +<span class="d">-function norm(u, host, rest, i) {</span> +<span class="d">- sub(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//, "", u)</span> +<span class="d">- i = index(u, "/")</span> +<span class="d">- if (i) { host = substr(u, 1, i - 1); rest = substr(u, i) }</span> +<span class="d">- else { host = u; rest = "" }</span> +<span class="d">- host = tolower(host)</span> +<span class="d">- sub(/^www\./, "", host)</span> +<span class="d">- sub(/\/+$/, "", rest)</span> +<span class="d">- return host rest</span> +<span class="d">-}'</span> +<span class="d">-</span> +<span class="d">-die () {</span> +<span class="d">- printf 'bm: %s\n' "$*" >&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 "$1" >/dev/null 2>&1</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-usage () {</span> +<span class="d">- cat <<'EOF'</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's URL to the clipboard</span> +<span class="d">- --print print a bookmark'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 <tag> only consider bookmarks with this tag</span> +<span class="d">- -S, --sort <o> 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 "$SBM_MENU" ] && return</span> +<span class="d">- if [ -n "$DISPLAY$WAYLAND_DISPLAY" ] && ! { [ -t 0 ] && [ -t 2 ]; }; then</span> +<span class="d">- order='dmenu fzf'</span> +<span class="d">- else</span> +<span class="d">- order='fzf dmenu'</span> +<span class="d">- fi</span> +<span class="d">- for menu in $order; do</span> +<span class="d">- if have "$menu"; 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 'no menu program found: install dmenu or fzf, or set $SBM_MENU'</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 "$@"</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 "$@"</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-# fzfpick <fzf options>: 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 "$@")</span> +<span class="d">- chosen=$(printf '%s\n' "$out" | sed -n 2p)</span> +<span class="d">- printf '%s\n' "${chosen:-$(printf '%s\n' "$out" | sed -n 1p)}"</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-# pick <prompt>: 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="$1 " ;;</span> +<span class="d">- dmenu) dmenu -i -l "$DMENULINES" -p "$1" ;;</span> +<span class="d">- *) custommenu pick "$1" ;;</span> +<span class="d">- esac</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-# pickmulti <prompt>: 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 '%s\n' "$choices" | fzfcmd --multi --print-query \</span> +<span class="d">- --prompt="$1 " --header='TAB marks, ENTER confirms. No match: ENTER creates the typed tag, ESC finishes.')</span> +<span class="d">- chosen=$(printf '%s\n' "$out" | sed 1d)</span> +<span class="d">- if [ -n "$chosen" ]; then</span> +<span class="d">- printf '%s\n' "$chosen"</span> +<span class="d">- break</span> +<span class="d">- fi</span> +<span class="d">- typed=$(printf '%s\n' "$out" | sed -n 1p)</span> +<span class="d">- [ -n "$typed" ] || break</span> +<span class="d">- printf '%s\n' "$typed"</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 'done | (choose when done)\n%s\n' "$choices" \</span> +<span class="d">- | dmenu -i -l "$DMENULINES" -p "$1")</span> +<span class="d">- # An empty answer means the menu was closed.</span> +<span class="d">- [ -z "$one" ] || [ "${one%% *}" = 'done' ] && break</span> +<span class="d">- printf '%s\n' "$one"</span> +<span class="d">- done</span> +<span class="d">- ;;</span> +<span class="d">- *) printf '%s\n' "$choices" | custommenu multi "$1" ;;</span> +<span class="d">- esac</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-# ask <prompt> [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 '%s%s: ' "$1" "${2:+ [$2]}" >&2</span> +<span class="d">- IFS= read -r reply || reply=</span> +<span class="d">- printf '%s\n' "${reply:-$2}"</span> +<span class="d">- ;;</span> +<span class="d">- dmenu)</span> +<span class="d">- if [ -n "$2" ]; then printf '%s\n' "$2"; fi | dmenu -p "$1:"</span> +<span class="d">- ;;</span> +<span class="d">- *)</span> +<span class="d">- if [ -n "$2" ]; then printf '%s\n' "$2"; fi | custommenu ask "$1"</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 "$SBM_COPY" ]; then $SBM_COPY</span> +<span class="d">- elif [ -n "$WAYLAND_DISPLAY" ] && 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 'no clipboard program found: set $SBM_COPY'</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 "$SBM_PASTE" ]; then $SBM_PASTE</span> +<span class="d">- elif [ -n "$WAYLAND_DISPLAY" ] && 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>/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 "$SBM_OPEN" ]; then $SBM_OPEN "$1"</span> +<span class="d">- elif have xdg-open; then xdg-open "$1"</span> +<span class="d">- else die 'no opener found: set $SBM_OPEN'</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'\t' '</span> +<span class="d">- BEGIN {</span> +<span class="d">- while ((getline line < ENVIRON["SBM_USAGE"]) > 0) {</span> +<span class="d">- split(line, f, "\t")</span> +<span class="d">- used[f[1]] = f[2]</span> +<span class="d">- }</span> +<span class="d">- order = ENVIRON["SBM_ORDER"]</span> +<span class="d">- }</span> +<span class="d">- NF == 0 || /^#/ { next }</span> +<span class="d">- ENVIRON["SBM_TAG"] != "" {</span> +<span class="d">- hit = 0</span> +<span class="d">- n = split($3, tags, " ")</span> +<span class="d">- for (i = 1; i <= n; i++)</span> +<span class="d">- if (tags[i] == ENVIRON["SBM_TAG"]) hit = 1</span> +<span class="d">- if (!hit) next</span> +<span class="d">- }</span> +<span class="d">- {</span> +<span class="d">- if (order == "used") key = sprintf("%09d", 999999999 - used[$1])</span> +<span class="d">- else if (order == "recent") key = sprintf("%09d", 999999999 - NR)</span> +<span class="d">- else if (order == "desc") key = tolower($2)</span> +<span class="d">- else if (order == "tag") key = tolower($3)</span> +<span class="d">- else key = ""</span> +<span class="d">- printf "%s\t%s\t%s\t%s\t%s\t%d\n", key, tolower($1), $1, $2, $3, used[$1]</span> +<span class="d">- }' "$BOOKMARKS" | LC_ALL=C sort -t"$TAB" -k1,1 -k2,2 | cut -f3-</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-# details <url>: a bookmark in full, one field per line.</span> +<span class="d">-details () {</span> +<span class="d">- rows | SBM_URL=$1 awk -F'\t' '$1 == ENVIRON["SBM_URL"] {</span> +<span class="d">- printf "%s\n%s\ntags: %s\nused: %d\n", $1, $2, $3, $4</span> +<span class="d">- exit</span> +<span class="d">- }'</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-is_bookmark () {</span> +<span class="d">- SBM_URL=$1 awk -F'\t' '$1 == ENVIRON["SBM_URL"] { found = 1; exit }</span> +<span class="d">- END { exit !found }' "$BOOKMARKS"</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="printf '%s\n%s\ntags: %s\nused: %s\n\n' {1} {2} {3} {4}; sleep 0.3 2>/dev/null; bm-page {1} 2>/dev/null"</span> +<span class="d">-fzfrows () {</span> +<span class="d">- awk -F'\t' 'NF {</span> +<span class="d">- desc = $2</span> +<span class="d">- if (length(desc) > 44) desc = substr(desc, 1, 42) ".."</span> +<span class="d">- printf "%s\t%-44s %-18s %s\n", $0, desc, $3, $1</span> +<span class="d">- }'</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-# withpreview <fzf command and options>: for the output of fzfrows.</span> +<span class="d">-withpreview () {</span> +<span class="d">- "$@" --delimiter="$TAB" --with-nth=5.. \</span> +<span class="d">- --preview-window=right:50%:wrap --preview="$PREVIEW"</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-# choose <prompt>: a bookmark's URL, or whatever else the user typed.</span> +<span class="d">-choose () {</span> +<span class="d">- if [ "$SBM_MENU" = fzf ]; then</span> +<span class="d">- rows | fzfrows | withpreview fzfpick --prompt="$1 "</span> +<span class="d">- else</span> +<span class="d">- rows | cut -f1-3 | pick "$1"</span> +<span class="d">- fi | cut -f1</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-# choose_url <prompt>: like choose, but only a bookmark will do.</span> +<span class="d">-choose_url () {</span> +<span class="d">- if [ -z "$(rows)" ]; then</span> +<span class="d">- [ -n "$tagfilter" ] && die "no bookmarks tagged '$tagfilter'"</span> +<span class="d">- die 'no bookmarks yet; add one with: bm --add'</span> +<span class="d">- fi</span> +<span class="d">- url=$(choose "$1")</span> +<span class="d">- [ -n "$url" ] || exit 1</span> +<span class="d">- is_bookmark "$url" || die "not a bookmark: $url"</span> +<span class="d">- printf '%s\n' "$url"</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-# find_dup <url>: print the bookmark that is the same page as <url>, if any.</span> +<span class="d">-find_dup () {</span> +<span class="d">- SBM_URL=$1 awk -F'\t' "$AWK_NORM"'</span> +<span class="d">- BEGIN { want = norm(ENVIRON["SBM_URL"]) }</span> +<span class="d">- NF && !/^#/ && norm($1) == want { print; exit }' "$BOOKMARKS"</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-# tmpfile <file>: create an empty temporary file and print its name. Beside</span> +<span class="d">-# <file> 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 "$1.tmp.$$" "${TMPDIR:-/tmp}/sbm.$$.${1##*/}"; do</span> +<span class="d">- if (umask 077; set -C; : > "$candidate") 2>/dev/null; then</span> +<span class="d">- printf '%s\n' "$candidate"</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 <file>: replace <file> 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 "$1") || die "cannot create a temporary file for $1"</span> +<span class="d">- cat > "$tmp" && cat "$tmp" > "$1"</span> +<span class="d">- rc=$?</span> +<span class="d">- rm -f "$tmp"</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 "$WLOCK" 2>/dev/null; do</span> +<span class="d">- pid=$(cat "$WLOCK/pid" 2>/dev/null)</span> +<span class="d">- if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then</span> +<span class="d">- [ "$(cat "$WLOCK/pid" 2>/dev/null)" != "$pid" ] || rm -rf "$WLOCK"</span> +<span class="d">- continue</span> +<span class="d">- fi</span> +<span class="d">- [ "$waited" -lt "${SBM_LOCK_WAIT:-10}" ] \</span> +<span class="d">- || die "$BOOKMARKS is in use (bm -e or bm-sync). If neither runs, remove $WLOCK"</span> +<span class="d">- sleep 1</span> +<span class="d">- waited=$((waited + 1))</span> +<span class="d">- done</span> +<span class="d">- echo $$ > "$WLOCK/pid"</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 'exit 1' HUP INT TERM</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-unlock () {</span> +<span class="d">- [ -z "$locked" ] || rm -rf "$WLOCK"</span> +<span class="d">- locked=</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-# bump <url>: count a use, for the "used" 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 "date +%s" for a POSIX that has no %s.</span> +<span class="d">-bump () {</span> +<span class="d">- [ -e "$USAGE" ] || : > "$USAGE" || return 0</span> +<span class="d">- SBM_URL=$1 awk -F'\t' '</span> +<span class="d">- BEGIN { OFS = "\t"; srand(); now = srand() }</span> +<span class="d">- $1 == ENVIRON["SBM_URL"] { $2 = $2 + 1; $3 = now; seen = 1 }</span> +<span class="d">- { print }</span> +<span class="d">- END { if (!seen) print ENVIRON["SBM_URL"], 1, now }</span> +<span class="d">- ' "$USAGE" | rewrite "$USAGE"</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 "$USERTAGS" ] || : > "$USERTAGS"</span> +<span class="d">- while IFS= read -r name; do</span> +<span class="d">- [ -n "$name" ] || continue</span> +<span class="d">- sed 's/[ |].*//' "$USERTAGS" | grep -Fxq -- "$name" \</span> +<span class="d">- || printf '%s | \n' "$name" >> "$USERTAGS"</span> +<span class="d">- done</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-# changed <message>: 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 "$1"; fi</span> +<span class="d">- if have bm-sync; then bm-sync -q >/dev/null 2>&1 & 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 "$1"</span> +<span class="d">- printf '%s' "$1" | clip_out</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-open_url () {</span> +<span class="d">- bump "$1"</span> +<span class="d">- opener "$1"</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'\t' \</span> +<span class="d">- '$1 == ENVIRON["SBM_URL"] { print NR; exit }' "$BOOKMARKS")</span> +<span class="d">- ${VISUAL:-${EDITOR:-vi}} "+${line:-1}" "$BOOKMARKS"</span> +<span class="d">- unlock</span> +<span class="d">- changed 'bm: edit'</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 "$BOOKMARKS" "$USAGE"; do</span> +<span class="d">- [ -e "$file" ] || continue</span> +<span class="d">- SBM_URL=$1 awk -F'\t' '$1 != ENVIRON["SBM_URL"]' "$file" \</span> +<span class="d">- | rewrite "$file" || die "could not rewrite $file"</span> +<span class="d">- done</span> +<span class="d">- unlock</span> +<span class="d">- changed "bm: delete $1"</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-# urlencode <text>: for a query string. awk has no ord(), hence the table.</span> +<span class="d">-urlencode () {</span> +<span class="d">- printf '%s' "$1" | LC_ALL=C awk '</span> +<span class="d">- BEGIN { for (i = 1; i < 256; i++) ord[sprintf("%c", i)] = i }</span> +<span class="d">- {</span> +<span class="d">- for (i = 1; i <= 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 == " ") out = out "+"</span> +<span class="d">- else out = out sprintf("%%%02X", ord[c])</span> +<span class="d">- }</span> +<span class="d">- }</span> +<span class="d">- END { printf "%s", out }'</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-# go <text>: open text that is not a bookmark. One word with "://" 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 "keyword | url with %s" 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">- *' '*) ;;</span> +<span class="d">- *://*) opener "$1"; return ;;</span> +<span class="d">- *.*) opener "https://$1"; return ;;</span> +<span class="d">- esac</span> +<span class="d">- engine=$SEARCH</span> +<span class="d">- query=$1</span> +<span class="d">- if [ "${1%% *}" != "$1" ] && [ -r "$ENGINES" ]; then</span> +<span class="d">- found=$(SBM_KEY=${1%% *} awk -F' *[|] *' \</span> +<span class="d">- '$1 == ENVIRON["SBM_KEY"] { print $2; exit }' "$ENGINES")</span> +<span class="d">- if [ -n "$found" ]; 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 "$(printf '%s\n' "$engine" | sed "s|%s|$(urlencode "$query")|")"</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-plumb () {</span> +<span class="d">- url=$(choose 'open:')</span> +<span class="d">- [ -n "$url" ] || exit 1</span> +<span class="d">- if is_bookmark "$url"; then open_url "$url"; else go "$url"; fi</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-# Exit status 3 means "already bookmarked".</span> +<span class="d">-add () {</span> +<span class="d">- url=$1</span> +<span class="d">- if [ -z "$url" ]; then</span> +<span class="d">- url=$(ask 'url' "$(clip_in | grep '://' | head -n 1)")</span> +<span class="d">- fi</span> +<span class="d">- url=$(printf '%s' "$url" | tr -d '[:space:]')</span> +<span class="d">- [ -n "$url" ] || exit 1</span> +<span class="d">-</span> +<span class="d">- dup=$(find_dup "$url")</span> +<span class="d">- if [ -n "$dup" ]; then</span> +<span class="d">- printf 'bm: already bookmarked:\n%s\n' "$dup" >&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's title.</span> +<span class="d">- title=</span> +<span class="d">- if have bm-title; then title=$(bm-title "$url"); fi</span> +<span class="d">- desc=$(ask 'description' "$title" | tr '\t' ' ')</span> +<span class="d">-</span> +<span class="d">- [ -e "$USERTAGS" ] || : > "$USERTAGS"</span> +<span class="d">- names=$(pickmulti 'tags:' < "$USERTAGS" | sed 's/[ |].*//' | sed '/^$/d' \</span> +<span class="d">- | sort -u)</span> +<span class="d">- printf '%s\n' "$names" | learn_tags</span> +<span class="d">- tags=$(printf '%s\n' "$names" | paste -sd ' ' -)</span> +<span class="d">-</span> +<span class="d">- lock</span> +<span class="d">- printf '%s\t%s\t%s\n' "$url" "$desc" "$tags" >> "$BOOKMARKS"</span> +<span class="d">- unlock</span> +<span class="d">- changed "bm: add $url"</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 "$BOOKMARKS") || die 'cannot create a temporary file'</span> +<span class="d">- lock</span> +<span class="d">- SBM_FILE=$BOOKMARKS SBM_NEW=$new awk -F'\t' "$AWK_NORM"'</span> +<span class="d">- BEGIN {</span> +<span class="d">- while ((getline line < ENVIRON["SBM_FILE"]) > 0) {</span> +<span class="d">- split(line, f, "\t")</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 "%s\t%s\t%s\n", $1, $2, $3 > ENVIRON["SBM_NEW"]; added++ }</span> +<span class="d">- END { printf "added %d, skipped %d duplicates\n", added, skipped }'</span> +<span class="d">- cut -f3 "$new" | tr ' ' '\n' | sort -u | learn_tags</span> +<span class="d">- cat "$new" >> "$BOOKMARKS"</span> +<span class="d">- unlock</span> +<span class="d">- rm -f "$new"</span> +<span class="d">- changed 'bm: merge'</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 '' )</span> +<span class="d">- if [ $? -eq 3 ]; then</span> +<span class="d">- ask 'already bookmarked; press ENTER' >/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="ENTER open ^Y copy ^A add ^D delete ^E edit ^S sort [$sortorder]"</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="$header" --prompt='bookmark: ')</span> +<span class="d">- [ -n "$out" ] || exit 1</span> +<span class="d">- typed=$(printf '%s\n' "$out" | sed -n 1p)</span> +<span class="d">- key=$(printf '%s\n' "$out" | sed -n 2p)</span> +<span class="d">- url=$(printf '%s\n' "$out" | 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 "$url" ]; then</span> +<span class="d">- [ -n "$key" ] && continue</span> +<span class="d">- [ -n "$typed" ] || exit 1</span> +<span class="d">- go "$typed"</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 "$url"; exit 0 ;;</span> +<span class="d">- ctrl-e) edit_url "$url" ;;</span> +<span class="d">- ctrl-d)</span> +<span class="d">- case $(ask "delete $url? (y/N)") in</span> +<span class="d">- y|Y|yes) delete_url "$url" ;;</span> +<span class="d">- esac</span> +<span class="d">- ;;</span> +<span class="d">- *) open_url "$url"; 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 '[add]\n'; rows | cut -f1-3; } | pick 'bookmark:')</span> +<span class="d">- [ -n "$chosen" ] || exit 1</span> +<span class="d">- if [ "$chosen" = '[add]' ]; then</span> +<span class="d">- add_inside</span> +<span class="d">- continue</span> +<span class="d">- fi</span> +<span class="d">- url=$(printf '%s\n' "$chosen" | cut -f1)</span> +<span class="d">- if ! is_bookmark "$url"; then</span> +<span class="d">- go "$chosen"</span> +<span class="d">- exit 0</span> +<span class="d">- fi</span> +<span class="d">- action=$( { printf 'open\ncopy\nedit\ndelete\n'</span> +<span class="d">- details "$url" | sed 's/^/ /'; } | pick 'do:')</span> +<span class="d">- case $action in</span> +<span class="d">- open) open_url "$url"; exit 0 ;;</span> +<span class="d">- copy) copy_url "$url"; exit 0 ;;</span> +<span class="d">- edit) edit_url "$url" ;;</span> +<span class="d">- delete) delete_url "$url" ;;</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 "$action" ]; then</span> +<span class="d">- printf 'bm: only one action at a time\n' >&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 <flag> <count of remaining arguments> <value></span> +<span class="d">-needarg () {</span> +<span class="d">- if [ "$2" -lt 2 ] || [ -z "$3" ]; then</span> +<span class="d">- printf 'bm: %s needs an argument\n' "$1" >&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">- ''|-*) ;;</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 "$1" $# "${2-}"</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 "$1" $# "${2-}"</span> +<span class="d">- sortorder=$2</span> +<span class="d">- shift</span> +<span class="d">- ;;</span> +<span class="d">- *)</span> +<span class="d">- printf 'bm: unknown argument: %s\n' "$1" >&2</span> +<span class="d">- usage >&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 'bm: unknown sort order: %s\n' "$sortorder" >&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 [ "$action" = 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 "$(dirname "$BOOKMARKS")" || die "cannot create directory for $BOOKMARKS"</span> +<span class="d">-[ -e "$BOOKMARKS" ] || : > "$BOOKMARKS" || die "cannot create $BOOKMARKS"</span> +<span class="d">-</span> +<span class="d">-if awk '!/\t/ && !/^#/ && NF { found = 1; exit } END { exit !found }' "$BOOKMARKS"</span> +<span class="d">-then</span> +<span class="d">- printf 'bm: %s has lines in the old format; run: bm-migrate\n' "$BOOKMARKS" >&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">- '')</span> +<span class="d">- if [ "$SBM_MENU" = 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 'copy:') && copy_url "$url" ;;</span> +<span class="d">- print) url=$(choose_url 'print:') && bump "$url" && printf '%s\n' "$url" ;;</span> +<span class="d">- edit) url=$(choose_url 'edit:') && edit_url "$url" ;;</span> +<span class="d">- delete) url=$(choose_url 'delete:') && delete_url "$url" \</span> +<span class="d">- && printf 'deleted %s\n' "$url" ;;</span> +<span class="d">- add) add "$addurl" ;;</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='[ALL]'</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 "${dmenu_args[@]}"</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-d_album() {</span> +<span class="d">- local artist="$1"</span> +<span class="d">- local albums</span> +<span class="d">-</span> +<span class="d">- mapfile -t albums < <(mpc list album artist "$artist")</span> +<span class="d">- if (( ${#albums[@]} > 1 )) ; then</span> +<span class="d">- {</span> +<span class="d">- printf '%s\n' "$all_name"</span> +<span class="d">- printf '%s\n' "${albums[@]}" | sort -f</span> +<span class="d">- } | dmenu -p album "${dmenu_args[@]}"</span> +<span class="d">- else</span> +<span class="d">- # We only have one album, so just use that.</span> +<span class="d">- printf '%s\n' "${albums[0]}"</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="%position% %title%"</span> +<span class="d">- local extra_format="(%artist% - %album%)"</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 "$extra_format" | sort | uniq | wc -l)</span> +<span class="d">- (( num_extras == 1 )) || format+=" $extra_format"</span> +<span class="d">-</span> +<span class="d">- track=$(mpc playlist -f "$format" | dmenu -p track "${dmenu_args[@]}")</span> +<span class="d">- printf '%s' "${track%% *}"</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=( "${@:$i}" )</span> +<span class="d">- break</span> +<span class="d">- fi</span> +<span class="d">-</span> +<span class="d">- case "$arg" 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 "$mode" 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 "$artist")</span> +<span class="d">- [[ $album ]] || exit 2</span> +<span class="d">-</span> +<span class="d">- mpc clear</span> +<span class="d">- if [[ $album == "$all_name" ]]; then</span> +<span class="d">- mpc find artist "$artist" | sort | mpc add</span> +<span class="d">- else</span> +<span class="d">- mpc find artist "$artist" album "$album" | sort | mpc add</span> +<span class="d">- fi</span> +<span class="d">-</span> +<span class="d">- mpc play >/dev/null</span> +<span class="d">- ;;</span> +<span class="d">- playlist)</span> +<span class="d">- mpc play "$(d_playlist)" >/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 && git init</span> +<span class="d">-# printf '%s\n' '*.usage' '*.bak*' > .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">-[ "${SBM_GIT:-auto}" != 0 ] || exit 0</span> +<span class="d">-dir=$(cd "$(dirname "$BOOKMARKS")" 2>/dev/null && pwd -P) || exit 0</span> +<span class="d">-top=$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null) || exit 0</span> +<span class="d">-[ "${SBM_GIT:-auto}" = 1 ] || [ "$top" = "$dir" ] || exit 0</span> +<span class="d">-</span> +<span class="d">-set -- "${1:-bm: update}" "$BOOKMARKS"</span> +<span class="d">-if [ -e "$USERTAGS" ] \</span> +<span class="d">- && [ "$(cd "$(dirname "$USERTAGS")" 2>/dev/null && pwd -P)" = "$dir" ]</span> +<span class="d">-then</span> +<span class="d">- set -- "$@" "$USERTAGS"</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 "$dir" add -- "$@" 2>/dev/null || exit 0</span> +<span class="d">-git -C "$dir" diff --cached --quiet -- "$@" && exit 0</span> +<span class="d">-if ! git -C "$dir" commit -q -m "$message" -- "$@" >/dev/null 2>&1; then</span> +<span class="d">- printf 'bm-commit: git commit failed in %s\n' "$dir" >&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 'usage: bm-page <url>\n' >&2</span> +<span class="d">- exit 2</span> +<span class="d">-fi</span> +<span class="d">-[ "${SBM_FETCH:-1}" != 0 ] || exit 0</span> +<span class="d">-case $(printf '%s' "$1" | tr '[:upper:]' '[:lower:]') 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 "<". 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 'Mozilla/5.0' -- "$1" 2>/dev/null \</span> +<span class="d">- | dd bs=1024 count=512 2>/dev/null | LC_ALL=C awk '</span> +<span class="d">- function utf8(c) {</span> +<span class="d">- if (c == 160) return " "</span> +<span class="d">- if (c < 128) return sprintf("%c", c)</span> +<span class="d">- if (c < 2048) return sprintf("%c%c", 192 + int(c / 64), 128 + c % 64)</span> +<span class="d">- if (c < 65536)</span> +<span class="d">- return sprintf("%c%c%c", 224 + int(c / 4096), 128 + int(c / 64) % 64, 128 + c % 64)</span> +<span class="d">- if (c < 1114112)</span> +<span class="d">- return sprintf("%c%c%c%c", 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 ""</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 <= length(name); i++)</span> +<span class="d">- n = n * 16 + index("0123456789abcdef", 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 "&" name ";"</span> +<span class="d">- }</span> +<span class="d">- function decode(s, out, i, j, name) {</span> +<span class="d">- out = ""</span> +<span class="d">- while ((i = index(s, "&")) > 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, ";")</span> +<span class="d">- name = substr(s, 1, j - 1)</span> +<span class="d">- if (j > 1 && j <= 12 && 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 "&"</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 " " 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]+/, " ", s)</span> +<span class="d">- if (s == "" || s == " ") {</span> +<span class="d">- if (s == " " && !brk) printf " "</span> +<span class="d">- return</span> +<span class="d">- }</span> +<span class="d">- if (brk) {</span> +<span class="d">- printf "%s", brk == 2 ? "\n\n" : "\n"</span> +<span class="d">- brk = 0</span> +<span class="d">- sub(/^ /, "", s)</span> +<span class="d">- }</span> +<span class="d">- printf "%s", s</span> +<span class="d">- }</span> +<span class="d">- BEGIN {</span> +<span class="d">- RS = "\001"</span> +<span class="d">- named["amp"] = "&"; named["lt"] = "<"; named["gt"] = ">"; named["quot"] = "\""</span> +<span class="d">- named["apos"] = "\047"; named["nbsp"] = " "</span> +<span class="d">- named["ndash"] = utf8(8211); named["mdash"] = utf8(8212); named["hellip"] = utf8(8230)</span> +<span class="d">- named["lsquo"] = utf8(8216); named["rsquo"] = utf8(8217)</span> +<span class="d">- named["ldquo"] = utf8(8220); named["rdquo"] = utf8(8221)</span> +<span class="d">- named["laquo"] = utf8(171); named["raquo"] = utf8(187); named["middot"] = utf8(183)</span> +<span class="d">- named["bull"] = utf8(8226); named["copy"] = utf8(169); named["reg"] = utf8(174)</span> +<span class="d">- named["trade"] = utf8(8482); named["deg"] = utf8(176); named["times"] = utf8(215)</span> +<span class="d">- named["euro"] = utf8(8364)</span> +<span class="d">- # Tags that start a line, and tags that start a paragraph.</span> +<span class="d">- split("br div li tr dt dd header footer nav main section article aside " \</span> +<span class="d">- "figure figcaption form details summary address hr", b, " ")</span> +<span class="d">- for (i in b) line[b[i]] = 1</span> +<span class="d">- split("p h1 h2 h3 h4 h5 h6 ul ol dl table blockquote pre", b, " ")</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("script style noscript template svg iframe object", b, " ")</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) == "%PDF-") {</span> +<span class="d">- print "(a PDF document)"</span> +<span class="d">- exit</span> +<span class="d">- }</span> +<span class="d">- n = split(page, part, "<")</span> +<span class="d">- text(part[1])</span> +<span class="d">- for (i = 2; i <= n; i++) {</span> +<span class="d">- p = part[i]</span> +<span class="d">- if (comment || substr(p, 1, 3) == "!--") {</span> +<span class="d">- k = index(p, "-->")</span> +<span class="d">- comment = !k</span> +<span class="d">- if (k && skip == "") text(substr(p, k + 3))</span> +<span class="d">- continue</span> +<span class="d">- }</span> +<span class="d">- k = index(p, ">")</span> +<span class="d">- if (!k) {</span> +<span class="d">- # A "<" in the text, or the end of a page that dd cut off.</span> +<span class="d">- if (skip == "" && i < n) text("<" 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) == "/"</span> +<span class="d">- name = tag</span> +<span class="d">- sub(/^\//, "", name)</span> +<span class="d">- sub(/[^a-z0-9].*/, "", name)</span> +<span class="d">- rest = substr(p, k + 1)</span> +<span class="d">- if (skip != "") {</span> +<span class="d">- if (closing && name == skip) {</span> +<span class="d">- skip = ""</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 && !closing && substr(tag, length(tag)) != "/") {</span> +<span class="d">- skip = name</span> +<span class="d">- continue</span> +<span class="d">- }</span> +<span class="d">- if (name == "title" && !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]+/, " ", t)</span> +<span class="d">- printf "\n\001T%s\n", 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 == "main") {</span> +<span class="d">- # Marks for the second awk: the main part of the page.</span> +<span class="d">- printf "\n\001%s\n", closing ? ">" : "<"</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 == "td" || name == "th") {</span> +<span class="d">- text(" ")</span> +<span class="d">- }</span> +<span class="d">- text(rest)</span> +<span class="d">- }</span> +<span class="d">- print ""</span> +<span class="d">- }' | LC_ALL=C awk '</span> +<span class="d">- {</span> +<span class="d">- gsub(/\302\240/, " ")</span> +<span class="d">- gsub(/\302[\200-\237]/, "")</span> +<span class="d">- gsub(/[ \t\r]+/, " ")</span> +<span class="d">- sub(/^ /, "")</span> +<span class="d">- sub(/ $/, "")</span> +<span class="d">- }</span> +<span class="d">- /^\001T/ {</span> +<span class="d">- if (title == "") {</span> +<span class="d">- title = substr($0, 3)</span> +<span class="d">- sub(/^ /, "", title)</span> +<span class="d">- }</span> +<span class="d">- next</span> +<span class="d">- }</span> +<span class="d">- /^\001</ { if (!opened) { opened = 1; from = n + 1 }; next }</span> +<span class="d">- /^\001>/ { if (opened && !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 != "") {</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 <= to; i++) {</span> +<span class="d">- if (line[i] == "") {</span> +<span class="d">- blank = seen</span> +<span class="d">- continue</span> +<span class="d">- }</span> +<span class="d">- if (blank) print ""</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">- }' | tr -d '\000-\010\013-\037\177'</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 <<'EOF'</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 'bm-sync: %s\n' "$*" >&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 "$quiet" ] || printf 'bm-sync: %s\n' "$*"</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-# setting <name>: a value from the config file.</span> +<span class="d">-setting () {</span> +<span class="d">- sed -n "s/^$1=//p" "$CONFIG" 2>/dev/null | head -n 1</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-# private <file>: make <file> 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; : > "$1") && chmod 600 "$1"</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-cleanup () {</span> +<span class="d">- rm -rf "$work"</span> +<span class="d">- [ -z "$locked" ] || rm -rf "$LOCK"</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 "$WLOCK" 2>/dev/null; do</span> +<span class="d">- pid=$(cat "$WLOCK/pid" 2>/dev/null)</span> +<span class="d">- if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then</span> +<span class="d">- [ "$(cat "$WLOCK/pid" 2>/dev/null)" != "$pid" ] || rm -rf "$WLOCK"</span> +<span class="d">- continue</span> +<span class="d">- fi</span> +<span class="d">- [ "$waited" -lt "${SBM_LOCK_WAIT:-10}" ] \</span> +<span class="d">- || die "$BOOKMARKS is in use (bm -e?): sync again later"</span> +<span class="d">- sleep 1</span> +<span class="d">- waited=$((waited + 1))</span> +<span class="d">- done</span> +<span class="d">- echo $$ > "$WLOCK/pid"</span> +<span class="d">- wlocked=1</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-wunlock () {</span> +<span class="d">- [ -z "$wlocked" ] || rm -rf "$WLOCK"</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 'email: ' >&2</span> +<span class="d">- read -r email || exit 1</span> +<span class="d">- printf 'password: ' >&2</span> +<span class="d">- stty -echo 2>/dev/null</span> +<span class="d">- IFS= read -r password</span> +<span class="d">- rc=$?</span> +<span class="d">- stty echo 2>/dev/null</span> +<span class="d">- printf '\n' >&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 '%s' "$password" | curl -sS -o "$work/out" -w '%{http_code}' \</span> +<span class="d">- --data-urlencode "email=$email" --data-urlencode 'password@-' \</span> +<span class="d">- -- "$server/api/login") || die "cannot reach $server"</span> +<span class="d">- [ "$code" = 200 ] || die "$(cat "$work/out")"</span> +<span class="d">- token=$(tr -d '\r\n' < "$work/out")</span> +<span class="d">-</span> +<span class="d">- mkdir -p "$(dirname "$CONFIG")" || exit 1</span> +<span class="d">- private "$CONFIG" || exit 1</span> +<span class="d">- printf 'server=%s\ntoken=%s\n' "$server" "$token" > "$CONFIG"</span> +<span class="d">- rm -f "$STATE"</span> +<span class="d">- say "signed in to $server"</span> +<span class="d">- sync</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-logout () {</span> +<span class="d">- if [ -e "$CONFIG" ]; then</span> +<span class="d">- private "$work/auth"</span> +<span class="d">- printf 'Authorization: Bearer %s\n' "$(setting token)" > "$work/auth"</span> +<span class="d">- curl -sS -o /dev/null -X POST -H "@$work/auth" -- "$(setting server)/api/logout" \</span> +<span class="d">- 2>/dev/null</span> +<span class="d">- rm -f "$CONFIG" "$STATE"</span> +<span class="d">- fi</span> +<span class="d">- say 'signed out'</span> +<span class="d">-}</span> +<span class="d">-</span> +<span class="d">-status () {</span> +<span class="d">- if [ ! -e "$CONFIG" ]; then</span> +<span class="d">- echo 'not signed in: run bm-sync login'</span> +<span class="d">- return</span> +<span class="d">- fi</span> +<span class="d">- echo "server: $(setting server)"</span> +<span class="d">- if [ -e "$STATE" ]; then</span> +<span class="d">- echo "last sync: $(ls -l "$STATE" | awk '{ print $6, $7, $8 }')"</span> +<span class="d">- else</span> +<span class="d">- echo 'last sync: never'</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 "$CONFIG" ] || die 'not signed in: run bm-sync login'</span> +<span class="d">- server=$(setting server)</span> +<span class="d">- token=$(setting token)</span> +<span class="d">- [ -n "$server" ] && [ -n "$token" ] || die "$CONFIG is damaged: run bm-sync login"</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 "$LOCK" 2>/dev/null; then</span> +<span class="d">- pid=$(cat "$LOCK/pid" 2>/dev/null)</span> +<span class="d">- if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then</span> +<span class="d">- : > "$LOCK/again"</span> +<span class="d">- exit 0</span> +<span class="d">- fi</span> +<span class="d">- rm -rf "$LOCK" # left by a bm-sync that was killed</span> +<span class="d">- mkdir "$LOCK" 2>/dev/null || die "cannot lock $LOCK"</span> +<span class="d">- fi</span> +<span class="d">- locked=1</span> +<span class="d">- echo $$ > "$LOCK/pid"</span> +<span class="d">-</span> +<span class="d">- private "$work/auth"</span> +<span class="d">- printf 'Authorization: Bearer %s\n' "$token" > "$work/auth"</span> +<span class="d">- if [ ! -e "$BOOKMARKS" ]; then</span> +<span class="d">- mkdir -p "$(dirname "$BOOKMARKS")" && : > "$BOOKMARKS" || 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 "$LOCK/again"</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 "$BOOKMARKS" > "$work/sent"</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 "$work/sent" ] || rm -f "$STATE"</span> +<span class="d">- base=$(cat "$STATE" 2>/dev/null)</span> +<span class="d">- code=$(curl -sS --max-time 60 -o "$work/out" -D "$work/head" -w '%{http_code}' \</span> +<span class="d">- -H "@$work/auth" -H 'Content-Type: text/plain; charset=utf-8' \</span> +<span class="d">- --data-binary "@$work/sent" -- "$server/api/sync?base=$base") \</span> +<span class="d">- || die "cannot reach $server"</span> +<span class="d">- case $code in</span> +<span class="d">- 200) ;;</span> +<span class="d">- 401) die 'not signed in: run bm-sync login' ;;</span> +<span class="d">- *) die "$(cat "$work/out")" ;;</span> +<span class="d">- esac</span> +<span class="d">- version=$(tr -d '\r' < "$work/head" | sed -n 's/^[Ss][Bb][Mm]-[Vv][Ee][Rr][Ss][Ii][Oo][Nn]: *//p')</span> +<span class="d">- [ -n "$version" ] || die 'the server gave no version'</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 "$work/sent" "$BOOKMARKS"; then</span> +<span class="d">- wunlock</span> +<span class="d">- tries=$((tries + 1))</span> +<span class="d">- [ $tries -lt 5 ] || die 'the file changes all the time: try again later'</span> +<span class="d">- continue</span> +<span class="d">- fi</span> +<span class="d">- wrote=</span> +<span class="d">- if ! cmp -s "$work/out" "$BOOKMARKS"; 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 "$STATE"</span> +<span class="d">- # Through cat, not mv, as in bm: a symlinked file stays a symlink.</span> +<span class="d">- cat "$work/out" > "$BOOKMARKS" || die "cannot write $BOOKMARKS"</span> +<span class="d">- wrote=1</span> +<span class="d">- fi</span> +<span class="d">- printf '%s\n' "$version" > "$STATE"</span> +<span class="d">- wunlock</span> +<span class="d">- if [ -n "$wrote" ] && command -v bm-commit >/dev/null 2>&1; then</span> +<span class="d">- bm-commit 'bm-sync: merge'</span> +<span class="d">- fi</span> +<span class="d">- [ -e "$LOCK/again" ] || break</span> +<span class="d">- done</span> +<span class="d">- say "in sync: $(grep -c -v -e '^#' -e '^[[:space:]]*$' "$BOOKMARKS") bookmarks"</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 >&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 >/dev/null 2>&1 || die 'curl is not installed'</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 "$work" || die "cannot create $work"</span> +<span class="d">-locked=</span> +<span class="d">-wlocked=</span> +<span class="d">-trap cleanup EXIT</span> +<span class="d">-trap 'exit 1' INT TERM</span> +<span class="d">-</span> +<span class="d">-case ${1:-sync} in</span> +<span class="d">- login) shift; login "$@" ;;</span> +<span class="d">- logout) logout ;;</span> +<span class="d">- status) status ;;</span> +<span class="d">- sync) sync ;;</span> +<span class="d">- *) usage >&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 <title> of a web page, or nothing.</span> +<span class="d">-# When it is installed, "bm --add" 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 'usage: bm-title <url>\n' >&2</span> +<span class="d">- exit 2</span> +<span class="d">-fi</span> +<span class="d">-[ "${SBM_FETCH:-1}" != 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 'Mozilla/5.0' -- "$1" 2>/dev/null \</span> +<span class="d">- | dd bs=1024 count=64 2>/dev/null | LC_ALL=C awk '</span> +<span class="d">- function title(s, start, end) {</span> +<span class="d">- start = index(tolower(s), "<title")</span> +<span class="d">- if (!start) return</span> +<span class="d">- s = substr(s, start)</span> +<span class="d">- s = substr(s, index(s, ">") + 1)</span> +<span class="d">- end = index(tolower(s), "</title")</span> +<span class="d">- if (end) s = substr(s, 1, end - 1)</span> +<span class="d">- gsub(/&lt;/, "<", s); gsub(/&gt;/, ">", s); gsub(/&quot;/, "\"", s)</span> +<span class="d">- gsub(/&#39;|&#x27;|&apos;/, "\047", s); gsub(/&nbsp;/, " ", s)</span> +<span class="d">- gsub(/&amp;/, "\\&", s)</span> +<span class="d">- gsub(/[ \t\r]+/, " ", s); sub(/^ /, "", s); sub(/ $/, "", s)</span> +<span class="d">- print s</span> +<span class="d">- }</span> +<span class="d">- { page = page " " $0 }</span> +<span class="d">- index(tolower($0), "</title") || NR >= 500 { exit }</span> +<span class="d">- END { title(page) }'</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 <meillo@marmaro.de></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 "$queue" | awk "/^$1\t/ {sub(/ $queue .*/,\"\");print}" </span> +<span class="d">- at -c "$1" | sed '</span> +<span class="d">- 1,/^# wv/d;</span> +<span class="d">- /^mail /{s,[^"]*",,; s,".*,,};</span> +<span class="d">- s,^,\t,</span> +<span class="d">- '</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 "$queue" | while read id rest ; do</span> +<span class="d">- listone "$id"</span> +<span class="d">- done</span> +<span class="d">- else</span> +<span class="d">- for i in "$@" ; do</span> +<span class="d">- listone "$i"</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 >&2 <<!</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 [ "$1" = "-l" ] ; then</span> +<span class="d">- shift</span> +<span class="d">- list "$@"</span> +<span class="d">- exit</span> +<span class="d">-fi</span> +<span class="d">-</span> +<span class="d">-echo At: `date +"%Y-%m-%d %H:%M" -d "$*"`</span> +<span class="d">-printf "Subject: "</span> +<span class="d">-read sub</span> +<span class="d">-</span> +<span class="d">-at -q "$queue" "$@" <<!</span> +<span class="d">-# wv</span> +<span class="d">-mail -s "Wv: $sub" `id -un` <<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> · <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 <13551856+equwal@users.noreply.github.com> +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:-"."} + results=$(mktemp) +<span class="d">-log=$(mktemp)</span> +<span class="d">-trap "tsp -K; rm -f "$results" "$log"" EXIT</span> +<span class="d">-echo "Updating repos in: "$dir""</span> +<span class="d">-tsp -S 8</span> +<span class="d">-find ""$dir"" -name ".git" -type d 2>/dev/null | while read git_dir; do</span> +<span class="d">- tsp -n git-update-worker.sh "$(dirname ""$git_dir"")" ""$results"" ""$log""</span> +<span class="d">-done</span> +<span class="d">-tail -f ""$log"" &</span> +<span class="d">-tail_pid=$!</span> +<span class="d">-tsp -w</span> +<span class="d">-kill "$tail_pid" 2>/dev/null</span> +<span class="d">-conflicts=$(grep -c "^REBASE_CONFLICT:" ""$results"" 2>/dev/null || echo 0)</span> +<span class="a">+trap 'rm -f "$results"' EXIT</span> +<span class="a">+echo "Updating repos in: $dir"</span> +<span class="a">+find "$dir" -name .git -type d -prune -print0 2>/dev/null |</span> +<span class="a">+ xargs -0 -P 8 -I{} "$(dirname "$0")/git-update-worker.sh" {} "$results"</span> +<span class="a">+conflicts=$(grep -c "^REBASE_CONFLICT:" "$results")</span> + echo "CHANGES:" +<span class="d">-grep "^UPDATED:" ""$results"" | while IFS='|' read prefix repo changelog; do</span> +<span class="d">- repo_name=$(echo ""$repo"" | sed "s|^"$dir"/||" | sed 's|^./||')</span> +<span class="d">- echo "\033[32m"$repo_name":\033[0m"</span> +<span class="d">- echo ""$changelog"" | tr ';' '\n' | sed 's/^/ /' | sed 's/^ $//'</span> +<span class="a">+sed -n 's/^UPDATED://p' "$results" | while IFS='|' read -r repo changelog; do</span> +<span class="a">+ repo_name=${repo#"$dir"/}</span> +<span class="a">+ printf '\033[32m%s:\033[0m\n' "$repo_name"</span> +<span class="a">+ printf '%s\n' "$changelog" | tr ';' '\n' | sed 's/^/ /' | sed 's/^ $//'</span> + done +<span class="d">-if grep -q "^REBASE_CONFLICT:" ""$results""; then</span> +<span class="d">- echo "\033[31mREBASE CONFLICTS:\033[0m"</span> +<span class="d">- grep "^REBASE_CONFLICT:" ""$results"" | while IFS='|' read prefix repo _; do</span> +<span class="d">- repo_name=$(echo ""$repo"" | sed "s|^"$dir"/||" | sed 's|^./||')</span> +<span class="d">- echo "\033[31m"$repo_name": requires manual resolution\033[0m"</span> +<span class="a">+if [ "$conflicts" -gt 0 ]; then</span> +<span class="a">+ printf '\033[31mREBASE CONFLICTS:\033[0m\n'</span> +<span class="a">+ sed -n 's/^REBASE_CONFLICT://p' "$results" | while IFS='|' read -r repo _; do</span> +<span class="a">+ repo_name=${repo#"$dir"/}</span> +<span class="a">+ printf '\033[31m%s: requires manual resolution\033[0m\n' "$repo_name"</span> + done +<span class="d">-fi</span> +<span class="d">-if [ ""$conflicts"" ] && [ ""$conflicts"" -gt 0 ] 2>/dev/null; then</span> +<span class="d">- notify-send "Git Rebase Conflicts" ""$conflicts" repos need manual resolution" -u critical -t 10000</span> +<span class="a">+ notify-send "Git Rebase Conflicts" "$conflicts repos need manual resolution" -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="$1"</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="$2" +<span class="d">-log="$3"</span> + name=$(basename "$repo") + old=$(cd "$repo" && git rev-parse HEAD 2>/dev/null) + if [ -f "$repo/.git/FETCH_HEAD" ]; then + if [ "$(find "$repo/.git/FETCH_HEAD" -mmin -5 2>/dev/null)" ]; then +<span class="d">- echo "[$name] skipped" >> "$log"</span> +<span class="a">+ echo "[$name] skipped"</span> + exit + fi + fi +<span class="h">@@ -15,14 +15,14 @@ if (cd "$repo" && git pull --rebase >/dev/null 2>&1); then</span> + : + elif (cd "$repo" && git rebase --abort >/dev/null 2>&1); then + echo "REBASE_CONFLICT:$repo|" >> "$results" +<span class="d">- echo "[$name] REBASE CONFLICT" >> "$log"</span> +<span class="a">+ echo "[$name] REBASE CONFLICT"</span> + exit + else +<span class="d">- echo "[$name] FAILED" >> "$log"</span> +<span class="a">+ echo "[$name] FAILED"</span> + exit + fi + new=$(cd "$repo" && git rev-parse HEAD 2>/dev/null) +<span class="d">-[ "$old" = "$new" ] && { echo "[$name] up to date" >> "$log"; exit; }</span> +<span class="d">-echo "[$name] UPDATED" >> "$log"</span> +<span class="a">+[ "$old" = "$new" ] && { echo "[$name] up to date"; exit; }</span> +<span class="a">+echo "[$name] UPDATED"</span> + changelog=$(cd "$repo" && git log --oneline --no-merges -3 "$old..$new" 2>/dev/null | tr '\n' ';') + echo "UPDATED:$repo|$changelog" >> "$results" +\ 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> · <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 <13551856+equwal@users.noreply.github.com> +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<tab>description<tab>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 '\t')</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='</span> +<span class="a">+function norm(u, host, rest, i) {</span> +<span class="a">+ sub(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//, "", u)</span> +<span class="a">+ i = index(u, "/")</span> +<span class="a">+ if (i) { host = substr(u, 1, i - 1); rest = substr(u, i) }</span> +<span class="a">+ else { host = u; rest = "" }</span> +<span class="a">+ host = tolower(host)</span> +<span class="a">+ sub(/^www\./, "", host)</span> +<span class="a">+ sub(/\/+$/, "", rest)</span> +<span class="a">+ return host rest</span> +<span class="a">+}'</span> +<span class="a">+</span> +<span class="a">+die () {</span> +<span class="a">+ printf 'bm: %s\n' "$*" >&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 "$1" >/dev/null 2>&1</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+usage () {</span> +<span class="a">+ cat <<'EOF'</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's URL to the clipboard</span> +<span class="a">+ --print print a bookmark'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 <tag> only consider bookmarks with this tag</span> +<span class="a">+ -S, --sort <o> 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 "$SBM_MENU" ] && return</span> +<span class="a">+ if [ -n "$DISPLAY$WAYLAND_DISPLAY" ] && ! { [ -t 0 ] && [ -t 2 ]; }; then</span> +<span class="a">+ order='dmenu fzf'</span> +<span class="a">+ else</span> +<span class="a">+ order='fzf dmenu'</span> +<span class="a">+ fi</span> +<span class="a">+ for menu in $order; do</span> +<span class="a">+ if have "$menu"; 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 'no menu program found: install dmenu or fzf, or set $SBM_MENU'</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 "$@"</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 "$@"</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+# fzfpick <fzf options>: 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 "$@")</span> +<span class="a">+ chosen=$(printf '%s\n' "$out" | sed -n 2p)</span> +<span class="a">+ printf '%s\n' "${chosen:-$(printf '%s\n' "$out" | sed -n 1p)}"</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+# pick <prompt>: 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="$1 " ;;</span> +<span class="a">+ dmenu) dmenu -i -l "$DMENULINES" -p "$1" ;;</span> +<span class="a">+ *) custommenu pick "$1" ;;</span> +<span class="a">+ esac</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+# pickmulti <prompt>: 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 '%s\n' "$choices" | fzfcmd --multi --print-query \</span> +<span class="a">+ --prompt="$1 " --header='TAB marks, ENTER confirms. No match: ENTER creates the typed tag, ESC finishes.')</span> +<span class="a">+ chosen=$(printf '%s\n' "$out" | sed 1d)</span> +<span class="a">+ if [ -n "$chosen" ]; then</span> +<span class="a">+ printf '%s\n' "$chosen"</span> +<span class="a">+ break</span> +<span class="a">+ fi</span> +<span class="a">+ typed=$(printf '%s\n' "$out" | sed -n 1p)</span> +<span class="a">+ [ -n "$typed" ] || break</span> +<span class="a">+ printf '%s\n' "$typed"</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 'done | (choose when done)\n%s\n' "$choices" \</span> +<span class="a">+ | dmenu -i -l "$DMENULINES" -p "$1")</span> +<span class="a">+ # An empty answer means the menu was closed.</span> +<span class="a">+ [ -z "$one" ] || [ "${one%% *}" = 'done' ] && break</span> +<span class="a">+ printf '%s\n' "$one"</span> +<span class="a">+ done</span> +<span class="a">+ ;;</span> +<span class="a">+ *) printf '%s\n' "$choices" | custommenu multi "$1" ;;</span> +<span class="a">+ esac</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+# ask <prompt> [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 '%s%s: ' "$1" "${2:+ [$2]}" >&2</span> +<span class="a">+ IFS= read -r reply || reply=</span> +<span class="a">+ printf '%s\n' "${reply:-$2}"</span> +<span class="a">+ ;;</span> +<span class="a">+ dmenu)</span> +<span class="a">+ if [ -n "$2" ]; then printf '%s\n' "$2"; fi | dmenu -p "$1:"</span> +<span class="a">+ ;;</span> +<span class="a">+ *)</span> +<span class="a">+ if [ -n "$2" ]; then printf '%s\n' "$2"; fi | custommenu ask "$1"</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 "$SBM_COPY" ]; then $SBM_COPY</span> +<span class="a">+ elif [ -n "$WAYLAND_DISPLAY" ] && 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 'no clipboard program found: set $SBM_COPY'</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 "$SBM_PASTE" ]; then $SBM_PASTE</span> +<span class="a">+ elif [ -n "$WAYLAND_DISPLAY" ] && 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>/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 "$SBM_OPEN" ]; then $SBM_OPEN "$1"</span> +<span class="a">+ elif have xdg-open; then xdg-open "$1"</span> +<span class="a">+ else die 'no opener found: set $SBM_OPEN'</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'\t' '</span> +<span class="a">+ BEGIN {</span> +<span class="a">+ while ((getline line < ENVIRON["SBM_USAGE"]) > 0) {</span> +<span class="a">+ split(line, f, "\t")</span> +<span class="a">+ used[f[1]] = f[2]</span> +<span class="a">+ }</span> +<span class="a">+ order = ENVIRON["SBM_ORDER"]</span> +<span class="a">+ }</span> +<span class="a">+ NF == 0 || /^#/ { next }</span> +<span class="a">+ ENVIRON["SBM_TAG"] != "" {</span> +<span class="a">+ hit = 0</span> +<span class="a">+ n = split($3, tags, " ")</span> +<span class="a">+ for (i = 1; i <= n; i++)</span> +<span class="a">+ if (tags[i] == ENVIRON["SBM_TAG"]) hit = 1</span> +<span class="a">+ if (!hit) next</span> +<span class="a">+ }</span> +<span class="a">+ {</span> +<span class="a">+ if (order == "used") key = sprintf("%09d", 999999999 - used[$1])</span> +<span class="a">+ else if (order == "recent") key = sprintf("%09d", 999999999 - NR)</span> +<span class="a">+ else if (order == "desc") key = tolower($2)</span> +<span class="a">+ else if (order == "tag") key = tolower($3)</span> +<span class="a">+ else key = ""</span> +<span class="a">+ printf "%s\t%s\t%s\t%s\t%s\t%d\n", key, tolower($1), $1, $2, $3, used[$1]</span> +<span class="a">+ }' "$BOOKMARKS" | LC_ALL=C sort -t"$TAB" -k1,1 -k2,2 | cut -f3-</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+# details <url>: a bookmark in full, one field per line.</span> +<span class="a">+details () {</span> +<span class="a">+ rows | SBM_URL=$1 awk -F'\t' '$1 == ENVIRON["SBM_URL"] {</span> +<span class="a">+ printf "%s\n%s\ntags: %s\nused: %d\n", $1, $2, $3, $4</span> +<span class="a">+ exit</span> +<span class="a">+ }'</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+is_bookmark () {</span> +<span class="a">+ SBM_URL=$1 awk -F'\t' '$1 == ENVIRON["SBM_URL"] { found = 1; exit }</span> +<span class="a">+ END { exit !found }' "$BOOKMARKS"</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="printf '%s\n%s\ntags: %s\nused: %s\n\n' {1} {2} {3} {4}; sleep 0.3 2>/dev/null; bm-page {1} 2>/dev/null"</span> +<span class="a">+fzfrows () {</span> +<span class="a">+ awk -F'\t' 'NF {</span> +<span class="a">+ desc = $2</span> +<span class="a">+ if (length(desc) > 44) desc = substr(desc, 1, 42) ".."</span> +<span class="a">+ printf "%s\t%-44s %-18s %s\n", $0, desc, $3, $1</span> +<span class="a">+ }'</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+# withpreview <fzf command and options>: for the output of fzfrows.</span> +<span class="a">+withpreview () {</span> +<span class="a">+ "$@" --delimiter="$TAB" --with-nth=5.. \</span> +<span class="a">+ --preview-window=right:50%:wrap --preview="$PREVIEW"</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+# choose <prompt>: a bookmark's URL, or whatever else the user typed.</span> +<span class="a">+choose () {</span> +<span class="a">+ if [ "$SBM_MENU" = fzf ]; then</span> +<span class="a">+ rows | fzfrows | withpreview fzfpick --prompt="$1 "</span> +<span class="a">+ else</span> +<span class="a">+ rows | cut -f1-3 | pick "$1"</span> +<span class="a">+ fi | cut -f1</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+# choose_url <prompt>: like choose, but only a bookmark will do.</span> +<span class="a">+choose_url () {</span> +<span class="a">+ if [ -z "$(rows)" ]; then</span> +<span class="a">+ [ -n "$tagfilter" ] && die "no bookmarks tagged '$tagfilter'"</span> +<span class="a">+ die 'no bookmarks yet; add one with: bm --add'</span> +<span class="a">+ fi</span> +<span class="a">+ url=$(choose "$1")</span> +<span class="a">+ [ -n "$url" ] || exit 1</span> +<span class="a">+ is_bookmark "$url" || die "not a bookmark: $url"</span> +<span class="a">+ printf '%s\n' "$url"</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+# find_dup <url>: print the bookmark that is the same page as <url>, if any.</span> +<span class="a">+find_dup () {</span> +<span class="a">+ SBM_URL=$1 awk -F'\t' "$AWK_NORM"'</span> +<span class="a">+ BEGIN { want = norm(ENVIRON["SBM_URL"]) }</span> +<span class="a">+ NF && !/^#/ && norm($1) == want { print; exit }' "$BOOKMARKS"</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+# tmpfile <file>: create an empty temporary file and print its name. Beside</span> +<span class="a">+# <file> 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 "$1.tmp.$$" "${TMPDIR:-/tmp}/sbm.$$.${1##*/}"; do</span> +<span class="a">+ if (umask 077; set -C; : > "$candidate") 2>/dev/null; then</span> +<span class="a">+ printf '%s\n' "$candidate"</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 <file>: replace <file> 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 "$1") || die "cannot create a temporary file for $1"</span> +<span class="a">+ cat > "$tmp" && cat "$tmp" > "$1"</span> +<span class="a">+ rc=$?</span> +<span class="a">+ rm -f "$tmp"</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 "$WLOCK" 2>/dev/null; do</span> +<span class="a">+ pid=$(cat "$WLOCK/pid" 2>/dev/null)</span> +<span class="a">+ if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then</span> +<span class="a">+ [ "$(cat "$WLOCK/pid" 2>/dev/null)" != "$pid" ] || rm -rf "$WLOCK"</span> +<span class="a">+ continue</span> +<span class="a">+ fi</span> +<span class="a">+ [ "$waited" -lt "${SBM_LOCK_WAIT:-10}" ] \</span> +<span class="a">+ || die "$BOOKMARKS is in use (bm -e or bm-sync). If neither runs, remove $WLOCK"</span> +<span class="a">+ sleep 1</span> +<span class="a">+ waited=$((waited + 1))</span> +<span class="a">+ done</span> +<span class="a">+ echo $$ > "$WLOCK/pid"</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 'exit 1' HUP INT TERM</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+unlock () {</span> +<span class="a">+ [ -z "$locked" ] || rm -rf "$WLOCK"</span> +<span class="a">+ locked=</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+# bump <url>: count a use, for the "used" 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 "date +%s" for a POSIX that has no %s.</span> +<span class="a">+bump () {</span> +<span class="a">+ [ -e "$USAGE" ] || : > "$USAGE" || return 0</span> +<span class="a">+ SBM_URL=$1 awk -F'\t' '</span> +<span class="a">+ BEGIN { OFS = "\t"; srand(); now = srand() }</span> +<span class="a">+ $1 == ENVIRON["SBM_URL"] { $2 = $2 + 1; $3 = now; seen = 1 }</span> +<span class="a">+ { print }</span> +<span class="a">+ END { if (!seen) print ENVIRON["SBM_URL"], 1, now }</span> +<span class="a">+ ' "$USAGE" | rewrite "$USAGE"</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 "$USERTAGS" ] || : > "$USERTAGS"</span> +<span class="a">+ while IFS= read -r name; do</span> +<span class="a">+ [ -n "$name" ] || continue</span> +<span class="a">+ sed 's/[ |].*//' "$USERTAGS" | grep -Fxq -- "$name" \</span> +<span class="a">+ || printf '%s | \n' "$name" >> "$USERTAGS"</span> +<span class="a">+ done</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+# changed <message>: 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 "$1"; fi</span> +<span class="a">+ if have bm-sync; then bm-sync -q >/dev/null 2>&1 & 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 "$1"</span> +<span class="a">+ printf '%s' "$1" | clip_out</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+open_url () {</span> +<span class="a">+ bump "$1"</span> +<span class="a">+ opener "$1"</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'\t' \</span> +<span class="a">+ '$1 == ENVIRON["SBM_URL"] { print NR; exit }' "$BOOKMARKS")</span> +<span class="a">+ ${VISUAL:-${EDITOR:-vi}} "+${line:-1}" "$BOOKMARKS"</span> +<span class="a">+ unlock</span> +<span class="a">+ changed 'bm: edit'</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 "$BOOKMARKS" "$USAGE"; do</span> +<span class="a">+ [ -e "$file" ] || continue</span> +<span class="a">+ SBM_URL=$1 awk -F'\t' '$1 != ENVIRON["SBM_URL"]' "$file" \</span> +<span class="a">+ | rewrite "$file" || die "could not rewrite $file"</span> +<span class="a">+ done</span> +<span class="a">+ unlock</span> +<span class="a">+ changed "bm: delete $1"</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+# urlencode <text>: for a query string. awk has no ord(), hence the table.</span> +<span class="a">+urlencode () {</span> +<span class="a">+ printf '%s' "$1" | LC_ALL=C awk '</span> +<span class="a">+ BEGIN { for (i = 1; i < 256; i++) ord[sprintf("%c", i)] = i }</span> +<span class="a">+ {</span> +<span class="a">+ for (i = 1; i <= 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 == " ") out = out "+"</span> +<span class="a">+ else out = out sprintf("%%%02X", ord[c])</span> +<span class="a">+ }</span> +<span class="a">+ }</span> +<span class="a">+ END { printf "%s", out }'</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+# go <text>: open text that is not a bookmark. One word with "://" 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 "keyword | url with %s" 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">+ *' '*) ;;</span> +<span class="a">+ *://*) opener "$1"; return ;;</span> +<span class="a">+ *.*) opener "https://$1"; return ;;</span> +<span class="a">+ esac</span> +<span class="a">+ engine=$SEARCH</span> +<span class="a">+ query=$1</span> +<span class="a">+ if [ "${1%% *}" != "$1" ] && [ -r "$ENGINES" ]; then</span> +<span class="a">+ found=$(SBM_KEY=${1%% *} awk -F' *[|] *' \</span> +<span class="a">+ '$1 == ENVIRON["SBM_KEY"] { print $2; exit }' "$ENGINES")</span> +<span class="a">+ if [ -n "$found" ]; 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 "$(printf '%s\n' "$engine" | sed "s|%s|$(urlencode "$query")|")"</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+plumb () {</span> +<span class="a">+ url=$(choose 'open:')</span> +<span class="a">+ [ -n "$url" ] || exit 1</span> +<span class="a">+ if is_bookmark "$url"; then open_url "$url"; else go "$url"; fi</span> +<span class="a">+}</span> +<span class="a">+</span> +<span class="a">+# Exit status 3 means "already bookmarked".</span> +<span class="a">+add () {</span> +<span class="a">+ url=$1</span> +<span class="a">+ if [ -z "$url" ]; then</span> +<span class="a">+ url=$(ask 'url' "$(clip_in | grep '://' | head -n 1)")</span> +<span class="a">+ fi</span> +<span class="a">+ url=$(printf '%s' "$url" | tr -d '[:space:]')</span> +<span class="a">+ [ -n "$url" ] || exit 1</span> +<span class="a">+</span> +<span class="a">+ dup=$(find_dup "$url")</span> +<span class="a">+ if [ -n "$dup" ]; then</span> +<span class="a">+ printf 'bm: already bookmarked:\n%s\n' "$dup" >&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's title.</span> +<span class="a">+ title=</span> +<span class="a">+ if have bm-title; then title=$(bm-title "$url"); fi</span> +<span class="a">+ desc=$(ask 'description' "$title" | tr '\t' ' ')</span> +<span class="a">+</span> +<span class="a">+ [ -e "$USERTAGS" ] || : > "$USERTAGS"</span> +<span class="a">+ names=$(pickmulti 'tags:' < "$USERTAGS" | sed 's/[ |].*//' | sed '/^$/d' \</span> +<span class="a">+ | sort -u)</span> +<span class="a">+ printf '%s\n' "$names" | learn_tags</span> +<span class="a">+ tags=$(printf '%s\n' "$names" | paste -sd ' ' -)</span> +<span class="a">+</span> +<span class="a">+ lock</span> +<span class="a">+ printf '%s\t%s\t%s\n' "$url" "$desc" "$tags" >> "$BOOKMARKS"</span> +<span class="a">+ unlock</span> +<span class="a">+ changed "bm: add $url"</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 "$BOOKMARKS") || die 'cannot create a temporary file'</span> +<span class="a">+ lock</span> +<span class="a">+ SBM_FILE=$BOOKMARKS SBM_NEW=$new awk -F'\t' "$AWK_NORM"'</span> +<span class="a">+ BEGIN {</span> +<span class="a">+ while ((getline line < ENVIRON["SBM_FILE"]) > 0) {</span> +<span class="a">+ split(line, f, "\t")</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 "%s\t%s\t%s\n", $1, $2, $3 > ENVIRON["SBM_NEW"]; added++ }</span> +<span class="a">+ END { printf "added %d, skipped %d duplicates\n", added, skipped }'</span> +<span class="a">+ cut -f3 "$new" | tr ' ' '\n' | sort -u | learn_tags</span> +<span class="a">+ cat "$new" >> "$BOOKMARKS"</span> +<span class="a">+ unlock</span> +<span class="a">+ rm -f "$new"</span> +<span class="a">+ changed 'bm: merge'</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 '' )</span> +<span class="a">+ if [ $? -eq 3 ]; then</span> +<span class="a">+ ask 'already bookmarked; press ENTER' >/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="ENTER open ^Y copy ^A add ^D delete ^E edit ^S sort [$sortorder]"</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="$header" --prompt='bookmark: ')</span> +<span class="a">+ [ -n "$out" ] || exit 1</span> +<span class="a">+ typed=$(printf '%s\n' "$out" | sed -n 1p)</span> +<span class="a">+ key=$(printf '%s\n' "$out" | sed -n 2p)</span> +<span class="a">+ url=$(printf '%s\n' "$out" | 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 "$url" ]; then</span> +<span class="a">+ [ -n "$key" ] && continue</span> +<span class="a">+ [ -n "$typed" ] || exit 1</span> +<span class="a">+ go "$typed"</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 "$url"; exit 0 ;;</span> +<span class="a">+ ctrl-e) edit_url "$url" ;;</span> +<span class="a">+ ctrl-d)</span> +<span class="a">+ case $(ask "delete $url? (y/N)") in</span> +<span class="a">+ y|Y|yes) delete_url "$url" ;;</span> +<span class="a">+ esac</span> +<span class="a">+ ;;</span> +<span class="a">+ *) open_url "$url"; 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 '[add]\n'; rows | cut -f1-3; } | pick 'bookmark:')</span> +<span class="a">+ [ -n "$chosen" ] || exit 1</span> +<span class="a">+ if [ "$chosen" = '[add]' ]; then</span> +<span class="a">+ add_inside</span> +<span class="a">+ continue</span> +<span class="a">+ fi</span> +<span class="a">+ url=$(printf '%s\n' "$chosen" | cut -f1)</span> +<span class="a">+ if ! is_bookmark "$url"; then</span> +<span class="a">+ go "$chosen"</span> +<span class="a">+ exit 0</span> +<span class="a">+ fi</span> +<span class="a">+ action=$( { printf 'open\ncopy\nedit\ndelete\n'</span> +<span class="a">+ details "$url" | sed 's/^/ /'; } | pick 'do:')</span> +<span class="a">+ case $action in</span> +<span class="a">+ open) open_url "$url"; exit 0 ;;</span> +<span class="a">+ copy) copy_url "$url"; exit 0 ;;</span> +<span class="a">+ edit) edit_url "$url" ;;</span> +<span class="a">+ delete) delete_url "$url" ;;</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 "$action" ]; then</span> +<span class="a">+ printf 'bm: only one action at a time\n' >&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 <flag> <count of remaining arguments> <value></span> +<span class="a">+needarg () {</span> +<span class="a">+ if [ "$2" -lt 2 ] || [ -z "$3" ]; then</span> +<span class="a">+ printf 'bm: %s needs an argument\n' "$1" >&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">+ ''|-*) ;;</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 "$1" $# "${2-}"</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 "$1" $# "${2-}"</span> +<span class="a">+ sortorder=$2</span> +<span class="a">+ shift</span> +<span class="a">+ ;;</span> +<span class="a">+ *)</span> +<span class="a">+ printf 'bm: unknown argument: %s\n' "$1" >&2</span> +<span class="a">+ usage >&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 'bm: unknown sort order: %s\n' "$sortorder" >&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 [ "$action" = 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 "$(dirname "$BOOKMARKS")" || die "cannot create directory for $BOOKMARKS"</span> +<span class="a">+[ -e "$BOOKMARKS" ] || : > "$BOOKMARKS" || die "cannot create $BOOKMARKS"</span> +<span class="a">+</span> +<span class="a">+if awk '!/\t/ && !/^#/ && NF { found = 1; exit } END { exit !found }' "$BOOKMARKS"</span> +<span class="a">+then</span> +<span class="a">+ printf 'bm: %s has lines in the old format; run: bm-migrate\n' "$BOOKMARKS" >&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">+ '')</span> +<span class="a">+ if [ "$SBM_MENU" = 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 'copy:') && copy_url "$url" ;;</span> +<span class="a">+ print) url=$(choose_url 'print:') && bump "$url" && printf '%s\n' "$url" ;;</span> +<span class="a">+ edit) url=$(choose_url 'edit:') && edit_url "$url" ;;</span> +<span class="a">+ delete) url=$(choose_url 'delete:') && delete_url "$url" \</span> +<span class="a">+ && printf 'deleted %s\n' "$url" ;;</span> +<span class="a">+ add) add "$addurl" ;;</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:-"."}</span> +<span class="a">+results=$(mktemp)</span> +<span class="a">+log=$(mktemp)</span> +<span class="a">+trap "tsp -K; rm -f "$results" "$log"" EXIT</span> +<span class="a">+echo "Updating repos in: "$dir""</span> +<span class="a">+tsp -S 8</span> +<span class="a">+find ""$dir"" -name ".git" -type d 2>/dev/null | while read git_dir; do</span> +<span class="a">+ tsp -n git-update-worker.sh "$(dirname ""$git_dir"")" ""$results"" ""$log""</span> +<span class="a">+done</span> +<span class="a">+tail -f ""$log"" &</span> +<span class="a">+tail_pid=$!</span> +<span class="a">+tsp -w</span> +<span class="a">+kill "$tail_pid" 2>/dev/null</span> +<span class="a">+conflicts=$(grep -c "^REBASE_CONFLICT:" ""$results"" 2>/dev/null || echo 0)</span> +<span class="a">+echo "CHANGES:"</span> +<span class="a">+grep "^UPDATED:" ""$results"" | while IFS='|' read prefix repo changelog; do</span> +<span class="a">+ repo_name=$(echo ""$repo"" | sed "s|^"$dir"/||" | sed 's|^./||')</span> +<span class="a">+ echo "\033[32m"$repo_name":\033[0m"</span> +<span class="a">+ echo ""$changelog"" | tr ';' '\n' | sed 's/^/ /' | sed 's/^ $//'</span> +<span class="a">+done</span> +<span class="a">+if grep -q "^REBASE_CONFLICT:" ""$results""; then</span> +<span class="a">+ echo "\033[31mREBASE CONFLICTS:\033[0m"</span> +<span class="a">+ grep "^REBASE_CONFLICT:" ""$results"" | while IFS='|' read prefix repo _; do</span> +<span class="a">+ repo_name=$(echo ""$repo"" | sed "s|^"$dir"/||" | sed 's|^./||')</span> +<span class="a">+ echo "\033[31m"$repo_name": requires manual resolution\033[0m"</span> +<span class="a">+ done</span> +<span class="a">+fi</span> +<span class="a">+if [ ""$conflicts"" ] && [ ""$conflicts"" -gt 0 ] 2>/dev/null; then</span> +<span class="a">+ notify-send "Git Rebase Conflicts" ""$conflicts" repos need manual resolution" -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="$1"</span> +<span class="a">+results="$2"</span> +<span class="a">+log="$3"</span> +<span class="a">+name=$(basename "$repo")</span> +<span class="a">+old=$(cd "$repo" && git rev-parse HEAD 2>/dev/null)</span> +<span class="a">+if [ -f "$repo/.git/FETCH_HEAD" ]; then</span> +<span class="a">+ if [ "$(find "$repo/.git/FETCH_HEAD" -mmin -5 2>/dev/null)" ]; then</span> +<span class="a">+ echo "[$name] skipped" >> "$log"</span> +<span class="a">+ exit</span> +<span class="a">+ fi</span> +<span class="a">+fi</span> +<span class="a">+(cd "$repo" && git remote update >/dev/null 2>&1)</span> +<span class="a">+if (cd "$repo" && git pull --rebase >/dev/null 2>&1); then</span> +<span class="a">+ :</span> +<span class="a">+elif (cd "$repo" && git rebase --abort >/dev/null 2>&1); then</span> +<span class="a">+ echo "REBASE_CONFLICT:$repo|" >> "$results"</span> +<span class="a">+ echo "[$name] REBASE CONFLICT" >> "$log"</span> +<span class="a">+ exit</span> +<span class="a">+else</span> +<span class="a">+ echo "[$name] FAILED" >> "$log"</span> +<span class="a">+ exit</span> +<span class="a">+fi</span> +<span class="a">+new=$(cd "$repo" && git rev-parse HEAD 2>/dev/null)</span> +<span class="a">+[ "$old" = "$new" ] && { echo "[$name] up to date" >> "$log"; exit; }</span> +<span class="a">+echo "[$name] UPDATED" >> "$log"</span> +<span class="a">+changelog=$(cd "$repo" && git log --oneline --no-merges -3 "$old..$new" 2>/dev/null | tr '\n' ';')</span> +<span class="a">+echo "UPDATED:$repo|$changelog" >> "$results"</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="$(mktemp)"</span> +<span class="a">+[ -e "$TEMP" ] && rm "$TEMP"</span> +<span class="a">+mkdir "$TEMP"</span> +<span class="a">+BASE="$(basename "$1")"</span> +<span class="a">+cp "$1" "$TEMP"</span> +<span class="a">+</span> +<span class="a">+cd "$TEMP"</span> +<span class="a">+pdfseparate "$PWD"/"$1" %d.pdf</span> +<span class="a">+rm "$BASE"</span> +<span class="a">+find . -name '*.pdf' -print0 | grep -z -v group4 | xargs -0 -L1 group4ify-aux</span> +<span class="a">+cat <(find . -name '*-group4.pdf' | sed 's:^./::' | sort -h | sed 's:^\|$:":g') <(echo "$BASE"-group4.pdf) | xargs pdfunite</span> +<span class="a">+cd -</span> +<span class="a">+mv "$TEMP"/"$BASE"-group4.pdf .</span> +<span class="a">+rm -r "$TEMP"</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 "$1" "$1"-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 = '''</span> +<span class="a">+<!DOCTYPE html></span> +<span class="a">+<html></span> +<span class="a">+ <head></span> +<span class="a">+ <link href="data:," rel="icon"></span> +<span class="a">+ <meta charset="UTF-8"></span> +<span class="a">+ <meta name="viewport" content="width=device-width,initial-scale=1.0"></span> +<span class="a">+ <title>http-collect</title></span> +<span class="a">+ <style></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">+ </style></span> +<span class="a">+ </head></span> +<span class="a">+ <body></span> +<span class="a">+ '''</span> +<span class="a">+ out += body</span> +<span class="a">+ out += '''</span> +<span class="a">+ </body></span> +<span class="a">+</html></span> +<span class="a">+ '''</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('.').replace('/', '_')</span> +<span class="a">+</span> +<span class="a">+</span> +<span class="a">+def sanitize_html(s):</span> +<span class="a">+ return s.replace('&', '&amp').replace('<', '&lt;').replace('>', '&gt;')</span> +<span class="a">+</span> +<span class="a">+</span> +<span class="a">+def suffixify(basename, suff):</span> +<span class="a">+ m = match(r'(.+)\.([^.]+)$', basename)</span> +<span class="a">+ if not m:</span> +<span class="a">+ return f'{basename} ({suff:03d})'</span> +<span class="a">+</span> +<span class="a">+ return f'{m.group(1)} ({suff:03d}).{m.group(2)}'</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(['ip', '-4', 'route']).decode('UTF-8')</span> +<span class="a">+ for line in rt.splitlines():</span> +<span class="a">+ m = match(r'default.* src ([0-9.]+)', 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('Could not find my IPv4, using 0.0.0.0', file=stderr)</span> +<span class="a">+ host = '0.0.0.0'</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'Failed to use {host}:{try_port}: {repr(e)}')</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('Could not find free port')</span> +<span class="a">+</span> +<span class="a">+ url = f'http://{host}:{port}/{secret}'</span> +<span class="a">+</span> +<span class="a">+ qr = QRCode()</span> +<span class="a">+ qr.add_data(f'http://{host}:{port}/{secret}')</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('/<secret_from_url>', method [diff truncated]