0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ubuntu 25.10 (Gnome 49) でGnome Shell拡張機能を開発してみた。大学の時間割をトップバーに表示する。

Posted at

はじめに

こんにちはkoruchaです。
学部4年にもなって大学の時間割を忘れます。
いちいち大学の公式サイトに行って時間割を確認するのはめんどくさすぎるので、Gnome Shellの拡張機能を作ってみました。

image.png
Ubuntuのトップバーに現在の授業終了までのカウントダウンや、次の授業が始まるまでの時間を表示する拡張機能です。

💻 開発環境

OS: Ubuntu 25.10
Desktop: GNOME 49.0
Windowing System: Wayland

🔧 開発手順

1. 開発環境の整備 (mutter-devkitのインストール)

Gnome Shell拡張機能の開発には、mutter-devkit というツールが必要だと公式ドキュメント (https://gjs.guide) に書かれています。

しかし、私の環境 (Ubuntu 25.10) で

$ sudo apt install mutter-devkit

を実行したところ、「パッケージが見つからない (Package not found)」というエラーに。

かなり調査した結果、Ubuntuでは以下のパッケージ名でインストールする必要があることが分かりました。

# mutter-devkitの代わりにこれらをインストール
$ sudo apt install libmutter-17-dev mutter-dev-bin

公式ドキュメント、ちょっと不親切では...?
Ubuntu用のパッケージ名が書いてなかったです。

2. プロジェクトの作成

プロジェクトの雛形は gnome-extensionsコマンドで作成するのが圧倒的に楽でした。

$ gnome-extensions create --interactive

これを実行すると、以下のように対話形式で必要な情報を入力していくだけで、拡張機能のテンプレートが作成されます。

名前はとても短い (理想は説明的な) 文字列にしてください。
たとえば: “Click To Focus”, “Adblock”, “Shell Window Shrinker”
名前: University Timetable

説明は、拡張機能が何をするかを一文で説明するものです。
たとえば: “Make windows visible on click”, “Block advertisement popups”, “Animate windows shrinking on minimize”
説明: Show university timetable on top panel

UUID は拡張機能のグローバルで一意の識別子です。
メールアドレス形式であるべきです (clicktofocus@janedoe.example.com)
UUID: university-timetable@example.com

利用可能なテンプレートから1つ選択:
1) プレーン  –  空の拡張機能
2) インジケーター  –  トップバーにアイコンを追加します
3) クイック設定アイテム  –  クイック設定にアイテムを追加
テンプレート [1-3]: 1

新しい拡張機能が /home/your-name/.local/share/gnome-shell/extensions/university-timetable@example.com に正常に作成されました。

驚いたのは、この対話式インターフェースが完璧に日本語対応していたことです。
Gnome BuilderでGUIからポチポチ作るものだと勘違いしていたので、CUI操作に怯えていましたが、迷うことなく作成できました。

3. コーディング (extension.js)

作成されたディレクトリ (~/.local/share/gnome-shell/extensions/university-timetable@example.com) に移動し、VSCodeなどのエディタで extension.js を編集していきます。

完成したコードがこちらです。

import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
import GLib from 'gi://GLib';
import St from 'gi://St';
import Clutter from 'gi://Clutter';

// 時間割
const TIMETABLE = [
    { name: "1限", start: { h: 9, m: 15 }, end: { h: 10, m: 45 } },
    { name: "2限", start: { h: 10, m: 55 }, end: { h: 12, m: 25 } },
    { name: "3限", start: { h: 13, m: 10 }, end: { h: 14, m: 40 } },
    { name: "4限", start: { h: 14, m: 50 }, end: { h: 16, m: 20 } },
    { name: "5限", start: { h: 16, m: 30 }, end: { h: 18, m: 0 } },
    { name: "6限", start: { h: 18, m: 10 }, end: { h: 19, m: 40 } }
];

// 時間割の時刻を今日の「Date」オブジェクトに変換するヘルパー
function getTodayTime(h, m) {
    const now = new Date();
    now.setHours(h, m, 0, 0); // 時、分、秒、ミリ秒
    return now;
}

// 拡張機能の本体
export default class UniversityTimetableExtension extends Extension {
    constructor(metadata) {
        super(metadata);
        this._indicator = null;
        this._timeout = null;
    }

