I posted My Raku script for Journaling in Joplin on Saturday, 22 May, 2021

Why mess with getopt when I can just use multi-dispatch?

My Raku script for Journaling in Joplin
Post Raku Lang joplin shell

Part of the Journaling in Joplin With Raku series.

My Raku script for Journaling in Joplin

Setting the scene

So yesterday I wrote a couple of one-liners for managing journal entries in the Joplin note-taking application, with help from Raku.

I made one for writing:

joplin use Journal && joplin edit $(date --iso=minute)

I made one for reading:

joplin use Journal \
  && raku -e '
    for qx{ joplin ls }.lines.sort {
      qqx{ joplin cat $_ }.subst(
        /^(<[\dT:\-]>+)/, { "# $0" }
      ).say
    }' \
  | python -m rich.markdown -

They work. That's great.

They don't work great, though. Mainly the one-liner for reading. It dumps every journal entry, which is both overwhelming and slow. Overwhelming because I only want to see today's journal entries most of the time. Slow because every one of those entries requires a separate call to joplin. Joplin is lovely, but it expects to be used as a persistent application. The command line functionality is optimized for convenience. It is not optimized for being hammered repeatedly by an overenthusiastic command line script.

I can fix the overwhelming. I can't properly fix the slow until I learn more about the Joplin API. At least I can make the experience less awful.

Make it less awful

Less hard-coding please

First things first. I might change the name of my journal notebook. You might want a daily diary rather than a giant stack of entries.

constant $notebook     = "Journal";
constant $entry-window = "minute";

Focusing on daily journals? Set $entry-window to "day".

Adding an entry

sub add-entry() {
  my $timestamp = DateTime.now.truncated-to($entry-window);
  my $command = "joplin use $notebook && joplin edit $timestamp";
  shell( $command );
}

Raku's DateTime classes provide the gist of what we got with GNU Date. truncated-to rounds our current timestamp — now — down to the minute.

$ raku -e 'DateTime.now.say'
2021-05-22T11:52:28.380996-07:00

$ raku -e 'DateTime.now.truncated-to("minute").say'
2021-05-22T11:52:00-07:00

It doesn't print exactly the same as date:

$ date --iso=minute
2021-05-22T11:52-07:00

It wouldn't take excessive effort to make them match, but I'm just not concerned about it at the moment.

Use a MAIN sub

Well, we went and put the logic for adding an entry into a function. We want to call that function at some point, right?

sub MAIN() {
  add-entry;
}

We don't need MAIN yet. If present, it's your Raku script's official entry point. If not, you have a plain old script. You're good either way. Having that entry point will make things easier to manage in a minute, though.

If we run this as-is, it adds a new entry.

$ raku journal
Note does not exist: "2021-05-22T12:18:00-07:00". Create it? (Y/n) y
...
Note has been saved.

Okay, fine. It works. So far it's neither tidier nor more readable than the initial one-liner. But writing an entry was never the problem.

The problem was reading the entries.

Read all the entries?

Back in our one-liner, collecting entries and reading them got smushed together. They're two distinct actions, though.

sub all-entries() {
  qqx{joplin use $notebook && joplin ls}.lines.sort;
}

sub read-entries(@entries) {
  @entries.map({
    qqx{ joplin cat $_}.subst(/^(<[\dT:\-]>+)/, { "# $0" })
  }).join;
}

Right. Now we have subs for writing, and subs for reading. How do we want to get at them? Maybe a callback table with action keywords? Maybe a fancy module?

Nope! Well — we could. But we don't need to. Raku has multi-dispatch!

Use multiple MAIN subs!

We replace the initial MAIN definition with these:

multi sub MAIN("add") {   #= Add an entry
  add-entry;
}

multi sub MAIN("read") {  #= Read all entries
  say read-entries(all-entries);
}

multi tells Raku to expect multiple definitions for this sub. Without it, the compiler gets annoyed.

Multiple dispatch means a few things depending on which language you're using — or which computer scientist you're asking. Basically it lets you avoid having one giant glob of a function with all sorts of special logic. You do that by having a different version of the function for different situations.

Where I got surprised? Most of the tiny amount I've read out there for multiple dispatch talks about basing on types or pattern matching against variables. You could absolutely do that with Raku.

