2022/08/04(木) [n年前の日記]
#2 [ruby][xscreensaver] ruby-gtk2を使ってxscreensaver用スクリーンセーバを作る
Ruby + ruby-gtk2 を使って、xscreensaver用スクリーンセーバを作れそうか試してみた。
一応ざっくり説明しておくと…。
環境は、Ubuntu Linux 20.04 LTS + Ruby 2.7.0 p0 + ruby-gtk2 3.4.1-2build1 + xscreensaver 6.04。ちなみに、Ubuntu Linux は Windows10 x64 21H2 + VMware Player上で動かしてる。
実行結果は以下のような感じ。
 一応ざっくり説明しておくと…。
- xscreensaver : Linux や Mac で利用できる、スクリーンセーバの管理プログラム。
- Ruby : プログラミング言語。作者様は日本人。
- ruby-gtk2 : Ruby から gtk2(GUIツールキット)を制御できるライブラリ。今現在は gtk3 が主流になりつつあるけど…。
環境は、Ubuntu Linux 20.04 LTS + Ruby 2.7.0 p0 + ruby-gtk2 3.4.1-2build1 + xscreensaver 6.04。ちなみに、Ubuntu Linux は Windows10 x64 21H2 + VMware Player上で動かしてる。
実行結果は以下のような感じ。
◎ 参考にしたスクリプト。 :
今回参考にさせてもらった Rubyスクリプトは以下。お天気情報を表示してくれる、Ruby製の xscreensaver用スクリーンセーバらしい。
_zhum/rubysaver: Xscreensaver module, displaying weather, forecast, clock and now playing song title and author.
_rubysaver/rs.rb at master - zhum/rubysaver
_zhum/rubysaver: Xscreensaver module, displaying weather, forecast, clock and now playing song title and author.
_rubysaver/rs.rb at master - zhum/rubysaver
◎ 必要なパッケージのインストール。 :
端末を開いて、以下を打ってインストール。
ちなみに、Ubuntu Linux 20.04 LTS の場合、デフォルトで Ruby 2.7.0 がインストールされているっぽい。
sudo apt install ruby-gtk2
ちなみに、Ubuntu Linux 20.04 LTS の場合、デフォルトで Ruby 2.7.0 がインストールされているっぽい。
◎ ソース。 :
前述のスクリプトから、xscreensaver に絡んでいる部分だけを抜き出して、簡単なサンプルにしてみた。処理内容としては、赤い円がウインドウ内を跳ね回るだけのスクリプト。
_04_xscrsav.rb
chmod +x 04_xscrsav.rb で実行権限をつけて、端末上で ./04_xscrsav.rb としてテスト実行。ウインドウが開いて、赤い円が跳ね回った。これでひとまず、xscreensaver を経由しなくても、アニメーション部分の動作テストや調整はできそうだなと…。
xscreensaver から呼び出せるように、設定ファイル ~/.xscreensaver を編集。
xscreensaverの設定画面を表示して、上記で追加した「RUBYSAVER」を選んでみると、プレビュー画面の中でも赤い円が跳ね回ってくれた。「プレビュー」ボタンをクリックしたら、フルスクリーンでテスト表示された。
これで、ハードルの高い C/C++ で書かなくても、Ruby + ruby-gtk2 で xscreensaver用スクリーンセーバを書くことができそうだと分かった。
_04_xscrsav.rb
#!/usr/bin/ruby
# encoding: UTF-8
require "gtk2"
require "logger"
# for gtk2
SIGNAL_DRAW = "expose_event"
# for gtk3
# SIGNAL_DRAW = "draw"
TMOUT = 16
$drawing_area = nil
$logger = Logger.new("/tmp/rubysaver-#{ENV["USER"]}.log", "monthly")
class RubyXscrApp < Gtk::Window
  def initialize
    super
    set_title "xscreensaver module"
    signal_connect "destroy" do
      $logger.warn "EXIT by destroy"
      Gtk.main_quit
    end
    signal_connect "delete-event" do
      $logger.warn "EXIT by delete-event"
      Gtk.main_quit
      false
    end
    signal_connect "delete_event" do
      $logger.warn "EXIT by delete_event"
      Gtk.main_quit
      false
    end
    realize
    ident = ENV["XSCREENSAVER_WINDOW"]
    if not ident.nil?
      $logger.warn "XSCREENSAVER_WINDOW = #{ident}"
      self.window = Gdk::Window.foreign_new(ident.to_i(16))
      self.window.set_events(Gdk::Event::EXPOSURE_MASK | Gdk::Event::STRUCTURE_MASK)
      x, y, width, height, depth = self.window.geometry
      # puts "window = #{self.window}, (x,y,w,h) = #{x}, #{y}, #{width}, #{height}"
      $logger.warn "window = #{self.window}, (x,y,w,h) = #{x}, #{y}, #{width}, #{height}"
      set_default_size width, height
      move(x, y)
      # set_window_position :center
    else
      x, y, width, height, depth = self.window.geometry
      width, height = 640, 360
      set_default_size width, height
      set_window_position :center
      puts "window = #{self.window}, (w,h) = #{width}, #{height}"
    end
    @x = width / 2
    @y = height / 2
    @dx = width.to_f / (60 * 1)
    @dy = height.to_f / (60 * 1.5)
    $drawing_area = @darea = Gtk::DrawingArea.new
    @bgcol = Gdk::Color.new(0x2000, 0x4000, 0xa000)
    @darea.modify_bg(:normal, @bgcol)
    @darea.set_size_request(width, height)
    @darea.signal_connect SIGNAL_DRAW do |widget, event|
      on_draw widget
    end
    @fixed = Gtk::Fixed.new
    @fixed.put(@darea, 0, 0)
    add(@fixed)
    show_all
  end
  def on_draw(widget)
    cr = widget.window.create_cairo_context
    x, y, width, height, depth = widget.window.geometry
    # update position
    r = 24
    @x += @dx
    @y += @dy
    @dx *= -1 if (@x <= r or @x >= width - r)
    @dy *= -1 if (@y <= r or @y >= height - r)
    # clear bg
    cr.set_source_rgb(0.1, 0.3, 0.5)
    cr.paint
    # draw circle
    cr.set_source_rgb(1.0, 0.0, 0.0)
    cr.arc(@x, @y, r, 0, 2 * Math::PI)
    cr.fill
    cr.destroy
  end
