Now Reading
Constructing a Hybrid Native Utility With Gleam and Tauri

Constructing a Hybrid Native Utility With Gleam and Tauri

2024-02-19 15:51:19

I took a couple of hours this weekend to experiment with constructing a hybrid
native app with Gleam and Tauri. This submit is a abstract of that undertaking. If
you’d similar to to see the code, I’ve printed that at:

https://forge.wezm.net/wezm/gleam-tauri-experiment

Screenshot of the application showing a name field, minus button, plus button, Greet button and the current time.

Screenshot of the appliance.

Introduction

Gleam is statically typed useful language initially written to focus on
the Erlang digital machine. Now it additionally has a JavaScript back-end that permits
Gleam code to run within the browser in addition to in node.js and Deno. The generated
JavaScript is kind of readable just like Elm and ReScript/ReasonML.

Gleam appeals to me as an possibility for writing front-end code as a result of it’s
stricter than TypeScript, has nominal sorts, is quick to compile, has a pleasant
all-in-one developer expertise like cargo with the gleam CLI.

One of many issues that makes writing front-end functions in Gleam possible
is the pleasant Lustre bundle. It’s an implementation of the Elm
architecture
in Gleam. For those who’ve used Elm a Lustre software will look
extraordinarily acquainted. On this context Gleam is sort of like an actively
maintained Elm with out the restrictions on interop with current JavaScript
code.

To get began right here’s some Gleam code that demonstrates a good chunk of the
language. My weblog doesn’t spotlight Gleam code for the time being so what’s proven
under is an image. See example.gleam for the supply file:

When run it outputs:

Celcius(1.8444444444444443)

The generated JavaScript (as of Gleam v1.0.0-rc2) is proven under. Whereas it’s
actually longer than what you would possibly naively write in JavaScript immediately it’s
fairly clear what’s happening.

import * as $int from "../gleam_stdlib/gleam/int.mjs";
import * as $io from "../gleam_stdlib/gleam/io.mjs";
import * as $record from "../gleam_stdlib/gleam/record.mjs";
import { toList, CustomType as $CustomType, divideFloat } from "./gleam.mjs";

export class F extends $CustomType {
  constructor(x0) {
    tremendous();
    this[0] = x0;
  }
}

export class C extends $CustomType {
  constructor(x0) {
    tremendous();
    this[0] = x0;
  }
}

export class Celcius extends $CustomType {
  constructor(x0) {
    tremendous();
    this[0] = x0;
  }
}

perform to_c(temp) {
  if (temp instanceof C) {
    let c = temp[0];
    return new Celcius(c);
  } else {
    let f = temp[0];
    return new Celcius(divideFloat((f - 32.0), 1.8));
  }
}

export perform avg(measurements) {
  let sum = $record.fold(
    measurements,
    0.0,
    (sum, val) => {
      let $ = to_c(val);
      let c = $[0];
      return sum + c;
    },
  );
  let size = (() => {
    let _pipe = $record.size(measurements);
    return $int.to_float(_pipe);
  })();
  return new Celcius(divideFloat(sum, size));
}

export perform most important() {
  let temps = toList([
    new C(22.0),
    new C(-5.0),
    new F(0.0),
    new C(0.0),
    new F(32.0),
  ]);
  return $io.debug(avg(temps));
}

Constructing a Hybrid Native App

Tauri is a framework for constructing hybrid native functions. By that I imply
an software that makes use of native code for the back-end and net expertise for the
person interface. That is just like Electron besides that Tauri doesn’t embrace
a replica of Chromium in each software, as an alternative counting on the system net view
on the host working system.

You implement your software logic in Rust and talk with the UI
by emitting and itemizing to occasions. The tip result’s a cross-platform desktop
app that may be a lot smaller than if it had been constructed with Electron.

This weekend I made a decision to attempt combining this stuff to see how possible it
could be to construct a hybrid desktop app with Gleam and Tauri. I began by
following the Tauri guide for setting up a Vite project. Vite
is a bundler that takes care of reworking supply recordsdata on the front-end as
nicely is offering a pleasant auto-reloading growth expertise.

As soon as that was working I initialised a Gleam undertaking in the identical listing:

gleam new --name gleamdemo gleam-demo

