tray_runner: システムトレイから任意のコマンドを実行するための常駐型プログラム
tray_runner
について
tray_runner
はシステムトレイから任意のコマンドを実行するための常駐型プログラムです。「自動化スクリプトは作ったけど、自動化スクリプトを起動するためにターミナルを開いてコマンドを打ち込むのがめんどくさい!」という思いから作成されました。軽量 (237 KB) で、設定を JSON で記述できることが特徴です。

tray_runner
の実行例
tray_runner
は https://github.com/inzkyk/tray_runner にて MIT ライセンスで公開されています。
使い方
tray_runner
は GitHub のリリースページからダウンロードできます。Windows 10 と Windows 11 で動作を確認しています。
ダウンロードした zip ファイルを解凍して tray_runner.exe
を実行すると、システムトレイ (デスクトップの右下の領域) にアイコンが表示されます。
デフォルトの設定ではアイコンを左クリックするとコンテキストメニューが開き、右クリックするとメモ帳が開きます。
設定の編集
tray_runner
は tray_runner_config.json
から設定を読み込みます。初期状態の tray_runner_config.json
は次の通りです:
{
"menu_items": [
{
"name": "電卓",
"command": "calc.exe"
},
{
"name": "メモ帳",
"command": "notepad.exe"
},
{
"name": "ペイント",
"command": "mspaint.exe"
},
{
"name": "エクスプローラー",
"command": "explorer.exe"
},
{
"name": "タスクマネージャー",
"command": "taskmgr.exe"
},
{
"name": "ウェブページを開く",
"menu_items": [
{
"name": "Google",
"menu_items": [
{
"name": "トップページ",
"command": "cmd /c start https://google.com"
},
{
"name": "Gmail",
"command": "cmd /c start https://mail.google.com"
},
{
"name": "Google マップ",
"command": "cmd /c https://www.google.com/maps"
}
]
},
{
"name": "Yahoo! Japan",
"menu_items": [
{
"name": "トップページ",
"command": "cmd /c start https://www.yahoo.co.jp"
},
{
"name": "Yahoo! ニュース",
"command": "cmd /c start https://news.yahoo.co.jp"
}
]
},
{
"name": "tray_runner 解説ページ",
"command": "cmd /c start https://inzkyk.xyz/software/tray_runner"
}
]
}
],
"right_click": "メモ帳"
}
このファイルを編集すればシステムトレイアイコンをクリックしたときの挙動を変更できます。
設定ファイルに何を書くべきかは上の例を見れば理解できると思いますが、ここに正確に書いておきます。tray_runner_config.json
が表す値は一つのオブジェクトである必要があります。そのオブジェクトが持つべきキーとバリューの意味は次の通りです:
-
menu_items
: コンテキストメニューの要素を定義する配列 (必須)各要素は次のキーとバリューを持つ:
name
: コンテキストメニューに表示される名前 (必須)- 次の二つのどちらか一方 (必須):
command
: コンテキストメニュー要素をクリックしたときに実行されるコマンドmenu_items
: 再帰的なコンテキストメニュー要素の定義
-
left_click
: システムトレイアイコンを左クリックしたときに実行されるコマンドの名前 (省略可能) -
right_click
: システムトレイアイコンを右クリックしたときに実行されるコマンドの名前 (省略可能)
left_click
または right_click
を指定すると、それと同じ値を name
に持つ menu_items
配列の要素の command
が左クリックまたは右クリック時に実行されます。
left_click
と right_click
を両方指定するとコンテキストメニューが開けなくなります。これは仕様です。このとき tray_runner
の終了はタスクマネージャから行ってください。
設定ファイルの場所はコマンドライン引数で指定できます。tray_runner.exe FILE
として起動すると、tray_runner_config.json
の代わりに FILE
から設定が読み込まれます。
コンテキストメニューには「設定ファイルを再読み込み」ボタンがあります。設定ファイルの読み込みに失敗した場合などに利用してください。また「設定ファイルを開く」ボタンもあります。
セキュリティ
tray_runner
のセキュリティは絶望的なので気をつけてください。tray_runner_config.json
に書かれたコマンド (をワイド文字列に変換したもの) がそのまま CreateProcess
関数に渡されます。また、「設定ファイルを開く」の機能では cmd /c "start FILE"
が使われており、これも悪用される可能性があります。
信頼できない設定ファイルを読み込まないように注意してください。tray_runner
は無保証 (MIT ライセンス) で配布されます。
便利なコマンド
tray_runner_config.json
で役立つかもしれないコマンドを紹介します。
既定のアプリケーションでファイルを開く
cmd /c start tray_runner_config.json
既定のブラウザでページを開く
cmd /c start https://google.com
エクスプローラーで特定のディレクトリを開く
explorer.exe C:\Windows\
ターミナルで PowerShell を起動しつつ特定のコマンドを実行する
windowsterminal.exe pwsh -NoExit -Interactive -Command dir
Chrome/Firefox で特定のサイトを開く
cmd /c "C:\Program Files\Google\Chrome\Application\chrome.exe" https://google.com
cmd /c "C:\Program Files\Mozilla Firefox\firefox.exe" https://google.com
インストール場所は適当に入れ替えてください。
その他の話題
思い出話とか。
作った理由
冒頭にも書いたように「自動化スクリプトは作ったけど、自動化スクリプトを起動するためにターミナルを開いてコマンドを打ち込むのがめんどくさい!」という思いから作りました。シェルの履歴機能を使えばコマンドはすぐに見つかりますが、それでもターミナルが起動していないときは実行が面倒です。
また、left_click
と right_click
の機能は bluetooth_audio_switch
をワンクリックで起動するために追加されました。
アイコン
tray_runner
のアイコンは Piskel で作成しました。Piskel には初めて触れましたが、特に問題なく便利に使えました。tray_runner
はプログラムのランチャーなのでアイコンはロケットです:

