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.

20 minpermissionsreadDirrenamewatch

What you'll build

A two-pane notes app. The sidebar lists every note in Documents/Notes; the right side edits it. Notes are ordinary .md files, so other apps (and the Files app) can open them too.

You'll need

Lessons 1 and 2.

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
Peek at the end result first, or build it step by step below.

Private files or shared files?

Private folderShared folder (e.g. Documents)
Who can see itOnly your appThe person, and other apps they allow
Permission"read": [], "write": []"read": ["home/Documents/Notes"]
Install promptNonePeople approve the folder when installing
Good forSettings, caches, app dataDocuments 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

1

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:

json
{
  "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 …"
}
javascript
// 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?

VibeOS refuses with an "Access denied" error. Try it later in the Console: await window.vos.fs.readDir("home/Pictures"). Details in Permissions.

Step 2 — List the notes

2

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".

javascript
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");
}
Checkpoint: the sidebar says "0 notes in Documents/Notes".

Step 3 — Open and create notes

3

Open a note

javascript
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;
}
4

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.

javascript
$("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); });
};
Checkpoint: click + New. "Untitled" appears in the sidebar and in the Files tab under home/Documents/Notes.

Step 4 — Save, rename and delete

5

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.

javascript
$("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);
});
6

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.

javascript
$("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); });
});
7

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.

javascript
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); });
};
Checkpoint: rename a note to "Ideas", then delete it. Both changes show in the Files tab.

Step 5 — Watch for changes

8

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.

javascript
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);
    });
  });
}
Checkpoint: in the Files tab, create a file called Shopping.md in home/Documents/Notes. It appears in the app's sidebar without restarting.

Practice

Show a word count

Show the number of words in the open note as you type.
Show a solution
javascript
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

Sort the sidebar so the note you edited last is at the top. Keep the order in the app's private folder, not in Documents, because it's app data rather than something the person owns.

Save a small index.json in your private folder that remembers when each note was last saved.

Add a Markdown preview

Add a Preview button that shows the note formatted. Because notes can come from anywhere, escape the text before turning Markdown into HTML.

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.
Up next · Lesson 4AI chatAdd hosted AI with no API keys, and handle plan and credit limits.