「Ace」を使って「TAURI」で「テキストエディタ」アプリを作ろう

2024年2月6日(火)
大西 武 (オオニシ タケシ)
第9回の今回は、JavaScriptライブラリ「Ace」を使って、「TAURI」でテキストエディタのデスクトップアプリを開発していきます。

ファイルを開いてテキストを読み込む

これまでの連載のように、TAURIコマンド関数を実装します。ここではファイルからテキストを読み込むだけのTAURIコマンドを実装します。図5のように、まずバックエンドのRust側で「File」→「Open」メニューが実行されたら、フロントエンドのJavaScript側でファイルダイアログを開いてファイル名をまたバックエンドのRust側に渡します。TAURIコマンドの「open_text」関数でテキストファイルが読み込まれたら、またフロントエンドのJavaScript側でAceエディタにテキストを表示します。

図5:「Rustのメニュー」→「JavaScript」→「Rust」→「JavaScript」の図

フロントエンドでファイルを開くダイアログ

「ファイルを開くダイアログ」に関しては、第6回でJavaScriptで実装した通りです。第5回のファイルを開くダイアログはRustクレートなので、今回とは違います。

TAURIイベントの「listen」メソッドで"open-file"メッセージの受け取り状態をセットします。"open-file"メッセージを受け取ったらダイアログから「.txt」「.html」「.js」の拡張子のファイルを開き、バックエンドのRust側のTAURIコマンド「open_text」関数にファイル名を送ります。開いたファイルをさらに「invoke」関数の"open_text"で受け取り、Aceエディタの「setValue」メソッドにテキストデータを渡します。

ここでは1つずつ拡張子を選ぶようになっていますが、複数の拡張子のファイルを同時にファイルダイアログに表示できるようにすると便利でしょう。例えば「extensions: ['txt','html','js']」のように拡張子を複数指定します。

・「src」→「main.js」ファイルのサンプルコード
01const { invoke } = window.__TAURI__.tauri;
02//ダイアログを扱えるように
03const { open,save } = window.__TAURI__.dialog;
04 
05let editor;
06     
07window.addEventListener("DOMContentLoaded", () => {
08  editor = ace.edit('text_edit');
09  editor.focus();
10  editor.setFontSize(16);
11  setFontSize();
12  setNew();
13  //Openメニューが呼ばれる設定
14  setOpen();
15  setMode();
16  setTheme();
17  editor.session.setMode('ace/mode/html');
18});
19//Openメニューが呼ばれるセットをする関数
20async function setOpen() {
21  await window.__TAURI__.event.listen('open-file', () => {
22    open({
23      filters: [
24        { name: 'Text', extensions: ['txt'] },
25        { name: 'HTML', extensions: ['html'] },
26        { name: 'JavaScript', extensions: ['js'] },
27      ],
28    }).then(async file => {
29      let text = await invoke("open_text", { path: file });
30      let ext = file.split("/").reverse()[0].split('.')[1];
31      editor.session.getDocument().setValue(text);
32      switch (ext.toLowerCase()) {
33        case "html":
34          editor.session.setMode('ace/mode/html');
35          break;
36        case "js":
37          editor.session.setMode('ace/mode/javascript');
38          break;
39        default:
40          editor.session.setMode('ace/mode/text');
41      }
42    });
43  });
44}
45(後略)

【サンプルコードの解説】
TAURIのダイアログの「open」関数と「save」関数を読み込みます。
DOMが全て読み込み完了したら「setOpen」関数を呼び出します。
setOpen関数でファイルを開く「Open」メニューが実行されたときの処理をします。「listen」メソッドで「Open」メニューの実行を受け取り、ファイルを開くダイアログを開いてファイル名が選択されたら、バックエンドからテキストデータを受け取って表示します。
ファイルの拡張子次第で「HTML」モードか「JavaScript」モードか、それ以外の拡張子の場合は「Text」モードにセットします。

バックエンドでテキストファイルを読み込む

「Open」カスタムメニューを「File」サブメニューに追加します。既にFileメニューはメニューバーに追加されています。

「Open」メニューが実行されたら「open-file」メッセージをフロントエンドのJavaScriptに送り、また返ってきたファイル名を指定してテキストファイルをTAURIコマンド「open_text」関数で開き、またフロントエンドへ読み込んだテキストデータを送ります。

