Skip to content
go-libfossil Artifact [31315f919c]
Login

Artifact [31315f919c]

Artifact 31315f919ccae62bb3e1befa495daf7a384f24278e7546e60c7b38d4d041c6f6:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="go-libfossil — pure-Go library and CLI for Fossil SCM repositories. Zero CGo, static binaries, WASM-ready.">
<meta name="color-scheme" content="light dark">
<title>go-libfossil — pure-Go Fossil SCM library</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
  --paper:  #f5f1e6;
  --paper-2:#ece7d8;
  --ink:    #14110b;
  --mute:   #5e5b53;
  --rule:   #14110b;
  --ember:  #c63a0f;
  --measure: 76ch;
  --gap:   clamp(2.25rem, 5vw, 3.5rem);
  --mono: "Geist Mono", ui-monospace, "SF Mono", "Cascadia Mono", "Roboto Mono", Consolas, "Courier New", monospace;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 16px; -webkit-text-size-adjust: 100%; }
body {
  background: var(--paper);
  color: var(--ink);
  font: 400 clamp(15px, 1.05vw, 17px)/1.7 var(--mono);
  font-feature-settings: "tnum" 1, "calt" 1, "ss01" 1;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}
.skip { position: absolute; left: -9999px; }
.skip:focus {
  position: fixed; top: 0.75rem; left: 0.75rem;
  background: var(--ember); color: var(--paper);
  padding: 0.5rem 0.75rem; z-index: 10; text-decoration: none;
}
header.topbar {
  border-bottom: 1px solid var(--rule);
  padding: clamp(0.625rem, 2.5vw, 0.75rem) clamp(1rem, 4vw, 1.5rem);
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 1rem; flex-wrap: wrap;
  font-size: 0.78125rem;
  letter-spacing: 0.04em;
}
.topbar a { color: inherit; text-decoration: none; border-bottom: 1px solid currentColor; }
.topbar a:hover { color: var(--ember); }
.topbar .name { font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; }
.topbar .meta { color: var(--mute); display: flex; gap: 1.25rem; flex-wrap: wrap; }
.topbar .meta span { display: inline-flex; gap: 0.4rem; }
.topbar .meta b { color: var(--ink); font-weight: 500; }

main {
  max-width: 88ch;
  margin: 0 auto;
  padding: clamp(2rem, 4vw, 3rem) clamp(1rem, 4vw, 1.5rem) clamp(3rem, 6vw, 4.5rem);
  overflow-wrap: anywhere;
}

.title {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.25rem;
  padding-bottom: clamp(2rem, 4vw, 2.75rem);
  margin-bottom: clamp(2rem, 4vw, 3rem);
  border-bottom: 1px solid var(--rule);
}
.title h1 {
  font-size: clamp(2.75rem, 13vw, 6.5rem);
  font-weight: 700;
  letter-spacing: -0.05em;
  line-height: 0.92;
}
.title .lead {
  font-size: clamp(1rem, 1.2vw, 1.0625rem);
  max-width: var(--measure);
  color: var(--ink);
  line-height: 1.6;
}
.spec {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.78125rem;
  margin-top: 0.5rem;
  letter-spacing: 0.02em;
}
.spec td { padding: 0.35rem 0; border-top: 1px dashed var(--rule); vertical-align: top; }
.spec td:first-child {
  width: 12rem;
  color: var(--mute);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-size: 0.6875rem;
  padding-top: 0.5rem;
}
.spec tr:first-child td { border-top: 1px solid var(--rule); }
.spec tr:last-child td { border-bottom: 1px solid var(--rule); }

