前回作ったaudio-bookmarkにつづき、Rust + React with Tauri でまたまた自己満足アプリを作りました!本記事では、作品紹介のついでに、Tauriならではな機能も紹介し、ポエムでTauriを布教していきます!
今回作ったのは win-win-map (Windows' Windows Map)というWindows用ウィンドウ管理ユーティリティアプリです。
リポジトリ: 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の機能紹介に飛びたい方は こちら
Tauriと百合の関係性に関する考察
「Tauriが人気を集めている百合作品?何言ってるんだ?この筆者」と思われたかもしれません。確かに「人気を集めている」は誇張でした。しかし実際、Tauriは魅力的な百合アニメたちに似た特徴を持っているんです。
まぞくのシャミ子と魔法少女桃。陰キャのぼっちと陽キャの喜多ちゃん。破天荒な千束と真面目なたきな。最近人気な百合作品は対照的な2人のバディ物な側面もあり、その対照的なことを起因とするイベントが物語を彩っています。
Tauriにもこの構造が認められるのです。型に憧れながら型に恵まれなかった(トランスパイラ止まりの)人気者TypeScriptと、型にめちゃくちゃ恵まれながらも完璧さゆえに(難しいせいで、開発者から)相手にされないRust…2人が協力して魅力的なデスクトップアプリケーションを作る……Tauriは、まるで現在佳境を迎えている百合アニメ2さながらのあらすじになっています。
Rustはシステムプログラミング言語としての地位は確立しつつある一方、GUIアプリ制作の手段としては弱いのが現実です。バックエンド以外での商業的価値を考えるとこの点は致命的かもしれません。
一応(Tauriを除く)候補を乱雑に挙げると、基本となるwinitを始めとして、OpenGLのようなGPU向けのグラフィックAPI wgpu、GTKのラッパーとしてのgtk-rs、そのほかeguiやIced等3、様々な種類が存在しますが、C#の.NET coreのような、キラーフレームワークはないという現状です。敷居の高さも相まって、Rustだけではどうしても(特にGUI周りの)エコシステムやコミュニティの成長に不安が残ります。
一方TypeScriptは数値計算以外では幅広い分野で使用が期待できそうですが、JavaScript譲りの不安要素や余剰機能があり、堅牢なアプリケーション制作に向いていると自信を持って言うのは厳しそうです。トランスパイルしてしまえば結局はJS、つまりインタプリタ言語であることも、その他の静的型付け言語を置き換えるほどの言語になれない一因でしょう。
しかし、TypeScriptにはRustにはない 設計変更の柔軟さとフロントエンド(GUI)構築にまつわる豊富なフレームワーク・ライブラリ・コミュニティがある というメリットが、そしてRustにはTypeScriptにはない システムの根幹を自信をもって作れる、OSのAPIやFFIをがっつり触れる というメリットがあります。
つまりこの「対照的な」2人が互いに手を取り協力したら…?最強で楽しいアプリケーションが作れると思いませんか?そんな夢を叶えてくれる百合作品もといフレームワークがTauriなのです!!
RustちゃんとTypeScriptちゃん 作: Novel AI4
ここまでふざけて話しましたが、真面目な話、開発効率を最大にするならRustとTypeScriptによる担当分野分けには文句の付けようがありません。Rustで書いている部分をTypeScriptで書こうとしたらリソース管理や非同期等で永遠に沼りそうですし、Rustでフロントエンドに相当する部分を書くぐらいなら豊富にライブラリや情報がネットに落ちているTypeScriptで書いたほうが苦労しないでしょう。
「1人で悩まず2人で手を取り合い事件解決していく」...素晴らしい王道パターンです。Tauriは今季(次世代?)の覇権フレームワークになる力を秘めているのです!
アプリの解説
Tauriの百合ポエムはこの辺にしてここからは今回作ったアプリ win-win-map の解説に入っていきます。
制作した動機
マウスカーソルやウィンドウが召喚できるwin-win-mapですが、正直「面白いだけのオモチャアプリでは?」と思われた方が多いのではないでしょうか?これはその通りで、需要がなさそうだったのも制作動機の一つだったりします。需要があれば何かしら既存のアプリが存在するものです5。
本当の動機は筆者の作業環境にあります。
筆者は普段ロフトベットの机で作業しています。しかし、たまにはベッドで寝ながら作業したいので、ベッドにもモニタを設置しています。こちらのモニタには、下の階のメインモニタをミラーリングしています。
ベッドでは、常に全モニタが見えているわけではありません。こうなると、ベッドに行くたびに、下の大きいモニタで作業したウィンドウやマウスカーソルを階上モニタに持って行く必要があります。
この作業が手間だったのが本アプリ制作の動機となりました。DIYみたいなノリです。
win-win-mapの機能
改めて機能紹介になります。
- ショートカットによりマウスカーソルを設定座標に召喚
- ショートカット及びボタンによりウィンドウを設定座標に召喚
- 閾値外(ベッド上モニタじゃない領域)にあるウィンドウの自動召喚
- マップ上ドラッグによるウィンドウ移動
偏った機能しかありませんが自己満足アプリなのでご愛嬌。要望があればissueを下さると幸いです。
自分しか使わないために設定項目も偏っていますが、テキストファイルを編集したりといった面倒な方法を避けています。
win-win-mapの大まかな構成・制作手順
audio-bookmarkと同様に、Win32API(windows-rs)を利用してウィンドウを動かしています。そのため構成も似通っています。
フロントエンドで使用した技術の説明は省略します。機能紹介を替わりとさせていただきます。
バックエンドで使用したWin32APIの解説はZennにて別記事にしてみました。興味があれば読んでいただけると幸いです。↓
大まかな制作手順は、
- Rust側だけでウィンドウやマウスカーソルを移動させるCLIアプリケーションをお試しで作り、APIとして盤石化する
- フロント側から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
バージョン確認詳細
> 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.
[dependencies]
tauri = { version = "1.2", features = ["path-all", "protocol-asset", "window-all"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tauriのfeatures
はtauri.conf.json
に依存して自動で書き換わるので気にしないでください。
まとめということでハンズオンで紹介した内容も改めて簡単に紹介しています。
例として挙げているソースコードは見やすさを優先した不完全なものです。適宜ハンズオンのソースコード等を参照してください。
また例ではunwrap
を多用していますが実際にアプリを作る時は適切なエラーハンドリングを心がけましょう。
目次6
バックエンド(Rust)編
コマンドとマクロの機能定義
Rust側の関数に#[tauri::command]
という属性風マクロをつけ、invoke_handler
に登録すると、TypeScript側からinvoke
関数で呼び出せるようになります。Tauriの最も基本となる機能です。
マクロをつける関数は同期関数でも非同期関数でもどちらでも構いません。例では同期関数につけています。
// 関数に属性風マクロをつける
#[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");
}
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 追記
invoke
は useEffect
を使うのは適さないケースが多い(get_entries
はめずらしく適切なケース)です。二回実行されて困るような処理ならば、可能な限りユーザーアクションに結びついたイベントハンドラに書きましょう(useRef
は最終手段です)。
参考にどうぞ: 「Reactでawaitしたら壊れた」「Reactでawaitしたら壊れた」 ~ useEffectの誤用と2回実行 ~ - Qiita
以降本記事でも useEffect
を多用していますが執筆当時筆者が↑を知らなかったためです。ご了承ください。
なおコマンドの引数として、TypeScript側で指定するものの他、関数を呼び出したウィンドウや、アプリのAppHandle
、アプリがメインループで所有しているリソース等を渡すことも可能です。
以下はバックエンド側で状態管理している例です。
// 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
で包んでいます。
フロント側
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;
参考
- Calling Rust from the frontend | Tauri Apps
- Accessing the Window in Commands
- Accessing Managed State
serdeとデータの構造定義
前項目の関数呼び出しにおいて、Rust関数をTypeScriptで扱うには引数も返り値もRustとTypeScriptの両方で扱える型である必要があります。
「両方で扱える型」は、「JSONで使える型」と考えて差支えありません。そのため文字列型や数値型は特に何もせずとも引数、返り値の型として指定できます。(なお返り値でResult
型を返すようにすると、Err
の時はcatch
メソッドが実行されるようになります。)
ユーザーが定義した型(構造体や列挙体)の場合、Rust側でserde
のトレイトを実装することで扱えるようになります。引数に使いたい型にはserde::Deserialize
、返り値に使いたい型にはserde::Serialize
を実装します。
以下は返り値用の型を定義する例です。
#[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"
}
*/
{
"type": "file",
"name": "memo.txt",
"path": "C:/Users/namnium/Desktop/memo.txt"
}
{
"type": "dir",
"name": "Desktop",
"path": "C:/Users/namnium/Desktop"
}
type Entry = {
type: 'dir' | 'file';
name: string;
path: string;
};
type Entries = Array<Entry>;
RustはEnumで、TypeScriptはユニオン型でと、それぞれの書きやすい方法で型を扱えている点が良いです。まるでRustとTypeScriptが型を通じて会話しているようです (重症)
参考
メニューとシステムトレイのUI拡張
Tauri製ソフトがただWebViewに何かを表示するだけのアプリだったらここまで熱中していなかったかもしれない...そう思わせる機能が「ウィンドウメニュー」と「システムトレイ」になります。どういう機能かはそれぞれの画像を見ていただければわかると思います。
ウィンドウメニュー・システムトレイともに、ビルドメソッドにおいて、項目と項目が押された時用のイベントハンドラを設定することで実装します。
ウィンドウメニューの例
// 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");
}
システムトレイの例
システムトレイの場合はtauri.conf.json
にも少し追記する必要があります。
{
// omit details
"tauri": {
// omit details
"systemTray": {
"iconPath": "icons/icon.png",
"iconAsTemplate": true
}
}
}
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
することでウィンドウなしの状態からアプリを始めることができます。
// 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
でイベント名と内容を全ウィンドウに通知しています。
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
でリッスンを開始し、クリーンアップで終了すると適切でしょう。
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
以下に、app
、cli
、fs
...といった具合にたくさんのモジュールが存在します。各モジュールを使うにはtauri.conf.json
のtauri.allowlist
以下で明示的に許可しなければならない場合があります。なるべくアプリが持つ権限を少なくするホワイトリスト方式であるためです。
設定ファイルとfsのファイル保存
一つ目に紹介するのはfs
モジュールによるファイルの読み込み・保存です。dialog
モジュールのopen
関数でWebアプリケーションと同様にファイル保存ダイアログを出せますが、せっかくローカルで動作するアプリなのですから、そちらではなく、直接ファイルにアクセスし読み込み・保存できるような機能を紹介します。
fs
モジュールを使うにはtauri.conf.json
のallowlist
にfs
の項目を追加します。
{
// 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
として設定ファイルを残し、変更時に保存するように、また、存在すれば起動時に読み込むようにする例です。
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
全体
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;
カスタムフック使用例
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.parse
とJSON.stringify
を使って大胆にオブジェクトをそのまま読み込み/吐きだししています。
Rustは一切関わらず、フロントエンド側ですべて完結し扱いも容易で、アプリ設定を簡単に設計でき実装が捗ります。
参考
ショートカットとイベントハンドラの機能実行
Tauri製ソフトがただWebViewに何かを表示するだけのアプリだったらここまで熱中していなかったかもしれない...その第2弾となる機能です。globalShortcut/register
でキーボードショートカットとそれに結び付くハンドラを設定できます。audio-bookmarkやwin-win-mapではこのショートカット機能をフル活用しています。
ここまでのAPI同様、ショートカット機能をオンにするためにはtauri.conf.json
に追記が必要です。
{
// omit details
"tauri": {
"allowlist": {
// omit details
"globalShortcut": {
"all": true
}
},
// omit details
}
}
次のコードはCtrl+c
を押すとカウントする例です。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
等に設定すると捗ります。
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における良い排他処理の書き方がわからず少し汚いコードになります。もし良い書き方をご存じの方おりましたらコメントいただけると幸いです。
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ならば、システムトレイ等に並びカッコいい機能であるアプリ通知も簡単に実現できます。
ただし例によって最初に小細工が必要です。
{
// omit details
"tauri": {
"allowlist": {
// omit details
"notification": {
"all": true
}
},
// omit details
}
}
sendNotification
という関数を、第一引数に通知タイトル、第二引数に内容を指定して呼び出すことで、画像に示したような通知が行えます。
OSに通知許可を得る処理が必要なため、この処理を合わせて以下のようにファイルに独立させて定義しておくと扱いやすいです。
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,
});
}
呼び出し例
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
という名前の引数を受け取れるようにしています。
{
// 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の双方から確認できます。
// 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");
}
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つ入れると上手く渡すことが可能でした。
> yarn tauri dev -- -- -- -- commandlinearg
参考
プラグインとアプリケーションの自動始動
本節ではtauri-plugin-autostartというプラグインを紹介します。本機能を導入すると、アプリケーションのスタートアップ7をアプリ側から登録できるようになります。
OS起動時にアプリを起動できるスタートアップは、筆者が作ったような常駐系ユーティリティアプリにはとても便利な機能です。ただプラグインである都合上導入までが他の機能よりも面倒ではあります。順に説明していきます。
Tauriにプラグインを導入していきます。まず、Cargo.tomlにてtauri-plugin-autostart
クレートを追加します。
# omit detail
[dependencies]
# omit details
+ tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
フロント側からも使うプラグインのため、TS側のパッケージも追加しておきます。
> yarn add https://github.com/tauri-apps/tauri-plugin-autostart
本パッケージの追加にはgitがあらかじめインストールされている必要があります。
また、後述のGitHub Actionsにて滞りなくインストールするために、package.json
ではssh
ではなくhttps
で取得するように書かれていることを確認しましょう。
下準備の最後に、Rust側でTauriビルダーにpluginを登録します。
// 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側からスタートアップの設定ができるようになります。
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
で生成されるインストーラー(後述)でインストールして確かめると効果を確認しやすいです。有効になっていれば、設定 > アプリ > スタートアップから確認できるようになっているはずです。
参考
機能分割とURL指定のマルチウィンドウ
GUIアプリケーションと言えば、シングルウィンドウだけではなく、設定ウィンドウやアラート、ツールタブなど、いくつかのウィンドウに分かれていることがしばしばです。例えばAviUtlなんかはウィンドウまみれです。
Tauriでも複数のウィンドウを生成することが可能です。
静的にウィンドウ生成
基本的な増やし方として、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
// 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
{
// omit details
"tauri": {
"allowlist": {
// omit details
"window": {
"all": true
}
},
// omit details
}
}
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.ts
のrollupOptionsにて下記のように記述することで、複数ページのバンドルが可能になります。筆者もこれ以上の詳細は現時点で理解していないです
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の例
<!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>
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
という画像をアイコンにしたい場合、以下のコマンドを打つだけで設定できます。
> 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以下にインストーラが生成されます。
> 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 でタグを指定したり、branches をrelease ではなく直接main にしたり等 |
... > matrix > platform | クロスプラットフォームに関する項目ですが、例えばWindows向けにしか作っていない場合はここを減らします。 |
伴い、ubuntu only となっている項目を消したりすることもありそうです。 |
|
actions/setup-node@v3のwith > node-version | nodeバージョンは手元のものに合わせましょう。 |
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
のみ修正し実際に走らせてみました。
修正点差分
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
> git branch
main
* release
> git add -A
> git commit -m "v0.0.1"
> git push origin release
プッシュ後にリポジトリのActionsページ (例) に飛ぶと、ワークフローが実行されていることが確認できます。
Error: Resource not accessible by integration
というエラーが出ることがあります。
再現条件が特定できなかったのですが、このエラーが出た際は
Settings > Actions > General > Workflow permissions
を、Read and write permissions
に変更してみてください。
参考: Github ActionsでResource not accessible by integrationが出た際の対処法
成功するとこんな感じです。
作成されたDraftからリリースを作成すればデプロイ完了です。
リポジトリのトップページ右にReleases
という項目があります。ここをクリックするとリリース一覧に飛べます。
すると先程作成したDraftがあります。
から編集ページに飛び、一番下緑色のPublish release
を押すことでリリースが作成されます。
リリースが生成されるとリポジトリのトップページ右に最新リリースへのリンクが出来ます。カッコいいリリースが出来ました!
参考
まとめ・所感
ここ数ヶ月Tauriを触った技術的な感想は、「言語は手段として捉え適材適所で替えた方が結局楽しい」というものでした。
少し前の自分であれば、覚えたての言語を激推しして、「それ〇〇でもできます!」と、ドキュメントが不十分だろうとライブラリが他の言語のラッパーしかなかろうと無理やりその言語を使って何かを作ることに喜びを覚えていました。
そんな折、RustとTypeScriptの両方を書いて分かったのは、「文法や強みが違っても"価値観"9が同じ言語たちがいる」ということでした。
むしろ、ある言語で学んだ考えが他の言語を学んだ時に出てきた時のほうが嬉しいのです。RustとTypeScriptは対照的な2人と言ったのですが、型理論や関数型パラダイムなど、モダン言語にあると嬉しいような共通点も多々あります。まるでぼっちちゃんも喜多ちゃんも「変わりたい」という気持ちをきっかけにギターを握ったみたいですね。
一つの言語だけにこだわっている限り決して知ることができない...色々な言語を知ることで見える景色があったのです。
そして、Tauriを使ったプログラミングは、RustとTypeScriptそれぞれの強みをより強調し、言語を超えた良いパラダイムの存在を教えてくれる...そんな景色を見せてくれる百合作品なのでした。
筆者の1番推しはTypeScript×Rust10ですが、この考え方は別な"カプ"でも流用できるんじゃないかと予想しています。
読者の方もぜひ一推しのカプを探してみましょう!ここまで読んでいただきありがとうございました。
その他 参考・引用
- Rust向けGUIツールキット「KAS」の作者が、RustのGUI対応状況を振り返る
- Rust: state of GUI, December 2022
- Rust GUI の決定版! Tauri を使ってクロスプラットフォームなデスクトップアプリを作ろう
-
TypeScriptではなくJavaScriptやAltJSとしてRust(with Yew)を指定することも可能ですが、本記事ではTypeScript with Reactで統一します。好きなので。 ↩
-
転天、記事執筆が思ったより長引いて投稿が最終回放映後になってしまいました(涙) 最終回最高でした(涙2) まだ観たことがない方は有料ですがdアニで配信されているのでそこから観ましょう。 ↩
-
DioxusというGUIライブラリも最近出たようですが、よく調べるとこれはTauriベースらしいので候補から外しました。なお、Bevyやnannou、Amethystといったゲーム用描画エンジンも外しています。 ↩
-
呪文は
2 girls, sisters, flat chest, steampunk, overcoat, programming, computer
です。唐突にこの挿絵を入れたのはトレンドがChatGPTに占領されているのでなんか対抗したくなったからです(単純)。課金した上でプロンプトエンジニアやってみて思ったのですが望みどおりの絵を描けるという能力は今後もしばらくAIに取って代わられない気がします...時にRustとTypeScriptがペアプロする話結構真面目に読みたいので今度ChatGPTにでも書かせますかね ↩ -
ないだろうと思っていたら実は既存アプリあったみたいなパターンだったとしても特に驚きはしないので、もし類似のアプリをご存じの方おりましたらコメントで教えていただけると幸いです(やっぱり気になるので)。 ↩
-
Windowsの場合スタートアップには、アプリ側から設定するタイプと、ユーザーが任意に指定するタイプがあります。リンク先は後者ですがそれは説明のためで本プラグインとは関係なく、本プラグインでは前者タイプで設定します。Windowsではアプリのスタートアップは設定 > アプリ > スタートアップから確認できます。 ↩
-
ある意味この注意もとい備忘録を書くために本機能を紹介しました。 ↩
-
本来ならパラダイムと記述するべき箇所ですが、「大切にしたいパラダイム」的な意味を持たせたかったためあえて価値観と表現しました。擬人法です。 ↩
-
TypeScriptがタチ、Rustがネコでたまにリバありかなって...まぁどっちでも良い派ですが() ↩