2024年7月22日月曜日

Tauri 2.0 の Android プラグインの Example を動かす

やること

Tauri 2.0 の Android プラグインを作りたくなったので、まずは Example を動かすところからやってみる。

前提

  • OS: Windows 11 Pro 23H2 ビルド 22631.3880
  • Docker Desktop: Version 4.32.0 (157355)
  • tauri2 と Android ビルドの環境が構築済みであること。
  • Windows 上に Android Studio がインストール済みであり、 adb コマンドが使えること

プラグインのひな形プロジェクト(Example) を作成

$ npx @tauri-apps/cli@next plugin new --android example
✔ What should be the Android Package ID for your plugin? · com.plugin.example

api プロジェクトのビルド

これで、フロントエンドから import { ping } from "tauri-plugin-example"; して ping(...) できるようになる。

cd tauri-plugin-example/
npm i
npm run build

Example アプリのビルド

「プラグインプロジェクト単体でビルド」という概念は無いので、 Example プロジェクトのビルドを行う。

cd example/tauri-app
npm i

@tauri-apps/cli@2.0.0-beta.22 時点では package.json の生成にバグがあり(node or npm のバージョンかも?) 推奨環境を利用していないため、npm error Unsupported URL Type "link:": link:../../ と言われてしまうため、パスを修正する。

sed -i -e 's/link://' ./package.json

そして Android プロジェクトのビルド。

identifiercom.tauri.dev, version0.0.0 のままだとビルドができないのでこれを書き換えてビルド。

sed -i -e 's/com\.tauri\.dev/dev.mikoto2000.study.android.plugin.example/' ./src-tauri/tauri.conf.json
sed -i -e 's/0\.0\.0/0.0.1/' ./src-tauri/tauri.conf.json
npm run tauri android init
npm run tauri android build -- --target aarch64 -d

これで、 src-tauri/gen/android/app/build/outputs/apk/universal/debug/app-universal-debug.apk に apk ファイルができるので、 Android 端末かエミュレーターか WSA にインストールする。

動作確認

自分の環境では物理端末が一番手っ取り早かったのでそれにインストール。

adb.exe install \\wsl.localhost\Ubuntu-24.04\home\mikoto\project\TIL\tauri\2.0.0-beta\plugin\android\firststep\tauri-plugin-example\examples\tauri-app\src-tauri\gen\android\app\build\outputs\apk\universal\debug\app-universal-debug.apk

Ping したら Pong が返ってくる。良さそう。

自分のプロジェクトに組み込みたい場合には、この Example プロジェクトの真似をして package.jsonCargo.toml にプラグインプロジェクトを登録すれば OK.

変更履歴

日付 変更内容
2024/7/22-1 新規作成
2024/7/22-2 「バグがあり」という記載が間違のようなので記載修正。

2024年7月20日土曜日

Rust のテンプレートエンジンを試す(Liquid 編)

やること

Rust のテンプレートエンジンを試す(Tera 編) の Liquiid 編。

今回は cobalt-org/liquid-rust: Liquid templating for Rust を試してみる。

前提

プロジェクト作成

cargo init
cargo add liquid

実装

テンプレートの作成

Rust のテンプレートエンジンを試す(Tera 編) と同じものを利用。

templates/mail.template.txt:

件名: 面談のお願い

{{ recipient_name }}様

はじめまして。{{ company_name }}の{{ sender_name }}と申します。

突然のご連絡失礼いたします。貴社のご活躍にいつも感銘を受けております。
この度、ぜひ{{ recipient_company_name }}の{{ recipient_name }}様とお話をさせていただきたく、面談の機会をお願い申し上げます。

具体的には、以下の日程でご都合の良いお時間をお知らせいただけますでしょうか。

{{ proposed_dates }}

面談の内容としましては、{{ discussion_topic }}についてお伺いできればと考えております。
ご多忙のところ恐縮ですが、少しのお時間をいただけますと幸いです。

お手数をおかけいたしますが、どうぞよろしくお願い申し上げます。

敬具

{{ sender_name }}
{{ company_name }}
{{ your_email }}
{{ your_phone_number }}

テンプレートレンダリング処理の実装

src/main.rs:

fn main() {
    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse_file("./templates/mail.template.txt")
        .unwrap();

    let proposed_dates = "1) 2024/6/10 12:00-13:00
2) 2024/6/17 12:00-13:00
3) 2024/6/24 12:00-13:00";

    let mail_info = liquid::object!({
            "company_name": "三光飼料 株式会社",
            "recipient_name": "山田 太郎",
            "recipient_company_name": "有限会社 リファレンス",
            "sender_name": "大雪 命",
            "your_phone_number": "000-0000-0000",
            "discussion_topic": "あのあれのあれ",
            "proposed_dates": proposed_dates,
            "your_email": "mikoto2000@example",
    });

    let output = template.render(&mail_info).unwrap();
    println!("{}", output);
}

動作確認

$ cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s
     Running `target/debug/firststep`
件名: 面談のお願い

山田 太郎様

はじめまして。三光飼料 株式会社の大雪 命と申します。

突然のご連絡失礼いたします。貴社のご活躍にいつも感銘を受けております。
この度、ぜひ有限会社 リファレンスの山田 太郎様とお話をさせていただきたく、面談の機会をお願い申し上げます。

具体的には、以下の日程でご都合の良いお時間をお知らせいただけますでしょうか。

1) 2024/6/10 12:00-13:00
2) 2024/6/17 12:00-13:00
3) 2024/6/24 12:00-13:00

面談の内容としましては、あのあれのあれについてお伺いできればと考えております。
ご多忙のところ恐縮ですが、少しのお時間をいただけますと幸いです。

お手数をおかけいたしますが、どうぞよろしくお願い申し上げます。

敬具

大雪 命
三光飼料 株式会社
mikoto2000@example
000-0000-0000

うん、良さそう。

うーん、やはり文字を流し込む程度の使い方だと、どのテンプレートエンジンも素直に使えて、差がわからん感じですね…。

liquid はテンプレートを登録して名前から利用するのができない感じなのかな?

以上。

参考資料

2024年7月19日金曜日

Rust のテンプレートエンジンを試す(Tera 編)

やること

以前 Go 言語のテンプレートエンジンを触った が、同じことを Rust でもやりたい。

Rust ではサードパーティのテンプレートエンジンがいくつかあるようなのでまずは Keats/tera を試してみる。

前提

プロジェクト作成

cargo init
cargo add tera
cargo add serde --features derive
cargo add serde_json

実装

テンプレートの作成

Go 言語でやった時 と同じものを利用。 (ただし、 tera 用にプレースホルダの書き方は変えてある)

templates/mail.template.txt:

件名: 面談のお願い

{{ recipient_name }}様

はじめまして。{{ company_name }}の{{ sender_name }}と申します。

突然のご連絡失礼いたします。貴社のご活躍にいつも感銘を受けております。
この度、ぜひ{{ recipient_company_name }}の{{ recipient_name }}様とお話をさせていただきたく、面談の機会をお願い申し上げます。

