Cross Platform Desktop App Example

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>