+38
.tangled/workflows/publish.yaml
+38
.tangled/workflows/publish.yaml
···
1
+
when:
2
+
- event: ['push']
3
+
branch: ['main']
4
+
- event: ['manual']
5
+
6
+
engine: 'nixery'
7
+
8
+
dependencies:
9
+
nixpkgs:
10
+
- coreutils
11
+
- curl
12
+
github:NixOS/nixpkgs/nixpkgs-unstable:
13
+
- gleam
14
+
- beamMinimal28Packages.erlang
15
+
16
+
environment:
17
+
SITE_PATH: 'dist'
18
+
SITE_NAME: 'webbed-site'
19
+
WISP_HANDLE: 'fruno.win'
20
+
21
+
steps:
22
+
- name: build site
23
+
command: |
24
+
export PATH="$HOME/.nix-profile/bin:$PATH"
25
+
26
+
gleam run
27
+
- name: deploy to wisp
28
+
command: |
29
+
# Download Wisp CLI
30
+
curl https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli
31
+
chmod +x wisp-cli
32
+
33
+
# Deploy to Wisp
34
+
./wisp-cli \
35
+
"$WISP_HANDLE" \
36
+
--path "$SITE_PATH" \
37
+
--site "$SITE_NAME" \
38
+
--password "$WISP_APP_PASSWORD"
+11
README.md
+11
README.md
assets/fonts/InclusiveSans-Italic.woff2
assets/fonts/InclusiveSans-Italic.woff2
This is a binary file and will not be displayed.
assets/fonts/InclusiveSans.woff2
assets/fonts/InclusiveSans.woff2
This is a binary file and will not be displayed.
assets/fonts/Myna.woff2
assets/fonts/Myna.woff2
This is a binary file and will not be displayed.
+1
assets/img/gleam.svg
+1
assets/img/gleam.svg
···
1
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#ffaff3" d="M50.417 7.19c1.816-5.147 8.57-6.338 12.038-2.122L80.63 27.166a12.189 12.189 0 0 0 9.118 4.44l28.629.697c5.466.133 8.676 6.177 5.735 10.771L108.68 67.177a12.165 12.165 0 0 0-1.415 10.04l8.172 27.411c1.557 5.223-3.2 10.155-8.493 8.78l-27.713-7.2a12.194 12.194 0 0 0-9.989 1.76l-23.578 16.245c-4.504 3.103-10.66.096-10.984-5.345l-1.696-28.554a12.169 12.169 0 0 0-4.763-8.95L5.477 63.993c-4.335-3.31-3.385-10.088 1.706-12.082l26.664-10.447a12.188 12.188 0 0 0 7.048-7.29z"/><path fill="#151515" d="M55.39.154c-3.23.57-6.183 2.715-7.405 6.178l-9.523 26.981a9.598 9.598 0 0 1-5.553 5.744L6.243 49.504c-6.842 2.68-8.165 12.092-2.332 16.547l22.744 17.37a9.571 9.571 0 0 1 3.75 7.047l1.696 28.553c.435 7.325 8.98 11.493 15.034 7.322l23.58-16.245v-.001a9.604 9.604 0 0 1 7.87-1.387l27.714 7.199c7.116 1.849 13.72-4.99 11.623-12.023l-8.17-27.41a9.578 9.578 0 0 1 1.114-7.905l15.432-24.105c3.957-6.181-.504-14.572-7.85-14.751l-28.63-.696a9.595 9.595 0 0 1-7.183-3.497L64.46 3.425C62.127.589 58.619-.417 55.389.153m.869 4.932c1.468-.26 3.07.248 4.206 1.627L78.639 28.81a14.78 14.78 0 0 0 11.052 5.383l28.63.695c3.585.088 5.544 3.783 3.618 6.79L106.508 65.78a14.761 14.761 0 0 0-1.716 12.172l8.171 27.41c1.018 3.415-1.892 6.44-5.363 5.538l-27.714-7.2a14.787 14.787 0 0 0-12.108 2.136l-23.58 16.245c-2.954 2.035-6.722.187-6.933-3.368L35.57 90.16a14.762 14.762 0 0 0-5.775-10.852L7.051 61.939c-2.837-2.167-2.26-6.31 1.078-7.619l26.666-10.447a14.782 14.782 0 0 0 8.545-8.84l9.523-26.98c.594-1.685 1.927-2.71 3.395-2.968"/><path fill="#151515" d="M47.093 72.832a5.082 5.082 0 1 0-1.766-10.01 5.082 5.082 0 0 0 1.766 10.01zm40.945-7.22a5.081 5.081 0 1 0-1.764-10.008 5.081 5.081 0 0 0 1.764 10.008zM63.356 71.7a2.594 2.594 0 0 0-1.434 1.365 2.59 2.59 0 0 0-.048 1.98 6.734 6.734 0 0 0 3.562 3.737h.001c.81.358 1.681.554 2.566.575h.002a6.755 6.755 0 0 0 2.591-.451h.001a6.735 6.735 0 0 0 2.222-1.409h.001a6.74 6.74 0 0 0 2.089-4.722 2.588 2.588 0 0 0-4.952-1.106c-.137.31-.212.645-.22.985a1.55 1.55 0 0 1-.134.595v.002a1.578 1.578 0 0 1-.87.828 1.572 1.572 0 0 1-1.201-.03h-.002a1.573 1.573 0 0 1-.5-.35v-.001a1.568 1.568 0 0 1-.329-.518 2.585 2.585 0 0 0-3.345-1.48z"/></svg>
+354
assets/style.css
+354
assets/style.css
···
1
+
:root {
2
+
color-scheme: light dark;
3
+
4
+
--color-fg: light-dark(#1f1f28, #dcd7ba);
5
+
--color-bg: light-dark(#dcd7ba, #1f1f28);
6
+
7
+
--color-fg-muted: light-dark(#363646, #bab28a);
8
+
/* lighter in dark mode, darker in light mode */
9
+
--color-bg-variant: light-dark(#363646, #bab28a);
10
+
11
+
--color-primary: light-dark(#957fb8, #938aa9);
12
+
--color-primary-variant: light-dark(#938aa9, #957fb8);
13
+
--color-secondary: light-dark(#6a9589, #7aa89f);
14
+
--color-select: #7e9cd8;
15
+
--color-select-variant: #7fb4ca;
16
+
17
+
--font: "Inclusive Sans", sans-serif;
18
+
--font-mono: "Myna", monospace;
19
+
20
+
color: var(--color-fg);
21
+
background: var(--color-bg);
22
+
23
+
font-family: var(--font);
24
+
font-size: calc(.333vw + 1em);
25
+
}
26
+
27
+
/* Fonts */
28
+
29
+
@font-face {
30
+
font-family: "Inclusive Sans";
31
+
font-style: normal;
32
+
src: url(/fonts/InclusiveSans.woff2) format(woff2);
33
+
font-weight: 300 700;
34
+
font-display: swap;
35
+
}
36
+
37
+
@font-face {
38
+
font-family: "Inclusive Sans";
39
+
font-style: italic;
40
+
src: url(/fonts/InclusiveSans-Italic.woff2) format(woff2);
41
+
font-weight: 300 700;
42
+
font-display: swap;
43
+
}
44
+
45
+
@font-face {
46
+
font-family: "Myna";
47
+
font-style: normal;
48
+
src: url(/fonts/Myna.woff2);
49
+
font-weight: 400;
50
+
}
51
+
52
+
/* Style */
53
+
54
+
main {
55
+
view-transition-name: main-content;
56
+
padding-bottom: 2em;
57
+
}
58
+
59
+
main > * {
60
+
max-width: 35rem;
61
+
margin-left: auto;
62
+
margin-right: auto;
63
+
}
64
+
65
+
::selection {
66
+
background: var(--color-select);
67
+
color: var(--color-bg);
68
+
}
69
+
70
+
.heading {
71
+
color: var(--color-primary);
72
+
font-family: var(--font-mono);
73
+
74
+
h1, h2, h3, h4, h5, h6 { font-weight: inherit }
75
+
76
+
&:hover .header-anchor {
77
+
color: var(--color-fg);
78
+
}
79
+
80
+
.header-anchor {
81
+
margin-right: 1ch;
82
+
text-decoration: none;
83
+
color: inherit;
84
+
85
+
&:hover + * {
86
+
color: var(--color-select-variant);
87
+
text-decoration: underline;
88
+
}
89
+
}
90
+
}
91
+
92
+
a {
93
+
color: var(--color-select);
94
+
text-decoration: none;
95
+
96
+
&:hover {
97
+
text-decoration: underline;
98
+
color: var(--color-select-variant);
99
+
}
100
+
}
101
+
102
+
/* Only works in flex containers because CSS works in mysterious ways */
103
+
.icon {
104
+
height: 1em;
105
+
width: 1em;
106
+
margin: auto 0.2em;
107
+
}
108
+
109
+
.cursor::before {
110
+
content: "█";
111
+
}
112
+
113
+
#navbar {
114
+
view-transition-name: navbar;
115
+
position: fixed;
116
+
z-index: 10;
117
+
display: flex;
118
+
flex-wrap: wrap;
119
+
font-family: var(--font-mono);
120
+
row-gap: 0.25em;
121
+
bottom: 0;
122
+
right: 1ch;
123
+
left: 1ch;
124
+
padding-bottom: 0.5em;
125
+
background: var(--color-bg);
126
+
127
+
.prompt-pointed-right {
128
+
width: 3ch;
129
+
height: 100%;
130
+
div {
131
+
width: 2ch;
132
+
height: 100%;
133
+
clip-path: polygon(0 0, 50% 0, 100% 50%, 50% 100%, 0 100%);
134
+
}
135
+
}
136
+
137
+
#prompt-left .prompt-rounded-left {
138
+
background: var(--color-primary);
139
+
}
140
+
141
+
#prompt-left {
142
+
display: flex;
143
+
color: var(--color-bg);
144
+
145
+
#prompt-user {
146
+
display: flex;
147
+
background: var(--color-primary);
148
+
padding-left: 1ch;
149
+
border-radius: 999em 0 0 999em;
150
+
151
+
& + .prompt-pointed-right {
152
+
background: linear-gradient(90deg, var(--color-primary) 25%, var(--color-secondary) 0 100%);
153
+
div {
154
+
background: var(--color-primary);
155
+
}
156
+
}
157
+
}
158
+
159
+
#prompt-dir {
160
+
display: flex;
161
+
background: var(--color-secondary);
162
+
163
+
& + .prompt-pointed-right {
164
+
background: linear-gradient(90deg, var(--color-secondary) 25%, var(--color-bg) 0 100%);
165
+
div {
166
+
background: var(--color-secondary);
167
+
}
168
+
}
169
+
}
170
+
}
171
+
172
+
#prompt-right {
173
+
.icon { margin-left: 0 }
174
+
float: right;
175
+
display: flex;
176
+
177
+
--commit-bg: color-mix(in srgb, var(--color-bg) 90%, var(--color-fg) 10%);
178
+
--gleam-bg: color-mix(in srgb, var(--color-bg) 80%, var(--color-fg) 20%);
179
+
180
+
.prompt-pointed-right:first-child {
181
+
z-index: 1;
182
+
background: linear-gradient(90deg, var(--color-bg) 25%, var(--commit-bg) 0 100%);
183
+
div {
184
+
background: var(--color-bg);
185
+
}
186
+
}
187
+
188
+
#prompt-commit {
189
+
display: flex;
190
+
background: var(--commit-bg);
191
+
color: var(--color-fg-muted);
192
+
193
+
a { color: inherit }
194
+
195
+
& + .prompt-pointed-right {
196
+
background: linear-gradient(90deg, var(--commit-bg) 25%, var(--gleam-bg) 0 100%);
197
+
div {
198
+
background: var(--commit-bg);
199
+
}
200
+
}
201
+
}
202
+
203
+
#prompt-gleam {
204
+
display: flex;
205
+
background: var(--gleam-bg);
206
+
207
+
& + .prompt-pointed-right {
208
+
width: 2ch;
209
+
background: linear-gradient(90deg, var(--gleam-bg) 25%, var(--color-bg) 0 100%);
210
+
div {
211
+
background: var(--gleam-bg);
212
+
}
213
+
}
214
+
}
215
+
}
216
+
217
+
#nav {
218
+
view-transition-name: nav;
219
+
flex-grow: 100;
220
+
display: flex;
221
+
222
+
ul {
223
+
display: flex;
224
+
list-style-type: none;
225
+
margin: 0;
226
+
padding: 0;
227
+
gap: 1ch;
228
+
229
+
a {
230
+
padding: 0 1ch;
231
+
border-radius: 0.25em;
232
+
color: var(--color-fg);
233
+
234
+
&:visited {
235
+
color: inherit;
236
+
}
237
+
238
+
/*
239
+
* Pseudo background element we can animate during page transitions
240
+
*/
241
+
&.active::before {
242
+
view-transition-name: nav-active-bg;
243
+
border-radius: 0.25em;
244
+
content: "";
245
+
position: absolute;
246
+
top: 0;
247
+
left: 0;
248
+
height: 100%;
249
+
width: 100%;
250
+
z-index: 50;
251
+
background: var(--color-fg);
252
+
}
253
+
254
+
&.active {
255
+
position: relative;
256
+
color: var(--color-bg);
257
+
background: var(--color-fg);
258
+
}
259
+
260
+
span {
261
+
position: relative;
262
+
z-index: 100;
263
+
}
264
+
265
+
&:hover {
266
+
color: var(--color-bg);
267
+
background: var(--color-fg);
268
+
}
269
+
}
270
+
};
271
+
}
272
+
273
+
.cursor {
274
+
margin-right: 2ch;
275
+
}
276
+
277
+
&:hover .cursor {
278
+
animation: blink 1.5s steps(1, start) infinite;
279
+
}
280
+
281
+
#nav-chevron {
282
+
display: none;
283
+
width: 1ch;
284
+
margin-right: 1ch;
285
+
}
286
+
}
287
+
288
+
/* I wanted to avoid breakpoints, but I can't figure out a way around this one */
289
+
@media (max-width: 700px) {
290
+
main { padding-bottom: 4em }
291
+
292
+
#navbar {
293
+
#nav-chevron { display: inherit }
294
+
295
+
#prompt-left {
296
+
margin-right: -3ch;
297
+
}
298
+
299
+
#nav {
300
+
order: 3;
301
+
width: 100vw;
302
+
}
303
+
304
+
#prompt-right .prompt-pointed-right:first-child {
305
+
background: linear-gradient(90deg, var(--color-secondary) 25%, var(--commit-bg) 0 100%);
306
+
div {
307
+
background: var(--color-secondary);
308
+
}
309
+
}
310
+
}
311
+
}
312
+
313
+
@media (max-width: 380px) {
314
+
#prompt-right { display: none !important }
315
+
}
316
+
317
+
@view-transition {
318
+
navigation: auto;
319
+
}
320
+
321
+
@keyframes blink {
322
+
50% { visibility: hidden }
323
+
}
324
+
325
+
@keyframes slide-out-up {
326
+
from { transform: translateY(0) }
327
+
to { transform: translateY(min(-100vh, -100%)) }
328
+
}
329
+
330
+
@keyframes slide-in-up {
331
+
from { transform: translateY(100vh) }
332
+
to { transform: translateY(0) }
333
+
}
334
+
335
+
::view-transition-old(main-content) {
336
+
animation: 161ms ease-in both slide-out-up;
337
+
}
338
+
339
+
::view-transition-new(main-content) {
340
+
animation: 161ms ease-in both slide-in-up;
341
+
animation-delay: 161ms;
342
+
}
343
+
344
+
::view-transition-group(navbar) {
345
+
z-index: 10;
346
+
}
347
+
348
+
::view-transition-group(nav-active-bg) {
349
+
z-index: 50;
350
+
}
351
+
352
+
::view-transition-group(nav) {
353
+
z-index: 100;
354
+
}
+24
gleam.toml
+24
gleam.toml
···
1
+
name = "webbed_site"
2
+
version = "1.0.0"
3
+
4
+
# Fill out these fields if you intend to generate HTML documentation or publish
5
+
# your project to the Hex package manager.
6
+
#
7
+
# description = ""
8
+
# licences = ["Apache-2.0"]
9
+
# repository = { type = "github", user = "", repo = "" }
10
+
# links = [{ title = "Website", href = "" }]
11
+
#
12
+
# For a full reference of all the available options, you can have a look at
13
+
# https://gleam.run/writing-gleam/gleam-toml/.
14
+
15
+
[dependencies]
16
+
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
17
+
# lustre ssg has a lot of outdated deps :/
18
+
lustre_ssg = { git = "https://github.com/fruno-bulax/lustre_ssg", ref = "6c132bd34ab75a1144d31c0f896cab0e3cbf80fc" }
19
+
lustre = ">= 5.4.0 and < 6.0.0"
20
+
gleam_time = ">= 1.6.0 and < 2.0.0"
21
+
simplifile = ">= 2.3.2 and < 3.0.0"
22
+
shellout = ">= 1.7.0 and < 2.0.0"
23
+
jot = ">= 8.0.0 and < 9.0.0"
24
+
tom = ">= 2.0.0 and < 3.0.0"
+34
manifest.toml
+34
manifest.toml
···
1
+
# This file was generated by Gleam
2
+
# You typically do not need to edit this file
3
+
4
+
packages = [
5
+
{ name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" },
6
+
{ name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" },
7
+
{ name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
8
+
{ name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" },
9
+
{ name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" },
10
+
{ name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" },
11
+
{ name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" },
12
+
{ name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" },
13
+
{ name = "gleam_stdlib", version = "0.67.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6CE3E4189A8B8EC2F73AB61A2FBDE49F159D6C9C61C49E3B3082E439F260D3D0" },
14
+
{ name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" },
15
+
{ name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" },
16
+
{ name = "jot", version = "8.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "houdini", "splitter"], otp_app = "jot", source = "hex", outer_checksum = "CCE11C8904B129CC9DA3A293B645884B91C96D252183F6DBCAEFA8F2587CAEFD" },
17
+
{ name = "lustre", version = "5.4.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "40E097BABCE65FB7C460C073078611F7F5802EB07E1A9BFB5C229F71B60F8E50" },
18
+
{ name = "lustre_ssg", version = "0.12.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_regexp", "gleam_stdlib", "jot", "lustre", "simplifile", "temporary", "tom"], source = "git", repo = "https://github.com/fruno-bulax/lustre_ssg", commit = "6c132bd34ab75a1144d31c0f896cab0e3cbf80fc" },
19
+
{ name = "shellout", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "shellout", source = "hex", outer_checksum = "1BDC03438FEB97A6AF3E396F4ABEB32BECF20DF2452EC9A8C0ACEB7BDDF70B14" },
20
+
{ name = "simplifile", version = "2.3.2", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "E049B4DACD4D206D87843BCF4C775A50AE0F50A52031A2FFB40C9ED07D6EC70A" },
21
+
{ name = "splitter", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "3DFD6B6C49E61EDAF6F7B27A42054A17CFF6CA2135FF553D0CB61C234D281DD0" },
22
+
{ name = "temporary", version = "1.0.0", build_tools = ["gleam"], requirements = ["envoy", "exception", "filepath", "gleam_crypto", "gleam_stdlib", "simplifile"], otp_app = "temporary", source = "hex", outer_checksum = "51C0FEF4D72CE7CA507BD188B21C1F00695B3D5B09D7DFE38240BFD3A8E1E9B3" },
23
+
{ name = "tom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "74D0C5A3761F7A7D06994755D4D5AD854122EF8E9F9F76A3E7547606D8C77091" },
24
+
]
25
+
26
+
[requirements]
27
+
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
28
+
gleam_time = { version = ">= 1.6.0 and < 2.0.0" }
29
+
jot = { version = ">= 8.0.0 and < 9.0.0" }
30
+
lustre = { version = ">= 5.4.0 and < 6.0.0" }
31
+
lustre_ssg = { git = "https://github.com/fruno-bulax/lustre_ssg", ref = "6c132bd34ab75a1144d31c0f896cab0e3cbf80fc" }
32
+
shellout = { version = ">= 1.7.0 and < 2.0.0" }
33
+
simplifile = { version = ">= 2.3.2 and < 3.0.0" }
34
+
tom = { version = ">= 2.0.0 and < 3.0.0" }
+84
pages/dots.djot
+84
pages/dots.djot
···
1
+
# what i use
2
+
3
+
We all use software. Good software, bad software, great software, shit software.
4
+
Here's a wall of text about the software I use.
5
+
Oh, also some hardware too, I guess.
6
+
7
+
## operating system
8
+
9
+
Arch (btw). I tried to like NixOS. I really did. I tried twice! I gave up twice…
10
+
11
+
The idea of having a single configuration for your whole system is incredibly
12
+
appealing, but I never got over Nix-the-language and the sheer complexity
13
+
of it all. So back to Arch it is.
14
+
15
+
## desktop environment
16
+
17
+
I am currently quite enjoying [Niri](https://github.com/YaLTeR/niri) in combination
18
+
with the [Noctalia](https://noctalia.dev/) shell. Tiling window managers are a way
19
+
of life, and Noctalia comes with basic shell things I need. If I never have to
20
+
configure a Waybar again, it'll be too soon.
21
+
22
+
I tend to go _all in_ on color schemes. After yeeeaars on Gruvbox, I switched to
23
+
[Kanagawa](https://github.com/rebelot/kanagawa.nvim) a while ago.
24
+
My shell, terminal, editor, GTK/QT theme, _even this website_ are styled accordingly.
25
+
26
+
## terminal stuff
27
+
28
+
Yeah, I thought it was hype too, but [Ghostty](https://ghostty.org/)
29
+
is actually really nice. No complaints so far!
30
+
31
+
After a long time of using zsh at work, I decided to pick up
32
+
[Elvish](https://elv.sh/) for a few days. While it's definitely worth
33
+
checking out, I ultimately went back to [fish](https://fishshell.com/).
34
+
Dunno why I left it, it's really, really nice! Bonus points for being
35
+
one of the blessed shells that usually gets shell completions out of the box.
36
+
37
+
Like everyone else I use [Starship](https://starship.rs/) for my prompt.
38
+
I will say that configuring that did _not_ spark joy. Until native
39
+
support comes along, I'm also using the
40
+
[starship-jj](https://gitlab.com/lanastara_foss/starship-jj) module.
41
+
42
+
## browser
43
+
44
+
I finally did it. I stopped using Firefox. It was a long time coming,
45
+
but evolving into a "Modern AI Browser" finally tipped me over the edge.
46
+
I'm currently using [Helium](https://helium.computer/), a chromium-based browser.
47
+
I definitely do miss some Firefox-isms, but overall I'm pretty happy.
48
+
49
+
By the way, if you're using Firefox, you're not seeing the multi-page
50
+
view transitions on this website!
51
+
52
+
## coding
53
+
54
+
I'll probably leave this a stub because otherwise it will escalate.
55
+
56
+
After years I finally wrote a decent config from actual scratch.
57
+
No LazyVim. Not even kickstart. Just nvim nightly and the new native
58
+
package manager. I mostly use plugins from the `mini` family, as well as
59
+
`blink` for completions and `flash` for navigation.
60
+
61
+
Of course I also use LSP. LSP is great! I just wish it was good…
62
+
63
+
## multimedia
64
+
65
+
When I abandoned the last remnants of Windows I jumped from VLC to mpv
66
+
because it didn't quite work right on Wayland somehow.
67
+
The default UI is butt-ugly (and this is coming from a VLC user!),
68
+
but my nvim config skills have prepared me for also configuring mpv.
69
+
Of course I've changed the colors over to Kanagawa.
70
+
71
+
I also have a local music library (sourced from Bandcamp), but I haven't
72
+
settled on a music player yet.
73
+
74
+
## keeeeeebs
75
+
76
+
i love dem ortho split keebs. looove em!
77
+
I use a [3w6](https://github.com/weteor/3W6) at work and a
78
+
[corne](https://github.com/foostan/crkbd) at home. The extra column is really
79
+
a must for the gaming-layer.
80
+
Both layouts are heavily based on [miryoku](https://github.com/manna-harbour/miryoku),
81
+
but I'm still stuck on QWERTY.
82
+
83
+
I'd really like to try something fancier with tenting or a curved surface or something,
84
+
but for now I'm happy with what I've got.
+13
pages/index.djot
+13
pages/index.djot
···
1
+
# hello, world
2
+
3
+
hi, I'm fruno, a professional software developer and amateur
4
+
[crytpid](https://en.wikipedia.org/wiki/List_of_cryptids) based in Vienna.
5
+
I also like opossums.
6
+
7
+
At my day job I use Kotlin, but my free time is mostly spent with
8
+
[Gleam](https://gleam.run/) or Rust (usually on the Gleam compiler).
9
+
You can find me over on
10
+
[GitHub](https://github.com/fruno-bulax) or [tangled](https://tangled.org/fruno.win),
11
+
which also hosts the [source code](https://tangled.org/fruno.win/webbed-site/)
12
+
of this very site! I'm not really on any socials but you can message me on
13
+
[Bluesky](https://bsky.app/profile/fruno.win).
+7
posts/2026-01-03-initial-commit.djot
+7
posts/2026-01-03-initial-commit.djot
+30
serve💅.py
+30
serve💅.py
···
1
+
#!/usr/bin/env python3
2
+
3
+
import http.server
4
+
import socketserver
5
+
import sys
6
+
import os.path
7
+
8
+
PORT = 8080
9
+
# Be careful in public networks!
10
+
# I just wanna check the site on my phone
11
+
HOST = "0.0.0.0"
12
+
ROOT = "./dist"
13
+
14
+
class CleanUrlHandler(http.server.SimpleHTTPRequestHandler):
15
+
def __init__(self, *args, **kwargs):
16
+
super().__init__(*args, directory=ROOT, **kwargs)
17
+
18
+
def do_GET(self):
19
+
# This is a very shitty check, but it's good enough
20
+
if not '.' in self.path and not os.path.isdir(self.translate_path(self.path)):
21
+
self.path += '.html'
22
+
23
+
super().do_GET()
24
+
25
+
with socketserver.TCPServer((HOST, PORT), CleanUrlHandler) as http:
26
+
print(f"Serving 💅 at {HOST}:{PORT}")
27
+
try:
28
+
http.serve_forever()
29
+
except KeyboardInterrupt:
30
+
sys.exit(0)
+50
src/blog.gleam
+50
src/blog.gleam
···
1
+
import component
2
+
import gleam/list
3
+
import gleam/string
4
+
import lustre/attribute
5
+
import lustre/element.{type Element}
6
+
import lustre/element/html
7
+
import lustre/ssg/djot
8
+
import simplifile
9
+
import tom
10
+
11
+
pub type Post {
12
+
Post(slug: String, title: String, content: String)
13
+
}
14
+
15
+
const posts_dir = "./posts"
16
+
17
+
pub fn posts() -> List(Post) {
18
+
let assert Ok(files) = simplifile.read_directory(posts_dir)
19
+
as "Failed to read posts directory"
20
+
21
+
files
22
+
|> list.sort(string.compare)
23
+
|> list.map(read_post)
24
+
}
25
+
26
+
fn read_post(filename: String) {
27
+
let assert [slug, "djot"] = string.split(filename, ".")
28
+
as "Unexpected post file type"
29
+
30
+
let assert Ok(content) = simplifile.read(posts_dir <> "/" <> filename)
31
+
as "Failed to read file"
32
+
let assert Ok(meta) = djot.metadata(content) as "Failed to read post metadata"
33
+
let assert Ok(title) = tom.get_string(meta, ["title"]) as "Missing post title"
34
+
35
+
Post(slug, title, content)
36
+
}
37
+
38
+
pub fn list_posts(posts: List(Post)) -> List(Element(msg)) {
39
+
[
40
+
component.header(1, "blog", [], [html.text("blog")]),
41
+
html.ul([], list.map(posts, post_list_item)),
42
+
]
43
+
}
44
+
45
+
fn post_list_item(post: Post) -> Element(msg) {
46
+
html.a([attribute.href("/blog/" <> post.slug)], [
47
+
html.text(post.slug),
48
+
html.text(".djot"),
49
+
])
50
+
}
+31
src/component.gleam
+31
src/component.gleam
···
1
+
import gleam/int
2
+
import gleam/string
3
+
import lustre/attribute.{type Attribute}
4
+
import lustre/element.{type Element}
5
+
import lustre/element/html
6
+
7
+
pub fn header(
8
+
level: Int,
9
+
id: String,
10
+
attrs: List(Attribute(msg)),
11
+
content: List(Element(msg)),
12
+
) -> Element(msg) {
13
+
let h = case level {
14
+
1 -> html.h1
15
+
2 -> html.h2
16
+
3 -> html.h3
17
+
4 -> html.h4
18
+
5 -> html.h5
19
+
6 -> html.h6
20
+
_ -> html.p
21
+
}
22
+
23
+
let anchor =
24
+
html.a([attribute.href("#" <> id), attribute.class("header-anchor")], [
25
+
html.text(string.repeat("#", int.min(6, level))),
26
+
])
27
+
28
+
html.div([attribute.class("heading")], [
29
+
h([attribute.id(id), ..attrs], [anchor, html.span([], content)]),
30
+
])
31
+
}
+26
src/meta.gleam
+26
src/meta.gleam
···
1
+
import gleam/result
2
+
import shellout
3
+
4
+
pub type SiteMeta {
5
+
SiteMeta(commit_hash: String, gleam_version: String)
6
+
}
7
+
8
+
pub fn fetch() -> Result(SiteMeta, String) {
9
+
use commit_hash <- result.try(commit_hash())
10
+
use gleam_version <- result.map(gleam_version())
11
+
SiteMeta(commit_hash:, gleam_version:)
12
+
}
13
+
14
+
fn commit_hash() -> Result(String, String) {
15
+
shellout.command(run: "git", with: ["rev-parse", "HEAD"], in: ".", opt: [])
16
+
|> result.map_error(fn(error) { "Failed to fetch commit hash: " <> error.1 })
17
+
}
18
+
19
+
fn gleam_version() -> Result(String, String) {
20
+
let result = shellout.command("gleam", ["--version"], ".", [])
21
+
case result {
22
+
Ok("gleam " <> version) -> Ok(version)
23
+
Ok(_) -> Error("gleam --version returned unexpected result")
24
+
Error(error) -> Error("Failed to fetch gleam version: " <> error.1)
25
+
}
26
+
}
+244
src/page.gleam
+244
src/page.gleam
···
1
+
import blog
2
+
import component
3
+
import gleam/dict
4
+
import gleam/list
5
+
import gleam/option.{None, Some}
6
+
import gleam/string
7
+
import jot
8
+
import lustre/attribute.{attribute}
9
+
import lustre/element.{type Element}
10
+
import lustre/element/html
11
+
import lustre/ssg/djot
12
+
import meta.{type SiteMeta}
13
+
import simplifile
14
+
15
+
pub type Page {
16
+
Index
17
+
Blog
18
+
Dots
19
+
20
+
BlogPost(blog.Post)
21
+
}
22
+
23
+
pub fn route(page: Page) {
24
+
case page {
25
+
Index -> "/"
26
+
Blog -> "/blog/"
27
+
Dots -> "/dots"
28
+
BlogPost(post) -> "/blog/" <> post.slug
29
+
}
30
+
}
31
+
32
+
pub fn title(page: Page) {
33
+
case page {
34
+
Index -> "/index"
35
+
Blog -> "/blog/"
36
+
Dots -> "/.dots"
37
+
BlogPost(post) -> post.title
38
+
}
39
+
}
40
+
41
+
pub type SiteInfo {
42
+
SiteInfo(posts: List(blog.Post), meta: SiteMeta)
43
+
}
44
+
45
+
pub fn render(page: Page, info: SiteInfo) -> Element(msg) {
46
+
html.html([attribute.lang("en")], [
47
+
html.head([], [
48
+
html.meta([attribute.charset("utf-8")]),
49
+
html.meta([
50
+
attribute.name("viewport"),
51
+
attribute.content("width=device-width, initial-scale=1.0"),
52
+
]),
53
+
html.title([], "fruno | " <> title(page)),
54
+
preload_font("InclusiveSans"),
55
+
preload_font("InclusiveSans-Italic"),
56
+
preload_font("Myna"),
57
+
html.link([
58
+
attribute.rel("stylesheet"),
59
+
attribute.href("/style.css"),
60
+
]),
61
+
]),
62
+
html.body([], [navbar(page, info.meta), html.main([], content(page, info))]),
63
+
])
64
+
}
65
+
66
+
const fonts_dir = "/fonts/"
67
+
68
+
fn preload_font(font: String) {
69
+
html.link([
70
+
attribute.rel("preload"),
71
+
attribute.href(fonts_dir <> font <> ".woff2"),
72
+
attribute.as_("font"),
73
+
attribute.type_("font/woff2"),
74
+
attribute.crossorigin("anonymous"),
75
+
])
76
+
}
77
+
78
+
const pages_dir = "pages/"
79
+
80
+
fn content(page: Page, info: SiteInfo) -> List(Element(msg)) {
81
+
let read_page = fn(file) {
82
+
let assert Ok(content) = simplifile.read(pages_dir <> file)
83
+
as "Failed to read djot file"
84
+
content
85
+
}
86
+
87
+
case page {
88
+
Index -> "index.djot" |> read_page |> djot.render(renderer())
89
+
Dots -> "dots.djot" |> read_page |> djot.render(renderer())
90
+
91
+
Blog -> blog.list_posts(info.posts)
92
+
BlogPost(post) -> djot.render(post.content, renderer())
93
+
}
94
+
}
95
+
96
+
fn navbar(page: Page, meta: SiteMeta) -> Element(msg) {
97
+
html.nav([attribute.id("navbar")], [
98
+
prompt_left(),
99
+
nav(page),
100
+
prompt_right(meta),
101
+
])
102
+
}
103
+
104
+
fn prompt_left() -> Element(msg) {
105
+
html.div([attribute.id("prompt-left")], [
106
+
html.div([attribute.id("prompt-user")], [
107
+
html.text("fruno"),
108
+
]),
109
+
html.div([attribute.class("prompt-pointed-right")], [html.div([], [])]),
110
+
html.div([attribute.id("prompt-dir")], [
111
+
html.text("~/webbed_site"),
112
+
]),
113
+
html.div([attribute.class("prompt-pointed-right")], [html.div([], [])]),
114
+
])
115
+
}
116
+
117
+
fn nav(page: Page) -> Element(msg) {
118
+
html.div([attribute.id("nav")], [
119
+
html.span([attribute.id("nav-chevron")], [html.text(">")]),
120
+
html.span([], [html.text("/")]),
121
+
html.span([attribute.class("cursor")], []),
122
+
html.ul([], [
123
+
nav_link(Index, page),
124
+
nav_link(Blog, page),
125
+
nav_link(Dots, page),
126
+
]),
127
+
])
128
+
}
129
+
130
+
fn nav_link(to: Page, active: Page) {
131
+
let is_active = case to, active {
132
+
BlogPost(_), BlogPost(_) | BlogPost(_), Blog -> True
133
+
_, _ if to == active -> True
134
+
_, _ -> False
135
+
}
136
+
html.li([], [
137
+
html.a(
138
+
[
139
+
attribute.href(route(to)),
140
+
attribute.classes([#("active", is_active)]),
141
+
],
142
+
[html.span([], [html.text(title(to))])],
143
+
),
144
+
])
145
+
}
146
+
147
+
fn prompt_right(meta: SiteMeta) -> Element(msg) {
148
+
html.div([attribute.id("prompt-right")], [
149
+
html.div([attribute.class("prompt-pointed-right")], [html.div([], [])]),
150
+
html.div([attribute.id("prompt-commit")], [
151
+
html.a(
152
+
[
153
+
attribute.href(
154
+
"https://tangled.org/fruno.win/webbed-site/commit/"
155
+
<> meta.commit_hash,
156
+
),
157
+
],
158
+
[html.text(string.slice(meta.commit_hash, 0, 4))],
159
+
),
160
+
]),
161
+
html.div([attribute.class("prompt-pointed-right")], [html.div([], [])]),
162
+
html.div([attribute.id("prompt-gleam")], [
163
+
html.img([
164
+
attribute.class("icon"),
165
+
attribute.alt("Lucy, the mascot of the gleam programming language"),
166
+
attribute.src("/img/gleam.svg"),
167
+
]),
168
+
html.text(meta.gleam_version),
169
+
]),
170
+
html.div([attribute.class("prompt-pointed-right")], [html.div([], [])]),
171
+
])
172
+
}
173
+
174
+
pub fn renderer() -> djot.Renderer(Element(msg)) {
175
+
let to_attributes = fn(attrs) {
176
+
use attrs, key, val <- dict.fold(attrs, [])
177
+
[attribute(key, val), ..attrs]
178
+
}
179
+
180
+
djot.Renderer(
181
+
codeblock: fn(attrs, lang, code) {
182
+
let lang = option.unwrap(lang, "text")
183
+
html.pre(to_attributes(attrs), [
184
+
html.code([attribute("data-lang", lang)], [html.text(code)]),
185
+
])
186
+
},
187
+
emphasis: fn(content) { html.em([], content) },
188
+
heading: fn(attrs, level, content) {
189
+
let assert Ok(id) = attrs |> dict.get("id") as "Missing id in header"
190
+
let attrs = to_attributes(attrs)
191
+
component.header(level, id, attrs, content)
192
+
},
193
+
link: fn(destination, attributes, content) {
194
+
let attributes = to_attributes(attributes)
195
+
196
+
case destination {
197
+
None -> html.span(attributes, content)
198
+
Some(url) -> html.a([attribute.href(url), ..attributes], content)
199
+
}
200
+
},
201
+
paragraph: fn(attrs, content) { html.p(to_attributes(attrs), content) },
202
+
bullet_list: fn(layout, _style, items) {
203
+
html.ul([], {
204
+
list.map(items, fn(item) {
205
+
case layout {
206
+
jot.Tight -> html.li([], item)
207
+
jot.Loose -> html.li([], [html.p([], item)])
208
+
}
209
+
})
210
+
})
211
+
},
212
+
raw_html: fn(content) { element.unsafe_raw_html("", "div", [], content) },
213
+
strong: fn(content) { html.strong([], content) },
214
+
text: fn(text) { html.text(text) },
215
+
code: fn(content) { html.code([], [html.text(content)]) },
216
+
image: fn(destination, attributes, alt) {
217
+
let attributes = to_attributes(attributes)
218
+
case destination {
219
+
None -> html.span(attributes, [html.text(alt)])
220
+
Some(url) ->
221
+
html.img([attribute.href(url), attribute.alt(alt), ..attributes])
222
+
}
223
+
},
224
+
linebreak: html.br([]),
225
+
thematicbreak: html.hr([]),
226
+
inline_math: fn(math) {
227
+
html.span([attribute.class("math inline")], [
228
+
html.text("\\(" <> math <> "\\)"),
229
+
])
230
+
},
231
+
display_math: fn(math) {
232
+
html.span([attribute.class("math display")], [
233
+
html.text("\\[" <> math <> "\\]"),
234
+
])
235
+
},
236
+
blockquote: fn(attrs, content) {
237
+
html.blockquote(to_attributes(attrs), content)
238
+
},
239
+
span: fn(attrs, content) {
240
+
html.span(to_attributes(attrs), [html.text(content)])
241
+
},
242
+
div: fn(attrs, content) { html.div(to_attributes(attrs), content) },
243
+
)
244
+
}
+34
src/webbed_site.gleam
+34
src/webbed_site.gleam
···
1
+
import blog
2
+
import gleam/dict
3
+
import gleam/list
4
+
import lustre/ssg
5
+
import meta
6
+
import page
7
+
8
+
pub fn main() {
9
+
let assert Ok(meta) = meta.fetch() as "Failed to fetch site meta"
10
+
let posts = blog.posts()
11
+
let info = page.SiteInfo(posts:, meta:)
12
+
13
+
let site =
14
+
ssg.new("./dist")
15
+
|> ssg.add_static_route(
16
+
page.route(page.Index),
17
+
page.render(page.Index, info),
18
+
)
19
+
// Special path here because wisp (the AT-Proto hosting platform)
20
+
// likes them that way
21
+
|> ssg.add_static_route("/blog/index", page.render(page.Blog, info))
22
+
|> ssg.add_static_route(page.route(page.Dots), page.render(page.Dots, info))
23
+
|> ssg.add_dynamic_route(
24
+
"/blog/",
25
+
posts
26
+
|> list.map(fn(post) { #(post.slug, page.BlogPost(post)) })
27
+
|> dict.from_list(),
28
+
page.render(_, info),
29
+
)
30
+
|> ssg.add_static_dir("./assets")
31
+
|> ssg.build
32
+
33
+
let assert Ok(_) = site as "Build failed"
34
+
}