A daily random note from my brain
To get myself more to think about my notes, I’ve setup automation to send me a random note from my digital brain every day at noon. I hope that when I get an email in my inbox with a random thought, it triggers additional ideas, which I can then write down again. As so far, I was not so good with revisiting old notes I’ve written once.
The system has the following components:
- A ruby script to select a random note
- A small piece of swift code to send the email (using the method described in Automating email sending with SwiftSMTP and launchd)
- A launchd agent to execute the script every day.
Selecting the note
The script runs without any arguments and picks one note from the brain. It can ignore notes with specific tags. I don’t want to get my journal files back or files I’ve archived.
#!/usr/bin/env ruby
require 'date'
# the main working directory of this script
# it will read from this folder
# path must be absolute and end with /
BRAIN_FOLDER = '<absolute path>/Brain/'
TAGS_TO_IGNORE = ['#archive', '#evening-entry', '#morning-entry', '#journal-week']
def load_file(file)
puts file
c = File.read(file).split(/[\n,\r]/)
c.join("\n")
end
def parse_title(content)
lines = content.split(/[\n]/)
if lines[0].start_with?('# ')
return lines[0][2, lines[0].length].strip()
end
return nil
end
def parse_tags(content, has_title)
lines = content.split(/[\n]/)
tag_line = has_title ? 1 : 0
raw_tags = lines[tag_line]
if !raw_tags.start_with?('#')
raw_tags = lines[tag_line + 1] # sometimes it can be on the thrid line (quotes files for example)
if !raw_tags.start_with?('#')
return []
end
end
return raw_tags.split(' ').select do |tag|
tag.start_with?('#')
end
end
def find_random_file(all_files, ignored_tags)
file = all_files.sample()
content = load_file(file)
title = parse_title(content)
tags = parse_tags(content, !title.nil?)
if tags.any?{|x| ignored_tags.include?(x)}
return nil
end
return { title: title, tags: tags, file: file, content: content }
end
def find_random_file_retry(all_files, ignored_tags)
count = 0
candidate = find_random_file(all_files, TAGS_TO_IGNORE)
while candidate.nil? and count < 30
candidate = find_random_file(all_files, TAGS_TO_IGNORE)
count += 1
end
return candidate
end
all_files = Dir["#{BRAIN_FOLDER}*.md"]
candidate = find_random_file_retry(all_files, TAGS_TO_IGNORE)
# This line hands of the selected not to the email programm
# It sends the filename without the md extension as the first argument
# and the path to the file as the second argument
%x(<ABSOLUTE_PATH_TO_MAILER>/MarkdownMailer #{File.basename(candidate[:file], '.md')} "#{candidate[:file]}")
Sending the email
The email sending programm uses the following dependencies:
We load the content of the provided file argument and render the markdown to HTML using the Ink Markdown parser. And then send this as a rich text email. The code is again a simplification as in a simple tool, and you should not save the email password in the code. But it makes the example a lot simpler and shorter.
The fileId argument is used to create a link to the original file so that when I want to write a new note, I can copy that link directly out of the email. And I don’t need to write it myself.
The generated HTML does not work right at the moment when it includes attachments; I’ve not yet added the logic for handling attachments correctly. Something for another day ;-)
To get details on how to set up Gmail to allow sending emails with this system, have a look at this post: Automating email sending with SwiftSMTP and launchd)
//
// Entry.swift
// MarkdownMailer
//
// Created by Chris on 09.01.22.
//
import Foundation
import ArgumentParser
import Ink
import SwiftSMTP
enum CustomError: Error {
// Throw when an invalid password is entered
case fail
}
@main
struct MarkdownMailer: ParsableCommand {
@Argument(help: "The file id.")
var fileId: String
@Argument(help: "The markdown file to send via email.")
var bodyFilePath: String
mutating func run() throws {
let url = URL(fileURLWithPath: bodyFilePath)
do {
let markdown = try String(contentsOf: url, encoding: .utf8)
let parser = MarkdownParser()
let html = parser.html(from: markdown)
let html2 = "<p>Note file is: [[\(fileId)]]</p>\(html)"
let inputFormatter = DateFormatter()
inputFormatter.dateFormat = "yyyy-MM-dd"
let dateString = inputFormatter.string(from: Date())
sendEmail(subject: "Hello from your Brain for \(dateString)", bodyHtml: html2)
}
catch {/* error handling here */}
}
func sendEmail(subject: String, bodyHtml: String) {
let smtp = SMTP(
hostname: "smtp.gmail.com", // SMTP server address
email: "", // username to login
password: "" // password to login
)
let from = Mail.User(name: "AutoMailer", email: "info@example.com")
let to = Mail.User(name: "Chris", email: "info@example.com")
let introduction = "<b>Random selected note to think about.</b>"
// Create an HTML `Attachment`
let htmlAttachment = Attachment(
htmlContent: "<p>\(introduction)</p>\(bodyHtml)"
)
let mail = Mail(
from: from,
to: [to],
subject: subject,
attachments: [htmlAttachment]
)
smtp.send(mail) { (error) in
if let error = error {
print(error)
MarkdownMailer.exit(withError: CustomError.fail)
}
print("Sending mail is done")
MarkdownMailer.exit()
}
dispatchMain()
}
}
Setting up the launchd service
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
"http://www.apple.
com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>ch.vmac.reminder</string>
<key>EnvironmentVariables</key>
<dict>
<key>LANG</key>
<string>en_US.UTF-8</string>
<key>LANGUAGE</key>
<string>en_US.UTF-8</string>
<key>LC_ALL</key>
<string>en_US.UTF-8</string>
</dict>
<key>Program</key>
<string>/usr/bin/ruby</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/ruby</string>
<string>/Users/chris/reminder.rb</string>
</array>
<key>StandardOutPath</key>
<string>/tmp/reminderStd.log</string>
<key>StandardErrorPath</key>
<string>/tmp/reminderErr.log</string>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>12</integer>
<key>Minute</key>
<integer>29</integer>
</dict>
</dict>
</plist>
I had a lot of issues getting this daemon to work. In the end, I found out that it was because of an encoding issue. The way to solve this was to add the StandardErrorPath argument to see the log output from my running script. It showed me the following error:
/Users/chris/reminder.rb:17:in `split': invalid byte sequence in US-ASCII (ArgumentError)
from /Users/christiaanverbree/reminder.rb:17:in `load_file'
from /Users/christiaanverbree/reminder.rb:56:in `find_random_file'
from /Users/christiaanverbree/reminder.rb:74:in `find_random_file_retry'
from /Users/christiaanverbree/reminder.rb:85:in `<main>'
And the final solution for the probelm was to set the environment variables:
<key>EnvironmentVariables</key>
<dict>
<key>LANG</key>
<string>en_US.UTF-8</string>
<key>LANGUAGE</key>
<string>en_US.UTF-8</string>
<key>LC_ALL</key>
<string>en_US.UTF-8</string>
</dict>
As discussed here Rails: Invalid byte sequence in US-ASCII (Argument Error)
Further thoughts
This script right now is very rudimentary. And there could be done a lot more. But for the moment, I will try out if it helps me to interact more with my older notes.
One of the improvements I could add would be to select notes without links (to other notes) more often. As for a real Zettelkasten, these links are critical. And right now, I have a lot of notes just swimming in my brain without any connection at all. And I was also thinking about saving the notes the script selected to prevent it from sending me the same note multiple times. But I think the chance of this happening right now is relatively small as I have quite some notes in my system already.
Comments
How to respond
Write your comment on your on page and link it to this page with the following link:
https://vmac.ch/posts/2022-01-16-daily-random-note/
Then insert the permalink to your post into the form below and submit it.
Alternatively you can reach me by email to: comment@vmac.ch