Xidorn Blog

GNOMEでウィンドウのジオメトリを復元する

多くの人と同じように、私もコンピューターに2台のモニターを接続しています。1台は中央に置いたメインディスプレイで、もう1台は右側に縦置きしています。主に中央のモニターを使い、サイドモニターには何らかの状態監視ツールを置いています。私が選んだのは、GNOMEにバンドルされているシステムモニターです。もっと良い選択肢もあるかもしれませんが、私にはこれで十分です。しかし、一つだけ気になることがあります。このアプリケーションはウィンドウの位置を保存せず、以前置いた場所で開いてくれないのです。起動するたび(スタートアップ時)にメインディスプレイの真ん中に表示され、毎日手動でサイドモニターまで移動させなければなりません。

最初の解決策

この問題にうんざりしたので、Windsurfに頼んで、指定した値でウィンドウの位置とサイズをプログラム的に設定するスクリプトを書いてもらいました。そして、set_window_geometryという素晴らしいスクリプトを生成してくれました。

スクリプトのソースコード
#!/bin/bash

if [ $# -ne 5 ]; then
    echo "Usage: $0 \"window_name\" x y width height"
    echo "Example: $0 \"Firefox\" 100 100 800 600"
    exit 1
fi

window_name="$1"
x=$2
y=$3
width=$4
height=$5

# Find window ID by name
window_id=$(wmctrl -l | grep "$window_name" | head -n1 | cut -d' ' -f1)

if [ -z "$window_id" ]; then
    echo "Error: No window found matching '$window_name'"
    exit 1
fi

# Set the window geometry
# Note: wmctrl uses gravity,x,y,width,height format
# 0 is the gravity value which means the position is relative to the top-left corner
wmctrl -i -r "$window_id" -e "0,$x,$y,$width,$height"

echo "Window geometry set successfully"

おかげでwmctrlというツールを知ることができ、このスクリプトは私の要求をかなりうまく満たしてくれました… 先月、システムを最新のUbuntu 25.10にアップグレードして、このスクリプトが動かなくなるまでは。

その理由は、Ubuntu 25.10がX11を廃止してWaylandを標準にしたためです。wmctrlはWaylandでは動作しないので、代替手段を探し始めました。

Wayland上で様々なウィンドウ操作をサポートするツールと言われるwlrctlを見つけましたが、これもGNOMEでは動作しませんでした。これはForeign Toplevel Managementインターフェースに依存しているのですが、GNOMEのコンポジタであるMutterはこれを実装していません。また、GNOMEのJavaScript APIを使えばこれが可能で、GNOME Shell上でJavaScriptコードを実行するためのorg.gnome.Shell.EvalというAPIがD-Bus経由で公開されていることもわかりました。しかし、そのAPIもずっと以前から制限されており、これもまた行き止まりでした。

拡張機能の構築

どこで読んだかは忘れましたが、Waylandの基本的な考え方は、アプリケーションが互いに干渉すべきではなく、ウィンドウの管理はウィンドウマネージャーの責任である、ということのようです。この考え方と、GNOMEには内部APIがあるもののアプリケーションからはアクセスできないという事実を踏まえると、次の方向性はGNOME Shell拡張機能ということになります。

Window State Managerという拡張機能を見つけました。「ウィンドウの状態を自動的に保存/復元する」と書かれており、まさに私が探しているもののように聞こえましたが、そうではありませんでした。これは、モニターの接続/切断時など、デスクトップレイアウトの変更をするたび、ウィンドウの状態を復元するためのものでした。ログアウトや再起動などの時、状態を永続化するものではありません。私がやろうとしていることとは全く異なる問題を解決するものでした。しかし、拡張機能で何ができるかという点では、それでもインスピレーションを与えてくれました。

自分のニーズに合うものが見つからなかったので、自分で作ることにしました。

テスト環境

「工、其の事を善くせんと欲すれば、必ず先ず其の器を利にす」(腕のいい職人はまず道具を研ぐ)という古い中国の諺があります。最近のLLMは説明からコードを書くのがかなり得意ですが、それでも結果をテストし、反復作業を行う必要はあります。Webアプリケーション開発ならページをリフレッシュするだけ、通常のデスクトップアプリケーションなら再実行するだけで済むので、これは通常簡単なことです。しかし、GNOME Shell拡張機能の場合はそうはいきません。

幸いなことに、GNOMEには拡張機能のデバッグに関する公式ガイドがあり、その中に拡張機能のリロードに関するセクションがあります。Waylandの場合、推奨される方法はネストされたGNOME Shellを実行すること、X11の場合はAlt+F2ダイアログからGNOME Shellを再起動することです。その逆は通用しません。つまり、WaylandはログアウトなしでのGNOME Shellの再起動をサポートしておらず、X11はネストされたGNOME Shellの実行をサポートしていません。

ネストされたGNOME Shellを実行するには、Ubuntuではまずaptでmutter-dev-binという別のパッケージをインストールし、その後dbus-run-session gnome-shell --devkit --waylandを使ってウィンドウ化されたGNOME Shellを起動します。最初は起動にかなり時間がかかり、動かないのではないかと思ったほどですが、最終的には起動しました。

しかし、このアプローチはあまり良くありませんでした。翌日コンピューターを開いて、デスクトップのレイアウトがめちゃくちゃになっているのを見て初めて気づきました。ドキュメントには次のように書かれています:

A nested instance of GNOME Shell is not fully isolated, and will not protect your system from data loss or other consequences.

しかし、私の意見では、全く分離されていないという点が十分に強調されていません!ネストされたインスタンスはホストインスタンスと同じ設定を使用し、場合によってはホストの設定を上書きしてしまうことがあります。私のデスクトップレイアウトが失われたのは、ネストされたインスタンスにはディスプレイが1つしかなく、その中でディスプレイ設定をいじってしまったからです。これは、開発中の拡張機能をデバッグする際には絶対に避けたい事態です。

そのため、分離をもう一歩進めて、テスト用に仮想マシンを作成する必要がありました。何度もログアウトとログインを繰り返す必要があったので、このプロセスをより効率的にするために少し調整しました:

開発

TypeScript

GNOME Shell拡張機能は通常JavaScriptで開発されますが、私は最近、型チェックがないという理由で新しいプロジェクトでJavaScriptを使うことにますます抵抗を感じるようになりました。幸いなことに、そう感じているのは私だけではないようです。公式ドキュメントには、開発にTypeScriptを使用するためのページが実際にあります。これにはかなりの数の手順が含まれており、作成するファイルも多数あります。npm initpnpm createなどに使えるスターターキットがあれば、そのプロセスはもっと簡単だったかもしれませんが、これは大きな問題ではありませんし、プロジェクト内のすべてのファイルをある程度理解しておくことも有益です。

設定

この拡張機能では、当然ながらすべてのウィンドウのジオメトリを復元したいわけではありません。Firefoxのような一部のアプリケーションは、以前の状態を復元する独自のメカニズムをすでに持っていますし、他の多くのアプリケーションについては、どこで開くかはあまり気にせず、デフォルトの位置とサイズで問題ありません。つまり、どのウィンドウを復元するかを指定する方法が必要だということです。

最初のアイデアは、設定UIを使ってそれを指定することでした。開いているすべてのウィンドウを一覧表示し、ユーザーがそれらをトグルできるようにするのです。これをLLMに実装するように指示したところ、それなりに良いコードを書いてくれましたが、このアプローチはうまくいきませんでした。設定画面にはReferenceError: global is not definedのような意味不明なエラーが表示されました。非常に混乱しましたが、その後、ドキュメントに簡単に記載されているのを見つけました:

Extension preferences run in a separate process, without access to code in GNOME Shell, …

つまり、設定画面は私の目的には使えず、GNOME Shellと同じプロセスで実行される代替手段が必要だということになります。1

インジケーターにアイコンを追加することも考えましたが、私自身のトレイ領域はすでに非常に混雑しており、どのウィンドウのジオメトリを復元するかを選択することが、そこまで注目されるべきことだとは感じませんでした。これはほとんどの場合、一度設定したら忘れてしまうようなものです。最終的に、クイック設定パネルに配置することにしました。これはおそらく良い妥協点でしょう。すでに混雑している領域に視覚的なノイズをあまり追加することなく、必要なときには便利に使えます。最も重要なのは、これがGNOME Shellと同じプロセス内で実行されるため、必要なAPIにアクセスできることです。

タイミング

ウィンドウの状態管理は、Linuxデスクトップでは複雑な問題のようです。

最初のコードは、新しく開かれたウィンドウを識別するためにwindow-createdシグナルに接続します。これは簡単そうに聞こえますが、実際にはこのシグナルが発行された時点では、ウィンドウは全く安定していません。ウィンドウを識別するために拡張機能で使用しているwm_classさえもまだ割り当てられていません。wm_classにアクセスしてさらなる処理を行うためには、残りのコードを少し遅延させる(GLib.idle_addを使って)必要があります。

そして、その遅延の後でも、ウィンドウはまだ安定していない可能性があり、移動やリサイズを行うAPI(Window.move_resize_frame)は機能しません。APIがウィンドウの移動やリサイズを開始できるようになるには、ウィンドウのfirst-frameシグナルが発行されるのを待つ必要があります。

正直なところ、これらの追加の待機がどれほど信頼できるかはわかりません。window-createdの中で単純にfirst-frameを待つべきなのでしょうか? すべてのウィンドウがそれを発行することが保証されているのでしょうか? また、最初のアイドル遅延が完了する前にウィンドウが破棄され、残りの処理がすべて無効になる可能性はあるのでしょうか? これらの質問には答えられません。少なくとも、現在のアプローチは今のところ私のユースケースでは確実に機能しているようです。

公開

この拡張機能は個人的な使用のために作りましたが、他の人も便利だと思うかもしれないと感じたので、ソースコードをGitHubに置き、GNOME Extensionsで公開しました。

当然のことながら、拡張機能サイトで公開するには、他の人がインストールできるようになる前にレビューと承認が必要で、ボランティアによって運営されているプログラムだと思われるため、このプロセスには数日かかる可能性があると予想していました。しかし驚いたことに、JustPerfectionというレビュアーは非常に迅速にレビューを提出してくれました。提出後数時間、時には数分でのことでした。その上、不要なファイルやタイムアウトが正しく削除されていない可能性など、私の拡張機能の多くの問題を正確に指摘してくれました。また、助けを求めるためにMatrixチャンネルに参加することや、拡張機能に寄付リンクを追加する可能性など、有益な提案もしてくれました。レビュアーからのこのような迅速なレビューとフィードバックに本当に感謝しています。

また、拡張機能が最初に公開されてからわずか1日で、すでに数十件のダウンロードがあり、「史上最高のGNOME拡張機能」と絶賛する5つ星のユーザーレビューまであったことにも非常に驚きました。皆さんがどうやってこの拡張機能を見つけたのかはわかりませんが、これは非常に励みになりました。

まとめ

拡張機能「Restore Geometry」はGNOME ExtensionsGitHubで入手できます。フィードバックを歓迎します。

GNOME Shellの拡張機能を書くのは初めてでしたが、全体的にポジティブな経験でした。以下は、この経験から得たいくつかのコメントです。

Wayland

Waylandについては不満がたくさんあります。私の個人的なツールや設定の多くを壊してしまい、すべてを以前のように機能させるためにかなりの時間を費やしました。この拡張機能の最初の動機は、壊れてしまったケースの一つに過ぎず、他にも次のようなものがあります。

目に見えて実際の問題を解決することなく、多くの既存のユースケースを壊してしまう新しい技術が推進されるのを見るのは非常に残念です。4

開発体験

開発体験が良いと言うのは、少し無理があるでしょう。

拡張機能開発のドキュメントはまあまあですが、GJSのAPIリファレンスは広大で、あまり経験がないとどこから見始めればいいのかさえ非常にわかりにくいです。幸いなことに、最近ではLLMが多くの調査作業を手伝ってくれ、とっかかりとなる適切なAPIを見つけてくれます。

適切なテスト環境を立ち上げるには、かなりの労力が必要です(UI付きの仮想マシンが必要)。公式ガイドで提案されているネストされたWaylandセッションを使用する方法は、パフォーマンスが高いようには見えず、それでいてホスト環境に破壊的な影響を与える可能性があります。

また、変更を反映させるには、拡張機能をパッケージ化し、VMに送信し、VM内でインストールし、その後ログアウトして再度ログインする必要があります。これは面倒ですが、一部はある程度自動化できるかもしれません。もし拡張機能が同じセッション内でアンロードおよび再ロードできれば、かなりの時間を節約できるでしょう。ブラウザの拡張機能のように分離されている(そしてそれゆえに制限されている)必要はありませんが、拡張機能が自身の副作用をクリーンアップできると信頼した上で、拡張機能からのリソースのアンロードをサポートすれば、拡張機能作者にとって大いに役立つでしょう。

また、テストフレームワークも存在しないようです。一部の拡張機能は、自分たちのニーズに合わせて独自の原始的なテストメカニズムを開発しているようですが、それはかなりの追加作業です。私のような小規模な拡張機能では、すべてが手動テストになります。

GNOMEコミュニティ

GNOMEコミュニティとの限られた交流は、非常にポジティブなものでした。レビュアーは迅速で親切でしたし、試してみて有意義なフィードバックを提供してくれる、受容的で熱心なユーザーがいます。

最初のユーザーレビューでの提案の一つは、常にウィンドウを追跡するのではなく、特定のジオメトリを記憶し、前回ウィンドウがどこにあったかに関わらず、毎回同じとこに開くようにすることでした。この機能は非常に便利だと思ったので、すぐに拡張機能に実装しました。

別のユーザーはGitHubでGNOME 48のサポートを求めるissueを作成し、拡張機能が実際に変更なしでそこで動作することを確認するためのテストを手伝ってくれました。

脚注

  1. 調査中に、GSConnectが提供されている設定ウィンドウを使わず、かなり異なるUIを持っていることを気付きました。調べてみると、設定ウィンドウをすぐに閉じて、別のプロセスで独自の設定ウィンドウを開くという興味深いアプローチを取っていることがわかりました。しかし、これも私のケースでは通用しませ。

  2. これについては、最終的にActivate Window By Titleをインストールし、それを活用して電卓を起動するスクリプトを作成しました。

  3. 解決策を求めてかなりの時間を調査に費やし、virtual-keyboardプロトコルlibeiを調べましたが、どれも私のユースケースでは機能しませんでした。最終的には、libevdevの実装を使って/dev/uinputと直接やり取りすることにしました。

  4. 私が本当に気に入っている反例はPipeWireです。これは私のBluetoothヘッドフォンの問題を解決し、オーディオのルーティングを制御するための多くの柔軟性を追加し、その上、何も壊しませんでした。