Max Nardit
Beetroot

Beetroot v1.6.0:Rust 検索エンジン、フォーカスを奪わないウィンドウ

Beetroot の検索エンジンが Rust に移行: アクセント畳み込み、タイポ許容、プレフィックスマッチ。さらに、ワークフローを中断しないフォーカス非奪取ウィンドウ。

Beetroot で最も煩わしかったのは、開くとフォーカスを奪うことでした。エクスプローラーでファイル名を変更しようと F2 を押し、入力を始め、それから履歴から何かを取るために Beetroot を開くと、エクスプローラーがフォーカスを失う。リネームが消える。IDE のリファクタリングダイアログ、検索ボックス、フォーカスに依存するもの全部で同じです。

v1.6.0 はこれを修正します。そしてついでに、検索エンジンを Rust で書き直しました。

ひと目で:

  • フォーカス非奪取ウィンドウ:Beetroot が他のアプリからフォーカスを奪わなくなる
  • Rust 検索エンジン:アクセント畳み込み、タイポ許容、プレフィックスマッチ。Fuse.js を削除
  • ウィンドウ位置:Beetroot の表示位置を選択可能: Center、Top Left、Top Right、Bottom Left、Bottom Right
  • 4 つの新しいテキスト変換:Remove spaces、Single line、Sort lines、Remove duplicates
  • 変換メニュー検索:リストが長くなったときに変換をフィルタ
  • 10 のバグ修正:Snipping Tool キャプチャ、正規表現クラッシュ、AI の <think> タグなど

ユーザーを中断しないクリップボードマネージャ

v1.6.0 以前は、Beetroot を開くのは Alt-Tab に似た動作でした。今していることからフォーカスを奪っていました。エクスプローラーの F2 リネーム ? 消える。IDE のリファクタリングダイアログ ? フォーカスを失う。「ここに入力」のプロンプトはすべて中断されました。

これは 1 行で直せそうな話に聞こえます。違いました。

Tauri v2 の win.show() は内部で ShowWindow(SW_SHOW) を呼びます。これがウィンドウをアクティブにします。普通のアプリには問題ありません。しかし Beetroot のフロントエンドは WebView2 (Chromium) の中で動いていて、WebView2 はフォーカスについて独自の考えを持っています。標準的な Win32 アプローチはすべて失敗しました。

アプローチ機能しなかった理由
WS_EX_NOACTIVATEWebView2 子プロセスがフラグを無視
SW_SHOWNOACTIVATEWebView2 がそれでもフォーカスを取る
Focus bounce (SetForegroundWindow を戻す)WM_KILLFOCUS がすでに発火、遅すぎる
親で WM_ACTIVATE をサブクラス化WebView2 子は親のメッセージをバイパス

修正は LockSetForegroundWindow(LSFW_LOCK) です。OS レベルのロックで、すべての プロセス (WebView2 の Chromium サブプロセスも含む) が前面ウィンドウを変更するのを防ぎます。show 前にロックし、後でアンロック。30 秒のセーフティタイマーが、何かおかしくなった場合に自動でアンロックします。

Beetroot にフォーカスがないため、キーボード入力は通常通りには届きません。ナビゲーションは低レベルキーボードフック (WH_KEYBOARD_LL) を介して機能し、no-focus モードがアクティブな間、矢印キー、Enter、Escape、Space、Alt の組み合わせをインターセプトします。ウィンドウ外のクリックで非表示になります。Win+V と同じ挙動です。

Beetroot がエクスプローラーの上で開いてもフォーカスを奪わない。エクスプローラーのフォルダが選択されたまま、Beetroot がクリップボード履歴を表示

おまけ: ホットキーで呼び出されたとき、Beetroot は always on top のウィンドウ (タスクマネージャーなど) より一時的に上に表示されます。そうでないと、フルスクリーンや always on top アプリがアクティブなときに見えません。

検索を Rust で書き直し

v1.5.1 の検索 は TypeScript のスコアリングシステムでした。5 段階、Map ベースの重複排除、ファジー用の Fuse.js。日常使用ではうまく機能していました。しかし検索は UI スレッドで実行され、すべてのキーストロークが IPC を通じて全アイテムを React state に流していました。500 アイテムなら問題ありません。10K では違います。

v1.6.0 は検索全体を Rust バックエンドに移しました (search.rs の約 700 行)。Tantivy でも Nucleo でもなく、v1.5.1 の 5 段階アーキテクチャを反映しつつネイティブで動作するカスタム実装です。

Phase内容スコア
1コンテンツ/ノート内の連続部分文字列1.0
2単語先頭トークン (camelCase、アンダースコア、ハイフン)0.75
3ソースアプリ/タイトル内の連続部分文字列0.5
4ソースアプリ/タイトル内の単語先頭トークン0.25
5レーベンシュタイン距離 ≤ 1 + プレフィックスマッチ ≥ 60%0.05〜0.15

Phase 5 が Fuse.js を完全に置き換えます。Fuse.js は変更版 Bitap アルゴリズムを使い、短い文字列ではうまく機能しましたが、長いクリップボードコンテンツではゴミを生成しました。新しいファジーフェーズは 単語ごとの編集距離 を使います。クエリの各単語をコンテンツの各単語と比較します。「timout」は「timeout」にマッチします (距離 1)。「mecrosoft」は「Microsoft」にマッチしません (距離 2)。きれいで予測可能です。

