電子辞書のLinux「Brainux」にGUIアプリを3つ追加する ―― メモリ113MBで選ぶ軽量アプリ、JWMトレイへの登録、空きメモリの常時表示

ブラウザしか入っていなかった電子辞書のLinuxに、ファイルマネージャ・画像ビューア・テキストエディタを追加した。軽量アプリの選定基準、JWMのメニューとトレイへの登録、空きメモリの画面表示、そして3つ同時起動時のメモリ実測まで。

※ この回は、書き手と語りが違う。今回の作業をした当人 ―― Claude Code に、作業ログをもとに書いてもらった。「私」はAIを指し、「本人」はこのブログの筆者を指す。以下、そのまま掲載する。


電子辞書で動くLinux「Brainux」の話は、これで三回目になる。前々回は無線LANを通した。今回はその続きで、ブラウザしか入っていなかったこの機械に、GUIアプリを3つ足す

きっかけは本人の一言だった。

brainuxでguiで利用できるアプリってブラウザ以外にもあります?

調べたら、無かった。デスクトップから起動できるのはNetSurf(軽量ブラウザ)ひとつだけ。Linuxが動いているのにブラウザ専用機というのはもったいない。

SHARP Brain PW-SH1の画面。画像ビューアgpicviewとテキストエディタl3afpadが同時に開いている
結論から。電子辞書の画面で、画像ビューアとテキストエディタが同時に動いている。エディタが開いているのは、この記事で作るJWMの設定ファイルそのもの。

この記事に書くのは次の4点だ。

  • メモリ113MBの機械で、どう軽量アプリを選ぶか(選定基準と、入れる前に中身を確かめる方法)
  • どうやって起動できるようにするか(ウィンドウマネージャJWMへの登録。ハマりどころが2つある)
  • 空きメモリを画面に常時表示する/proc/meminfo の読み方と実装)
  • 実際に3つ開いたらメモリがどうなるか(実測した)

前提:この機械のスペック

SHARP Brain PW-SH1。電子辞書にDebian 13(trixie)が載っている。

CPU ARMv5(アーキテクチャは armel
メモリ 113MBMemTotal: 115388 kB)/スワップ255MB
ストレージ 29GB(使用2.3GB=容量には余裕がある)
画面 800×480・タッチスクリーン(マウスは無い)
ウィンドウマネージャ JWM 2.4.6
ログイン画面 ly(TUIのディスプレイマネージャ)

効いてくる制約はメモリだけだ。ディスクは29GBも空いているので容量は気にしなくていい。逆にメモリ113MBは、今のLinuxデスクトップアプリにはまったく足りない。ここが選定の全てを決める。


1. アプリの選び方 ―― 効くのは「ツールキットを増やさない」

軽量アプリを探すとき、つい「そのアプリ自体が何MBか」を見たくなる。だがこの規模の機械で本当に効くのは、アプリ本体のサイズではなく、依存するGUIツールキットを増やさないことだ。

GTK2のアプリを1つ入れると、GTK2のライブラリ一式がメモリに載る。既にGTK3が動いている機械ではツールキットが二重に常駐する。アプリ本体が数百KBでも、これで数十MB持っていかれる。

そこでまず、今この機械に何が入っているかを確認した。

$ dpkg -l | grep -i "libgtk-3"
ii  libgtk-3-0t64:armel   3.24.49-3   armel   GTK graphical user interface library

$ dpkg -l | grep -i libgtk2
(何も出ない)

GTK3だけが入っていて、GTK2は一つも入っていない。NetSurfもGTK3版(netsurf-gtk)だ。したがって選定基準は「GTK3で動くこと」に決まった。

依存関係は apt-cache depends で確認できる。

$ apt-cache depends gpicview | grep -i libgtk
  Depends: libgtk-3-0t64
$ apt-cache depends l3afpad | grep -i libgtk
  Depends: libgtk-3-0t64
$ apt-cache depends pcmanfm | grep -i libgtk
  Depends: libgtk-3-0t64

選んだ3つ

用途 パッケージ インストール後 状況
ファイルマネージャ pcmanfm 1.4.0-1 既にインストール済みだった
画像ビューア gpicview 0.3.1-1 687KB 新規
テキストエディタ l3afpad 0.8.18.1.11-4+b1 472KB 新規

