On 15 December 2022, GitHub called it a day for the ATOM IDE, which holds a special place in software history as the first real Electron application. (However, the project has found new life in a community–run fork called Pulsar).
Electron allowed developers to use standard web technologies that they were already familiar with to build cross-platform desktop applications for Windows, Mac, and Linux. This is all great for applications like Atom, VS Code, and Postman, but it is not always ideal. For example, one common criticism of Electron apps is that they often use up a lot of memory relative to what a lower-level language, like C, C++, Rust, or Go would use.
In this article, we’ll explore Tauri, a new framework for building binaries for most major desktop platforms. In the tutorial portion of the article, we’ll investigate how to create basic commands in Tauri, how to add a window menu, and how to build an application. Let’s get started!
To follow along with our simple example, you can access the full code here.
Tauri is a new framework that offers what people like most about Electron but fixes many of the security and performance concerns. Tauri offers the ability to design your UI using web technologies like HTML, CSS, and JavaScript but allows you to use lower-level languages to write the application and backend logic.
At the time of writing, Tauri primarily supports Rust as the backend language, but its API can be implemented across multiple languages. Over time, we can expect C, C++, Go, Ruby, Python, and even JavaScript backend bindings as users decide to implement them.
Like Electron, Tauri uses a multi-process approach. The Core process is essentially the backend of the application with all the data models and application logic written in Rust. The WebView process is the application UI that is built in the JavaScript framework of your choice. You can even use non-JavaScript options like Rust using Dominator or ClojureScript.
The two processes communicate either through events or commands, which are RPC calls. Either process can emit events that the other process can listen for to trigger logic in one process when the other process does a particular action.
The WebView process can issue commands to trigger pre-defined functions in the core process. These are essential RPC Calls.
To get everything installed including prerequisite system libraries, I recommend following this guide from the official docs. You’ll need Node.js and Rust installed. Once everything is set up, you can create an app by running the following command:
npx create-tauri-app
You’ll be given several options of what package manager, framework/language to use for the WebView. For this tutorial, select SolidJS with the plain JS template.
cd
into the new folder and run npm install
. Then, run npm run tauri dev
, which turns on the Core and WebView processes so you can preview it at localhost:3000
. The page essentially just displays a number that is counting each second.
Pay attention to the terminal output because there may be libraries and environmental variables you need to install to get the app to build successfully. I’m working from a POP OS, and I had to install several libraries before it had all the dependencies.
Our app, which I’ve called /Your-Tauri-App
, will use the following folder structure:
/Your-Tauri-App | |-- /node_modules //All the NPM Libraries | |-- /src // Your frontend UI Code for the WebView Process | |-- /src-tauri // Your backend code for the Core process in Rust | |-- .gitignore // files to be ignored by git |-- index.html // just the HTML file the WebView mounts to |-- package-lock.json // lockfile for npm |-- package.json // config file for npm |-- pnpm-lock.yaml // lock file for pnpm |-- readme.md // template readme |-- jsconfig.json // javascript Configurations |-- vite.config.js // Vite configurations
You can think of commands like writing your routes in the backend of a standard web application. However, instead of using fetch
to make a request to a route, we’ll use invoke
to call one of these commands from our frontend.
Let’s create a very basic command in /src-tauri/src/main.rs
:
#![cfg_attr( all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows" )] // Our Tauri Command #[tauri::command] fn return_string(word: String) -> String{ return word } fn main() { tauri::Builder::default() // Register Command with Tauri App .invoke_handler(tauri::generate_handler![return_string]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }
The #[tauri::command]
macro turns our function into a command, and we register our command with the app with the invoke_handler
method in the Tauri builder.
Then, using the invoke
method, we call this command from Solid. Let’s update our App.js
file:
import { invoke } from '@tauri-apps/api/tauri' import {createSignal} from "solid-js" function App() { const [word, setWord] = createSignal("") const handleClick = async (event) => { // invoke backend action and save result in variable const result = await invoke("return_string", { word: "You triggered the tauri action!" }) // alert the return value setWord(result) } return ( <div> <h1>Word signal is currently: {word}</h1> <button onClick={handleClick}>Trigger the Tauri Action</button> </div> ); } export default App;
In the code above, we import the invoke method import { invoke } from '@tauri-apps/api/tauri';
. We call invoke
and pass it two arguments, a string with the command’s name to invoke and an object with any argument by name.
Now, when we click on the new **Click Me**
button, the text we sent will be sent back, confirming that we successfully invoked the method from our backend. That’s it! You can create commands using Rust as well as any Rust libraries to handle data management needs for your application.
You may want to add menu options to the application window. Doing so is quite simple. From our Rust code we’ll import Tauris menu-defining tools:
//main.rs use tauri::{CustomMenuItem, Menu, MenuItem, Submenu};
We can then define menu items, like so:
// Define two sub items for the submenu let hello = CustomMenuItem::new("hello".to_string(), "Hello"); let goodbye = CustomMenuItem::new("goodbye".to_string(), "Goodbye"); // define the submenu let submenu = Submenu::new("Menu", Menu::new().add_item(hello).add_item(goodbye)); // create main menu object let menu = Menu::new() // add native copy functionality .add_native_item(MenuItem::Copy) // add our submenu .add_submenu(submenu);
Then we can pass the menu to the app builder:
fn main() { tauri::Builder::default() .menu(menu) .invoke_handler(tauri::generate_handler![return_string]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }
The entire file should look like this:
#![cfg_attr( all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows" )] use tauri::{CustomMenuItem, Menu, MenuItem, Submenu}; // Our Tauri Command #[tauri::command] fn return_string(word: String) -> String{ return word } fn main() { // Define two sub items for the submenu let hello = CustomMenuItem::new("hello".to_string(), "Hello"); let goodbye = CustomMenuItem::new("goodbye".to_string(), "Goodbye"); // define the submenu let submenu = Submenu::new("Menu", Menu::new().add_item(hello).add_item(goodbye)); // create main menu object let menu = Menu::new() // add native copy functionality .add_native_item(MenuItem::Copy) // add our submenu .add_submenu(submenu); tauri::Builder::default() .menu(menu) // Register Command with Tauri App .invoke_handler(tauri::generate_handler![return_string]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }
You should now see the menu in the window. Next, we can add code to our builder to associate actions or methods with menu item clicks:
#![cfg_attr( all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows" )] use tauri::{CustomMenuItem, Menu, MenuItem, Submenu}; // Our Tauri Command #[tauri::command] fn return_string(word: String) -> String{ return word } fn main() { // Define two sub items for the submenu let hello = CustomMenuItem::new("hello".to_string(), "Hello"); let goodbye = CustomMenuItem::new("goodbye".to_string(), "Goodbye"); // define the submenu let submenu = Submenu::new("Menu", Menu::new().add_item(hello).add_item(goodbye)); // create main menu object let menu = Menu::new() // add native copy functionality .add_native_item(MenuItem::Copy) // add our submenu .add_submenu(submenu); tauri::Builder::default() .menu(menu) //register menu events .on_menu_event(|event| { match event.menu_item_id() { "hello" => { event.window().maximize(); } "goodbye" => { event.window().minimize(); } _ => {} } }) // Register Command with Tauri App .invoke_handler(tauri::generate_handler![return_string]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }
We attach the .on_menu_event
method to pattern match depending on the menu item that is clicked. In the above code, we made the "hello"
menu item maximize the window and the "goodbye"
menu item minimize the window.
You can see many of the other methods we can invoke on the window here in the Rust docs, such as methods to close the window or quit the program.
At the time of writing, Tauri doesn’t support cross-compilation, so it’ll compile to whatever operating system you are currently on, in my case, Linux.
To enable compilation, all you have to do is run npm run tauri build
. Just make sure to update the identifier property in the tauri.config.json
to whatever you want.
For me, after the build completed, there were AppImage
and Deb
files available in src-tauri/target/bundle
.
Keep in mind we have truly two applications running simultaneously: our Solid app acting as the UI and our Rust app handling the window and backend functionality of the app. So, debugging both of these codebases can differ.
When compiling your code you may get errors relating to missing system libraries like this:
Package cairo was not found in the pkg-config search path. Perhaps you should add the directory containing `cairo.pc' to the PKG_CONFIG_PATH environment variable No package 'cairo' found Package cairo was not found in the pkg-config search path. Perhaps you should add the directory containing `cairo.pc' to the PKG_CONFIG_PATH environment variable No package 'cairo' found
You’ll need to find the paths for these files, assuming you installed all the prerequisites listed in Tauris documentation, and define the PKG_CONFIG_PATH
. For Linux, these files are likely somewhere in /usr
so run the following in order to search for them:
sudo find /usr -name cairo.pc
This should give you the exact location of the files. Then, you can construct a command like the below to rehabilitate the PKG_CONFIG_PATH
:
export PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig:$PACKAGE_CONFIG_PATH
Add this to your .bashrc
so you don’t have to run this on every new terminal session.
When debugging the Rust code, you can print messages to the terminal using the Rust print command; this can be useful for debugging messages within commands or menu events when they don’t work predictably.
For example, maybe we want to confirm the Rust app is receiving the argument from the Solid add for the command we created. In that case, we can include a debugging message in the function like so:
// Our Tauri Command #[tauri::command] fn return_string(word: String) -> String{ // debugging message println!("The frontend sent us this argument {}", word); return word }
When we trigger this command from the frontend of our app, we can see the message in our terminal and confirm receipt in Rust.
We can use JavaScript console.log
for debugging to log messages for code that occurs in the frontend WebView codebase. Usually, this code will show up in the browser developer tools, but we now have our own native window.
We can run the app with normal browser dev tools available by adding a debug
flag:
npm run tauri dev --debug
Now we can use normal keyboard shortcuts to open the browser developer tools in our running app to inspect console.log
and all the other aspects you’d expect in the context of a web browser.
Tauri opens up desktop apps using web technologies and your favorite frontend framework to better performance and smaller bundles. Tauri will certainly tempt many Electron developers to make the switch for apps that need enhanced performance.
You can find the code from this tutorial on GitHub. Although we just scratched the surface of what Tauri is capable of, I recommend reading up on the following cool features:
Happy coding!
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.