Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions examples/08_tauri_paraview_ws/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Tauri + WebSocket

This example leverages tauri for just its WebView and lets trame act as the full HTTP server by serving its content over HTTP and WebSocket. Additionally, this example also demonstrates using ParaView for rendering and computation.

## Tauri project

Since Tauri is written in Rust, let's get setup with its dev environment.

```bash
# Install rust
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

# Enable rust within shell
. "$HOME/.cargo/env"

# Install tauri-cli
cargo install tauri-cli
```

Then, the infrastructure (./src-tauri) was generated by running the following commands.

__Caution__: No need to redo it since we committed the generated structure to the repo.

```bash
$ cargo tauri init

✔ What is your app name? · Cone
✔ What should the window title be? · Cone
✔ Where are your web assets (HTML/CSS/JS) located, relative to the "<current dir>/src-tauri/tauri.conf.json" file that will be created? · ./www
✔ What is the url of your dev server? · ./www
✔ What is your frontend dev command? ·
✔ What is your frontend build command? ·
```

From the default content, we edited the following set of files:

- `./src-tauri/src/main.rs`: Generic main application designed for trame which can remain the same regardless of your application.
- `./src-tauri/sidecar/*`: OS specific launcher for a trame server. The mac and linux version are just plain bash scripts while the Windows one is a simple compiled C++ application that forward the args to the actual trame python executable generated by PyInstaller. Those can be re-used as-is for any trame app. A departure from the other examples, this app uses `pvpython` to launch the app instead of the packaged binary.
- `./src-tauri/www/*`: Basic content for splashscreen and temporary main window content before we apply a redirect. Those are static and could be adjusted by adding your own image as splashscreen.
- `./src-tauri/icons`: Remove default ones (`rm -rf ./src-tauri/icons`)
- `./src-tauri/Cargo.toml`: Added new dependencies and required feature.
- `./src-tauri/tauri.conf.json`: Edited the following sections
- tauri > allowlist: + shell/sidecar
- tauri > bundle > externalBin : ["sidecar/trame"]
- tauri > bundle > identifier : "trame.cone"
- tauri > bundle > resources : ["server"] # pyinstaller generated app
- tauri > bundle > targets : ["appimage", "nsis", "msi", "app", "dmg"]
- tauri > security > csp : "default-src 'self' 'unsafe-inline' ws: localhost; script-src 'unsafe-eval' 'self';"
- tauri > windows : main-not-visible + splashscreen

## Copy application to container

Since the app uses `pvpython` to launch the application, the python executables need to be copied over to the server directory for it to be bundled

```bash
cp cone.py src-tauri/server
```

## Trame example

We use a simple cone example since it does not have any complex python dependency. Since we're bundling a `ParaView` based application, and `ParaView` not being `pip` installable, this example uses `conda`


```bash
conda create --name tauri -c conda-forge python=3.13
conda activate tauri
pip install trame trame-vtk trame-vuetify pyinstaller
conda install paraview=5.13.3
```

Build bundle for tauri inside `./src-tauri/server/*` while skipping the web content.

```bash
python -m PyInstaller
--clean --noconfirm \
--distpath src-tauri \
--name server --hidden-import pkgutil \
--collect-all paraview
cone.py
```

Generate webcontent for tauri to bundle

```bash
python -m trame.tools.www --output ./src-tauri/www
```

## Tauri bundle

In order to build and bundle the application, just run

```bash
# Generate icon for application using ./app-icon.png
cargo tauri icon

# Generate application
cargo tauri build
```

## Running application

### Linux and macOS
```bash
open ./src-tauri/target/release/bundle/macos/Cone.app
```
Binary file added examples/08_tauri_paraview_ws/app-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
84 changes: 84 additions & 0 deletions examples/08_tauri_paraview_ws/cone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import os
from trame.app import get_server
from trame.ui.vuetify3 import SinglePageLayout
from trame.widgets import vuetify3 as vuetify, paraview
from trame.decorators import TrameApp, change, life_cycle

from paraview import simple

# Configure ParaView for headless operation when using standard Python interpreter
# (pvpython sets this automatically, but regular python requires manual configuration)
from paraview.modules import vtkRemotingCore as rc

rc.vtkProcessModule.GetProcessModule().UpdateProcessType(
rc.vtkProcessModule.PROCESS_BATCH, 0
)


@TrameApp()
class Cone:
"""
This application uses the ParaView simple API to create a cone
and display it in a web application using Tauri. This application
uses remote rendering.
"""

def __init__(self, server=None):
self.server = get_server(server)
self.state = self.server.state
self.ctrl = self.server.controller

cone = simple.Cone()
representation = simple.Show(cone)
view = simple.Render()

self.cone = cone
self.representation = representation
self.view = view
self.resolution = 6

self.ui = self._build_ui()

@change("resolution")
def update_cone(self, resolution, **kwargs):
self.cone.Resolution = resolution
self.ctrl.view_update()

def _build_ui(self):
with SinglePageLayout(self.server) as layout:
with layout.content:
with vuetify.VContainer(fluid=True, classes="pa-0 fill-height"):
html_view = paraview.VtkRemoteView(self.view, ref="view")
self.ctrl.view_reset_camera = html_view.reset_camera
self.ctrl.view_update = html_view.update