Notice: I initially known as my software videopls there are nonetheless some
references to it within the code.

I then adopted Erika Rowland’s guide to using Gleam with Vite. This
resulted in a easy counter demo operating within the Tauri window. At this level
the Gleam code was nearly equivalent to Erika’s submit.

Screenshot of the application showing a counter with plus and minus buttons

Section 1 full.

Now got here the uncharted waters: the right way to combine Tauri’s command
system
to invoke instructions within the back-end. Instructions are a form
of in-process communication mechanism the place the UI can invoke a perform
carried out in Rust on the back-end.

I added a Tauri command to the back-end:

// src-tauri/src/most important.rs

#[tauri::command]
fn greet(title: &str) -> String {
    format!("Hiya, {}!", title)
}

I then wanted to have the ability to use the invoke function from the
@tauri-apps/api npm package. Following the sample I noticed
in different Gleam packages. I created a JavaScript file to behave as a bridge between
Gleam and @tauri-apps/api:

// src/ffi/instructions.js

import { invoke } from '@tauri-apps/api/core';
import { Okay, Error } from "../../construct/dev/javascript/videopls/gleam.mjs";

export async perform greet(title) {
  attempt {
    return new Okay(await invoke('greet', { title: title }));
  } catch (error) {
    return new Error(error.toString());
  }
}

I might then outline the exterior perform within the Gleam code and name it:

// src/demo.gleam

@exterior(javascript, "./ffi/instructions.js", "greet")
pub fn greet(title: String) -> Promise(Consequence(String, String))

The problem was greet is an async perform, so it returns a promise, which
doesn’t combine right into a lustre.simple software nicely. Luckily there
the much less easy lustre.application that provides results. After taking a look at some
current code I used to be lastly about to provide you with a working resolution. The complete
Gleam code is proven under. get_greeting and do_get_greeting being the primary
elements of curiosity.

// src/demo.gleam

import gleam/int
import gleam/javascript/promise.{sort Promise}
import lustre
import lustre/attribute as attr
import lustre/ingredient.{sort Aspect}
import lustre/ingredient/html
import lustre/occasion
import lustre/impact.{sort Impact}

pub fn most important() {
  let app = lustre.software(init, replace, view)
  let assert Okay(dispatch) = lustre.begin(app, "#app", Nil)

  dispatch
}

sort Mannequin {
  Mannequin(depend: Int, greeting: String, title: String)
}

fn init(_) -> #(Mannequin, Impact(Msg)) {
  #(Mannequin(0, "", ""), impact.none())
}

pub sort Msg {
  Increment
  Decrement
  Greet
  GotGreeting(String)
  UpdateName(String)
}

fn replace(mannequin: Mannequin, msg: Msg) -> #(Mannequin, Impact(Msg)) {
  case msg {
    Increment -> #(Mannequin(..mannequin, depend: mannequin.depend + 1), impact.none())
    Decrement -> #(Mannequin(..mannequin, depend: mannequin.depend - 1), impact.none())
    Greet -> #(mannequin, get_greeting(mannequin.title))
    GotGreeting(greeting) -> #(
      Mannequin(..mannequin, greeting: greeting),
      impact.none(),
    )
    UpdateName(title) -> #(Mannequin(..mannequin, title: title), impact.none())
  }
}

fn get_greeting(title: String) -> Impact(Msg) {
  impact.from(do_get_greeting(title, _))
}

fn do_get_greeting(title: String, dispatch: fn(Msg) -> Nil) -> Nil {
  greet(title)
  |> promise.map(fn(response) {
    case response {
      Okay(greeting) -> GotGreeting(greeting)
      Error(err) -> GotGreeting("Error: " <> err)
    }
  })
  |> promise.faucet(dispatch)

  Nil
}

@exterior(javascript, "./ffi/instructions.js", "greet")
pub fn greet(title: String) -> Promise(Consequence(String, String))

fn update_name(textual content: String) -> Msg {
  UpdateName(textual content)
}

// -- VIEW

