Mozc

Ubuntu + fcitx5 で IME モードをトップバーの色で示す GNOME 拡張を作った(作ってもらった)

Ubuntu デスクトップで日本語入力してると、いま英語モードなのか日本語モードなのかわからなくなることがよくある。トップバーの右端に「あ」とか「A」は出てるんだけど、作業中は目線が画面中央にあるので全然気づかない。

何回も Ghostty の tmux でプレフィックス打って効かないことが続くの嫌になった。心底嫌になった。

macOS だと入力モード切り替え時に画面上に色付きのバーで今のモードを表示してくれるやつがあったのに…

既製品を探した

Claude さんに相談してみて、GNOME Extensions も漁ったけど、ぴったりのものがない。

  • Kimpanel(extension/261): fcitx5 と連携してトップバーにモード表示できる。でも文字表示なので見づらさは変わらない
  • Input-Method Status Indicator(extension/68): IBus 向けなので fcitx5 環境では微妙
  • カーソル近くに表示するやつ: Linux/GNOME には存在しなかった

そんなとき

「無ければ作ればいいじゃんw」

みたいなノリで Claude 様がおっしゃられたので

「まじっすか、それでお願いします!」

って言ったら10分もかからずにできた。すげーなおい。

俺指示したの「色の指定は外出しファイルにしてリアルタイムに反映できるようにしといて」の雑な1行だけ。

そんな雑な指示によって作られたものが以下です。

作ったもの(作ってもらったもの)

fcitx5 が提供している fcitx5-remote コマンドをポーリングして、返り値に応じてトップバーの色を切り替える GNOME Shell 拡張。

~/.local/share/gnome-shell/extensions/ime-indicator@local/
├── extension.js
├── colors.json
└── metadata.json

metadata.json

{
  "name": "IME Indicator",
  "description": "fcitx5 の入力モードに応じてトップバーの背景色を切り替える",
  "uuid": "ime-indicator@local",
  "shell-version": ["46"],
  "version": 1
}

colors.json

色の設定だけを切り出したファイル。ここを編集すると即時反映される。

{
  "jp": "#641414",
  "en": "#000000"
}

extension.js

import { Extension } from "resource:///org/gnome/shell/extensions/extension.js";
import * as Main from "resource:///org/gnome/shell/ui/main.js";
import GLib from "gi://GLib";
import Gio from "gi://Gio";

export default class IMEIndicatorExtension extends Extension {
  enable() {
    this._colors = { jp: "#641414", en: "#000000" };
    this._isJp = null;
    this._loadColors();
    this._watchColors();
    this._updateState();
    this._timeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 400, () => {
      this._updateState();
      return GLib.SOURCE_CONTINUE;
    });
  }

  disable() {
    if (this._timeout) {
      GLib.source_remove(this._timeout);
      this._timeout = null;
    }
    if (this._monitor) {
      this._monitor.cancel();
      this._monitor = null;
    }
    Main.panel.set_style("");
  }

  _loadColors() {
    try {
      const [ok, contents] = GLib.file_get_contents(`${this.path}/colors.json`);
      if (ok) {
        this._colors = JSON.parse(new TextDecoder().decode(contents));
        if (this._isJp !== null) this._applyColor(this._isJp);
      }
    } catch (_) {}
  }

  _watchColors() {
    const file = Gio.File.new_for_path(`${this.path}/colors.json`);
    this._monitor = file.monitor_file(Gio.FileMonitorFlags.NONE, null);
    this._monitor.connect("changed", (_m, _f, _o, eventType) => {
      if (eventType === Gio.FileMonitorEvent.CHANGES_DONE_HINT)
        this._loadColors();
    });
  }

  _updateState() {
    try {
      const [ok, stdout] = GLib.spawn_command_line_sync("fcitx5-remote");
      if (!ok) return;
      const state = parseInt(new TextDecoder().decode(stdout).trim());
      const isJp = state === 2;
      if (isJp === this._isJp) return;
      this._isJp = isJp;
      this._applyColor(isJp);
    } catch (_) {}
  }

  _applyColor(isJp) {
    const color = isJp ? this._colors.jp : this._colors.en;
    Main.panel.set_style(`background-color: ${color};`);
  }
}

ポイント

fcitx5-remote の返り値