Gtkなデスクトップ環境でGtkとQtのテーマと格闘する

Gtk環境でのQt

作業環境はManjaro XFceであり、実際の利用環境はCinnamonで、KDE Plasmaは入れていない。

KDE環境だとQtのstyle (QtだとthemeではなくてVisual Styleと呼ぶ。Windows風)は色々入っていたりするのだけども、こういうGtkデスクトップ環境だと古臭いの(ClearlooksとかMotif)とかしか入っていなかったりして、Qtアプリの見かけがだいぶよろしくない。

そこでまず、QtらしいStyleを導入する。 Qt4とQt5のStyleは分かれていて、Manjaro/Archだとkde4やkde5という名前で分けられているようだ。 とりあえず見た限りplasmaパッケージに含まれてはいるものの、KDE本体に依存するようなパッケージはない模様。

Qtのテーマ設定はQt4はqtconfig-qt4、Qt5はqt5ctで行う。

これでKDEとは関係ないけれどQtを使うアプリ(例えばEksterteraとかq4wineとか)の見た目がよくなり、 Ruby qtbindingsも快適に使うことができる。

そう、そもそも今回、ruby-qtbindingsを使ったら見た目がひどかったことからスタートしているのだ。

Gtk2テーマ

Gtk2は恐ろしく鬼門だ。 特に厄介なのが次のアプリ

  • Sylpheed / Claws Mail
  • Mikutter
  • Leafpad

なぜならば

  • Sylpheed / Calws Mailはテーマによっては起動しない。
  • Sylpheed / Claws Mail / Mikutterはスクロールバーがおかしいテーマが多い
  • MikutterはBreezeテーマなどタブの表示が変になるものも多い
  • スクロールバーずれ程度で問題がなく美しいOxygen-GtkはLeafpadはSEGVる

Leafpadを分離するのが良案か。

#!/bin/bash

GTK2_RC_FILES=/usr/share/themes/Adapta-Eta/gtk-2.0/gtkrc exec /usr/bin/leafpad

なお、Gtk3版のl3afpadを使うという手もある。

XFce4は.gtkrc-2.0や環境変数で使用するgtkrcを指定していると、全体的にそれに従う。 外観の設定に左右される範囲よりも大きいようだ。

Oxygen-gtkは良いのだけどなかなか問題がある

  • Gtk3アプリでは表示がおかしくなる可能性が高い
  • Xfce4ではアイコンが反映されなくなる
  • 部分的にUIフォントを頑なに無視する
  • もう開発も止まっているようで今後使えなくなるかも

安定して使えるGtk2テーマがなくなるのはなかなか痛い。見た目はよくはないが、XFceテーマシリーズが安定しているようだ。

Gtk3

Gtk3のほうはもっとシンプル。

Gtk2テーマを指定してXFceを起動すると、Gtk3テーマもそれに従おうとするので、明示する必要がある。

Gtk3も同一テーマで構わないものであれば良いのだが、oxygen-gtkは駄目だ。

if [[ $DESKTOP_SESSION == xfce ]]
then
  export GTK_THEME=Adapta-Eta
fi

しかし、これでもアプリケーションから開くときにGeditを指定するとOxygen-Gtkを使用してしまう。 同様のケースでGeditはfcitxが死んでいる状態で起動したりするので、コマンド名で指定して起動する必要がある。

Cinnamonではデスクトップ設定が反映されるので設定しないほうが良い。

GUIツールキットのお話

今回はRuby qtbindingsを使おうとしたところからスタートした。

RubyでのGUIはまるでスタート廃れたかのような状態にあり、まるでruby-gnome2が唯一の選択肢であるかのようである。 ただ、ruby-qtbindingsは開発は継続しているし、Windowsでも動作するようになっているようだ。

個人的にあまりGtkは好まない。 UIとして使いにくいというのもあるし、設定が複雑でうまく反映されないというような理由もある。 プログラミング上も、QtのほうがAPIは使いやすい。

UIとして優れているのはGtk2だと思う。

Gtk3はメニューが非常に少なく、わかりにくい。 また、ファイルダイアログが劣悪で、ファイルを開くときはまだ良いのだが、ディレクトリを開く場合は非常に辛い。 というのは、何もファイルを選択しない状態で入力を開始した場合のみキーボードからのパス入力を受け付けるが、 Enterを押した時点で「開く」動作をする。 そのため、ディレクトリを開く動作のときには最後まで入力するか、クリックするかしか選べない。

Qt4/Qt5のダイアログはファイル名としてディレクトリパスを入力することもできるが、やはりディレクトリを開く場合には一発で開かれてしまう。 さらに登録される場所がさらに限定的となり、Gtk3よりなお悪い。 なお、XFce4ではQtのファイルダイアログをGtk3のものに置き換えるようだ。

Gtk2のファイルダイアログではファイルリスト側にフォーカスされていればWindows同様にインクリメンタルサーチとなり、 ファイル名欄が常に存在しディレクトリに対して入力を行うとまずディレクトリに移動するという振る舞いとなっている。 これはおそらく最も適切である。 (だが、これも場合によるようで、Qt4/5のような振る舞いをする場合もある)

