NENO Logo

NENO User Manual

Hello and welcome to the NENO user manual. Thank you for your interest in NENO. We hope that this manual provides you with all you need to become a productive user.

What is NENO?

NENO Editor view
NENO Editor view

NENO is a powerful note-taking app that helps you create and manage your personal knowledge garden. A personal knowledge garden is a graph of notes linked together. Here is a nice introduction to personal knowledge graphs by Dan McCreary. With NENO, you retain full control over your data because you decide where it is stored: On your device, on a cloud storage of your choice, or even on a server under your control.
NENO is open-source software which means it is completely free to use.

NENO Editor view in dark mode
NENO Editor view in dark mode

Design Principles

It is important to understand why NENO is designed the way it is. So let's consider our 4 core design principles.

Freedom to exit

NENO and its data format are not only open-source, they are designed for credible exit as they follow the file over app paradigm:

File over app is a philosophy: if you want to create digital artifacts that last, they must be files you can control, in formats that are easy to retrieve and read. Use tools that give you this freedom.

NENO's exchange format is not one big JSON blob which might have an open specification but is hard to be used with another app. NENO's data format is a bunch of human-readable plain text files which can be opened and edited with any simple text editor. Even if you decide to stop using NENO, you can still access your notes and use them with another app.

We think that credible exit is an essential factor for serious tools for thought, because a person's knowledge is so precious that there should not even be the slightest possibility for it to get stuck or be lost in a proprietary app or hard-to-parse data format.

Local-first

Local-first means that you own your data, in spite of the cloud. We want to give ownership and agency to our users. We want to give them the freedom to store and use their data in any way they want. Another way to put it is one of the three inverted key relationships that define today's computing, proposed by Bernhard Seefeld: Services come to the data instead of data going to services.

Bottom-up organization

NENO prominently shows backlinks of notes to enable serendipity and a bottom-up organization of ideas. Tyler Angert sums this up pretty well:

Anytime you link to something, both ends of the link should have references to it. This means that organization of pages and ideas is bottom up. You don’t have to create folders or categories ahead of time; the relationships between things define their organization. The idea of things being in one place becomes antiquated. You shouldn’t have to spend extra time deciding what folder something should live in when it’s related 10 other things.