具体的には、以下の日程でご都合の良いお時間をお知らせいただけますでしょうか。

{{ proposed_dates }}

面談の内容としましては、{{ discussion_topic }}についてお伺いできればと考えております。
ご多忙のところ恐縮ですが、少しのお時間をいただけますと幸いです。

お手数をおかけいたしますが、どうぞよろしくお願い申し上げます。

敬具

{{ sender_name }}
{{ company_name }}
{{ your_email }}
{{ your_phone_number }}

テンプレートレンダリング処理の実装

src/main.rs:

use std::process::exit;

use serde::{Deserialize, Serialize};
use tera::{Context, Tera};

// テンプレートに流し込むデータを定義した構造体
// serde_json の Serialize を付与しておくと、
// テンプレートの情報元にできる。
#[derive(Serialize, Deserialize, Clone, Debug)]
struct MailInfo {
    // 自分の会社名
    pub company_name: String,
    // 相手の名前
    pub recipient_name: String,
    // 相手の会社名
    pub recipient_company_name: String,
    // 自分の名前
    pub sender_name: String,
    // 自分の電話番号
    pub your_phone_number: String,
    // 議題
    pub discussion_topic: String,
    // 候補日時
    pub proposed_dates: String,
    // 自分の電話番号
    pub your_email: String,
}

fn main() {
    // glob 形式でファイル群を指定できる。
    // 必ず * を付ける必要があり、 Tera がテンプレート名として登録する際には ** は削除される。
    // template 専用ディレクトリを作ってそこにテンプレートを全部放り込む運用が必要
    let tera = match Tera::new("./templates/*.template.txt") {
        Ok(t) => t,
        Err(e) => {
            println!("Parsing error(s): {}", e);
            exit(1);
        }
    };

    {
        let proposed_dates = "1) 2024/6/10 12:00-13:00
2) 2024/6/17 12:00-13:00
3) 2024/6/24 12:00-13:00";

        let mail_info = MailInfo {
            company_name: "三光飼料 株式会社".to_string(),
            recipient_name: "山田 太郎".to_string(),
            recipient_company_name: "有限会社 リファレンス".to_string(),
            sender_name: "大雪 命".to_string(),
            your_phone_number: "000-0000-0000".to_string(),
            discussion_topic: "あのあれのあれ".to_string(),
            proposed_dates: proposed_dates.to_string(),
            your_email: "mikoto2000@example.com".to_string(),
        };

        // 情報をテンプレートに流し込み、レンダリングする
        let mail_string = tera
            .render(
                "mail.template.txt",
                &Context::from_serialize(&mail_info).unwrap(),
            )
            .unwrap();

        // レンダリングした結果を表示
        println!("{}", mail_string);
    }
}

動作確認

$ cargo run
   Compiling firststep v0.1.0 (/workspaces/TIL/rust/template/tera/firststep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.66s
     Running `target/debug/firststep`
件名: 面談のお願い

山田 太郎様

はじめまして。三光飼料 株式会社の大雪 命と申します。

突然のご連絡失礼いたします。貴社のご活躍にいつも感銘を受けております。
この度、ぜひ有限会社 リファレンスの山田 太郎様とお話をさせていただきたく、面談の機会をお願い申し上げます。

具体的には、以下の日程でご都合の良いお時間をお知らせいただけますでしょうか。

1) 2024/6/10 12:00-13:00
2) 2024/6/17 12:00-13:00
3) 2024/6/24 12:00-13:00

面談の内容としましては、あのあれのあれについてお伺いできればと考えております。
ご多忙のところ恐縮ですが、少しのお時間をいただけますと幸いです。

お手数をおかけいたしますが、どうぞよろしくお願い申し上げます。

敬具

大雪 命
三光飼料 株式会社
mikoto2000@example.com
000-0000-0000

うん、良さそう。

表面しか触っていないからだと思うけど、 Go 言語の text/template とあまり変わらない感じがする。

以上。

参考資料

2024年7月18日木曜日

Tauri の setup 関数内で State の初期化を行う

前回 の書き方だと、setup 関数内で求めたもろもろを State に渡せないので、 setup 内で State の初期化をする方法をまとめる。

前提

  • 開発環境構築は済んでいるものとする
  • Tauri:
    • tauri-cli: 1.5.14
    • rust: 1.78.0
    • node: v22.2.0
  • State をベースに作業をしたので、そのディレクトリとの差分を取ってください

実装

App が manage 関数を持っているので、それに渡すだけ。

コード内にコメントでも書いてあるが、 例えば、 userDataDir 内に SQLite3 DB を置いてマイグレートして コネクションをはって、そのコネクションをステートに入れるとかいう使い方ができる。

src-tauri/src/main.rs:

// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use std::sync::{
    atomic::{AtomicUsize, Ordering},
    Arc, Mutex,
};

use tauri::{Manager, State};

// State 用構造体
// AtomicUsize は、既にスレッドセーフなので
// Arc と Mutex で囲む必要はない。
struct Counter {
    count: AtomicUsize,
}

#[tauri::command]
fn inc_count(counter: State<'_, Counter>) -> usize {
    // 最大値のチェックは省略
    counter.count.fetch_add(1, Ordering::Relaxed) + 1
}

#[tauri::command]
fn dec_count(counter: State<'_, Counter>) -> usize {
    if counter.count.load(Ordering::Relaxed) > 0 {
        counter.count.fetch_sub(1, Ordering::Relaxed) - 1
    } else {
        0
    }
}

// けんぱスタック
// `けん` か `ぱ` を要素に持つ配列を記録する。
struct KenPaStack {
    ken_pa: Arc<Mutex<Vec<String>>>,
}

#[tauri::command]
fn put_ken(ken_pa_stack: State<'_, KenPaStack>) {
    ken_pa_stack
        .ken_pa
        .lock()
        .unwrap()
        .append(&mut vec!["けん".to_string()]);
}

#[tauri::command]
fn put_pa(ken_pa_stack: State<'_, KenPaStack>) {
    ken_pa_stack
        .ken_pa
        .lock()
        .unwrap()
        .append(&mut vec!["ぱ".to_string()]);
}

#[tauri::command]
fn get_ken_pa_stack(ken_pa_stack: State<'_, KenPaStack>) -> Vec<String> {
    ken_pa_stack.ken_pa.lock().unwrap().clone()
}

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            // 例えば userDataDir 内に SQLite3 DB を置いてマイグレートして、
            // コネクションをステートに入れるとかいう使い方ができる。

            // State として管理してほしいインスタンスを登録する
            app.manage(Counter {
                count: AtomicUsize::new(0),
            });
            app.manage(KenPaStack {
                ken_pa: Arc::new(Mutex::new(Vec::new())),
            });

            Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            inc_count,
            dec_count,
            put_ken,
            put_pa,
            get_ken_pa_stack
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

src/App.tsx:

ここは前回と全く同じ。

import { useState } from "react";
import "./App.css";
import { invoke } from "@tauri-apps/api";

