45: Documents and Programs
Following one with the theme of last week, my work on this little note-taking tool prototype hasn't been the most practical, but it has been fun.
Moving around documents
I started by implementing basic vim movements in the editor. While this isn't useful in the long term ( I plan to re-implement the edtior using CodeMirror at some point) its a pretty huge quality of life boost as it get's this prototype working the way the rest of my computer does.
The most basic step here is getting making the keys hjkl
work like the arrow
keys, h
and l
corresponding to left and right, and j
and k
, down and
up. The magic comes from a) not having to move your hands from their default
typing position and b) having these keys do the same thing everywhere.
Vim of course is a whole lot more than just moving a cursor around with these keys, it's a grammar for manipulating text . You have different verbs, like copy, paste, delete, and nouns like word, or line. Most of the richness is enabled by it's modal nature. It seperates navigating around text and editting text into two seperate modes you can toggle between. This frees up the entire keyboard to be used as commands for interacting, instead of having everything be a shortcut.
Interacting with programs
In this application, though, you're not only interacting with text documents, there are also programs. These are special documents that are are dynamic. They change based on some internal state. You can modify that state by sending the document messages.
For example here's a program I wrote to make a meditation log. It just renders out a list of meditation sessions, and you can send it a message with a time and some text and it'll create a new entry.
const formatHelp = `Submit meditation log prefixed with minutes meditated`
const dateParams = {month: 'short', day: "2-digit", year: 'numeric'}
return {
render: async (state, ctx) => {
let sessions = state.sessions || []
let sessionsString = (await Promise.all(sessions.map(async (session)=> {
let doc = await ctx.get(session.document)
return `- [[${doc.title}]] ${session.length} minutes`
}))).join('\n\n')
return `${formatHelp}
${sessionsString}
`
},
update: async (msg, state, ctx) => {
let minutes = msg.slice(0, msg.indexOf(' '))
let content = msg.slice(msg.indexOf(' ') + 1)
let date = new Date()
let entry = await ctx.create(`Meditation - ${date.toLocaleDateString([], dateParams)}`, content)
let sessions = state.sessions || []
sessions.push({document: entry.id, date: date.toISOString(), length: parseInt(minutes)})
return {sessions}
}
}
I actually had functionality similar to this last week, except for the ctx
pieces you see in there. That's what lets these programs read and write to other
documents. It's a particularly interested corner of the API to explore, as
there's a bunch of differnet ways you could set it up. The main constraint is
trying to maintain meaningful links, even when it's programs connecting and not
plain documents.
Anyways, can you spot where the program gets extremely annoying? It's hinted
at a bit in the first line. In the first two lines of the update
function, I
have to parse out the minutes meditated from the whole message.
While this is simple enough to do, it means that every single program has to define it's own grammar for how it responds to inputs, and more complicated ones could get even trickier.
I also want to be able to fluently move between editing the source code of a program, viewing it's output, interacting with. I want to be able to combine interactions across multiple different documents!
To me, the obvious answer here is code. If the documents are defining their interactions in code, why not just let the user execute those functions directly? Essentially, give every program a command-line at the bottom. 1
This would enable chaining functions together, as well as more explicit interactions overall. Where you lose out is in syntax overhead. Every interaction needs to have parentheses, quotes, and all the other noise. I think the tradeoffs are worth it, but it's also worth thinking about where we can do better.
How can they combine?
The next step for me, combining the two interaction modes, vim keys and executing code to interact with documents, into one. One approach is binding code to key combinations, and I think that can work really well. An alternative could be making it easy to execute code written in a document.
-
One of the things I've been struggling with is how many different things this project rhymes with. It's kinda like emacs but with javascript. Or, it's kind of like literate programming. Or kinda like a fancy REPL. All of the above? ↩