multi sub MAIN(Str $action where { $action == "add" }) { ... }

multi sub MAIN(Str $action where { $action == "read" }) { ... }

Thing is, we're not doing anything with $action. We read if the first command line argument is read. We add if it's add. Raku is happy enough matching that first argument against literal strings. I assume other multi-dispatch languages can to the same, but nobody's been uncouth enough to bring it up in polite company. It's always "this type" and "that pattern" or "this enum."

What happens if we try to run the script with no arguments? Well, if we didn't remove that initial version of MAIN we get a compiler error about redefining the sub. But once that's out of the way, we no longer have a default path into the application!

No worries. Raku's special handling of the entry point sub shows us the accepted usage.

$ raku journal.raku
Usage:
  journal.raku add -- Add an entry
  journal.raku read -- Read all entries

And that's where those #= comments come in. They provide extra detail for the usage message displayed.

Let's try them out. We'll add an entry and then make sure it shows up when we read them.

$ raku journal.raku add
Note does not exist: "2021-05-22T12:55:00-07:00". Create it? (Y/n)
...
Note has been saved.
$ raku journal.raku read
...
# 2021-05-22T12:55:00-07:00

[multi-dispatch]: https://docs.raku.org/language/functions#Multi-dispatch

Raku Joplin journaling script, now with [multi-dispatch][]!

Nice. Of course, at this point I'm being handed 142 lines of text, and it's taking about 12 seconds to do it. All that work and we finally reached the full "overwhelming and slow" point we were at this morning.

Things are about to improve!

I just want today

I've been carefully using ISO 8601 format for my entries. That means I can filter to a specific date — or year, month, hour, etc — by constructing a date fragment and grabbing each entry that starts with the fragment.

sub filtered-entries(Str $date-funnel) {
  all-entries.grep({ .starts-with($date-funnel) });
}

Getting today's entries then becomes a matter of extracting a yyyy-mm-dd string from the ever-so-helpful DateTime.

sub entries-for-today() {
  filtered-entries DateTime.now.yyyy-mm-dd
}

Note

2021-05-24
@b2gills mentioned that I could also use Date.today here!

I haven't messed with parameter coercion yet, but that looks like it will come in handy.

Need to touch up my reading MAIN definition, though.

multi sub MAIN("read") {  #= Read today's entries
  say read-entries( entries-for-today );
}

Does it work? Is it fast?

$ time raku journal.raku read
# 2021-05-22T08:12:00-07:00

Millie let me sleep in until 7:54. How gracious.

[@liztormato]: https://twitter.com/liztormato
[Reddit]: https://www.reddit.com/r/rakulang/comments/nif2sf/cli_journaling_in_joplin_with_raku_brian_wisti/

Oh and last night's Joplin / Raku post got some legs. [@liztormato][] even
shared it on [Reddit][]. That's cool. I always hope they like it when I post
something about #RakuLang.

# 2021-05-22T10:43:00-07:00

My note script needs an option for "read yesterday's notes."

# 2021-05-22T12:18:00-07:00 Making sure that my Raku Joplin journaling script lets me add an entry.

# 2021-05-22T12:55:00-07:00

[multi-dispatch]: https://docs.raku.org/language/functions#Multi-dispatch

Raku Joplin journaling script, now with [multi-dispatch][]!

real    0m3.815s
user    0m3.966s
sys     0m0.502s

It works. It's — it's not fast by any means, but 3.8 seconds is much faster than 12. Again, there's an API waiting for when I'm bored of abusing Joplin's command line conveniences.

And maybe yesterday

Most of my deep dives into the journal will be from inside the Joplin app. A quick glance at yesterday's notes could still be useful.

Of course a DateTime lets me ask for an earlier DateTime.

sub entries-for-yesterday() {
  my $yesterday = DateTime.now.earlier(days => 1);  # or :1day for the terse
  filtered-entries $yesterday.yyyy-mm-dd;
}

For MAIN I could add and match against a subcommand. I think instead I will adjust my top-level commands to reflect the most common cases.

multi sub MAIN("today") {  #= Read today's entries
  say read-entries( entries-for-today );
}

multi sub MAIN("yesterday") { #= Read yesterday's entries
  say read-entries( entries-for-yesterday );
}

