Tauri Friction Log
This is a friction log for my first experiences with Tauri as an app developer.
Background experience
- 2 years of Rust experience at my former gig.
- Used to maintain the Rust async book for the async WG (part of the extended Rust team).
- Old time web developer, so JS is still near to my heart.
- Built an earlier version of my app in Electron.
- Passionate about developer experience.
The App
The business logic for my app is split between Go and JS. I'm planning to use Tauri for app/window management, packaging, distribution and possibly updates. The main business logic between JS and Go will be handled through streaming JSON bidirectionally over local network or unix domain sockets (not sure yet).
Trying Tauri
I looked at Tauri because Electron (and similar) is so bulky, and I no longer need Node.js for my app. Webviews are good enough today to replace Chromium as well, so why not?
Features I need
Since I had an Electron version, I already had a sense of what specifically I needed from Tauri (besides webview), so that's what I focused on prototyping:
- Background mode: Users can turn on/off background mode in which case (a) the app minimizes to tray and (b) the app auto-launches on reboot.
- Notifications: I need a couple of simple notifications to alert the user that they need to take action.
- Drag & drop files and dirs: Crucial for my use case.
- Universal binaries on MacOS: Not strictly necessary but this simplifies distribution.
- Updates: At least update notifications, ideally one-click installs.
What worked well
- Setup:
- Getting started with a hello world app was a breeze. (I spent way more time installing Rust on Windows than setting up Tauri.)
- Examples, examples, examples. They are so important - there's simply no viable replacement. Thankfully I found lots of examples directly in the repo. Kudos!
- Icon generation:
- Generating icon files was amazingly easy - much better than when I used electron!
- Loved that attention to detail that MacOS tray icon was monochrome by default.
- Feature request: Supporting two source icons (large and small) would help, since dock/taskbar icon are usually embedded in an opaque circle/squircle, and tray icon has only the logo.
- Custom commands:
- A breeze, love the macro. So far, it's been working great.
- Drag-and-drop:
- Works great so far.
- The events apply to entire window and not individual DOM elements, which could make it hard to use existing JS libraries and if you have multiple drop-zones.
- Open file/dir dialog:
- Works great.
- Unfortunately, you can't multi-select files and directories on either system - you have to choose upfront. (This worked in Electron, at least on MacOS)
- System tray:
- I couldn't add or remove system tray icon at runtime (found this issue). I decided to keep the tray icon even if user runs in foreground mode.
- Otherwise it works great.
What took work
- Auto launch:
- There was no option to auto-launch on startup in Tauri, which I think is fine. I used the auto-launch crate which worked great, but it was quite messy to get the path to the MacOS package (
/Applications/MyApp.app
) using the tauri APIs so I used std::env::current_exe()
and cd'd up from there. I'm planning to create a plugin for this.
- Bundling:
- I do dmg, deb and wix (or whatever the windows bundling is called?). It all works great, except:
- Universal binaries on MacOS wasn't supported, but I got it to work by fiddling around with the cli. See PR.
- MacOS dock/app control:
- No option to show/hide the applicaton itself (
NSApplication.show/hide
), although this exists in Tao. I forked Tauri and managed to add a window.[show/hide]_application
method (although it probably belongs on AppHandle
). Happy to send a PR.
- Tauri doesn't seem to react to clicking the dock icon (I believe this is the
NSApplication.activate
event). Due to a lucky coincidence, hide_application
(see above) solved this problem (while window.hide()
did not), I assume because MacOS by default re-shows the application when you click the dock icon.
- Show/hide dock icon at runtime (for my background mode) wasn't supported. However, I again forked Tauri and added
set_activation_policy
to the window object (again, AppHandle
would've been nicer), so I could use it at runtime. It works great! Happy to send PR.
- Summary: It feels as if the concept of "application" and its lifecycle doesn't map well to the MacOS model, instead you have to fumble with
Window
objects - which didn't work too well.
What was confusing
NOTE: Most things here are architectural, and many are probably not worth the churn of breaking changes. Also, I am new to the code base so some of these opinions may be misinformed.
- Directory hierarchy and config files
- It feels a bit awkward that the frontend code sits in root (
/package.json
) and that the main application entry point sits in a sub dir (/src-tauri
). I'd almost expect it to be the opposite.
- Is
tauri.conf.json
really necessary? Spontenously I wonder if Cargo.toml
simply could have a tauri
section instead, so everything would be in one file. I'm probably missing something though.
- State:
- I was expecting
State
to be provided as a mutable in event handlers, but I had to use interior mutability. (Although I guess this is unavoidable if you need the state outside of the event loop.)
- Platform-specific code:
- Some features are tagged behind a cfg directive based on target OS, like
Builder.set_activation_policy
. Perhaps it's better to no-op if the system doesn't support it? I had to sprinkle cfg directives in my business logic. (This is a very minor issue).
- Indirection across types/crates (internal to Tauri):
- The dependency tree across the different internal crates in Tauri, Tao, Wry and Winit was quite susbstantial - I needed to fork several projects to make relatively small changes. I also needed to create a couple of mirror types and proxy functions (boilerplate, duplication).
- Btw, not saying this is easy to simplify. Dependencies are really difficult and I understand Tauri do not control all of them.
- Builder/App/Window/Runtime separation of concerns:
- I can't quite put my finger on it, but it felt like the responsibilities and relationships between each of these entities, as well as the proxy- and handler types, were not entirely clear.
- A couple examples:
- (See above): MacOs puts a strong emphasis on the application, yet there is not much you can do with the
AppHandle
.
- There's a
Builder.on_page_load
event - doesn't that belong on the Window
type?
- In my
Builder.run
handler, I had to access the main window through the AppHandle
every time an event triggered. Perhaps events like Ready
and CloseRequested
belong to the Window type too?
- Other functions, like
Window.available_monitors
aren't window specific at all?
Up next
- Sidecar
- Includes IPC (JSON stream) between my sidecar app and the frontend
- Notifications
- Deduplicate open app:
- On some systems, you can open an app multiple times, which doesn't make sense for my case. I will need to make sure only one instance is launched at a given time.
- Notarizing, signing
- Updating
- Telemetry (not needed for a while)