7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【隙間開発】Tauri × Svelteでエイリアスエディタを作ってみた!

Last updated at Posted at 2025-10-31

はじめに

こんにちは、genimuraです。
今回はTauri 2 × Svelte 5でデスクトップアプリを作ってみました。

ターミナル作業中にエイリアスを素早く追加・整理したい一方、.zprofileを毎回エディタで開いて手作業するのは面倒で、記述ミスで壊れやすいという問題がありました。そこで「安全に、直感的に」追加・編集できるデスクトップアプリとして、「Alias Editor」を実装してみました。

この記事では、初心者でも「同じものを作れる」ように、段階的に学べる内容と手順をまとめています。躓きやすい点はチェックリスト化し、コードは最小限のスニペットで理解を助けます。

この記事で学べること

  • Tauri×Rustの基本(IPCの流れ、非同期I/O、Resultでのエラー設計)
  • Svelte 5の新リアクティブAPI($state/$effect/$props)とストア設計の基礎
  • .zprofileを壊さないための「パース→統合→生成」戦略と型同期

こんな感じのアプリを作りました。

alias_editor.png

まずは全体像を掴んでおくといいと思います

  • フロント(Svelte):UI、フォーム検証、検索・フィルタ、表示
  • バック(Tauri/Rust):.zprofileの読み書き、バックアップ、パース/生成、構文検証
  • 通信(IPC):Svelteからinvoke('command')でRustの#[command]関数を呼ぶ

作っていく流れはこんな感じです

  1. 開発環境を整える(Node/Rust/Tauri CLI)
  2. 最初のIPCとしてread_zprofileを作り、画面に中身を出す
  3. 正規表現で「alias行」を理解・抽出する(コメントアウトも含む)
  4. エイリアス以外の行を壊さないように分離して保持する
  5. Svelteのフォームで追加/編集/検証を実装する
  6. 保存時は「生成→バックアップ→書き込み」の順で安全に処理する
  7. 最小権限・CSPなど、必要なセキュリティ設定を確認する

始める前に確認しておくこと

  • Node.js 18+ と Rust(stable)がインストールされている
  • Tauriの前提(Xcode Command Line Tools 等)が満たされている(macOS)
  • .zprofileの場所とバックアップ戦略を理解している

技術スタックとアーキテクチャ

今回使った技術スタックは以下の通りです。

  • フロントエンド: Svelte 5 + TypeScript + Tailwind + Vite
  • バックエンド: Tauri 2 + Rust(Tokio/Serde)

責務分離はそこまで複雑ではないですが、以下のように分けています。

  • Rust側: .zprofile 読み書き、バックアップ、パース/生成、構文検証
  • Svelte側: 表示・編集UI、検索/フィルタ、検証と保存フロー

Tauri×Rustで学んだこと

IPCコマンド定義と呼び出し

Tauriでは、Rust側でコマンドを定義して、フロントエンドから呼び出すことができます。

#[command]
pub async fn read_zprofile() -> Result<String, String> { /* ... */ }

フロントエンドからはこんな感じで呼び出します。

import { invoke } from '@tauri-apps/api/core';
const content = await invoke<string>('read_zprofile');

非同期ファイルI/OとResultエラー設計

Tokioでブロッキングを回避し、Result<T, String>で明確な失敗経路を作るようにしました。
例外ではなく戻り値でハンドリングし、UIへ人間が読めるメッセージを返すようにしています。

バックアップと最小権限、コンテキスト保持

書き込み前に自動バックアップするようにしました。
また、.zprofileの「エイリアス行」と「非エイリアス行」を分離して保持しています。保存時に非エイリアス行を先頭に復元し、エイリアスはカテゴリ別に追記するようにしています。