This also means that there are no pre-defined categories like "book", "project", or "person" in NENO (or categories like the ones used in Tiago Forte's PARA model, for that matter). However you can create any category you need yourself by creating a note for that category and linking to that note from all notes that belong to this category.

Modeless markup

NENO does not use Markdown or different editing modes or views. Why? Gordon Brander sums it up nicely:

Markup like [HTML's or Markdown's link syntax] implies that apps should have a distinction between an edit-mode meant for writing, and a view-mode meant for reading. This creates indirection. Editing becomes something abstract. You have to hold in your head the difference between the symbolic markup and the rendered text, and imagine the rendered text while editing. This kind of indirection might be ok for an audience-centric publishing tool, but feels like a lot of cognitive overhead for taking notes.

Instead of offering several modes or a WYSIWYG editor with rich markup features that require a specialized editor, we want to keep it simple and provide a single-mode editor, and a text format with minimal markup features, optimized for efficient note-taking without the need for eyeball parsing.

Getting started

To get started with NENO, you do not need to create an account or install any software. NENO is a web app that runs in your browser.

Open the app.

Optional installation

Even though you don't need to install NENO, doing so has some advantages. Click on the install button in the address bar of your browser to install NENO as a PWA (Progressive Web App). This way, you can start NENO from your device like any other app.

Installation dialog for NENO in the browser
Installation dialog for NENO in the browser
NENO app icon in macOS dock
NENO app icon in macOS dock

Opening a folder

When you open NENO for the first time, you will be asked to select a folder where your notes will be stored. NENO stores all data in a folder of your choice on your device. If you want, you could also select a cloud storage folder so that your notes are synchronized to all your devices. You could also use a Git repository to manage your notes. That way, you also get versioning for free. Even though NENO is a web app, it does not send any user data to a web server.

At the time of writing, NENO works with Chrome, Edge, and Opera.

Taking notes

Taking notes in NENO is quite straight-forward. Just start writing your note in the editor and save it, either via the save button or via CTRL/CMD + S. You will see that the note list on the left now contains your first note:

My first note
My first note

The note is now stored in the selected folder and you can open it with any text editor.

Notes are written in Subtext, a format specifically designed for note-taking. Subtext documents are easily composable because Subtext is a block-based plain-text format with minimal markup syntax. Here's a guide to get you started with Subtext.

The features of Subtext
The features of Subtext

Linking other notes

You can link to other notes either via [[Wikilinks]] or /slashlinks. When using slashlinks, the editor will show a transclusion of the linked note.
When you create another note, you can add a link to the first note by clicking on the "Link to this note" button with the chain icon next to the list entry of that note:

Link to another note
Link to another note

Since NENO also displays the backlinks of a note, you might want to create notes for tags. For example, create a note called "quotes", and create another note with the following text:

> If you try and take a cat apart to see how it works, the first thing you have on your hands is a non-working cat. - Douglas Adams
[[quotes]]

If you now want to see all the quotes in your note graph, open the "quotes" note and look at the backlinks.

Slugs

Every note that you create is assigned a slug, which is a unique key to identify and find the note. The slug is also used as the note's filename. The slug is either generated automatically based on the note's content, or you can set it manually. When referencing a note in a Wikilink, the link text is normalized to a slug. E.g. when writing the Wikilink [[My Favorite Note (probably)]], the note with the slug my-favorite-note-probably from the file my-favorite-note-probably.subtext will be linked. The same note will be linked when using these Wikilinks:

When using slashlinks, you have to use the unmodified slug: /my-favorite-note-probably

Updating slugs and references

Slugs of already saved notes can be updated at any time. When editing the slug of an already saved note in the slug input, a toggle with the label "Update references" is displayed. When you save the new slug with the toggle activated, all wikilinks and slashlinks throughout the whole graph that reference the current note will also be updated.

Editing a slug of an already saved note will show the "Update
    references" toggle.
Editing a slug of an already saved note will show the "Update references" toggle.

If the title of the current note can be normalized to the new note slug, Wikilink references will be assigned the title as link text.

Example:
Let's change the slug of a note from old-note to new-note with the "Update references" toggle activated.
All slashlinks will change accordingly from /old-note to /new-note.
All wikilinks will change from Old note to New note when the title of the new note is "New note".
All wikilinks will change from Old note to new-note when the title of the new note is "Old note".

Be aware that the state of the toggle is saved for future slug edits.

Aliases

You can add aliases to a note. Aliases are alternative slugs that can be used to find the note. Click on the plus icon in the right corner of the slug bar to create a note alias, and then enter the alias in the new alias field.

A note with several aliases
A note with several aliases

Aliases are a useful feature when writing notes in multiple languages. For example, you can create a note with the slug "entropy" and assign it the German word "entropie" as a note alias. Or you could add the alias "complex" to the note "complexity" and reference that note in a sentence like This algorithm is [[complex]].

Pins

You can pin notes to the top bar by clicking on the pin icon. Click it again to unpin the note.

Pinned notes on the top bar
Pinned notes on the top bar

Drag and drop pins within the title bar to move them to another position. Drag and drop pins to the editor section to insert a wikilink to the pinned note.

Search

By default, entering a value in the search input will search for note titles that include all the given words. By entering a token prefix followed by a : before the actual search string, you can change this behavior.

Token prefixes

exact: - Search for exact titles
Only notes whose title is an exact match are displayed.

Search for exact matches
Search for exact matches

duplicates:url - Show notes with the same URLs
When you have a lot of notes, chances are that the same URL is included in several notes. This search query helps you find those notes.

duplicates:title - Show notes with non-unique titles

ft: - Full-text search
Perform a full-text search on all notes by typing ft: followed by the search query into the search box. The full-text search is case-insensitive.

has-block: - Search for notes that contain a specific block type.
For example, has-block:list. The following block types are supported:

has-media: - Search for notes that contain a specific media type.
For example, has-media:audio. The following media types are supported:

has-url: - Search for notes that contain a specific URL.
For example, has-url:https://example.com

links-to: - Find all notes that link to a specific note, identified by its slug.
For example, links-to:target-note-slug

$[KEY]: - Search for notes with a key-value pair whose key is [KEY].
For example, $check-back: will find all notes that have a key-value pair with the key check-back.

$[KEY]:[VALUE] - Search for notes with a key-value pair whose key is [KEY] and whose value is [VALUE].
For example, $check-back:2040-01-01 will find all notes that have a key-value pair with the key check-back and a value that includes 2040-01-01.

Search presets

You can save every search query as a preset by opening the "Search presets" view by clicking the button next to the search input field. You can also quickly query saved presets.

Search presets
Search presets

Keyboard Shortcuts

NENO has a few keyboard shortcuts to make your note-taking experience more efficient.

When the search input is focused you can select results with / and open the selected note with Enter.

Working with files

You can import any file into NENO and reference it in multiple notes, either via drag and drop, or by clicking on the "Upload file" button in the editor view. When importing a file, a slug will be created based on the file's name, by convention starting with the prefix files/, e.g. files/image.png. A slashlink to the file and a transclusion of the file are automatically added to the currently active note. If applicable, the transclusion contains a preview (images, plain text) or a player for the file (video, audio).

Note with a video
Note with a video

In the files view, you get an overview of all files in your notes folder. You can also search for files whose slugs start with a specific string by entering that string into the search input, e.g. files/. If you want to find all files that end with a specific string, type ends-with: into the search input field, followed by the desired string, e.g. ends-with:.jpg.

Single file view

In the single file view, you can see all notes that reference the file. You can also create a new note that references the file.

Single file view
Single file view

Stats

You might be interested in seeing some stats on your graph, or in how many components it has. Use the "Stats" view for this, which you can find in the app menu.

Scripting

Scripting is probably the most powerful (and also most dangerous) feature of NENO, as it allows the programmatic manipulation of your knowledge graph using the full flexibility of a JavaScript environment. Use with caution and be sure to back up your data beforehand.

The possibilities are endless, but here are some example use cases:

NENO offers two ways to run scripts: programmable notes, which let you embed JavaScript directly inside any note, and standalone script files managed via the Scripts view (or the Files view).

Programmable notes

Any note can become a programmable note by adding a fenced run block to it. The editor detects the block, executes its contents in a sandboxed worker, and renders the script's output immediately below the closing fence. The output is updated automatically as you edit the script — no save or button click is required.

```run
println("Hello from a programmable note!");
println("This graph contains " + graph.notes.size + " note(s).");
```

After a short debounce on any edit in the note, NENO checks each run block and re-executes only those whose code has changed since the previous run. Scripts also re-run when you save the note or switch to it. The rendered output is not part of the saved note content; only the fenced block is persisted to disk. Removing the run block also removes its output. Inside a run block, the variable thisNote gives you access to the surrounding note (see Variables below).

Programmable notes are perfect for live dashboards over your graph: counts, todo aggregations, calendar listings, link audits, etc. For one-off transformations or larger jobs you may prefer a standalone script file (see below).

Creating a standalone script

Open the "Scripts" view from the navigation rail. It lists all existing scripts (files under scripts/ with the .neno.js suffix) and provides a "Create script" form at the bottom: enter a name, click "Create", and the new script appears in the list, ready to be opened in the script editor. Scripts can also be reached from the Files view, where they show up alongside any other files.

Variables

The following variables are defined in the JavaScript environment of both standalone scripts and programmable notes:

notesProvider

A collection of functions for accessing and manipulating the currently loaded graph.

Type: NotesProvider

storageProvider

A collection of functions for reading from and writing to the file system. Operations are limited to the graph folder. Use this interface if you need a low-level way of manipulating the graph or if you want to save arbitrary files.

Type: StorageProvider

graph

A representation of the currently loaded graph, including all notes, aliases, and some metadata.

Type: GraphObject

print

A function to add text to the output without a trailing line break.

Type: (val: string): void

println

A function to add a line to the output.

Type: (val: string): void

printNoteTitle

A function to add a clickable note title to the output. The given title is sluggified to derive the linked note's slug, so the link navigates to the note that the title would resolve to.

Type: (title: string): void

thisNote

Inside a programmable note, the surrounding note. Includes the note's slug, raw content, parsed blocks, and any keyValues declared in the note. undefined when the script is run as a standalone script file. Inside a mod module, thisNote always refers to the module's own note, not the importing note.

Example scripts

List all aliases and their canonical slugs

println(
    "Graph contains "
    + graph.aliases.size
    + " aliase(s):"
);

for (const [alias, slug] of graph.aliases) {
    println(
        alias.padEnd(25, " ") + "=> " + slug);
}

List events

All events (links in the form of [[YYYY-MM-DD]], e.g. [[2024-06-01]]) will be listed here, ordered by date and grouped by month.

const SHOW_NOTE_CREATED_EVENTS = false;
const START_YEAR = 2024;
const START_MONTH = 5;

const MONTH_NAMES = [
  "January",
  "February",
  "March",
  "April",
  "May",
  "June",
  "July",
  "August",
  "September",
  "October",
  "November",
  "December",
];

function createEvent(note, date, type) {
  const month = parseInt(date.substring(5, 7));
  const year = parseInt(date.substring(0, 4));
  return {
    title: getNoteTitle(note.content),
    date,
    month,
    year,
    type,
  };
}

function getEventsOfNote(slug, note) {
  const events = [];

  if (SHOW_NOTE_CREATED_EVENTS) {
    const creationDate = note.meta.createdAt
      .substring(0, 10);
    events.push(
      createEvent(note, creationDate, "NOTE_CREATED"),
    );
  }

  if (slug.match(/\d{4}-\d{2}-\d{2}/)) {
    const date = slug.match(/\d{4}-\d{2}-\d{2}/)[0];
    const type = "MENTIONED_IN_SLUG";
    events.push(createEvent(note, date, type));
  }
  const dateMatches = note.content.matchAll(
    /\[\[(\d{4}-\d{2}-\d{2})\]\]/g,
  );
  for (const match of dateMatches) {
    const date = match[1];
    const type = "MENTIONED_IN_CONTENT";
    events.push(createEvent(note, date, type));
  }

  return events;
}

const table = Array.from(graph.notes.entries())
  .map(([slug, note]) => [...getEventsOfNote(slug, note)])
  .flat()
  .filter((event) => {
    return event.year > START_YEAR
      || (
        event.year === START_YEAR
        && event.month >= START_MONTH
      );
  })
  .toSorted((a,b) => {
    if (a.date > b.date) return 1;
    if (a.date < b.date) return -1;
    return 0;
  })
  .reduce((accumulator, thisEvent, i, events) => {
    const previousEvent = events[i-1];
    if ((!previousEvent) || previousEvent.month !== thisEvent.month) {
      if (previousEvent) {
        accumulator += "\n";
      }
      accumulator += MONTH_NAMES[thisEvent.month - 1] + " " + thisEvent.year + "\n";
    }
    return accumulator += thisEvent.date
      + " | " + thisEvent.title.padEnd(35, " ")
      + " | " + thisEvent.type
      + "\n";
  }, "");

println(table);

List all invalid links

Prints a list of red links (non-existing slugs that are linked somewhere), and how often and in which notes they are referenced.

const redLinks = new Map();

const allExistingSlugs = new Set([
  ...graph.notes.keys(),
  ...graph.aliases.keys(),
  ...graph.files.keys(),
]);

for (const [slug, note] of graph.notes.entries()) {
  const spans = getAllInlineSpans(graph.indexes.blocks.get(slug));
  const linkTargets = getSlugsFromInlineText(spans);
  for (const linkTarget of linkTargets) {
    if (!allExistingSlugs.has(linkTarget)) {
      if (redLinks.has(linkTarget)) {
        redLinks.get(linkTarget).add(slug);
      } else {
        redLinks.set(linkTarget, new Set([slug]));
      }
    }
  }
}

Array.from(redLinks.entries())
  .toSorted((a, b) => b[1].size - a[1].size)
  .forEach(([target, refs]) => {
    println(
      target.padEnd(20, " ")
      + " | " + refs.size
      + " | " + Array.from(refs)
    );
  });
  

Reusable modules with mod blocks

You can share code between scripts by defining modules in mod fenced blocks inside any note, and loading them from run blocks in other notes via the use() function.

To define a module, add a single mod fenced block to a note. Whatever the block returns is the module's exported value. For example, a note with the slug math-utils might contain:

```mod
const double = (x) => x * 2;
const triple = (x) => x * 3;
return { double, triple };
```

Import it from any run block using await use("slug"), where slug is the slug of the note that contains the mod block:

```run
const m = await use("math-utils");
println(m.double(21));
```

Modules themselves may call use() to load further modules. Import cycles (module A importing module B that imports A) are detected and raise an error.

Inside a module, the same globals are available as in run blocks (notesProvider, graph, println, etc.). thisNote refers to the module's own note rather than the caller's — so if a module needs the caller's context, pass it in as a function argument.

Modules are re-fetched on every script execution, so editing a mod block takes effect the next time an importing run block runs (for instance, when its note is reopened or its code is edited).

Known issues

Browsers installed with Flatpak

On Linux, we do not recommend using NENO with a browser installed via Flatpak. In our tests, Chrome lacked the necessary permissions to access PWAs in the local user directory; more importantly, loading a graph folder from disk was very slow compared to a Chrome installation from the RPM file provided by Google.

Acknowledgements

NENO's primitives are inspired by the work of the Subconscious project.

Significant inspiration for the project comes from my work at the Niklas Luhmann Archive, from the people of the Subconscious Discord channel and from the work of Andy Matuschak.

NENO is built with the help of many open-source projects. Here are the most important ones:

License

NENO is licensed under the Apache License, Version 2.0.