-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwelcome.js
More file actions
436 lines (408 loc) · 21.3 KB
/
Copy pathwelcome.js
File metadata and controls
436 lines (408 loc) · 21.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
// When there's no opened local dir, a temporary FS is provided.
// Temporary FS includes welcome files, so to demonstrate the app.
// First we try to create OPFS storage, fallback to our own in-memory FS on failure.
async function getTemporaryStorageDirHandle() {
// Safari ships OPFS but its FileSystemFileHandle exposes only
// createSyncAccessHandle (worker-only). Older Chromium has no
// FileSystemFileHandle.remove(). Fall back to the in-memory FS if
// either of those write/delete entry points is missing so app code
// doesn't blow up mid-flow.
if (!opfsIsFullyUsable()) {
console.warn('OPFS missing createWritable or remove, using in-memory FS');
isMemFS = true;
return getMemFSRoot();
}
// OPFS requires a secure context (https or localhost), not available on file://
try {
const root = await navigator.storage.getDirectory();
// Skip the seed only when the LAST welcome file is already on disk.
// Using "any entries" as the seeded marker is racy: a reader
// arriving mid-seed sees the first dir already written and returns
// root with partial contents. Probing for a file that's written
// last means concurrent callers all re-run the seed (idempotent
// via {create:true}) and none of them returns until all welcome
// files exist.
const lastFile = Object.keys(WELCOME_FILES).pop();
let seeded = true;
try { await root.getFileHandle(lastFile); }
catch { seeded = false; }
if (seeded) {
return root;
}
// If a welcome file was archived (moved to /archive/), don't
// re-seed it. Archive flattens names, so a Set of names covers
// both root-level and nested welcome files.
const archived = new Set();
try {
const archiveDir = await root.getDirectoryHandle('archive');
for await (const entry of archiveDir.values()) {
if (entry.kind === 'file') archived.add(entry.name);
}
} catch { /* no archive dir yet */ }
async function createFiles(obj, dirHandle) {
for (const [name, data] of Object.entries(obj)) {
if (data.isFile) {
if (archived.has(name)) continue;
const fileHandle = await dirHandle.getFileHandle(name, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(data.content);
await writable.close();
} else {
const subDirHandle = await dirHandle.getDirectoryHandle(removeTrailingSlash(name), { create: true });
await createFiles(data, subDirHandle);
}
}
}
await createFiles(WELCOME_FILES, root);
// Retouch My project.md file, so it appears in chat's quick buttons for better demo.
if (!archived.has('My Project.md')) {
await new Promise(r => setTimeout(r, 10));
const fh = await root.getFileHandle('My Project.md', { create: true });
const w = await fh.createWritable();
await w.write(WELCOME_FILES['My Project.md'].content);
await w.close();
}
return root;
} catch (e) {
console.warn('OPFS unavailable, using in-memory FS:', e.message);
isMemFS = true;
return getMemFSRoot();
}
}
// Returns true only when the browser exposes every FileSystemFileHandle
// method the app relies on. Used by both the temporary-storage path and
// app.js's getRootDirHandle to decide between OPFS and the in-memory FS.
function opfsIsFullyUsable() {
if (typeof FileSystemFileHandle === 'undefined') return false;
const proto = FileSystemFileHandle.prototype;
return typeof proto.createWritable === 'function'
&& typeof proto.remove === 'function';
}
let memFSRoot = null;
function getMemFSRoot() {
if (memFSRoot) return memFSRoot;
memFSRoot = new MemDir('');
function populate(obj, parent) {
for (const [name, data] of Object.entries(obj)) {
if (data.isFile) {
const file = new MemFile(name, data.content || '');
file.parent = parent;
parent.entries[name] = file;
} else {
const dir = new MemDir(removeTrailingSlash(name));
parent.entries[dir.name] = dir;
populate(data, dir);
}
}
}
populate(WELCOME_FILES, memFSRoot);
return memFSRoot;
}
class MemFile {
constructor(name, content = '') {
this.kind = 'file';
this.name = name;
this.content = content;
// We want "My project.md" to appear in chat's quick buttons, for better demo.
this.lastModified = Date.now() + (name === 'My Project.md' ? 1 : 0);
this.parent = null;
}
async getFile() {
const content = this.content;
return {
name: this.name,
lastModified: this.lastModified,
size: new Blob([content]).size,
type: '',
text: async () => content,
arrayBuffer: async () => new TextEncoder().encode(content).buffer,
};
}
async createWritable(opts = {}) {
let buffer = opts.keepExistingData ? this.content : '';
let pos = opts.keepExistingData ? buffer.length : 0;
const self = this;
return {
async write(data) {
const text = typeof data === 'string' ? data : await new Blob([data]).text();
buffer = buffer.slice(0, pos) + text + buffer.slice(pos + text.length);
pos += text.length;
},
async seek(offset) { pos = offset; },
async close() {
self.content = buffer;
self.lastModified = Date.now();
},
};
}
async remove() {
if (this.parent) delete this.parent.entries[this.name];
}
}
class MemDir {
constructor(name) {
this.kind = 'directory';
this.name = name;
this.entries = {};
}
async getDirectoryHandle(name, opts = {}) {
if (!this.entries[name]) {
if (!opts.create) throw new DOMException(`"${name}" not found`, 'NotFoundError');
this.entries[name] = new MemDir(name);
}
return this.entries[name];
}
async getFileHandle(name, opts = {}) {
if (!this.entries[name]) {
if (!opts.create) throw new DOMException(`"${name}" not found`, 'NotFoundError');
const file = new MemFile(name);
file.parent = this;
this.entries[name] = file;
}
return this.entries[name];
}
// Mirrors FileSystemDirectoryHandle.removeEntry. Required by fs.js's
// remove(path) and removeDir(dirPath); the non-standard
// fileHandle.remove() isn't available on Safari OPFS or this in-memory
// FS, so fs.js prefers the parent-directory form.
async removeEntry(name, opts = {}) {
const entry = this.entries[name];
if (!entry) {
throw new DOMException(`"${name}" not found`, 'NotFoundError');
}
if (entry.kind === 'directory' && Object.keys(entry.entries).length > 0 && !opts.recursive) {
throw new DOMException('Directory not empty', 'InvalidModificationError');
}
delete this.entries[name];
}
async *values() {
for (const entry of Object.values(this.entries)) yield entry;
}
}
// When a user starts in the temporary FS and later opens a local folder,
// copy their in-mem files to local fs.
async function moveUserFiles(targetRoot) {
const welcome = new Set(['Help.md']);
(function collect(obj, prefix) {
for (const [name, data] of Object.entries(obj)) {
if (data.isFile) {
welcome.add(prefix + name);
// Archive flattens names, archived welcome files keep theirs
welcome.add('archive/' + name);
} else {
collect(data, prefix + removeTrailingSlash(name) + '/');
}
}
})(WELCOME_FILES, '');
const tempRoot = await getTemporaryStorageDirHandle();
async function moveDir(srcDir, getDestDir, prefix) {
const entries = [];
for await (const entry of srcDir.values()) entries.push(entry);
let destDir = null;
const ensureDest = async () => destDir || (destDir = await getDestDir());
for (const entry of entries) {
const relPath = prefix + entry.name;
if (entry.kind === 'directory') {
await moveDir(
entry,
async () => (await ensureDest()).getDirectoryHandle(entry.name, { create: true }),
relPath + '/',
);
continue;
}
if (welcome.has(relPath)) continue;
const dest = await ensureDest();
let exists = true;
try { await dest.getFileHandle(entry.name); } catch { exists = false; }
if (exists) continue;
const content = await (await entry.getFile()).arrayBuffer();
const fileHandle = await dest.getFileHandle(entry.name, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(content);
await writable.close();
await srcDir.removeEntry(entry.name);
}
}
await moveDir(tempRoot, async () => targetRoot, '');
}
function prefetchWelcomeImages() {
const urls = ['img/slipbox.webp', 'img/tomas_sanchez.jpg'];
for (const url of urls) {
const link = document.createElement('link');
link.rel = 'prefetch';
link.as = 'image';
link.href = url;
document.head.appendChild(link);
}
}
const WELCOME_FILES = {
"brain/": {
"We think that we understand, but in reality we just know.md": {
"content": "Reading and rereading can easily fool us into believing that we understand a text. Rereading is especially dangerous because of the mere-exposure effect: The moment we become familiar with something, we start believing we also understand it. On top of that, we also tend to like it it more.\n\n[Brain is the most complex object in known universe](/brain/Brain%20is%20the%20most%20complex%20object%20in%20known%20universe.md)\n[Zettelkasten](/brain/Zettelkasten.md)",
isFile: true,
},
"Brain is the most complex object in known universe.md": {
"content": "Nothing will make you appreciate human intelligence like learning about how unbelievably challenging it is to try to create a computer as smart as we are. Building skyscrapers, putting humans in space, figuring out the details of how the Big Bang went down - all far easier than understanding our own brain or how to make something as cool as it\n\n[We think that we understand, but in reality we just know](/brain/We%20think%20that%20we%20understand,%20but%20in%20reality%20we%20just%20know.md)\n[Zettelkasten](/brain/Zettelkasten.md)",
isFile: true,
},
"Change your environment instead of using willpower.md": {
"content": "When scientists analyze people who appear to have tremendous self-control, it turns out those individuals aren’t all that different from those who are struggling. Instead, “disciplined” people are better at structuring their lives in a way that does not require heroic willpower and self-control.\n\nIt is the way Luhmann and his slipbox worked together that allowed him to move freely and flexibly between different tasks and levels of thinking. It is about having the right tools and knowing how to use them - and very few understand that you need both.\n\n" +
"Luhmann was able to focus on the important things right in front of him, pick up quickly where he left off and stay in control of the process because the structure of his work allowed him to do this.\n\n" +
"[Zettelkasten](/brain/Zettelkasten.md)",
isFile: true,
},
"Zettelkasten.md": {
"content":
"Zettelkasten, slipbox, or card file - a collection of small items of information stored on paper slips, linked to each other through subject headings or links. It has often been used as a system of note-taking and personal knowledge management for research, study, and writing.\n\n" +
"Niklas Luhmann described his slipbox as a \"competent communication partner\", a \"second memory\" he could communicate with.\n\n" +
"\n\n" +
"## Main Zettelkasten principles\n" +
"#### The principle of atomicity\n" +
"Each note should contain one idea and one idea only. This makes it possible to link ideas with a laser focus.\n\n" +
"#### The principle of autonomy\n" +
"Each note should be autonomous, meaning it should be self-contained and comprehensible on its own. This allows notes to be moved, processed, separated, and concatenated independently of its neighbors. It also ensures that notes remain useful even if the original source of information disappears.\n\n" +
"#### Always link your notes\n" +
"Whenever you add a note, make sure to link it to already existing notes. Avoid notes that are disconnected from other notes. As Luhmann himself put it, \"each note is just an element that derives its quality from the network of links in the system. A note that is not connected to the network will be lost, will be forgotten by the Zettelkasten\".\n\n" +
"**Files.md** has just enough features for you to grow your **Zettelkasten**. 🌱\n\n" +
"[Change your environment instead of using willpower](/brain/Change%20your%20environment%20instead%20of%20using%20willpower.md)\n" +
"[We think that we understand, but in reality we just know](/brain/We%20think%20that%20we%20understand,%20but%20in%20reality%20we%20just%20know.md)\n" +
"[Links](/Links.md)",
isFile: true,
},
},
"happiness/": {
"Abundant meditation.md": {
"content": "Once you are relaxed, picture yourself living in an abundant world.\nIn this abundant world, there are no restraints or limitations.\nGood things flow past you continuously.\nImagine every abundant thing you have ever desired - car, home, friends, love, joy, wealth, success, peace of mind, challenge.\nVisualize yourself living your life surrounded by this abundance.\n\n\n\nThe deeper meaning behind abundance visualization isn't necessarily about accumulating material possessions like money or cars.\n\nTrue abundance thinking is more about recognizing that in a world without artificial limitations, we would understand that our value and fulfillment don't come from external possessions. Instead, it's about cultivating an internal sense of “enough” and recognizing the richness that already exists in our lives.\n\n// If you had abundance without limitations, what would you do?\n\n// What stops you from doing it now?\n\n[Boredom is just an emotion](/happiness/Boredom%20is%20just%20an%20emotion.md)",
isFile: true,
},
"Boredom is just an emotion.md": {
"content": "It's not an indicator that you're doing something wrong in your life.\n\nBefore we had phones and technologies, we would just sit around the fire and we would talk. We wouldn't call that boring, that was just life.\n\n\n\nAnd now we have that endless need for entertainment. When nothing is happening, we think it's wrong and we need to fix it.\n\nNon eventfulness is just a part of our life, and you can embrace it as peace, or you can frantically try to create more chaos.\n\n[Abundant meditation](/happiness/Abundant meditation.md)",
isFile: true,
},
},
"🪴 Welcome.md": {
"content":
"To store files locally, [open or create a folder](cmd:openDir).\n\n" +
"Use [chat](cmd:openChat) to dump whatever is on your mind.\n\n" +
"Press `Cmd+K` or `Ctrl+K` to quick switch between files.\n\n" +
"[Markdown Guide](/Markdown%20Guide.md)\n[Zettelkasten](/brain/Zettelkasten.md)\n[Hotkeys](/Hotkeys.md)\n[Links](/Links.md)",
isFile: true,
},
"Links.md": {
"content": "Links are important.\n" +
"\n" +
"Relations among ideas are far more important than the ideas themselves.\n" +
"Learning is making meaningful connections.\n\n" +
"Type `[` to insert a new link.\n\n" +
"[Markdown Guide](/Markdown%20Guide.md)\n" +
"[Zettelkasten](/brain/Zettelkasten.md)",
isFile: true,
},
"Markdown Guide.md": {
"content":
"Create headers with `# header`.\nAdd more # symbols for smaller headers: `## smaller header`.\n" +
"\n" +
"## Text Formatting\n" +
"- **Bold text** using `**bold**` **(Cmd/Ctrl + B)**\n" +
"- *Italic text* using `*italic*` **(Cmd/Ctrl + I)**\n" +
"- ***Bold and italic*** using `***text***`\n" +
"- ~~Strikethrough~~ using `~~text~~`\n" +
"- `Inline code` using backticks\n" +
"\n" +
"## Link\n" +
"You can insert your own links by typing `[`.\n" +
"\n" +
"## List\n" +
"- First item\n" +
"- Second item\n" +
" - Third item\n\n" +
"1. First item\n" +
"2. Second item\n" +
" 1. Third item\n\n" +
"## Checklist\n" +
"- [x] Completed task\n" +
"- [ ] Incomplete task\n\n" +
"Syntax:\n`- [ ] Item`\n" +
"\n" +
"## Table\n" +
"| Action | Hotkey |\n" +
"| --- | --- |\n" +
"| Insert table | `Cmd/Ctrl + T` |\n" +
"| Insert checkbox | `Cmd/Ctrl + Y` |\n\n" +
"## Image\n" +
"\n" +
"\n" +
"*You can paste your own images via `Cmd/Ctrl + V`*\n\n" +
"## Blockquote\n" +
">This is a blockquote. It can span multiple lines and is great for highlighting important information or quotes from other sources.\n" +
"\nSyntax:\n`> This is a blockquote`\n" +
"\n" +
"## Code Block\n" +
"```\n" +
"Here is some code.\n" +
"```\n" +
"\n" +
"## Math\n" +
"$\\LaTeX$ is fully supported: $e^{i\\pi} + 1 = 0$\n" +
"\n" +
"[Links](/Links.md)\n" +
"[My Project](/My%20Project.md)",
isFile: true,
},
"Hotkeys.md": {
"content":
"| Hotkey | Action |\n" +
"| -------- | -------- |\n" +
"| `[` | Insert a link to a file |\n" +
"| `Cmd+K` / `Ctrl+K` | Open file search modal |\n" +
"| `Cmd+N` / `Ctrl+N` | New file |\n" +
"| `Cmd+M` / `Ctrl+M` | Move file |\n" +
"| `Cmd+D` / `Ctrl+D` | Delete file |\n" +
"| `Cmd+Enter` / `Ctrl+Enter` | Open chat |\n" +
"| `Cmd+Shift+Enter` / `Ctrl+Shift+Enter` | Toggle chat dialog |\n" +
"| `Cmd+[` / `Ctrl+[` | Go to previous file |\n" +
"| `Cmd+]` / `Ctrl+]` | Go to next file |\n" +
"| `Cmd+~` / `Ctrl+~` | Toggle sidebar |\n" +
"| `Cmd+B` / `Ctrl+B` | Toggle **bold** formatting |\n" +
"| `Cmd+I` / `Ctrl+I` | Toggle *italic* formatting |\n" +
"| `Cmd+Y` / `Ctrl+Y` | Insert checkbox |\n" +
"| `Cmd+T` / `Ctrl+T` | Insert table |\n" +
"| `Cmd+Shift+E` / `Ctrl+Shift+E` | Encrypt selected text |\n" +
"| `Cmd` / `Ctrl` + `Click` | Copy from `code` element |\n" +
"| `Cmd` / `Ctrl` + `Click` | Open a link |\n" +
"| `Ctrl` + `Cmd` + `Space` | Insert emoji (MacOS) |\n" +
"\n" +
"[Markdown Guide](/Markdown%20Guide.md)",
isFile: true,
},
"My Project.md": {
"content":
"You can dump project related thoughts here.\n" +
"\n" +
"[Links](/Links.md)",
isFile: true,
},
}
function getHelpContent() {
// Concatenate Hotkeys and Markdown Guide into one Help.md. Drop any
// line that is solely a `[text](file.md)` link - those references
// are dead-ends once the welcome files are merged into one
// document, and the user wants them gone (not just the syntax).
// Anchored to line start so a stray "[" inside an inline-code
// table cell (e.g. ``| `[` |`` in the Hotkeys table) can't be
// mistaken for a link. Then collapse the blank gaps the removal
// leaves behind so we don't end up with three+ newlines in a row.
const stripMdFileLinks = s => s
.replace(/^[ \t]*\[[^\]\n]+\]\([^)\n]*\.md\)[ \t]*\n?/gm, '')
.replace(/\n{3,}/g, '\n\n');
// Drop the "#### Image" chapter - it points at an external image
// and isn't useful in the merged Help.md. Match runs until the
// next #### header or end of string.
const stripImagesChapter = s => s.replace(/#### Image\n[\s\S]*?(?=#### |$)/g, '');
return stripImagesChapter(stripMdFileLinks(
WELCOME_FILES["Hotkeys.md"].content +
"\n\n" +
"## Markdown Guide\n\n" +
WELCOME_FILES["Markdown Guide.md"].content
));
}