Tauri: Mixing JavaScript With Rust for GUI Desktop Apps
We revisit Tauri, a framework to build desktop applications with any frontend framework and a Rust core. We check out the version 2.0 beta.
Jul 27th, 2024 5:00am by
Photo by Tengyart on Unsplash.
We get the security of Rust but the familiarity and flexibility of web development.We’ll try and see if the path has gotten a bit smoother to building a UI app that I can run fully packaged on my Mac. Tauri still refers to itself a ‘toolkit’, which is still true. Conceptually, Tauri acts as a static web host. So Tauri works with Rust crates and the system’s native web view to output a modest-sized executable application. In theory, we get the security of Rust but the familiarity and flexibility of web development. The getting started route is looking a bit fresher, with the now popular single line start. Before we do, I suspect that I have an old Rust installation, so I should update this. Using the prerequisite instructions:
At the end, it reminds you to start a new shell or to source the env file. I note a new friendlier accent to all this — as if, maybe, Rust is now popular!
Ok, now I should be able to use the Tauri one-liner:
Note that we are already going into the beta for Tauri 2.0.
The template install options recognize the more varied nature of the toolkit. I could use .NET, but I’ll use JavaScript for a more general-purpose view. Obviously, Rust is also available.
I kept my slightly old npm / node combination and built my template:
Then we run the template within the dev environment:
This builds all the packages we need to start with and the first time takes a few minutes. These will be how Rust talks to your OS windowing. And eventually, it launches the application:
So we have an app started and popping up, appearing in my tray as a standard Mac app.
OK, let’s take a look at how this is made up. Before we dive in, note that hitting the icons starts a browser page, and entering your name in the text box and pressing the button displays a greeting:
This will help us work out the bit of Rust later on. The code structure is what one would expect for a web app:
I chose vanilla JavaScript, so we get a very vanilla index.html in our template:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="styles.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri App</title>
<script type="module" src="/main.js" defer></script>
</head>
<body>
<div class="container">
<h1>Welcome to Tauri!</h1>
<div class="row">
<a href="https://tauri.app" target="_blank">
<img src="/assets/tauri.svg" class="logo tauri" alt="Tauri logo" />
</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank" >
<img src="/assets/javascript.svg" class="logo vanilla" alt="JavaScript logo" />
</a>
</div>
<p>Click on the Tauri logo to learn more about the framework</p>
<form class="row" id="greet-form">
<input id="greet-input" placeholder="Enter a name..." />
<button type="submit">Greet</button>
</form>
<p id="greet-msg"></p>
</div>
</body>
</html>
div displays an image in an anchor, which deals with the link behavior. Note that the JavaScript is in main.js, and that the app title on the window itself is not that which is defined here. And we have a very old school form for entering the input text. So we know that we will have to process that form to extract the entered name, and place the result in the final p. This is the content of main.js:
const { invoke } = window.__TAURI__.core;
let greetInputEl;
let greetMsgEl;
async function greet() {
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value });
}
window.addEventListener("DOMContentLoaded", () => {
greetInputEl = document.querySelector("#greet-input");
greetMsgEl = document.querySelector("#greet-msg");
document.querySelector("#greet-form").addEventListener("submit", (e) => {
e.preventDefault();
greet();
});
});
And in that we have some Rust code within src, in main.rs:
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
{
"productName": "thenewstack",
"version": "0.0.0",
"identifier": "com.tauri.dev",
"build": { "frontendDist": "../src" },
"app": { "withGlobalTauri": true, "windows": [
{
"title": "thenewstack",
"width": 800,
"height": 600
}
],
"security": { "csp": null } },
"bundle": { "active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
...
]
}
}
{
...
"identifier": "io.thenewsatck",
...
"app" : {
"windows": [
{
"title": "Welcome to TheNewStack",
"width": 600,
"height": 200
}
...
},
...
}
}
Then we alter the message code appropriately. This will force the build to check for changes.
Finally, we run the full build, to see what it does with the executable.
This also takes time, since it is the first time. The result is a dmg and an app file. Once we move the app into the application folder, we can execute it as a normal Mac app:
The app size is still a little chubby (10.7 MB), but I have done nothing to pare down the crates that would automatically get added to the template.
Conclusion
I think we get from zero to hero very quickly with the template, although the flexibility of allowing for a range of JavaScript frameworks does make everything a little more complex. I wonder if a more opinionated approach might be better. But overall I think Tauri is still a very solid solution to creating desktop apps without worrying about window internals.
YOUTUBE.COM/THENEWSTACK
Tech moves fast, don't miss an episode. Subscribe to our YouTube
channel to stream all our podcasts, interviews, demos, and more.