function App() {

  const [count, setCount] = useState(0);

  const [kenPaStack, setKenPaStack] = useState<Array<string>>([]);

  async function incCount() {
    let count = await invoke<number>('inc_count', {});
    setCount(count);
  }

  async function decCount() {
    let count = await invoke<number>('dec_count', {});
    setCount(count);
  }

  async function putKen() {
    invoke<number>('put_ken', {});
  }

  async function putPa() {
    invoke<number>('put_pa', {});
  }

  async function getKenPaStack() {
    let kenPaStack = await invoke<Array<string>>('get_ken_pa_stack', {});
    setKenPaStack(kenPaStack);
  }

  return (
    <div className="container">
      <h1>Welcome to Tauri!</h1>

      <h2>Counter example:</h2>
      <div>
        <button onClick={decCount}>-</button>
        <input type="text" value={count} readOnly></input>
        <button onClick={incCount}>+</button>
      </div>

      <h2>KEN-PA example:</h2>
        <button onClick={putKen}>けん</button>
        <button onClick={putPa}>ぱ</button>
        <button onClick={getKenPaStack}>けんけんぱできた?</button>
        <div>
          <ul>
          {
            kenPaStack.map((e) => <li>{e}</li>)
          }
          </ul>
        </div>
    </div>
  );
}

export default App;

動作確認

初期化位置が変わっただけなので挙動に変更なし。OK.

以上。

参考資料

WSL2 の Ubuntu 24.04 環境を整える

やること

今使っている WSL2 のディストリビューションがまだ Ubuntu 22.04 だったので、 24.04 に切り替え、初期設定を行う。

インストール

WSL のアップデート

> wsl --update
更新プログラムを確認しています。
Linux 用 Windows サブシステムの最新バージョンは既にインストールされています。

インストール可能なディストリビューションの確認

> wsl --list --online
インストールできる有効なディストリビューションの一覧を次に示します。
'wsl.exe --install <Distro>' を使用してインストールします。

NAME                                   FRIENDLY NAME
Ubuntu                                 Ubuntu
Debian                                 Debian GNU/Linux
kali-linux                             Kali Linux Rolling
Ubuntu-18.04                           Ubuntu 18.04 LTS
Ubuntu-20.04                           Ubuntu 20.04 LTS
Ubuntu-22.04                           Ubuntu 22.04 LTS
Ubuntu-24.04                           Ubuntu 24.04 LTS
OracleLinux_7_9                        Oracle Linux 7.9
OracleLinux_8_7                        Oracle Linux 8.7
OracleLinux_9_1                        Oracle Linux 9.1
openSUSE-Leap-15.5                     openSUSE Leap 15.5
SUSE-Linux-Enterprise-Server-15-SP4    SUSE Linux Enterprise Server 15 SP4
SUSE-Linux-Enterprise-15-SP5           SUSE Linux Enterprise 15 SP5
openSUSE-Tumbleweed                    openSUSE Tumbleweed

Ubuntu 24.04 のインストール

> wsl --install Ubuntu-24.04
インストール中: Ubuntu 24.04 LTS
Ubuntu 24.04 LTS がインストールされました。
Ubuntu 24.04 LTS を起動しています...
Installing, this may take a few minutes...
Please create a default UNIX user account. The username does not need to match your Windows username.
For more information visit: https://aka.ms/wslusers
Enter new UNIX username: mikoto
New password:
Retype new password:
passwd: password updated successfully
Installation successful!
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