    enable() {
        // トップパネルに表示するボタン(インジケーター)を作成
        this._indicator = new PanelMenu.Button(0.5, 'University Timetable', false);
        
        // 表示するラベル
        this._indicator.label = new St.Label({ 
            text: '読込中...', 
            y_align: Clutter.ActorAlign.CENTER 
        });
        this._indicator.add_child(this._indicator.label);

        // パネルのステータス領域に追加
        Main.panel.addToStatusArea(this.uuid, this._indicator);

        // 1秒ごとに _updateDisplay 関数を実行するループを開始
        this._timeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => {
            this._updateDisplay();
            return GLib.SOURCE_CONTINUE; // GLib.SOURCE_CONTINUEを返すとループが継続
        });
    }

    disable() {
        // 拡張機能が無効になったらループを停止し、インジケーターを破棄
        if (this._timeout) {
            GLib.Source.remove(this._timeout);
            this._timeout = null;
        }
        
        if (this._indicator) {
            this._indicator.destroy();
            this._indicator = null;
        }
    }

    // 毎秒実行されるメインロジック
    _updateDisplay() {
        const now = new Date();
        let labelText = "🕒 授業時間外"; // デフォルトのテキスト

        for (const period of TIMETABLE) {
            const startTime = getTodayTime(period.start.h, period.start.m);
            const endTime = getTodayTime(period.end.h, period.end.m);

            if (now >= startTime && now < endTime) {
                // 授業中のとき
                const diffS = Math.round((endTime.getTime() - now.getTime()) / 1000);
                const mins = String(Math.floor(diffS / 60)).padStart(2, '0');
                const secs = String(diffS % 60).padStart(2, '0');
                labelText = `📚 ${period.name} 終了まで ${mins}:${secs}`;
                break; // 該当する時間割を見つけたらループ終了
            
            } else if (now < startTime) {
                // 次の授業開始前の時
                const diffS = Math.round((startTime.getTime() - now.getTime()) / 1000);
                
                if (diffS <= 60 * 60) { // 60分以内ならカウントダウン
                    const mins = String(Math.floor(diffS / 60)).padStart(2, '0');
                    const secs = String(diffS % 60).padStart(2, '0');
                    labelText = `🔔 ${period.name} 開始まで ${mins}:${secs}`;
                } else { // 60分以上前なら開始時刻を表示
                    labelText = `🕒 次: ${period.name} (${period.start.h}:${String(period.start.m).padStart(2, '0')})`;
                }
                break; // 該当する時間割を見つけたらループ終了
            }
        }
        
        // ラベルのテキストを更新
        this._indicator.label.text = labelText;
    }
}

4. ソースコードの解説

このコードは、Gnome Shell拡張機能の基本的なお作法に沿って作られています。

a. Import文 (必要な部品の読み込み)

import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
import GLib from 'gi://GLib';
import St from 'gi://St';
import Clutter from 'gi://Clutter';

Gnome Shell拡張機能特有の書き方です。

  • Extension: 拡張機能の本体(クラス)を作るための基本部品です。

  • Main: Gnome ShellのメインUI(トップバーなど)にアクセスするために使います。

  • PanelMenu: トップバーに表示するボタン(インジケーター)を作るために使います。

  • GLib: Gtkライブラリのコア機能。今回は1秒ごとに処理を行うタイマー (timeout_add_seconds) のために使っています。

  • St (StWidget): ラベル (St.Label) など、UIの部品を作るために使います。

  • Clutter: UI部品の配置(中央揃えなど)のために使っています。

b. 時間割データの定義


const TIMETABLE = [
    // ... (時間割データ) ...
];

function getTodayTime(h, m) {
    // ... (指定した時刻の Date オブジェクトを返す) ...
}

ここはシンプルで、TIMETABLE に授業の開始・終了時刻を定義しています。
getTodayTime は、単純な「時・分」の情報を、今日の日付の「特定の時刻」を表すDateオブジェクトに変換するヘルパー関数です。

c. 拡張機能の本体 (UniversityTimetableExtension クラス)