API的に、あるいは設計的に見ればGtk2からGtk3、Qt3からQt4というのは大幅に進化している。 実際に「よくなった」のだ。

だが、現状でGtk3、あるいはQt5の熟成度というのは全く足りていない。 Qt4からQt5はある程度の互換性を持った状態で進化している。 その進化はわからないでもない。コンピュータの進化に合わせて、あるいは周辺環境の進化に合わせてコンポーネントを作り直した。 その際にいくつかのいまひとつな設計を修正し、テーマデザインにフラットデザインを推奨した。

だが、そのような変更は必要だったのか疑問だ。 内部的な進化や、標準テーマデザインの変更に関しては互換性を維持しての改修でもよかったのではなかろうか。

期間的にGtk3やQt5は日が浅い。 Gtk3のコンセプトが不評ということもあるが、2011年にGtk2は開発終了しているにも関わらずGtk3への移行が進んでいないプロジェクトは多い(XFce4もそのひとつだろう)。 現状ではGtk2を採用するプロジェクトもGtk3への移行を模索する時期にきているが、Gtk3に移行できているプロジェクトはまだ少ない。 Qtに関しては各プロジェクトが苦労してQt3から移行を完了し、あるいは移行できなかったプロジェクトは廃止されているが、 Qt4を採用しつづけるプロジェクトはまだ少なくない。

この状態でさらに非互換性を持つGtk4やQt6へ移行する必要があるのだろうか。 いずれのツールキットもより熟成すべきであるように私には思える。

また、GUIプログラミングも以前は盛んでwxwidgetなどが流行したときもあったのだが、早々に廃れてしまった。 WXWidget自体は現在も開発が続けられているが、wxrubyは既に終了しており、rwxは十分に開発・テストされていないようだ。 RubyにおけるGUIプログラミングの選択肢は非常に乏しく、qtbindingsも将来的には不透明だ。

さらに、Gtkのその問題点から開発者が離れている。 これはGtkの問題点というよりも、Gnomeの方針に開発者がついてきていないという問題なのだが、 GnomeはGtk4においても方針を維持しており、今後もこの傾向は続くだろう。

ユーザー側にはあまり見えない状況ではあるが、実はLinuxのGUI周りはかなり不安定な状況にある。 個人的にはQtがより普及し、バインディングが安定していてくれると良いのだが…

MailDeliver2に「確認できる着信通知」

動機

Cinnamonの通知機能は使いにくい。

通知はずっと残すか(どんどん増えて超重くなる)、表示が終了したら消すかしかない。
メールの着信通知をNotify Sendで出しているのだけれども、見逃していまうと確認しようがないと、通知が常時きてるような環境ではその時に見られないので、まず確認できない。

着信に気づいてから確認したいのだが、その機能がなかった。
そこで、かねてからの希望だった、「Systrayに常駐し、クリックするとこれまで受け取ったメールが確認できる」という機能を追加することにした。

30分くらい、と思ったが5時間ほどかかった。

苦戦

まずSystrayをどう実現するかだが、yadを使った。
Zenityのforkらしいのだけれども、なんかだいぶ違う。

yadをどう使うかというのは随分悩んだ。
yadの--commandではクォートがエスケープされてしまうため、スペースで必ず分割されてしまう。そのため、置き換えが発生する部分は別にくくりだすしかない。
--listenで実行するコマンドを随時更新できるが、ここでも同様の問題があるし、そもそも標準入力から渡す方法というのは結構限られる。

結局3つに分割している。データ整形用のスクリプト、yadのcommandになるスクリプト、そしてyad自体を起動するスクリプトだ。

当初、4つだった。一時的に保存され、通知と共に消されてしまうヘッダをとっておくためのプラグインだが、不毛なので(そしてそれが必要になるプラグインはおそらく多いので)Maildeliver本体に組み込まれ、LeaveHeadersという設定を可能にした。

時間がかかったのは細かいミスが重なったわけで、未知のエラーには遭遇していない。

いずれにせよ、これでメールを遡って確認できるようになった。ダイアログのOKでクリア、cancelで保持だ。

Yadの起動をRubyで行っている理由は結構単純で、複雑なエスケープなしにスペースやspecial characterを含むエントリを分割するために行志向にしていて、行単位でargを与えるのがRubyだと楽だから。

Yadのspecial character

結局yadでなくZenityを使っていたのは、Yadはエントリーに対して結構複雑なエスケープを必要とするためで、とりあえず手っ取り早い方法としてである。

簡単にいえば、Yadの--listでエントリが表示されなかったり、ひとつ上の内容が表示されたりしていた。
ドキュメントにないので自力で試した限り

  • <または&が含まれる場合、そのエントリはテーブル的に上の項目をクローンする
  • エスケープ方法はXML(&lt;及び&amp;)
  • ところが&rt;には対応しておらず、&が含まれるためクローンされてしまう

こういうわけのわからない仕様はやめてほしい。

PureBuilder2 TopicPath機能の追加

変更点

最新の変更@Gist

概要

