#!/usr/bin/env python3 """Emits every static mockup page. Run: python3 build.py""" import os OUT = os.path.dirname(os.path.abspath(__file__)) SHELL = ''' barerepo{title}

{sr}

{body}
{fl}{fr}
''' def topbar(user='john', right_extra=''): return ('
\n' '
\n' ' barerepo\n' ' new\n' '
\n' '
\n' ' search /\n' f' {user}{right_extra}\n' '
\n' '
\n') def crumb(path, right): return ('
\n' f'
{path}
\n' f'
{right}
\n' '
\n') def repotabs(active, extra_right='jump to file t'): tabs = ['log', 'files', 'threads 3', 'runs', 'config'] out = ('
\n') for t in tabs: if t.split()[0] == active: out += (f' {t}\n') else: out += f' {t}\n' out += f' {extra_right}\n
\n' return out def lnk(t): return f'{t}' def row(inner, muted=False, tall=False): c = ' color:var(--text-muted);' if muted else '' pad = '13px 18px' if tall else '12px 18px' return f'
\n{inner}\n
\n' def between(a, b): return (f'
' f'{a}{b}
') def sub(t): return f'
{t}
' def box(t, mono_muted=True): c = 'var(--text-secondary)' if mono_muted else 'var(--text-primary)' return (f'
{t}
') def field(label, value, hint=''): h = f'\n
{hint}
' if hint else '' return (f'
\n' f'
{label}
\n' f'
{value}
{h}\n
') def btn(t): return (f'
{t}
') def hunk_start(header): """Reads the new-side start line out of an @@ header.""" import re m = re.search(r'\+(\d+)', header) return int(m.group(1)) if m else 1 def diff(header, lines): """A diff box. Lines carry their number, because 35.3 says to press it.""" out = ('
\n' f'
{header}
\n') n = hunk_start(header) num = ('display:inline-block; width:30px; margin-right:9px; text-align:right; ' 'color:var(--text-muted); text-decoration:underline;') for kind, txt in lines: if kind == '+': st = 'background:var(--bg-success); color:var(--text-success);' gutter = f'{n}' txt = gutter + '+ ' + txt n += 1 elif kind == '-': st = 'background:var(--bg-danger); color:var(--text-danger);' plain = num.replace('text-decoration:underline;', '') gutter = f' ' txt = gutter + '- ' + txt elif kind == 'note': st = 'color:var(--text-muted); text-align:center;' else: st = 'color:var(--text-secondary);' gutter = f'{n}' txt = gutter + '  ' + txt n += 1 out += f'
{txt}
\n' return out + '
\n' PAGES = {} # ---------------------------------------------------------------- identity PAGES['signin.html'] = dict( title=' · sign in', fl='barerepo', fr='barerepo 0.1.0', sr='Sign in page where the server challenges an ssh key instead of asking for a password.', body=('
barerepo' 'new account
\n' '
\n' '
sign in
\n' + field('name', 'john|') + '
' + btn('send challenge') + '
\n' '
\n' '
we give you a nonce. you sign it with the key you already have.
\n' '
' 'printf \'%s\' \'<nonce>\' | ssh-keygen -Y sign -f ~/.ssh/id_ed25519 -n barerepo-auth -
\n' '
that is stock openssh. nothing to install.
\n' '
or br auth john, ' 'which does the same thing in one step.
\n' '
\n
\n')) PAGES['keys.html'] = dict( title=' · keys', fl='john / keys', fr='barerepo 0.1.0', sr='Page listing ssh keys, runner tokens and feed tokens, each with a revoke control.', body=(topbar() + crumb('john / keys', 'the only state the server owns') + row('
ssh keys
') + row(between('ed25519 SHA256:8fK2q0mR4vXeN1pLzT9wBcJdSgYo3Ea7kVnQxMuP2r', 'added mar 2026') + sub('laptop · last used 2h · revoke')) + row(between('ed25519 SHA256:Qw3rTy7uIoP0aSdFgHjKlZxCvBnM4eR8tYu1IoP2aSd', 'added apr 2026') + sub('uproar · last used 4d · revoke')) + row('
runner tokens
') + row(between('uproar.local', 'created 3d') + sub('john/johnbot · labels build, test · last seen 40m · ' 'revoke')) + row('
feed tokens
') + row(between('inbox', 'created 3d') + sub('read only · last read 20m · ' 'revoke')) + row(btn('new key') + '  ' + btn('new runner token') + '  ' + btn('new feed token')) + row(sub('a token is shown once, inside the command that uses it. only its hash is kept, ' 'so it cannot be shown again. lost one? revoke it and make another.')) + row(sub('a key signs you in and pushes. a runner token attaches one machine to one ' 'repository. a feed token reads one feed and can write nothing.')) + row(sub('everything else is in .barerepo/config, in the repository it belongs to.')))) # ---------------------------------------------------------------- repo start PAGES['new-repo.html'] = dict( title=' · new repo', fl='barerepo', fr='barerepo 0.1.0', sr='Form for creating a new repository with a choice of default branch name.', body=(topbar() + '
\n' '
new repository
\n' + field('name', 'johnbot|', 'john/johnbot') + field('description', 'optional') + field('default branch', 'master', 'any branch name') + field('visibility', 'public') + '
' + btn('create') + '
\n
\n' + '
\n' '
' 'or skip this form. push to a name that does not exist.
\n' + box('git remote add origin git@barerepo:john/<name>
git push -u origin master') + '
\n' '
the branch you push becomes the default branch.
\n' '
the repository starts private. public is one line in .barerepo/config.
\n' '
\n
\n')) PAGES['repo-empty.html'] = dict( title=' · johnbot', fl='john / johnbot', fr='barerepo 0.1.0', sr='Empty repository page showing a single block of shell commands to paste.', body=(crumb('john / johnbot', 'empty · master') + '
\n' '
paste this
\n' '
' 'git init
git remote add origin git@barerepo:john/johnbot
' 'mkdir -p .forge && printf \'[repo]\\nvisibility = "public"\\n\' > .barerepo/config
' 'git add -A
' 'git commit -m "first"
git push -u origin master
\n' '
' 'the third line is what makes it public. without it the repository stays private, ' 'because there is nowhere else to keep that.
\n' '
\n' '
already have a repo somewhere?
\n' '
git remote set-url origin git@barerepo:john/johnbot ' '&& git push --all
\n
\n
\n')) # ---------------------------------------------------------------- repo browse PAGES['repo-files.html'] = dict( title=' · johnbot files', fl='master', fr='barerepo 0.1.0', sr='Repository file tree listing directories and files with last commit information.', body=(crumb('john / johnbot', 'master · clone git@barerepo:john/johnbot') + repotabs('files') + row(between('irc/', 'john · 6h') + sub('drop the retry loop, it never fired')) + row(between('cmd/', 'john · 2d') + sub('split the daemon out')) + row(between('config.go', 'lisa · 2h') + sub('fix panic when config is empty')) + row(between('config_test.go', 'lisa · 2h') + sub('cover the empty case')) + row(between('go.mod', 'dave · 3d') + sub('bump deps')) + row(between('.barerepo/config', 'john · 3d') + sub('add lisa to push')) + row(between('README', 'john · 4mo') + sub('it is an irc bot')))) PAGES['repo-file.html'] = dict( title=' · config.go', fl='history · raw', fr='barerepo 0.1.0', sr='File view with blame information shown in the left gutter beside each line.', body=(crumb('john / johnbot / config.go', '61 lines · 1.4kb · master') + repotabs('files') + '
\n' + ''.join( f'
' f'
{b}
' f'
{n}
' f'
{c}
\n' for b, n, c in [ ('a3f9c2 lisa 2h', '41', '  f, err := os.Open(path)'), ('a3f9c2 lisa 2h', '42', '  if err != nil {'), ('a3f9c2 lisa 2h', '43', '    return nil, err'), ('a3f9c2 lisa 2h', '44', '  }'), ('a3f9c2 lisa 2h', '45', '  if len(raw) == 0 {'), ('a3f9c2 lisa 2h', '46', '    return Default(), nil'), ('a3f9c2 lisa 2h', '47', '  }'), ('7c1e08 john 4mo', '48', '  return cfg, nil'), ('7c1e08 john 4mo', '49', '}'), ]) + '
\n')) PAGES['repo-commit.html'] = dict( title=' · a3f9c2', fl='parent 8b1d44', fr='barerepo 0.1.0', sr='Single commit page showing message, metadata and full diff.', body=(crumb('john / johnbot / a3f9c2', 'lisa · 2h · 1 file · +4 -1') + row('
fix panic when config is empty
' + sub('empty config.toml hit a nil deref on load. return defaults instead.')) + row(sub('config.go') + diff('@@ -41,7 +41,10 @@ func Load', [ (' ', 'f, err := os.Open(path)'), ('-', 'return cfg'), ('+', 'if len(raw) == 0 {'), ('+', '  return Default(), nil'), ('+', '}'), ('+', 'return cfg, nil'), (' ', '}')])) + row(sub('reachable from master · closed thread 47 · build ok on uproar.local')))) PAGES['repo-compare.html'] = dict( title=' · compare', fl='master...refs/proposals/47', fr='barerepo 0.1.0', sr='Compare view between two arbitrary refs showing combined diff stats.', body=(crumb('john / johnbot / compare', 'master...refs/proposals/47') + repotabs('log') + row('
' + box('master') + '...' + box('refs/proposals/47') + '
' + '
' + sub('3 commits · 2 files · +81 -12 · no conflicts') + '
') + row(sub('config.go · +7 -1') + diff('@@ -41,7 +41,10 @@ func Load', [ ('-', 'return cfg'), ('+', 'if len(raw) == 0 {'), ('+', '  return Default(), nil'), ('+', '}')])) + row(sub('config_test.go · +74 -11') + diff('@@ -12,4 +12,18 @@ func TestLoad', [ ('+', 'func TestLoadEmpty(t *testing.T) {'), ('note', '70 more added lines')])))) # ---------------------------------------------------------------- threads PAGES['threads.html'] = dict( title=' · threads', fl='3 open · 41 closed', fr='barerepo 0.1.0', sr='Thread list where issues and code proposals appear in one combined list.', body=(crumb('john / johnbot / threads', '3 open · 41 closed') + repotabs('threads', lnk('open') + ' · ' + lnk('merged') + ' · ' + lnk('closed') + ' · ' + lnk('all')) + row(between('47  panic when config file is empty', 'lisa · 2h') + sub('has proposal · +81 -12 · build ok · 2 replies')) + row(between('46  switch to pure-go sqlite', 'mark · 1d') + sub('has proposal · +14 -9 · build failed')) + row(between('44  does this work behind a socks proxy?', 'anon · 5d') + sub('question · no proposal · 6 replies')) + row('
' '45  bump deps' 'dave · 3d
' + sub('merged · tip reachable from master'), muted=True))) PAGES['thread-new.html'] = dict( title=' · new thread', fl='john / johnbot', fr='barerepo 0.1.0', sr='New thread form with an optional field for attaching a pushed proposal ref.', body=(crumb('john / johnbot / new thread', 'refs/notes/threads') + '
\n' + field('title', 'panic when config file is empty|') + '
\n' '
body
\n' '
markdown|
\n
\n' + field('attach a ref', 'optional', 'push first, then paste the ref') + '
' + btn('open') + '
\n
\n')) # ---------------------------------------------------------------- runners PAGES['runner-setup.html'] = dict( title=' · add a runner', fl='john / johnbot / runs', fr='barerepo 0.1.0', sr='Runner setup page showing one paste-ready command per operating system with the token already embedded.', body=(crumb('john / johnbot / add a runner', 'rt_live_7Kq2mXe') + '
\n' '
' 'paste on any machine you want to build on.
\n' '
linux, macos
\n' + box('curl -sL barerepo.sh | sh -s rt_live_7Kq2mXe') + '
windows
\n' + box('irm barerepo.sh/ps | iex; forge runner rt_live_7Kq2mXe') + '
' 'already have the binary
\n' + box('barerepo runner rt_live_7Kq2mXe --labels build,test') + '
\n' '
the runner dials out. it needs no inbound port and no public address.
\n' '
token scopes to this repo. revoke it on your keys page.
\n' '
this page refreshes the moment a runner attaches.
\n' '
a runner is a program because it long-polls. the protocol is plain http:
\n' '
' 'POST /runner/attach · GET /runner/poll · POST /runner/log · ' 'POST /runner/done
\n' '
write your own if you would rather. chapter 15 is the whole spec.
\n' '
\n
\n')) PAGES['runners.html'] = dict( title=' · runners', fl='2 attached', fr='barerepo 0.1.0', sr='List of attached build machines with labels, platform and last seen time.', body=(crumb('john / johnbot / runners', '2 attached') + repotabs('runs', lnk('add a runner')) + row(between('uproar.local', 'idle · 40m ago') + sub('linux/amd64 · labels build, test · 214 runs')) + row(between('lisa-mbp', 'busy · now') + sub('darwin/arm64 · labels build · 31 runs')) + row(between('winbox', 'offline · 6d ago') + sub('windows/amd64 · labels build · 4 runs · ' 'forget'), muted=True))) PAGES['runs.html'] = dict( title=' · runs', fl='john / johnbot', fr='barerepo 0.1.0', sr='List of build runs with status, trigger and duration.', body=(crumb('john / johnbot / runs', 'newest first') + repotabs('runs', lnk('runners') + ' · ' + lnk('add a runner')) + row(between('a3f9c2  build ok · test ok', '18s · 40m') + sub('refs/proposals/47 · uproar.local')) + row(between('e91b7d  build failed', '6s · 1d') + sub('refs/proposals/46 · uproar.local')) + row(between('8b1d44  build ok · test ok', '21s · 6h') + sub('master · lisa-mbp')))) PAGES['run.html'] = dict( title=' · run e91b7d', fl='refs/proposals/46', fr='barerepo 0.1.0', sr='Run detail page showing raw build log output as plain scrollable text.', body=(crumb('john / johnbot / run e91b7d', 'uproar.local · 6s · 1d ago') + row('
build failed · exit 2
' + sub('refs/proposals/46 · switch to pure-go sqlite · mark')) + '
\n' '
$ go build ./...
\n' '
go: downloading modernc.org/sqlite v1.34.1
\n' '
go: downloading modernc.org/libc v1.55.3
\n' '
store/db.go:14:2: ' 'cannot find module providing package github.com/mattn/go-sqlite3
\n' '
exit status 2
\n' '
$ exit 2
\n
\n' + row(sub(lnk('raw log') + ' · ' + lnk('rerun') + ' · 18kb')))) # ---------------------------------------------------------------- search / edges PAGES['search.html'] = dict( title=' · search', fl='"backoff"', fr='barerepo 0.1.0', sr='Search results combining code matches, threads and repositories in one list.', body=(topbar() + '
' + field('', 'backoff|') + sub('47 results') + '
\n' + row(sub('code') + '
johnbot / irc/conn.go:88
' + '
' 'time.Sleep(backoff(i))' '
') + row(sub('code') + '
johnbot / irc/retry.go:9
' + '
' 'func backoff' '(n int) time.Duration {
') + row(sub('thread') + '
johnbot 44 · does this work behind a socks proxy?
' + sub('mark mentions backoff twice · 5d')) + row(sub('repo') + '
john / johnbot
' + sub('irc bot that refuses to leave')))) PAGES['push-rejected.html'] = dict( title=' · push rejected', fl='john / johnbot', fr='barerepo 0.1.0', sr='Page explaining exactly which server hook rejected a push and how to proceed.', body=(crumb('john / johnbot / push rejected', 'e91b7d · 2m ago') + row('
you pushed to refs/heads/master
' + sub('.barerepo/config [access] push = ["john", "lisa"] · you are mark')) + '
\n' '
' 'push here instead. it needs no permission.
\n' + box('git push origin HEAD:refs/proposals/new') + '
\n' + row(sub('the same message was printed in your terminal.')))) PAGES['404.html'] = dict( title=' · not found', fl='barerepo', fr='barerepo 0.1.0', sr='Not found page.', body=(topbar() + '
\n' '
404
\n' '
' 'john/johnbot exists. that path in it does not.
\n
\n')) # ---------------------------------------------------------------- index def build_index(): groups = [ ('identity', [ ('signup.html', 'signup', 'ssh key is the only credential'), ('signin.html', 'sign in', 'server challenges the key'), ('keys.html', 'keys and tokens', 'the only state the server owns'), ]), ('repo, first contact', [ ('new-repo.html', 'new repo', 'default branch is master'), ('repo-empty.html', 'empty repo', 'one block to paste'), ]), ('browsing', [ ('repo-log.html', 'repo log', 'diffs inline, this is the landing page'), ('repo-files.html', 'file tree', 'secondary, not the front door'), ('repo-file.html', 'file view', 'blame in the gutter, always on'), ('repo-commit.html', 'commit', 'one commit, full diff'), ('repo-compare.html', 'compare', 'any ref against any ref'), ]), ('threads', [ ('threads.html', 'thread list', 'issues and proposals in one list'), ('thread.html', 'thread', 'discussion stored in git notes'), ('thread-new.html', 'new thread', 'attach a ref to make it a proposal'), ]), ('runners', [ ('runner-setup.html', 'add a runner', 'one line, token already in it'), ('runners.html', 'runners', 'your machines'), ('runs.html', 'runs', 'triggered by push'), ('run.html', 'run detail', 'raw log, no step theater'), ]), ('activity', [ ('inbox.html', 'inbox', 'chronological, no read state, atom'), ('releases.html', 'releases', 'tag, notes, attached files'), ]), ('everything else', [ ('search.html', 'search', 'code, threads, repos in one result set'), ('profile.html', 'profile', 'repo list'), ('repo-config.html', 'repo config', 'settings as a versioned file'), ('push-rejected.html', 'push rejected', 'says what the hook refused'), ('404.html', '404', ''), ]), ] body = ('
' 'barerepo · ui mockups
\n') for name, items in groups: body += (f'
{name}
\n') for href, label, desc in items: body += (f'
{label}
' f'
{desc}
\n') return dict(title=' · mockups', fl='24 views', fr='barerepo 0.1.0', sr='Index of every mockup page in this set.', body=body) PAGES['inbox.html'] = dict( title=' · inbox', fl='inbox', fr='barerepo 0.1.0', sr='Chronological list of events on repositories and threads the user participates in.', body=(topbar() + crumb('inbox', lnk('atom') + ' · ' + lnk('feed token')) + row(between('lisa opened proposal 47 on johnbot', '2h') + sub('panic when config file is empty')) + row(between('john replied on johnbot thread 47', '1h') + sub('Default() allocates every call. make it a package var?')) + row(between('build failed on johnbot refs/proposals/46', '1d') + sub('uproar.local · exit 2')) + '
last visited 2d ago
\n' + row(between('mark opened thread 44 on johnbot', '5d') + sub('does this work behind a socks proxy?'), muted=True) + row(between('dave merged proposal 45 on johnbot', '3d') + sub('bump deps'), muted=True) + row(sub('events older than 90 days are dropped')))) PAGES['releases.html'] = dict( title=' · releases', fl='john / johnbot', fr='barerepo 0.1.0', sr='List of tagged releases with notes and attached files.', body=(crumb('john / johnbot / releases', 'refs/notes/releases') + repotabs('log', 'newest first') + row(between('v1.2.0', 'john · 3d') + sub('fixes the empty config panic. thanks lisa.') + '
' + sub('johnbot-linux-amd64 · 8.1mb · johnbot-darwin-arm64 · 7.9mb') + '
') + row(between('v1.1.0', 'john · 2mo') + sub('reconnect handling') + '
' + sub('johnbot-linux-amd64 · 8.0mb') + '
') + row(sub('release notes clone with the repository. attached files do not.')))) # ---------------------------------------------------------------- folded in # These five pages were hand-written before this generator existed and drifted # away from it. They are here now, so `python3 build.py` really does write # every page from one shared chrome, which is what the README claims. PAGES['signup.html'] = dict( title=' · signup', fl='barerepo', fr='barerepo 0.1.0', sr='Signup page where an ssh public key is the only credential collected.', body=('
barerepo' 'sign in
\n' '
\n' '
new account
\n' + field('name', 'john|') + field('public key', 'ssh-ed25519 ' 'AAAAC3NzaC1lZDI1NTE5AAAAIH8fK2q0mR4vXeN1pLzT9wBcJdSgYo3Ea7kVnQxMuP2r', 'cat ~/.ssh/id_ed25519.pub') + '
' + btn('create') + '
\n' '
\n' '
your key is your account. there is no email and no password.
\n' '
losing every key loses the account, ' 'because there is no out-of-band recovery channel.
\n' '
add a second key from another machine today.
\n' '
\n
\n')) PAGES['repo-log.html'] = dict( title=' · johnbot', fl=lnk('older'), fr='barerepo 0.1.0', sr='Repository log with every commit diff expanded inline. This is the landing page.', body=(crumb('john / johnbot', 'master · clone git@barerepo:john/johnbot') + repotabs('log') + row(between('a3f9c2  fix panic when config is empty', 'lisa · 2h') + sub('config.go · +4 -1') + diff('@@ -41,7 +41,10 @@ func Load', [ (' ', 'f, err := os.Open(path)'), ('-', 'return cfg'), ('+', 'if len(raw) == 0 {'), ('+', '  return Default(), nil'), ('+', '}'), ('+', 'return cfg, nil'), (' ', '}')]), tall=True) + row(between('8b1d44  drop the retry loop, it never fired', 'john · 6h') + sub('irc/conn.go · +0 -23') + diff('@@ -88,23 +88,0 @@ func (c *Conn) dial', [ ('-', 'for i := 0; i < maxRetry; i++ {'), ('-', '  time.Sleep(backoff(i))'), ('note', '18 more removed lines')]), tall=True) + row(between('dave  merged proposal 45, bump deps', '14 files · +302 -288') + sub('3d · large diff collapsed · ' + lnk('expand')), tall=True))) PAGES['repo-config.html'] = dict( title=' · .barerepo/config', fl=lnk('history') + ' · ' + lnk('blame') + ' · ' + lnk('raw'), fr='barerepo 0.1.0', sr='Repository settings shown as a versioned file in the repository rather than a settings form.', body=(crumb('john / johnbot / .barerepo/config', 'edited 3d by john · a3f9c2') + repotabs('config') + '
\n' '
[repo]
\n' '
default_branch = "master"
\n' '
visibility    = "public"
\n' '
archived      = false
\n' '
\n' '
[proposals]
\n' '
accept_from  = "anyone"
\n' '
require_runs = ["build", "test"]
\n' '
\n' '
[access]
\n' '
push            = ' '["john", "lisa"]
\n' '
allow_force_push = []
\n' '
\n' '
[runners]
\n' '
"uproar.local" = ["build", "test"]
\n' '
\n' + row(sub('edit the file, commit, push. there is no settings form. ' 'git log -p .barerepo/config ' 'shows who changed what.')) + row(sub('raw file content is served from this host as a download. private files have no raw ' 'link until the operator sets a separate raw host.')))) PAGES['thread.html'] = dict( title=' · thread 47', fl='threads · 3 open', fr='barerepo 0.1.0', sr='Discussion thread where issues and proposals are one object, stored in git notes.', body=(crumb('john / johnbot / thread 47', 'refs/notes/threads/47') + row('
panic when config file is empty
' + sub('opened by lisa · 2h · has proposal · ' 'refs/proposals/47 · +81 -12'), tall=True) + row(sub('lisa · 2h') + '
fresh install, empty config.toml, immediate nil deref on line 44. ' 'reproduces every time.
', tall=True) + row(sub('lisa · 2h · attached a proposal') + diff('config.go @@ -41,7 +41,10 @@', [ ('+', 'if len(raw) == 0 {'), ('+', '  return Default(), nil'), ('+', '}')]), tall=True) + row(sub('john · 1h · on config.go:43') + '
Default() allocates every call. make it a package var?
', tall=True) + row(sub('uproar.local · 40m · self-hosted runner') + '
build ok · test ok · 18s
', tall=True) + row('
reply|
', tall=True) + row(sub('this thread is in your clone. read it with no network, and read it if barerepo stops.') + '
' + box('git fetch origin "refs/notes/*:refs/notes/*"
' 'git log --show-notes=threads/47') + '
', tall=True))) PAGES['profile.html'] = dict( title=' · john', fl='barerepo', fr='barerepo 0.1.0', sr='User profile listing repositories sorted by last push.', body=(topbar() + '
\n' '
\n' '
jd
\n' '
john
\n' '
' 'writes bots. mostly go.
\n' '
\n' '
14 repos
\n
joined mar 2026
\n
ssh keys: 3
\n' '
' + lnk('atom') + '
\n' '
\n
\n' '
\n' '
' 'repositoriessorted by pushed
\n' + ''.join( '
\n' f'
{nm}' f'{when}
\n' f'
{desc}
\n' f'
{meta}
\n' '
\n' for nm, when, desc, meta in [ (lnk('johnbot'), '2h', 'irc bot that refuses to leave', 'go · 4.1mb · master · 3 proposals'), (lnk('dotfiles'), '1d', 'nvim, zsh, too many aliases', 'shell · 210kb · master'), ('uproar private', '4d', 'server configs, do not read', 'nix · 88kb · main'), (lnk('sqlite-notes'), '3w', 'append-only notes over sqlite', 'c · 1.2mb · master · archived'), ]) + '
showing 4 of 14 · ' + lnk('all') + '
\n
\n
\n')) PAGES['index.html'] = build_index() for fname, p in PAGES.items(): with open(os.path.join(OUT, fname), 'w', encoding='utf-8') as f: f.write(SHELL.format(**p)) print(fname)