end
Gtk.init
window = RubyXscrApp.new
# animation
GLib::Timeout.add(TMOUT) do
  $drawing_area.queue_draw if (not $drawing_area.nil?)
  true
end
Gtk.main
chmod +x 04_xscrsav.rb で実行権限をつけて、端末上で ./04_xscrsav.rb としてテスト実行。ウインドウが開いて、赤い円が跳ね回った。これでひとまず、xscreensaver を経由しなくても、アニメーション部分の動作テストや調整はできそうだなと…。
xscreensaver から呼び出せるように、設定ファイル ~/.xscreensaver を編集。
programs:                                                                     \
                   "RUBYSAVER"                                                \
                                  /home/USERNAME/prg/ruby/ruby-gtk2/04_xscrsav.rb \n\
                                maze -root                                  \n\
- TAB幅は8文字、TAB文字有効で編集を行う。
- 「programs:」以下に、追加するスクリーンセーバの名前と、Rubyスクリプトのパスを記述。
- 行の最後が「\」なら継続行、「\n\」ならスクリーンセーバ1つ分の記述が終わったことを示す、のだと思う。
xscreensaverの設定画面を表示して、上記で追加した「RUBYSAVER」を選んでみると、プレビュー画面の中でも赤い円が跳ね回ってくれた。「プレビュー」ボタンをクリックしたら、フルスクリーンでテスト表示された。
これで、ハードルの高い C/C++ で書かなくても、Ruby + ruby-gtk2 で xscreensaver用スクリーンセーバを書くことができそうだと分かった。
◎ 問題点。 :
上記のサンプルは、少々問題が残ってる。xscreensaverの設定画面でスクリーンセーバ種類を切り替えた際に、以下のメッセージが表示されてしまう。
「GdkWindow 0xXXXX が予期せず破壊された」的な警告が出ている。おそらく、xscreensaver が強制的にスクリーンセーバを殺してしまうことで、こういう警告が出ているのかなと…。何か正しい終了手順があるなら、対策したいものだけど…。
$ xscreensaver-settings: 06:27:44: XScreenSaver-debug: Name com.canonical.AppMenu.Registrar does not exist on the session bus
Gdk-WARNING **: GdkWindow 0x5000023 unexpectedly destroyed
        from /home/USERNAME/prg/ruby/ruby-gtk2/04_xscrsav.rb:125:in `<main>'
「GdkWindow 0xXXXX が予期せず破壊された」的な警告が出ている。おそらく、xscreensaver が強制的にスクリーンセーバを殺してしまうことで、こういう警告が出ているのかなと…。何か正しい終了手順があるなら、対策したいものだけど…。
◎ 少し解説。 :
xscreensaver は、スクリーンセーバを呼び出す際、「このウインドウを使ってスクリーンセーバの描画処理をするべし」的に、環境変数 XSCREENSAVER_WINDOW に16進数文字列でウインドウハンドルを設定してくれる。
_XScreenSaver Manual
_XScreenSaver FAQ
つまり、
実際、xscreensaver のFAQページには、mpv という動画再生ソフトを呼び出して動画を再生するサンプルが載っているけど、それは環境変数 XSCREENSAVER_WINDOW を mpv のコマンドラインオプションに渡すことで目的を果たしている。
まあ、xscreensaver のドキュメントには、「ウインドウハンドルを渡して処理できる言語やフレームワークはそれほど多くないから、実際にはC/C++で書くことになるだろうね」と書いてあったりするのだけど…。
_README.hacking.edit.txt
幸い、Ruby は環境変数の有無を調べることができるし、ruby-gtk2 は指定されたウインドウハンドルを自身のウインドウとして再設定する機能があるっぽいので、こうして xscreensaver用スクリーンセーバを書けそう、という話になるわけで。
一応、そのあたりの処理を以下に抜き出してみる。
ちょっとハマったのは、ウインドウの表示位置の指定方法。参考にしたスクリプトは、set_window_position :center を呼んで表示位置を決めていたっぽいのだけど、それだと xscreensaver設定画面のプレビュー窓に位置が合ってくれなかった。move(x, y) にしてみたら上手くいった。
さておき。今回、描画処理は、RubyXscrAppクラス内の on_draw() の中でやっている。件のメソッドの中を魔改造すれば違う描画処理になってくれるはず。
アニメーションさせるためには、一定の周期で描画処理を呼び出す指定が必要だけど、ruby-gtk2 の場合、GLib::Timeout.add(ミリ秒) do - end で指定ができるらしい。以前は Gtk.timeout_add(ミリ秒) で指定してたらしいけど、その指定は非推奨になったそうで。
描画は、Cairo::Context を使う方法と、Gdk::Drawable を使う方法の2種類があるそうだけど、今回は前者を使ってみた。ただ、Mac上で動かしたときは、Cairo::Context を毎回破棄しないとおかしくなるらしいので、一応 .destroy を呼んで破棄するようにしておいた。 *1
最初、DrawingArea をどこに登録すればいいのか分からなかったけど、どうやら gtk2 のウインドウに登録してやればいいようだなと…。
Gtk::Fixed は、座標指定で配置するためのWidgetらしい。
_XScreenSaver Manual
_XScreenSaver FAQ
つまり、
- 「環境変数 XSCREENSAVER_WINDOW があるかどうかを調べることができて」
- 「XSCREENSAVER_WINDOW で指定されたウインドウハンドルを使って描画できる」
実際、xscreensaver のFAQページには、mpv という動画再生ソフトを呼び出して動画を再生するサンプルが載っているけど、それは環境変数 XSCREENSAVER_WINDOW を mpv のコマンドラインオプションに渡すことで目的を果たしている。
Q. How do I make XScreenSaver play a video clip?
A. Install mpv and add something like the following to the "programs" preference in your .xscreensaver file:
"Movies" mpv --really-quiet --no-audio --fs --loop=inf \
--no-stop-screensaver --shuffle \
--wid=$XSCREENSAVER_WINDOW \
$HOME/Videos/poop.mp4 \n\
Or, point it at an .m3u file instead: a text file listing your video files, one per line.
まあ、xscreensaver のドキュメントには、「ウインドウハンドルを渡して処理できる言語やフレームワークはそれほど多くないから、実際にはC/C++で書くことになるだろうね」と書いてあったりするのだけど…。
_README.hacking.edit.txt
幸い、Ruby は環境変数の有無を調べることができるし、ruby-gtk2 は指定されたウインドウハンドルを自身のウインドウとして再設定する機能があるっぽいので、こうして xscreensaver用スクリーンセーバを書けそう、という話になるわけで。
一応、そのあたりの処理を以下に抜き出してみる。
    ident = ENV["XSCREENSAVER_WINDOW"]
    if not ident.nil?
      # xscreensaver
      self.window = Gdk::Window.foreign_new(ident.to_i(16))
      self.window.set_events(Gdk::Event::EXPOSURE_MASK | Gdk::Event::STRUCTURE_MASK)
      x, y, width, height, depth = self.window.geometry
      set_default_size width, height
      move(x, y)
    else
      # not xscreensaver
      x, y, width, height, depth = self.window.geometry
      width, height = 640, 360
      set_default_size width, height
      set_window_position :center
    end
ちょっとハマったのは、ウインドウの表示位置の指定方法。参考にしたスクリプトは、set_window_position :center を呼んで表示位置を決めていたっぽいのだけど、それだと xscreensaver設定画面のプレビュー窓に位置が合ってくれなかった。move(x, y) にしてみたら上手くいった。
さておき。今回、描画処理は、RubyXscrAppクラス内の on_draw() の中でやっている。件のメソッドの中を魔改造すれば違う描画処理になってくれるはず。
アニメーションさせるためには、一定の周期で描画処理を呼び出す指定が必要だけど、ruby-gtk2 の場合、GLib::Timeout.add(ミリ秒) do - end で指定ができるらしい。以前は Gtk.timeout_add(ミリ秒) で指定してたらしいけど、その指定は非推奨になったそうで。
GLib::Timeout.add(TMOUT) do $drawing_area.queue_draw if (not $drawing_area.nil?) true end
描画は、Cairo::Context を使う方法と、Gdk::Drawable を使う方法の2種類があるそうだけど、今回は前者を使ってみた。ただ、Mac上で動かしたときは、Cairo::Context を毎回破棄しないとおかしくなるらしいので、一応 .destroy を呼んで破棄するようにしておいた。 *1
最初、DrawingArea をどこに登録すればいいのか分からなかったけど、どうやら gtk2 のウインドウに登録してやればいいようだなと…。
    $drawing_area = @darea = Gtk::DrawingArea.new
    @bgcol = Gdk::Color.new(0x2000, 0x4000, 0xa000)
    @darea.modify_bg(:normal, @bgcol)
    @darea.set_size_request(width, height)
...
    @fixed = Gtk::Fixed.new
    @fixed.put(@darea, 0, 0)
    add(@fixed)
Gtk::Fixed は、座標指定で配置するためのWidgetらしい。
◎ 懸念事項。 :
 Debian Linux において、ruby-gtk2 パッケージは、Debian 11 bullseye の時点までは公式に用意されている。故に、Debian系である Ubuntu Linux 20.04 LTS でもインストールして利用することができたのだけど。この ruby-gtk2 パッケージは、将来的にどうなるかちょっと分からないなと…。
_Debian -- bullseye の ruby-gtk2 パッケージに関する詳細
上記ページを見ると、Debian 9 stretch、Debian 10 buster、Debian 11 bullseye の時点までは公式パッケージとして ruby-gtk2 が用意されていた。ただ、おそらくは Debian 12 になるのであろう bookworm にはパッケージが用意されてないっぽい。sid には用意されているみたいだけど…。
例えば、ここで python3-pygame パッケージ情報を眺めると、こちらは Debian 12 bookworm 用パッケージも用意されているように見える。ruby-gtk2 とは扱いが違う。
_Debian -- bookworm の python3-pygame パッケージに関する詳細
そんなわけで、ひょっとすると次期バージョンの Debian系では、ruby-gtk2 を使って xscreensaver用スクリーンセーバを作成するのは難しくなるのかもしれない。ruby-gtk2 パッケージが無くなっているかもしれないので…。
ruby-gtk3 で書き直せたら、しばらくは安心(?)なのかもしれない。自分は知識が無いのでちょっと無理だけど…。
_Debian -- bullseye の ruby-gtk2 パッケージに関する詳細
上記ページを見ると、Debian 9 stretch、Debian 10 buster、Debian 11 bullseye の時点までは公式パッケージとして ruby-gtk2 が用意されていた。ただ、おそらくは Debian 12 になるのであろう bookworm にはパッケージが用意されてないっぽい。sid には用意されているみたいだけど…。
例えば、ここで python3-pygame パッケージ情報を眺めると、こちらは Debian 12 bookworm 用パッケージも用意されているように見える。ruby-gtk2 とは扱いが違う。
_Debian -- bookworm の python3-pygame パッケージに関する詳細
そんなわけで、ひょっとすると次期バージョンの Debian系では、ruby-gtk2 を使って xscreensaver用スクリーンセーバを作成するのは難しくなるのかもしれない。ruby-gtk2 パッケージが無くなっているかもしれないので…。
ruby-gtk3 で書き直せたら、しばらくは安心(?)なのかもしれない。自分は知識が無いのでちょっと無理だけど…。
◎ 参考ページ。 :
_Ruby で GTK+2 グラフィック - Marginalia
_noanoa 日々の日記 : Ruby/GTK2,GTK3 プログラミング Tips(1)- 基本,ウィンドウ
_noanoa 日々の日記 : Ruby/GTK2,GTK3 プログラミング Tips(2)- 部品の配置
_noanoa 日々の日記 : Ruby/GTK2,GTK3 プログラミング Tips(4)- 画像、アニメーション
_noanoa 日々の日記 : Ruby/GTK2,GTK3 プログラミング Tips(8)- 描画
_noanoa 日々の日記 : Ruby/GTK3を今時のGlade, XML, Builder, CSSで書く8 - 描画
_Rubyでクリエィティブコーディング - Qiita
_Ruby GTK2入門
_[gtk2] "DrawingArea + Cairo" on macOS; crash reports emerge - Issue #1081 - ruby-gnome/ruby-gnome
_A Python screensaver for xscreensaver (Linux) | alvinalexander.com
_noanoa 日々の日記 : Ruby/GTK2,GTK3 プログラミング Tips(1)- 基本,ウィンドウ
_noanoa 日々の日記 : Ruby/GTK2,GTK3 プログラミング Tips(2)- 部品の配置
_noanoa 日々の日記 : Ruby/GTK2,GTK3 プログラミング Tips(4)- 画像、アニメーション
_noanoa 日々の日記 : Ruby/GTK2,GTK3 プログラミング Tips(8)- 描画
_noanoa 日々の日記 : Ruby/GTK3を今時のGlade, XML, Builder, CSSで書く8 - 描画
_Rubyでクリエィティブコーディング - Qiita
_Ruby GTK2入門
_[gtk2] "DrawingArea + Cairo" on macOS; crash reports emerge - Issue #1081 - ruby-gnome/ruby-gnome
_A Python screensaver for xscreensaver (Linux) | alvinalexander.com
*1: でも、Mac なら JavaScript でスクリーンセーバが書けるという話も見かけたのだよな…。だったらフツーは JavaScript で書くよな…。
 
[   ツッコむ ]
以上です。