tray_runner: システムトレイから任意のコマンドを実行するための常駐型プログラム

tray_runner について

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

tray_runner の実行例
tray_runner の実行例

tray_runnerhttps://github.com/inzkyk/tray_runner にて MIT ライセンスで公開されています。

使い方

tray_runner は GitHub のリリースページからダウンロードできます。Windows 10 と Windows 11 で動作を確認しています。

ダウンロードした zip ファイルを解凍して tray_runner.exe を実行すると、システムトレイ (デスクトップの右下の領域) にアイコンが表示されます。

デフォルトの設定ではアイコンを左クリックするとコンテキストメニューが開き、右クリックするとメモ帳が開きます。

設定の編集

tray_runnertray_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 が表す値は一つのオブジェクトである必要があります。そのオブジェクトが持つべきキーとバリューの意味は次の通りです:

left_click または right_click を指定すると、それと同じ値を name に持つ menu_items 配列の要素の command が左クリックまたは右クリック時に実行されます。

left_clickright_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_clickright_click の機能は bluetooth_audio_switch をワンクリックで起動するために追加されました。

アイコン

tray_runner のアイコンは Piskel で作成しました。Piskel には初めて触れましたが、特に問題なく便利に使えました。tray_runner はプログラムのランチャーなのでアイコンはロケットです:

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.exetaskmgr.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; // 未使用の要素からなる連結リストの先頭
    // ...
};

世代型インデックスに関する参考文献:

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 マクロはその名残りです。

連結リストに関する参考文献:

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_Jsonyyjson ライブラリのラッパーです。ラップするだけだからすぐできるだろうと思っていたら、それなりに時間がかかりました。yyjson では入力データが YYJSON_PADDING_SIZE (= 4) バイトのパディングを末尾に持たなければならないので、ファイルを読み込む関数に padding 引数を追加する改修が必要になりました。


  1. 世代型インデックスを説明する英語の文章では、この問題が「ABA 問題 (ABA problem)」と呼ばれることが多かったです。ただ Wikipedia の記事を読む限り、ABA 問題は並列プログラミングで「同じ値に変更されたとき、変更があった事実を見逃す」問題を指すようです。世代型インデックスが解決する問題を ABA 問題と言っていいのかは分かりません。「カウンタを追加する」という対処が似てるからいいのか...? よく考えれば、どこも「ABA」ではないような...? ↩︎