So last time I worked on mealplanner I mostly just wrote tests for the recipe data store and for the code that imports a Plated recipe from a PDF. They pass, so it works to some degree, but no idea if it’ll work for every possible Plated PDF. Making sure it does is what’s next on my list.

To do that, and for future parts of mealplanner, I started looking into creating a script or command that would import every Plated recipe PDF into a database that is compatible with what the recipe data store worked on. And of course, for this, I want to re-use my Plated import code that uses Tauri APIs (I definitely don’t want to write it again in node and have two copies).

So ideally, I wanted to add some sort of command line interface to mealplanner, where running it with mealplanner import-plated-recipes --recipes=/path/to/recipes/dir --out=plated-recipes.db would run the import and not start the app.

Turns out Tauri has some limited support for interacting with the CLI. You can read CLI arguments, via a getMatches() function, but that’s about it. You can’t actually write to the console (the terminal console, not the browser console). AND to top it off, passing CLI arguments to the tauri dev command appears to be broken (at the time of writing this).

But I still need this functionality, so I ended writing a tiny Tauri plugin to write to stdout/stderr and (after a lot of fumbling) found the reason for the bug and a workaround (so I wouldn’t have to build the entire app every time I wanted to run a command, either for testing or to actually do something).

The Tauri plugin is the first bit of Rust I’ve ever written (hooray?) and is a very, very tiny bit of code:

use tauri::{
  command,
  plugin::{Builder, TauriPlugin},
  AppHandle, Runtime,
};

#[command]
async fn write<R: Runtime>(
  _app: AppHandle<R>,
  message: String,
) {
  println!("{}", message)
}

#[command]
async fn ewrite<R: Runtime>(
  _app: AppHandle<R>,
  message: String,
) {
  eprintln!("{}", message)
}

/// Initializes the plugin.
pub fn init<R: Runtime>() -> TauriPlugin<R> {
  Builder::new("stdout")
    .invoke_handler(tauri::generate_handler![write, ewrite])
    .build()
}

I made a gitlab repo for it and am using it directly through git (so no need to publish to a package manager anywhere).

The bug was a bit harder to figure out. You can launch a Tauri app in development mode using the tauri dev command (run through npm, yarn or whatever). And you’re supposed to be able to supply CLI arguments to your Tauri app via tauri dev -- -- ...arguments go here..., but this did not work for me.

Running tauri dev -- -- import-plated-recipes gave me an error complaining that --no-default-features was not a valid option for mealplanner. Which is correct, it’s not, but I never supplied it.

I had to clone the Tauri repo and do some digging, but it seems there’s a small bug where if --no-default-features, an option for cargo, is not supplied already, it is simply appended to the CLI argument list, when Tauri is figuring out what to pass to cargo and what to pass to the Tauri app.

So if it’s not supplied by me, it ends up changing the command to tauri dev -- -- import-plated-recipes --no-default-features. So of course it’ll error.

The workaround I’m using is simple, just supply --no-default-features manually:

tauri dev -- --no-default-features -- import-plated-recipes

(The first -- separates the arguments that are passed to cargo, the second to the Tauri app.)

(Also, I’m very lucky Tauri devs had that little bit of code that checked for --no-default-features’s presence, otherwise I wouldn’t have a workaround, and I’d have to report the bug and wait for a fix.)

This started the app correctly, and, miraculously, the calls to my tiny Tauri plugin worked on the first try (always a welcome surprise).

Now that I’ve figured all that out, I can actually get to implementing the mass import code, which is what I’ll be doing next. And, oh, I might remember to actually report the bug. I should just do it, but, I mean…