fn view(mannequin: Mannequin) -> Aspect(Msg) {
  let depend = int.to_string(mannequin.depend)

  html.div([], [
    html.h1([], [element.text("Gleam + Vite + Tauri")]),
    html.div([attr.class("field text-center")], [
      html.label([attr.for("greet_name")], [element.text("Name")]),
      ingredient.textual content(" "),
      html.enter([
        attr.type_("text"),
        attr.name("greet_name"),
        event.on_input(update_name),
      ]),
    ]),
    html.p([attr.class("text-center")], [
      element.text(model.greeting <> " " <> count <> " ✨"),
    ]),
    html.div([attr.class("text-center")], [
      html.button([event.on_click(Decrement)], [element.text("-")]),
      html.button([event.on_click(Increment)], [element.text("+")]),
      html.button([event.on_click(Greet)], [element.text("Greet")]),
    ]),
  ])
}

I added a Greet message for when the “Greet” button is clicked. Within the replace
perform that doesn’t replace the mannequin however calls get_greeting as its
side-effect. That builds an Impact from do_get_greeting, which calls the
FFI perform and maps the Consequence to a GotGreeting message containing the
greeting or an error message.

replace then handles the GotGreeting message by updating the mannequin, which in
flip updates the UI. I’m skipping over the Mannequin, view, replace
structure of this Lustre software because it’s principally the Elm
architecture
. An identical sample is seen in Purpose React, ReScript, and React
with actions and reducers
.

At this level I had labored out the right way to invoke Rust features within the back-end by way of
Tauri instructions however I wished to take it step additional. In an actual software you
can think about that the back-end is perhaps performing actions that it wants to inform
the UI about. For instance, when up to date knowledge is on the market after a sync.
To do that Tauri gives a means for each elements of the appliance to emit
occasions with a payload, and hear for these occasions. It’s all similar to how
occasions work in JavaScript.

See Also

I wished to check this out by periodically having the back-end emit an occasion and
have the UI hear for the occasion and replace because of this. I made a decision to have
the back-end emit the present time every second as a UNIX timestamp. Understanding
how to do that on back-end stumped me for a bit however I ultimately labored out I
might spawn a thread within the setup perform:

// src-tauri/src/most important.rs

use std::time::{Length, On the spot, SystemTime, UNIX_EPOCH};
use tauri::EventTarget;
use tauri::Supervisor;

fn most important() {
    tauri::Builder::default()
        .setup(|app| {
            let app = app.deal with().clone();
            std::thread::spawn(transfer || {
                loop {
                    let now = SystemTime::now();
                    let period = now.duration_since(UNIX_EPOCH).unwrap();
                    app.emit_to(EventTarget::any(), "tick", period.as_secs())
                        .unwrap();
                    std::thread::sleep(Length::from_secs(1));
                }
            });
            Okay(())
        })
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .count on("error whereas operating tauri software");
}

In a manufacturing software you’d desire a mechanism for cleanly shutting the
thread down however for experimentation functions I skipped that. Now I wanted to
hear for the tick occasion on the UI. I added one other glue perform to the FFI
file:

// src/ffi/instructions.js

export async perform listenForTick(handler) {
  await hear('tick', (occasion) => {
    handler(occasion.payload);
  });
}

And added a perform to the Gleam code to name it and dispatch a message when
it was acquired:

// src/demo.gleam

fn bind_clock() -> Impact(Msg) {
  impact.from(fn(dispatch) {
    listen_for_tick(fn(time) > dispatch
    )

    Nil
  })
}

As a primary cross I simply rendered the quantity within the UI however I then prolonged it to
parse the timestamp right into a JavaScript Date and render the stringified model
of it. Surprisingly the gleam_javascript bundle doesn’t have Date bindings
but so I created some for what I wanted:

// src/ffi/js_extra.js

export perform from_unix(timestamp) {
    return new Date(timestamp * 1000);
}

export perform date_to_string(date) {
    return date.toString();
}

I feel in a really perfect world easy bindings like this (particularly toString)
would have the ability to be expressed solely although the @exterior attribute. That
doesn’t appear to be potential but however whether it is please let me know.

I sure these in Gleam:

// src/demo.gleam

pub sort Date

@exterior(javascript, "./ffi/js_extra.js", "from_unix")
pub fn new_date(timestamp: Int) -> Date

@exterior(javascript, "./ffi/js_extra.js", "date_to_string")
pub fn date_to_string(date: Date) -> String

and up to date the appliance to make use of them. The result’s a clock on the backside of
the web page that updates every second:

