84
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【TS×Rust】Tauriの便利機能紹介またはTauriを百合作品だと考察するポエム的な何か

Last updated at Posted at 2023-03-23

前回作ったaudio-bookmarkにつづき、Rust :crab: + React :atom: with Tauri :stars: でまたまた自己満足アプリを作りました!本記事では、作品紹介のついでに、Tauriならではな機能も紹介し、ポエムでTauriを布教していきます!

今回作ったのは win-win-map (Windows' Windows Map)というWindows用ウィンドウ管理ユーティリティアプリです。

winwinmap_demo.gif

リポジトリ: https://github.com/anotherhollow1125/win-win-map
ダウンロードページ: https://github.com/anotherhollow1125/win-win-map/releases

制作した動機は後ほど紹介しますが、動画の通り、本アプリケーションを使うことでマウスポインタ、ウィンドウをショートカットキー等で特定座標に召喚したり、マップでドラッグすることでウィンドウを動かしたりできます。

はじめに

Rust大好きな大学院生のnamniumと申します!よく研究そっちのけでアプリ制作をしたり百合アニメを観たりしています。(研究しろ)

百合アニメといえば2022年は豊作でしたね。まちカドまぞく2丁目リコリス・リコイル水星の魔女ぼっち・ざ・ろっく!、…等々、お気に入り作品ばかりです。

実はプログラミング界隈でも密かに人気を集めている百合作品があります……そう!それこそがGUIフレームワーク Tauriなのです!

Tauriとは?

Tauriは、Rust製のクロスプラットフォームGUIフレームワークです。この先のポエムを読むためには次の3点を抑えておけば大丈夫です。

  • Tauriは、OSのWebViewを呼び出す仕組みを使い、Electronのようなデスクトップアプリケーションを実現する
  • OSとのやり取りや重たい計算等のバックエンドはRustが担当する
  • GUI描画等のフロントエンドはTypeScript1が担当する

舞台Tauri.png

より詳細な説明は以前書いたハンズオン記事で説明しています。

以降ポエムとアプリ紹介が続くのでTauriの機能紹介に飛びたい方は こちら

Tauriと百合の関係性に関する考察

「Tauriが人気を集めている百合作品?何言ってるんだ?この筆者」と思われたかもしれません。確かに「人気を集めている」は誇張でした。しかし実際、Tauriは魅力的な百合アニメたちに似た特徴を持っているんです。

まぞくのシャミ子と魔法少女桃。陰キャのぼっちと陽キャの喜多ちゃん。破天荒な千束と真面目なたきな。最近人気な百合作品は対照的な2人のバディ物な側面もあり、その対照的なことを起因とするイベントが物語を彩っています。

Tauriにもこの構造が認められるのです。型に憧れながら型に恵まれなかった(トランスパイラ止まりの)人気者TypeScriptと、型にめちゃくちゃ恵まれながらも完璧さゆえに(難しいせいで、開発者から)相手にされないRust…2人が協力して魅力的なデスクトップアプリケーションを作る……Tauriは、まるで現在佳境を迎えている百合アニメ2さながらのあらすじになっています。

Rustはシステムプログラミング言語としての地位は確立しつつある一方、GUIアプリ制作の手段としては弱いのが現実です。バックエンド以外での商業的価値を考えるとこの点は致命的かもしれません。

一応(Tauriを除く)候補を乱雑に挙げると、基本となるwinitを始めとして、OpenGLのようなGPU向けのグラフィックAPI wgpu、GTKのラッパーとしてのgtk-rs、そのほかeguiIced3、様々な種類が存在しますが、C#の.NET coreのような、キラーフレームワークはないという現状です。敷居の高さも相まって、Rustだけではどうしても(特にGUI周りの)エコシステムやコミュニティの成長に不安が残ります。

一方TypeScriptは数値計算以外では幅広い分野で使用が期待できそうですが、JavaScript譲りの不安要素や余剰機能があり、堅牢なアプリケーション制作に向いていると自信を持って言うのは厳しそうです。トランスパイルしてしまえば結局はJS、つまりインタプリタ言語であることも、その他の静的型付け言語を置き換えるほどの言語になれない一因でしょう。

しかし、TypeScriptにはRustにはない 設計変更の柔軟さとフロントエンド(GUI)構築にまつわる豊富なフレームワーク・ライブラリ・コミュニティがある というメリットが、そしてRustにはTypeScriptにはない システムの根幹を自信をもって作れる、OSのAPIやFFIをがっつり触れる というメリットがあります。

つまりこの「対照的な」2人が互いに手を取り協力したら…?最強で楽しいアプリケーションが作れると思いませんか?そんな夢を叶えてくれる百合作品もといフレームワークがTauriなのです!!

TS×Rust挿絵
RustちゃんとTypeScriptちゃん 作: Novel AI4

ここまでふざけて話しましたが、真面目な話、開発効率を最大にするならRustとTypeScriptによる担当分野分けには文句の付けようがありません。Rustで書いている部分をTypeScriptで書こうとしたらリソース管理や非同期等で永遠に沼りそうですし、Rustでフロントエンドに相当する部分を書くぐらいなら豊富にライブラリや情報がネットに落ちているTypeScriptで書いたほうが苦労しないでしょう。

「1人で悩まず2人で手を取り合い事件解決していく」...素晴らしい王道パターンです。Tauriは今季(次世代?)の覇権フレームワークになる力を秘めているのです!

アプリの解説

Tauriの百合ポエムはこの辺にしてここからは今回作ったアプリ win-win-map の解説に入っていきます。

winwinmapキャプション.png

制作した動機

マウスカーソルやウィンドウが召喚できるwin-win-mapですが、正直「面白いだけのオモチャアプリでは?」と思われた方が多いのではないでしょうか?これはその通りで、需要がなさそうだったのも制作動機の一つだったりします。需要があれば何かしら既存のアプリが存在するものです5

本当の動機は筆者の作業環境にあります。

ロフトベッド環境図2.png

筆者は普段ロフトベットの机で作業しています。しかし、たまにはベッドで寝ながら作業したいので、ベッドにもモニタを設置しています。こちらのモニタには、下の階のメインモニタをミラーリングしています。

ベッドでは、常に全モニタが見えているわけではありません。こうなると、ベッドに行くたびに、下の大きいモニタで作業したウィンドウやマウスカーソルを階上モニタに持って行く必要があります。

この作業が手間だったのが本アプリ制作の動機となりました。DIYみたいなノリです。

win-win-mapの機能

改めて機能紹介になります。

  • ショートカットによりマウスカーソルを設定座標に召喚
  • ショートカット及びボタンによりウィンドウを設定座標に召喚
  • 閾値外(ベッド上モニタじゃない領域)にあるウィンドウの自動召喚
  • マップ上ドラッグによるウィンドウ移動

偏った機能しかありませんが自己満足アプリなのでご愛嬌。要望があればissueを下さると幸いです。

自分しか使わないために設定項目も偏っていますが、テキストファイルを編集したりといった面倒な方法を避けています。

win-win-mapの大まかな構成・制作手順

audio-bookmarkと同様に、Win32API(windows-rs)を利用してウィンドウを動かしています。そのため構成も似通っています。

winwinmap構成2.png

フロントエンドで使用した技術の説明は省略します。機能紹介を替わりとさせていただきます。

バックエンドで使用したWin32APIの解説はZennにて別記事にしてみました。興味があれば読んでいただけると幸いです。↓

大まかな制作手順は、

  1. Rust側だけでウィンドウやマウスカーソルを移動させるCLIアプリケーションをお試しで作り、APIとして盤石化する
  2. フロント側からRustで作った機能を呼び出すようにしてMUIでGUIを作る

といった感じです。Rustでがっちりと土台を作ってからTypeScriptでふわふわした部分を実装する手順になっています。

RustだけだとCLI止まりになってしまうような自作アプリが、TypeScriptが作るGUIのおかげで便利なユーティリティツールに一瞬で化けるのです!これこそがTauriによるDIYの醍醐味であり、他のフレームワークにはない強みになっています。

自分の世界に籠っていたRustをTypeScriptが外に連れ出して無双する、Tauriはそんな爽快劇なんです。

Tauriの便利機能紹介

筆者としてのメインコンテンツはここまでで終わりですが、読者にとってはここからがメインでしょう() というわけで、オススメ機能紹介です。

本節では、いくつかTauriアプリを作ってきた中で「Tauriフレームワークのここがすごい!Tauri魂!」となった要素を備忘録としてピックアップしました。「Tauriを使えばこんなことも実現できるのか~」みたいな雰囲気で読んでいただけると幸いです。

なお気分としては冒頭で挙げたハンズオン記事の続きとして書いているので、ここまでを読んでTauriに触れたくなった方はハンズオンの方にも目を通してみてください。

ハンズオンではWindowsで開発を行っていたため、本記事で紹介する機能も全てWindowsを前提として検証し記述しています。検証に関わる各アプリのバージョンは以下の通りです。漏れがあればコメントで尋ねていただけると幸いです。

  • tauri: 1.2
  • cargo: 1.66.0
  • yarn: 1.22.19
  • tauri-cli: 1.2.3
バージョン確認詳細
PowerShell
> rustup --version
rustup 1.25.1 (bb60b1e89 2022-07-12)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.66.0 (69f9c33d7 2022-12-12)`
> cargo --version
cargo 1.66.0 (d65d197ad 2022-11-15)
> node -v
v18.4.0
> npm -v
8.13.1
> yarn -v
1.22.19
> cd tauri-project
> yarn tauri --version
yarn run v1.22.19
$ tauri --version
tauri-cli 1.2.3
Done in 0.15s.
Cargo.toml(一部抜粋)
[dependencies]
tauri = { version = "1.2", features = ["path-all", "protocol-asset", "window-all"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

tauriのfeaturestauri.conf.jsonに依存して自動で書き換わるので気にしないでください。

まとめということでハンズオンで紹介した内容も改めて簡単に紹介しています。

例として挙げているソースコードは見やすさを優先した不完全なものです。適宜ハンズオンのソースコード等を参照してください。
また例ではunwrapを多用していますが実際にアプリを作る時は適切なエラーハンドリングを心がけましょう。

目次6

バックエンド(Rust)編

コマンドとマクロの機能定義

Rust側の関数に#[tauri::command]という属性風マクロをつけ、invoke_handlerに登録すると、TypeScript側からinvoke関数で呼び出せるようになります。Tauriの最も基本となる機能です。

マクロをつける関数は同期関数でも非同期関数でもどちらでも構いません。例では同期関数につけています。

Rust
// 関数に属性風マクロをつける
#[tauri::command]
fn get_entries(path: &str) -> Result<Vec<Entry>, String> {
    let res = /* omit details. return Vec<Entry> value */;

    Ok(res)
}

fn main() {
    tauri::Builder::default()

        // invoke_handlerで登録する
        .invoke_handler(tauri::generate_handler![get_entries])

        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
TypeScript
const App = () => {
  const [dir, setDir] = useState<string | null>(null);
  const [entries, setEntries] = useState<Entries | null>(null);

  // omit details

  useEffect(() => {
    (async () => {

      const entries = await invoke<Entries>("get_entries", { path: dir })
        .catch(err => {
          console.error(err);
          return null;
        });

      setEntries(entries);
    })();
  }, [dir]);

  // omit details
}

export default App;

invokeの第一引数に呼び出したい関数名、第二引数にその関数の引数をオブジェクトで指定して呼び出します。

invokeはRust関数に関係なく非同期関数であるため、例えばReactの場合は例に示したようにuseEffect内で非同期関数に包んで呼び出す等が主な使い方になります。

Reactの場合、useEffectはデバッグ時に二度走ることに気をつけましょう。(参考: useEffectがマウント時に2回実行される - Qiita )
もしRust関数を一度しか呼び出してほしくない場合、useRefを使うなどで対策できます。

2023/07/19 追記

invokeuseEffect を使うのは適さないケースが多い(get_entriesはめずらしく適切なケース)です。二回実行されて困るような処理ならば、可能な限りユーザーアクションに結びついたイベントハンドラに書きましょう(useRef は最終手段です)。

参考にどうぞ: 「Reactでawaitしたら壊れた」「Reactでawaitしたら壊れた」 ~ useEffectの誤用と2回実行 ~ - Qiita

以降本記事でも useEffect を多用していますが執筆当時筆者が↑を知らなかったためです。ご了承ください。

なおコマンドの引数として、TypeScript側で指定するものの他、関数を呼び出したウィンドウや、アプリのAppHandle、アプリがメインループで所有しているリソース等を渡すことも可能です。

以下はバックエンド側で状態管理している例です。

Rust
// omit details

use std::sync::Mutex;

#[tauri::command]
fn get_state(state: tauri::State<'_, Mutex<bool>>) -> bool {
    let state = state.lock().unwrap();
    *state
}

#[tauri::command]
fn toggle_state(state: tauri::State<'_, Mutex<bool>>) -> bool {
    let mut state = state.lock().unwrap();
    *state = !*state;
    *state
}

fn main() {
    let state = Mutex::new(false);

    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![get_entries, toggle_state, get_state])

        // リソース登録
        .manage(state)

        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

State自体がDerefを実装しているのでArcは不要です。例では可変性を持たせたいのでMutexで包んでいます。

フロント側
TypeScript
import { useEffect, useState } from 'react';
import { invoke } from '@tauri-apps/api';

const App = () => {
  const [state, setState] = useState(false);

  useEffect(() => {
    (async () => {
      const s = await invoke<boolean>("get_state");
      setState(s);
    })();
  }, []);

  return (
    <>
      <div>state: {state ? "on" : "off"}</div>
      <button onClick={() => invoke<boolean>("toggle_state").then((s: boolean) => setState(s))}>toggle</button>
    </>
  );
}

export default App;

参考

serdeとデータの構造定義

前項目の関数呼び出しにおいて、Rust関数をTypeScriptで扱うには引数も返り値もRustとTypeScriptの両方で扱える型である必要があります。

「両方で扱える型」は、「JSONで使える型」と考えて差支えありません。そのため文字列型や数値型は特に何もせずとも引数、返り値の型として指定できます。(なお返り値でResult型を返すようにすると、Errの時はcatchメソッドが実行されるようになります。)

ユーザーが定義した型(構造体や列挙体)の場合、Rust側でserdeのトレイトを実装することで扱えるようになります。引数に使いたい型にはserde::Deserialize、返り値に使いたい型にはserde::Serializeを実装します。

以下は返り値用の型を定義する例です。

Rust側型定義
#[derive(serde::Serialize)]
#[serde(tag = "type")]
enum Entry {
    #[serde(rename = "file")]
    File { name: String, path: String },
    #[serde(rename = "dir")]
    Dir { name: String, path: String },
}

// EntryにSerializeが実装されると、Vec<Entry>にもSerializeが自動実装される

/* 例
File {
    name: "memo.txt",
    path: "C:/Users/namnium/Desktop/memo.txt"
}

Dir {
    name: "Desktop",
    path: "C:/Users/namnium/Desktop"
}
*/
例がJSON化すると
{
    "type": "file",
    "name": "memo.txt",
    "path": "C:/Users/namnium/Desktop/memo.txt"
}

{
    "type": "dir",
    "name": "Desktop",
    "path": "C:/Users/namnium/Desktop"
}
TypeScript側型定義
type Entry = {
  type: 'dir' | 'file';
  name: string;
  path: string;
};

type Entries = Array<Entry>;

RustはEnumで、TypeScriptはユニオン型でと、それぞれの書きやすい方法で型を扱えている点が良いです。まるでRustとTypeScriptが型を通じて会話しているようです :relieved: (重症)

参考

メニューとシステムトレイのUI拡張

Tauri製ソフトがただWebViewに何かを表示するだけのアプリだったらここまで熱中していなかったかもしれない...そう思わせる機能が「ウィンドウメニュー」と「システムトレイ」になります。どういう機能かはそれぞれの画像を見ていただければわかると思います。

ウィンドウメニュー・システムトレイともに、ビルドメソッドにおいて、項目と項目が押された時用のイベントハンドラを設定することで実装します。

ウィンドウメニューの例

image.png

Rust
// omit details

use tauri::{CustomMenuItem, Menu, WindowMenuEvent, Submenu, Wry};

// 項目の設定
fn create_menu() -> Menu {
    let quit = CustomMenuItem::new("quit".to_string(), "Quit");
    let close = CustomMenuItem::new("close".to_string(), "Close");
    let submenu = Submenu::new("File", Menu::new().add_item(quit).add_item(close));
    let menu = Menu::new()
        .add_item(CustomMenuItem::new("hide", "Hide"))
        .add_submenu(submenu);

    menu
}

// イベントハンドラ
fn on_main_menu_event(event: WindowMenuEvent<Wry>) {
    match event.menu_item_id() {
        "hide" => event.window().hide().unwrap(),
        "quit" | "close" => event.window().close().unwrap(),
        _ => {}
    }
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![get_entries])

        // 登録
        .menu(create_menu())
        .on_menu_event(on_main_menu_event)

        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

システムトレイの例

image.png

システムトレイの場合はtauri.conf.jsonにも少し追記する必要があります。

tauri.conf.json
{
  // omit details
  "tauri": {
    // omit details
    "systemTray": {
      "iconPath": "icons/icon.png",
      "iconAsTemplate": true
    }
  }
}

Rust側の記述はウィンドウメニューの場合に似ています。

Rust
// omit details

use tauri::{Manager, AppHandle, Wry};
use tauri::{SystemTray, CustomMenuItem, SystemTrayMenu, SystemTrayMenuItem, SystemTrayEvent};

// 項目の設定
fn create_systemtray() -> SystemTray {
    let hide = CustomMenuItem::new("hide".to_string(), "Hide");
    let show = CustomMenuItem::new("show".to_string(), "Show");
    let quit = CustomMenuItem::new("quit".to_string(), "Quit");
    let tray_menu = SystemTrayMenu::new()
        .add_item(hide)
        .add_item(show)
        .add_native_item(SystemTrayMenuItem::Separator)
        .add_item(quit);

    let tray = SystemTray::new().with_menu(tray_menu);

    tray
}


// イベントハンドラ
fn on_system_tray_event(app: &AppHandle<Wry>, event: SystemTrayEvent) {
    match event {
        SystemTrayEvent::MenuItemClick { ref id, .. } if id == "quit" => std::process::exit(0),
        SystemTrayEvent::MenuItemClick { ref id, .. } if id == "hide" => {
            let window = app.get_window("main").unwrap();
            window.hide().unwrap();
        },
        SystemTrayEvent::MenuItemClick { ref id, .. } if id == "show" => {
            let window = app.get_window("main").unwrap();
            window.show().unwrap();
        },
        _ => {}
    }
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![get_entries])

        // 登録
        .system_tray(create_systemtray())
        .on_system_tray_event(on_system_tray_event)

        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

システムトレイの設定があることからわかる通り常駐アプリケーションとしてアプリを作成することも可能です。setupメソッドで最初にhideすることでウィンドウなしの状態からアプリを始めることができます。

Rust
// omit details

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![get_entries])
        .system_tray(create_systemtray())
        .on_system_tray_event(on_system_tray_event)

        .setup(|app| {
            // hide window on startup
            let window = app.get_window("main").unwrap();
            window.hide().unwrap();

            Ok(())
        })

        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

ユーティリティアプリDIY勢としては地味にうれしい機能たちでした。

参考

フロントエンドとイベント経由の相互作用

invokeによってTypeScriptからRustの関数を呼ぶことでRust側に干渉できました。ではRustからTypeScript側に干渉するにはどうすればよいでしょうか?TypeScriptの関数をRustから呼び出すのは何か違う気がします。

ここで登場するのがRustからTypeScriptへのイベント通知になります。次のコードは、システムトレイが押された時にTypeScript側に通知する例です。emit_allでイベント名と内容を全ウィンドウに通知しています。

Rust
use tauri::{Manager, AppHandle, Wry};
use tauri::{SystemTray, SystemTrayEvent};

use std::sync::Mutex;

// イベント通知に載せる付加情報の型
#[derive(Clone, serde::Serialize)]
struct Payload {
  count: usize,
}

fn on_system_tray_event(app: &AppHandle<Wry>, event: SystemTrayEvent) {
    match event {
        SystemTrayEvent::LeftClick { .. } => {
            let count_state: tauri::State<'_, Mutex<usize>> = app.state();
            let mut count = count_state.lock().unwrap();
            *count += 1;

            // 全ウィンドウにイベント通知
            app.emit_all("system_tray_clicked", Payload {
                count: *count,
            }).unwrap();

        },
        _ => {}
    }
}

fn main() {
    let state = Mutex::new(0_usize);

    tauri::Builder::default()

        // 各種登録
        .manage(state)
        .system_tray(SystemTray::new())
        .on_system_tray_event(on_system_tray_event)

        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

フロントエンド側ではlisten関数を使うことでイベント通知を受け取れるようになります。listenはイベント監視をやめるためのハンドラunlistenを返すので、監視を止めたいタイミングでunlistenを呼ぶようにします。Reactの場合useEffectでリッスンを開始し、クリーンアップで終了すると適切でしょう。

TypeScript
import { useEffect, useState } from "react";
import { listen } from "@tauri-apps/api/event";

const App = () => {
  const [count, setCount] = useState<number>(0);

  useEffect(() => {
    let already_unmounted = false;
    let unlisten = () => {};

    (async () => {
      // イベントリッスン開始
      const unlsn = await listen<{ count: number }>(
        "system_tray_clicked",
        (event) => {
          setCount(event.payload.count);
        }
      );

      if (already_unmounted) {
        // await 終了時点でアンマウントされている場合すぐ閉じる
        unlsn();
      } else {
        unlisten = unlsn;
      }
    })();

    return () => {
      already_unmounted = true;

      // イベントリッスン終了
      unlisten();
    };
  }, []);

  return (
    <>
      <div>count: {count}</div>
    </>
  );
};

export default App;

筆者は使わないため紹介しませんでしたが、TypeScriptからRust側へイベント通知を送ることも可能です。invoke呼び出しだけで良くない...?

参考

フロントエンド(TypeScript)編

Tauriにはフロントエンド側から使用できる便利なAPIが多々あります。全ては紹介しませんが、このうちからいくつか抜粋して紹介します。

@tauri-apps/api以下に、appclifs...といった具合にたくさんのモジュールが存在します。各モジュールを使うにはtauri.conf.jsontauri.allowlist以下で明示的に許可しなければならない場合があります。なるべくアプリが持つ権限を少なくするホワイトリスト方式であるためです。

設定ファイルとfsのファイル保存

一つ目に紹介するのはfsモジュールによるファイルの読み込み・保存です。dialogモジュールのopen関数でWebアプリケーションと同様にファイル保存ダイアログを出せますが、せっかくローカルで動作するアプリなのですから、そちらではなく、直接ファイルにアクセスし読み込み・保存できるような機能を紹介します。

fsモジュールを使うにはtauri.conf.jsonallowlistfsの項目を追加します。

tauri.conf.json
{
  // omit details
  "tauri": {
    // omit details
    "allowlist": {
      "all": false,
      "fs": {
        "all": true,
        "scope": ["$APPCONFIG", "$APPCONFIG/*"]
      }
    },
  }
}

fsモジュール独特な項目にscopeがあります。機能と同様に、アクセスできるファイルパスもホワイトリスト方式となっているためです。$APPCONFIGのようなディレクトリ変数の一覧は公式にあります。

上記例ではscopeに設定ファイル用のディレクトリを指定しています。アプリの設定を保存しておく用途としてfsモジュールによる保存はとても便利です。

Reactを使用しているならば、設定保存には専用のカスタムフックを作成すると、いい感じで設定の自動保存機能を記述できます。

次のコードは、アプリ用のディレクトリ(C:/Users/YourName/AppData/Roaming/AppName等)以下にconfig.jsonとして設定ファイルを残し、変更時に保存するように、また、存在すれば起動時に読み込むようにする例です。

./hooks/config-hook.ts(抜粋)
import {
  readTextFile,
  writeTextFile,
  exists,
  createDir,
  BaseDirectory,
} from "@tauri-apps/api/fs";

// omit details

const useConfig = (): useConfigRes => {
  // omit details

  // 初期化時
  useEffect(() => {
    // omit details

    initializeAsyncFn.current = async () => {
      try {

        // 設定ファイルの読み込み
        const profileBookStr = await readTextFile("config.json", {
          dir: BaseDirectory.AppConfig,
        });

        // パース
        const configFile = JSON.parse(profileBookStr) as Config;
        const config = { ...DefaultConfig(), ...configFile };
        setConfig(config);
      
      } catch (error) {
        // 初回はファイルがないのでエラー
        console.warn(error);
        setConfig({
          ...DefaultConfig(),
        });

      }
    };
    initializeAsyncFn.current();
  }, []);

  // 設定変更時
  useEffect(() => {
    // omit details

    (async () => {

      // ディレクトリ存在チェック
      const ext = await exists("", { dir: BaseDirectory.AppConfig });
      if (!ext) {
        await createDir("", { dir: BaseDirectory.AppConfig });
      }

      // 設定ファイルへの書き出し
      await writeTextFile("config.json", JSON.stringify(config), {
        dir: BaseDirectory.AppConfig,
      });

    })();
  }, [config]);

  return [config, { setUserName, setAge }];
};

// omit details
全体
./hooks/config-hook.ts
import { useState, useEffect, useRef } from "react";
import {
  readTextFile,
  writeTextFile,
  exists,
  createDir,
  BaseDirectory,
} from "@tauri-apps/api/fs";

export interface Config {
  userName: string;
  age: number;
}

const DefaultConfig = () => {
  return {
    userName: "Anonymous",
    age: 0,
  };
};

export interface ConfigMethods {
  setUserName: (name: string) => void;
  setAge: (age: number) => void;
}

type useConfigRes = [Config | undefined, ConfigMethods];

const useConfig = (): useConfigRes => {
  const [config, setConfig] = useState<Config | undefined>(undefined);

  const setUserName = (name: string) => {
    const c = config ?? DefaultConfig();
    c.userName = name;
    setConfig({ ...c});
  };

  const setAge = (age: number) => {
    const c = config ?? DefaultConfig();
    c.age = age;
    setConfig({ ...c});
  };

  const initializeAsyncFn = useRef<(() => Promise<void>) | undefined>(
    undefined
  );

  // 初期化時
  useEffect(() => {
    if (initializeAsyncFn.current !== undefined) {
      return;
    }

    initializeAsyncFn.current = async () => {
      try {
        // ファイルロード
        const profileBookStr = await readTextFile("config.json", {
          dir: BaseDirectory.AppConfig,
        });
        const configFile = JSON.parse(profileBookStr) as Config;
        const config = { ...DefaultConfig(), ...configFile };
        setConfig(config);
      } catch (error) {
        console.warn(error);
        setConfig({
          ...DefaultConfig(),
        });
      }

      console.log("Config initialized");
    };
    initializeAsyncFn.current();
  }, []);

  // 設定変更時
  useEffect(() => {
    if (config === undefined) {
      return;
    }

    (async () => {
      // ファイル保存
      const ext = await exists("", { dir: BaseDirectory.AppConfig });
      if (!ext) {
        await createDir("", { dir: BaseDirectory.AppConfig });
      }

      await writeTextFile("config.json", JSON.stringify(config), {
        dir: BaseDirectory.AppConfig,
      });
    })();
  }, [config]);

  return [config, { setUserName, setAge }];
};

export default useConfig;
カスタムフック使用例
App.tsx
import useConfig from "./hooks/config-hook";

const App = () => {
  const [config, configMethods] = useConfig();

  return (
    <>
      User Name: <input type="text" value={config?.userName ?? ""} onChange={e => configMethods.setUserName(e.target.value)}/>
      <br />
      User Age: <input type="number" value={config?.age ?? 0} onChange={e => configMethods.setAge(parseInt(e.target.value))}/>
    </>
  );
}

export default App;

readTextFileでファイルの読み込み、writeTextFileでファイルの書き出しができます。また、例ではJSON.parseJSON.stringifyを使って大胆にオブジェクトをそのまま読み込み/吐きだししています。

Rustは一切関わらず、フロントエンド側ですべて完結し扱いも容易で、アプリ設定を簡単に設計でき実装が捗ります。

参考

ショートカットとイベントハンドラの機能実行

Tauri製ソフトがただWebViewに何かを表示するだけのアプリだったらここまで熱中していなかったかもしれない...その第2弾となる機能です。globalShortcut/registerでキーボードショートカットとそれに結び付くハンドラを設定できます。audio-bookmarkやwin-win-mapではこのショートカット機能をフル活用しています。

ここまでのAPI同様、ショートカット機能をオンにするためにはtauri.conf.jsonに追記が必要です。

tauri.conf.json
{
  // omit details
  "tauri": {
    "allowlist": {
      // omit details
      "globalShortcut": {
        "all": true
      }
    },
    // omit details
  }
}

次のコードはCtrl+cを押すとカウントする例です。TypeScriptだけでお手軽に完結します。

TypeScript
import { useState, useEffect, useRef } from 'react';
import { register, unregisterAll } from "@tauri-apps/api/globalShortcut";

const App = () => {
  const inner_count = useRef<number>(-1);
  const [count, setCount] = useState<number>(0);

  useEffect(() => {
    if (inner_count.current >= 0) {
      return;
    }
    inner_count.current = 0;

    (async () => {

      // 全ショートカットの解除
      await unregisterAll();

      // ショートカットの設定
      await register("Ctrl+c", () => {
        inner_count.current += 1;
        setCount(inner_count.current);
      });
    })();

  }, []);

  return (
    <div>count: {count}</div>
  );
}

export default App;

register関数の第一引数に渡すショートカット文字列に関する詳細な仕様(修飾キーには何が指定できるか等)が見つからなかったのですが、色々試した感じではElectronのものと同様のようです。

ショートカット文字列構築用の関数を定義してonKeyDown等に設定すると捗ります。

TypeScript
const constructShortcut = (
  e: React.KeyboardEvent<HTMLDivElement>
): string => {
  const shortcut_list = [];
  if (e.altKey) {
    shortcut_list.push("Alt");
  }
  if (e.ctrlKey) {
    shortcut_list.push("Control");
  }
  if (e.shiftKey) {
    shortcut_list.push("Shift");
  }

  if (shortcut_list.indexOf(e.key) == -1) {
    shortcut_list.push(e.key);
  }

  const shortcut = shortcut_list.join("+");

  return shortcut;
};
constructShortcut 利用例

input要素のonKeyDownイベントハンドラにて利用しています。input要素にフォーカスが当たった状態でキーボードでキーバインドを"そのまま"入力すると希望したショートカットが記録される仕組みになっています。

Reactにおける良い排他処理の書き方がわからず少し汚いコードになります。もし良い書き方をご存じの方おりましたらコメントいただけると幸いです。

TypeScript
import { useState, useEffect, useRef } from 'react';
import { register, unregisterAll } from "@tauri-apps/api/globalShortcut";

const constructShortcut = (
  e: React.KeyboardEvent<HTMLDivElement>
): string => {
  const shortcut_list = [];
  if (e.altKey) {
    shortcut_list.push("Alt");
  }
  if (e.ctrlKey) {
    shortcut_list.push("Control");
  }
  if (e.shiftKey) {
    shortcut_list.push("Shift");
  }

  if (shortcut_list.indexOf(e.key) == -1) {
    shortcut_list.push(e.key);
  }

  const shortcut = shortcut_list.join("+");

  return shortcut;
};

const App = () => {
  const inner_count = useRef<number>(0);
  const mutex = useRef<boolean>(false);
  const [count, setCount] = useState<number>(0);
  const [shortcut, setShortcut] = useState<string>("Ctrl+c");

  const registerShortcut = async (shortcut: string) => {
    if (mutex.current) {
      return;
    }
    mutex.current = true;

    try {
      await unregisterAll();
  
      await register(shortcut, () => {
        inner_count.current += 1;
        setCount(inner_count.current);
      });
    } finally {
      mutex.current = false;
    }
  };

  useEffect(() => {
    (async () => {
      await registerShortcut(shortcut);
    })();
  }, [shortcut]);

  return (
    <>
      <div>count: {count}</div>
      <input
        type="text"
        value={shortcut}
        onKeyDown={(e) => {
          const s = constructShortcut(e);
          setShortcut(s);
        }}
      />
    </>
  );
}

export default App;

参考

ユーザーとOSのアプリ通知

Tauriならば、システムトレイ等に並びカッコいい機能であるアプリ通知も簡単に実現できます。

image.png

ただし例によって最初に小細工が必要です。

tauri.conf.json
{
  // omit details
  "tauri": {
    "allowlist": {
      // omit details
      "notification": {
        "all": true
      }
    },
    // omit details
  }
}

sendNotificationという関数を、第一引数に通知タイトル、第二引数に内容を指定して呼び出すことで、画像に示したような通知が行えます。

OSに通知許可を得る処理が必要なため、この処理を合わせて以下のようにファイルに独立させて定義しておくと扱いやすいです。

notification.ts
import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/api/notification';

export async function initNotification() {
    await checkPermission();
}

async function checkPermission(): Promise<boolean> {
    let permissionGranted = await isPermissionGranted();
    if (!permissionGranted) {
        const permission = await requestPermission();
        permissionGranted = permission === 'granted';
    }
    return permissionGranted;
}

export async function showNotification(title: string, body: string) {
    if (!(await checkPermission())) {
        return;
    }

    await sendNotification({
        title,
        body,
    });
}
呼び出し例
TypeScript
import { showNotification } from "./notification";

const App = () => {
  return (
    <>
      <button onClick={() => showNotification("Hello!", "from tauri")}>Show Notification</button>
    </>
  );
}

export default App;

参考

Tauri編

最後のTauri編では分類しづらかったり機能面以外で便利なものであったり等を紹介していきます。

CLIとコマンドラインの引数指定

ここで紹介するのはアプリがコマンドライン引数を受け取れるようにする機能です。本機能は実は本記事の執筆を決めてから試したのですが、ハンズオン時点で取り組めていなかった&とても便利な機能と感じたため紹介します。

「GUIアプリなのにコマンドライン引数要らなくない?読み飛ばそう」と思ったそこのあなた!あなたのためにここに書くと、GUIのコマンドライン引数は(少なくともWindowsでは)次のようなシーンで使えます。

  • ファイルを右クリックして出るコンテキストメニューの「プログラムから開く」でアプリを指定するとき
  • アプリのショートカットにファイルをD&Dしたとき

上記のシーンではアプリのコマンドライン引数にファイルパスが渡されます。特定の拡張子のファイルを扱うアプリ等を作るならばほしい機能に思えてきませんか...?

毎度の通りtauri.conf.jsonに設定を追記することで使用できるようになります。ただしallowlistに入れるのではなく、システムトレイと同様にtauri直下の一要素として追記します。この例では、filepathという名前の引数を受け取れるようにしています。

tauri.conf.json
{
  // omit details
  "tauri": {
    "cli": {
      "description": "dsc",
      "longDescription": "long dsc",
      "beforeHelp": "before help",
      "afterHelp": "after help",
      "args": [
        {
          "name": "filepath",
          "index": 1,
          "takesValue": true
        }
      ]
    }
    // omit details
  }
}

受け取った引数はRust、TypeScriptの双方から確認できます。

Rust
// omit details

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![get_entries])

        .setup(|app| {

            // コマンドライン引数の取得
            let Ok(matches) = app.get_cli_matches() else { return Ok(()) };

            println!("CLI matches: {:?}", matches);

            Ok(())
        })

        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
TypeScript
import { useState, useEffect } from 'react';
import { getMatches } from '@tauri-apps/api/cli';

const App = () => {
  const [matches, setMatches] = useState<string>("-");

  useEffect(() => {
    (async () => {

      // コマンドライン引数の取得
      const m = await getMatches();
      setMatches(m.args.filepath?.value?.toString() ?? "-");

    })();
  }, []);

  return (
    <>Arg: {matches}</>
  );
}

export default App;

yarn tauri devコマンドにより本機能を試すにはちょっと工夫が必要です。cargoなど、内部で別なコマンドを実行するタイプのコマンドでは内部コマンドに渡す引数を表すのに--を間に入れますが、筆者の環境ではこれを4つ入れると上手く渡すことが可能でした。

powershell
> yarn tauri dev -- -- -- -- commandlinearg

参考

プラグインとアプリケーションの自動始動

本節ではtauri-plugin-autostartというプラグインを紹介します。本機能を導入すると、アプリケーションのスタートアップ7をアプリ側から登録できるようになります。

OS起動時にアプリを起動できるスタートアップは、筆者が作ったような常駐系ユーティリティアプリにはとても便利な機能です。ただプラグインである都合上導入までが他の機能よりも面倒ではあります。順に説明していきます。

Tauriにプラグインを導入していきます。まず、Cargo.tomlにてtauri-plugin-autostartクレートを追加します。

Cargo.toml
# omit detail

[dependencies]
# omit details
+ tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }

フロント側からも使うプラグインのため、TS側のパッケージも追加しておきます。

PowerShell
> yarn add https://github.com/tauri-apps/tauri-plugin-autostart

本パッケージの追加にはgitがあらかじめインストールされている必要があります。
また、後述のGitHub Actionsにて滞りなくインストールするために、package.jsonではsshではなくhttpsで取得するように書かれていることを確認しましょう。

下準備の最後に、Rust側でTauriビルダーにpluginを登録します。

Rust
// omit details

use tauri_plugin_autostart::MacosLauncher;

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![get_entries])

        // プラグイン適用
        .plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, None))

        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

第一引数はMacOS用のものですがほかのプラットフォームでも指定必須になっています。第二引数は起動時に渡すコマンドライン引数を指定するものです。

ここまで設定することで、TypeScript側からスタートアップの設定ができるようになります。

TypeScript
import { enable, isEnabled, disable } from "tauri-plugin-autostart-api";
import { useEffect, useState } from "react";

const App = () => {
  const [asState, setAsState] = useState<boolean | undefined>(undefined);

  useEffect(() => {
    (async () => {
      setAsState(await isEnabled());
    })();
  }, []);

  return (
    <>
      {asState !== undefined ? <>
        <p>Autostart is {asState ? "enabled" : "disabled"}</p>
        <button onClick={() => {
          if (asState) {
            disable();
          } else {
            enable();
          }
          setAsState(!asState);
        }}>
          {asState ? "Disable" : "Enable"} Autostart
        </button>
      </> : <>loading...</>}
    </>
  );
}

export default App;

スタートアップの状況を取得するisEnabledは非同期関数のため、useEffect内で取得しています。

yarn tauri devではなく、yarn tauri buildで生成されるインストーラー(後述)でインストールして確かめると効果を確認しやすいです。有効になっていれば、設定 > アプリ > スタートアップから確認できるようになっているはずです。

image.png

参考

機能分割とURL指定のマルチウィンドウ

GUIアプリケーションと言えば、シングルウィンドウだけではなく、設定ウィンドウやアラート、ツールタブなど、いくつかのウィンドウに分かれていることがしばしばです。例えばAviUtlなんかはウィンドウまみれです。

Tauriでも複数のウィンドウを生成することが可能です。

静的にウィンドウ生成

基本的な増やし方として、tauri.conf.jsonに追記するだけでウィンドウを増やせます。

tauri.conf.json
{
  // omit details
  "tauri": {
    // omit details
    "windows": [
      {
        "fullscreen": false,
        "resizable": true,
        "title": "tauri-react-player",
        "width": 800,
        "height": 600
      },
      // ウィンドウを追加
      {
        "label": "subwindow",
        "title": "Sub Window",
        // "url": "sub.html" // 後述
      }
    ]
  }
}

urlフィールドを指定しない場合、メインウィンドウと同じウィンドウが生成されます。基本的には、urlを指定し別ページを作ります。

別ページの作成方法はReact等の場合ちょっと複雑なため後述します。

動的にウィンドウ生成

この節はスペース節約の都合上折りたたみます。特に面白みもないので...

Rust
Rust
// omit details

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![get_entries])

        .setup(|app| {
            let _sub_window = tauri::WindowBuilder::new(
                app,
                "RustSubWindow",
                tauri::WindowUrl::App("sub.html".into()),
            )
            .build()?;

            Ok(())
        })

        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
TypeScript
tauri.conf.json
{
  // omit details
  "tauri": {
    "allowlist": {
      // omit details
      "window": {
        "all": true
      }
    },
    // omit details
  }
}
TypeScript (src/main.tsxファイル)
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./styles.css";

// ウィンドウ生成
const webview = new WebviewWindow("TSSubWindw", { url: "sub.html" });

// 生成成功時のコールバック
webview.once("tauri://created", function () {
  console.log("created");
});

// 生成失敗時のコールバック
webview.once("tauri://error", function (e) {
  console.log("error:", e);
});

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

何かしらの処理やデータに対応したウィンドウを作成したい場合に役立ちそうです。

別ページについて

本機能で解説したかったのはどちらかというとこちらになります。

バニラであったり、静的なページであったりと、モジュールバンドラーを使用せず済む場合は、単にページを追加するだけで問題ないのですが、バンドルが必要なページの場合、各バンドラに設定の追記が必要です。記事作成時点のtauri-cli 1.2.3ではバンドラにviteを使用するようになっているため、本節ではviteでの場合を説明します。

...と言ってもそこまで複雑ではなく、vite.config.tsrollupOptionsにて下記のように記述することで、複数ページのバンドルが可能になります。筆者もこれ以上の詳細は現時点で理解していないです

vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
+ import { resolve } from "path";

// omit details

export default defineConfig(async () => ({
  // omit details
  build: {
    // omit details
+    rollupOptions: {
+      input: {
+        index: resolve(__dirname, "index.html"),
+        config: resolve(__dirname, "sub.html"),
+      },
+    },
  },
}));
バンドルされるsub.htmlの例
sub.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Sub Page</title>
  </head>

  <body>
    <div id="root"></div>
    <script type="module" src="/src/sub.tsx"></script>
  </body>
</html>
src/sub.tsx
import React from "react";
import ReactDOM from "react-dom/client";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <>Bundled Sub Page</>
  </React.StrictMode>
);

体感ですが、もしかしたらTauri自体のマルチウィンドウ設定よりもバンドラごとのこういった設定の方法の方が検索で見つけにくいかもしれません。

参考

アイコン生成と自動反映の画竜点睛

GUIアプリは独自のアイコン、独自のショートカットがあってこそ完成します!よね...?

とはいえ、タスクバー用のアイコン、システムトレイ用のアイコン、ウィンドウ左上のアイコン、ショートカットのアイコン、、、と、設定しなければならない対象は多く、サイズも色々用意する必要がありそうです。

そんなあなたの願望に応え、Tauriには各種アイコンを1枚の画像から設定するCLIアプリケーションが内包されています。例えばimage.pngという画像をアイコンにしたい場合、以下のコマンドを打つだけで設定できます。

PowerShell
> yarn tauri icon image.png

アイコンを変更したい場合はまた上記コマンドを打てば良いのですが、Windowsの場合だとショートカットアイコンのみ変更されない時があります。
そのような場合は一度targetフォルダごとふっ飛ばしてビルドし直すと変更されます8

アイコンが決まると一気にアプリが完成した感が出ますね!

参考

インストーラとGitHubActionsのデプロイ革命

皆さんは完成したアプリケーションをどのように配布していますか...?zipに圧縮したり使用者にGitHubからクローンして手元でビルドしてもらうなどでしょうか...?折角アプリを作っても/作りたくても導入しにくいとモチベに影響しそうです。

でも安心してください。Tauriは配布する手段の中では一番楽な、インストーラを生成してくれます。yarn tauri devではなく、yarn tauri buildコマンドを打つだけで、例えばWindows向けならばsrc-tauri > target > release > bundle > msi以下にインストーラが生成されます。

PowerShell
> yarn tauri build
...
> ls src-tauri/target/release/bundle/msi

    ディレクトリ: C:\path\to\tauri-react-player\src-tauri\target\release\bundle\msi

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2023/03/22      0:35        2404352 tauri-react-player_0.0.0_x64_en-US.msi

ちなみにインストーラを使うとWebView2もインストールされるため、 Windows10でも動きます

インストーラとそのチェックサムを配布サイトに置いておくのも良いですが、Tauri公式が提供するGitHub Actionsを使うとインストーラをGitHub側で作成しさらにリリースページで配布までできます!ワークフローの改ざんは厳しそうですし、GitHubに公開したコードそのままでビルドされるので、チェックサムを使った配布よりも信頼できる配布方法と言えるでしょう。

公式に掲載されているでは、releaseブランチプッシュ時にリリースDraftを作成するようになっています。

.github > workflowsにワークフローファイルを置きプッシュすることで、条件が揃えば(例ではreleaseにプッシュされれば)ワークフローが走ります。以下はカスタムで触ることがあるかもしれない項目です。

項目 内容
on > push tagsでタグを指定したり、branchesreleaseではなく直接mainにしたり等
... > matrix > platform クロスプラットフォームに関する項目ですが、例えばWindows向けにしか作っていない場合はここを減らします。
伴い、ubuntu onlyとなっている項目を消したりすることもありそうです。
actions/setup-node@v3のwith > node-version nodeバージョンは手元のものに合わせましょう。
.github/workflows/build.yaml
name: 'publish'
on:
  push:
    branches:
      - release

jobs:
  publish-tauri:
    permissions:
      contents: write
    strategy:
      fail-fast: false
      matrix:
        platform: [macos-latest, ubuntu-20.04, windows-latest]

    runs-on: ${{ matrix.platform }}
    steps:
      - uses: actions/checkout@v3
      - name: setup node
        uses: actions/setup-node@v3
        with:
          node-version: 16
      - name: install Rust stable
        uses: dtolnay/rust-toolchain@stable
      - name: install dependencies (ubuntu only)
        if: matrix.platform == 'ubuntu-20.04'
        run: |
          sudo apt-get update
          sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
      - name: install frontend dependencies
        run: yarn install # change this to npm or pnpm depending on which one you use
      - uses: tauri-apps/tauri-action@v0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tagName: app-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version
          releaseName: 'App v__VERSION__'
          releaseBody: 'See the assets to download this version and install.'
          releaseDraft: true
          prerelease: false

node-versionのみ修正し実際に走らせてみました。

修正点差分
.github/workflows/build.yaml
name: 'publish'
on:
  push:
    branches:
      - release

jobs:
  publish-tauri:
    permissions:
      contents: write
    strategy:
      fail-fast: false
      matrix:
        platform: [macos-latest, ubuntu-20.04, windows-latest]

    runs-on: ${{ matrix.platform }}
    steps:
      - uses: actions/checkout@v3
      - name: setup node
        uses: actions/setup-node@v3
        with:
-          node-version: 16
+          node-version: 18
      - name: install Rust stable
        uses: dtolnay/rust-toolchain@stable
      - name: install dependencies (ubuntu only)
        if: matrix.platform == 'ubuntu-20.04'
        run: |
          sudo apt-get update
          sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
      - name: install frontend dependencies
        run: yarn install # change this to npm or pnpm depending on which one you use
      - uses: tauri-apps/tauri-action@v0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tagName: app-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version
          releaseName: 'App v__VERSION__'
          releaseBody: 'See the assets to download this version and install.'
          releaseDraft: true
          prerelease: false
PowerShell
> git branch
  main
* release
> git add -A
> git commit -m "v0.0.1"
> git push origin release

プッシュ後にリポジトリのActionsページ (例) に飛ぶと、ワークフローが実行されていることが確認できます。

image.png

Error: Resource not accessible by integrationというエラーが出ることがあります。
再現条件が特定できなかったのですが、このエラーが出た際は
Settings > Actions > General > Workflow permissions
を、Read and write permissionsに変更してみてください。

参考: Github ActionsでResource not accessible by integrationが出た際の対処法

成功するとこんな感じです。

image.png

作成されたDraftからリリースを作成すればデプロイ完了です。

リポジトリのトップページ右にReleasesという項目があります。ここをクリックするとリリース一覧に飛べます。

すると先程作成したDraftがあります。

リリースドラフト.png

:pencil: から編集ページに飛び、一番下緑色のPublish releaseを押すことでリリースが作成されます。

image.png

リリースが生成されるとリポジトリのトップページ右に最新リリースへのリンクが出来ます。カッコいいリリースが出来ました!

image.png

参考

まとめ・所感

ここ数ヶ月Tauriを触った技術的な感想は、「言語は手段として捉え適材適所で替えた方が結局楽しい」というものでした。

少し前の自分であれば、覚えたての言語を激推しして、「それ〇〇でもできます!」と、ドキュメントが不十分だろうとライブラリが他の言語のラッパーしかなかろうと無理やりその言語を使って何かを作ることに喜びを覚えていました。

そんな折、RustとTypeScriptの両方を書いて分かったのは、「文法や強みが違っても"価値観"9が同じ言語たちがいる」ということでした。

むしろ、ある言語で学んだ考えが他の言語を学んだ時に出てきた時のほうが嬉しいのです。RustとTypeScriptは対照的な2人と言ったのですが、型理論や関数型パラダイムなど、モダン言語にあると嬉しいような共通点も多々あります。まるでぼっちちゃんも喜多ちゃんも「変わりたい」という気持ちをきっかけにギターを握ったみたいですね。

一つの言語だけにこだわっている限り決して知ることができない...色々な言語を知ることで見える景色があったのです。
そして、Tauriを使ったプログラミングは、RustとTypeScriptそれぞれの強みをより強調し、言語を超えた良いパラダイムの存在を教えてくれる...そんな景色を見せてくれる百合作品なのでした。

筆者の1番推しはTypeScript×Rust10ですが、この考え方は別な"カプ"でも流用できるんじゃないかと予想しています。

読者の方もぜひ一推しのカプを探してみましょう!ここまで読んでいただきありがとうございました。

その他 参考・引用

  1. TypeScriptではなくJavaScriptやAltJSとしてRust(with Yew)を指定することも可能ですが、本記事ではTypeScript with Reactで統一します。好きなので。

  2. 転天、記事執筆が思ったより長引いて投稿が最終回放映後になってしまいました(涙) 最終回最高でした(涙2) まだ観たことがない方は有料ですがdアニで配信されているのでそこから観ましょう。

  3. DioxusというGUIライブラリも最近出たようですが、よく調べるとこれはTauriベースらしいので候補から外しました。なお、BevynannouAmethystといったゲーム用描画エンジンも外しています。

  4. 呪文は2 girls, sisters, flat chest, steampunk, overcoat, programming, computerです。唐突にこの挿絵を入れたのはトレンドがChatGPTに占領されているのでなんか対抗したくなったからです(単純)。課金した上でプロンプトエンジニアやってみて思ったのですが望みどおりの絵を描けるという能力は今後もしばらくAIに取って代わられない気がします...時にRustとTypeScriptがペアプロする話結構真面目に読みたいので今度ChatGPTにでも書かせますかね

  5. ないだろうと思っていたら実は既存アプリあったみたいなパターンだったとしても特に驚きはしないので、もし類似のアプリをご存じの方おりましたらコメントで教えていただけると幸いです(やっぱり気になるので)。

  6. 各項目のタイトルが不自然なのは命名規則に従っているためです。

  7. Windowsの場合スタートアップには、アプリ側から設定するタイプと、ユーザーが任意に指定するタイプがあります。リンク先は後者ですがそれは説明のためで本プラグインとは関係なく、本プラグインでは前者タイプで設定します。Windowsではアプリのスタートアップは設定 > アプリ > スタートアップから確認できます。

  8. ある意味この注意もとい備忘録を書くために本機能を紹介しました。

  9. 本来ならパラダイムと記述するべき箇所ですが、「大切にしたいパラダイム」的な意味を持たせたかったためあえて価値観と表現しました。擬人法です。

  10. TypeScriptがタチ、Rustがネコでたまにリバありかなって...まぁどっちでも良い派ですが()

84
44
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
84
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?