前回は、Tauri+SycamoreでHelloWorld(UIもほんの少しだけ変更)しました。 次は、ファイルの読み書きを行い、データを保存してみます。
前回はこちら a3colorr.hatenablog.com
環境
- Macbook Pro
- M1 pro
- macOS 13.3.1
- rustc 1.68.2
- tauri-build 1.2
やること
次のユースケースの通りです。
- テキストフィールドに単語などを入力する
- Saveボタンを押す
- HOMEディレクトリにある
word_data.yaml
に入力した単語などを保存する
今回はYAML形式でテキストファイルにデータを保存します。
YAML形式でファイルに書き込むときは、以下のようにリストで書き込むことにします。
- what - where - who
完成イメージはこんな感じ
ファイルの読み書き処理を書くfiles.rs
を作る
main.rs
と同じディレクトリにファイルを作っても良いのですが、せっかくなので、src-tauri/src
にディレクトリyaml_io
を作り、その中にfiles.rs
を作ります。
$ cd src-tauri/src
$ mkdir yaml_io
$ touch yaml_io/mod.rs
$ touch yaml_io/files.rs
// mod.rsに次の1行を追加
pub mod files
これ以降は、files.rs
に処理を書いていきます。
YAML形式を扱えるようにする
YAMLファイル自体はテキストファイルなので、ファイルの読み書きさえできれば後は自力で、Rustで扱える形(HashMap
など)にパースしたり、ファイルに書き込める形式に加工したりしても良いのですが、既存の優秀なクレートを使った方が良いことづくめです。
そこでYAMLを扱うクレートを探したところ、serde_yaml
が良さそうでしたので、今回はこれをありがたく使わせて頂きました。
github.com
serde_yaml
のGithubリポジトリにあるREADMEに書いてある通り、serde
とserde_yaml
の依存関係をsrc-tauri/Cargo.toml
に追加します。Cargo.tmol
には既にserde
の依存関係があったので、serde_yaml
だけ追加しました。
#src-tauri/Cargo.toml [dependencies] tauri = { version = "1.2", features = ["shell-open"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9" <-これ
Cargo.toml
を保存したら、すぐにcargo tauri dev
で依存関係をインストールしても良いですし、最後の最後に行っても良いです。
YAMLの扱いですが、
Rustでリストっぽいものというと、配列([])かベクタ(Vecserde_yaml
ではVec<Value>
のエイリアスとしてSequence
が定義されているので、今回はこれを利用します。Vec<Value>
のValue
もまた、serde_yaml
で定義されているYAMLで有効な値となります。中身はBool
やString
などが列挙型で定義されています。
serde_yaml
のリファレンスを参考に、以下のような文字列->YAML変換yaml_from
とYAML->文字列変換yaml_string_to
の2つの関数を作りました。
/* files.rs */ use serde_yaml; use serde_yaml::Sequence; // file_dataをYAMLに変換する fn yaml_from(file_data: &str) -> Result<Sequence, serde_yaml::Error> { let yaml: Sequence = serde_yaml::from_str(file_data)?; return Ok(yaml); } // Sequence型のdataをString型に変換する fn yaml_string_to(data: Sequence) -> Result<String, serde_yaml::Error> { let string_data = serde_yaml::to_string(&data)?; return Ok(string_data); }
ファイルを読み書きできるようにする
ファイルを読み込むread_word
とファイルに書き込むwrite_word
を追加します。
ファイルパスや変換するデータは引数で受け取るようにしました。
これで4つの関数ができましたが、個別に呼ぶとソースコードがにぎやかになるので、読み込みと書き込みで'reading'とwriting
を1つ呼べば良いようにします。外から呼ぶのはこの2つだけにしたいので、この2つだけをpub
としました。
/* files.rs */ use serde_yaml; use serde_yaml::Sequence; use std::fs; use std::io; // file_dataをYAMLに変換する fn yaml_from(file_data: &str) -> Result<Sequence, serde_yaml::Error> { let yaml: Sequence = serde_yaml::from_str(file_data)?; return Ok(yaml); } // Sequence型のdataをString型に変換する fn yaml_string_to(data: Sequence) -> Result<String, serde_yaml::Error> { let string_data = serde_yaml::to_string(&data)?; return Ok(string_data); } // file_pathを読み込んで、Result<String, serde_yaml::Error>を返す fn read_word(file_path: &str) -> Result<String, io::Error> { match fs::read_to_string(file_path) { Ok(file) => { return Ok(file); }, Err(e) => { println!("Error: {}", e); return Err(e); } }; } // file_pathを開いて、yaml_stringを書き込む fn write_word(file_path: &str, yaml_string: &str) -> Result<(), io::Error> { match fs::write(file_path, yaml_string) { Ok(_) => { return Ok(()); }, Err(e) => { println!("Error: {}", e); return Err(e); } }; } // data_pathを読み込んで、Sequence型の値を返す // ただし、data_pathが存在しない場合とYAMLのシリアライズに失敗した場合は、 // 空のValueを返す pub fn reading(data_path: &str) -> Sequence { let data = match read_word(data_path) { Ok(data) => { data }, Err(e) => { println!("Error: {}", e); return Sequence::new(); } }; match yaml_from(&data) { Ok(yaml) => { return yaml; }, Err(e) => { println!("Error: {}", e); return Sequence::new(); } }; } // Sequence型のdataをString型のyaml_stringに変換して、file_pathに書き込む pub fn writing(data: Sequence, file_path: &str) -> bool { let yaml_string: String = match yaml_string_to(data) { Ok(yaml_string) => { yaml_string }, Err(e) => { println!("Error: {}", e); return false; } }; match write_word(file_path, &yaml_string) { Ok(_) => { return true; }, Err(e) => { println!("Error: {}", e); return false; } }; }
tauri-src/src/main.rs
からYAMLの読み書き関数を呼ぶ
main.rs
にreading
関数とwriting
関数を定義して、各々の中で先ほどのreturn yaml_io::files::reading
やreturn yaml_io::files::writing
を呼びます。
また、データを保存するYAMLword_data.yaml
はホームディレクトリに保存することにします。
// Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use serde_yaml::{Value, Sequence}; use std::env; #[tauri::command] fn inputed(word: &str) -> String { // reading()でSequence型の値を取得してdataに代入する // その後、nameをdataに追加する let mut data = reading(); data.push(Value::String(word.to_string())); // dataを書き込む writing(data); // UIに表示する文字列を返す return format!("I saved the {} in YAML!", word); } mod yaml_io; fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![inputed]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } // word_data.yamlを読み込む fn reading() -> Sequence { return yaml_io::files::reading(&file_create_path()); } // word_data.yamlに書き込む fn writing(data: Sequence) { yaml_io::files::writing(data, &file_create_path()); } // word_data.yamlのパスを作成する // ここでは、ホームディレクトリにword_data.yamlを作成する fn file_create_path() -> String { let home_path = env::var("HOME").unwrap(); return format!("{}/word_data.yaml", home_path); }
UIを作る
最後に、UIを作ります。
単語等を入力できれば十分なので、最初に作られたテンプレートUIから余分なものを削除して、変数名を変更しました。
/* src/app.rsを一部抜粋 */ #[derive(Serialize, Deserialize)] struct InputedArgs<'a> { word: &'a str, } #[component] pub fn App<G: Html>(cx: Scope) -> View<G> { let word = create_signal(cx, String::new()); let inputed_word = create_signal(cx, String::new()); let inputed = move |e: Event| { e.prevent_default(); spawn_local_scoped(cx, async move { let new_msg = invoke("inputed", to_value(&InputedArgs { word: &word.get() }).unwrap()).await; log(&new_msg.as_string().unwrap()); inputed_word.set(new_msg.as_string().unwrap()); }) }; view! { cx, main(class="container") { form(class="row",on:submit=inputed) { input(id="word-input", bind:value=word, placeholder="Enter a word...") button(type="submit") { "Save" } } p { b { (inputed_word.get()) } } } } }
あとは、cargo tauri dev
でアプリを起動して動作を確認します。