アクセント畳み込み:「cafe」が「café」を見つけ、「resume」が「résumé」を見つけます。エンジンは Unicode NFD 分解を使って、マッチング前に結合マークを削除します。クエリとコンテンツの両方が同じ方法で正規化されるので、ダイアクリティカルマークは検索に対して不可視になります。

真の利点はスピードではなくアーキテクチャです。 JS の検索はキーストロークごとに IPC で全アイテムを React state にロードしていました (1 回の呼び出しで 500 オブジェクト)。Rust の検索はフィルタ済みの結果 + マッチインデックス + フィルタカウントを 1 回の IPC 呼び出しで返します。ブリッジ越しのデータ量が減り、React のレンダリングが減り、入力が滑らかになります。

アイテム数JS (v1.5.1)Rust (v1.6.0)
500〜2 ms〜2 ms
10K〜7 ms
100K〜50 ms (推定)

500 アイテムでは生の検索時間は同じです。違いはその周りのすべてにあります。IPC ペイロード、React state の更新、レンダーサイクル。

ウィンドウ位置

Beetroot は常に現在のモニタの中央に表示されていました。今は選べます: CenterTop LeftTop RightBottom LeftBottom Right

Beetroot の Settings でウィンドウ位置のオプションと Remember selected filter のトグルを表示

同じ設定パネルに Remember selected filter が追加されました。最後にアクティブだったフィルタ (Starred、Text、Images など) は、毎回 All にリセットされる代わりに、開く間で保持されます。

変換メニューの検索

組み込み変換に加えてカスタム AI プロンプトを追加していると、メニューが長くなります。今は上部に検索ボックスがあります。「upper」と入力すれば UPPERCASE にジャンプ、「sort」で Sort lines を見つけられます。

Beetroot の変換メニューで、検索バーが変換をフィルタリング

4 つの新しいテキスト変換:

変換内容
Remove spacesすべての空白を削除
Single line複数行のテキストを 1 行に結合
Sort linesアルファベット順に並べ替え
Remove duplicates行を重複排除

AI は不要、瞬時にローカルで実行されます。

その他の改善

  • 画像のソース追跡:画像も、テキストクリップと同様に、どのアプリ・ウィンドウから来たかを記録するようになりました
  • ローカライズされたタイムスタンプ:「3 分前」「2 日前」のラベルが、常に英語ではなくアプリ言語を使う
  • Unicode タイトルケース:キリル文字、CJK、アラビア文字、その他のスクリプトで正しく動作
  • より良い AI エラー:レート制限とコンテンツフィルタブロックが、汎用的な失敗ではなく具体的なメッセージを表示
  • Space でプレビューをトグル:Space で開き、もう一度 Space で閉じる (以前は Escape が必要)
  • ピン留めモードの貼り付けフィードバック:「Copied to clipboard」のトーストを表示

バグ修正

Snipping Tool のスクリーンショットが取得されない:2 つのバグが重なっていました。1 つ目: Windows の CanIncludeInClipboardHistory フォーマット (Snipping Tool、Office、メモ帳が使用) がパスワードマネージャシグナルとして誤検出されていたため、スクリーンショットが破棄されていました。2 つ目: クリップボードプラグインがファイル形式を画像形式より先にチェックしており、Snipping Tool は両方を設定するため、ファイルチェックが画像チェックより先に early return していました。

正規表現クラッシュ^a*b? のような空文字列にマッチするパターンが無限ループを引き起こしていました。今は適切に処理されます。

AI の <think> タグ:Anthropic Claude と DeepSeek のモデルは、応答を <think>...</think> 推論タグでラップすることがあります。これらは出力から除去されるようになりました。

スター付きアイテムのソート:スター付きクリップが All タブで誤って先頭に浮上していました。今は厳密に時系列順です。スター付きクリップだけを見るには Starred フィルタを使ってください。

さらに: リッチテキスト貼り付けの重複、ローカル AI エンドポイントが UI を最大 5 秒ブロック、急速なスリープ/復帰でのクリップボードモニタの競合、非表示/表示後にオーバーレイが残る、メニューを閉じた後に変換結果が適用される、複数行クリップで検索ハイライトが間違った位置に表示される問題。

アップデート方法

Beetroot は自動でアップデートを提案します。または GitHub から v1.6.0 をダウンロード してください。

ディスカッション

コメント欄はありません。議論は X で行っています。

Max Nardit

Max Nardit

@mnardit

ほかの記事

Beetroot v1.6.6:Office 修正

Excel と Word のセルが値ではなくスクリーンショットとしてキャプチャされていました。Microsoft Store の自動起動が密かに壊れていました。画像サムネイルがギガバイト単位の RAM を消費していました。v1.6.6 はこの 3 つに加え、大型の 1.6.5 AI Vision リリース後のセキュリティと信頼性の作業を修正します。

Beetroot v1.6.0:Rust 検索とフォーカス非奪取ウィンドウ