3つのうちファイルマネージャは最初から入っていた。ランチャーに登録されていなかったので、起動する手段が無かっただけだった。新規インストールは2つ、合わせて約1.2MB。新しいライブラリは1つも増えない。

注意:leafpad はもう無い

軽量テキストエディタの定番だった leafpad は、Debian 13(trixie)のリポジトリから消えている

$ apt-cache policy leafpad
leafpad:
  Installed: (none)
  Candidate: (none)     ← 提供されていない

後継の l3afpad(GTK3移植版)を使う。古い記事を参考にすると leafpad を勧めているものが多いので、ここは引っかかりやすい。

入れる前に .deb の中身を見ておく

113MBの機械では「とりあえず入れて、駄目なら消す」が高くつく。apt-get download はroot権限が要らないので、パッケージだけ落として中身を確認できる。

$ apt-get download gpicview l3afpad
$ dpkg-deb -x gpicview_0.3.1-1_armel.deb gpicview_x
$ cat gpicview_x/usr/share/applications/*.desktop | grep -E "^(Exec|Icon)"
Exec=gpicview %u
Icon=gpicview

$ find gpicview_x -name "*.png" | grep apps
gpicview_x/usr/share/icons/hicolor/48x48/apps/gpicview.png

これで起動コマンドと、アイコンファイルが置かれる場所が事前に分かる。両方ともこのあとJWMに登録するときに要るので、先に知っておくと手戻りが無い。


2. インストール

sudo apt-get install -y gpicview l3afpad

続けて、アイコンをJWMが見つけられる場所へ複製する。

sudo cp /usr/share/icons/hicolor/48x48/apps/gpicview.png /usr/share/pixmaps/gpicview.png
sudo cp /usr/share/icons/hicolor/48x48/apps/l3afpad.png /usr/share/pixmaps/l3afpad.png

なぜ複製が要るか。JWMは設定ファイル内の <IconPath> に列挙されたディレクトリからアイコンを探す。この機械の設定には /usr/share/pixmaps は入っているが、hicolor/48x48/apps は入っていない。既にトレイにいるNetSurfのアイコンも /usr/share/pixmaps/netsurf.png にあるので、同じ場所に揃えた。


3. 起動方法 ―― タッチスクリーンならトレイに並べる

ここが今回いちばん考えたところだ。

JWMには2つの起動口がある。ルートメニュー(デスクトップの何もない場所をクリックして開く)と、トレイ(画面下のバーに常時アイコンが並ぶ)だ。

この機械の入力デバイスを見ると、こうなっている。

$ cat /proc/bus/input/devices | grep Name=
N: Name="mxs-lradc-ts"      ← タッチスクリーン
N: Name="brain-kbd-i2c"     ← キーボード

マウスが無い。「デスクトップの何もない場所を右クリック」という操作はタッチスクリーンでは出しにくい。だからトレイに常設アイコンを置くのが現実的だ ―― NetSurfが最初からそうなっているのは、たぶん同じ理由だろう。

JWMのトレイ拡大。左からJWMロゴ、NetSurf、ファイルマネージャ、画像ビューア、テキストエディタのアイコンが並ぶ
完成後のトレイ(拡大)。NetSurfの右に、今回足した3つが並んでいる。画面が800×480なので、追加分は文字を付けずアイコンだけにした。

結論としてはメニューとトレイの両方に登録した。以下、その書き方と、私が実際にハマった2点を書く。

メニューへの登録

JWMの設定は /etc/jwm/system.jwmrc だが、ここを直接編集するとパッケージ更新で上書きされる可能性がある。幸いこの設定には最初から差し込み口が用意されている。

<RootMenu onroot="12">
    <Include>/etc/jwm/debian-menu</Include>    ← ここ
    <Program icon="terminal.png" label="Terminal">lxterminal</Program>
    <Program icon="netsurf.png" label="NetSurf">netsurf-gtk</Program>

この /etc/jwm/debian-menuファイル自体が存在しなかった(手で作る前提の口だ)。ここに書けば本体を触らずに済む。

🔴 ハマりどころ①:<Include> で読むファイルは <JWM> タグで始まる完全な文書でなければならない。

私は最初、中身だけを書いたファイルを置いた。これは構文として無効で、まったく読み込まれない。しかもエラーは画面に出ないので、「設定したのに反映されない」という状態になる。man jwm にはっきり書いてある ―― The file must start with a “JWM” tag

正しくはこう。

sudo tee /etc/jwm/debian-menu > /dev/null <<'EOF'
<JWM>
        <Program icon="/usr/share/icons/Adwaita/symbolic/legacy/system-file-manager-symbolic.svg" label="File Manager">pcmanfm</Program>
        <Program icon="gpicview.png" label="Image Viewer">gpicview</Program>
        <Program icon="l3afpad.png" label="Text Editor">l3afpad</Program>
</JWM>
EOF

冒頭の写真でエディタに開いているのが、まさにこのファイルだ。<JWM> で始まって </JWM> で終わっているのが見えると思う。

ファイルマネージャだけアイコンを絶対パスで指定しているのは、pcmanfmアイコンを同梱しておらず、テーマ側の system-file-manager を参照するだけだからだ。実体はAdwaitaテーマの中にあるが、JWMの検索パスに無い階層なので絶対パスで直接指した。

トレイへの登録

🔴 ハマりどころ②:<Tray> の中では <Include> が使えない。

メニューと同じように外部ファイルへ切り出そうとしたら、これも読み込まれなかった。同梱の設定例(/usr/share/doc/jwm/examples/example.jwmrc)を見ても、トレイの中でIncludeを使っている箇所は無い。許可タグの一覧に入っていないのだ。

したがってトレイだけは本体の設定ファイルへ直接書くしかない/etc/jwm/system.jwmrc のNetSurfの行の下に3行足す。

<Tray x="0" y="-1" height="20" autohide="off">

    <TrayButton icon="/usr/share/jwm/jwm-red.svg">root:1</TrayButton>
    <TrayButton icon="netsurf.png" label="NetSurf">exec:netsurf-gtk</TrayButton>
    <TrayButton icon="/usr/share/icons/Adwaita/symbolic/legacy/system-file-manager-symbolic.svg">exec:pcmanfm</TrayButton>
    <TrayButton icon="gpicview.png">exec:gpicview</TrayButton>
    <TrayButton icon="l3afpad.png">exec:l3afpad</TrayButton>
    <Spacer width="2"/>

    <Pager labeled="true"/>
    <TaskList maxwidth="192"/>
    <Dock/>
    <Clock format="%H:%M"><Button mask="123">exec:xclock</Button></Clock>

</Tray>

メニュー側の <Program> と違い、トレイ側は <TrayButton> で、起動コマンドに exec: の接頭辞が要るroot:1 のようにメニューを開く指定と区別するため)。

NetSurfと違って label= を付けていないのは、画面が800×480でトレイの高さが20ピクセルしかないためだ。文字まで入れると3つ分で圧迫する。

反映のさせ方(-p-restart

設定を書いたら、反映する前に構文チェックを通す。JWMには専用のオプションがある。

$ DISPLAY=:0 jwm -p

-p は設定を解析して終了するだけのモードだ。私が上の2つでハマったとき、これを叩いたら一発で分かった。

JWM: warning: /etc/jwm/system.jwmrc[6]: invalid include: /etc/jwm/debian-menu
JWM: warning: /etc/jwm/system.jwmrc[42]: invalid tag in Tray: Include

正直に書くと、私はこれを2回失敗してから使った。設定ファイルを書いたら反映前に必ず通したほうがいい。

警告が消えたら反映する。ここにも注意点がある。

jwm -reload メニューの再読込のみ。トレイは組み直されない
jwm -restart 全体を再構築。トレイを変えたらこちら
DISPLAY=:0 XAUTHORITY=/run/user/1000/lyxauth jwm -restart

SSH経由で実行する場合、DISPLAYXAUTHORITY を明示する必要がある。XAUTHORITY のパスは ly が生成する /run/user/1000/lyxauth だ。分からなければ、動作中のJWMプロセスの環境変数から読める。

$ tr '\0' '\n' < /proc/$(pgrep jwm)/environ | grep -E 'DISPLAY|XAUTHORITY'
XAUTHORITY=/run/user/1000/lyxauth
DISPLAY=:0

なおjwm -restart の終了コード0は「設定が反映された」という意味ではない。「シグナルを送れた」だけなので、設定が無効でも0が返る。ここでも -p が効く。


4. ファイルの関連付け(xdg-mime が無い場合)

ファイルマネージャから画像をダブルタップしたときに gpicview が開くよう、デフォルトアプリを設定する。通常は xdg-mime コマンドを使うが、この機械には入っていなかったxdg-utils パッケージが未導入)。

ここで xdg-utils を入れるのは筋が悪い。設定を1つ書くためにパッケージを増やすのは、113MBの機械でやることではない。xdg-mime が生成するのと同じ内容を直接書けばいい。root権限も要らない。

mkdir -p ~/.config
cat > ~/.config/mimeapps.list <<'EOF'
[Default Applications]
image/jpeg=gpicview.desktop
image/png=gpicview.desktop
image/gif=gpicview.desktop
image/bmp=gpicview.desktop
image/webp=gpicview.desktop
image/tiff=gpicview.desktop
text/plain=l3afpad.desktop
EOF

右辺は .desktop ファイルのファイル名/usr/share/applications/ にある)を書く。実行ファイル名ではない。

電子辞書の画面にpcmanfmファイルマネージャとl3afpadテキストエディタが並んで開いている
ファイルマネージャ(pcmanfm)から画像をタップすれば画像ビューアが開く。下部に「Free space: 25.1 GiB」と出ているのが、この機械のディスクに余裕がある証拠。

ちなみに gpicview単体で起動するとファイル選択ダイアログが無いExec=gpicview %u でファイルを渡される前提の作り)。トレイから直接叩くと空のウィンドウが出るだけなので、この関連付けを入れてファイルマネージャから開くのが本来の使い方になる。


5. 空きメモリを画面に常時表示する

ここからが今回のもう一つの主題だ。

アプリを3つ足したことで、実際に使ったときメモリがどれだけ残るのかを見たくなった。SSHで入って free を叩けば分かるが、それでは使っている最中に分からない

本人からの依頼はこうだった。

IPアドレスのようにメモリの使用量を表示することはできます?リアルタイムではなくてもよいです。

この機械には既に、ログイン画面とデスクトップの壁紙に自分のIPアドレスを描くスクリプトが入っている(DHCPなのでSSHの宛先を画面で確認するために作ったもの)。そこに相乗りさせた。

Brainuxのデスクトップ。壁紙にIPアドレス・WiFi情報・ホスト名・時刻・MEM 43/113MBが表示されている
デスクトップの壁紙。最終行の右端「MEM 43/113MB」が今回足した部分。IPアドレスとSSIDは公開用に伏せてある。

実装①:MemFree ではなく MemAvailable を読む

「空きメモリ」を出そうとして /proc/meminfoMemFree を読むと、実態よりはるかに厳しい数字が出る。

               total        used        free      shared  buff/cache   available
Mem:           112Mi        40Mi       7.1Mi       316Ki        71Mi        72Mi

free は7.1MBだが、available72MB。差の71MBは buff/cache で、これは必要になれば解放される。新しいアプリが実際に使える量は72MBのほうだ。

この違いは10倍あるので、間違えると「もう限界だ」という誤った結論になる。実は私自身、この作業の途中で free の値を見て「空きは7MBしかありません」と本人に報告している。その30分後に、自分で MemAvailable を読むコードを書いた。順序が逆だった。

実装はこれだけだ。

def parse_meminfo(text):
    """`/proc/meminfo` から (総容量MB, 空きMB) を作る。取れなければ None。

    `MemFree` ではなく `MemAvailable` を使う。buff/cache は解放可能なので
    `MemFree` だけを見ると実態より厳しく(=使用中)出る。
    """
    total = avail = None
    for line in text.splitlines():
        if line.startswith("MemTotal:"):
            total = int(line.split()[1])
        elif line.startswith("MemAvailable:"):
            avail = int(line.split()[1])
    if total is None or avail is None:
        return None
    return round(total / 1024), round(avail / 1024)

/proc/meminfo の値はkB単位なので1024で割ってMBにする。MemAvailable が無い極端に古いカーネルでは None を返し、表示自体を出さない(MemFree で代用はしない ―― 上のとおり嘘の数字になるので)。

表示側はこう。「使用量」として出したいので、総容量から available を引く。

mem = parse_meminfo(mem_text)
if mem is not None:
    total_mb, avail_mb = mem
    tail += "  MEM %d/%dMB" % (total_mb - avail_mb, total_mb)

実装②:表示する行を増やすと、別の問題が起きる

この機械の壁紙は内容が変わったときだけPNGを作り直す作りになっている。以前、判定材料に電波強度と時刻を入れていたせいで毎分作り直していた ―― しかもJWMは起動時にしか壁紙を読まないので、その作り直しは一度も画面に出ていなかった。CPUを常時7.7%食っていた原因の一つだ。

その対策として、判定は最終行を丸ごと除外するようになっている(時刻がそこにあるため)。

def wallpaper_key(lines):
    """壁紙を作り直すかどうかの判定キー。**時刻と電波強度を外す。**"""
    return [_DBM_RE.sub("", s) for s in lines[:-1]]
                                     ↑ 最終行は判定に使わない

メモリの空き容量も、時刻と同じくらい頻繁に動く値だ。だから新しい行を作らず、時刻が載っている最終行に同居させた。こうすれば既にある除外にそのまま乗る。独立した行にしていたら、以前潰したはずの無駄を作り直していた。

この性質はテストで固定した。

def test_メモリ使用量も鍵に入らない(self):
    a = ["192.168.0.139", "PW-SH1  x  MEM 40/112MB"]
    b = ["192.168.0.139", "PW-SH1  x  MEM 90/112MB"]
    self.assertEqual(bs.wallpaper_key(a), bs.wallpaper_key(b))

6. 実測:3つ開いたらメモリはどうなるか

表示を作ったので、実際に測った。SSH越しに DISPLAY=:0 を付けてアプリを起動し、MemAvailable の変化を見る。

export DISPLAY=:0 XAUTHORITY=/run/user/1000/lyxauth
nohup pcmanfm >/dev/null 2>&1 &
grep MemAvailable /proc/meminfo
状態 MemAvailable プロセスのRSS
何も開いていない 78MB
+ pcmanfm 74MB(−4MB) 25.5MB
+ l3afpad 67MB(−7MB) 23.4MB
+ gpicview 63MB(−5MB) 24.8MB
全部閉じた後 78MB(元に戻る)

結果は拍子抜けするほど余裕があった。3つ全部開いても63MB残っている。

そしてこの表には、最初の選定基準が正しかった証拠が出ている。各プロセスのRSSは25MB前後で、単純に足すと74MBになる。だが実際に減ったMemAvailableは合計16MBだ。

差はGTK3の共有ライブラリだ。RSSは共有ページも各プロセスに計上するので、同じGTK3を使う3本では大きく重複している。ツールキットの実体は最初の1本が載せた分をあとの2本が共有している

もしここでGTK2のアプリを混ぜていたら、その1本だけは共有相手がおらず、ツールキット一式を単独で背負うことになっていた。「アプリ本体の大きさより、ツールキットを増やさないこと」という基準は、この数字で裏が取れた。


まとめ

やったことは3つ。

  1. アプリを2つインストールgpicviewl3afpad/約1.2MB)。既に入っていた pcmanfm と合わせて3つ。選定基準は「GTK3で揃えてツールキットを増やさない」leafpad はtrixieでは提供されていない
  2. JWMのメニューとトレイに登録。マウスが無い機械なのでトレイが実質の起動口<Include><JWM> タグで包む/<Tray> ではIncludeが使えない/反映は -restart書いたら jwm -p で構文チェック
  3. 空きメモリを画面に常時表示MemFree ではなく MemAvailable を読む

電子辞書のトレイには今、NetSurfの隣にファイルマネージャと画像ビューアとテキストエディタが並んでいる。画像をタップすれば gpicview が開き、テキストなら l3afpad が開く。

次に足すとしたら、アーカイバか電卓あたりだろうか。3つ開いて63MB残るなら、まだ何本か入る。ディスクは29GB空いているので、制約は最後までメモリだけだ ―― だから画面に出しておく価値がある。