Lesson 3 of 6
Markdown notes
So far your apps only used their private folder. Now you'll work with files people can see and use elsewhere: real .md files in their Documents folder.
What you'll build
You'll need
You'll learn
- When to use a shared folder instead of the private one
- How to ask for access to one folder
- How to list, create, rename and delete files
- How to refresh when files change somewhere else
Private files or shared files?
| Private folder | Shared folder (e.g. Documents) | |
|---|---|---|
| Who can see it | Only your app | The person, and other apps they allow |
| Permission | "read": [], "write": [] | "read": ["home/Documents/Notes"] |
| Install prompt | None | People approve the folder when installing |
| Good for | Settings, caches, app data | Documents people own: notes, photos, exports |
Notes belong to the person, not the app, so they go in Documents. Ask for the smallest folder that works: home/Documents/Notes, not all of home.
Step 1 — Ask for the folder
Add the folder to your permissions
In the IDE's Info tab, add home/Documents/Notes to both "Readable directories" and "Writable directories". Your manifest now says:
{
"vibe": 1,
"name": "Folder Notes",
"iconEmoji": "🗒️",
"iconBg": "linear-gradient(135deg,#f59e0b,#ef4444)",
"description": "Write Markdown notes that live as real .md files in Documents/Notes.",
"version": "1.0.0",
"width": 820,
"height": 560,
"permissions": {
"read": [
"home/Documents/Notes"
],
"write": [
"home/Documents/Notes"
]
},
"html": "<!doctype html>… the whole app, as one string …"
}// This folder is listed in the app's read/write permissions.
var FOLDER = "home/Documents/Notes";
var notes = []; // file names, e.g. "Ideas.md"
var current = null; // the file that's open
var saveTimer = null;What if the app goes outside that folder?
await window.vos.fs.readDir("home/Pictures"). Details in Permissions.Step 2 — List the notes
Read the folder with readDir
vos.fs.readDir(folder) returns entries like { name: "Ideas.md", type: "text" }. Keep only .md files. The folder won't exist until the first note is saved, so treat an error as "no notes yet".
function loadList() {
// A missing folder just means "no notes yet".
return window.vos.fs.readDir(FOLDER).catch(function () { return []; })
.then(function (entries) {
notes = entries
.filter(function (e) { return e.type !== "folder" && /\.md$/i.test(e.name); })
.map(function (e) { return e.name; })
.sort(function (a, b) { return a.localeCompare(b); });
renderList();
});
}
function renderList() {
var list = $("list");
list.innerHTML = "";
notes.forEach(function (name) {
var li = document.createElement("li");
li.textContent = name.replace(/\.md$/i, "");
if (name === current) li.className = "active";
li.onclick = function () { openNote(name); };
list.appendChild(li);
});
setStatus(notes.length + (notes.length === 1 ? " note" : " notes") + " in Documents/Notes");
}Step 3 — Open and create notes
Open a note
function openNote(name) {
return window.vos.fs.readFile(FOLDER, name).then(function (text) {
current = name;
$("title").value = name.replace(/\.md$/i, "");
$("editor").value = text || "";
showEditor(true);
renderList();
});
}
function showEditor(on) {
$("blank").hidden = on;
$("bar").hidden = !on;
$("editor").hidden = !on;
}Create a note with a free name
Pick "Untitled", or "Untitled 2" if that's taken, and write a starter file. writeFile creates Documents/Notes automatically the first time.
$("new").onclick = function () {
var name = "Untitled.md", n = 2;
while (notes.indexOf(name) !== -1) name = "Untitled " + (n++) + ".md";
// writeFile creates Documents/Notes the first time if it's missing.
window.vos.fs.writeFile(FOLDER, name, "# New note\n\n")
.then(loadList)
.then(function () { return openNote(name); })
.then(function () { $("title").select(); })
.catch(function (err) { setStatus("Couldn't create: " + err.message); });
};home/Documents/Notes.Step 4 — Save, rename and delete
Save while typing
The same pause-then-save idea as before. Note the copy of current: if the person switches notes during the pause, the text still goes to the right file.
$("editor").addEventListener("input", function () {
var name = current, text = $("editor").value;
setStatus("Saving…");
clearTimeout(saveTimer);
saveTimer = setTimeout(function () {
saveTimer = null;
window.vos.fs.writeFile(FOLDER, name, text)
.then(function () { setStatus("Saved " + name); })
.catch(function (err) { setStatus("Save failed: " + err.message); });
}, 400);
});Rename by editing the title
vos.fs.rename(from, to) takes two full paths. Characters that aren't allowed in file names are replaced, and an existing name is refused instead of overwritten.
$("title").addEventListener("keydown", function (e) {
if (e.key !== "Enter") return;
var next = $("title").value.trim().replace(/[\\/:*?"<>|]/g, "-") + ".md";
if (next === ".md" || next === current) return;
if (notes.indexOf(next) !== -1) { setStatus("A note with that name already exists"); return; }
// rename takes full paths on both sides.
window.vos.fs.rename(FOLDER + "/" + current, FOLDER + "/" + next)
.then(function () { current = next; return loadList(); })
.catch(function (err) { setStatus("Rename failed: " + err.message); });
});Delete, with a second click to confirm
Apps run in a protected window where pop-ups like confirm() don't work. A "click again" button is a simple, friendly alternative.
var armed = false;
$("delete").onclick = function () {
// Sandboxed apps can't use confirm(), so ask for a second click.
if (!armed) {
armed = true;
$("delete").textContent = "Click again to delete";
setTimeout(function () { armed = false; $("delete").textContent = "Delete"; }, 3000);
return;
}
armed = false;
$("delete").textContent = "Delete";
window.vos.fs.deleteFile(FOLDER, current).then(function () {
current = null;
showEditor(false);
return loadList();
}).catch(function (err) { setStatus("Delete failed: " + err.message); });
};Step 5 — Watch for changes
Refresh when the folder changes
Shared files can change without your app knowing: another app edits them, or the same person is on a second computer. vos.fs.watch(folder, callback) calls you when anything inside changes. It returns a function you can call to stop watching.
function start() {
loadList();
// Refresh when anything in the folder changes: another app, another
// device, or the Files tab in the IDE.
window.vos.fs.watch(FOLDER, function () {
loadList().then(function () {
if (current && notes.indexOf(current) === -1) { current = null; showEditor(false); }
else if (current && !saveTimer && document.activeElement !== $("editor")) openNote(current);
});
});
}Shopping.md in home/Documents/Notes. It appears in the app's sidebar without restarting.Practice
Show a word count
Show a solution
function words(text) {
var m = text.trim().match(/\S+/g);
return m ? m.length : 0;
}
// in the editor's input listener:
setStatus(words($("editor").value) + " words");Sort by most recently edited
Save a small index.json in your private folder that remembers when each note was last saved.
Add a Markdown preview
Start small: turn lines starting with # into headings and wrap **text** in <strong>. Escape < and & first.
Recap
- Use the private folder for app data and a shared folder for things people own.
- Ask for the smallest folder you need, for both read and write.
- readDir lists files; rename takes full paths; deleteFile removes one file.
- watch keeps your app up to date when files change elsewhere.