133728e barerepo 1mo
1
#!/usr/bin/env python3
133728e barerepo 1mo
2
"""Emits every static mockup page. Run: python3 build.py"""
133728e barerepo 1mo
4
import os
133728e barerepo 1mo
6
OUT = os.path.dirname(os.path.abspath(__file__))
133728e barerepo 1mo
8
SHELL = '''<!doctype html>
133728e barerepo 1mo
9
<html lang="en">
133728e barerepo 1mo
10
<head>
133728e barerepo 1mo
11
<meta charset="utf-8">
133728e barerepo 1mo
12
<meta name="viewport" content="width=device-width, initial-scale=1">
133728e barerepo 1mo
13
<title>barerepo{title}</title>
133728e barerepo 1mo
14
<link rel="stylesheet" href="tokens.css">
133728e barerepo 1mo
15
</head>
133728e barerepo 1mo
16
<body>
133728e barerepo 1mo
17
<div class="wrap">
133728e barerepo 1mo
19
<div style="font-family: var(--font-mono); font-size: 13px; line-height: 1.6; color: var(--text-primary); background: #ffffff; border: 0.5px solid var(--border); border-radius: 12px; min-height: calc(100vh - 26px); display: flex; flex-direction: column;">
133728e barerepo 1mo
21
<h2 class="sr-only">{sr}</h2>
133728e barerepo 1mo
22
{body}
133728e barerepo 1mo
23
<div style="position:sticky; bottom:0; margin-top:auto; background:#ffffff; border-top:0.5px solid var(--border); border-radius:0 0 12px 12px; padding:9px 18px; display:flex; justify-content:space-between; color:var(--text-muted); font-size:12px;">
133728e barerepo 1mo
24
<span>{fl}</span><span>{fr}</span>
133728e barerepo 1mo
25
</div>
133728e barerepo 1mo
27
</div>
133728e barerepo 1mo
29
</div>
133728e barerepo 1mo
30
</body>
133728e barerepo 1mo
31
</html>
133728e barerepo 1mo
32
'''
133728e barerepo 1mo
34
def topbar(user='john', right_extra=''):
133728e barerepo 1mo
35
return ('<div style="display:flex; align-items:center; justify-content:space-between; padding:9px 18px; '
133728e barerepo 1mo
36
'border-bottom:0.5px solid var(--border);">\n'
133728e barerepo 1mo
37
' <div style="display:flex; gap:20px; align-items:center;">\n'
133728e barerepo 1mo
38
' <span style="letter-spacing:-0.5px; text-decoration:underline;">barerepo</span>\n'
133728e barerepo 1mo
39
' <span style="color:var(--text-secondary); text-decoration:underline;">new</span>\n'
133728e barerepo 1mo
40
' </div>\n'
133728e barerepo 1mo
41
' <div style="display:flex; gap:16px; align-items:center; color:var(--text-secondary);">\n'
133728e barerepo 1mo
42
' <span style="border:0.5px solid var(--border); border-radius:8px; padding:3px 10px; '
133728e barerepo 1mo
43
'color:var(--text-muted);">search /</span>\n'
133728e barerepo 1mo
44
f' <span>{user}</span>{right_extra}\n'
133728e barerepo 1mo
45
' </div>\n'
133728e barerepo 1mo
46
'</div>\n')
133728e barerepo 1mo
48
def crumb(path, right):
133728e barerepo 1mo
49
return ('<div style="display:flex; align-items:center; justify-content:space-between; padding:9px 18px; '
133728e barerepo 1mo
50
'border-bottom:0.5px solid var(--border);">\n'
133728e barerepo 1mo
51
f' <div style="color:var(--text-secondary);">{path}</div>\n'
133728e barerepo 1mo
52
f' <div style="color:var(--text-muted); font-size:12px;">{right}</div>\n'
133728e barerepo 1mo
53
'</div>\n')
133728e barerepo 1mo
55
def repotabs(active, extra_right='jump to file <span style="border:0.5px solid var(--border); border-radius:4px; padding:0 5px;">t</span>'):
133728e barerepo 1mo
56
tabs = ['log', 'files', 'threads 3', 'runs', 'config']
133728e barerepo 1mo
57
out = ('<div style="display:flex; gap:22px; padding:8px 18px; border-bottom:0.5px solid var(--border); '
133728e barerepo 1mo
58
'color:var(--text-secondary); font-size:12px;">\n')
133728e barerepo 1mo
59
for t in tabs:
133728e barerepo 1mo
60
if t.split()[0] == active:
133728e barerepo 1mo
61
out += (f' <span style="color:var(--text-primary); border-bottom:1.5px solid var(--text-primary); '
133728e barerepo 1mo
62
f'padding-bottom:4px;">{t}</span>\n')
133728e barerepo 1mo
63
else:
133728e barerepo 1mo
64
out += f' <span style="text-decoration:underline;">{t}</span>\n'
133728e barerepo 1mo
65
out += f' <span style="margin-left:auto; color:var(--text-muted);">{extra_right}</span>\n</div>\n'
133728e barerepo 1mo
66
return out
133728e barerepo 1mo
68
def lnk(t):
133728e barerepo 1mo
69
return f'<span style="text-decoration:underline;">{t}</span>'
133728e barerepo 1mo
71
def row(inner, muted=False, tall=False):
133728e barerepo 1mo
72
c = ' color:var(--text-muted);' if muted else ''
133728e barerepo 1mo
73
pad = '13px 18px' if tall else '12px 18px'
133728e barerepo 1mo
74
return f'<div style="padding:{pad}; border-bottom:0.5px solid var(--border);{c}">\n{inner}\n</div>\n'
133728e barerepo 1mo
76
def between(a, b):
133728e barerepo 1mo
77
return (f'<div style="display:flex; justify-content:space-between;">'
133728e barerepo 1mo
78
f'<span>{a}</span><span style="color:var(--text-muted); font-size:12px;">{b}</span></div>')
133728e barerepo 1mo
80
def sub(t):
133728e barerepo 1mo
81
return f'<div style="color:var(--text-muted); font-size:12px;">{t}</div>'
133728e barerepo 1mo
83
def box(t, mono_muted=True):
133728e barerepo 1mo
84
c = 'var(--text-secondary)' if mono_muted else 'var(--text-primary)'
133728e barerepo 1mo
85
return (f'<div style="background:var(--surface-1); border:0.5px solid var(--border); border-radius:8px; '
133728e barerepo 1mo
86
f'padding:9px 12px; color:{c}; font-size:12px; word-break:break-all;">{t}</div>')
133728e barerepo 1mo
88
def field(label, value, hint=''):
133728e barerepo 1mo
89
h = f'\n <div style="color:var(--text-muted); font-size:12px; margin-top:4px;">{hint}</div>' if hint else ''
133728e barerepo 1mo
90
return (f'<div style="margin-bottom:16px;">\n'
133728e barerepo 1mo
91
f' <div style="color:var(--text-muted); font-size:12px; margin-bottom:5px;">{label}</div>\n'
133728e barerepo 1mo
92
f' <div style="border:0.5px solid var(--border-strong); border-radius:8px; padding:7px 11px; '
133728e barerepo 1mo
93
f'word-break:break-all;">{value}</div>{h}\n</div>')
133728e barerepo 1mo
95
def btn(t):
133728e barerepo 1mo
96
return (f'<div style="border:0.5px solid var(--text-primary); border-radius:8px; padding:7px 16px; '
133728e barerepo 1mo
97
f'display:inline-block;">{t}</div>')
133728e barerepo 1mo
99
def hunk_start(header):
133728e barerepo 1mo
100
"""Reads the new-side start line out of an @@ header."""
133728e barerepo 1mo
101
import re
133728e barerepo 1mo
102
m = re.search(r'\+(\d+)', header)
133728e barerepo 1mo
103
return int(m.group(1)) if m else 1
133728e barerepo 1mo
106
def diff(header, lines):
133728e barerepo 1mo
107
"""A diff box. Lines carry their number, because 35.3 says to press it."""
133728e barerepo 1mo
108
out = ('<div style="border:0.5px solid var(--border); border-radius:8px; overflow:hidden; font-size:12px; '
133728e barerepo 1mo
109
'margin-top:8px;">\n'
133728e barerepo 1mo
110
f' <div style="padding:3px 11px; color:var(--text-muted); background:var(--surface-1); '
133728e barerepo 1mo
111
f'border-bottom:0.5px solid var(--border);">{header}</div>\n')
133728e barerepo 1mo
112
n = hunk_start(header)
133728e barerepo 1mo
113
num = ('display:inline-block; width:30px; margin-right:9px; text-align:right; '
133728e barerepo 1mo
114
'color:var(--text-muted); text-decoration:underline;')
133728e barerepo 1mo
115
for kind, txt in lines:
133728e barerepo 1mo
116
if kind == '+':
133728e barerepo 1mo
117
st = 'background:var(--bg-success); color:var(--text-success);'
133728e barerepo 1mo
118
gutter = f'<span style="{num}">{n}</span>'
133728e barerepo 1mo
119
txt = gutter + '+ ' + txt
133728e barerepo 1mo
120
n += 1
133728e barerepo 1mo
121
elif kind == '-':
133728e barerepo 1mo
122
st = 'background:var(--bg-danger); color:var(--text-danger);'
133728e barerepo 1mo
123
plain = num.replace('text-decoration:underline;', '')
133728e barerepo 1mo
124
gutter = f'<span style="{plain}"> </span>'
133728e barerepo 1mo
125
txt = gutter + '- ' + txt
133728e barerepo 1mo
126
elif kind == 'note':
133728e barerepo 1mo
127
st = 'color:var(--text-muted); text-align:center;'
133728e barerepo 1mo
128
else:
133728e barerepo 1mo
129
st = 'color:var(--text-secondary);'
133728e barerepo 1mo
130
gutter = f'<span style="{num}">{n}</span>'
133728e barerepo 1mo
131
txt = gutter + ' ' + txt
133728e barerepo 1mo
132
n += 1
133728e barerepo 1mo
133
out += f' <div style="padding:2px 11px; {st}">{txt}</div>\n'
133728e barerepo 1mo
134
return out + '</div>\n'
133728e barerepo 1mo
136
PAGES = {}
133728e barerepo 1mo
138
# ---------------------------------------------------------------- identity
133728e barerepo 1mo
140
PAGES['signin.html'] = dict(
133728e barerepo 1mo
141
title=' · sign in', fl='barerepo', fr='barerepo 0.1.0',
133728e barerepo 1mo
142
sr='Sign in page where the server challenges an ssh key instead of asking for a password.',
133728e barerepo 1mo
143
body=('<div style="display:flex; align-items:center; justify-content:space-between; padding:9px 18px; '
133728e barerepo 1mo
144
'border-bottom:0.5px solid var(--border);"><span style="letter-spacing:-0.5px;">barerepo</span>'
133728e barerepo 1mo
145
'<span style="color:var(--text-secondary);">new account</span></div>\n'
133728e barerepo 1mo
146
'<div style="padding:26px 18px; max-width:430px;">\n'
133728e barerepo 1mo
147
' <div style="margin-bottom:20px;">sign in</div>\n'
133728e barerepo 1mo
148
+ field('name', 'john<span style="color:var(--text-muted);">|</span>') +
133728e barerepo 1mo
149
' <div style="margin-bottom:18px;">' + btn('send challenge') + '</div>\n'
133728e barerepo 1mo
150
' <div style="color:var(--text-muted); font-size:12px; line-height:1.9;">\n'
133728e barerepo 1mo
151
' <div>we give you a nonce. you sign it with the key you already have.</div>\n'
133728e barerepo 1mo
152
' <div style="color:var(--text-secondary);">'
133728e barerepo 1mo
153
'printf \'%s\' \'<nonce>\' | ssh-keygen -Y sign -f ~/.ssh/id_ed25519 -n barerepo-auth -</div>\n'
133728e barerepo 1mo
154
' <div>that is stock openssh. nothing to install.</div>\n'
133728e barerepo 1mo
155
' <div>or <span style="color:var(--text-secondary);">br auth john</span>, '
133728e barerepo 1mo
156
'which does the same thing in one step.</div>\n'
133728e barerepo 1mo
157
' </div>\n</div>\n'))
133728e barerepo 1mo
159
PAGES['keys.html'] = dict(
133728e barerepo 1mo
160
title=' · keys', fl='john / keys', fr='barerepo 0.1.0',
133728e barerepo 1mo
161
sr='Page listing ssh keys, runner tokens and feed tokens, each with a revoke control.',
133728e barerepo 1mo
162
body=(topbar() +
133728e barerepo 1mo
163
crumb('john / <span style="color:var(--text-primary);">keys</span>',
133728e barerepo 1mo
164
'the only state the server owns') +
133728e barerepo 1mo
165
row('<div style="color:var(--text-muted); font-size:12px;">ssh keys</div>') +
133728e barerepo 1mo
166
row(between('ed25519 SHA256:8fK2q0mR4vXeN1pLzT9wBcJdSgYo3Ea7kVnQxMuP2r', 'added mar 2026') +
133728e barerepo 1mo
167
sub('laptop · last used 2h · <span style="color:var(--text-secondary); text-decoration:underline;">revoke</span>')) +
133728e barerepo 1mo
168
row(between('ed25519 SHA256:Qw3rTy7uIoP0aSdFgHjKlZxCvBnM4eR8tYu1IoP2aSd', 'added apr 2026') +
133728e barerepo 1mo
169
sub('uproar · last used 4d · <span style="color:var(--text-secondary); text-decoration:underline;">revoke</span>')) +
133728e barerepo 1mo
170
row('<div style="color:var(--text-muted); font-size:12px;">runner tokens</div>') +
133728e barerepo 1mo
171
row(between('uproar.local', 'created 3d') +
133728e barerepo 1mo
172
sub('john/johnbot · labels build, test · last seen 40m · '
133728e barerepo 1mo
173
'<span style="color:var(--text-secondary); text-decoration:underline;">revoke</span>')) +
133728e barerepo 1mo
174
row('<div style="color:var(--text-muted); font-size:12px;">feed tokens</div>') +
133728e barerepo 1mo
175
row(between('inbox', 'created 3d') +
133728e barerepo 1mo
176
sub('read only · last read 20m · '
133728e barerepo 1mo
177
'<span style="color:var(--text-secondary); text-decoration:underline;">revoke</span>')) +
133728e barerepo 1mo
178
row(btn('new key') + ' ' + btn('new runner token') +
133728e barerepo 1mo
179
' ' + btn('new feed token')) +
133728e barerepo 1mo
180
row(sub('a token is shown once, inside the command that uses it. only its hash is kept, '
133728e barerepo 1mo
181
'so it cannot be shown again. lost one? revoke it and make another.')) +
133728e barerepo 1mo
182
row(sub('a key signs you in and pushes. a runner token attaches one machine to one '
133728e barerepo 1mo
183
'repository. a feed token reads one feed and can write nothing.')) +
133728e barerepo 1mo
184
row(sub('everything else is in .barerepo/config, in the repository it belongs to.'))))
133728e barerepo 1mo
186
# ---------------------------------------------------------------- repo start
133728e barerepo 1mo
188
PAGES['new-repo.html'] = dict(
133728e barerepo 1mo
189
title=' · new repo', fl='barerepo', fr='barerepo 0.1.0',
133728e barerepo 1mo
190
sr='Form for creating a new repository with a choice of default branch name.',