Extension クラスを継承して作ります。

  • enable(): 拡張機能がONになった時、呼ばれる関数です
    • this._indicator = new PanelMenu.Button(...)
      • トップバーに表示するボタン領域を作成します
    • this._indicator.label = new St.Label(...)
      • ボタンの中に表示する「読込中...」という初期ラベルを作成します
    • Main.panel.addToStatusArea(...)
      • 作成したボタンを、トップバーの右側(ステータス領域)に追加します
    • this._timeout = GLib.timeout_add_seconds(...)
      • 1秒ごとに _updateDisplay() 関数を実行するタイマーをセットします。GLib.SOURCE_CONTINUE を返すことで、タイマーが継続します
  • disable(): 拡張機能がOFFになった時、呼ばれる関数です
    • GLib.Source.remove(this._timeout)
      • enableでセットしたタイマーを停止します。これを忘れると、拡張機能をOFFにしても処理が裏で動き続けてしまいます
    • this._indicator.destroy()
      • トップバーに追加したボタンを削除します
  • _updateDisplay(): 1秒ごとに呼ばれるメインロジックです
    • const now = new Date();

      • 現在時刻を取得します
    • for (const period of TIMETABLE)

      • TIMETABLE のデータを上から順にチェックします
    • ケース1: 授業中 (now >= startTime && now < endTime)

      • 現在の時刻が、ある授業の開始時刻と終了時刻の間にある場合
      • 終了時刻までの残り時間(分・秒)を計算し、「📚 4限 終了まで 30:15」 のような文字列を生成します
    • ケース2: 次の授業前 (now < startTime)

      • 現在の時刻が、ある授業の開始時刻より前の場合(=これから始まる)
      • 開始時刻までの残り時間を計算し、60分以内なら 「🔔 5限 開始まで 08:30」、60分以上前なら 「🕒 次: 5限 (16:30)」 のような文字列を生成します
  • this._indicator.label.text = labelText;
    • 計算結果の文字列を、トップバーのラベルに反映させます

5. デバッグと動作確認

コードを書いたら、動作確認です。Gnome Shell拡張機能は、専用のデバッグ環境を立ち上げてテストするのが良いとのこと。

# 拡張機能のテスト用デスクトップを別ウィンドウで起動
$ dbus-run-session -- gnome-shell --devkit

これを実行すると...
image.png
こんな感じで、現在のデスクトップの中に別のGnomeデスクトップがウィンドウとして立ち上がります。

このテスト用デスクトップ内で、拡張機能の動作を確認します。

ここで、gnome-shell-extension-manager (「拡張機能マネージャー」アプリ) をあらかじめインストールしておくと、拡張機能がエラーで停止した際、簡単にログを確認できるため、デバッグが非常に捗りました。

# 拡張機能マネージャーのインストールはこれ
$ sudo apt install gnome-shell-extension-manager

6. 拡張機能の有効化 (本番反映)

デバッグ環境で問題なく動作することを確認したら、いよいよメインのデスクトップで有効化します。

テスト用デスクトップ (mutter-devkit) で動作確認が完了したら、メインのデスクトップ環境に反映させます。

一番確実なのはPCを再起動することですが、開発中は「Gnome Shellだけ」を再起動する方が早いです。

  • X11 (Xorg) の場合
    • Alt + F2 キーを押し、表示された入力欄に r と入力して Enter を押します。
  • Wayland の場合
    • Waylandでは上記の方法が使えないため、一度ログアウトして再ログインするか、PCを再起動するのが手軽で確実です。(または、gnome-shell --replace コマンドを使う方法もありますが、現在のセッションが強制終了する可能性があるため注意が必要です)

私はWayland環境だったので、PCを再起動して反映を確認しました。
無事、トップバーに時間割が表示されるようになりました!

苦戦したポイント

  • 日本語の情報が皆無
    • Gnome Shell拡張機能開発は、ただでさえ情報が少ない領域ですが、Gnome 49 (Ubuntu 25.10) という最新すぎる環境を選んだため、頼れるのは公式ドキュメント (https://gjs.guide/extensions/) のみでした。すべて英語で、読むのが大変でした
  • GnomeバージョンによるAPIの違い
    • 過去のGnomeバージョンの記事(Gnome 42など)を見つけても、Import文の書き方 (imports.ui.main など古い形式) やAPIが現在 (Gnome 49) と全く異なり、参考にできませんでした。Gnome開発はバージョン追従が非常にシビアだと痛感しました

おわりに

いろいろと苦戦しましたが、基本的には公式ドキュメントのGetting Started (https://gjs.guide/extensions/development/creating.html) を順に読み進めていけば、問題なく進むと思います。

Gnome Shell拡張機能は、https://extensions.gnome.org/ でいろんな方が作ったものが公開されているので、一度使ってみてください。割といろんなことができるみたいで、奥深いな〜と思いました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?