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 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.
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 we think that a person's knowledge is so precious that there should not even be the slightest possibility for it to get stuck or even get 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.
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.
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.
Beware that yor web browser has to support a feature called "File System Access API". At the time of writing, the desktop versions of Chrome, Edge, Opera, and Safari do support it. Mobile browser versions are not supported yet. We recommend to use a Chromium-based browser (Chrome, Opera, Edge, etc.).
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:
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 as Subtext is a block-based plain-text format with minimal markup syntax. Here's a guide to get you started with 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:
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 a note's filename.
The slug is either generated based on the note's content, or you can
manually set it.
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:
[[my favorite note probably]]
[[My-Favorite.Note-Probably]]
[[My Favorite Note *probably*]]
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 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.
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.
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 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 topbar by clicking on the pin icon. Click it again to unpin the note.
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 words given.
By entering a token prefix followed by a
:
before
the actual search string, you can change that behavior.
Token prefixes
exact:
- Search for exact titles
Only notes whose title is an exact match are displayed.
duplicates:url
- Show notes with 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:
- paragraph
- heading
- code
- quote
- unordered-list-item
- ordered-list-item
- key-value-pair
has-media:
- Search for notes that contain a specific media type.
For example has-media:audio
. The following media types are supported:
- audio
- image
- video
has-url:
- Search for notes that contain a specific url
For example has-url:https://example.com
$[KEY]:
- Search for notes with key-value pair with
the key [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 key-value pair with
the key [KEY]
and the value [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.
Keyboard Shortcuts
NENO has a few keyboard shortcuts to make your note-taking experience more efficient.
- Save note:
CTRL
/CMD
+S
- Create new note:
CTRL
/CMD
+B
- Go to search input:
CTRL
/CMD
+E
- Wrap selection in Wikilink:
CTRL
/CMD
+I
- Create new alias for active note:
CTRL
/CMD
+U
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 is 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).
In the files view, you get an overview of all files in your notes folder.
You can also search for files whose slugs that 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.
Stats
It might be interesting to you to see some stats on your graph or see 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 backup your data beforehand.
The possibilities are endless but here are some example use cases:
- creating a list of red links (links to non-existing notes) and how often they occur
- creating a list of events based on dates mentioned in the note graph
- creating a website with aggregated data from the note graph.
Creating a script
Go to "Files" view. Scroll to the section "Create script" at the bottom, enter the name of the script and click on "Create". You should now be able too see the new script in the files list. Click on it and then click on "Open in script editor".
Variables
The following variables are defined in the JavaScript environment:
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
println
A function to add a line to the output.
Type: (val: string): void
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) ); });
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:
- React
- TypeScript
- Node.js
- Lexical
- Vite
- Subtext
License
NENO is licensed under the Apache License, Version 2.0.