【matomete-copy】
コピーを繰り返してテキストを集める
Windowsアプリを作った
(Tauri)

コピーを繰り返してテキストを集めるだけのアプリを作りました。
名前は matomete-copy で、Tauri v2 + Vanilla TypeScript で動くWindows向けの単機能ツールです1

関連記事

1. なぜ作ったか

チャット履歴を別のドキュメントにまとめようとすると、全文選択できる範囲で区切られてしまい、何度もコピーペーストを繰り返すことになります。
クリップボード履歴アプリを使っても、拾い上げて並べ直す手間は変わりません。

必要なのは「コピーするたびに自動で集まり、チェックした項目だけをまとめて貼り付けられる」軽い作業台でした。
クリップボード管理ツールとしての汎用性より、「今の作業用の一時的な集積場」として割り切った設計にしています。

1. なぜ作ったか

1.1. Tauriを選んだ理由

Electron は機能が多い代わりにバイナリが重くなりがちです2
このアプリはクリップボードを監視してテキストを並べるだけで、ブラウザエンジンを丸ごと抱える必要はありませんでした。
Tauri はOSのネイティブWebViewを使うため、配布物が軽く、Rustで書いたバックエンドロジックを直接呼び出せます。

フロントエンドはReactやVueではなくVanilla TypeScriptにしました。
単一ページで状態管理も単純な構成なので、フレームワークを持ち込む理由がありませんでした。

2. クリップボード監視の実装

クリップボード監視はポーリングで実装しています。
700ms間隔でテキストを読み取り、前回と異なる内容なら項目に追加します。

最初は navigator.clipboard をそのまま使っていましたが、HTML形式(リッチテキスト)の書き込みがTauriのWebView上で安定しませんでした3
そこでRust側に read_clipboard_content コマンドと write_clipboard_html コマンドを追加し、クリップボードアクセスをネイティブコードに移しました。
使用しているのは arboard というRustクレートで、テキストとHTML形式を同時に読み書きできます4

#[tauri::command]
fn read_clipboard_content() -> Result<ClipboardContent, String> {
    let mut clipboard = Clipboard::new()...;
    let text = match clipboard.get_text() {
        Ok(text) => text,
        Err(ClipboardError::ContentNotAvailable) => String::new(),
        Err(error) => return Err(...),
    };
    let html = match clipboard.get().html() {
        Ok(html) if html.trim().is_empty() => None,
        Ok(html) => Some(html),
        Err(_) => None,  // HTML取得失敗はエラーではなく「なし」として扱う
    };
    Ok(ClipboardContent { text, html })
}Code language: Rust (rust)

HTML取得の失敗を None として扱うのがポイントで、最初はHTML読み取り失敗でコマンド全体をエラーにしていたため、テキストコピーにも影響が出ていました。

2.1. アプリ自身のコピーを除外する

記録中に「まとめコピー」ボタンを押すと、アプリが書き込んだテキストが次のポーリングで拾われてしまいます。
これを防ぐため、コピー操作のたびに書き込んだテキストを ignoredTexts セットに追加し、監視側でスキップするようにしています。

state.ignoredTexts.add(combinedText);Code language: TypeScript (typescript)

コピーボタン押下時には記録を停止することで、仮に除外処理が抜けても二重収集が起きないようにもしています5

2.2. ドメインロジックの分離とテスト

domain.ts にはDOM操作もTauri呼び出しも含まない純粋な関数だけを置きました。
テキスト正規化、項目作成、チェック済み項目の連結、HTML生成などです。
VitestでこのファイルだけをUnit Testしており、クリップボード操作やWebViewがなくても回帰テストを回せます6

export function shouldRecordText(text: string, lastRecordedText: string): boolean {
  return text.length > 0 && text !== lastRecordedText;
}

export function buildCombinedHtml(items: ClipItem[]): string {
  const blocks = items
    .filter(item => item.enabled)
    .map(buildItemHtml)
    .filter(html => html.length > 0);
  return blocks.join("<p><br></p>");
}Code language: TypeScript (typescript)

UIに依存しない関数を domain.ts に集めるだけで、テストを書く障壁がかなり下がります。
Tauriアプリはブラウザと違いテスト環境の構築がやや面倒なので、ロジック層を切り離しておく効果が大きいです。

3. 配布形式

npm run tauri build で NSIS インストーラEXEとMSIを同時に生成できます7
今回はさらにポータブル版(単体EXE)を dist-portable/ にコピーする構成にしました。
インストーラなしで使いたい場面にも対応できます。

ビルド中にWindowsのページングファイル不足(os error 1455)でRustコンパイルが落ちる場合は、並列数を制限する -j 2 オプションか CARGO_BUILD_JOBS=1 で回避できます。

3.1. Windowsアプリ特有のはまりどころ

Tauri v2のWindowsビルドには Rust toolchain に加えて Visual Studio Build Tools 2022 と Visual C++ Build Tools が必要です。
最初は cargo 未導入でビルド自体が通らず、環境整備から始めることになりました8

もう一つはターミナルウィンドウの問題です。
インストールしたアプリを起動するとコンソールウィンドウが一緒に開く現象が起きました。
src-tauri/src/main.rs にリリースビルド時の指定を追加することで解消しています9

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]Code language: Rust (rust)

ウィンドウサイズの永続化も一度はまりました。
setSize を呼ぶには Tauri の capability に core:window:allow-set-size を明示的に追加する必要があります。
権限なしで呼んでも例外にならず静かに失敗するため、原因の特定に時間がかかりました10

4. まとめにかえて