section { margin-top: var(--gap); }
section > .num {
  display: block;
  font-size: 0.6875rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--mute);
  margin-bottom: 0.4rem;
}
h2 {
  font-size: clamp(1.125rem, 1.6vw, 1.3125rem);
  font-weight: 700;
  letter-spacing: -0.01em;
  margin-bottom: 1.25rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--rule);
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  flex-wrap: wrap;
}
h2 .anchor {
  font-size: 0.6875rem;
  font-weight: 400;
  color: var(--mute);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

p { max-width: var(--measure); margin-bottom: 1rem; }
p:last-child { margin-bottom: 0; }
strong { font-weight: 700; }

a {
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid var(--ink);
}
a:hover { color: var(--ember); border-bottom-color: var(--ember); }

code {
  font: inherit;
  font-size: 0.94em;
  background: var(--paper-2);
  padding: 0.05em 0.35em;
}

ol, ul { padding-left: 0; max-width: var(--measure); list-style: none; }
ol li, ul li {
  margin-bottom: 0.875rem;
  padding-left: 2.75rem;
  position: relative;
  line-height: 1.65;
}
ol { counter-reset: item; }
ol li {
  counter-increment: item;
}
ol li::before {
  content: counter(item, decimal-leading-zero);
  position: absolute;
  left: 0;
  top: 0.05em;
  font-weight: 700;
  font-size: 0.78125rem;
  letter-spacing: 0.04em;
  color: var(--ember);
  width: 2rem;
}
ul li::before {
  content: "—";
  position: absolute;
  left: 0.5rem;
  color: var(--mute);
}

.code-block {
  margin-bottom: 1rem;
  border: 1px solid var(--rule);
  background: var(--paper-2);
}
.code-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.35rem 0.5rem 0.35rem 0.85rem;
  border-bottom: 1px solid var(--rule);
  background: var(--paper);
  font-size: 0.625rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--mute);
}
pre {
  padding: 0.875rem 1.25rem;
  font-size: 0.85rem;
  line-height: 1.65;
  overflow-x: auto;
  white-space: pre;
  font-family: var(--mono);
  color: var(--ink);
}
pre.ascii {
  background: transparent;
  border: 1px solid var(--rule);
  padding: clamp(0.75rem, 2.5vw, 1.25rem);
  font-size: clamp(0.7rem, 1.7vw, 0.8125rem);
  line-height: 1.45;
  color: var(--ink);
  margin-bottom: 1rem;
  overflow-x: auto;
}

.diagram {
  display: block;
  width: 100%;
  min-width: 30rem;
  height: auto;
  margin-bottom: 0.5rem;
  font-family: var(--mono);
  color: var(--ink);
}
.diagram .rule { stroke: var(--rule); stroke-width: 1; opacity: 0.5; }
.diagram .box { fill: none; stroke: var(--rule); stroke-width: 1; }
.diagram .head {
  font-size: 11px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  fill: var(--mute);
}
.diagram .item { font-size: 13px; fill: var(--ink); }
.diagram .stack { font-size: 12px; fill: var(--mute); font-style: italic; }
.diagram .flow line { stroke: var(--ember); stroke-width: 1.5; fill: none; }
.diagram .arrowhead { fill: var(--ember); }
.diagram .flow-label {
  font-size: 10px;
  fill: var(--ember);
  letter-spacing: 0.6px;
  text-transform: uppercase;
}

.copy-btn {
  background: transparent;
  color: var(--ink);
  border: 1px solid var(--ink);
  font: inherit;
  font-size: 0.625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.15rem 0.55rem;
  cursor: pointer;
  flex-shrink: 0;
}
.copy-btn:hover, .copy-btn:focus-visible { background: var(--ink); color: var(--paper); outline: none; }
.copy-btn[data-state="copied"] { color: var(--ember); border-color: var(--ember); background: var(--paper); }