tray_runner
のアイコン (拡大)
元々は炎の周りに白い煙が書いてあったのですが、システムトレイに表示されたときの視認性が悪かったので削除しました。実際のロケット打ち上げ映像をいくつか確認してみたところ、アイコンのようにロケットが傾いている (それなりの高度にある) とき煙はほとんど見えないようです。この角度で煙がモクモクと出ている場合、ロケットは墜落直前です。
tray_runner
はアイコンのデータを uint8_t
の配列に変換したもの (tray_runner_icon.hpp
) を直接利用するので、PNG ファイルはレポジトリに含まれません。画像を uint8_t
の配列に変換する Python スクリプトは ChatGPT に書いてもらいました。
ウイルスチェックに引っかかる
開発中の tray_runner
を GitHub Action でビルドしたところ、生成される実行形式ファイルがウイルスと判定されました。Chrome からはそもそもダウンロードできず、他のブラウザからダウンロードしても Windows Defender に削除されて実行できない状況でした。
ChatGPT に相談したところ「埋め込まれているリソースに問題があるのかもしれない」と言われたので、サンプルの設定テキスト (JSON) を出力する機能を削除したところ誤検知は止まりました。calc.exe
や taskmgr.exe
が含まれるテキストリテラルがあったのがまずかったのだと思います。でもウイルスチェックとしてそれでいいのか...?
AI にも書けそう
tray_runner
はよくある「ランチャーアプリ」なので、AI に「システムトレイ常駐型のランチャーアプリを C++ と WinAPI で作って」などと頼めば一発でそれなりのものが出てきます。時間をかければ tray_runner
より豪華なプログラムも書けるでしょう。tray_runner
に実装したような細かいメモリ管理を実装できるかどうかが気になります (やらない)。
amalgam.cpp
の行数
tray_runner
のソースコードを全てまとめた amalcam.cpp
は 27480 行で、そのうち約半分は JSON パーサーの yyjson です。自分で簡単なフォーマットを実装すればもっと行数は少なくできたと思いますが、JSON の読み込みをやってみたかったので yyjson を使いました。
ix ライブラリ
これまでに公開したソフトウェアと同様に、tray_runner
では ix ライブラリを利用しました。tray_runner
で重要な役割を果たすクラスをいくつか紹介します。たかがランチャーアプリにしては重装備すぎる気もしますが、今後のプログラムで使えるだろうと思って妥協せずに作成しました。
ix_GenerationalIndex
, ix_Handle<T>
ix_GenerationalIndex
は世代型インデックス (generational index) や世代型ハンドル (generational handle) といった名前で呼ばれるデータ構造です。世代を表す整数と、インデックスを表す整数からなります:
class ix_GenerationalIndex
{
uint32_t m_generation;
uint32_t m_index;
// ...
};
ix_Handle<T>
は ix_GenerationalIndex
に型パラメータを付けただけのラッパーです。
ix_Pool<T>
ix_Pool<T>
は ix_Handle<T>
を添え字として利用する配列風のデータ構造です。世代型配列 (generational array) や世代型アリーナ (generational arena) とも呼ばれます。
ix_Pool<T>
を使うと通常の配列と同様に値の構築と破棄が実行できます。値を構築する construct
メソッドが返すのは ix_Handle<T>
であり、これを使って構築した値にアクセスします。
ix_Handle<T>
は好きにコピーでき、値が破棄されるとアクセス時に nullptr
が返るようになります。また、「添え字に対応する場所にある値が破棄されてから再利用されたために、アクセス結果が全く関係ない値になる」問題 (いわゆる ABA 問題1) は発生しません。
// ix_Pool<int> の使用例
ix_Pool<int> pool;
// 要素の追加: ix_Handle<int> が返る
ix_Handle<int> gidx = pool.construct(100);
ix_EXPECT_EQ(gidx.generation(), 1);
ix_EXPECT_EQ(gidx.index(), 0);
ix_EXPECT(pool.is_valid(gidx));
// ix_Handle<int> を使った要素の取得
int *p = pool.index(gidx);
ix_EXPECT_EQ(*p, 100);
// 要素の破棄
pool.destruct(gidx);
ix_EXPECT(!pool.is_valid(gidx));
// 別の要素の追加
ix_Handle<int> another_gidx = pool.construct(200);
ix_EXPECT(pool.is_valid(another_gidx));
ix_EXPECT_EQ(another_gidx.generation(), 2);
ix_EXPECT_EQ(another_gidx.index(), 0); // 第 0 要素が再利用される
p = pool.index(another_gidx);
ix_EXPECT_EQ(*p, 200);
// ABA 問題は起こらない: 破棄済みの ix_Handle<int> を使ってアクセスすると nullptr が返る
p = pool.index(gidx);
ix_EXPECT_EQ(p, nullptr);
ix_Pool<T>
は T
型の値 (に管理用データを付けたもの) を連続した領域に確保し、未使用の要素を連結リストで管理して破棄された値を再利用します。そのため値の構築と破棄は高速に実行でき、メモリの断片化も起こりません。
template <typename T>
class ix_Pool
{
struct Slot
{
T value;
uint32_t generation;
uint32_t next;
};
Slot *m_slots = nullptr; // ix_Pool が持つ唯一の配列
uint32_t m_capacity = 0;
// ...
uint32_t m_free_list_head = 0; // 未使用の要素からなる連結リストの先頭
// ...
};
世代型インデックスに関する参考文献:
- Handles are the better pointers
- Vale's Memory Safety Strategy: Generational References and Regions
-
HypeHype: Mobile Rendering Architecture
- 26 ページに「Generational Pools and Handles」というスライドがある
-
Data Structures Part 1: Bulk Data - Our Machinery
- bulk data に weak pointer の機能を追加する方法として generation フィールドを使う例が出てくる
- Sebastian Aaltonen 氏の X スレッド
ix_LinkedList<T>
, ix_LinkedListManager<T>
ix_LinkedListManager<T>
は複数の連結リストを管理 (作成・改変・破棄) するためのクラスです。木構造を扱うとき、各ノードが持つ子のリストを std::vector
などのベクトルで扱うと木が高くて各ノードの子が少ないとき無駄が多いと思ったので作りました。
ix_LinkedListManager<T>
では全てのリストの全てのノードが単一の配列の要素となります。そのため ix_Pool<T>
と同じように操作は高速で、断片化も最小限です。
ix_LinkedListManager<int> manager;
ix_LinkedList<int> list = manager.new_list();
manager.insert(list, 1);
manager.insert(list, 10);
manager.insert(list, 100);
manager.insert(list, 1000);
manager.insert(list, 10000);
uint32_t index_of_100 = 0;
int sum = 0;
ix_ITERATE_LINKED_LIST(manager, list, x)
{
// x の型は int *
sum += *x;
if (*x == 100)
{
index_of_100 = manager.index_of(x);
}
}
ix_EXPECT_EQ(sum, 11111);
ix_EXPECT_EQ(index_of_100, 2);
ix_LinkedList<int> another_list = manager.new_list();
manager.insert(another_list, 2);
manager.insert(another_list, 20);
manager.insert(another_list, 200);
manager.insert(another_list, 2000);
manager.insert(another_list, 20000);
uint32_t index_of_200 = 0;
sum = 0;
ix_ITERATE_LINKED_LIST(manager, another_list, x)
{
// x の型は int *
sum += *x;
if (*x == 200)
{
index_of_200 = manager.index_of(x);
}
}
ix_EXPECT_EQ(sum, 22222);
ix_EXPECT_EQ(index_of_200, 7); // 全てのリストの全てのノードは同じ配列上に配置される
manager.erase_at(list, index_of_100);
int *p = manager.insert(another_list, 200000);
ix_EXPECT_EQ(manager.index_of(p), index_of_100); // index_of_100 番目のノードが再利用される
ユーザーが作成するリストは環状双方向連結リスト、未使用のノードからなるフリーリストは単方向連結リストとなります。二種類のリストが利用されるのは、特定のノードがフリーリストに含まれるかどうかをデストラクタで高速に判定できる必要があるためです。この点があるために、ix_LinkedListManager<T>
の実装は大変でした。sanity_check
メソッドや ix_LINKED_LIST_MANAGER_DEBUG
マクロはその名残りです。
連結リストに関する参考文献:
- In Defense Of Linked Lists - by Ryan Fleury - Digital Grove
- Parsing XML at the Speed of Light - The Performance of Open Source Software [拙訳]
ix_ContextMenuManager
ix_ContextMenuManager
はコンテキストメニュー (右クリックメニュー) を管理するためのクラスです。OS が提供するハンドルや関数の簡単なラッパーであり、メニューの木構造を表現するために ix_LinkedList<T>
を利用します。
静的なメニューであれば、std::initializer_list
を使って一度に構築できます。子要素を持つメニューであっても構築可能です:
ix_ContextMenuManager manager;
ix_ContextMenu menu = manager.create_menu_with({
ix_ContextMenuItem::string("文字列です", []() { ix_log_debug("クリックされました"); }),
ix_ContextMenuItem::grayed_string("クリックできない文字列です"),
ix_ContextMenuItem::checkbox(
"チェックボックスです",
true, // 初期値
[](bool new_value) {
if (new_value)
{
ix_log_debug("チェックされました");
}
else
{
ix_log_debug("チェックが外されました");
}
}
),
ix_ContextMenuItem::separator(),
ix_ContextMenuItem::popup(
"ポップアップメニュー",
manager.create_menu_column_with({
ix_ContextMenuItem::string("子要素 1", []() { ix_log_debug("子要素 1 です"); }),
ix_ContextMenuItem::string("子要素 2", []() { ix_log_debug("子要素 2 です"); }),
ix_ContextMenuItem::string("子要素 3", []() { ix_log_debug("子要素 3 です"); }),
ix_ContextMenuItem::string("子要素 4", []() { ix_log_debug("子要素 4 です"); }),
})
),
});
// ...
// ウィンドウプロシージャ
switch (uMsg)
{
// ...
case WM_COMMAND: {
return g->context_menu_manager.win_process_WM_COMMAND(hWnd, uMsg, wParam, lParam);
// ...
}
// ...
// メイン処理
if (clicked)
{
menu.render(hWnd, x, y);
}
tray_runner
ではメニューが静的でない (JSON ファイルの内容から決まる部分がある) ので、メニュー要素を一つずつ追加する方法でメニューが構築されます。
現在 ix_ContextMenuManager
は Windows しかサポートしていませんが、中間データ構造は OS に依存していないので、他の OS (あるいはレンダラ) にも対応できるはずです。
ix_SystemTrayManager
ix_SystemTrayManager
はシステムトレイアイコンを管理するためのクラスです。アイコンの画像、そしてアイコンがクリックされたときに実行する関数を設定できます:
constexpr uint8_t icon_width = 32;
constexpr uint8_t icon_height = 32;
uint8_t icon_data[4 * icon_width * icon_height] = {}; // ファイルなどから読み出す
ix_SystemTrayManager manager;
ix_SystemTrayItem tray_item = manager.create_item(
"タイトル",
icon_data,
icon_width,
icon_height,
[]() { ix_log_debug("左クリックされました"); },
[]() { ix_log_debug("右クリックされました"); }
);
manager.install(hWnd, tray_item);
// ...
// ウィンドウプロシージャ
switch (uMsg)
{
// ...
case ix_WM_TRAYICON: {
return g->system_tray_manager.win_process_ix_WM_TRAYICON(hWnd, uMsg, wParam, lParam);
}
// ...
}
ix_SystemTrayManager
の作成では、システムトレイアイコンのピクセルの色からなる配列から OS のアイコンハンドルを作って返す win_create_icon_from_rgba_array
関数に手間取りました。ドキュメントを呼んでもいまいちよく理解できなかったので、正しそうな結果が得られるまでデータのアラインメントやビット操作や座標系をいじり続けました。現在の実装が本当に正しいかどうかは分かりません。
ix_Json
ix_Json
は yyjson ライブラリのラッパーです。ラップするだけだからすぐできるだろうと思っていたら、それなりに時間がかかりました。yyjson では入力データが YYJSON_PADDING_SIZE
(= 4
) バイトのパディングを末尾に持たなければならないので、ファイルを読み込む関数に padding
引数を追加する改修が必要になりました。
-
世代型インデックスを説明する英語の文章では、この問題が「ABA 問題 (ABA problem)」と呼ばれることが多かったです。ただ Wikipedia の記事を読む限り、ABA 問題は並列プログラミングで「同じ値に変更されたとき、変更があった事実を見逃す」問題を指すようです。世代型インデックスが解決する問題を ABA 問題と言っていいのかは分かりません。「カウンタを追加する」という対処が似てるからいいのか...? よく考えれば、どこも「ABA」ではないような...? ↩︎