Tauriはフロントエンドの知識で始められて、必要なところだけRustを使う構成が取れるので、「Webのスキルを使いながらデスクトップアプリを作りたい」ケースにちょうど合っています。
クリップボード操作のように、WebAPIだけでは安定しない部分をRustコマンドとして切り出す設計は、今後のTauriプロジェクトでも使い回せそうです。

matomete-copy は現在 v0.1.11 で、基本的な収集・編集・まとめコピー・Markdown保存が動いています11

4.1. 【補足】ダウンロードと起動

このプログラムは未署名されていないので、ダウンロードすると警告が表示されます。
心配な場合は、ソースコードからご自身でビルドしてください。

圧縮フォルダーを「すべて展開」し、実行します。

4.1. 【補足】ダウンロードと起動

WindowsのSmartscreenが有効だと止められます。
「不明な発行元」と表示されていますが、詳細情報から「実行する」を選択します。

一度、実行すると同じファイルの場合は二回目からは、すぐに実行できます。

まだダウンロード数が少ないので、もしかすると、セキュリティソフトによって弾かれて、削除されるかもしれません。

その場合は、CodexやClaude Codeなどで仕様から自分でツールを作ってください。

  1. Tauri v2(Tauri 2.0)は2024年10月2日にstable版がリリース。v1がデスクトップ(Windows/macOS/Linux)専用だったのに対し、v2からiOS/Androidのモバイルプラットフォームもサポートされた。セキュリティモデルもv1のallowlistから、ウィンドウやWebViewごとに権限セットを定義するケイパビリティシステムに刷新されている。 – Tauri 2.0 Stable Release
  2. ElectronはアプリごとにChromiumランタイムを同梱するため、圧縮後でもWindows/macOSアプリが96MB以上になるケースが多い。TauriはOSにインストール済みのWebViewを利用するため、バイナリ本体のサイズを大幅に抑えられる。 – Tauri Tutorial: Build a Cross-Platform App in 13 Steps [2026]
  3. navigator.clipboard.write() はAsync Clipboard APIの一部で、利用にはセキュアコンテキスト(HTTPSまたはlocalhost)とclipboard-writeパーミッションが必要。TauriのWebViewからnavigator.clipboard系のAPIを呼び出すと、WebViewのセキュリティ制約によりパーミッションダイアログが表示される場合がある。 – Support for native webview clipboard methods without security prompt · tauri-apps/tauri
  4. arboardは1PasswordがメンテナンスするオープンソースのRust製クリップボードライブラリ。Windows/macOS/Linuxに対応し、テキストと画像の読み書き、HTML形式の書き込みをクロスプラットフォームな統一APIで提供する。crates.ioでの累計ダウンロード数は1,800万を超え、Rustのクリップボード操作では事実上の標準クレートになっている。 – 1Password/arboard
  5. Tauri公式の@tauri-apps/plugin-clipboard-managerはv2からHTML形式の書き込みをサポートしているが、WebViewとネイティブレイヤーをまたぐ非同期操作でスレッドアンセーフなクリップボードアクセスが起きる問題が報告されている。RustコマンドとしてarboardでHTMLを書き込む設計はこの問題を回避し、書き込んだテキストをアプリ側で正確にトラッキングできるという利点もある。 – Clipboard operations crash on macOS due to thread safety violation · tauri-apps/plugins-workspace
  6. VitestはViteが提供するユニットテストフレームワーク。Viteの設定・プラグイン・モジュール解決をそのまま流用するため、Viteベースのプロジェクトでは追加設定がほぼ不要。JestとほぼAPIが互換で、コールドスタートが約200msとJestの2〜4秒に対して大幅に速い。 – Getting Started | Guide | Vitest
  7. NSIS(Nullsoft Scriptable Install System)はWinampの開発元Nullsoftが作ったオープンソースのWindowsインストーラ作成ツール。スクリプトベースで柔軟なインストーラを記述でき、ランタイムのオーバーヘッドが34KBと軽量。tauri buildはNSISを使ってセットアップEXEを自動生成する。 – NSIS: Nullsoft Scriptable Install System
  8. rustupのWindowsデフォルトターゲットはx86_64-pc-windows-msvcで、MicrosoftのネイティブリンカーとVisual C++ランタイムライブラリを必要とする。GNUツールチェーン(MSYS2/MinGW経由のx86_64-pc-windows-gnu)でビルドする方法もあるが、TauriはMSVCツールチェーンを推奨している。 – Windows – The rustup book
  9. #![windows_subsystem = "windows"]はRust RFC 1665で正式化されたクレート属性で、WindowsリンカーにGUIサブシステムとしてビルドするよう指示する。これを指定しないとコンソールサブシステムとして扱われ、起動時にターミナルウィンドウが開く。cfg_attr(not(debug_assertions), ...)と組み合わせることで、デバッグビルドでのコンソール出力を維持しながらリリースビルドのみターミナルを非表示にできる。 – 1665-windows-subsystem – The Rust RFC Book
  10. Tauri v2のケイパビリティシステムでは、WebView(フロントエンド)からアクセスできるコマンドやAPIをsrc-tauri/capabilities/*.jsonに明示的に記述する。権限が設定されていないAPIを呼び出すと例外を投げずに静かに失敗する(silent fail)ことがあり、DevToolsのコンソールに出るrejected Promiseのメッセージが原因特定の手がかりになる。 – Tauri Sandbox Permissions — Why Your Command Silently Does Nothing
  11. ボタンのアイコンにはLucideを使用している。LucideはFeather Iconsのコミュニティフォークとして生まれたオープンソースのSVGアイコンライブラリで、1,600以上のアイコンをReact/Vue/Svelte/バニラJSなど複数の公式パッケージで提供する。ツリーシェイキングに対応しており、使用したアイコンだけをバンドルに含められる。 – Lucide Icons