// エイリアス行の抽出(コメントアウト含む)と非エイリアス行の保持
let re = Regex::new(r#"^alias\s+([a-zA-Z0-9_-]+)=(?:'([^']*)'|\"([^\"]*)\")\s*(?:#\s*(.*))?$"#).unwrap();

Svelte 5で学んだこと

新リアクティブAPIでのフォーム検証と編集フロー

Svelte 5の新しいリアクティブAPIを使って、フォームの検証を実装しました。

<script lang="ts">
  let name = $state(''); let command = $state('');
  let nameError = $state(''); let commandError = $state('');
  $effect(() => { nameError = !name.trim() ? 'エイリアス名は必須' : ''; });
  $effect(() => { commandError = !command.trim() ? 'コマンドは必須' : ''; });
</script>

ストア設計(検索・フィルタ・統計)

複数コンポーネント間でエイリアス一覧を共有し、検索/カテゴリ/有効状態で絞り込むために、ストアを使いました。

export const aliases = writable<Alias[]>([]);
export const searchFilters = writable({ query: '', category: '', enabled: null as boolean | null });
export const filteredAliases = derived([aliases, searchFilters], ([$a, $f]) =>
  $a.filter(x => /* クエリ/カテゴリ/有効で絞り込み */)
);

UI/UXと責務分離

編集フォームは保存ロジックと検証に集中させ、リスト/検索は親に委譲するようにしました。
Tailwindで軽量にモダンな見た目とダークモード対応も実装しています。

事前知識の超要約:エイリアス/.zprofileとTauri/Svelteの基礎

エイリアスと.zprofile

エイリアスとは、alias ll='ls -la' のようにコマンドに別名を付ける仕組みです。
macOSのzshでは通常ホーム直下の.zprofileに保存され、ログインシェルで読み込まれます。
壊しやすいポイントとしては、クォートの閉じ忘れ、等号やスペースの混入、エンコーディングの不一致などがあります。

Tauri 2(Rustバックエンド)

フロントエンドからinvoke('command')でRustの#[command] fnを呼ぶIPC構造になっています。
ファイルI/OやOS連携はRust側で行い、UIはWeb技術で書けるのが特徴です。
セキュリティは「コマンドのホワイトリスト化」「CSP」「ファイルアクセスの最小化」が基本です。

Svelte 5(フロントエンド)

新リアクティブAPIとして、$stateで状態、$effectで副作用、$propsで親からの受け渡しができます。
ストア(writable/derived)で複数コンポーネント間の状態を共有・計算できます。
Tailwindで見た目を素早く構築できるのも便利です。

覚えておくと楽になること

  • Rustは例外ではなくResultで失敗を返す文化なので、UI側で人間が理解できるメッセージを表示するようにしています
  • 文字列はエンコーディングと改行コードの違いで壊れやすいので、保存前にバックアップが必須です

環境構築

インストール

まずは必要なツールをインストールします。

  1. Rust(rustup):curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  2. Node.js 18+(nvm推奨):nvm install 18 && nvm use 18
  3. Tauri CLI:npm i -D @tauri-apps/cli

プロジェクト作成

プロジェクトを作成してみましょう。

npx @tauri-apps/cli create-app --template svelte-ts alias-editor
cd alias-editor
npm install

動作確認

開発サーバーを起動して、動作確認をしてみます。

npm run tauri:dev

チェックリスト

以下の点を確認しておくと良いと思います。

  • Rust toolchain がエラーなく動く(rustc -V
  • Tauriの前提(macOSのCommand Line Tools等)が満たされている
  • npm run tauri:devでウィンドウが起動する

トラブル時のヒント

署名・権限関連は「リリースビルド時」に現れやすいので、開発中はまずdev起動を安定化させることをおすすめします。
依存の再インストールが必要な場合は、rm -rf node_modules && npm ci、Rustはcargo cleanも試してみてください。

最初のIPC:read_zprofile をつくる(Rust⇔TSの往復)

まずは、フロント(Svelte)からRustコマンドを呼び、.zprofileの内容を取得して画面に表示してみましょう。

手順

1. Rustでコマンドを定義

// src-tauri/src/commands/mod.rs
#[command]
pub async fn read_zprofile() -> Result<String, String> {
    let path = get_zprofile_path().map_err(|e| e.to_string())?;
    if !path.exists() { return Ok(String::new()); }
    tokio::fs::read_to_string(&path)
        .await
        .map_err(|e| format!("読み込み失敗: {}", e))
}

2. lib.rsでコマンドを登録

tauri::Builder::default()
  .invoke_handler(tauri::generate_handler![read_zprofile])
  .run(tauri::generate_context!())
  .expect("error while running tauri application");

3. フロントから呼び出して表示

import { invoke } from '@tauri-apps/api/core';
import { writable } from 'svelte/store';
export const rawZprofile = writable('');

export async function loadZprofile() {
  const content = await invoke<string>('read_zprofile');
  rawZprofile.set(content);
}

4. 画面で表示

<script lang="ts">
  import { rawZprofile, loadZprofile } from './stores';
  $: $rawZprofile;
  loadZprofile();
</script>

<pre class="whitespace-pre-wrap">{$rawZprofile}</pre>

理解ポイント

  • #[command]で公開、invoke('name')で呼ぶ。名前は関数名と一致させます
  • 戻り値はResult<T, String>にして、UIに人間が読めるエラーを届けるようにしています

よくある失敗

  • IPCコマンド未登録:Unrecognized IPC commandgenerate_handler![...]に追加するのを忘れがちです
  • 型不一致:invoke<string>(...)の型引数とRust側のResult<String, String>が対応しているか確認しましょう

チェックリスト

  • dev起動時にIPCエラーが出ていない
  • 画面に.zprofileの内容が表示されている

パース入門:正規表現でエイリアスを理解する(境界ケースと簡易テスト)

alias name='command' # desc 形式を抽出し、名前・コマンド・説明・有効/無効を識別できるようにします。

要点コード(Rust)

let re = Regex::new(r#"^alias\s+([a-zA-Z0-9_-]+)=(?:'([^']*)'|\"([^\"]*)\")\s*(?:#\s*(.*))?$"#)
  .map_err(|e| format!("正規表現のコンパイルに失敗: {}", e))?;

if let Some(c) = re.captures(&alias_line) {
  let name = c.get(1).unwrap().as_str();
  let command = c.get(2).or_else(|| c.get(3)).unwrap().as_str();
  let description = c.get(4).map(|m| m.as_str().trim().to_string());
}

境界ケース

以下のようなケースに対応する必要があります。

  • コメントアウト行:# alias foo='bar' → 無効扱いだがパースはします
  • ダブル/シングルクォート両対応:"cmd"'cmd'
  • 説明の有無:#以降をtrim()して空ならNone
  • 日本語説明:UTF-8前提、エディタと保存形式を統一します

簡易テスト(擬似)

以下のようなパターンでテストしてみると良いと思います。

OK: alias ll='ls -la'
OK: alias gs="git status" # gitの状態
OK: # alias old='legacy' # 旧設定
NG: alias spaced name='x'   # スペース入りの名前
NG: alias eq=ual='x'        # =を含む名前

チェックリスト

以下の点を確認しておくと良いと思います。

  • 代表パターン(上記OK例)がすべて通る
  • NG例はスキップまたは検知される

非エイリアス行の保持:壊さないための設計(再構成の流れ)

.zprofileにはエイリアス以外の設定(Amazon Q、Homebrew、環境変数、PATH設定など)が多数あります。これらを破壊せず、エイリアスだけを編集・再生成したいと思います。

アプローチ

以下のような流れで実装しました。

  1. 読み込み時に、行ごとに「エイリアス」「非エイリアス」に分離
  2. UIで編集するのはエイリアス側のみ
  3. 保存時は「非エイリアス行(原文)→ エイリアス行(生成)」の順で統合

要点コード(Rust)

for line in content.lines() {
  let trimmed = line.trim();
  if trimmed.starts_with("alias ") || trimmed.starts_with("# alias ") {
    // パースしてVec<Alias>に格納(enabledはコメントで判定)
  } else if !trimmed.is_empty() {
    // ツール由来の見出し行などは除外しつつ、非エイリアス行として保持
    non_alias_lines.push(line.to_string());
  }
}

生成時の並び

以下のような順序で生成するようにしています。

  • 先に非エイリアス行をそのまま出力
  • 続けて「# Aliases managed by Alias Editor」などの見出し
  • カテゴリ別(common/dev/files/network/custom)にエイリアスを整形して出力

利点

このアプローチの利点は以下の通りです。

  • 既存ファイルの文脈・コメント・順序を最大限尊重できる
  • ツールが触る範囲を明確化(事故を減らす)

チェックリスト

以下の点を確認しておくと良いと思います。

  • 保存後も、非エイリアスの設定が元のとおり残っている
  • エイリアスセクションはツール管理の見出し以下にまとまっている

UIを組む:Svelte 5の$state/$effectによるフォームと検証

エイリアスの新規追加/編集フォームを作り、入力中にリアルタイム検証を行うようにしました。

要点コード

<script lang="ts">
  let name = $state('');
  let command = $state('');
  let description = $state('');
  let enabled = $state(true);
  let nameError = $state('');
  let commandError = $state('');

  function validateName(v: string) {
    if (!v.trim()) return 'エイリアス名は必須です';
    if (v.includes(' ') || v.includes('=')) return 'スペース/="は使用不可';
    if (v.length > 20) return '20文字以内にしてください';
    return '';
  }
  function validateCommand(v: string) {
    if (!v.trim()) return 'コマンドは必須です';
    return '';
  }

  $effect(() => { nameError = validateName(name); });
  $effect(() => { commandError = validateCommand(command); });
</script>

理解ポイント

  • $stateは「その値を使う場所が自動で再描画」されるようになっています
  • $effectで依存値が変わるたびにバリデーションを走らせるようにしています

チェックリスト

以下の点を確認しておくと良いと思います。

  • 無効な入力時は保存ボタンが無効化されるか
  • エラーメッセージがリアルタイムで切り替わるか

ストア設計:検索・フィルタ・統計を段階実装(derivedの感覚)

複数コンポーネント間でエイリアス一覧を共有し、検索/カテゴリ/有効状態で絞り込むようにしました。

要点コード

export const aliases = writable<Alias[]>([]);
export const searchFilters = writable<{ query: string; category: string; enabled: boolean | null }>({
  query: '', category: '', enabled: null
});
export const filteredAliases = derived([aliases, searchFilters], ([$a, $f]) =>
  $a.filter(a => {
    if ($f.query) {
      const q = $f.query.toLowerCase();
      if (!a.name.toLowerCase().includes(q)
        && !a.command.toLowerCase().includes(q)
        && !(a.description ?? '').toLowerCase().includes(q)) return false;
    }
    if ($f.category && a.category !== $f.category) return false;
    if ($f.enabled !== null && a.enabled !== $f.enabled) return false;
    return true;
  })
);

チェックリスト

以下の点を確認しておくと良いと思います。

  • クエリ/カテゴリ/有効状態を変えると即座にリストが変わる
  • 統計(総数/有効/無効)が正しく更新される

保存フロー:生成→バックアップ→書き込み(ロールバック観点)

保存時の事故(上書き破壊)を避け、復旧可能な形にするようにしました。

フロー

以下のような流れで実装しています。

  1. UIから現在のエイリアス一覧を取得
  2. Rustへ渡して.zprofile文字列を生成(カテゴリ別、コメント対応)
  3. 既存.zprofileがあればバックアップを作成
  4. 新しい内容で書き込み

要点コード(Rust)

#[command]
pub async fn write_zprofile(content: String) -> Result<(), String> {
  let path = get_zprofile_path().map_err(|e| e.to_string())?;
  if path.exists() { create_backup().await.map_err(|e| e.to_string())?; }
  tokio::fs::write(&path, content).await
    .map_err(|e| format!("書き込み失敗: {}", e))
}

UI側の流れ(擬似)

const content = await invoke<string>('generate_zprofile_content_with_context', {
  aliases: $aliases,
  nonAliasLines: $nonAliasLines
});
await invoke('write_zprofile', { content });

ロールバック観点

以下の点に注意して実装しました。

  • バックアップの命名はタイムスタンプを含める(上書き防止)
  • 失敗時はパスを表示して手動復旧できるようにする

チェックリスト

以下の点を確認しておくと良いと思います。

  • バックアップが期待どおりのディレクトリに作成される
  • 書き込み失敗時に、UIでユーザーが復旧方法を把握できる

セキュリティと配布:最小権限/CSP/ビルドと署名

最小権限

Rust側コマンドは必要最小に限定し、generate_handler![...]に明示登録するようにしました。
.zprofile以外のパスには触れないようにしています(将来の拡張時も意識します)。

CSP

開発時は緩めでも、配布時はscript-src等を見直すようにしています。
外部通信が不要なら拒否し、必要ならドメインを限定するようにしています。

ビルドと署名(macOS)

  • dev: npm run tauri:dev
  • release: npm run tauri:build
  • 配布には署名やGatekeeperの考慮が必要(組織の手順に従う)

チェックリスト

以下の点を確認しておくと良いと思います。

  • 未登録コマンドは呼べないことを確認
  • 配布バイナリでCSP/署名のエラーが出ない

つまずきノート:実際のエラーと解決の道筋

実際にハマったポイントと、その解決方法をまとめました。

よくある症状と対処

  • IPCコマンド未登録 → generate_handler!を確認するのを忘れがちです
  • 権限/パスで失敗 → get_home_dir()やパス解決をログに出し検証します
  • 文字化け/改行混在 → 編集エディタの設定統一、\nに正規化します
  • 正規表現がヒットしない → クォートやコメントの扱い、前後の空白を再確認します

診断のコツ

まず「どちら側で失敗したか」(UI or Rust)を切り分けることが重要です。
RustはResultで詳細を返す習慣にし、UIで人間が読める形に変換するようにしています。

用語ミニ辞典(初心者向け)

  • IPC(Inter-Process Communication):プロセス間通信。ここではSvelte(Web)→Rust(Tauri)間の呼び出し
  • #[command]:Rust関数をTauriから呼べるように公開する属性
  • $state/$effect:Svelte 5の新リアクティブAPI。状態と副作用
  • derived store:他のストアから計算される値を提供するストア

まとめ:再現手順と次の拡張

再現手順まとめ

以下のような流れで実装しました。

  1. 環境構築 → dev起動
  2. read_zprofileで内容の取得(IPC往復)
  3. 正規表現でエイリアスを抽出、非エイリアス行を保持
  4. Svelteフォームで編集と検証
  5. 生成→バックアップ→書き込みの保存フロー
  6. 最小権限/CSPで安全性を確認

次の拡張

今後追加したい機能としては、以下のようなものがあります。

  • よく使うテンプレート、インポート/エクスポート、履歴、構文提案

型の同期とデータ整合性

Rust構造体とTSインターフェースを対応させ、IPC越しのデータ整合性を確保するようにしました。
命名規則差(snake_case/camelCase)とOptionalの取り扱いを明示しています。

#[derive(Serialize, Deserialize)]
pub struct Alias { pub id: String, pub name: String, pub command: String, pub description: Option<String>, /* ... */ }
export interface Alias { id: string; name: string; command: string; description?: string; /* ... */ }

つまずきと回避策(Tips)

正規表現の罠

シングル/ダブルクォート両対応が必要です。コメントアウト行(# alias ...)の扱いに注意が必要です。

コメント/空行処理

非エイリアス行は極力そのまま保持し、ツール由来コメントは再生成で制御するようにしています。

権限系エラー

Tauri側の許可設定とホームディレクトリ解決を先に検証するようにしています。

感想

初めてTauriとSvelteを使ってデスクトップアプリを作ってみました。
TauriもRustもほとんど初めて使ったのですが、AIといっしょに対話をしながら作ることができたので、意外とすんなりと作ることができました。
SvelteはWeb技術でフロントエンドを書けるので、開発速度が速く、学習コストが低いと感じました。Vue Likeに近いので、Vueの経験者であればすぐに使い始められると思います。

7
1
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?