Alvaro Ramirez: A richer Journelly org capture template
A richer Journelly org capture template
In addition to including user content, Journelly entries typically bundle a few extra details like timestamp, location, and weather information, which look a little something like this:
Behind the scenes, Journelly entries follow a fairly simple org structure:
* [2025-04-23 Wed 13:24] @ Waterside House :PROPERTIES: :LATITUDE: 51.518714352892665 :LONGITUDE: -0.17575820941499262 :WEATHER_TEMPERATURE: 11.4°C :WEATHER_CONDITION: Mostly Cloudy :WEATHER_SYMBOL: cloud :END: Try out Rice Guys #food #london on Wednesdays in Paddington [[file:Journelly.org.assets/images/C5890C25-5575-4F52-80D9-CE0087E9986C.jpg]]
While out and capturing entries from my iPhone, I rely on Journelly to leverages iOS location and weather APIs to include relevant information. On the other hand, when capturing from my Macbook, I rely on a basic Emacs org capture template (very similar to Jack Baty's):
(setq org-capture-templates '(("j" "Journelly" entry (file "path/to/Journelly.org") "* %U @ Home\n%?" :prepend t)))
These templates yield straightforward entries like:
* [2025-05-16 Fri 12:42] @ Home A simple entry from my Macbook.
I've been using this capture template for some time. It does a fine job, though you'd notice location and weather info aren't captured. No biggie, since the location of my laptop isn't typically relevant, but hey today seemed like a perfect day to get nerd snipped by @natharari.
And so, off I went, to look for a utility to capture location from the command line. I found CoreLocationCLI, which leverages the equivalent macOS location APIs. As a bonus, the project seemed active (modified only a few days ago).
Installing CoreLocationCLI via Homebrew was a breeze:
brew install corelocationcli
The first time you run corelocationcli, you'll get an message like:
"CoreLocationCLI" can't be opened because it is from an unidentified developer...
You'll need to follow CoreLocationCLI's instructions:
To approve the process and allow CoreLocationCLI to run, go to System Settings ➡️ Privacy & Security ➡️ General, and look in the bottom right corner for a button to click.
After approving the process, I ran into a snag:
$ CoreLocationCLI CoreLocationCLI: ❌ The operation couldn’t be completed. (kCLErrorDomain error 0.)
Lucky for me, the README had the solution:
Note for Mac users: make sure Wi-Fi is turned on. Otherwise you will see kCLErrorDomain error 0.
Oddly, my WiFi was turned on, so I went ahead and toggled it. Success:
$ CoreLocationCLI 51.51871 -0.17575
We can start by wrapping this command-line utility return coordinates along with reverse geolocation (ie. description):
(defun journelly-get-location () "Get current location. Return in the form: `((lat . 51.51871) (lon . -0.17575) (description . \"Sunny House\")) Signals an error if the location cannot be retrieved." (unless (executable-find "CoreLocationCLI") (error "Needs CoreLocationCLI (try brew install corelocationcli)")) (with-temp-buffer (if-let ((exit-code (call-process "CoreLocationCLI" nil t nil "--format" "%latitude\t%longitude\t%thoroughfare")) (success (eq exit-code 0)) (parts (split-string (buffer-string) "\t"))) `((lat . ,(string-to-number (nth 0 parts))) (lon . ,(string-to-number (nth 1 parts))) (description . ,(string-trim (nth 2 parts)))) (error "No location available"))))
Now that we're able to get the current location, we need a way to fetch weather info. I discarded using WeatherKit on macOS, since it requires setting up a developer account and obtaining an API key. No worries, I found the great MET Norway API which is freely available without the need for keys.
(defun journelly-fetch-weather (lat lon) "Fetch weather data from MET Norway API for LAT and LON. Return the parsed JSON object." (let* ((url (format "https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=%s&lon=%s" lat lon)) (args (list "-s" url))) (with-temp-buffer (apply #'call-process "curl" nil t nil args) (goto-char (point-min)) (json-parse-buffer :object-type 'alist))))
We can take it for a spin with:
(journelly-fetch-weather 51.51871 -0.17575)
We get a nice object with a chunky time series (cropped for readability):
((type . "Feature") (geometry (type . "Point") (coordinates . [-0.1758 51.5187 30])) (properties (meta (updated_at . "2025-05-16T11:17:44Z") (units (air_pressure_at_sea_level . "hPa") (air_temperature . "celsius") (cloud_area_fraction . "%") (precipitation_amount . "mm") (relative_humidity . "%") (wind_from_direction . "degrees") (wind_speed . "m/s"))) (timeseries . [((time . "2025-05-16T12:00:00Z") (data (instant (details (air_pressure_at_sea_level . 1025.6) (air_temperature . 18.0) (cloud_area_fraction . 4.7) (relative_humidity . 44.2) (wind_from_direction . 17.6) (wind_speed . 3.6))) (next_12_hours (summary (symbol_code . "fair_day")) (details)) (next_1_hours (summary (symbol_code . "clearsky_day")) (details (precipitation_amount . 0.0))) (next_6_hours (summary (symbol_code . "clearsky_day")) (details (precipitation_amount . 0.0))))) ... ((time . "2025-05-26T00:00:00Z") (data (instant (details (air_pressure_at_sea_level . 1007.3) (air_temperature . 12.6) (cloud_area_fraction . 28.1) (relative_humidity . 91.3) (wind_from_direction . 258.7) (wind_speed . 3.5)))))])))
Journelly entries need only a tiny subset of the returned object, so let's add a helper to extract and format as preferred.
(defun journelly-fetch-weather-summary (lat lon) "Fetch weather data from MET Norway API for LAT and LON. Return in the form: '((temperature . \"16.9°C\") (symbol . \"cloudy\"))." (let* ((data (journelly-fetch-weather lat lon)) (now (current-time)) (entry (seq-find (lambda (entry) (let-alist entry (time-less-p now (date-to-time .time)))) (let-alist data .properties.timeseries))) (unit (let-alist data .properties.meta.units.air_temperature))) (unless entry (error "Couldn't fetch weather data")) (let-alist entry `((temperature . ,(format "%.1f%s" .data.instant.details.air_temperature (cond ((string= unit "celsius") "°C") ((string= unit "fahrenheit") "°F") (t (concat " " unit))))) (symbol . ,(alist-get 'symbol_code .data.next_1_hours.summary))))))
We can take it for a spin with:
(journelly-fetch-weather-summary 51.51871 -0.17575)
Nice! Look at that, it's a sign that I should finish writing and go outside!
'((temperature . "19.0°C") (symbol . "clearsky_day"))
I really should go outside, I'm just so close now… Or so I thought! That symbols (ie. "clearsky_day") isn't recognizable by Journelly, which relies on SF Symbols returned by WeatherKit. I need some sort of mapping between these symbols. Gosh, I need to go outside and this is a perfect task for a robot! Whipped out chatgpt-shell and asked the LLM robots to take on this grunt work, who gave me:
{ "clearsky_day": "sun.max", "clearsky_night": "moon.stars", "clearsky_polartwilight": "sun.horizon", ... "snowshowers_and_thunder_day": "cloud.sun.bolt.snow", "snowshowers_and_thunder_night": "cloud.moon.bolt.snow", "thunderstorm": "cloud.bolt" }
We're in elisp land so who wants json? Hey robot, I need an alist:
Won't the LLM make mapping errors? Most certainly! But for now, I'm just getting a rough prototype and I need to get moving if I want to go outside!
We plug our mapping into an elisp function
(defun journelly-resolve-metno-to-sf-symbol (symbol) "Resolve Met.no weather SYMBOL strings to a corresponding SF Symbols." (let ((symbols '(("clearsky_day" . "sun.max") ("clearsky_night" . "moon.stars") ("clearsky_polartwilight" . "sun.horizon") ... ("snowshowers_and_thunder_day" . "cloud.sun.bolt.snow") ("snowshowers_and_thunder_night" . "cloud.moon.bolt.snow") ("thunderstorm" . "cloud.bolt")))) (map-elt symbols symbol)))
Does it work? Kinda seems like it.
(journelly-resolve-metno-to-sf-symbol (map-elt (journelly-fetch-weather-summary 51.51871 -0.17575) 'symbol))
"sun.max"
We got everything we need now, let's put the bits together:
(defun journelly-generate-metadata () (let* ((location (journelly-get-location)) (weather (journelly-fetch-weather-summary (map-elt location 'lat) (map-elt location 'lon)))) (format "%s :PROPERTIES: :LATITUDE: %s :LONGITUDE: %s :WEATHER_TEMPERATURE: %s :WEATHER_SYMBOL: %s :END:" (or (map-elt location 'description) "-") (map-elt location 'lat) (map-elt location 'lon) (alist-get 'temperature weather) (journelly-resolve-metno-to-sf-symbol (alist-get 'symbol weather)))))
Lovely, we now get the metadata we need.
Waterside House :PROPERTIES: :LATITUDE: 51.51871 :LONGITUDE: -0.175758 :WEATHER_TEMPERATURE: 18.5°C :WEATHER_SYMBOL: sun.max :END:
Damn, the temperature is dropping. I really do need to go outside. So close now!
All we have to do is plug our journelly-generate-metadata
function into our org template and… Bob's your uncle!
(setq org-capture-templates '(("j" "Journelly" entry (file "path/to/Journelly.org") "* %U @ %(journelly-generate-metadata)\n%?" :prepend t)))
We can now invoke our trusty M-x org-capture
and off we go…
While the code currently lives in my Emacs config, it's available on GitHub. If you do take it for a spin, it may crash and burn. I blame the weather. In the UK, when sunny, you rush to go outside! 🌞🏃♂️💨
-1:-- A richer Journelly org capture template (Post Alvaro Ramirez)--L0--C0--2025-05-16T14:30:37.000Z