with layout.toolbar:
vuetify.VSpacer()
vuetify.VSlider(
v_model=("resolution", 6),
min=3,
max=60,
step=1,
hide_details=True,
dense="compact",
style="max-width: 300px;",
)
with vuetify.VBtn(icon=True, click=self.server.controller.reset_camera):
vuetify.VIcon("mdi-crop-free")

return layout

# Use os.write for immediate unbuffered output to ensure Tauri
# receives the port number before Python's stdout buffer flushes
@life_cycle.server_ready
def _tauri_ready(self, **_):
os.write(1, f"tauri-server-port={self.server.port}\n".encode())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's how you prevent ParaView buffering?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! os.write very eagerly writes to stdcout, so we don't have to wait around for ParaView to flush. -- This still only prints the two lines that we have in the calls. ParaView still works on a separate buffer and flushes later. So we might see prints happening not in chronological order.


@life_cycle.client_connected
def _tauri_show(self, **_):
os.write(1, "tauri-client-ready\n".encode())


if __name__ == "__main__":
app = Cone()
app.server.start()
13 changes: 13 additions & 0 deletions examples/08_tauri_paraview_ws/config_app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash

set -e
set -x

python -m PyInstaller \
--clean --noconfirm \
--distpath src-tauri \
--name server --hidden-import pkgutil \
--collect-all paraview
cone.py

python -m trame.tools.www --output ./src-tauri/www
7 changes: 7 additions & 0 deletions examples/08_tauri_paraview_ws/src-tauri/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
icons
server
Cargo.lock
www
27 changes: 27 additions & 0 deletions examples/08_tauri_paraview_ws/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["Kitware"]
license = ""
repository = ""
default-run = "app"
edition = "2021"
rust-version = "1.60"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[build-dependencies]
tauri-build = { version = "1.5.1", features = [] }

[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.6.1", features = ["shell-sidecar"] } # Added sidecar
async-std = "1.12.0" # Added for sleep

[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
# DO NOT REMOVE!!
custom-protocol = [ "tauri/custom-protocol" ]
3 changes: 3 additions & 0 deletions examples/08_tauri_paraview_ws/src-tauri/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash
rootdir=`dirname $0`
rootdir=`cd $rootdir && cd .. && pwd`

$rootdir/Resources/server/server $@
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash
rootdir=`dirname $0`
rootdir=`cd $rootdir && cd .. && pwd`

$rootdir/Resources/server/server $@
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash
rootdir=`dirname $0`
rootdir=`cd $rootdir && cd ../lib/cone && pwd`

$rootdir/server/server $@
45 changes: 45 additions & 0 deletions examples/08_tauri_paraview_ws/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]

use tauri::api::process::{Command, CommandEvent};
use tauri::Manager;
use std::time::Duration;
use async_std::task;

fn main() {
tauri::Builder::default()
.setup(|app| {
let splashscreen_window = app.get_window("splashscreen").unwrap();
let main_window = app.get_window("main").unwrap();

let (mut rx, _) = Command::new_sidecar("trame")
.expect("failed to create sidecar")
.args(["--server", "--port", "0", "--timeout", "1"])
.spawn()
.expect("Failed to spawn server");

tauri::async_runtime::spawn(async move {
while let Some(event) = rx.recv().await {
if let CommandEvent::Stdout(line) = event {
if line.contains("tauri-server-port=") {
let tokens: Vec<&str> = line.split("=").collect();
let port_token = tokens[1].to_string();
let port = port_token.trim();
// println!("window.location.replace('http://localhost:{}/')", port);
let _ = main_window.eval(&format!("window.location.replace('http://localhost:{}/')", port));
}
if line.contains("tauri-client-ready") {
task::sleep(Duration::from_secs(2)).await;
splashscreen_window.close().unwrap();
main_window.show().unwrap();
}
}
}
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running application");
}
86 changes: 86 additions & 0 deletions examples/08_tauri_paraview_ws/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"build": {
"beforeBuildCommand": "",
"beforeDevCommand": "",
"devPath": "./www",
"distDir": "./www"
},
"package": {
"productName": "Cone",
"version": "0.1.0"
},
"tauri": {
"allowlist": {
"all": false,
"shell": {
"sidecar": true,
"scope": [
{
"name": "sidecar/trame",
"sidecar": true
}
]
}
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"deb": {
"depends": []
},
"externalBin": ["sidecar/trame"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "trame.cone",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": ["server"],
"shortDescription": "",
"targets": ["appimage", "nsis", "msi", "app", "dmg"],
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": {
"csp": "default-src 'self' 'unsafe-inline' ws: localhost; script-src 'unsafe-eval' 'self';"
},
"updater": {
"active": false
},
"windows": [
{
"fullscreen": false,
"height": 600,
"resizable": true,
"title": "Cone",
"width": 800,
"visible": false
},
{
"label": "splashscreen",
"width": 400,
"height": 200,
"center": true,
"decorations": false,
"resizable": false,
"visible": true,
"alwaysOnTop": true,
"url": "splashscreen.html"
}
]
}
}
Loading