・「src-tauri」→「src」→「main.rs」ファイルのサンプルコード
01#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
02 
03use tauri::{CustomMenuItem, Menu, MenuItem, Submenu,Manager};
04//ファイルの読み書きを扱うクレート
05use std::fs;
06//ファイルを開くTAURIコマンド関数
07#[tauri::command]
08fn open_text(path: &str) -> String {
09  match fs::read_to_string(path) {
10    Ok(text) => text,
11    Err(_) => String::from("UTF-8エンコードのファイルを開いてください。")
12  }
13}
14 
15fn main() {
16  let new_file = CustomMenuItem::new(
17    "new".to_string(), "New");
18  //Openメニュー
19  let open_file = CustomMenuItem::new(
20    "open".to_string(), "Open");
21(中略)
22  let menu_file = Submenu::new(
23    "File",
24    Menu::new()
25      .add_item(new_file)
26      //Openメニューの追加
27      .add_item(open_file)
28      .add_native_item(MenuItem::Separator)
29      .add_item(quit));
30(中略)
31  //main.jsからopen_text関数が呼ばれた時のセット
32  tauri::Builder::default()
33    .invoke_handler(tauri::generate_handler![
34      open_text])
35  .menu(menu)
36    .on_menu_event(|event| {
37      match event.menu_item_id() {
38        "new" => {
39          event
40            .window()
41            .emit_all("new-file", "")
42            .unwrap();
43        }
44        //Openメニューを実行する時
45        "open" => {
46          event
47            .window()
48            .emit_all("open-file", "")
49            .unwrap();
50        }
51(中略)
52        _ => {}
53      }
54    })
55    .run(tauri::generate_context!())
56    .expect("error while running tauri application");
57}

【サンプルコードの解説】
「use」文でファイルを扱う「std::fs」クレートを使えるようにします。
TAURIコマンド「open_text」関数で「path」引数の「UTF-8」形式のテキストファイルを読み込んで文字列データを返します。
「File」→「Open」メニューを追加します。
TAURIビルダーの「invoke_handler」メソッドでTAURIコマンド「open_text」関数の呼び出しを登録します。
TAURIビルダーで「Open」メニューが実行されたら"open-file"メッセージをフロントエンドのJavaScriptへ送ります。

テキストをファイル名を指定して保存する

ファイルを開くのとは逆に、ファイルに名前を付けて保存するTAURIコマンド関数を実装します。テキストデータを保存するだけなので、保存した後にテキストデータを返して表示するなどの処理はファイルを開いたときより1手順少なくて済みます。

今回のTAURIコマンド関数の中身はテキストファイルを読み書きするだけなので、とてもシンプルに実装できます(図6)。もちろんファイルを読み書きする関数がRustに備わっているからですが、ピッタリの関数や構造体やメソッドがあると便利なので、クレートはとても重要です。

図6:フォントサイズ12pxでHTMLモードでterminalテーマのAceエディタの図

フロントエンドで名前を付けて保存するダイアログ

「名前を付けて保存ダイアログ」に関しては、第6回でJavaScriptで実装した通りです。第5回の名前を付けて保存ダイアログはRustクレートなので今回とは違います。

あまりお勧めできませんが、ファイルに名前を付けて保存ダイアログも複数の拡張子を同時に指定できます。

・「src」→「main.js」ファイルのサンプルコード
01const { invoke } = window.__TAURI__.tauri;
02const { open,save } = window.__TAURI__.dialog;
03 
04let editor;
05     
06window.addEventListener("DOMContentLoaded", () => {
07  editor = ace.edit('text_edit');
08  editor.focus();
09  editor.setFontSize(16);
10  setFontSize();
11  setNew();
12  setOpen();
13  //Saveメニューが呼ばれる設定
14  setSave();
15  setMode();
16  setTheme();
17  editor.session.setMode('ace/mode/html');
18});
19//Saveメニューが呼ばれるセットをする関数
20async function setSave() {
21  await window.__TAURI__.event.listen('save-file', () => {
22    save({
23      filters: [
24        { name: 'Text', extensions: ['txt'] },
25        { name: 'HTML', extensions: ['html'] },
26        { name: 'JavaScript', extensions: ['js'] },
27      ],
28    }).then(async file => {
29      let txt = editor.session.getDocument().getValue();
30      await invoke("save_text", {
31        path:file,
32        text:txt
33      });
34    });
35  });
36}
37(後略)