The ultimate Gleam software seems like this:

// src/demo.gleam

import gleam/int
import gleam/javascript/promise.{sort Promise}
import lustre
import lustre/attribute as attr
import lustre/ingredient.{sort Aspect}
import lustre/ingredient/html
import lustre/occasion
import lustre/impact.{sort Impact}

pub fn most important() {
  let app = lustre.software(init, replace, view)
  let assert Okay(dispatch) = lustre.begin(app, "#app", Nil)

  dispatch
}

sort Mannequin {
  Mannequin(depend: Int, greeting: String, title: String, time: Int)
}

fn init(_) -> #(Mannequin, Impact(Msg)) {
  #(Mannequin(0, "", "", 0), bind_clock())
}

pub sort Msg {
  Increment
  Decrement
  Greet
  GotGreeting(String)
  UpdateName(String)
  Tick(Int)
}

fn replace(mannequin: Mannequin, msg: Msg) -> #(Mannequin, Impact(Msg)) {
  case msg {
    Increment -> #(Mannequin(..mannequin, depend: mannequin.depend + 1), impact.none())
    Decrement -> #(Mannequin(..mannequin, depend: mannequin.depend - 1), impact.none())
    Greet -> #(mannequin, get_greeting(mannequin.title))
    GotGreeting(greeting) -> #(
      Mannequin(..mannequin, greeting: greeting),
      impact.none(),
    )
    UpdateName(title) -> #(Mannequin(..mannequin, title: title), impact.none())
    Tick(time) -> #(Mannequin(..mannequin, time: time), impact.none())
  }
}

fn get_greeting(title: String) -> Impact(Msg) {
  impact.from(do_get_greeting(title, _))
}

fn do_get_greeting(title: String, dispatch: fn(Msg) -> Nil) -> Nil {
  greet(title)
  |> promise.map(fn(response) {
    case response {
      Okay(greeting) -> GotGreeting(greeting)
      Error(err) -> GotGreeting("Error: " <> err)
    }
  })
  |> promise.faucet(dispatch)

  Nil
}

fn bind_clock() -> Impact(Msg) {
  impact.from(fn(dispatch) {
    listen_for_tick(fn(time) > dispatch
    )

    Nil
  })
}

@exterior(javascript, "./ffi/instructions.js", "greet")
pub fn greet(title: String) -> Promise(Consequence(String, String))

sort UnlistenFn =
  fn() -> Nil

@exterior(javascript, "./ffi/instructions.js", "listenForTick")
pub fn listen_for_tick(handler: fn(Int) -> Nil) -> Promise(UnlistenFn)

pub sort Date

@exterior(javascript, "./ffi/js_extra.js", "from_unix")
pub fn new_date(timestamp: Int) -> Date

@exterior(javascript, "./ffi/js_extra.js", "date_to_string")
pub fn date_to_string(date: Date) -> String

fn update_name(textual content: String) -> Msg {
  UpdateName(textual content)
}

fn tick(time: Int) -> Msg {
  Tick(time)
}

// -- VIEW

fn view(mannequin: Mannequin) -> Aspect(Msg) > new_date
    

Conclusion

I efficiently constructed a hybrid native software with Gleam and Tauri. Whereas
what I constructed is clearly experimental code I feel it was sufficient to work out the
method and patterns you could possibly use to construct a bigger software. Utilizing Gleam
to construct an internet parts or net front-ends appears fairly possible.

Some unanswered questions I’ve from this experiment are:

  1. Does binding to exterior features within the JS platform or npm packages all the time
    require some JS glue code? It appears it does for the time being.
  2. What’s the proper method to import gleam.mjs from JavaScript code?
  3. What’s the construction of the Gleam construct listing?
    • I see dev and prod sub-directories.
    • Is the prod on used when focusing on JavaScript (I can’t see any
      equal of Cargo’s --release within the gleam CLI assist).

The complete undertaking code is on the market right here:

https://forge.wezm.net/wezm/gleam-tauri-experiment

Thanks

Particular because of the next of us:

Source Link

What's Your Reaction?
Excited
0
Happy
0
In Love
0
Not Sure
0
Silly
0
View Comments (0)

Leave a Reply

Your email address will not be published.

2022 Blinking Robots.
WordPress by Doejo

Scroll To Top