Use Rust and JS to Build a Simple Cross Platform Desktop App
- Rust Quickstart
- Tauri Quickstart
- Configure Tailwind
- Use Rust to Connect to DuckDB
- Use VueJS and Tailwind to Present Data
Tauri is very similar to Wails, except that it is based on Rust instead of Go. Both platforms let you choose from several frontend frameworks, and both platforms allow you to pass data back and forth between the JS and the compiled code. This guide will walk you through setting up Rust, Tauri, and Tailwind.
Rust Quickstart
First you need to install rustup. Warning, this executes random code from the internet.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Then you should update rustup.
rustup update
Check to make sure cargo is installed.
cargo --version
From here, you can perform a number of actions.
- create a project with
cargo new <project>
- build your project with
cargo build
- run your project with
cargo run
- test your project with
cargo test
- build documentation for your project with
cargo doc
- publish a library to crates.io with
cargo publish
Running, cargo new hello
and cargo run
will build and run a hello world program 🎉
Tauri Quickstart
Create a Tauri project. This will run a CLI installer and setup a project for you in a new directory. Warning, this executes random code from the internet.
sh <(curl https://create.tauri.app/sh)
I made the selections: tauri-app > Javascript > pnpm > Vue > Javascript. Then run,
cd tauri-app
pnpm install
pnpm tauri dev
The line pnpm tauri dev
will run the code. Any changes to the source code on the Rust or JS side will automatically update the UI.
Configure Tailwind
This was a little tricky. From the root of the directory, run the following.
pnpm install tailwindcss postcss-cli autoprefixer
npx tailwindcss init
The command npx tailwindcss init
will create a tailwind.config.js
file. Edit it to look like this:
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./src/**/*.{vue,js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
Now create postcss.config.js
at the root of the directory.
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Now put this index.css
next to main.js
in ./src/
.
@tailwind base;
@tailwind components;
@tailwind utilities;
Update main.js
to look like this.
import { createApp } from "vue";
import App from "./App.vue";
import "./index.css";
createApp(App).mount("#app");
And simplify App.vue
to look something like this.
<script setup>
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
import Greet from "./components/Greet.vue";
import "./index.css"; // -- new --
</script>
<template>
<div class="m-4">
<h1 class="text-3xl">Welcome to Tauri!</h1>
<Greet />
</div>
</template>
<style scoped>
</style>
Now you should be able to start using Tailwind.
Use Rust to Connect to DuckDB
First, add the duckdb crate by cd-ing into ./src-tauri/
and running,
cargo add duckdb
I ran into the following issue building the binary: ld: library 'duckdb' not found
, so I updated the duckdb line in my cargo.toml
file in ./src-tauri/
to this.
duckdb = { version = "0.10.2", features = [
"bundled"
] }
Then, in ./src-tauri/src/main.rs
I created a struct to model my database table, with serialization.
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use duckdb;
use serde::{Serialize, Deserialize};
#[derive(Debug)]
#[derive(Serialize, Deserialize)]
struct LogAsciiFile {
api: String,
wellname: String,
latitude: f32,
longitude: f32,
}
Then I fleshed out the fetch_data
function that will be called from VueJS.
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn fetch_data(path: &str) -> Vec<LogAsciiFile> {
let result = duckdb::Connection::open(path);
let conn = match result {
Ok(conn) => conn,
Err(err) => {
println!("Error opening connection: {:?}", err);
return Vec::new();
}
};
// query table by rows
let result = conn.prepare("SELECT * FROM log_ascii_file_table;");
let mut stmt = match result {
Ok(stmt) => stmt,
Err(err) => {
println!("Error querying: {:?}", err);
return Vec::new();
}
};
let log_ascii_file_iter = match stmt.query_map([], |row| {
Ok(LogAsciiFile {
api: row.get(1)?,
wellname: row.get(2)?,
latitude: row.get(4)?,
longitude: row.get(5)?,
})
}) {
Ok(iter) => iter,
Err(err) => {
println!("Error querying map: {:?}", err);
return Vec::new();
}
};
let mut wellnames: Vec<LogAsciiFile> = Vec::new();
for log_ascii_file in log_ascii_file_iter {
let las = log_ascii_file.unwrap();
wellnames.push(las);
}
return wellnames;
}
Finally, the main
function exposes Rust functions to VueJS.
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![fetch_data])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Use VueJS and Tailwind to Present Data
This imports an invoke
function that can be used to call Rust functions from VueJS. Then we wrap the Rust fetch_data
function with a JS fetch_data
function, and call it on a form submit in the Vue template.
<script setup>
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/tauri";
import TableRow from "./TableRow.vue";
const result = ref("");
const path = ref("");
let showTable = false;
function fetch_data() {
invoke("fetch_data", { path: path.value }).then((resp) => {
result.value = resp;
if (result.value.length > 0) {
showTable = true;
}
});
}
</script>
<template>
<form class="row" @submit.prevent="fetch_data">
<div class="my-2 flex">
<button class="border rounded p-1 px-2 mr-2 bg-blue-500 text-white" type="submit">Run</button>
<input class="w-full border rounded p-1" v-model="path" placeholder="Enter a path..." />
</div>
</form>
<div v-show="showTable" class="mt-4">
<table>
<thead>
<tr>
<th class="text-left">API</th>
<th class="text-left">Well Name</th>
<th class="text-left">Latitude</th>
<th class="text-left">Longitude</th>
</tr>
</thead>
<tbody v-for="row in result" :key="row.id">
<table-row :api="row.api" :wellname="row.wellname" :latitude="row.latitude" :longitude="row.longitude" />
</tbody>
</table>
</div>
</template>