.table-scroll {
  margin: 0 calc(-1 * clamp(0rem, 2vw, 0.5rem));
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
table.cmp {
  width: 100%;
  min-width: 30rem;
  border-collapse: collapse;
  font-size: 0.875rem;
  margin-bottom: 0.75rem;
}
table.cmp th, table.cmp td {
  text-align: left;
  padding: 0.625rem 0.875rem;
  vertical-align: top;
  border-bottom: 1px dashed var(--rule);
}
table.cmp thead th {
  border-bottom: 1px solid var(--rule);
  border-top: 1px solid var(--rule);
  font-size: 0.6875rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  background: var(--paper-2);
}
table.cmp td:first-child, table.cmp th:first-child {
  padding-left: 0.875rem;
  width: 30%;
  font-weight: 500;
  color: var(--mute);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.75rem;
}
table.cmp td:last-child, table.cmp th:last-child { padding-right: 0.875rem; }
table.cmp .col-self { color: var(--ember); font-weight: 700; }
table.cmp tbody tr:last-child td { border-bottom: 1px solid var(--rule); }

.fineprint {
  font-size: 0.75rem;
  color: var(--mute);
  margin-top: 0.5rem;
  letter-spacing: 0.02em;
}

footer {
  border-top: 1px solid var(--rule);
  margin-top: var(--gap);
  padding: 1.5rem 0;
  font-size: 0.78125rem;
  color: var(--mute);
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 1rem;
  letter-spacing: 0.04em;
}
footer a { color: inherit; border-bottom-color: var(--mute); }

@media (prefers-color-scheme: dark) {
  :root {
    --paper:  #15140f;
    --paper-2:#1f1d17;
    --ink:    #ece8dc;
    --mute:   #908b80;
    --rule:   #ece8dc;
    --ember:  #ff5a26;
  }
}
@media print {
  body { background: white; color: black; font-size: 11pt; line-height: 1.5; }
  .code-block, pre { background: white; color: black; border: 1px solid black; }
  a { color: black; border-bottom: 0; text-decoration: underline; }
  a::after { content: " (" attr(href) ")"; font-size: 0.85em; color: #555; }
  .copy-btn, .skip { display: none; }
  section { break-inside: avoid; }
}
@media (max-width: 540px) {
  header.topbar { flex-direction: column; align-items: flex-start; gap: 0.5rem; }
  .topbar .meta { gap: 0.75rem; row-gap: 0.35rem; }
  .spec td:first-child { width: 8rem; font-size: 0.625rem; }
  .spec td { padding: 0.4rem 0; }
  table.cmp { font-size: 0.6875rem; min-width: 0; table-layout: auto; }
  table.cmp th, table.cmp td { padding: 0.45rem 0.4rem; }
  table.cmp tbody td { white-space: normal; word-break: normal; overflow-wrap: break-word; }
  table.cmp thead th {
    font-size: 0.5rem;
    letter-spacing: 0.06em;
    padding: 0.5rem 0.3rem;
    white-space: nowrap;
  }
  table.cmp td:first-child, table.cmp th:first-child {
    padding-left: 0.55rem;
    width: 28%;
    font-size: 0.5625rem;
    letter-spacing: 0.04em;
  }
  table.cmp td:last-child, table.cmp th:last-child { padding-right: 0.55rem; }
  ol li, ul li { padding-left: 2.25rem; }
  ol li::before { width: 1.75rem; }
  footer { flex-direction: column; gap: 0.5rem; align-items: flex-start; }
  .copy-btn { font-size: 0.5625rem; padding: 0.1rem 0.4rem; }
  pre { font-size: 0.78rem; padding: 0.75rem 0.9rem; }
  .code-bar { padding: 0.3rem 0.4rem 0.3rem 0.7rem; }
}
@media (max-width: 380px) {
  .topbar .name { letter-spacing: 0.12em; }
  .spec td:first-child { width: 7rem; }
  table.cmp { font-size: 0.625rem; }
  table.cmp th, table.cmp td { padding: 0.4rem 0.25rem; }
  table.cmp thead th { font-size: 0.4375rem; padding: 0.45rem 0.2rem; }
  table.cmp td:first-child, table.cmp th:first-child { font-size: 0.5rem; padding-left: 0.4rem; width: 26%; }
}
</style>
</head>
<body>

<a href="#main" class="skip">skip to content</a>

<header class="topbar">
  <div class="name">go-libfossil</div>
  <div class="meta">
    <span>v <b>{{ .Site.Params.version | default "0.4.2" }}</b></span>
    <span>license <b>MIT</b></span>
    <span><a href="https://github.com/danmestas/go-libfossil">github</a></span>
    <span><a href="/docs/">docs</a></span>
  </div>
</header>

<main id="main">

<section class="title">
  <h1>go-libfossil</h1>
  <p class="lead">A pure-Go library for Fossil SCM repositories. Zero CGo. Static binaries. WASM-ready.</p>
  <table class="spec">
    <tbody>
      <tr><td>Project</td>      <td>go-libfossil — pure-Go Fossil SCM library</td></tr>
      <tr><td>Language</td>     <td>Go (module <code>github.com/danmestas/libfossil</code>)</td></tr>
      <tr><td>Runtime deps</td> <td>none · pluggable SQLite drivers (modernc · ncruces)</td></tr>
      <tr><td>Binaries</td>     <td>1 (libfossil CLI)</td></tr>
      <tr><td>License</td>      <td>MIT</td></tr>
      <tr><td>Status</td>       <td>v{{ .Site.Params.version | default "0.4.2" }} · alpha · breaking changes possible</td></tr>
    </tbody>
  </table>
</section>

<section>
  <span class="num">§01</span>
  <h2>Install <span class="anchor">install</span></h2>
  <div class="code-block">
    <div class="code-bar">
      <span>shell</span>
      <button type="button" class="copy-btn" data-copy="install" aria-label="Copy install command">copy</button>
    </div>
    <pre id="install">$ go install github.com/danmestas/libfossil/cmd/libfossil@latest
$ libfossil --help</pre>
  </div>
  <p>That is the entire install. The binary is pure-Go, statically linked, and CGo-free &mdash; drop it on any Linux/macOS/Windows host with no runtime dependency.</p>
</section>

<section>
  <span class="num">§02</span>
  <h2>What it is <span class="anchor">overview</span></h2>
  <p>libfossil is a pure-Go reader and writer for <a href="https://fossil-scm.org">Fossil SCM</a> repositories &mdash; the SQLite-backed <code>.fossil</code> files used by Fossil. It exposes a Go API for opening repos, reading the timeline, committing, and syncing over HTTP, plus a <code>libfossil</code> CLI driving the same code paths.</p>
  <p>No CGo means static binaries that cross-compile cleanly. A <code>GOOS=wasip1</code> build runs in browsers and edge runtimes. Two SQLite drivers (<code>modernc</code>, <code>ncruces</code>) are swappable depending on whether you want pure-Go ergonomics or a smaller WASM footprint.</p>
  <ul>
    <li><strong>One library, three pluggable axes.</strong> Swap the SQLite driver (<code>modernc</code>/<code>ncruces</code>), the sync transport (HTTP/custom), or the observer (OTel/custom) without touching application code.</li>
    <li><strong>Static everywhere.</strong> Static binaries cross-compile cleanly. WASM works.</li>
    <li><strong>Embeddable.</strong> Drop <code>libfossil.Open</code> into any Go service that needs to read <code>.fossil</code> repos &mdash; build pipelines, mirror servers, code-search indexers.</li>
  </ul>
</section>

<section>
  <span class="num">§03</span>
  <h2>Why you might want it <span class="anchor">why</span></h2>
  <p>Three reasons to reach for libfossil instead of shelling out to the upstream <code>fossil</code> binary or using CGo bindings.</p>
  <p><strong>You're embedding Fossil into a Go service.</strong> Build pipelines, code-search indexers, mirror servers, and AI-agent tools that read repository history all benefit from a library API instead of subprocess parsing. No exec calls, no version drift between hosts, observable through Go's standard <code>context.Context</code> and OpenTelemetry hooks.</p>
  <p><strong>You're shipping to constrained targets.</strong> Static binaries cross-compile in seconds; CGo bindings don't. WASM compilation works out of the box. The cost is a slightly slower SQLite (modernc is pure Go); the upside is the binary runs on any target without a build farm.</p>
  <p><strong>You want one library, not five subprocesses.</strong> Calling <code>fossil</code> over <code>os/exec</code> means re-parsing CLI text on every request. libfossil exposes typed Go values directly.</p>
</section>

<section>
  <span class="num">§04</span>
  <h2>How it fits together <span class="anchor">architecture</span></h2>
  <div class="table-scroll">
    <svg class="diagram" viewBox="0 0 640 260" role="img" aria-labelledby="arch-title arch-desc" preserveAspectRatio="xMidYMid meet">
      <title id="arch-title">go-libfossil architecture</title>
      <desc id="arch-desc">Callers (cli, library, wasm) invoke methods on the Repo handle. The Repo delegates to three pluggable axes: db driver, transport, observer. Repository state lives on disk in a .fossil SQLite file.</desc>

      <defs>
        <marker id="lf-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
          <path class="arrowhead" d="M0,0 L10,5 L0,10 Z"/>
        </marker>
      </defs>

      <g class="rule">
        <line x1="40" y1="38" x2="160" y2="38"/>
        <line x1="240" y1="38" x2="400" y2="38"/>
        <line x1="480" y1="38" x2="600" y2="38"/>
      </g>

      <g class="head">
        <text x="100" y="28" text-anchor="middle">callers</text>
        <text x="320" y="28" text-anchor="middle">core</text>
        <text x="540" y="28" text-anchor="middle">plugins</text>
      </g>

      <g class="item">
        <text x="100" y="76" text-anchor="middle">cli</text>
        <text x="100" y="100" text-anchor="middle">library</text>
        <text x="100" y="124" text-anchor="middle">wasm</text>
      </g>

      <rect class="box" x="240" y="60" width="160" height="100"/>
      <g class="item">
        <text x="320" y="88" text-anchor="middle">Open</text>
        <text x="320" y="112" text-anchor="middle">Sync</text>
        <text x="320" y="136" text-anchor="middle">Timeline</text>
      </g>

      <rect class="box" x="490" y="60" width="100" height="100"/>
      <g class="item">
        <text x="540" y="88" text-anchor="middle">db driver</text>
        <text x="540" y="112" text-anchor="middle">transport</text>
        <text x="540" y="136" text-anchor="middle">observer</text>
      </g>

      <rect class="box" x="240" y="200" width="160" height="40"/>
      <g class="item">
        <text x="320" y="225" text-anchor="middle">.fossil (SQLite)</text>
      </g>

      <g class="flow">
        <line x1="160" y1="100" x2="234" y2="100" marker-end="url(#lf-arrow)"/>
        <line x1="400" y1="88" x2="486" y2="88" marker-end="url(#lf-arrow)"/>
        <line x1="490" y1="136" x2="404" y2="136" marker-end="url(#lf-arrow)"/>
        <line x1="320" y1="160" x2="320" y2="196" marker-end="url(#lf-arrow)"/>
      </g>

      <g class="flow-label">
        <text x="197" y="92" text-anchor="middle">calls</text>
        <text x="443" y="80" text-anchor="middle">delegate</text>
        <text x="447" y="154" text-anchor="middle">events</text>
        <text x="334" y="183" text-anchor="start">persist</text>
      </g>
    </svg>
  </div>
  <p class="fineprint"><code>Repo</code> is the only handle your code touches. Three pluggable axes around it decide how SQLite runs, how sync talks to remotes, and where telemetry goes — swappable without changing application code.</p>
</section>

<section>
  <span class="num">§05</span>
  <h2>Compared to neighbors <span class="anchor">comparison</span></h2>
  <div class="table-scroll">
  <table class="cmp">
    <thead>
      <tr>
        <th></th>
        <th class="col-self">go-libfossil</th>
        <th>shell-out to fossil</th>
        <th>CGo libfossil bindings</th>
      </tr>
    </thead>
    <tbody>
      <tr><td>Runtime deps</td>     <td class="col-self">none</td>            <td><code>fossil</code> binary in PATH</td> <td>C toolchain + libsqlite3</td></tr>
      <tr><td>Static binary</td>    <td class="col-self">yes</td>             <td>host fossil only</td>                  <td>host SQLite only</td></tr>
      <tr><td>WASM target</td>      <td class="col-self">yes (wasip1)</td>    <td>no</td>                                 <td>no</td></tr>
      <tr><td>Observability</td>    <td class="col-self">context + OTel</td>  <td>parse stderr</td>                       <td>none built-in</td></tr>
      <tr><td>Embeddable in Go</td> <td class="col-self">yes</td>             <td>subprocess only</td>                    <td>yes (with CGo)</td></tr>
      <tr><td>API stability</td>    <td class="col-self">alpha; v0.x</td>     <td>stable</td>                             <td>tracks C upstream</td></tr>
    </tbody>
  </table>
  </div>
  <p class="fineprint">Comparison reflects libfossil's current scope. The upstream Fossil binary remains the reference implementation; libfossil targets the embedding-into-Go-services niche.</p>
</section>

<section>
  <span class="num">§06</span>
  <h2>What it is not <span class="anchor">limits</span></h2>
  <p><strong>No GUI.</strong> Fossil ships an embedded web UI; libfossil does not. If you need it, run the upstream <code>fossil</code> binary alongside.</p>
  <p><strong>No hosting (yet).</strong> libfossil reads and writes <code>.fossil</code> files; it does not run an HTTP server that serves them to other Fossil clients. Mirroring works as a sync target, not a host.</p>
  <p><strong>No magic schema migrations.</strong> libfossil tracks the upstream Fossil schema. Breaking format changes upstream require a libfossil update before you can open the new format.</p>
</section>

<section>
  <span class="num">§07</span>
  <h2>Get going <span class="anchor">start</span></h2>
  <div class="code-block">
    <div class="code-bar">
      <span>shell</span>
      <button type="button" class="copy-btn" data-copy="quickstart" aria-label="Copy quickstart commands">copy</button>
    </div>
    <pre id="quickstart">$ go install github.com/danmestas/libfossil/cmd/libfossil@latest
$ libfossil --help                            # CLI commands
$ libfossil --version</pre>
  </div>
  <p>Walkthrough: <a href="/docs/quickstart/">/docs/quickstart</a> · Architecture: <a href="/docs/architecture/">/docs/architecture</a> · SDK reference: <a href="/docs/reference/sdk/">/docs/reference/sdk</a> · Source: <a href="https://github.com/danmestas/go-libfossil">github.com/danmestas/go-libfossil</a>.</p>
</section>

</main>

<footer>
  <div>go-libfossil &nbsp;·&nbsp; MIT &nbsp;·&nbsp; v{{ .Site.Params.version | default "0.4.2" }} &nbsp;·&nbsp; {{ now.Year }} &nbsp;·&nbsp; <a href="https://github.com/danmestas/go-libfossil">github.com/danmestas/go-libfossil</a></div>
</footer>

<script>
(function () {
  var btns = document.querySelectorAll(".copy-btn");
  btns.forEach(function (b) {
    b.addEventListener("click", function () {
      var t = document.getElementById(b.dataset.copy);
      if (!t) return;
      var s = t.textContent.replace(/^\s*\$\s?/gm, "").replace(/\s+#.*$/gm, "").trim();
      var done = function () {
        b.dataset.state = "copied";
        b.textContent = "copied";
        setTimeout(function () { b.dataset.state = ""; b.textContent = "copy"; }, 1500);
      };
      if (navigator.clipboard && window.isSecureContext) {
        navigator.clipboard.writeText(s).then(done).catch(function () { b.textContent = "select+copy"; });
      } else {
        var ta = document.createElement("textarea");
        ta.value = s;
        ta.style.position = "fixed";
        ta.style.left = "-9999px";
        document.body.appendChild(ta);
        ta.select();
        try { document.execCommand("copy"); done(); } catch (e) { b.textContent = "select+copy"; }
        document.body.removeChild(ta);
      }
    });
  });
})();
</script>

</body>
</html>