クリップボードのテキストを読み書きする

clipboard-manager の writeText / readText / clear と Rust の ClipboardExt で、テキストのコピー・貼り付け・消去を行う。default では何も許可されない権限の設定も示す。

システム情報 対象: Tauri 2.x 更新日: 読了目安: 約8分 sys-007
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. コピーと貼り付け
  4. 書式付きでコピーする・消去する
  5. 2. バックエンドから実装する (Rust)
  6. 動作確認
  7. よくあるエラーと対処法
  8. OS ごとの違いと注意点
  9. 関連レシピ

「コピー」ボタンで ID や URL をクリップボードに入れる、「貼り付け」ボタンで読み取った文字列を入力欄に入れる、パスワードをコピーして一定時間後に消す、といったテキストのやり取りは Clipboard Manager プラグインで行います。JS からは writeText() / readText() / clear()、Rust からは ClipboardExt の同名のメソッドを使います。つまずきやすいのは権限の設定と、読み取れないときに例外になる点です。画像は クリップボードに画像をコピーする で扱います。

前提条件

npm run tauri add clipboard-manager

npm パッケージと Rust クレートが追加され、lib.rs にプラグインの登録が入ります。

注意したいのは、clipboard-manager:default が何も許可しない点です。クリップボードには他のアプリでコピーしたパスワードなども入るため、使う操作を 1 つずつ許可します。core:default にも含まれません。

関数権限
writeText()clipboard-manager:allow-write-text
readText()clipboard-manager:allow-read-text
writeHtml()clipboard-manager:allow-write-html
clear()clipboard-manager:allow-clear
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "clipboard-manager:allow-write-text",
    "clipboard-manager:allow-read-text",
    "clipboard-manager:allow-write-html",
    "clipboard-manager:allow-clear"
  ]
}

使わないものは外します。特に読み取りを許可すると、そのウィンドウで動くスクリプトはユーザーの操作が無くてもクリップボードを読めるようになります。コピーしかしないアプリなら allow-write-text だけで足ります。

1. フロントエンドから実装する (TypeScript)

コピーと貼り付け

readText() は、クリップボードが空のときや画像しか入っていないときに null や空文字列を返すのではなく、例外を投げます。「読めなければ何もしない」処理は try / catch で書き、権限の設定漏れまで握りつぶさないよう、エラーの文面で分けます。

import { writeText, readText } from '@tauri-apps/plugin-clipboard-manager';

// data-copy 属性の文字列をコピーするボタン
document.querySelectorAll<HTMLButtonElement>('button[data-copy]').forEach((btn) => {
  btn.addEventListener('click', async () => {
    await writeText(btn.dataset.copy ?? ''); // clipboard-manager:allow-write-text
    btn.textContent = 'コピーしました';
  });
});

// 貼り付け。テキストが無ければ null を返す
export async function pasteText(): Promise<string | null> {
  try {
    return await readText(); // clipboard-manager:allow-read-text
  } catch (e) {
    if (String(e).includes('not allowed')) throw e; // 権限の設定漏れは表に出す
    return null; // 空、または画像などテキスト以外
  }
}

書式付きでコピーする・消去する

writeHtml(html, altText) は、HTML と、書式を扱えない貼り付け先のためのプレーンテキストを同時に書き込みます。HTML を受け付けるワープロやメールには太字などが残り、テキストエディタには altText が入ります。altText を省略するとプレーンテキスト側が空になる場合があるので、必ず渡します。clear() はテキストだけでなく画像も含めてクリップボードを空にします。

import { writeHtml, clear } from '@tauri-apps/plugin-clipboard-manager';

export async function copyOrder(id: string) {
  // 書式付き(HTML)と、プレーンテキストの代わりを一緒に書き込む
  await writeHtml(`<b>注文番号</b>: ${id}`, `注文番号: ${id}`); // allow-write-html
}

export async function wipeClipboard() {
  await clear(); // allow-clear。画像も消える
}

2. バックエンドから実装する (Rust)

tauri_plugin_clipboard_manager::ClipboardExt を use すると、AppHandle やウィンドウで clipboard() が使えます。capability の権限は JS からの呼び出しにだけ効くので、Rust で処理すれば WebView 側に allow-read-text を渡さずに済みます。クリップボードの中身を読むのは Rust のコマンドだけにして、必要な結果だけを JS に返す設計にできます。

