Request for comments: Trilium SDK #4012
Replies: 9 comments 7 replies
-
Tagging some folks for visibility: @zadam @Nriver @perfectra1n @soulsands @meichthys @SiriusXT @passionate2023 @BeatLink @mabeyj |
Beta Was this translation helpful? Give feedback.
-
Looks great! |
Beta Was this translation helpful? Give feedback.
-
This is very well spelled out! When it comes to the Core functionality, makes sense. Providing abstractions that are easier to work with (but those that still allow you to access lower-level functionality when needed) is great. When it comes to the Extension functionality, I'd be interested in seeing more and more examples. I understand that you're hoping to leverage the Core functionality, but I'm not quite sure what use cases exist there. For the Sync, I'm interested to see the implementation of it. For this to be successful, I think practical examples / documentation will be the lifeblood. I've always found decorators / class mixin inheritance to lead to a great deal of "where is this coming from?" "why was this used?". It looks very interesting, so I'm excited to see what it can do. |
Beta Was this translation helpful? Give feedback.
-
Pretty interesting. Might keep my eye on this |
Beta Was this translation helpful? Give feedback.
-
It looks very good. The folder synchronization is great, and I really hope to see it implemented, as it can attract more users who do not use servers, thereby allowing trilium to receive more feedback. As for language, I personally think that TypeScript may be more suitable. It also has decorators, and is much more maintainable than JavaScript. For an SDK, stability is very important. Moreover, it is more compatible with JavaScript and is in the same technology stack, making it more conducive to collaboration with trilium. At the same time, people who write extensions are more likely to be familiar with JS/TS, which also lowers the barrier to entry for extensions. I hope to have an SDK that can use trilium's UI components, interactive functions, and even the database more freely, although the database is also related to synchronization issues. If it's TypeScript, extensions can be directly published to npm. An official extension repository can be created, and extensions can be updated via pr, and the source code can be reviewed by everyone. Extensions provide some metadata, including version, installation scripts, download links, etc. Trilium can maintain installed plugin information and available plugin information in a simple way. @zadam 's opinion is very important. |
Beta Was this translation helpful? Give feedback.
-
Thanks all for the feedback!
Thanks for bringing that up. So the idea of the declarative (subclassed) note feature in Another thing you get for free is the ability to sync it to Trilium. To install an extension, you could just instantiate the root note and then call Here's an example of what a theme would look like using @singleton
@note_type('code')
@mime('text/css')
@label('appTheme', 'MyTheme')
@content('my-theme.css')
class MyTheme(Note): pass Then there would have to be some kind of registration or heuristic discovery to identify what kind of extension it is. But using from trilium_sdk.extension import Theme
@content('my-theme.css')
class MyTheme(Theme): pass This way it's very concise, and as a bonus it can be automatically discovered by the Here's an example of a widget using only @singleton
@note_type('code')
@mime('application/javascript;env=frontend')
@label('widget')
@content('SyntaxHighlightWidget.js')
class SyntaxHighlightWidget(Note): pass And the same using from trilium_sdk.extension import Widget
class SyntaxHighlightWidget(Widget): pass Here the default Some extensions may be customizable by adding attributes. Users could then take an extension and subclass it, adding their own Another interesting implication is that extensions can use other extensions, or resources from them like individual scripts. That could be done by simply importing them. Here's how a widget from extension_b import ScriptB
@children(ScriptB)
class WidgetA(Widget): pass Since Besides Trilium scripts, there could be whole Javascript libraries reused amongst different extensions this way.
I feel that for sure. Thanks for the thoughts, I see what you're saying. I definitely thought about that, but ultimately I felt it comes down to preference, and I happen to have a strong preference for Python. I think its readability and philosophy is great for this sort of thing. Just learned a bit about decorators in TypeScript just now and it's pretty neat. I think I'm too deep in the Python ecosystem myself, but don't see why they couldn't coexist if someone wanted to build that in TypeScript. |
Beta Was this translation helpful? Give feedback.
-
This is some interesting stuff. I can't at this point give a detailed feedback - some of these approaches are quite different from the line of thinking I use to have (e.g. the "notes as code" approach) and can't really fathom all the consequences. That's not a criticism, I'm curious to see some novel approaches, esp. if they could breathe some fresh air into topics which I've thought about (e.g. extensions), but couldn't form a conceptualization which I'd like. |
Beta Was this translation helpful? Give feedback.
-
Thanks @zadam for the thoughts. After thinking about this more, I don't like the idea of having a particular language/package manager as the main delivery mechanism for extensions. This will inevitably fracture the ecosystem, with extensions you can only install if you use a certain package manager. I'm working on a filesystem-based note tree format specification for use in This format is designed to be as concise and maintainable as possible. Therefore you don't have to specify things like positions of attributes/branches, and note/attribute ids are deterministically generated based on the file path. I envision to simply maintain a .yaml of note metadata alongside an optional file with the same base name representing the content. So an extension directory structure would look like:
Extension developers could maintain this structure manually if they didn't want to go the language-specific route. There would similarly be a need to share extensions in a way which isn't language-specific, but accessible in a standard format by tools. I propose using GitHub itself - you could simply put a URL to the extension repo in a config file, and the tool could manage syncing and installing it. There could be separate ecosystems of Python- and JavaScript-based extensions for "notes as code" approaches, but they would all be compiled into the same format and installed in the same way. I'll publish the documentation for this and raise a separate RFC for further discussion, but I'd be interested in any comments in the meantime. |
Beta Was this translation helpful? Give feedback.
-
Hi everyone, Wanted to give an update here: things are coming along smoothly, albeit not as quickly as I was hoping. Right now I'm looking at the end of July for an initial release. Also, we have a name: TriliumAlchemy! Here's an example from the user guide of cloning your todos (identified by the label from trilium_alchemy import Session, Note
from datetime import date
from my_secrets import HOST, TOKEN
with Session(HOST, token=TOKEN) as session:
# get todos
todo_list: list[Note] = session.search('#todo')
# clone to today's day note
today: Note = session.get_day_note(date.today())
for todo in todo_list:
today += todo
# new branches created upon exiting context |
Beta Was this translation helpful? Give feedback.
-
Problem statement
Trilium provides a very powerful mechanism to create extensions and an API for automation; it turns Trilium into your own custom app. However there are a few things which I think can help to fully realize this potential.
High level local automation
By this I mean, it would be nice to run your own scripts locally and interface with Trilium. They can even serve a REST API which gives you a custom backend, basically using Trilium as a frontend for your own app. ETAPI is a perfect way to interface with Trilium, but it's a low level interface - there should be an abstraction on top of it.
Synchronization with a local folder
Trilium has a robust note structure which is more powerful than a mere filesystem can represent, but I'm confident it can be done in a way that enables you to maintain your note tree locally in git and synchronize it with Trilium.
Extension development, distribution, and installation
In order to facilitate sharing and reuse of Trilium extensions, there needs to be a developer-friendly mechanism to develop extensions and a user-friendly way to install them. Installing an extension should be no more effort than a
pip install
followed by a command to sync the package you installed to a specified destination in Trilium.I think this could enable really neat features, like photo managers and AI assistants.
Originally posted by @SiriusXT in #3857 (comment)
Plan
I've started working on a Python package which is divided into 3 components, each of which will build on the previous ones. It's not in a shareable state yet, but I wanted to get feedback and ideas on the overall design, and the example Python note interfaces shown below. Once I have a stable enough API and docs I'll share it here.
Core: High level interface to work with notes/attributes/branches. Provides a way to define notes by subclassing the Note class. Attributes and parents/children can be accessed intuitively. This is about 95% complete coding wise, but I still need to write docs, so I'm hoping this part will be fully done by the end of June. This would be pretty useful if you're a developer and want to write your own custom automation. You can define a hierarchy of notes in Python and sync it to Trilium with just a few lines of code.
Extension: Uses core component to define a way to easily create and share Trilium extensions. An extension is just a Python file with classes which subclass Widget, Template, WorkspaceTemplate, etc. I've found things like attributes and note tree structures can be described very concisely in Python, so it should be really nice to maintain extensions this way. The structure of the extension is automatically generated, with child notes for "Widgets", "Templates", etc. For note content like js/css, you just maintain those files alongside the .py or in whatever folder you prefer.
Sync: Can install Python-defined note trees (one-way sync or "push") and sync (in both directions) arbitrary note trees from various formats. Plugin mechanism used to implement custom formats including markdown conversion to HTML notes, and even more advanced things like processing photos to generate thumbnails.
You can also use this to define your own custom folder structures which are parsed into notes. For example, say you have a folder called "Trips" and each folder under it describes a trip, with folders like "Days" and "Photos". Then you can write a custom plugin to convert each .md file under the "Days" folder to a note with a specific template or attributes. Takes a .yaml config file specifying which extensions you want to be installed, which folders you want synced using which plugins, and the destination notes for each.
This will be a big effort but I think it's all doable. I'm hoping to have it done by the end of the year, although I wouldn't be surprised if it took longer. But I'm building it because I need it for my own system - I really think having a local folder of notes that you can maintain in git and sync with Trilium, and maintaining extensions using Python packages, is the way to go.
Why Python?
I figured this may be a common question, so wanted to address it here. The main selling point for capturing note definitions and structures in Python is that it's a clean, declarative-oriented language. Javascript is great for imperative or functional programming, but I think Python is well suited for more of a declarative style of programming. For example you can define note structures by simply subclassing the base Note class or custom reusable Note subclasses.
Of course, extensions themselves are written in Javascript and CSS. The idea is to use Python to stitch together note attributes and structures, and leverage the package infrastructure. The .js/.css files to be installed will simply reside in the same package.
What's working
Here's a quick preview of some things you can do. This example shows how you could design a simple template to manage your phone contacts, and how you could automate maintaining it. Maybe you want to write a script to import your contacts from Google Takeout.
Declarative note definition
To start with, we create a mixin to represent an address which we can use in multiple different templates:
This creates a reusable class encapsulating promoted attributes.
@label_def
creates a label definition, withpromoted
by default. It's defined as follows:This results in the label e.g.
label:streetAddress=promoted,single,text
. There's also@relation_def
which creates a relation definition.Then, say you wanted to embed an address in a Contact template:
The class itself is generally empty, relying on decorators to define attributes and children. This enables a note to intuitively inherit the attributes/children of its ancestor classes rather than overriding them. (What you're doing when you use a decorator like
@label
is creating a new init method which is added to a linked list of methods. When the note is instantiated, this list is traversed and invoked to populate the note's attributes and children.) There is aninit
method you can define to inject your own arbitrary logic when the class is instantiated.Attributes and children are ordered by Python's MRO. That is, attributes from more distant ancestors are added towards the end. This code results in the following template:
When you instantiate
Contact
you have a note with the labelstemplate
andiconClass
, and a bunch of promoted attributes.@singleton
makes it so the note's id is derived by the fully qualified class name, e.g.my_templates.Contact
. Then every time you instantiate it, you'll get the same note and any updates will be automatically pushed to Trilium when you callcontext.flush()
. At that point you don't even need a sync tool, you can just run a few lines of Python when you make changes.Of course,
Contact
still needs to be placed in the note hierarchy. (I've just now hacked it up to add it to my root note.) You could have a "Templates" note which you create in Trilium and designate to hold your Python-maintained templates. Then you can represent it like:Then when you instantiate
Templates
and callcontext.flush()
,Contact
will be automatically created/updated.@children
is used to add the provided classes as children. If the children of a singleton aren't also a singleton, an id is deterministically generated - this enables reuse of notes for different templates, like "TaskList" which you can place under both "Home" and "Work" and they'll be separate notes. (If you put a singleton note in more than one place in the hierarchy it will be automatically cloned.)If there are no children specified, any existing children of a singleton will be deleted - so children of notes created this way can't be manually added by default. The system doesn't try to distinguish between children you created in the UI and children you previously set, but are just now deleting. If you want to maintain child notes in the UI, you have to simply use the
@leaf
decorator; this means you can't use@children
, and when instantiated it won't tamper with any child notes.Note automation
Say you wanted to programmatically create a note which has
Contact
as atemplate
relation target. You first declare a class which has atemplate
relation toContact
:Then you similarly just instantiate it. The resulting object will be a new note with a
template
relation to theContact
template note created previously.There's an intuitive way to access attributes via
my_note.attributes
, ormy_note.attributes.owned
/my_note.attributes.inherited
. You can check if a note has an attribute by doing'myAttribute' in my_note.attributes
, or.owned
/.inherited
. If you prefer more verbosity, there areLabel
andRelation
classes you can instantiate and add to the note; this also allows you to setinheritable
on the fly. (Same for branches: there's a more succinct way and a more verbose way which allows access to prefix/isInheritable/isExpanded.) Position fields for attributes and branches are automatically maintained for you.When a
Note
(or subclass) doesn't havenote_id
explicitly passed as a kwarg, and isn't a singleton, a new id is generated by Trilium when you callcontext.flush()
.Alternatively, you could have done the following:
Here's how to add
alice
to your "Contacts" note which is designated by the labelcontactsRoot
:The last major feature for the Core component which I'm still working on is managing note content. Currently you can set it like:
You'll be able to specify content for subclassed notes by using e.g.
@content('my_script.js')
. This will be the basis for implementing extensions in the next stage.Design overview
As hinted at above, the system keeps track of any changes you make and only commits them when you call the
flush()
API, either on the context (flush all notes/attributes/branches with changes) or on a particular object. This is known as a Unit of Work design pattern. Additionally it maintains a cache of notes, and each unique note has a single instance. The following illustrates how that works:Objects are lazy-loaded; that is, note metadata and content are only fetched once you access them. Once fetched, it will remain cached until you explicitly clear it from the cache using
my_note.invalidate()
.Acknowledgments
There are already Python libraries aiming to abstract ETAPI; see trilium-py and pytrilium. This project is more about providing a high level interface (inspired largely by SQLAlchemy) and enabling an ecosystem of extensions in a common format which can be installed using a common CLI tool.
Beta Was this translation helpful? Give feedback.
All reactions