Welcome to Ubuntu 24.04 LTS (GNU/Linux 5.15.153.1-microsoft-standard-WSL2 x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

 System information as of Thu Jul 18 12:30:14 JST 2024

  System load:  0.52                Processes:             28
  Usage of /:   0.1% of 1006.85GB   Users logged in:       0
  Memory usage: 2%                  IPv4 address for eth0: 172.23.180.66
  Swap usage:   0%


This message is shown once a day. To disable it please create the
/home/mikoto/.hushlogin file.
mikoto@mnmain:~$

OK.

初期設定

apt upgrade

apt を更新。

sudo apt update
sudo apt upgrade

~/bin にパスを通す

いつも自作のシングルバイナリやスクリプトはここに入れているので、ディレクトリを作ってパスを通す。

mkdir ~/bin
echo 'export PATH="$PATH:~bin"' >> ~/.bashrc

継続して使いたいファイルのコピー

手抜きして 20.04 から .ssh, .gitconfig をコピー。 Windows のエクスプローラーでやったのち、 .ssh は権限を変更。

chmod 700 .ssh
chmod 700 .ssh/*

本当なら ssh-keygen で鍵を作って GitHub に登録。

.vim の配置

.vim 用のリポジトリがあるので、それをクローン。

git clone --recursive git@github.com:mikoto2000/dotvim .vim

docker のインストール

Dev container がないと開発できない体になってしまっているので、 docker をインストールする。

Install Docker Engine on Ubuntu | Docker Docs を見ながら作業実施。

sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# 一般ユーザーが sudo 無しで docker コマンドを使えるようにする

```sh
sudo usermod -aG docker mikoto

ログアウトしてログインすると、 mikoto ユーザーが sudo 無しに docker コマンドを使えるようになる。

$ exit
> wsl -d Ubuntu-24.04
$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
c1ec31eb5944: Pull complete
Digest: sha256:1408fec50309afee38f3535383f5b09419e6dc0925bc69891e79d84cc4cdcec6
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

OK.

使用ツールのダウンロード

なんだかんだ言って今使ってるのって devcontainer.vim だけか。 ~/bin にダウンロードする。

curl -L https://github.com/mikoto2000/devcontainer.vim/releases/download/v1.0.3/devcontainer.vim-linux-amd64 -o ~/bin/devcontainer.vim
chmod u+x ~/bin/devcontainer.vim

ひとまずはこれだけでいいはず。

devcontainer.vim の動作確認

最近作っている OASIZ_TimeLogger2 をクローンして開発環境を立ち上げられるか試す。

mkdir ~/project
cd ~/project
git clone git@github.com:mikoto2000/OASIZ_TimeLogger2
cd OASIZ_TimeLogger2
devcontainer.vim start .

初回起動が遅すぎて別ターミナル開いて ps aux してしまった。初回の docker build がすごく重い…

時間はかかったが Vim が起動し、ビルドも実行もできた。 WSLg で Windows 側に Window も表示された。 良さそう。

Ubuntu 20.04 をデフォルトに設定

良さそうなので Ubuntu 20.04 をデフォルトに設定する。

> wsl --set-default Ubuntu-24.04

新しく wsltty を開いて、デフォルトが効いているか確認。

$ cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo

OK.

以上。

参考資料

2024年7月17日水曜日

Tauri のプラグインを作る(Hello, World! 編)

やること

最終目標は「Tauri2.0 の Android インテント送信プラグインを作る」なのだけど、 プラグイン自体がよくわからないので最小構成で作ってみる。

作るものは hello-world プラグイン。

on_xxx 時に標準出力を出力するのと、 プラグインが提供するコマンド hello コマンドを実装する。

利用される側(プラグイン)の作成

プラグインプロジェクトの作成

cd ${STUDY_ROOT}
npx @tauri-apps/cli plugin init --api -d . -n helloworld
cd tauri-plugin-helloworld
npm i

バックエンドプラグイン実装

setup, on_webview_ready, on_page_load 時に plintln! するのと、 プラグインコマンドとして hello を定義し、 呼ばれたら Rust 側で `Hello, from Plugin!!!” と標準出力する。

tauri-plugin-helloworld/src/commands.rs:

use tauri::command;

// プラグインコマンドの定義
// 普通のコマンドと変わらない
#[command]
pub async fn hello() {
  println!("Hello, from Plugin!!!");
}

tauri-plugin-helloworl/dsrc/libs.rs:

mod commands;

use tauri::{
    plugin::{Builder, TauriPlugin},
    Runtime,
};

// プラグインの初期化
// init 関数内で Builder を組み立て、 build した結果を返却する。
pub fn init<R: Runtime>() -> TauriPlugin<R> {
    println!("Initialize helloworld plugin!");
    Builder::new("helloworld")
       .setup(|_app| {
           println!("Setup!");
           Ok(())
       })
       .on_webview_ready(|_window| {
           println!("OnWebViewReady!");
       })
       .on_page_load(|_window, _payload| {
           println!("OnPageLoad!");
       })
      .invoke_handler(tauri::generate_handler![commands::hello])
      .build()
}

フロントエンドプラグイン実装

フロントエンドプラグインを作らないと、先ほど作った hello コマンドは

await invoke('plugin:helloworld|hello')

という形で呼び出す必要がある。

これは面倒なのでラッパーを作る。

tauri-plugin-helloworld/webview-src/index.ts:

import { invoke } from '@tauri-apps/api/tauri'

export async function hello() {
  await invoke('plugin:helloworld|hello')
}

作成した後、 npm run build を実行すると、ビルド結果が tauri-plugin-helloworl/dwebview-dist に出力される。

利用する側の作成

プラグインを利用するプロジェクトの作成

作成したプラグインプロジェクトの隣に、プラグインを利用する側のアプリケーションプロジェクトを作成する。

$ cd ${STUDY_ROOT}
$ npm create tauri-app@latest

> tauri-plugin-helloworld-api@0.0.0 npx
> create-tauri-app

✔ Project name · app
✔ Choose which language to use for your frontend · TypeScript / JavaScript - (pnpm, yarn, npm, bun)
✔ Choose your package manager · npm
✔ Choose your UI template · React - (https://react.dev/)
✔ Choose your UI flavor · TypeScript

Template created! To get started run:
  cd app
  npm install
  npm run tauri dev

$ cd app
$ npm i
$ npm run tauri dev

バックエンドプロジェクトのクレート更新(Cargo.toml の更新)

Cargo.toml に作成したプラグインプロジェクトを登録する。

app/src-tauri/Cargo.toml:

[package]
name = "app"
version = "0.0.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"

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

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

[dependencies]
tauri = { version = "1", features = ["shell-open"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-helloworld = { path = "../../tauri-plugin-helloworld" } # この行を追加(本来は crates.io に公開したものを使う)

[features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]

プラグインの登録

アプリの Builder にプラグインを登録する。

app/src-tauri/src/main.rs:

// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .plugin(tauri_plugin_helloworld::init()) // この行を追加でプラグインを有効化
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

フロントエンドのパッケージ追加(package.json の更新)

dependencies"tauri-plugin-helloworld2-api": "file:../tauri-plugin-helloworld/webview-dist/" という形で作成したパッケージを追加。

npm i ../tauri-plugin-helloworkd

フロントエンドからプラグインコマンドを呼び出す

プラグインコマンドを呼び出すために、ボタンひとつだけのアプリに作り替える。

app/src/App.tsx:

import { hello } from "tauri-plugin-helloworld-api";
import "./App.css";

function App() {

  return (
    <div className="container">
      <button onClick={() => hello()}>hello</button>
    </div>
  );
}

export default App;

動作確認

Initialize helloworld plugin!, Setup!, OnWebViewReady!, OnPageLoad! が出力されるし、 ボタンを押すと Hello, from Plugin!!! と表示される。

良さそう。以上。

参考資料

2024年7月9日火曜日

notify クレートを使って Rust でファイル監視する

やること

ファイルの内容変更を監視し、ファイル変更があったら何かしらを行うヤツを作る。

前提

プロジェクト作成

cargo init
cargo add notify

実装

いくつか u8 配列を用意して、エンディアン指定で抜き出してみた。

// 対象のファイルを監視し、ファイルに変更が行われたらイベントを標準出力へ出力します。
// 監視方法の recommend は、Linux なら大抵 inotify, Windows は監視用の Win32 API を利用するらしい。
//
// 第一引数: 監視対象ファイル
// 第二引数: 監視方法(recommend or polling)
use std::{env, path::Path, sync::mpsc, time::Duration};

use notify::{Config, PollWatcher, RecursiveMode, Watcher};

fn main() {
    // 引数パース
    let args: Vec<String> = env::args().collect();
    let is_polling = if args[2] == "recommend" {
        false
    } else if args[2] == "polling" {
        true
    } else {
        panic!("第二引数に未知の監視方法が入力されました。 recommend か polling を入力してください。");
    };
    let path = Path::new(&args[1]).as_ref();
    println!("Start watch: {:?}", path);

    // 監視イベントを通知するためのチャンネルを作る。
    // Watcher がファイルの更新を検知したら、 tx へ書き込むので、
    // 通知を受け取りたい側は rx を読む。
    let (tx, rx) = mpsc::channel();

    // watcher の作成
    let _watcher: Box<dyn Watcher> = if is_polling {
        // ポーリングでファイル監視する
        // ポーリングタイミングは `with_poll_interval` で指定した期間だが、
        // アプリへのイベント発火は「変更が行われたとき」となる。
        println!("ポーリングで 1 秒間隔に監視を行います。");
        let config = Config::default().with_poll_interval(Duration::from_secs(1));
        let mut poll_watcher = PollWatcher::new(tx, config).unwrap();
        poll_watcher
            .watch(path, RecursiveMode::NonRecursive)
            .unwrap();
        Box::new(poll_watcher)
    } else {
        println!("inotify による監視を行います。");
        let mut recommended_watcher = notify::recommended_watcher(move |event| {
            let _ = tx.send(event);
        })
        .unwrap();
        recommended_watcher
            .watch(path, RecursiveMode::NonRecursive)
            .unwrap();
        Box::new(recommended_watcher)
    };

    // ファイルに変更が行われると、rx チャンネルにイベントが来るので、
    // それを読み込んで表示する。大抵は別スレッドでやることになるだろう。
    while let Ok(res) = rx.recv() {
        match res {
            Ok(event) => println!("Event: {:?}", event),
            Err(error) => println!("Error: {:?}", error),
        }
    }
}

動作確認

$ cargo run -- /workspaces/TIL/rust/file_wtach/notify/firststep/README.md recommend
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/firststep /workspaces/TIL/rust/file_wtach/notify/firststep/README.md recommend`
Start watch: "/workspaces/TIL/rust/file_wtach/notify/firststep/README.md"
recommend による監視を行います。
Event: Event { kind: Modify(Data(Any)), paths: ["/workspaces/TIL/rust/file_wtach/notify/firststep/README.md"], attr:tracker: None, attr:flag: None, attr:info: None, attr:source: None }
Event: Event { kind: Modify(Data(Any)), paths: ["/workspaces/TIL/rust/file_wtach/notify/firststep/README.md"], attr:tracker: None, attr:flag: None, attr:info: None, attr:source: None }
Event: Event { kind: Modify(Metadata(Any)), paths: ["/workspaces/TIL/rust/file_wtach/notify/firststep/README.md"], attr:tracker: None, attr:flag: None, attr:info: None, attr:source: None }
Event: Event { kind: Access(Close(Write)), paths: ["/workspaces/TIL/rust/file_wtach/notify/firststep/README.md"], attr:tracker: None, attr:flag: None, attr:info: None, attr:source: None }

色々と情報を持った Event が渡されれてくるので、それを基に処理が書ける。

以上。

参考資料

2024年7月6日土曜日

serde_yaml を使って Rust で YAML をパースするやつを Internally tagged で作り直した

やること

serde_yaml を使って Rust で YAML をパースする で YAML のパースを行ったが、 Internally tagged 形式で構造体を定義したほうがその後の処理がやりやすそうだったのでやってみた。

前提

実装

main.rs:

use std::{fs::File, io::BufReader};

use serde::{Deserialize, Serialize};

// ビットフラグの 1 ビットを表す構造体
#[derive(Serialize, Deserialize, Debug)]
struct LayoutItem {
    // 表示名
    name: String,
    // ビットフラグのビット位置
    position: u8,
    // ビットが 1 だった時に表示する値
    true_label: Option<String>,
    // ビットが 0 だった時に表示する値
    false_label: Option<String>,
}

// コンフィグの要素は、以下 5 種類のどれかとなる
// - UINT8
// - UINT16
// - UINT32
// - UINT64
// - FLAGS
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
enum ConfigItem {
    UINT8(BasicConfigItem),
    UINT16(BasicConfigItem),
    UINT32(BasicConfigItem),
    UINT64(BasicConfigItem),
    FLAGS(BitFlagConfigItem),
}

// 数値データの単位を表す構造体
#[derive(Serialize, Deserialize, Debug)]
struct BasicConfigItem {
    // 表示名
    name: String,
    // ファイル先頭からのオフセット
    offset: u8,
    // オフセットから何バイト読み込むか
    size: u8,
    // エンディアン
    endianness: Option<String>,
}

// ビットフラグデータの単位を表す構造体
#[derive(Serialize, Deserialize, Debug)]
struct BitFlagConfigItem {
    // 表示名
    name: String,
    // ファイル先頭からのオフセット
    offset: u8,
    // オフセットから何バイト読み込むか
    size: u8,
    // エンディアン
    endianness: Option<String>,
    // type が FLAGS の時のみ利用されるフィールド
    layout: Vec<LayoutItem>,
}

fn main() {
    // yaml ファイルを読み込み、 Reader 化
    let yaml = "./yaml/setting.yaml";
    let yaml_file = File::open(yaml).unwrap();
    let reader = BufReader::new(yaml_file);

    // serde に Reader を渡し、YAML を構造体へデシリアライズ
    // 構造体の定義さえできてしまえば 1 行で完了。
    let config: Vec<ConfigItem> = serde_yaml::from_reader(reader).unwrap();

    // デシリアライズされた構造体を走査して表示
    for ci in config {
        match ci {
            ConfigItem::UINT8(i)
            | ConfigItem::UINT16(i)
            | ConfigItem::UINT32(i)
            | ConfigItem::UINT64(i) => {
                println!("name: {}", i.name);
                println!("offset: {}", i.offset);
                println!("endianness: {}", i.endianness.unwrap_or("".to_string()));
                println!("layout:");
            }
            ConfigItem::FLAGS(i) => {
                println!("name: {}", i.name);
                println!("offset: {}", i.offset);
                println!("endianness: {}", i.endianness.unwrap_or("".to_string()));
                println!("layout:");
                for l in i.layout {
                    println!("name: {}", l.name);
                    println!("position: {}", l.position);
                    println!("true_label: {}", l.true_label.unwrap_or("".to_string()));
                    println!("false_label: {}", l.false_label.unwrap_or("".to_string()));
                }
            }
        }
    }
}

以上。

参考資料

byteorder クレートを使って Rust でバイナリファイルをパースする

やること

固定長バイナリファイルを読み込み、指定したバイト範囲の数値表現を取得するものを作る。

前提

プロジェクト作成

cargo init
cargo add byteorder

実装

いくつか u8 配列を用意して、エンディアン指定で抜き出してみた。

main.rs:

use std::io::Cursor;

use byteorder::{ByteOrder, BigEndian, LittleEndian, ReadBytesExt};

fn main() {

    let offset = 0;
    let size = 2;

    // ビッグエンディアンで 12345 を格納
    let big_endian_byte_array = vec![48, 57];
    let big_endian_number = BigEndian::read_u16(&big_endian_byte_array[offset..offset+size]);

    // リトルエンディアンで 12345 を格納
    let little_endian_byte_array = vec![57, 48];
    let little_endian_number = LittleEndian::read_u16(&little_endian_byte_array[offset..offset+size]);

    println!("big_endian_number: {}", big_endian_number);
    println!("little_endian_number: {}", little_endian_number);

    // 1 バイト目に 8, 2, 3 バイト目に、リトルエンディアンで 256 を格納
    let byte_array = vec![8, 0, 1];
    // u8 は単純に取ってこれるから byteorder にメソッドはないらしい
    let number_8 = byte_array[0];
    let number_256 = LittleEndian::read_u16(&byte_array[1..1+2]);

    println!("number_8: {}", number_8);
    println!("number_256: {}", number_256);


    // Cursor も使える
    let mut rdr = Cursor::new(byte_array);

    let rdr_8 = rdr.read_u8().unwrap();
    let rdr_256 = rdr.read_u16::<LittleEndian>().unwrap();

    println!("rdr_8: {}", rdr_8);
    println!("rdr_256: {}", rdr_256);
}

動作確認

$ cargo run
   Compiling firststep v0.1.0 (/workspaces/TIL/rust/binary/ByteReader/firststep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s
     Running `target/debug/firststep`
big_endian_number: 12345
little_endian_number: 12345
number_8: 8
number_256: 256
rdr_8: 8
rdr_256: 256

うん、良さそう。

以上。

参考資料

2024年7月5日金曜日

serde_yaml を使って Rust で YAML をパースする

やること

設定ファイルとして YAML を採用したので、 Rust での扱い方を確認する。

前提

プロジェクト作成

cargo init
cargo add serde --features="derive"
cargo add serde_yaml

実装

mikoto2000/binp: バイナリファイルからデータを抽出するツール。 の設定ファイルの構造をパースしてみる。

./yaml/setting.yaml:

- name: UINT64_value
  offset: 0
  size: 8
  type: UINT64
  endianness: LITTLE
- name: UINT32_value
  offset: 8
  size: 4
  type: UINT32
  endianness: LITTLE
- name: UINT16_value
  offset: 12
  size: 2
  type: UINT16
  endianness: LITTLE
- name: UINT8_value
  offset: 14
  size: 1
  type: UINT8
  endianness: LITTLE
- name: BIT_FLAG
  offset: 15
  size: 1
  data_type: FLAGS
  layout:
    - name: LED1
      position: 0
    - name: LED2
      position: 1
      true_label: "high"
      false_label: "low"

main.rs:

use std::{fs::File, io::BufReader};

use serde::{Deserialize, Serialize};

// ビットフラグの 1 ビットを表す構造体
#[derive(Serialize, Deserialize, Debug)]
struct LayoutItem {
    // 表示名
    name: String,
    // ビットフラグのビット位置
    position: u8,
    // ビットが 1 だった時に表示する値
    true_label: Option<String>,
    // ビットが 0 だった時に表示する値
    false_label: Option<String>,
}

// パースする単位を表す構造体
// 本当は layout あり・なしで型定義を分けるのが良いのだろうが、
// firststep だしまぁいいかという感じで。
#[derive(Serialize, Deserialize, Debug)]
struct ConfigItem {
    // 表示名
    name: String,
    // ファイル先頭からのオフセット
    offset: u8,
    // オフセットから何バイト読み込むか
    size: u8,
    // データ型
    // UINT,INT 8-64 と FLAGS(ビットフラグ)
    #[serde(alias = "type")]
    data_type: String,
    // エンディアン
    endianness: Option<String>,
    // type が FLAGS の時のみ利用されるフィールド
    layout: Option<Vec<LayoutItem>>,
}

fn main() {
    // yaml ファイルを読み込み、 Reader 化
    let yaml = "./yaml/setting.yaml";
    let yaml_file = File::open(yaml).unwrap();
    let reader = BufReader::new(yaml_file);

    // serde に Reader を渡し、YAML を構造体へデシリアライズ
    // 構造体の定義さえできてしまえば 1 行で完了。
    let config: Vec<ConfigItem> = serde_yaml::from_reader(reader).unwrap();

    // デシリアライズされた構造体を走査して表示
    for ci in config {
        println!("name: {}", ci.name);
        println!("offset: {}", ci.offset);
        println!("type: {}", ci.data_type);
        println!("endianness: {}", ci.endianness.unwrap_or("".to_string()));
        println!("layout:");
        if ci.layout.is_some() {
            for l in ci.layout.unwrap() {
                println!("name: {}", l.name);
                println!("position: {}", l.position);
                println!("true_label: {}", l.true_label.unwrap_or("".to_string()));
                println!("false_label: {}", l.false_label.unwrap_or("".to_string()));
            }
        }
    }
}

動作確認

$ cargo run
   Compiling firststep v0.1.0 (/workspaces/TIL/rust/yaml/serde_yaml/firststep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.21s
     Running `target/debug/firststep`
name: UINT64_value
offset: 0
type: UINT64
endianness: LITTLE
layout:
name: UINT32_value
offset: 8
type: UINT32
endianness: LITTLE
layout:
name: UINT16_value
offset: 12
type: UINT16
endianness: LITTLE
layout:
name: UINT8_value
offset: 14
type: UINT8
endianness: LITTLE
layout:
name: BIT_FLAG
offset: 15
type: FLAGS
endianness: 
layout:
name: LED1
position: 0
true_label: 
false_label: 
name: LED2
position: 1
true_label: high
false_label: low

表示は汚いけどまぁ想定通りにできてはいそう。

以上。

参考資料

clap-rs で Rust のコマンドライン引数をパースする

やること

clap-rs/clap: A full featured, fast Command Line Argument Parser for Rust を使ってオプションパースする。

clap-rs には、 Builder を使ってオプションを定義する方法と、 Derive を使ってオプションを定義する方法のふたつがある。

今回は、 Derive を使用してオプションを定義していく。

サブコマンドなどの機能もあるようだが、今回は単純なオプションパースのみを行う。

前提

プロジェクト作成

cargo init
cargo add clap --features derive

実装

オプション用の構造体を作成し、フィールドとしてオプションを作成。 それぞれにアトリビュートを付けることでそれがコマンドラインオプションであることを表す。

アトリビュート詳細は clap::_derive - Rust

use clap::Parser;


#[derive(Parser, Debug)]
#[command(version, about = "コマンドライン引数パース処理練習プログラム")]
struct Args {
    #[arg(short, long, help = "文字列オプション")]
    string: String,

    #[arg(short, long, default_value = "default", help = "オプション変数はデフォルト値を入れる感じ")]
    default_string: String,

    #[arg(long, help = "u8 オプション")]
    number_u8: u8,

    #[arg(long, help = "u16 オプション")]
    number_u16: u16,

    #[arg(short, long, help = "f32 オプション")]
    f32: f32,

    #[arg(short, long, help = "真偽値オプション")]
    bool: bool,

    #[arg(long, help = "文字配列オプション")]
    string_array: Vec<String>,
}

fn main() {
    let args = Args::parse();

    println!("文字列オプションを渡せますよー: {}", args.string);
    println!("文字列オプションオプションを渡せますよー: {}", args.default_string);
    println!("数値(u8)オプションを渡せますよー: {}", args.number_u8);
    println!("数値(u16)オプションを渡せますよー: {}", args.number_u16);
    println!("数値(f32)オプションを渡せますよー: {}", args.f32);
    println!("真偽値オプションを渡せますよー: {}", args.bool);
    println!("配列オプションを渡せますよー: {:?}", args.string_array);
}

単純なオプションを定義していった。

動作確認

$ cargo run -- --help
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/clap-rs_firststep --help`
コマンドライン引数パース処理練習プログラム

Usage: clap-rs_firststep [OPTIONS] --string <STRING> --number-u8 <NUMBER_U8> --number-u16 <NUMBER_U16> --f32 <F32>

Options:
  -s, --string <STRING>                  文字列オプション
  -d, --default-string <DEFAULT_STRING>  オプション変数はデフォルト値を入れる感じ [default: default]
      --number-u8 <NUMBER_U8>            u8 オプション
      --number-u16 <NUMBER_U16>          u16 オプション
  -f, --f32 <F32>                        f32 オプション
  -b, --bool                             真偽値オプション
      --string-array <STRING_ARRAY>      文字配列オプション
  -h, --help                             Print help
  -V, --version                          Print version


$ cargo run -- -s aaa \
>         --number-u8 8 \
>         --number-u16 16 \
>         --string-array kore \
>         --string-array sore \
>         -b \
>         -f 32.33 \
>         --default-string ddd
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/clap-rs_firststep -s aaa --number-u8 8 --number-u16 16 --string-array kore --string-array sore -b -f 32.33 --default-string ddd`
文字列オプションを渡せますよー: aaa
文字列オプションオプションを渡せますよー: ddd
数値(u8)オプションを渡せますよー: 8
数値(u16)オプションを渡せますよー: 16
数値(f32)オプションを渡せますよー: 32.33
真偽値オプションを渡せますよー: true
配列オプションを渡せますよー: ["kore", "sore"]

うん、良さそう。

以上。

参考資料

2024年7月3日水曜日

Tauri の State で状態を管理する

Tauri に状態の保存を任せられるので、任せられるものは任せていきましょう。

前提

  • 開発環境構築は済んでいるものとする
  • Tauri:
    • tauri-cli: 1.5.14
    • rust: 1.78.0
    • node: v22.2.0
  • delete-default-event をベースに作業をしたので、そのディレクトリとの差分を取ってください

実装

今回は、定番のカウンターと、配列操作を作ってみる。

Tauri が提供する StateArc<Mutex<xxx>> を突っ込むイメージ。

src-tauri/src/main.rs:

// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use std::sync::{
    atomic::{AtomicUsize, Ordering},
    Arc, Mutex,
};

use tauri::State;

// State 用構造体
// AtomicUsize は、既にスレッドセーフなので
// Arc と Mutex で囲む必要はない。
struct Counter {
    count: AtomicUsize,
}

#[tauri::command]
fn inc_count(counter: State<'_, Counter>) -> usize {
    // 最大値のチェックは省略
    counter.count.fetch_add(1, Ordering::Relaxed) + 1
}

#[tauri::command]
fn dec_count(counter: State<'_, Counter>) -> usize {
    if counter.count.load(Ordering::Relaxed) > 0 {
        counter.count.fetch_sub(1, Ordering::Relaxed) - 1
    } else {
        0
    }
}

// けんぱスタック
// `けん` か `ぱ` を要素に持つ配列を記録する。
struct KenPaStack {
    ken_pa: Arc<Mutex<Vec<String>>>,
}

#[tauri::command]
fn put_ken(ken_pa_stack: State<'_, KenPaStack>) {
    ken_pa_stack
        .ken_pa
        .lock()
        .unwrap()
        .append(&mut vec!["けん".to_string()]);
}

#[tauri::command]
fn put_pa(ken_pa_stack: State<'_, KenPaStack>) {
    ken_pa_stack
        .ken_pa
        .lock()
        .unwrap()
        .append(&mut vec!["ぱ".to_string()]);
}

#[tauri::command]
fn get_ken_pa_stack(ken_pa_stack: State<'_, KenPaStack>) -> Vec<String> {
    ken_pa_stack.ken_pa.lock().unwrap().clone()
}

fn main() {
    tauri::Builder::default()
        // State として管理してほしいインスタンスを登録する
        .manage(Counter {
            count: AtomicUsize::new(0),
        })
        .manage(KenPaStack {
            ken_pa: Arc::new(Mutex::new(Vec::new())),
        })
        .invoke_handler(tauri::generate_handler![
            inc_count,
            dec_count,
            put_ken,
            put_pa,
            get_ken_pa_stack
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

src/App.tsx:

rust で実装したコマンドを呼び出し、結果を表示する。

import { useState } from "react";
import "./App.css";
import { invoke } from "@tauri-apps/api";

function App() {

  const [count, setCount] = useState(0);

  const [kenPaStack, setKenPaStack] = useState<Array<string>>([]);

  async function incCount() {
    let count = await invoke<number>('inc_count', {});
    setCount(count);
  }

  async function decCount() {
    let count = await invoke<number>('dec_count', {});
    setCount(count);
  }

  async function putKen() {
    invoke<number>('put_ken', {});
  }

  async function putPa() {
    invoke<number>('put_pa', {});
  }

  async function getKenPaStack() {
    let kenPaStack = await invoke<Array<string>>('get_ken_pa_stack', {});
    setKenPaStack(kenPaStack);
  }

  return (
    <div className="container">
      <h1>Welcome to Tauri!</h1>

      <h2>Counter example:</h2>
      <div>
        <button onClick={decCount}>-</button>
        <input type="text" value={count} readOnly></input>
        <button onClick={incCount}>+</button>
      </div>

      <h2>KEN-PA example:</h2>
        <button onClick={putKen}>けん</button>
        <button onClick={putPa}>ぱ</button>
        <button onClick={getKenPaStack}>けんけんぱできた?</button>
        <div>
          <ul>
          {
            kenPaStack.map((e) => <li>{e}</li>)
          }
          </ul>
        </div>
    </div>
  );
}

export default App;

動作確認

カウンターのインクリメント・デクリメントができていることと、 けんぱスタックにけんとぱが積まれていくことが確認できる。

良さそう。

今回は例の用途が良くなく、あまり意味のないモノになってしまったが、 そもそもフロントエンドに渡さなくていい状態や、 ステートマシンの結果の一部のみをフロントエンドに渡すなどという使い方をするのかな? という感じです。

あと、 State の '_ が何なのか分からないのでご存じの型教えていただけると嬉しいです。

以上。

参考資料

2024年7月2日火曜日

Tauri で XDG Base Directory を取得する

この辺りに則っていると考えることが少なくなるので、利用していきましょう。

前提

  • 開発環境構築は済んでいるものとする
  • Tauri:
    • tauri-cli: 1.5.14
    • rust: 1.78.0
    • node: v22.2.0
  • delete-default-event をベースに作業をしたので、そのディレクトリとの差分を取ってください

実装

今回は、以下 3 つの箇所から取得するのを実際に試してみる。

  • 初期化時(setup 関数内)
  • イベント内
  • コマンド処理内

基本的には、 App か AppHandle から PathResolver を取得し、そこから各ディレクトリを取得する。

src-tauri/src/main.rs:

前述の 3 つのタイミングでディレクトリを取得して表示するように実装。

// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use tauri::{api::path::*, AppHandle, Manager};

// コマンドでは、引数に自動挿入される AppHandle から pathresolver が取得できる。
#[tauri::command]
fn print_xdg_base_directories(app_handle: AppHandle) {
    println!("==== コマンド内で取得して表示開始 ====");

    // PathResolver の取得
    let path_resolver = app_handle.path_resolver();

    let resource_dir = path_resolver.resource_dir();
    println!("resource_dir: {:?}", resource_dir);

    let resolve_resource = path_resolver.resolve_resource("../assets/logo.svg");
    println!("resolve_resource: {:?}", resolve_resource);

    let app_config_dir = path_resolver.app_config_dir();
    println!("app_config_dir: {:?}", app_config_dir);

    let app_data_dir = path_resolver.app_data_dir();
    println!("app_data_dir: {:?}", app_data_dir);

    let app_local_data_dir = path_resolver.app_local_data_dir();
    println!("app_local_data_dir: {:?}", app_local_data_dir);

    let app_cache_dir = path_resolver.app_cache_dir();
    println!("app_cache_dir: {:?}", app_cache_dir);

    let app_log_dir = path_resolver.app_log_dir();
    println!("app_log_dir: {:?}", app_log_dir);

    println!("==== コマンド内で取得して表示終了 ====");
}

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            // setup 関数では、app から path_resolve を取得すれば OK.
            println!("==== setup 関数内で取得して表示開始 ====");

            // PathResolver の取得
            let path_resolver = app.path_resolver();

            let resource_dir = path_resolver.resource_dir();
            println!("resource_dir: {:?}", resource_dir);

            let resolve_resource = path_resolver.resolve_resource("../assets/logo.svg");
            println!("resolve_resource: {:?}", resolve_resource);

            let app_config_dir = path_resolver.app_config_dir();
            println!("app_config_dir: {:?}", app_config_dir);

            let app_data_dir = path_resolver.app_data_dir();
            println!("app_data_dir: {:?}", app_data_dir);

            let app_local_data_dir = path_resolver.app_local_data_dir();
            println!("app_local_data_dir: {:?}", app_local_data_dir);

            let app_cache_dir = path_resolver.app_cache_dir();
            println!("app_cache_dir: {:?}", app_cache_dir);

            let app_log_dir = path_resolver.app_log_dir();
            println!("app_log_dir: {:?}", app_log_dir);

            // Tauri2 で削除予定
            let app_dir = path_resolver.app_dir();
            println!("app_dir: {:?}", app_dir);

            // Tauri2 で削除予定
            let log_dir = path_resolver.log_dir();
            println!("log_dir: {:?}", log_dir);

            // `tauri::api::path::` での取得
            println!("audio_dir: {:?}", audio_dir());
            println!("cache_dir: {:?}", cache_dir());
            println!("config_dir: {:?}", config_dir());
            println!("data_dir: {:?}", data_dir());
            println!("desktop_dir: {:?}", desktop_dir());
            println!("document_dir: {:?}", document_dir());
            println!("download_dir: {:?}", download_dir());
            println!("executable_dir: {:?}", executable_dir());
            println!("font_dir: {:?}", font_dir());
            println!("home_dir: {:?}", home_dir());
            println!("local_data_dir: {:?}", local_data_dir());
            println!("picture_dir: {:?}", picture_dir());
            println!("public_dir: {:?}", public_dir());
            println!("runtime_dir: {:?}", runtime_dir());
            println!("template_dir: {:?}", template_dir());
            println!("video_dir: {:?}", video_dir());

            println!("==== setup 関数内で取得して表示終了 ====");

            {
                // イベントは、クロージャで path_resolve を受け渡せば取得できる
                let path_resolver = app.path_resolver().clone();
                let _ = app.listen_global("event", move |_| {
                    println!("==== イベント内で取得して表示開始 ====");

                    let resource_dir = path_resolver.resource_dir();
                    println!("resource_dir: {:?}", resource_dir);

                    let resolve_resource = path_resolver.resolve_resource("../assets/logo.svg");
                    println!("resolve_resource: {:?}", resolve_resource);

                    let app_config_dir = path_resolver.app_config_dir();
                    println!("app_config_dir: {:?}", app_config_dir);

                    let app_data_dir = path_resolver.app_data_dir();
                    println!("app_data_dir: {:?}", app_data_dir);

                    let app_local_data_dir = path_resolver.app_local_data_dir();
                    println!("app_local_data_dir: {:?}", app_local_data_dir);

                    let app_cache_dir = path_resolver.app_cache_dir();
                    println!("app_cache_dir: {:?}", app_cache_dir);

                    let app_log_dir = path_resolver.app_log_dir();
                    println!("app_log_dir: {:?}", app_log_dir);

                    // Tauri2 で削除予定
                    let app_dir = path_resolver.app_dir();
                    println!("app_dir: {:?}", app_dir);

                    // Tauri2 で削除予定
                    let log_dir = path_resolver.log_dir();
                    println!("log_dir: {:?}", log_dir);

                    println!("==== イベント内で取得して表示終了 ====");
                });
            }

            Ok(())
        })
        .invoke_handler(tauri::generate_handler![print_xdg_base_directories])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

src/App.tsx:

単純にコマンド呼び出しとイベント送信をするだけ。

import { invoke } from "@tauri-apps/api";
import "./App.css";
import { emit } from "@tauri-apps/api/event";

function App() {

  return (
    <div className="container">
      <h1>Welcome to Tauri!</h1>

      <button onClick={() => {
        invoke('print_xdg_base_directories', {});
      }}>コマンド実行</button>
      <button onClick={() => {
        emit('event', {});
      }}>イベント送信</button>

    </div>
  );
}

export default App;

動作確認

起動時・コマンド実行時・イベント送信時、それぞれでディレクトリが取得できていることを確認できる。

...(snip)
==== コマンド内で取得して表示開始 ====
resource_dir: Some("/workspaces/TIL/tauri/1.0.0/XdgBaseDirectory/tauri-delete-default-event/src-tauri/target/debug")
resolve_resource: Some("/workspaces/TIL/tauri/1.0.0/XdgBaseDirectory/tauri-delete-default-event/src-tauri/target/debug/_up_/assets/logo.svg")
app_config_dir: Some("/home/node/.config/dev.mikoto2000.study.tauri.delete-default-event")
app_data_dir: Some("/home/node/.local/share/dev.mikoto2000.study.tauri.delete-default-event")
app_local_data_dir: Some("/home/node/.local/share/dev.mikoto2000.study.tauri.delete-default-event")
app_cache_dir: Some("/home/node/.cache/dev.mikoto2000.study.tauri.delete-default-event")
app_log_dir: Some("/home/node/.config/dev.mikoto2000.study.tauri.delete-default-event/logs")
==== コマンド内で取得して表示終了 ====
==== イベント内で取得して表示開始 ====
resource_dir: Some("/workspaces/TIL/tauri/1.0.0/XdgBaseDirectory/tauri-delete-default-event/src-tauri/target/debug")
resolve_resource: Some("/workspaces/TIL/tauri/1.0.0/XdgBaseDirectory/tauri-delete-default-event/src-tauri/target/debug/_up_/assets/logo.svg")
app_config_dir: Some("/home/node/.config/dev.mikoto2000.study.tauri.delete-default-event")
app_data_dir: Some("/home/node/.local/share/dev.mikoto2000.study.tauri.delete-default-event")
app_local_data_dir: Some("/home/node/.local/share/dev.mikoto2000.study.tauri.delete-default-event")
app_cache_dir: Some("/home/node/.cache/dev.mikoto2000.study.tauri.delete-default-event")
app_log_dir: Some("/home/node/.config/dev.mikoto2000.study.tauri.delete-default-event/logs")
app_dir: Some("/home/node/.config/dev.mikoto2000.study.tauri.delete-default-event")
log_dir: Some("/home/node/.config/dev.mikoto2000.study.tauri.delete-default-event/logs")
==== イベント内で取得して表示終了 ====

良さそう。

以上。

参考資料

以上。