これまでTopicPath機能はPureBuilderで提供されて来なかった。
そのため、個別のドキュメントとテンプレートにおいて実装可能な機能として紹介されてきたが、一般的な要望であるため今回の変更で取り込んだ。

このTopicPath機能は整形されたHTMLを返すわけではなく、シンボルと文字列からなる配列を返す。
PureBuilderの本来の設計に従い、サイトの階層構造に等しいディレクトリ構成を採用し、かつドキュメントごとにTopicPathが固定される状態であれば、かなり楽にTopicPathが生成できる。

これまではTopicPathはドキュメントあたりで設定することが勧められていた。これは、ACCS indexが組み込みで機能するためだ。

この機能を使えば設定ファイルによって、そのディレクトリの親パスを定義し、そして文書タイトルがStringとして追加される。

詳細

つまり、ディレクトリで設定されているのが

[ :Foo, :Bar ]journal/

で、文書タイトルがBazであるならば、

[ :Foo, :Bar, "Baz" ]

となる。

別にシンボルである必要はない。だが、シンボルを推奨している。マップを使用することが推奨されているためだ。
だが、特にシンボルであることを期待しているコードは組み込まれていない。

だから、例えば

[ { Address: "http://example.com/foo/", Title: "Foo" }, "Baz ]

のような構造にしても構わない。

実際にそれを使うコードとしては

% tp = DOC.mktopicpath
% tp.each do |i|
%   if i.kind_of?(String)
        <li><%= i %></li>
%   else
        <li><a href="<%= 	DOC.pbenv[:TopicPathMap][i][1] %>"><%= DOC.pbenv[:TopicPathMap][i][0] %></a></li>
%   end
% end if tp

のようになる。

ただし、Indexの場合はディレクトリで定義されている階層は今いるページなのであるから、その場合はtitleとpathの最終エレメントは重複しているはずだ。

そこで、:Indexが定義されている場合は、最終エレメントを取り除くことにした。

過渡期のコード

タイトルは"title"なのか"Title"なのか:Titleなのか、といったところに揺れがある。

正式には"title"を使用することになっている。だが、おそらくはreservedなキーは大文字で始めることにしたほうがいいだろう。

この互換性を維持するコードにしてある。

また、すでにページあたりでTopicPathを設定している場合に備え、現状はdevelブランチのみの対応だ。


途中で放置したため、書くべきことを忘れました。
ゴメンナサイ。

PureDocとKramdownのhn要素のidを統一する

「Markdownで書いたページがTOCのリンクが切れている」という問題に気づき、急遽対処を行った

問題は、Kramdownが生成するヘッダーIDが、単純にテキストを用いるものではなかった、ということだ。

KramdownはどうしてもIDにACSIIのみを使うようになっており、そのためになかなか複雑な処理をしている。

これを統一するため、Kramdownのこの処理をしているところを探したところ、base.rbにあるKramdown::Converter::Base#generate_idであることがわかった。

これを、常にテキストを用いるようにオーバーライトする。ほんとはオーバーライドしたかったのだが、Kramdownの構造的にそれは割と難しい。

PureBuilderのコードをいじれば、一応動くようにはなる。これは、そのままのテキストを使うようにして、PureDocと揃えたためだ。

Gist

PureDocのほうも複数の形式をサポート。その中でデフォルトは今までどおりテキストを使う形式だが、Kramdownに合わせ、重複した場合の対処を加えた。

これによってテキストだけではIDを判断できなくなったので、IDもTOCの中に含めるようにした。

Gist

サイト構築で画像関連機能

ウォーターマークをいれた画像を半自動で用意する

Gist

ウォーターマークのベースはさすがに自動生成できるレベルにはなかったので、Inkscapeで作成した。
縦と横の二種類を用意している。

fig/にはオリジナルサイズの、thumb/にはサムネイルサイズの画像を配置するが、この時にcompositeを使ってウォーターマークを合成している。

また、幅1600pxを越える画像はリサイズしている。

@figbaseuriの不完全さ

PureDocにもわずかな変更を加えたが、これはattrを排除するためにすぎない。

大きいのはParser::PureDocにおける

        # Other settings for PureDoc
        begin
        if config[:purebuilder_config][:puredoc_tune].kind_of? Proc
          config[:purebuilder_config][:puredoc_tune].call(::DOC)
        end
        rescue
          STDERR.puts "!!ERROR in PureDoc TUNE:" + $!
        end

という変更だろう。

これによって、@config[:puredoc_tune]を使って各PureDocオブジェクトに対して設定が可能になった。
これはMarkdownドキュメントに対しては無効となる。

Gist

「各article要素を取得してループ→各要素に対して各figure要素を取得してループ→イベントリスナー追加」という単純な構造ではある。

ライトボックス用に用意されたimg要素のsrcを変更し、さらに最大幅と最大高をウィンドウサイズに合わせてから表示する、という処理が追加され、ちゃんと機能するものになった。

figure要素に入っていないものはサムネイルの構造を持っていないとみなす方式。

Paiza プログラミング彼女の解法と不満

Paizaで展開している、「プログラミング彼女」というサブコンテンツ。

めがね

Paiza B級。

ビットマップマッチングだが、値は

0 1 1 1
1 0 1 1
0 1 0 0
0 1 1 0

のような文字列で与えられる。マッチングパターンも同様だ。

結構馬鹿馬鹿しい。

その1

ip = []
tp = []

gets.chomp.to_i.times {|n| ip << gets.delete(" ") }
gets.chomp.to_i.times {|n| tp << gets.chomp.delete(" ") }
tp[0] = Regexp.new(tp[0])

ip.each_with_index do |ips, ipi|
  ips.scan(tp.first) do
    offset = $`.length
    if tp[1 .. -1].each.with_index.all? do |tpp, tpi|
                                          ip[ipi + (tpi + 1)][offset, tpp.length] == tpp
                                        end
      print "#{ipi} #{offset}"
      exit 0
    end
  end
end

これが通過しなかった。

手元でテストする限り、これで通過しないケースは発見できなかったのだが、case4で0.11secでこけてしまう。

これは納得できないところだ。

文字列の正規表現マッチングを行っているため、かなり魔術的に見えるだろう。
しかし、実はそうでもない。

「文字列」というが、Rubyの場合は文字列はバイト列も含む。
バイナリ検索にしてもバイトパターンのマッチングは普通なので、別にそのために文字列検索を行ってもなんら問題はない。

本来は多重配列を用いたマトリックスを求めていたのだろうけれども、Rubyの能力を考えれば配列よりも文字列のほうがはるかに扱いやすいし速いので、ちょっと魔術的でもこちらのほうが良いと考えた。

その2

matched = Array.new
base = Array.new
target = Array.new

gets.chomp.to_i.times { base << gets.chomp.delete(" ") }
gets.chomp.to_i.times { target << gets.chomp.delete(" ") }


target.each do |ts|
  matched << ( l = Hash.new )
  base.each_with_index do |x, i|
    o = -1
    while o = x.index(ts, (o + 1))
      l["#{i} #{o}"] = [i, o]
    end
  end
end

print(matched.first.keys.select do |k| 
  y, x = *matched.first[k]
  matched.length.times do |n|
    unless matched[n]["#{y + n} #{x}"]
      break nil
    end
  end
end.first)

ダメだと言われたので改修してみた。
各パターンにおいてマッチするメトリックをハッシュに格納し、マッチ側パターンの行数縦に連続するものをマッチとみなす。
速度的にも効率的にも悪くない。これで通過

水着

Paiza A級。

階乗の、末尾0を除いた末尾9桁を出力せよ、という問題。
本来はInteger Overflow対策が求められるのだろうが、RubyはBignumがあるため、そこは不要。

しかし、実効速度に伴うタイムアウトという壁が立ちふさがった。

その1

def fact(n)
    if n <= 1
        n
    else
        n * fact(n - 1)
    end
end

base_n = gets.chomp.to_i
ret_n = fact(base_n).to_s
ret_n =~ (/(.{0,8}[^0])0*$/)
print $1.sub(/^0*/, "")

理論上はOKで、実際テストではOKだったが、Case3でコケる。
おそらくはコールスタック枯渇。

その2

num = (1 .. gets.chomp.to_i).inject {|x, sum| x * sum }
print num.to_s.sub(/0*$/, "")[-9 .. -1]

コールスタックが枯渇しないように、Enumerable#injectを使ったループに変更した。
だが、Case4で15.00secで失敗する。おそらくはタイムアウト。

その3の1

print((1 .. gets.chomp.to_i).inject {|x, sum| (x * sum).to_s.sub(/0*$/, "").to_i % 1000000000 })

タイムアウトを避けるため、常に9桁に抑制することにした。

このコード、計算結果が1000000000だった場合に、商余が0になってしまうため失敗するが、事前に0を除いているために起こりえない。

だが、これはあっさり失敗してしまう。
手元では通過していたため、失敗理由がわからなかった。

その3の2(通過)

print((1 .. gets.chomp.to_i).inject {|x, sum| (x * sum).to_s.sub(/0*$/, "").to_i % 1000000000000 } % 1000000000)

計算桁を増やして最後に9桁に落とすようにした。
これで通過したが、計算桁はなんとなく増やしているだけなので、理論的には失敗しうる。

その3についての検証

r = (1 .. gets.to_i).inject([1, 1]) do |sum, x|
  puts "---" + x.to_s
  puts( a = (( x * sum[0] ).to_s.sub(/0*$/, "").to_i % 1000000000) )
  puts( b = (( x * sum[1] ).to_s.sub(/0*$/, "").to_i % 100000000000) )
  sum = [a, b]
end

puts "**ANSWER"
puts r[0]
puts r[1] % 1000000000

puts "*****"

p(137921024 * 50)
p(83137921024 * 50)
p((137921024 * 50).to_s.sub(/0*$/, "").to_i)
p((83137921024 * 50).to_s.sub(/0*$/, "").to_i)
p((137921024 * 50).to_s.sub(/0*$/, "").to_i % 1000000000)
p((83137921024 * 50).to_s.sub(/0*$/, "").to_i % 100000000000)

なんとなく想像はついたが、検証してみた。

!50の結果が変わってしまう。これは、!49の時点では

137921024
83137921024

と9桁/11桁で正しいが、!50になると

68960512
41568960512

と8桁/11桁になってしまう。0を除く前の時点で

6896051200
4156896051200

1桁増えているが、0が末尾に2つ増えており、0を除くことで2桁減る。結果として1桁減ってしまうことになる。

計算を続けていくと、この誤差は消滅するが(桁上がりすれば良い)、与えられた数によっては失敗することになる。
これを考慮すると正しくは

print((1 .. gets.chomp.to_i).inject {|x, sum| (x * sum).to_s.sub(/0*$/, "").to_i % (1000000000 * (10 ** ($&.length.zero? ? 1 : $&.length) ) ) } % 1000000000)

とすれば良いはず。

Paizaへの不満

条件が予測できない

条件は一応書かれてはいるが、それ以外の条件が多すぎる。

コールスタックの枯渇は予測できるからいいとしても、タイムアウトという条件は記載されていないし、めがねのその1が失敗する理由もわからない。

これで技術を評価されるというのはとても不満だし、だいたい大した機能もない開発環境で、妥当性検証の方法も単一のサンプル、それもどのような入力かを明確にしないままであり、入力の仕様も(業界では一般的な形式なのかもしれないが)非常にわかりにくい。

デバッグできない

失敗した場合でもなぜ失敗したのか分からない。そのためデバッグすることができない。

提出前に失敗した場合でも、全体出力を見ることができず、かつデバッグせずに提出することを前提としているためデバッグはかなり難しい。

私はこうしたことは不毛だと思っている。プログラムはちゃんと磨き上げて動くコードにするものだろう。そして間違いなく動くことが確認されて、はじめてリリースだ。

確認もデバッグもできないままということで、一度しか提出チャンスがないということにも強い違和感を覚える。

いつからこんなつまらない業界になったのか

これはPaizaに限った話ではないけれど、IT業界が、まず履歴書、そして業務履歴書なんてつまらない業界になってしまったのはいつからなんだろう。

少なくとも、私がお呼ばれした時には能力さえあればいいという空気があった。その時点でコンピュータを使うこなせる人間なんて変人が多かったし、型なしの掟破りという印象が強い。

だが、近年は、他業界よりもさらに「即戦力」ばかり求められ、経験がなければやらせないというところばかりで、「経験はどこでつめますか?」という状態。
結局そのために経歴の捏造が横行しているわけだ。

年齢も経歴も関係なしの実力次第の世界だと思うのに、そこまで経歴に固執するのは一体なぜなのだろう。

折角、コードを直接みることができるPaizaであるにも関わらず、そのフォーマットを要求するのはいかがなものか。さらに「趣味/1年未満」というくくりは一体なんなのか。
そして、定型的な業務におけるカテゴリしか用意していないのもなんなのか。実際はもっと多様で、能力とは直結していないはずなのだが。

PureBuilder2を使ったリニューアル(1)

PureBuilder1のサイトがPureBuilder2で動作するように調整すると共に、PureBuilder2の機能を検証する作業を開始した。
現在はサイトの更新が滞っているので、それが完了すれば効率的に更新できるようになる。

ただ、従来は設定ファイルにロジックを組み込む形だったため、変更点は大きく、そのまま持ってくるというわけにはいかない。
そもそも、PureBuilder1が十分なツールでなく、ツールをサイトごとに書いて完成というものだったのが問題なのだが。

まず、PureDocドキュメントはそのまま持ち込むことができる。
従来の.rebuild_rulesファイルや、ReasonSetにおけるglobal.rcファイルに変えて、.purenuilderrc.rbを用意することとなる。

おおよそそれでいいのだが、既にrcファイルで組み込んでテンプレートで利用していたような情報は.purebuilderrc.rbファイルに組み込まなくてはいけない。
また、これによって従来環境変数で処理していた物を、DOC.pbenvを使うように整合性をとらなくてはいけない。

このデバッグ作業に伴って、Purebuild Allに.rebuild_all_timeを無視して全てを対象とする-cを追加した。

そして、問題になったのがプロフィール部分だ。

これまで、プロフィールは単独のRubyスクリプトだった。rebuild_rulesから呼ばれることを爽亭しているため、設定は環境変数を通じて受け取ってはいるが、ドキュメントの生成、テンプレートへの埋め込み、そしてファイルの出力までプロフィール自体で行っていた。

しかし、これをそのままにできるわけではない。
設計上、これをPureDocであるとみなした処理したほうが早い。
だが、問題として、設定はあくまでもドキュメントを生成したあとに使えるようになっているため、ドキュメント内で使うことができない。PureDocを拡張してPureBuilder向けスクリプトにすることがかなり難しいのだ。

DOC内の@bodyStringまたはArrayという扱いだったが、実際にStringだと参照時に@body.joinを呼ぶためエラーになってしまう。この点については、PureDocのほうを修正することとなった。

こうした拡張がいまいちしづらいことを考えると、修正が必要かもしれない。
また、既存のPurebuildスクリプトを用いるのではなく、DSLによってビルドを指示できる仕組みも追加すべきだろう。

FM2+KillerのLinuxセットアップ: 既存のManjaro Linux (Arch Linux系)環境をクローンする

あらまし

元々、メインのLinux環境はFM2+Killerだったが、Z400に移行したことで位置づけが変わった。

メインの作業環境はZ400上にあり、既に構築済みだ。
FM2+Killerは冗長環境として、同様にLinuxを構築し、いつでも使えるようにしておくのが望ましい。

可能であれば、ハードウェアも全く同じものを用意すれば、簡単にクローンできる。
だが、Z400とFM2+Killerでは色々と違いがある。特に大きいのはグラフィックスカードの種類と、ディスプレイの数の違いだ。

さらに、FM2+Killerは

  • 障害時にメインと同様に使うことができるアカウント
  • サテライト的に使う一時作業用アカウント
  • 彼女がうちにいる時に作業に使うアカウント

の3つをセットアップする

インストール

私が使っているのはManjaro Linuxである。当然ながら同じManjaro Linuxを導入する。

既にManjaro Linux 15.09がリリースされているが、15.09はUEFIにインストールすることができないバグがある。特に

0.8.13 XFceをインストールし、アップグレードする。
違いはあとから埋める。

通常どおり、yaourt -Syuuでアップグレードした上で、新しいカーネル(4.1)を導入する。4.2でないのは、AMDユーザーに勧めない、ということなので。

インストーラで作るユーザーはメイン環境と同じユーザーにすること。
でないと、UID/GIDの違いでディスクを差し替えただけでは動かなくなる。

パッケージを揃える

メイン環境と同じパッケージが入っていればもし作業環境を作るにしても、少ない手間で可能だ。

Arch Linuxにパッケージを揃える機能はなさそうだったので、スクリプトを書いた。

#!/usr/bin/ruby
# -*- mode: ruby; coding: UTF-8 -*-


PLIST = File.exist?(".previous_autoyaourt_target_list") ? File.open(".previous_autoyaourt_target_list", "r").each.map {|i| i.strip[/^\S+/] } : nil 
TLIST = ARGF.each.map {|i| i.strip[/^\S+/] }
EXCLUDE = [ /nvidia/i, /^linux/, /^libva/, /catalyst/, /maxthon/, /^opencl/, /^ocl-/, /^libcl/ ]

File.open(".previous_autoyaourt_target_list", "w") {|i| i.puts(TLIST) }


loop do
  clist = IO.popen("pacman -Q", "r") { |io| io.each.map {|i| i.strip[/^\S+/] } }
  to_install = ( TLIST - clist ).delete_if {|i| EXCLUDE.any? {|r| r === i } }
  
  if to_install.empty?
    break
  end
  
  system("yaourt", "-S", *to_install[0, 15]) or abort "Yaourt failed!!!"
end

if PLIST && ! ( dlist = PLIST - TLIST ).empty?
  
  puts "*******************************************"
  puts "CLEAN UP PHASE"
  puts "*******************************************"
  
  
  
  system("yaourt", "-R", *dlist)
end

Manjaro Linuxは標準でyaourtは入れているが、Rubyが入っていない。
Rubyをインストールし、元となる環境で

pacman -Q > target-paclist

のようにした上でこれを持ってくる必要がある。そして

ruby yaourtsyncer.rb target-paclist

のようにするわけだ。

--noconfirmオプションはつけていない。問題が発生することがあり、またひとつずつ確認したほうが安全だからだ。

別にパッケージデータそのものを持ってきて(/var/cache/pacman/pkg/以下にある)インストールする方法もあるのだが、今回は1台クローンするだけの話だし、健康にビルドしていく。

事前にsudoのタイムアウトを外しておいたほうが良いが、visudoはあるのにviがない。先にviをインストールしておく。gvimをインストールしてリンクしておいても構わない。

もし手間を短縮するなら--noconfirmをつけてもいいのだが、いずれにせよ手をかけねばならない状態になったり、失敗した時にいちいち外したりしないといけなかったりする。

これでうまくいかないのが、旧リポジトリからいれているパッケージ、失われたパッケージ、壊れてしまったパッケージだ。

旧リポジトリから入れているのがxcursor-lcd-*、なくなってしまったのがjoyutils、などなど。
これらはmakepkgでビルドしたものに関してはパッケージを持っているだろうし、pacmanやyaourtで入れたのなら/var/cache/pacman/pkg以下にある。これを導入する。

グラフィックスドライバに関連するものについては個別の環境によるため、除外している。
カーネルは元々yaourtでいれるようにはなっていないので、これも除外。

ちなみに、fcitx-mozc-utやJava関連、inkscape-gtk3-bzrはビルドが非常に長いので流用したほうが良い。

ユーザー

残り2つのユーザーを追加し、設定する。

これらユーザーはこのコンピュータのローカルなものなので、好きなように設定して構わない。

サテライト環境ではsshfsを用いてマウントすることで、UID/GIDの差を吸収できる。

冗長環境

当然ながらユーザーの設定も、元のPCに合わせたものにしたい。

私の環境ではホームディレクトリの下のディレクトリにbtrfsサブボリュームがマウントされており、また別のディレクトリがEncFSのマウントポイントにもなっている。

単純に持ってきてしまうと、btrfsの膨大なデータをコピーしてしまうし(5TB近い)、EncFSのデータを復号化したまま持ってきてしまう。

これを避けるため、マスター環境のサブボリュームマウントポイントを同様に作り、sshfsでマウントし、

$ rm -rf (^(.mountpoint))(#qD)

して

$ rsync -avH -x --exclude=/.cache -e ssh "$HOSTNAME":./ ~/

これはなるべく、コンソール上で作業したほうがよい。rootでは、FUSEを使うため支障がある。

~/.cacheは特に同期する必要がなく、同期するととても時間がかかるので省略。--delete-afterなどはとても危険。

私の場合、~/.sshがシンボリックリンクなので失敗する。
この状態で削除とリンクを同時に行えばよい。リンク自体は別にsshfsを解除していても-fオプションで可能。

なお、これで気がついたのだが、こうしてまっさらにしてコピーしても、Cinnamonの壁紙とテーマはなぜか反映されず、ローカルの設定が保たれる。ローカルで設定した後に吹き飛ばしてもだ。

なお、このテストの家庭で全データを吹き飛ばしかけた。
幸い気づくのが早く、重要なデータの損失はなかった。

rsyncのファイルシステムをこえない-xオプションは非常に便利。

なお、autostart関連は除外したかったのだが、うまくいかなかった。
これは--excludeの書き方の問題。

Markdownで書かれたスライドをHTMLとポータブルPDFと印刷用PDFにする

Aki SI&EのPR用に書いたものだが、Markdownで書き、Web(HTML)、配布用PDF、印刷用PDFの3種類を生成する考えでいた。

手でやろうかと思っていたのだが、再生成の回数も多そうだったので、自動化できるようにした。

Markdownで記述したドキュメントと、オリジナルサイズの画像ディレクトリ、mogrify -geometry x300でリサイズした画像ディレクトリが用意されている。
Web用のものについてはHTMLで出力し、リサイズされた画像のWebサーバー上のコピーを参照しなければならない。

PDFについては、MarkdownをLaTeXに変換してという方法もあるにはあったが、画像を含めた細かなデザインを施したかったため、HTMLをベースにPDFに変換することにした。

MarkdownコンバータはRubyのKramdownを使用。改ページはないため、wkhtmltopdfを用いてPDFに変換することとした。
参照しているディレクトリの違いで2回ループを回す。

それぞれHTMLは別に出力し、テンポラリファイルとして出力したHTMLを元にPDFのテンポラリファイルを生成、unitepdfを使ってこれを結合する。

専用のコードなので多くをハードコーディングしているが、これで正常に機能する。

#!/usr/bin/ruby
# -*- mode: ruby; coding: UTF-8 -*-

require 'kramdown'
require 'erb'

def treat_md(file, findex)
  $article_body = doc = Kramdown::Document.new(File.read(file)).to_html
  
  $article_title = doc[/<h1[^>]*>(.*?)<\/h1>/, 1]
  
  foot = Array.new
  if findex > 0
    foot << '<a href="' + ( SOURCEFILES[findex - 1].sub(/\.md$/, '')  + ".html" ) + '">前へ</a>'
  end
  
  foot << '<a href="/si/" target="_top">Aki SI&Eトップページへ</a>'
  
  if findex < ( SOURCEFILES.length - 1 )
    foot << '<a href="' + ( SOURCEFILES[findex + 1].sub(/\.md$/, '')  + ".html" ) + '">次へ</a>'
  end
  
  $article_footer = foot.join
end


TEMPLATE = <<'END'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" http://www.w3.org/TR/xhtml1/dtd/xhtml1-transitional.dtd">
<html>
  <head>
    <title><%= $article_title %></title>
    <meta http-equiv="content-language" content="ja" />
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <style>
body {
% case $mode
% when :Web
  font-size: 130%;
  color: #333;
  font-weight: bold;
  margin: 0px;
  padding: 0px;
% else
  font-size: 18pt;
  color: #000;
  font-weight: bold;
  margin: 2cm auto;
  padding: auto;
% end
  background-color: #fff;
  font-family: "Migu 1C"
}   

h3 {
  font-size: 135%;
}

#Container {
  margin: auto auto 100px;
  max-width: 950px;
  text-align: center;
}

img {
% case $mode
% when :Web
  height: 300px;
% else
  height: 18em;
  max-width: 100%;
% end
}

ul {
  text-align: left;
  margin: auto 1.8em;
}


#ArticleFooter {
% if $mode != :Web
  display:none;
% end
  position: fixed;
  bottom: 0px;
  margin: 0px;
  width: 100%;
  max-height: 100px;
  background-color: #339;
  color: #fff;
  text-align: center;
}

#ArticleFooter a {
  padding: 0 0.8em;
  text-decoration: none;
  color: #fff
}

#ArticleFooter a:hover {
  text-decoration: underline;
}
    </style>
  </head>
  <body>
    <div id="Container">
    <%= $article_body.gsub(
      %r{src="img/},
      %(src="#{$img_type}_img/)
    ) 
    %>
  </div>
    <div id="ArticleFooter">
      <%= $article_footer %>
    </div>
  </body>
</html>
END

$article_body = nil
$article_footer = nil
$article_title = nil

SOURCEFILES = Dir.entries(".").select {|i| i =~ /\.md$/ }.sort

# HTML
$img_type = "/img/si/intro_slideshow/resized"
$mode = :Web

SOURCEFILES.each_with_index do |x, i|
  begin
  treat_md(x, i)
  File.open("out/#{x.sub(".md", ".html")}", "w") do |f|
    f.puts(ERB.new(TEMPLATE, nil, "%<").result)
  end
  ensure
    $article_body = nil
    $article_footer = nil
    $article_title = nil
  end
end

# Web PDF
$img_type = "resized"
$mode = :PDF

SOURCEFILES.each_with_index do |x, i|
  begin
  treat_md(x, i)
  fn = "out/_tmp_#{x.sub(".md", ".html")}"
    File.open(fn, "w") do |f|
      f.puts(ERB.new(TEMPLATE, nil, "%<").result)
    end
  system('wkhtmltopdf "%s" "%s"' % [ fn, fn.gsub(".html", ".pdf") ])
  ensure
    $article_body = nil
    $article_footer = nil
    $article_title = nil
  end
end

system 'pdfunite out/_tmp_*.pdf out/portable.pdf'
system 'rm out/_tmp_*'

# Printable
$img_type = "original"
$mode = :PDF

SOURCEFILES.each_with_index do |x, i|
  begin
  treat_md(x, i)
  fn = "out/_tmp_#{x.sub(".md", ".html")}"
    File.open(fn, "w") do |f|
      f.puts(ERB.new(TEMPLATE, nil, "%<").result)
    end
  system('wkhtmltopdf "%s" "%s"' % [ fn, fn.gsub(".html", ".pdf") ])
  ensure
    $article_body = nil
    $article_footer = nil
    $article_title = nil
  end
end

system 'pdfunite out/_tmp_*.pdf out/printable.pdf'
system 'rm out/_tmp_*'

完成したものについては

Google DOCs Viewerはスライドショー向きではないので、ウェブ版を使うか、PDFをダウンロードするのがお勧め。

PureBuilder2(3) Markdown for Blog

今やブログは主に Markdownで書かれている。

当初、blogtrというPuredoc用のスクリプトを書いたが、ブログ程度であれば圧倒的にMarkdownで事足りることが多いからだ。
実のところ、ACCSコンテンツなどでもMarkdownで十分なケースは多く、そのためにPureBuilder2はMarkdownをサポートする。

Markdownを変換し、ヘッダーを操作するblogmdはPureBuilder1.5で既に実装されたが、これはPandocを使ったスクリプトである。
PureBuilder2は全面的にRubyでいく予定であるため、MarkdownトランスレータにはKramdownを使用している。

これに合わせてKramdownを採用することにした。
機能的にはほとんど変わらないが、唯一の違いとして、手前に

* TOC
{:toc}

を入れるようにした。
これにより、KramdownはTOCを自動生成する。Pandocにはなかった便利な機能で、ちゃんとオフセットしたTOCを生成してくれる。

ちなみに、PureBuilderのMarkdown用ライブラリがKramdownをモンキーパッチングで拡張し、自動的にPureDocオブジェクトのメタ情報としてヘッダーを埋め込むので、XHTPureDocを使ってTOCを作ることもできなくはない。

まだpush予定はないため、コードを掲載する。

#!/usr/bin/ruby
# -*- mode: ruby; coding: UTF-8 -*-

require "purebuilder/purebuilder"
require "kramdown"
require "optparse"

opt = {}
op = OptionParser.new

op.on("-m", "--marshal") {|v| opt[:marshal] = true }
op.on("-M", "--without-meta") {|v| opt[:marshal] = false }
op.parse!(ARGV)


# String for Kramdown's TOC
TOC_PREFIX = "* TOC\n{:toc}\n\n"

# Get article file
sourcefile = ARGV.length == 1 ?  ARGV[0] : nil
sourcestr = ARGF.read

pbp = PureBuilder::Parser.new(sourcestr, sourcefile)
pbp.proc_header

html = Kramdown::Document.new(TOC_PREFIX + sourcestr.gsub(//m, "") ).to_html

if opt[:marshal]
  Marshal.dump({body: html, head: DOC.meta}, STDOUT)
else
  puts html
end

これだけ見るとシンプルだが、PureBuilderとの関係が深く、結構中まで突っ込んでみることになってしまった。
PureBuilderの設計があまりよくないのかもしれないとも思ったが、そもそもPureBuilderのMarkdownサポートは「MarkdownをPureDocに見せかける」ものなので、PureBuilderとPureDocの密結合はやむをえまい。

これで初めて動かすこととなったPureBuilderだが、これによってPureBuilder、さらにPureBuilder登場によって修正されたPureDocのバグが発見され、デバッグにかなりの手間を費やした。

基本的にはシンプルだが、オプションへの対応を加えたため、いくらか複雑化した。
オプションは、将来的にパイプしてAPI経由でのブログアップロードに対応するため、メタを含めたMarshalで渡すためのものだ。