And it works!

$ raku journal.raku yesterday
# 2021-05-21T09:00-07:00

Alarm 07:00, stayed in bed as long as I could. Thanks to the dogs, that was 15
minutes. Oh well.

[Homebrew]: https://brew.sh
[Nix]: https://nixos.org/
[using Nix on Debian]: https://ariya.io/2020/05/nix-package-manager-on-ubuntu-or-debian

Got the Raspberry Pi 4 set up with Raspbian, and the 500GB external drive
attached. Thinking about package managers. I know [Homebrew][] but I could
maybe try [Nix][]. There's a post about [using Nix on Debian][].
...
# 2021-05-21T21:29:00-07:00

Just about to post my Raku Joplin Journaling One-liners, but maybe a couple
screenshots? People love screenshots.

Maybe they do. Maybe they don't. *I* love screenshots.

What about formatting?

Honestly? I'm not going to worry about it right now. Piping to Rich or Glow suffices when I want it pretty.

╔══════════════════════════════════════════════════════════════════════════════╗
║                          2021-05-22T08:12:00-07:00                           ║
╚══════════════════════════════════════════════════════════════════════════════╝

Millie let me sleep in until 7:54. How gracious.

Oh and last night's Joplin / Raku post got some legs. @liztormato even shared it
on Reddit. That's cool. I always hope they like it when I post something about
#RakuLang.

╔══════════════════════════════════════════════════════════════════════════════╗
║                          2021-05-22T10:43:00-07:00                           ║
╚══════════════════════════════════════════════════════════════════════════════╝

My note script needs an option for "read yesterday's notes."

╔══════════════════════════════════════════════════════════════════════════════╗
║                          2021-05-22T12:18:00-07:00                           ║
╚══════════════════════════════════════════════════════════════════════════════╝

Making sure that my Raku Joplin journaling script lets me add an entry.

╔══════════════════════════════════════════════════════════════════════════════╗
║                          2021-05-22T12:55:00-07:00                           ║
╚══════════════════════════════════════════════════════════════════════════════╝

Raku Joplin journaling script, now with multi-dispatch!

╔══════════════════════════════════════════════════════════════════════════════╗
║                          2021-05-22T14:05:00-07:00                           ║
╚══════════════════════════════════════════════════════════════════════════════╝

Guess I'm about ready to post the second Raku Joplin journaling post. Ah, the
joys of hyperfocus.

Better stretch my legs and get back to the official task list after.

The complete script

#!/usr/bin/env raku

constant $notebook     = "Journal";
constant $entry-window = "minute";

sub add-entry() {
  my $timestamp = DateTime.now.truncated-to($entry-window);
  my $command = "joplin use $notebook && joplin edit $timestamp";
  shell( $command );
}

sub all-entries() {
  qqx{joplin use $notebook && joplin ls}.lines.sort;
}

sub filtered-entries(Str $date-funnel) {
  all-entries.grep({ .starts-with($date-funnel) });
}

sub entries-for-today() {
  filtered-entries DateTime.now.yyyy-mm-dd;
}

sub entries-for-yesterday() {
  my $yesterday = DateTime.now.earlier(days => 1);  # or :1day for the terse
  filtered-entries $yesterday.yyyy-mm-dd;
}

sub read-entries(@entries) {
  @entries.map({
    qqx{ joplin cat $_}.subst(/^(<[\dT:\-]>+)/, { "# $0" })
  }).join;
}

multi sub MAIN("add") {   #= Add an entry
  add-entry;
}

multi sub MAIN("today") {  #= Read today's entries
  say read-entries( entries-for-today );
}

multi sub MAIN("yesterday") { #= Read yesterday's entries
  say read-entries( entries-for-yesterday );
}

multi sub MAIN("all") { #= Read all entries (SLOW!)
  say read-entries( all-entries );
}

Social

Got a comment? A question? More of a comment than a question? Talk to me about this post!

Indieweb Social

Did you mention this somewhere? I'd love it if you sent me the link!

disclaimer about timing

Mentions are sent to webmention.io. I fetch the latest mentions when building the site, so I may not see your feedback right away. Especially if my site's broken, which is often the case.

Public replies and mentions might be shared on the site, but I try to do a little quality check first.

Site Links