読み取り(read_text())はメインスレッドで呼ばないようにします。Linux では、WebView からコピーした内容を読むときなどにアプリ全体が固まることがあります。async の付かないコマンドはメインスレッドで実行されるので、読み取るコマンドは async fn にし、時間をおいて行う処理は別スレッドに回します。

use std::time::Duration;
use tauri_plugin_clipboard_manager::ClipboardExt;

#[tauri::command]
fn copy_text(app: tauri::AppHandle, text: String) -> Result<(), String> {
    app.clipboard().write_text(text).map_err(|e| e.to_string())
}

// 読み取りは async にしてメインスレッドを避ける
#[tauri::command]
async fn paste_text(app: tauri::AppHandle) -> Result<String, String> {
    app.clipboard().read_text().map_err(|e| e.to_string())
}

/// パスワードなどをコピーし、30 秒後にまだ同じ内容なら消す
#[tauri::command]
fn copy_secret(app: tauri::AppHandle, secret: String) -> Result<(), String> {
    app.clipboard().write_text(secret.as_str()).map_err(|e| e.to_string())?;
    std::thread::spawn(move || {
        std::thread::sleep(Duration::from_secs(30));
        // その間にユーザーが別の物をコピーしていたら消さない
        if app.clipboard().read_text().ok().as_deref() == Some(secret.as_str()) {
            let _ = app.clipboard().clear();
        }
    });
    Ok(())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_clipboard_manager::init()) // Rust だけで使う場合も必要
        .invoke_handler(tauri::generate_handler![copy_text, paste_text, copy_secret])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';

await invoke('copy_secret', { secret: 'p@ssw0rd' }); // JS 側に clipboard の権限は不要
const text = await invoke<string>('paste_text');
console.log(text);

write_text() は &str と String のどちらも受け取ります。トレイやメニュー、グローバルショートカットのハンドラーでも AppHandle があれば同じように呼べます。ただしトレイとメニューのハンドラーはメインスレッドで動くので、そこで読み取るときは std::thread::spawn などで別スレッドに移します。

動作確認

npm run tauri dev で起動し、コピーボタンを押してからメモ帳などに貼り付けると、同じ文字列が入ります。copyOrder('12345') の後にワープロへ貼ると「注文番号」が太字になり、テキストエディタには altText の文字列が入ります。copy_secret を呼んで 30 秒待つとクリップボードが空になり、pasteText() は null を返します。途中で別の文字列をコピーした場合は消えません。

DevTools のコンソールで読み書きを確かめる例です。

import { writeText, readText } from '@tauri-apps/plugin-clipboard-manager';

await writeText('Hello, Tauri!');
console.log(await readText()); // Hello, Tauri!

よくあるエラーと対処法

  • 「clipboard-manager.read_text not allowed. Permissions associated with this command: clipboard-manager:allow-read-text」: 権限の追加漏れです。clipboard-manager:default だけでは何も許可されません。リリースビルドでは「Command plugin:clipboard-manager|read_text not allowed by ACL」になります。追加したら tauri dev を再起動します。
  • 「The clipboard contents were not available in the requested format or the clipboard is empty.」: readText() / read_text() で、クリップボードが空か、画像などテキスト以外しか入っていません。空の貼り付けとして扱います。
  • 「The native clipboard is not accessible due to being held by another party.」: 別のアプリがクリップボードを使用中です。少し待ってから再試行します。
  • Rust で「state() called before manage() for」で始まるパニック: .plugin(tauri_plugin_clipboard_manager::init()) の登録漏れです。JS から使わない場合も登録が必要です。
  • Linux で貼り付けた瞬間にアプリが固まる: 同期コマンドやメインスレッドで read_text() を呼んでいます。async fn のコマンドか別スレッドに移します。

OS ごとの違いと注意点

  • Android / iOS: 扱えるのはプレーンテキストだけです。writeHtml() は「Unsupported on this platform」で失敗します。
  • Android: clear() が本当に消去するのは Android 9(API レベル 28)以上で、それより古い端末では空文字列を書き込んで代わりにします。
  • writeText() の label オプション: モバイル向けの指定で、デスクトップでは使われません。
  • Linux: 前述のとおり、Rust から読み取るときはメインスレッドを避けます。
  • 共通: クリップボードは OS 全体で 1 つなので、書き込むとユーザーが直前にコピーしていた内容は上書きされます。自動でコピーするのは、ボタンを押したときなどユーザーの操作をきっかけにした場合に限るのが無難です。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する