Type-safe Scala 3 toolkit for building Tauri applications.
Tausi provides a robust, idiomatic Scala.js interface to the Tauri runtime. Unlike wrappers around the TypeScript API, Tausi interacts directly with window.__TAURI_INTERNALS__, offering superior type safety while maintaining mimal runtime overhead.
Status: The Scala.js frontend API is ready for experimental use. The Scala Native backend for writing plugins is currently in development.
Tausi bypasses the @tauri-apps/api JavaScript layer entirely, communicating directly with the Tauri Rust core via the underlying IPC protocol.
- Minimal-Overhead: Direct access to global IPC methods; no extra bundle size from JS dependencies.
- Type-Safe: All commands, events, and parameters are validated at compile time.
- Effect Agnostic: Core API uses
Future; modules provided for Cats Effect and ZIO. - Action-Oriented: API designed around actions (
SetTitle) rather than generic request objects.
Add the following to your build.sbt:
// Core API (Scala.js)
libraryDependencies += "io.github.arashi01" %%% "tausi-api" % "0.0.1-SNAPSHOT"
// Optional: Cats Effect Integration
libraryDependencies += "io.github.arashi01" %%% "tausi-cats" % "0.0.1-SNAPSHOT"
// Optional: ZIO Integration
libraryDependencies += "io.github.arashi01" %%% "tausi-zio" % "0.0.1-SNAPSHOT"
// Optional: Laminar Integration (stream-to-Laminar bridge)
libraryDependencies += "io.github.arashi01" %%% "tausi-laminar" % "0.0.1-SNAPSHOT"Tausi uses a typeclass-based command system. Commands are defined as types (e.g., SetTitle) rather than string literals.
import tausi.api.core.invoke
import tausi.api.commands.window.{given, *}
import scala.concurrent.ExecutionContext.Implicits.global
// 1. Zero-argument commands (implicit resolution)
val version: Future[String] = invoke(using app.version)
// 2. Parameterised commands
invoke(SetTitle("main", "My App")).map { _ =>
println("Title updated")
}
// 3. Named parameters
invoke(SetSize(
label = "main",
width = 800,
height = 600
))Events use a type-safe Event[A] abstraction that couples event identity with payload type at the type level, following the same pattern as Commands.
import tausi.api.Event
import tausi.api.events
import tausi.api.codec.Codec
import scala.concurrent.ExecutionContext.Implicits.global
// 1. Define event payload types
final case class SurveySubmission(data: String) derives Codec
// 2. Define events as given instances
given surveySubmitted: Event[SurveySubmission] = Event.define("survey-submitted")
given backendReady: Event[Unit] = Event.define0("backend-ready")
// 3. Listen - Event resolved implicitly, handler receives Either for error handling
events.listen[SurveySubmission] {
case Right(msg) => println(s"Received: ${msg.payload.data}")
case Left(err) => println(s"Decode error: ${err.message}")
}
// 4. Emit - payload type verified against Event instance
events.emit(SurveySubmission("test data"))
// 5. Listen once for Unit events
events.once[Unit] {
case Right(_) => println("Backend ready!")
case Left(err) => println(s"Error: ${err.message}")
}import cats.effect.IO
import cats.effect.std.Dispatcher
import tausi.cats.*
import tausi.api.commands.window.{given, *}
val program: IO[Unit] = for
_ <- invoke(SetTitle("main", "My App"))
_ <- invoke(SetSize("main", 1024, 768))
_ <- IO.println("Window configured")
yield ()import zio.*
import tausi.zio.*
import tausi.api.commands.window.{given, *}
val program: ZIO[Any, TausiError, Unit] = for
_ <- invoke(SetTitle("main", "My App"))
_ <- invoke(SetSize("main", 1024, 768))
_ <- ZIO.log("Window configured")
yield ()Both effect modules provide runWith extension methods for executing effects from UI event handlers:
import zio.*
import tausi.zio.*
given Runtime[Any] = Runtime.default
button(
onClick --> { _ =>
myCommand.runWith(
onSuccess = result => updateUI(result),
onError = err => showError(err.message)
)
}
)
// Or with unified Either callback
myEffect.runWith {
case Right(result) => handleSuccess(result)
case Left(error) => handleError(error)
}
// Fire-and-forget (errors silently dropped - use with caution)
loggingEffect.runWithUnsafe()import cats.effect.IO
import cats.effect.std.Dispatcher
import tausi.cats.*
given Dispatcher[IO] = ??? // from IOApp or Resource
button(
onClick --> { _ =>
myCommand.runWith(
onSuccess = result => updateUI(result),
onError = err => showError(err.getMessage)
)
}
)Tausi provides a type-safe bridge between effect streams (ZIO ZStream, fs2 Stream) and Laminar observables:
// Add dependency
libraryDependencies += "io.github.arashi01" %%% "tausi-laminar" % "0.0.1-SNAPSHOT"import tausi.laminar.*
import tausi.zio.ZStreamIO
import tausi.zio.ZStreamIO.given
import zio.stream.ZStream
// Create a ZIO stream
val counter: ZStreamIO[Int] = ZStream.iterate(0)(_ + 1).take(10)
// Tier 1: Unsafe (errors logged, dropped) - for prototyping
val eventStream: EventStream[Int] = counter.toStreamUnsafe
// Tier 2: Either-based (errors as values)
val signal: Signal[Either[TausiError, Int]] =
counter.toSignal(Left(TausiError.StreamError("Loading...")))
// Tier 3: Full state (recommended for production)
val stateSignal: Signal[StreamState[Int]] = counter.toStateSignalThe StreamState ADT provides full lifecycle visibility:
enum StreamState[+A]:
case Running // Stream is loading
case Value(value: A) // Latest value received
case Failed(error: TausiError) // Stream failed
case Completed // Stream completed (no final value)
case CompletedWith(value: A) // Stream completed with final valueUsage in Laminar:
div(
child <-- myStream.toStateSignal.map {
case StreamState.Running => div(cls := "spinner", "Loading...")
case StreamState.Value(data) => renderData(data)
case StreamState.Failed(err) => div(cls := "error", err.message)
case StreamState.Completed => div("Done")
case StreamState.CompletedWith(d) => renderData(d)
}
)For custom Tauri plugins, define commands using the Command.define factory:
import tausi.api.Command
import tausi.api.codec.Codec
// 1. Define Parameter Type with Codec derivation
final case class Greet(name: String) derives Codec
// 2. Define Command using the factory method
given greet: Command[Greet, String] = Command.define[Greet, String]("greet")
// 3. Usage
invoke(Greet("Alice"))For commands that take no parameters:
// Define a zero-argument command
given getVersion: Command0[String] = Command.define0[String]("get_version")
// Usage - no arguments needed
val version: Future[String] = invokeCommands can be transformed to work with different types:
import tausi.api.Command
// Transform response type
val intVersion: Command0[Int] = getVersion.mapRes(_.toInt)
// Transform request type
val simpleGreet: Command[String, String] = greet.contramapReq(name => Greet(name))
// Change command ID (useful for testing/mocking)
val testGreet: Command[Greet, String] = greet.withCommandId("test_greet")For custom application events, define events using the Event.define factory:
import tausi.api.Event
import tausi.api.codec.Codec
// 1. Define Payload Type with Codec derivation
final case class SurveyProgress(currentPage: Int, totalPages: Int) derives Codec
// 2. Define Event using the factory method
given surveyProgress: Event[SurveyProgress] = Event.define("survey-progress")
// 3. Usage - listen
events.listen[SurveyProgress] {
case Right(msg) => println(s"Page ${msg.payload.currentPage} of ${msg.payload.totalPages}")
case Left(err) => println(s"Error: ${err.message}")
}
// 4. Usage - emit
events.emit(SurveyProgress(2, 5))For events that carry no data:
// Define a Unit event
given backendReady: Event[Unit] = Event.define0("backend-ready")
// Listen for it
events.once[Unit] {
case Right(_) => println("Backend is ready!")
case Left(err) => println(s"Error: ${err.message}")
}
// Emit it
events.emit(())Events can be transformed to work with different payload types:
import tausi.api.Event
// Transform payload type (bidirectional)
val stringProgress: Event[String] =
Event.mapPayload(surveyProgress)(
sp => s"${sp.currentPage}/${sp.totalPages}",
str => {
val parts = str.split("/")
SurveyProgress(parts(0).toInt, parts(1).toInt)
}
)
// Change event name
val renamedEvent: Event[SurveyProgress] = surveyProgress.withName("progress-update")- Core JS API: Type-safe commands, events, and plugins.
- Effect Integration: Cats Effect and ZIO support.
- Scala Native Backend: Write Tauri plugins directly in Scala Native (in progress).
- SBT Plugin: Automate cargo/tauri build integration.
MIT