Terminal-themed markdown diary

Single-user diary served as one Rust binary with the Svelte frontend
embedded.

- axum 0.8 + SQLite (sqlx), migrations run at startup
- stateless HMAC session cookie; credentials from .env, compared in
  constant time
- FTS5 full-text search, user input rewritten into quoted prefix terms
- media uploads linked to entries by scanning bodies for /api/media/<id>,
  which scopes share-token media access and entry deletion
- unguessable share links (192-bit token) serving a public read-only page
- vim keybindings via CodeMirror plus a modal browse layer and : command
  line; disabled on touch devices
This commit is contained in:
nvrl committed 2026-09-05 05:39:25 +02:00
commit 4002a35305
41 files changed
+6195

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
<script lang="ts">
import { api } from './api'
import { hooks } from './commands'
import { embedSnippet } from './markdown'
import { diary } from './store.svelte'
import { formatBytes, formatStamp } from './util'
async function remove(id: string, filename: string) {
if (!confirm(`delete ${filename}? entries that embed it will break.`)) return
await diary.guard(async () => {
await api.deleteMedia(id)
await diary.loadMedia()
})
}
</script>
<div class="sheet">
<header>
<span class="accent">:media</span>
<span class="faint">{diary.media.length} files</span>
<button onclick={() => (diary.overlay = 'none')}>esc</button>
</header>
<ul>
{#each diary.media as file (file.id)}
<li>
<div class="thumb">
{#if file.mime.startsWith('image/')}
<img src={file.url} alt={file.filename} loading="lazy" />
{:else}
<span class="faint">{file.mime.split('/')[0]}</span>
{/if}
</div>
<div class="info">
<span class="name">{file.filename}</span>
<span class="faint">
{formatBytes(file.size)} · {formatStamp(file.created_at)} ·
{file.entry_id ? `entry:${file.entry_id}` : 'unattached'}
</span>
</div>
<div class="actions">
{#if diary.editing}
<button
onclick={() => {
hooks.insertText(embedSnippet(file))
diary.overlay = 'none'
}}>insert</button
>
{/if}
<button onclick={() => diary.copy(location.origin + file.url)}>copy url</button>
<button onclick={() => remove(file.id, file.filename)}>rm</button>
</div>
</li>
{:else}
<li class="faint">nothing uploaded yet — attach a file from the editor.</li>
{/each}
</ul>
</div>
<style>
.sheet {
display: flex;
flex-direction: column;
gap: 10px;
width: min(100%, 90ch);
max-height: 100%;
background: var(--bg-alt);
border: 1px solid var(--accent-dim);
border-radius: var(--radius);
padding: 14px 16px;
overflow: hidden;
}
header { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
ul { list-style: none; margin: 0; padding: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
li { display: flex; align-items: center; gap: 12px; padding: 4px; border-bottom: 1px solid var(--line); }
.thumb {
width: 48px;
height: 36px;
flex: none;
display: grid;
place-items: center;
background: var(--bg);
border: 1px solid var(--line);
overflow: hidden;
font-size: 10px;
}
.thumb img { width: 100%; height: 100%; object-fit: cover; }
.info { display: flex; flex-direction: column; min-width: 0; flex: 1; font-size: 12px; }
.name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.actions { display: flex; gap: 4px; flex: none; }
</style>