【サンプルコードの解説】
DOMが全て読み込み完了したら「setSave」関数を呼び出します。
「setSave」関数の「listen」メソッドでバックエンドのRust側から送られるSaveメニューの受け取りを登録します。「invoke」関数では第1引数でTAURIコマンド「save_text」関数を呼び出し、第2引数に「連想配列」でTAURIコマンド関数の引数名を任意の数だけまとめて渡せます。

バックエンドでテキストファイルを保存する

「Save」カスタムメニューを「File」サブメニューに追加します。Fileメニューは既にメニューバーに追加されています。

TAURIコマンド「save_text」関数内の「write」関数も、「read_to_string」関数のときのように「Result」型で場合分けした方が良いでしょう。「Result」型と「Option」型の使い方の解説が不十分だったので、次回で解説します。

・「src-tauri」→「src」→「main.rs」ファイルのサンプルコード
01(前略)
02//ファイルを保存するTAURIコマンド関数
03#[tauri::command]
04fn save_text(path: &str,text: &str) {
05  fs::write(path, text).unwrap();
06}
07 
08fn main() {
09  let new_file = CustomMenuItem::new(
10    "new".to_string(), "New");
11  let open_file = CustomMenuItem::new(
12    "open".to_string(), "Open");
13  //Saveメニュー
14  let save_file = CustomMenuItem::new(
15    "save".to_string(), "Save");
16(中略)
17  let menu_file = Submenu::new(
18    "File",
19    Menu::new()
20      .add_item(new_file)
21      .add_item(open_file)
22      //Saveメニューの追加
23      .add_item(save_file)
24      .add_native_item(MenuItem::Separator)
25      .add_item(quit));
26(中略)
27  //main.jsからsave_text関数が呼ばれた時のセット
28  tauri::Builder::default()
29    .invoke_handler(tauri::generate_handler![
30      open_text,
31      save_text])
32  .menu(menu)
33    .on_menu_event(|event| {
34      match event.menu_item_id() {
35        "new" => {
36          event
37            .window()
38            .emit_all("new-file", "")
39            .unwrap();
40        }
41        "open" => {
42          event
43            .window()
44            .emit_all("open-file", "")
45            .unwrap();
46        }
47        //Saveメニューを実行する時
48        "save" => {
49          event
50            .window()
51            .emit_all("save-file", "")
52            .unwrap();
53        }
54(中略)
55        _ => {}
56      }
57    })
58    .run(tauri::generate_context!())
59    .expect("error while running tauri application");
60}

【サンプルコードの解説】
TAURIコマンド「save_text」関数でテキストファイルを「path」引数名で、「text」引数データを「UTF-8」形式で保存します。
「File」→「Save」メニューを作成し、メニューバーに追加します。
TAURIビルダーの「invoke_handler」メソッドでTAURIコマンド「save_text」関数の呼び出しを登録します。
TAURIビルダーで「Save」メニューが実行されたら"save-file"メッセージをフロントエンドのJavaScriptへ送ります。

おわりに

今回は、新たにメニューを表示したり実行したりするメニューバーや、JavaScriptでRust側からメッセージを受け取るlistenの機能が出てきました。これでRustとJavaScriptとのやり取りがほとんど実装できるようになりました。

Aceライブラリには他にも様々な設定があるので、いろいろ試してみると良いでしょう。工夫次第では「Visual Studio Code」のTAURI版まで作れてしまうかもしれません。

著者
大西 武 (オオニシ タケシ)
1975年香川県生まれ。大阪大学経済学部経営学科中退。プログラミング入門書など30冊以上を商業出版する作家。Microsoftで大賞やNTTドコモでグランプリなど20回以上全国区のコンテストに入賞するアーティスト。オリジナルの間違い探し「3Dクイズ」が全国放送のTVで約10回出題。
https://profile.vixar.jp

連載バックナンバー

開発言語技術解説
第15回

「TAURI」でデータベースを使ってみよう

2024/5/10
第15回の今回は「TAURI」でオープンソースのデータベース「SQLite3」を使用して、テーブル表に表示する解説をしていきます。
開発言語技術解説
第14回

「TAURI」で気象庁の「CSVデータ」を解析する

2024/5/1
第14回の今回は気象庁のWebサイトから指定した地域の1年間の気象データをダウンロードして「TAURI」で解析していきます。
開発言語技術解説
第13回

「TAURI」で「簡易RSSリーダー」を開発してみよう

2024/4/16
第13回の今回は「TAURI」で「RSSフィード」を読み込んでWebページに一覧表示し、リンクのページを開くための新規ウィンドウを作成するところまでを解説します。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています