この記事を作った動機

 なんか ChatGPT に色々要望を投げたら、とりあえず動くものが自分で理解してなにか作れる前にできたので、まずはそっちに集中しようかなと思いまとめを作ろうとしてみた。

 結局まともに動かせるようにするために、 ChatGPT とのやり取りで出てきた着想を元に、色々調べて自分で組み合わせたり、コードを書いたりすることになった。

前回の記事

機能の概要

 Gnome 拡張機能として、トップバーに全角半角切り替えキーを表示し、押したら、入力モードがアルファベットを直接入力するモードと、日本語をローマ字で入力するモードに切り替えられるようにする。

 主にタッチパネル環境を使って日本語入力するときに、Gnome OSK が US配列しか日本語レイアウトを指定しても表示しないために、補助として作った。

sample画像

 想定された利用する状況としては、以下のようなものがある。

  • GNOME 47 以降
  • タッチパネル運用
  • GNOME OSK を利用する
  • Wayland 環境

 ちなみに X11 環境である場合は、onboardなどすでにある OSK とかを使ったほうがいいかもしれない。

リポジトリ

基本的な動作

  1. pyhton のキーイベントを送る常駐サーバがある。
     (セキュリティの都合により汎用性はなく “`” の日本語キーボードで言う全角半角キー相当のキーイベントしか送らない)
  2. ボタンが押されたら、localhost 経由で Gnome 拡張機能からリクエストを送る

python サーバ

必要なパッケージ

  • python
  • python-uinput
  • python-websockets
yay -S python python-uinput python-websockets

最新版

 最初は、ChatGPT に書いてもらったコードを動かしていたが、次第に HTTP 通信だと限界があることがわかり、 WebSocket へ移行した過程で、結局自分でネットで python のsockets の公式ライブラリなり、色んなサイトを漁ってコードを書くことになった。ChatGPT が書いたコードについては、uinput 以外は、結局いろんなサイトからの試しながら切り貼りといった感じである。

#!/usr/bin/env python3
import uinput
import asyncio

from websockets.asyncio.server import serve
from websockets.exceptions import ConnectionClosedOK

device = uinput.Device([uinput.KEY_GRAVE])

async def hello(websocket):
    while True:
        try:
            message = await websocket.recv()
            if(message == "enter"):
                print("working")
                device.emit_click(uinput.KEY_GRAVE)
            print(f"<<< {message}")
        except ConnectionClosedOK:
            break

async def main():
    async with serve(hello, "localhost", 50096) as server:
        await server.serve_forever()

if __name__ == "__main__":
    asyncio.run(main())

systemd service テンプレート

  • 有効化
sudo systemctl enable --now ZenkakuHankakuKeyd.service
  • ZenkakuHankakuKeyd.service(まだ未完成)
[Unit]
Description=ZenkakuHankaku-key extension backend

[Service]
Type=simple
User=root

ExecStart=/path/to/backend.py
Restart=on-abort

[Install]
WantedBy=multi-user.target
  • 配置
# arch linux
/etc/systemd/system/ZenkakuHankakuKeyd.service

細かいこと

最新版を書く過程で躓いたこと

socket が勝手に閉じないようにする

Part 1 - Send & receive - websockets 15.0.1 documentationによれば、単純に While とかで無限ループしていればいいとのこと。

async def handler(websocket):
    while True:
        try:
            message = await websocket.recv()
        except ConnectionClosedOK:
            break
        print(message)

色々試した形跡

main.py

 以下は ChatGPT-5 のモデルが吐き出したコードである。

#!/usr/bin/env python3
from flask import Flask
import uinput

app = Flask(__name__)

device = uinput.Device([uinput.KEY_GRAVE])

@app.route("/trigger")
def trigger():
    device.emit_click(uinput.KEY_GRAVE)  # press+release
    return "OK\n"

if __name__ == "__main__":
    # listen only on localhost for safety
    app.run(host="127.0.0.1", port=50096)

 このままでも、とりあえずhttp://127.0.0.1:PORT/triggerに wget とかで HTTP リクエストを送ると、キーイベントが OS 側に送られ、バックエンドとしては動作したが、以下のようにエラーが出てしまうので python - Flask at first run: Do not use the development server in a production environment - Stack Overflow を参考に waitress を使うようにすることにした。

  • 警告メッセージが出る例
sudo ./main.py
#  * Serving Flask app 'main'
#  * Debug mode: off
# WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
#  * Running on http://127.0.0.1:50096
# Press CTRL+C to quit
# 127.0.0.1 - - [21/Aug/2025 18:46:34] "GET /trigger HTTP/1.1" 200 -

main.py (修正版)

 以下は自分で修正したコードである。と入ってもシバンを書き換えて、参考資料を元に waitress を使うようにしただけであり、全然私は慣れてなかったり知らないものも使われてたりする。flask とか waitress はこの記事を書いた時点では全然使ったことないし。これでとりあえず警告は出なくなった。

#!/bin/python
from flask import Flask
import uinput

app = Flask(__name__)

device = uinput.Device([uinput.KEY_GRAVE])

@app.route("/trigger")
def trigger():
    device.emit_click(uinput.KEY_GRAVE)  # press+release
    return "OK\n"

if __name__ == "__main__":
    from waitress import serve
    # listen only on localhost for safety
    serve(app, host="127.0.0.1", port=50096)

試行錯誤の過程で不要になったパッケージ

 WebSocket を使う案に移行したため、以下のパッケージは使わなくなった。

  • python-flask
  • python-waitress

Gnome 拡張機能側

ブラウザで標準の API は使えない

 fetch API とかは使えないので、http とかネットワーク関連でなにかしたかったら、libsoup を使う必要がある。fetch API とか使うおうとすると、以下のようなエラーとなる。

sudo journalctl /usr/bin/gnome-shell

# ...
#     Stack trace:
#     _sendBacktick@file:///home/username/.local/share/gnome-shell/extensions/ZenkakuHankaku-Key@www.nyanmo.info/extension.js:32:23
#     _init/<@file:///home/username/.local/share/gnome-shell/extensions/ZenkakuHankaku-Key@www.nyanmo.info/extension.js:55:13
#     activate@resource:///org/gnome/shell/ui/popupMenu.js:193:14
#     _init/<@resource:///org/gnome/shell/ui/popupMenu.js:110:24
#     @resource:///org/gnome/shell/ui/init.js:21:20
#                                               
#  8月 21 19:48:05 hostname gnome-shell[93119]: Failed to claim accelerometer: タイムアウトしました
#  8月 21 19:48:49 hostname gnome-shell[93119]: ReferenceError: fetch is not defined
# ...

エラーで拡張機能がそもそも起動しないときのデバッグ方法

  1. alt + F2 を押す
    コマンドを実行
  2. lg を実行する
    コマンドを実行1
    デバックメニューが開けた状態
  3. Extention タブを開く
    エラーを表示を押す
  4. エラーを表示する
    エラー内容が出てくる

拡張機能を認識させたり変更を反映する

 単純には、ログアウトしてログインしなおしたり、コマンドを実行のところで、restart を実行したりすることで、とにかく Gnome-Shell のユーザセッションを再起動しないといけない模様である。

  • wayland 環境だと restart できないらしい
    waylandではrestartは使えないらしい

画像を使う方法がマゾ

 最終的に以下のように、パスを割り出す方法でとりあえず動くようになった。

        // /home/username までわかる
        let homedir     = GLib.get_home_dir() + "/"
        
        // "ZenkakuHankaku-Key@www.nyanmo.info" 作った拡張機能名に置き換える
        let extdir      = homedir + ".local/share/gnome-shell/extensions/ZenkakuHankaku-Key@www.nyanmo.info/"

        // 拡張機能のフォルダを起点として、画像を指定する
        let iconPath    = extdir + "imgs/targetImage.png"

        // やっと画像を読み込んで表示できる
        let gicon = Gio.icon_new_for_string(`${iconPath}`);
        this.add_child(new St.Icon({
            gicon: gicon,
            style_class: 'system-status-icon switchButton',
        }));

続き

参考にしたサイトとか

フリースペース

コーディング関連で調べたこと

St.Widget (PanelMenu.Button) のシグナルの参考になるところ

GJS と GNOME 拡張機能における画像などのあり方

拡張機能までのパスの解決方法 (今のところ不明)

GJS における HTTP や WebSocket 事情 (libsoup)

python 関連

JS websocket