Since my last post on reddit asking for some help regarding Emacs and TiddlyWikis REST API
I gained some elisp
knowledge I’d like to share.
Maybe you want to go directly to the Emacs configuration.
TiddlyWiki 5
For those of you who haven’t heard of TiddlyWiki yet:
TiddlyWiki is a personal wiki and a non-linear notebook for organising and
sharing complex information. It is an open-source single page application wiki
in the form of a single HTML file that includes CSS, JavaScript, and the
content. It is designed to be easy to customize and re-shape depending on
application. It facilitates re-use of content by dividing it into small pieces
called Tiddlers. – Wikipedia
You use the wiki as a single HTML page or via nodejs
. With nodejs
we can talk to
Tiddlywiki via its REST API.
I’ve been using TiddlyWikis REST API to serve a instance via AWS Lambda and DynamoDB
for the data storage. The project itself is called widdly and there is also a demo at
tiddly.info/serverless.
Every single page inside the wiki is called tiddler
.
On the philosophy of tiddlers: “The purpose of recording and organising information is so
that it can be used again. The value of recorded information is directly proportional to
the ease with which it can be re-used.”
A tiddler
has following format:
1
2
3
4
5
|
{
"title": "HelloThere",
"tags": "FirstTag [[Second Tag]]",
"my-custom-field": "Field value"
}
|
Code Snippet 1:
Tiddler JSON format
Next I’ll show you how to setup your TiddlyWiki instance.
I have a public “digital garden” aka wiki available at https://brainfck.org
Basic setup
I use node.js
to run my TiddlyWiki instance.
The REST API is only available within the nodeJS environment.
For isolation reasons I use Docker
to run it. Here is my Dockerfile
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
FROM mhart/alpine-node
# Create a group and user
RUN addgroup -g 984 -S appgroup
RUN adduser -h /DATA/wiki -u 1000 -S appuser -G appgroup
# Tell docker that all future commands should run as the appuser user
ENV TW_BASE=/DATA TW_NAME=wiki TW_USER="xxx" TW_PASSWORD="xxx" TW_LAZY=""
ENV TW_PATH ${TW_BASE}/${TW_NAME}
WORKDIR ${TW_BASE}
RUN npm install -g npm@8.10.0
RUN npm install -g tiddlywiki http-server
# COPY plugins/felixhayashi /usr/lib/node_modules/tiddlywiki/plugins/felixhayashi
# RUN ls -la /usr/lib/node_modules/tiddlywiki/plugins
COPY start.sh ${TW_BASE}
# Change ownership
RUN chown appuser:appgroup /DATA/start.sh
EXPOSE 8181
USER appuser
ENTRYPOINT ["/DATA/start.sh"]
CMD ["/DATA/start.sh"]
|
And as for start.sh
:
1
2
3
4
5
6
7
|
#!/usr/bin/env sh
# Start image server
http-server -p 82 /DATA/wiki/images &
# Start tiddlywiki server
tiddlywiki /DATA/wiki --listen port=8181 host=0.0.0.0 csrf-disable=yes
|
Code Snippet 3:
Bash script to start a simple http-server (for uploading images) and the tiddlywiki server instance (node.js)
Now you should be able to call the API (via curl
for example):
1
|
curl http://127.0.0.1:8181/recipes/default/tiddlers/Emacs | jq
|
Code Snippet 4:
Now you should be able to call the API (via
curl
for example).
1
2
3
4
5
6
7
8
9
|
{
"title": "Emacs",
"created": "20210623082136326",
"modified": "20210623082138258",
"tags": "Topics",
"type": "text/vnd.tiddlywiki",
"revision": 0,
"bag": "default"
}
|
request.el
I use request.el
I know there might be better alternatives. But in my case it’s been totally
sufficient and Elisp beginner friendly.
for crafting and sending HTTP requests. So what is request.el
all about?
Request.el is a HTTP request library with multiple backends. It supports url.el
which is shipped with Emacs and curl command line program. User can use curl
when s/he has it, as curl is more reliable than url.el. Library author can use
request.el to avoid imposing external dependencies such as curl to users while
giving richer experience for users who have curl. – Source
GET
Let’s have a look how a simple (GET) API call looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
(let*
((httpRequest
(request "https://api.chucknorris.io/jokes/random"
:parser 'json-read
:sync t
:success (cl-function
(lambda (&key data &allow-other-keys)
(message "I sent: %S" data)))))
(data (request-response-data httpRequest)))
;; Print information
(cl-loop for (key . value) in data
collect (cons key value)))
|
1
2
3
4
5
6
7
8
|
((categories .
[])
(created_at . "2020-01-05 13:42:19.576875")
(icon_url . "https://assets.chucknorris.host/img/avatar/chuck-norris.png")
(id . "YNmylryESKCeA5-TJKm_9g")
(updated_at . "2020-01-05 13:42:19.576875")
(url . "https://api.chucknorris.io/jokes/YNmylryESKCeA5-TJKm_9g")
(value . "The descendents of Chuck Norris have divided into two widely known cultures: New Jersey and New York."))
|
POST
Sending a POST
request is also an easy task:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
(let*
((httpRequest
(request "http://httpbin.org/post"
:type "POST"
:data '(("key" . "value") ("key2" . "value2"))
:parser 'json-read
:sync t
:success (cl-function
(lambda (&key data &allow-other-keys)
(message "I sent: %S" data)))))
(data (request-response-data httpRequest))
(err (request-response-error-thrown httpRequest))
(status (request-response-status-code httpRequest)))
;; Print information
(cl-loop for (key . value) in data
collect (cons key value)))
|
And here is the result:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
((args)
(data . "")
(files)
(form
(key . "value")
(key2 . "value2"))
(headers
(Accept . "*/*")
(Accept-Encoding . "deflate, gzip, br, zstd")
(Content-Length . "21")
(Content-Type . "application/x-www-form-urlencoded")
(Host . "httpbin.org")
(User-Agent . "curl/7.83.1")
(X-Amzn-Trace-Id . "Root=1-62cdbc5c-52d3ad32436c1cb8778808e5"))
(json)
(origin . "127.0.0.1")
(url . "http://httpbin.org/post"))
|
Emacs
1
2
|
;; default tiddlywiki base path
(setq tiddlywiki-base-path "http://127.0.0.1:8181/recipes/default/tiddlers/")
|
GET tiddler
Let’s GET a tiddler:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
(let*
((httpRequest
(request (concat tiddlywiki-base-path "Emacs")
:parser 'json-read
:sync t
:success (cl-function
(lambda (&key data &allow-other-keys)
(message "I sent: %S" data)))))
(data (request-response-data httpRequest))
(err (request-response-error-thrown httpRequest))
(status (request-response-status-code httpRequest)))
;; Print information
(cl-loop for (key . value) in data
collect (cons key value)))
|
1
2
3
4
5
6
7
|
((title . "Emacs")
(created . "20210623082136326")
(modified . "20210623082138258")
(tags . "Topics")
(type . "text/vnd.tiddlywiki")
(revision . 0)
(bag . "default"))
|
PUT a new tiddler
Creating a new tiddler is also simple. Using ob-verb
This package is really helpful especially when you do literate programming with org-babel.
let’s add a PUT
request to the API:
1
2
3
4
5
6
7
8
9
10
|
PUT http://127.0.0.1:8181/recipes/default/tiddlers/I%20love%20Elisp
x-requested-with: TiddlyWiki
Content-Type: application/json; charset=utf-8
{
"title": "I love Elisp",
"tags": "Emacs [[I Love]]",
"send-with": "verb",
"text": "This rocks!"
}
|
Check if tiddler was indeed created:
1
2
3
|
GET http://127.0.0.1:8181/recipes/default/tiddlers/I%20love%20Elisp
x-requested-with: TiddlyWiki
Accept: application/json; charset=utf-8
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
HTTP/1.1 200 OK
Content-Type: application/json
Date: Wed, 13 Jul 2022 10:03:27 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked
{
"title": "I love Elisp",
"tags": "Emacs [[I Love]]",
"fields": {
"send-with": "verb"
},
"text": "This rocks!",
"revision": 1,
"bag": "default",
"type": "text/vnd.tiddlywiki"
}
|
Now let’s translate that to request.el
code. This I’ll some extra complexity: I’ll add
a function (defun
) to PUT
a new tiddler for us, where name, tags and body of the tiddler are variable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
;; Define function for inserting new tiddlers
(defun insert-tiddler(name tags body)
(let*
(
(tiddler-title name)
(url-path (url-hexify-string tiddler-title))
(tiddler-tags tags)
(tiddler-body body)
(httpRequest
(request (concat tiddlywiki-base-path url-path)
:type "PUT"
:data (json-encode
`(
("title" . ,tiddler-title)
("created" . ,(format-time-string "%Y%m%d%H%M%S%3N"))
("modified" . ,(format-time-string "%Y%m%d%H%M%S%3N"))
("tags" . ,tiddler-tags)
("text" . ,tiddler-body)
("type" . "text/vnd.tiddlywiki")))
:headers '(
("Content-Type" . "application/json")
("X-Requested-With" . "Tiddlywiki")
("Accept" . "application/json"))
:encoding 'utf-8
:sync t
:complete
(function*
(lambda (&key data &allow-other-keys)
(message "Inside function: %s" data)
(when data
(with-current-buffer (get-buffer-create "*request demo*")
(erase-buffer)
(insert (request-response-data data))
(pop-to-buffer (current-buffer))))))
:error
(function* (lambda (&key error-thrown &allow-other-keys&rest _)
(message "Got error: %S" error-thrown)))
)))
(format "%s:%s"
(request-response-headers httpRequest)
(request-response-status-code httpRequest)
)))
;; Insert 2 tiddlers
(insert-tiddler "I love Elisp" "Elisp [[I Love]]" "This rocks!")
|
1
|
"((etag . \"default/I%20love%20Elisp/61:\") (content-type . text/plain) (date . Wed, 13 Jul 2022 12:30:33 GMT) (connection . keep-alive) (keep-alive . timeout=5)):204"
|
Some explanations:
- in line 6 I URL encode the
tiddler-title
I love Elisp
should become I%20love%20Elisp
- in line 21 some headers are set
X-Requested-With
is required to be set to TiddlyWiki
Content-Type
should be json
- we also accept
json
as a response
- in line 13 we specify the
data
to be sent to the API
- each field (key, value sets) is set accordingly (see 10)
- I set the
created
and modified
fields using format-time-string
Now let’s check again if tiddler really exists:
1
2
3
|
GET http://127.0.0.1:8181/recipes/default/tiddlers/I%20love%20Elisp
x-requested-with: TiddlyWiki
Accept: application/json; charset=utf-8
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
HTTP/1.1 200 OK
Content-Type: application/json
Date: Wed, 13 Jul 2022 12:40:22 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked
{
"title": "I love Elisp",
"created": "20220713143033566",
"modified": "20220713143033566",
"tags": "Elisp [[I Love]]",
"text": "This rocks!",
"type": "text/vnd.tiddlywiki",
"revision": 61,
"bag": "default"
}
|
Use cases
Now what can you do with this little custom functions? Let me share my use cases.
Add bookmark
A bookmark in my TiddlyWiki represents a tiddler of following format:
1
2
|
GET http://127.0.0.1:8181/recipes/default/tiddlers/chashell
Accept: application/json; charset=utf-8
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
HTTP/1.1 200 OK
Content-Type: application/json
Date: Wed, 13 Jul 2022 12:49:58 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked
{
"title": "chashell",
"created": "20210519103441485",
"modified": "20210519103528982",
"fields": {
"name": "chashell",
"note": "Chashell is a Go reverse shell that communicates over DNS. It can be used to bypass firewalls or tightly restricted networks.",
"url": "https://github.com/sysdream/chashell"
},
"tags": "Golang Security Tool Bookmark",
"type": "text/vnd.tiddlywiki",
"revision": 0,
"bag": "default"
}
|
Every bookmarks consists of a name, a note and an url. Every tiddler supposed to be a bookmark is tagged by Bookmark
. In this chashell
is
a tiddler and at the same time a bookmark in my wiki.
This is the entry in my public Tiddlywiki instance: https://brainfck.org/#chashell.
As part of my daily routine, I go through my pocket entries and decide which ones I should bookmark in Tiddlywiki. These are my keybindings
for the getpocket major mode:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
(map! :map pocket-reader-mode-map
:after pocket-reader
:nm "d" #'pocket-reader-delete
:nm "SD" #'dorneanu/pocket-reader-send-to-dropbox
:nm "a" #'pocket-reader-toggle-archived
:nm "B" #'pocket-reader-open-in-external-browser
:nm "e" #'pocket-reader-excerpt
:nm "G" #'pocket-reader-more
:nm "TAB" #'pocket-reader-open-url
:nm "tr" #'pocket-reader-remove-tags
:nm "tN" #'dorneanu/pocket-reader-remove-next
:nm "C-b" #'dorneanu/tiddlywiki-add-bookmark
:nm "ta" #'pocket-reader-add-tags
:nm "gr" #'pocket-reader-refresh
:nm "p" #'pocket-reader-search
:nm "U" #'pocket-reader-unmark-all
:nm "y" #'pocket-reader-copy-url
:nm "Y" #'dorneanu/pocket-reader-copy-to-scratch)
|
Let’s have a look at dorneanu/tiddlywiki-add-bookmark
:
Again: You can find all my customized functions in my dotfiles.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
(defun dorneanu/tiddlywiki-add-bookmark ()
"Adds a new bookmark to tiddlywiki. The URL is fetched from clipboard or killring"
(require 'url-util)
(interactive)
(pocket-reader-copy-url)
(setq my-url (org-web-tools--get-first-url))
(setq url-html (org-web-tools--get-url my-url))
(setq url-title (org-web-tools--html-title url-html))
(setq url-title-mod (read-string "Title: " url-title))
(setq url-path (url-hexify-string url-title-mod))
(setq url-note (read-string (concat "Note for " my-url ":")))
(setq url-tags (concat "Bookmark "(read-string "Additional tags: ")))
(request (concat tiddlywiki-base-path url-path)
:type "PUT"
:data (json-encode `(("name" . ,url-title-mod) ("note" . ,url-note) ("url" . ,my-url) ("tags" . ,url-tags)))
:headers '(("Content-Type" . "application/json") ("X-Requested-With" . "TiddlyWiki") ("Accept" . "application/json"))
:parser 'json-read
:success
(cl-function
(lambda (&key data &allow-other-keys)
(message "I sent: %S" (assoc-default 'args data))))
:complete (lambda (&rest _) (message "Added %s" (symbol-value 'url-title-mod)))
:error (lambda (&rest _) (message "Some error"))
:status-code '((400 . (lambda (&rest _) (message "Got 400.")))
(418 . (lambda (&rest _) (message "Got 418.")))
(204 . (lambda (&rest _) (message "Got 202."))))
)
)
|
Add quote
After reading each book I usually do some post-reading/post-processing. While I could use the
Tiddlywiki web interface to add new tiddlers, I’d rather do it from Emacs directly.
Often I need to insert new quotes from book (or web articles). How to I do this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
(defun dorneanu/tiddlywiki-add-quote ()
"Adds a new quote"
(interactive)
(setq quote-title (read-string "Quote title: " quote-title))
(setq url-path (url-hexify-string quote-title))
(setq quote-source (read-string (concat "Source for " quote-title ": ") quote-source))
(setq quote-body (read-string (concat "Text for " quote-title ": ")))
(setq quote-tags (concat "quote "(read-string "Additional tags: ")))
(request (concat tiddlywiki-base-path url-path)
:type "PUT"
:data (json-encode `(
("title" . ,quote-title)
("created" . ,(format-time-string "%Y%m%d%H%M%S%3N"))
("modified" . ,(format-time-string "%Y%m%d%H%M%S%3N"))
("source" . ,quote-source)
("tags" . ,quote-tags)
("text" . ,quote-body)
("type" . "text/vnd.tiddlywiki")))
:headers '(("Content-Type" . "application/json") ("X-Requested-With" . "TiddlyWiki") ("Accept" . "application/json"))
:parser 'json-read
:success
(cl-function
(lambda (&key data &allow-other-keys)
(message "I sent: %S" (assoc-default 'args data))))
:complete (lambda (&rest _) (message "Added quote <%s>" (symbol-value 'quote-title)))
:error (lambda (&rest _) (message "Some error"))
:status-code '((400 . (lambda (&rest _) (message "Got 400.")))
(418 . (lambda (&rest _) (message "Got 418.")))
(204 . (lambda (&rest _) (message "Got 202."))))
)
)
|
I simply invoke M-x dorneanu/tiddlywiki-add-quote
and read-string
will ask for a quote title, some source of the quote (e.g. a book)
and of course the actual text.
Hydra
I’ve recently discovered hydra and I came up with some hydra also for TiddlyWiki:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
(defhydra hydra-tiddlywiki (:color blue :hint nil)
"
Tiddlywiki commands^
---------------------------------------------------------
_b_ Add new bookmark
_j_ Add new journal entry
_t_ Add new tiddler
_q_ Add new quote
"
("b" dorneanu/tiddlywiki-add-bookmark)
("j" vd/tw5-journal-file-by-date)
("q" dorneanu/tiddlywiki-add-quote)
("t" dorneanu/tiddlywiki-add-tiddler))
;; Keybindings
(my-leader-def
:infix "m w"
"h" '(hydra-tiddlywiki/body :which-key "Open Tiddlywiki hydra")
"j" '(vd/tw5-journal-file-by-date :which-key "Create/Open TW5 Journal file")
"s" '(my/rg-tiddlywiki-directory :which-key "Search in TW5 directory"))
|
This way I press M m w h
and the TiddlyWiki hydra will pop up.
Conclusion
I hope some day there will be a full (elisp) package for TiddlyWiki combining some of the
functionalities/ideas mentioned here. If you have anything to add/share, please let me know.