ウェブサイト全文検索システムの開発

開発経緯

NamazuやGroongaも試したのだが、いまひとつ望むものにはならなかったので、シンプルで美しい全文検索システムを書くことにした。

これは、第一にはGoogle検索に依存しているMimir Yokhamaの検索機能を自前で持つこと、 第二にはChienomiを含むWordPressのシステムの置換えである。

検索システムの開発自体はそれほど難しくないと思うのだが、どのように動作するのが望ましいかということを考えると非常に難しい。 Googleの検索システムは非常に高度なので、それに匹敵するものを作るのは難しいのだ。

だが、ここはPureBuilder Simplyにふさわしいシンプルなものを目指すことにする。

設計その1

とりあえず、grepを使えば話が早いのだが、HTMLだと余計な要素を含んだ検索になってしまう。 HTMLからタグを除去するのは難しくないが、どういうポリシーで除去するのか、いつどうやって除去するのか、などが難しい。

PureBuilder Simplyの構成から言えば、生成時に、Pandocで生成するのが望ましい。

$ pandoc index.md -t plain index.txt

だが、生成時に生成を全く無視してインデックスを生成するのはどうだろうか? そもそもPre pluginsはソースファイルを、Post Pluginsは生成したHTMLファイルを加工するものであるため、本来の目的から逸脱してしまう。 例えばPre Pluginsを使って

とかもできる。

ただ、今のところ検索対象になるようなソースファイルを加工するようなPre plguinsを使っていないため、別にこのようにする必要性はない。

設計その2

あとから処理するためのもの。 .indexes.rbm に基づいて処理を行う方式。

検索

いずれにせよここまでやってしまえば検索は簡単。 grepで検索できる状態なので、シンプルに検索可能。

AND検索の要領としては

ものすごく検索対象が多い場合は、検索対象そのものを絞り込んでいくほうがいいだろう。 だが、プロセス起動回数が増えることを考えると、そのような場合は自前実装のほうが良い可能性が高い。

OR検索はもっと簡単で

スペースの取り扱い方とか、case問題とか考え始めると難しい。 ただ、世の中そんな複雑な検索をしている検索エンジンはあまりないし、多分ローカルにそんなもの作ったところで報われないのでこれくらいでいいような気もする。

ANDまたはORではなく自由にANDとORを結合できるようにした場合は、expr自体に評価できるメソッドを追加すると良い。例えば

といった感じである。

あとがき

検索機能の実装自体は難しくないのだが、ChienomiをPureBuilder Simply化するという話になると結構難しい。

既にかなりの記事があり、検索からの流入も多いため、どうしても全記事に対してマップせざるをえない。 これもなかなか面倒だ。

だが、もっと問題Chienomiの記事は書き方が一定でない、ということだ。

Chienomiの記述形式はPOD, RDoc, ACCS2, PureDoc, PureDoc2, PureDoc2::Markdown, Pandoc Markdownがある。 PureBuilder SimplyはPandocでの処理を前提としているため、なんとかしないといけない。

過去記事については諦める方針ならばHTMLとして抜き出すという方法もあるのだが(Mimir Yokohamaでウェブサイトのサルベージでよく使う方法だ)、 できれば避けたいというのもある。

また、タグとカテゴリのつけ方が一定ではないため、これを処理しなければならない。

さらに厄介なのがメディアファイルだ。 WordPressはメディアファイルの使い方が独特だし、そのためにメディアファイルについてはWordPress上で追加する方法をとっていた。 さらにサイト移行時にメディアファイルを紛失したこともあって、結構大規模な作業になると思う。

そのことを考えると一筋縄ではいかない。

それはともかくとして、サイトの検索で非常に複雑な演算子を使いたがる人はまずいない、 どころかサイト内検索なんてほぼ使われていないに等しいので、基本的な検索機能で十分だと思うのだが、それであればこの通り実装はとても簡単だ(例によって設計で稼いだ感があるが)。

というわけでちょっとした実装例、そしてシェルスクリプトサンプルとして役立てば幸いである。

Linux zsh/デュプレクサ/ssh設定の勘所

ここのところ大作をいくつも書いているため、忙しさも手伝ってなかなかアップロードできない。 この話もLinuxを使えるようセットアップする話をまとめようと思っていたのだが、それはあまりに時間がかかってしまうので、かいつまんで解説しようと思う。

世の中にはLinuxをインストールだけ繰り返すという人もいるのだが、だいたいそういう人はセットアップをしない。

1台だけのコンピュータを使う人、というのも、実はLinux手練(というよりも達人)の中でも結構多いのだが、1台だけ使っているうちにはその中で順次セットアップを煮詰めていけば良いのだが、Linuxは複数台使ってこそその真価を発揮するし、そうなると素早く適切にセットアップすることが求められる。 特にConoHaでインスタンスを立てたり閉じたりする場合にはなかなか重要だ。

ここでは頻繁にセットアップを行うがまだ手順を確立していない人、あるいはセットアップ自体確立していない人のために私のコツをご紹介しよう。

なお、ここでは前提としてManjaro Linuxあるいは(サーバーにおいては)Arch Linuxをセットアップする前提にあり、その中で極力少ない労力でセットアップしようとしている。 基本的な方針は他のディストリビューションでも応用できるはずだが、労力の多寡に関してはこの限りではない。

また、シェルはZshを使っていることを前提としている。 Bashを使っている人はZshに乗り換えてしまえば良いと思うが、Fishを使っている人に関しては私はわからないので留意していただきたい。

初期セットアップに関して

とりあえずアップデートして再起動

# pacman -Syu
# reboot

vimがないと設定に困るのでvimを用意する。 viも別パッケージであるのだが、むしろ不便なのでシンボリックリンクにする。 vim-pluginsが入っていればだいたい良いので、一緒にいれておく。これでvimで苦痛を感じることなく使うことができるようになる。 (デスクトップではvimではなくgvimを入れる。そうするとXセレクションを扱えるようになる)

# pacman -S vim vim-plugins
# (cd /bin; ln -s vim vi)

設定ファイルをViなんかで書けるか! Emacsにしろ! という人はEmacsも入れる。 私はEmacsを使わないのであまり詳しくない。 こちらもデスクトップではemacsパッケージを入れる。

# pacman -S emacs-nox

ここで一般ユーザーを作っておく。もちろん、Manjaroでは必要のない作業。 Manjaroでは一般的なデスクトップユーザーに追加すべきグループが多いので留意する必要がある。

# useradd -m -U -c "First User" -s /bin/zsh -G wheel,storage,sys,network luser
# passwd luser

wheelグループにsudoを許すようにする。 $wheelの行のコメントアウトを外す。NOPASSWDになっている行ではないほうが良いだろう。

# visudo

Zshをログインシェルにしたが、まだ設定していないのでbashで入る。

# sudo -u luser bash -l

AURを扱うのであればとりあえずyayを入れるのがお勧め。 Archでも2パッケージで済むからだ。それにタイプ数も少ない

$ sudo pacman -S go
$ git clone 'https://aur.archlinux.org/yay.git
$ cd yay
$ makepkg
$ sudo pacman -U yay*.pkg.tar.xz

あとはほしければTrizenでも入れておけば良い。 yaourtとpacaurは「安全ではないソフトウェア」になりつつあるらしいので、とりあえずお勧めはしない。

$ yay -S trizen

w3mとlvがあればとりあえず文書を読むのは楽になる。 コンソール作業する場合は必須

$ yay -S w3m lv

ターミナルマルチプレクサを入れる。Powerlevel9kはGNU Screenで位置がバグる問題があるので、ここでtmuxを入れておく。 一応、screenも入れておく。

$ yay -S screen tmux

Moshは便利だと思うけれど、私はUDPを透過するファイアウォール設定をしたくないため、ここではMoshの話はしない。

Zshとターミナルマルチプレクサ

とりあえず

私が新インスタンスに対して最初にすることは、Zshを導入しセットアップすることである。

ZshからFishに変えた、あるいはZshが使えなくてBashがいいという人の多くはZshのセットアップができていない。 ディストリビューションに良いZshの設定が含まれていることは稀だし、Zshの設定は非常に多くて難しい。 マニュアルを読んで設定を作り上げてこそなのだが、それをしない人が圧倒的に多い。 (似たようなことはVimにも言えるが、Vimの場合はそれが気になるケースは少ないかもしれない)

実は話は実に簡単で

$ sudo pacman -S grml-zsh-config zsh-completions

これで再ログインすれば立派に使えるZshが出来上がっている。

grmlの設定は非常に練られていて、多くの場合これで十分だろう。 (場合によっては調整がいるかもしれない)

なお、Archのgrml-zsh-configはskelが含まれているのだが、Manjaroは含まれていない。 skelのほうは便利ツールがコメントアウトされているものなので、別になくても構わない。

なお、Zshの設定ファイルに日本語を使うとサーバーではトラブルのもとになるので注意してほしい。

オプション

だが、もう少し練ることにしよう。 まず、grmlの設定ではAUTO_CONTINUEが有効ではないので、間違ってフォアグラウンドで起動してdisownしたあとSIGCONTをわざわざ送る必要がある(実際はこのケースではbgしてからdisownするほうが良い)。

重複する部分が多いが、重要なオプションは設定しておこう。

私は次も設定しているが、多分いらない。(RC_EXPAND_PARAMはgrmlではオフかも)

シンタックスハイライト

プラグインの中では便利なもの。 ただし、いくつかのオプションが制限される。

% sudo pacman -S zsh-syntax-highlighting

履歴の機能を拡張

grmlの履歴機能はhistory-beginning-{for,back}ward-endを採用している。 これは、「コマンド部分は途中ならその位置、コマンド部分が入力されていればその部分を維持し、カーソル位置を末尾としてヒストリをたどる」というものだ。

だが、個人的にはオプションも含めてカーソル位置まで維持してくれるほうが好きだ。 全面的に書き換えるのではなく、PgUp/PgDown時はカーソル位置を維持してヒストリをたどるようにする。

本来は記述が足りていないが、この内容はgrmlを前提としている。

履歴を残す量も設定しておこう。 HISTSIZEは検索で辿れる量、SAVEHISTはファイルに残す量だ。

viモード

私はviモード使いなので、設定しておく。

なお、逆にEmacsキーバインドで使いたい人で、EDITORvivimnvimにしている人はちゃんと設定する必要がある。

ターミナルマルチプレクサ

SSHの場合は切れないとか、セッション増やしたいとかだいたい起きるので、使用させることにする。

なお、これでtmuxの設定で

set -g default-terminal "xterm-256color"

とかすると地獄をみるので絶対にしてはいけない。 必ず

set -g default-terminal "screen.xterm-256color"

または

set -g default-terminal "screen-256color"

とすること。

なお、私はGNU Screen使いなので、tmuxのキーバインドはscreen互換にしてある。

プロンプトとテーマモード

私は普段はgrmlプロンプトテーマを使っているが、SSHではPowerlevel9kを使っている。 これは、見やすく、わかりやすいため。普段から使わないのはプロンプトが戻るのがちょっと遅いからだ。

% sudo pacman -S zsh-theme-powerlevel9k awesome-terminal-fonts powerline-fonts

私はテーマ読み込みにこんなことをしている。

前述の$_DEFAULT_SHELLMODEはこのためのものだ。 だが、常にPowerlevel9kで良いのなら、別にロード部分を直接書いてもいい。

grmlと競合してしまうので、prompt offすること。

私の設定はこんな感じ。

このあたりは好みなのだが、実はPowerlevel9kの公式マニュアルにはあるが動かないものというのが結構合ったりする。 典型的には$POWERLEVEL9K_SHORTEN_STRATEGYは多くがうまく動かない。

ポイントになるのが、プロンプトにcontextを使わずuser hostと分けた上でホストのREMOTEのみ色を設定していること。

基本的には「ローカルの場合はどのマシンでも同じ色でいいが、リモートの場合はどのマシンか判別できたほうがよい」と思う。 別にこれ以外のプロンプトテーマでもホスト名は出しているのだが、現在作業中のホストを勘違いしてやっちまった、ということはしょっちゅうある。

まずホスト名に一貫性があってわかりやすい名前をつけることが大切だ。

% sudo hostnamectl set-hostname thinkpad-x1

私の場合は花の名前をつけることにしている。 実はこれは1993年の出来事に由来しており、運用は1998年から、と私としてはとても歴史がある。

さらにメリットとして花なので、色が連想できる。私は$POWERLEVEL9K_HOST_REMOTE_{FORE,BACK}GROUNDはその花の代表的な色をモチーフにした色使いにしている。 それぞれのマシンでイメージが離れた花の名前をつけているため、色被りも少ない。

簡単でわかりやすいのは、機種名とケースの色だろうか。VPSは難しい。

なお、

vi_modeを正しく表示するために必要な部分。

SSH

基本的なSSH

とりあえずroot鍵を登録してパスワード認証は閉じる。

まずはログインするほうでssh-keygen -f <file>によって生成した.pubのほうのファイルを ログインされるほうの~root/.ssh/authorized_keysにコピーする。

なお、.sshはパーミッション0600であること。

このroot鍵を登録するステップはサーバーに対するもので、直接ログインできるのであればスキップすべき作業である。

そして/etc/ssh/sshd_config (ssh_configではない!)を

PasswordAuthentication no

としておく。 この時点で一旦リロード

% sudo systemctl reload sshd

同じ要領でユーザー鍵を登録する。 鍵そのものも分けたほうがいい。

% ssh-keygen -f server-luser_rsa
% rsync -e "ssh -i server-root_rsa" server-luser_rsa.pub root@server:/home/luser/.ssh/authorized_keys
% ssh -i server-root_rsa root@server "chown luser:luser -R /home/luser

以降のために~/.ssh/configに設定しておく。

Host server-luser
  User luser
  Port 22
  HostName server.example.org
  IdentityFile ~/.ssh/server-luser_rsa

これでssh server-luserとして入れるようになる。

なお、HostNameだが、LANで同じセグメント内にいるのであればZeroconfによる.localを使えばいいだろう。 あるいは、各マシンを固定アドレスとして/etc/hostsに書いておくというのも手。 /etc/hosts上の名前は1マシン1つではなく、役割ごとに名前をつけておくと、その役割が他のマシンに移ったときにあまり苦労しなくて済む。dnsmasqで配るという方法もある。

SSHに関する話は以前にしたので、応用技としてはそのあたりを参考にしてくれると良い。

いざというときのための暗号化経路

なにかのときのため、ネットワークごとに1台は透過的にアクセスできると良いだろう。 これはいくつかの方法がある。

最も簡単なのは、Socksプロキシを使うことだ。

% ssh -TND 4711 -o ServerAliveInterval 10 -o ServerAliveCountMax 3 luser@server

これでlocalhost:4711をSOCKS5プロキシとして使用すればSSHサーバーを経由してアクセスすることが可能になる。 これは、ウェブブラウザやメールクライアントで有用である。これは公衆Wi-Fiからアクセスする程度の場合に有効だ。

もうひとつは、SSH経由の環境から利用できるようにしておくことだ。 w3mやMutt, Vim, Emacs, lvなど端末から利用できるアプリケーションを一通り揃えておけば、GUIは使えなくてもひととおりの作業が可能だろう。

本当にそのネットワークを通じてアクセスする必要が生じた場合はどうだろうか? 私はそんなサバイバルな経験を何度かしているが、普通の人はあまりない。 常時必要とするのであればVPNを用意しておけば良いのたでが、緊急避難的に使用できるようにしておくと何かと便利だ。

まずは双方にpppをインストールしておく。

% sudo pacman -S ppp

ログインするサーバーに対してはパスワードをかけたコマンド用rootキーを使うのが最も確実で安全。

command="/usr/sbin/pppd nodetach notty noauth",pty,no-X11-forwarding,no-port-forwarding,no-agent-forwarding ssh-rsa ...

さらに実際に使うときにログインした上でフォワーディングを許す。

# echo 1 > /proc/sys/net/ipv4/ip_forward
# iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

そしてつなぐ。

% sudo pppd updetach noauth silent nodeflate pty "sudo -u luser ssh server-ssh-ppp" ipparam vpn 192.168.32.1:192.168.32.2

192.168.64.0/24に対してアクセスできるようにしたい場合:

% sudo ip route add 192.168.64.0/24 via 192.168.32.1

すべての未知のホストにこの経路でアクセスしたい場合。サーバーのアドレスは10.0.8.1、現在のデフォルトゲートウェイは192.168.1.1だとすると:

% sudo ip route add 10.0.8.1 via 192.168.1.1
% sudo ip route replace default via 192.168.32.1 proto static metric 101

シェルスクリプトで並列処理

なんだか検索件数が多いので、シェルスクリプトによるコンカレンシーのお話をしよう。

ただし、bad design (変数を書き換えるとか、相互にやりとりするとか)は除外する。

また、Zshを前提とする。

投げっぱなし

まず基本。投げっぱなしはとても簡単。

シェルスクリプトではジョブコントロールは無効になっているので、SIGHUPの送信はなされないので、さっさと終了してしまっても大丈夫。

処理の終了を待ちたい場合はwait

flock

flockを使う方法は簡単でシンプル。 ロックファイルを使ってファイルデスクリプタを開きっぱなしにし、そのファイルデスクリプタを指定してロックする。

まずファイルデスクリプタ9.lockファイルをライトモードでオープンする場合

そしてファイルデスクリプタ9を閉じる場合

これを利用するとこんな感じ。

ロックしている間に共有しているリソースの読み込み/変更を行い、ファイルデスクリプタを閉じる。

リソースを読むより簡単な方法は、ひとつのストリームを共有したファイルデスクリプタとして開き、 ロックを中に読むことである。

ワーカーを生成するサブシェルの標準入力はqueueファイルにリダイレクトされている。 そのため、ファイルデスクリプタ0queueファイルなのだが、そのサブシェルの子プロセスであるワーカープロセスはリダイレクトしていないため、このファイルデスクリプタが共有される。 結果、全てのワーカーはqueueファイルを標準入力とするのだが、ストリーム自体を共有しているため、どのワーカーが一行読んだとしてもストリームの位置が変更され、次に読み込む位置は他のワーカーにとっても変更される。実際

WORKER 1: foo
WORKER 2: bar
WORKER 3: baz

となる。

producer-consumer キュー

もっと凝ったことがしたいのであればUNIXドメインソケットを使ってproducer部分をシングルスレッド化することができる。

zsocket -lのタイミングで接続を受け付けているのだが、zsocket -lしていないタイミングでは接続しようとするプロセスをブロックするため、producer側の処理は直列に行われる。

双方向性があるときはproducerと直接やりとりできるのはメリット。

Orbit designの場合

私が採用しているOrbit designはレギュレーターはZshスクリプトなので、基本的にこのような並列化手順をとっている。 とはいえ、ものによっては直列(serial)になっていたりする。

ただ、並列化されているものが多い。ワーカープロセスは最も多いもので5。

基本的にワーカースクリプトが受け取るのは処理対象ID(ほとんどの場合ファイルパス)だけである。 それ以外の情報はスクリプト側で生成するか、別途取得するかする。

Mimir Yokohamaに「いいね機能」「コメント機能」を追加

概要

Weekly 10000PVを達成して機能強化に力の入っているMimir Yokohamaのウェブサイト。

連続の機能強化でついに「いいね機能」と「コメント機能」が追加された。

実は先日の「お問い合わせフォームの実装」は単にその機能を実装する最小限ではなく、簡単なアプリケーションを実装できるプラットフォームになっており、 アプリケーションを追加する条件が整っていたのだ。 また、そのためのテストもしてあった。

そのため、実は今回コード追加はわずかで、両方合わせても23行ほどにとどまる。 ごく簡単だが、確証が持てないためにテストと本番環境のための修正を行ったりして結構な時間がかかった。

これに関してはみるべきところはあまりない。 受け取ったパラメータをファイルに書き込めば良いだけだからだ。

ちなみに、連打しやすいアプリケーションを入れるために連打の対策もサーバーにしてあった。 実は先のパフォーマンスチューニングはこの対策によってパフォーマンスが低下してしまったため、これをカバーするついでに行ったものだった。

いいね機能の設計

いいね機能はごくシンプルだ。

Pandocテンプレートを使ってページタイトルを埋め込むことができるので、これだけ使うのであれば単にテンプレートの中にフォームを書けばいいだけ、ということになる。

実際はリファラ(Rack::Request#referer)及びユーザーエージェント(Rack::Request#user_agent), IPアドレス(Rack::Request#ip)も特定に使用している。

ポイントは「一度送信したら表示を変更、送信を無効化する」ということだ。

inputタグのdisabled属性を使うことで送信ボタンを無効化している。

そう、この機能はHTML上でインラインで書かれているのだ。 このような書き方はW3C的には推奨されないのだが、Googleは推奨している。 実際、これだけのためにJavaScriptファイルをロードさせることをしたくなかったので、インラインにした。

ポイントは

  • Legacy DOMにおいてフォーム部品は連想配列のようにアクセスできるようになっている
  • submitボタンのラベルはvalueである
  • disabledによってフォーム部品を無効化できる

である。

これらの処理は送信の「前に」行われる。 これは正しいことではないが、問題はない。 なぜならば

  • 送信できなかったからといってユーザーが修正するなどの手を入れる余地はない
  • 特に返信を必要とするものでもないので、送信失敗はクリティカルな問題でもない
  • サーバーエラーなどは表示されるようになっている

からだ。

また、画面変遷せずに送るだけ…というと、Ajaxで非同期に送るしかないように考えるかもしれないが、 実際は現代のブラウザは基本的に2XXステータスで空コンテンツを返すとページ変遷しないようになっている。

このような用途のために204(No Content)が用意されているため、204を返す仕様だ。 できるだけブラウザの標準機能に頼るようにしている。

このボタン、めっちゃgooglebotが押してくる…

コメント機能の設計

公開されるものではなく、sanitizeしなくても問題が発生しない設計になっている(単に文字列として扱う以上の取り扱いがなされる条件で使用しない)ため、非常に楽だ。

難しく考えるよりも、シンプルで挙動をちゃんと把握できていて、余計なことをしない方法をとるのが最も楽に、確実に、バグなく設計できる。

ただ、この機能に関しては「コメントフォームの表示」などが必要になり、JavaScriptが必要になった。 また、コメントするというあまりとらない行為のための部品であり、常にロードすることは非常に好ましくない。

そこでとった方法は「JavaScriptの遅延ロード」であり、「クリック時にelementを追加してJavaScriptを読み込む」という方法だ。

これでscript要素を追加している。

では呼び出されたあとどうしているのか…というと、こんな感じだ。

そう、コメントフォームは標準でHTMLに含まれていない。

パフォーマンス的にみても親切機能のためにもう重くなりつつあるドキュメントをこれ以上重くしたくないので、 「フォーム部分はJavaScriptがロードされたときにinnerHTMLで書く、という方法をとっている。

「クリックされたときにはじめて必要とされるのでドキュメントノードを追加してロードする」という手法は稀に使われるが、さすがにそれでHTMLドキュメントそのものを生成するというのはまず見ない手法になっている。

連打されたときのことを考えて、clickイベントを発生する値に特殊なプロパティを埋め込み、何度も実行されないようにしている。 なお、これは本当に連打したときだけ機能するもので、JavaScriptシングルスレッドなので一回このスクリプトに入っていまえばこのスクリプト中に割り込まれることはない。 だが、連打されるとイベントがキューに入ってしまうので何度も実行されてしまうことから、そうしたことがないようにしている。 シングルスレッドなので、このスクリプトが実行されているのにプロパティが設定されないうちにまた実行されるということはない。

元になるフォーム自体は存在していて、submitボタンを配置すればフォームの送信は可能だ。 そのため、submitイベントに対するイベントリスナーを設定するものはHTML上に静的に存在している。

その上で「フォームはやっぱり閉じられたほうがいいな」ということでボタンクリックに対するイベントの変更、及びボタンラベルの変更を行っている。

送信を行った場合はもうコメントフォームは使わないので、コメントフォームは非表示にして(削除はしていない)ラベルを変更している。

ラベル変更だが、input部品とは違ってbutton要素は子要素テキストノードがラベルになっているので、dataプロパティの書き換えによって変更している。

もともとHTML上でLevel0 DOMイベントを使っているので、スクリプト上でもLevel0 DOMによってイベントを変更している。 いつもaddEventListenerを使っていたので、珍しい。 イベントの削除はイベントにnullを代入するだけだ。

なお、HTMLで全て含めてしまえばJavaScriptは排除できるのだが、これ以上余計な要素を組み込むことはできれば避けたいためこのような仕様になっている。既にDOMコンパイルは割と重い。

また、デザインポリシーからいって、多くの場合余計なコメントフォームを常に表示させておくというのは美しくないとも思っている。 JavaScriptを使わずCSSでオンオフするようにもできるが、そうすると今度はボタンのテキストを変更するのが難しいし、連打や再送信を防ぐのも難しい。

このことから、なにがなんでもJavaScriptを使わないのが正義、ということでなければ、 このような付加コンテンツはユーザービリティの点からも素直に使うべきだと思う。

こちらもいいね機能同様、送信する前に状態を変更してしまい、成功すれば204を返す仕様。

Legacy DOM と DOM Level0 Eventに関して

Legacy DOMやDOM Level0 Eventについて意見をもとめられることがたまにあるのだが、私としてはあまり勧められないと考えている。

非常に簡単なので学習にはいいが、Legacy DOMはHTMLの構造に依存する。例えば

として、

とかやってしまうと、ドキュメントの構造が変わるたびに修正だ。

だが、「フォームにのみnameで使い」「フォームはW3C DOMで特定する」のであれば悪くない。

DOM Level0 Eventはイベントリスナーが1つしか登録できないためモジュール設計になっている場合や、なんらかのライブラリを使っている場合は使ってはいけない。 イベントを「追加」する場合はLevel2で、イベントを「設定」する場合はLevel0という使い分けも考えられる。 HTMLに直接書く場合はLevel0しか書けないので、その部品の基本的な動作と定義されているならLevel0でもいい。

ただし、その場合でもできればLevel2 Eventを「後から追加する」のが適切なので、DOM Level0 Eventの使いどころは今回のようなものが唯一だと思う。

先のシェルスクリプトを形にしました

一時キーボード無効

Temporary Disable Keyboard @GitHub

Xinputを使用してデバイスを無効/有効にするためのもの。

主な変更点は次の通り

  • 無効化を「時間制」「ダイアログ」「無限」から選択できるようにした
  • Zenityダイアログを若干調整
  • ランチャー用の.desktopファイルを追加

Zshスクリプトだが、case内で非常に珍しい;&(フォールスルーする)を使用している。

ターミナルエミュレータ選択機能

Terminal Selector @GitHub

主な変更点は次の通り

  • ランチャー用の.desktopファイルを追加
  • KDE Service用の.desktopファイルを追加
  • Nemo用の.nemo_actionファイルを追加
  • 対応する端末を大幅に増加
  • 利用できない端末を選択肢から除外するように変更
  • Zenityダイアログを調整
  • 柔軟に端末を追加できるようにファイルマネージャでの起動用の引数対応が連想配列で使用できるように変更

連想配列を使用するための方式は次のようなものだ。

わざわざ連想配列をテンプレート文字列とし、そこから配列に変換している理由は

  • 連想配列に格納できるのは文字列のみ
  • 置き換えしてからではDIRがIFSを含む可能性がある

【検索ワードに応えて】 ThinkPad X1 Carbon (2017, シルバー) のお話, Linux関連, その他

久しぶりの検索ワード反応企画。 ThinkPad X1 Carbon関連の検索が多かったので、併用されたワードに従ってコメントさせていただくことにする。

なお、ThinkPad X1 CarbonについてはYouTube (はるかみ☆ チャンネル)で開封動画を掲載させていただいているので、よかったらそちらもどうぞ。

また、そのほかLinux関連の検索にも回答させていただく。

ThinkPad X1関連

基本的なレビュー

ラップトップをガリガリ使うモバイラーなら持たない理由が見つからない。

XPS13のような13.3インチの中でも小型のものを除けばボディは13.3インチと変わりないサイズだ。 しかし14インチの画面は明らかに見やすく、作業もしやすいし、人に画面を見せるときにも良い。

そして非常に軽量でバッテリーマイレージも素晴らしい。 もちろん、キーボードはあらゆる現行ラップトップの中で最高だと思う。

唯一欠点としては天板の外板が弱い。 既に凹み、割れがある。それほど目立たないし、動作には問題ないのだが、ちょっとカナシイ。

買い時について

「高性能」「最先端」などを意識したい場合はリリース間もない頃が良い。

リリース直後は割引はなく、およそ半年程度で30%程度の割引になる。モデル末期は40%を越える程度の割引だ。

基本的にはThinkPadの場合30%程度割り引いた状態で競争力のある通常価格で、割引のない最初期というのはフリーク向けと考えられる。 なので買い時を気にするような人なら直後はないだろう。

春頃には30%前後の割引になっていたりするから(現時点でサイト公式のクーポンが28%、恐らくメルマガや店頭クーポンならもう少し行くだろう)、それぐらいが買い時ではなかろうか。

少しでも安く買いたい人は、翌年モデルが発表されてから決めるといい。 その頃にはかなり安くなっているし、在庫が残っていれば翌年モデルが出てからでも安く買える。 翌年モデルを待つべきかどうかの判断も出来てお得だ。

2017と2018に関しては、X1 Carbonとしての進化は微々たるものだったけれど、第8世代プロセッサになったという点がとても大きい。 ただ、私はかなり安く買えたので、そのタイミングでよかったと思う。

シルバーのThinkPadについて

良いと思う。

ThinkPadらしくないという批判はあるだろうけれども、マットだけれども黒のピーチスキンよりもサラッとした手触りで汚れもつきにくく目立たない。

シルバーのラップトップはそれなりに存在するので目立たないということは言えるけれども、「Hacker’s itemたるThinkPad」と「美しくおしゃれな高級ラップトップ」を両立させる意図は十分に達成できている。

控え目にいっても最高にかっこいい。

国産と中国産について

何も違わないから安心してほしい。 400万円するようなP920でも中国産だ。

なお、納期はかかる。X1は予定納期よりも遅かったという人が珍しくないようなので注意してほしい。

その他の検索ワード回答

シェルスクリプトの並列実行

基本的な形式は次のとおり

シェルスクリプトの並列実行は待ち合わせず投げっぱなしにするのが基本。

ただし、どうしても待ち合わたい場合はwaitを使う方法もある。

とした時、$!でサブシェルのPIDが取れるので、

としておけば待ち合わせる必要があるタイミングで

とすることができる。これはBashでもZshでも共通。

入出力をやりとりするのであれば、下流のパイプに流すべきで、親プロセスが出力を披露ようなことは考えないほうがよい。

どうしてもであればファイルに書いてwaitするか、もしくはZshならProccess Substitutionを使用する。

あるいは大量の処理があり、複数のワーカーを走らせたい場合はflockを使うのが無難だろう。 例えば以下のようにする。

要点は以下の通りだ。

  • 関数workerが各ワーカーが実行する内容
  • これをサブシェル内でworker nの形で実行する
  • ファイルデスクリプタはクローンされるので、標準入力から読んだ場合、親シェルもサブシェルも同じものを読み進めるし、位置も同時に変化する
  • しかし、もし同時に読んでしまうとおかしなことになるので、読んでいる間は他のシェルに読んでもらっては困る
  • そこでロックファイルを作り、これをロックすることで排他制御する。いわゆるドットロック。
  • exec 9>| .lockでファイルデスクリプタ9番をロック用に確保
  • 開きっぱなしになるので、flockでこのファイルをロック
  • ロックを獲得できたら標準入力から読む
  • 読み終わったらファイルデスクリプタを閉じてロックも解放
  • これでキューから1行読めたので、処理を進める
  • 「1行では足りないよ!」という場合、エントリをファイルに書いておいてディレクトリにまとめ、キューはファイル名にするとかすれば良い

AMD APU (Godavari/Kaveri) と Linux

特に問題はない。

以前はCatalystドライバのおかげでずいぶん苦労させられたけれども、 AMDGPUになって以来目立った問題は発生していない。

割と電気を食うけれど性能は微妙なので嬉しくはないと思う。 コストパフォーマンス的にみれば、これくらいの性能がおいしいという人は多いと思う。 ビデオ関連も充実しているし、Killerシリーズなんかは機能も充実しているしね。

XMPP

ちょっと話題が広すぎて何を求めているのかがわからない。ごめんよ。

DPI

これかなり広い。

LinuxでのHi-DPIの話なら/etc/X11/xorg.conf.d/以下にモニターの設定ファイルを書く。 40-monitor.confとかで。

Section "Monitor"
    Identifier             "<default monitor>"
    DisplaySize            286 179    # In millimeters
EndSection

で、KDE Plasmaを使うといい感じになる。 Plasmaを使わない場合については、Qtアプリケーションについてはなんとかなるけれど、GTKに関してはうまいことスケールしない。

LightDMに関してはGreeterの設定ファイルにxft-dpi=と書きましょう。

Hi-DPIはLinuxでは結構苦手にしている感じ。

フォントの設定はまた別。

某人物について

コメントする気はありません。

VP9のハードウェアエンコード

  • Intel QSVを使ってください
  • LinuxならVA-API経由ffmpegが良いよ
  • ただし画質は絶望的だったとは言っておく

Intel QSV * H.265

  • LinuxならVA-API経由ffmpeg
  • 速度はそこそこ。X.265とは雲泥の差。CPU負荷は0ではないというか、普通に40%くらいはいく
  • ちゃんとVA-APIドライバ入れようね

scale_vaapi

VA-APIを使用する場合の出力画面サイズ指定。 変更しない場合は省略して大丈夫。

AMD VCE / NVIDIA NVENC

VCEはLinux的にはVA-API経由。なのでQSVと一緒。

ただし、AMDのビデオドライバはVA-APIだけじゃなくVDPAUも使える。 ところがVA-APIがエンコード/デコードなのに対してVDPAUはデコードのみ。

VA-APIをVDPAUに転送するドライバ(libva-vdpau-driver)と、VDPAUをVA-APIに転送するドライバ(libvdpau-va-gl)があり、 VDPAUを有効にして、VA-APIに転送するドライバを使ってしまうと(Nvidiaの場合はVA-APIが使えないのでこうする)デコードのみになってしまって使えない。

ちゃんとlibva-mesa-driverを使いましょう。

Nvidiaの場合はVA-APIをサポートしておらず、VDPAUはエンコードができないので、 NVENCは専用のインターフェイスになっている。

どっちが優れているというのは難しいけれど、Nvidiaのほうが対応フォーマットは多い。

DiscordとSlack

基本的にはDiscordがいいと思うし、最近はSlackも微妙だと思うのだけれども、「両方あると嬉しい」という面もある。

Discordの場合「ユーザーという概念がある」ということが大きい。

Discrodでつながると、どのようなつながりであれ、「その人」というのが見えてしまう。 Discordは通知をカスタマイズできないため、「通知をオンにしている全ての人からの通知が一律に行われる」ということになる。 棲み分けをする方法がない。

これはLINEと同様の問題である。 恋人だろうが、ゲーム仲間だろうが、仕事の人間だろうが、区別する方法がないのだ。 アカウントの切り替えは難しいためアカウントで分けるのも現実的ではないし、アカウントで認識されているから一時的な連絡先にもしにくい。

Slackの場合はやりとりもつながりもあくまでそのチーム内でのことなので、仕事の関係などであればSlackのほうが便利だ。

自前メールサーバーからのメールをGMailが受け取らない

主にはホスト認証の関係。

GMailは送られてきたメールサーバーが、本当にそのメールを送る資格があるかをチェックする。

設定方法はいくつかあるけれど、SPFレコードを書くのが楽。 「メール SPF」で検索すると色々出てくる。

LinuxとRealTek ネットワークカード (RTL8152ほか)

ASIXよりはマシだけれど安定して苦しめてくれるRealTekのネットワークカード。

Linuxではネットワークアダプタは

Intel > Atheros (Killer) > Broadcom > RealTek > Asix

だからね。

RealTekのWiFiモジュールであるRTL8152のドライバはカーネルモジュールとしてある。 Arch Linuxの場合はDKMSになっていて、AURからインストール可能。 場合によってはRTL8153をブラックリストに入れる必要がある。

インストールしなくても動作はするけれど、うまく交信できなくなったりする。 安定性の面から言えば入れるべきだけれど、入れたところでうまくいかない場合もある。

「うちのディストリビューションにはないよ!!」という方。 Archにおいで。むしろManjaroにおいで。

Linuxでキーボードをつないだまま掃除したい

キーボードが汚れているなぁ、と気づいたとき、あるいはキーボードがべたつくとき、キーボードを掃除したいけれど電源を切りたくないということはないだろうか。 特にデスクトップの後方につないでいる場合や、ラップトップの場合にはキーボードを外すのも面倒な話だ。 スリープにしてもキーボードに触ったら復帰してしまうし、なかなか厄介だ。

そこで一時的にキーボードをつないだまま無効にする方法を考えてみた。

キーボードを無効にする方法としてはXによるデバイス認識であればxinputが利用できる。

次の方法で認識されているデバイスを確認する。

あまり見やすくはない。

⎡ Virtual core pointer                      id=2    [master pointer  (3)]
⎜   ↳ Virtual core XTEST pointer                id=4    [slave  pointer  (2)]
⎜   ↳ ELECOM ELECOM UltimateLaser Mouse         id=9    [slave  pointer  (2)]
⎣ Virtual core keyboard                     id=3    [master keyboard (2)]
    ↳ Virtual core XTEST keyboard               id=5    [slave  keyboard (3)]
    ↳ Power Button                              id=6    [slave  keyboard (3)]
    ↳ Power Button                              id=7    [slave  keyboard (3)]
    ↳ FCL USB Keyboard                          id=8    [slave  keyboard (3)]

キーボード全体をオフにしてしまうと困るかもしれない。 なので、Virtual core keyboardをオフにすることは思いとどまったほうがいいだろう。

FCL USB Keyboard(idは8)を無効にするには次のようにする。(手前側の8がID、後のほうは固定の値)

有効に戻すにはこうだ。

外付けキーボードが他に用意できるならいきなり無効にしても構わないが、そうできない場合は時間で復帰するようにしたほうがいいだろう。

安全のためアプレットから復帰できるようにするほうがいいだろう。 マウスで操作できるよう、Zenityを使ってon/offできるようにした。 また、off時にはタイマーをかけることもでき、タイマーをかけない場合は確認される。

Gistで公開している。 Zshスクリプトであり、実際にZshの機能を使用しているのでBashでは動かない。 Zenityはなかなかコマンド置換が難しく、(f)は欠かせない。

Mimir Yokohama ウェブサイトの「タグ機能」の仕組み

Mimir Yokohamaのウェブサイトにこっそりとタグ機能が追加された。

だが、PureBuilder Simply自体にはタグ機能がない。 この実現方法は発想力勝負な部分があった。

ドキュメントにデータを持たせる

「記事情報」でも行われている方法として、Markdown YAML Frontmatter内に情報をもたせ、Pandocテンプレートで存在する場合だけエレメントを生成するような手法を取っている。

例えば帯域においては

というYAML Frontmatterが書かれている。 記事情報などは自動的に生成することができないため記事ごとにかかれており、若干執筆コストを上げている面もあるが、 なにしろMimir Yokohamaには力が入っているのでそれくらいどうということはない。 基本的なフォーマットをコピペしてしまえばそれほど難しくない部分でもある。

ちなみに、Pandocテンプレートで

$if(pickable)$
Something
$endif$

とした場合、pickable: no (つまりfalse)ならばここは生成されない。

この追加情報としてtagsが加わったのである。

問題は検索

タグを表示することは簡単だが、普通に考えればタグから同一タグの記事を辿りたいし、タグで検索もしたい。

単純な方法としてGoogleを使うこともできるのだが、それは必ずしもタグつきの記事が上位にくるわけではなく、思ったようには動作しない。 ちゃんと検索機能を用意する必要があったのだが、できればPureBuilder Simplyの枠組みの中で行いたいところである。

PureBuilder Simplyは原則として「MarkdownまたはReST文書から生成する」という前提になっており、 ACCSもindexデータベースからMarkdownドキュメントを生成し、このあとはPandocで処理している。

なのでPureBuilder Simplyの枠組みで処理するためにはMarkdownドキュメントを生成しなくてはいけない。

それなら全てのindexを探し回ってタグを集めればいいじゃない。

ARGV.eachしているので、全ての.indexes.rbmを指定すれば良い。 あとはpbsimply-pandocで処理できるが、タグに登場した.indexes.rbmは実際に記事が存在しているものではなく拾ってほしくないので消しておく。

.indexes.rbmとして書き出すようにした意図の一部に、このように外部からドキュメントデータにアクセスするというものがあった。

これによってドキュメント解析しなくてもメタデータを利用して機能拡張してページに含むことができる。

テンプレートにとうとう限界が

だいぶ魔改造されているPandocテンプレートだが、今回は限界が垣間見えた。

タグクラウドらしくエントリ数の多いタグを大きく表示したいのだが、Pandocテンプレートに計算機能や比較機能がなく、CSSにもないため、 Pandocテンプレートだけでは実現できない。MarkdownにHTMLを直接書くという方法はあるが(Markdown自体はRubyで生成しているため)。

また、URIエンコーディングをする方法はデータを二重に持たせる以外になく、それでもふたつの値を同時にとるイテレータがPandocテンプレートにないため、URIだけでURIエンコーディングをおこなう方法がない。

eRubyを使ってもいいのだが、できれば使いたくない。 現時点ではタグクラウドの大きさ分けはしておらず、URIもURIエンコーディングせずに使用している。

アメブロから更新時に一部を抽出して通知する

私はかなりの頻度でアメブロのとあるブログエントリの更新をチェックしているのだが、ほとんどの場合重要な情報はそのごく一部である。 しかし、アメブロにスマホでアクセスするたびに、くだらない広告や関心のないTV出演者の話に通信量が割かれ、時間も割かれることを大変に腹立たしく思っていた。

そこで、PCからページを取得し、スクレイピングする方針で考えたものである。 スクレイピングした内容はDiscordで通知する。

具体的な内容は差し障るので抽象化する。

コード

解説

「アメブロから」という難しさ

ユーティリティスクリプトでは「問題自体を可能な限り引き下げ、ゆるく解決する」のが重要である。 開発コストを下げるべきなのだ。

amebloの内容を見ると、独特なエディタから生成されるものだけに、エディタ上での記述はちょっとした違いで大幅に異なるHTMLコンテンツが生成される。 私は投稿者とは連携していないので、ちょっとした記述のブレで抽出すべきコンテンツを見失う可能性がある。

生成されるタグと比べると見た目上の違いは少ない。 言い回しの違いや、スペースの有無程度である。

そこで、w3mを活用する。w3m-dumpオプションは、見た目に近い形でレンダリングした上でテキストにして出力する。 アメブロの執筆者はエディタ上の見た目で書いている可能性が高く、このようにレンダリングしてしまったほうがブレは抑えられる。

その中から情報を抽出する。 ある正規表現に一致する行、またはある正規表現の範囲をアドレスとして出力すれば多くの場合は十分だろう。

続くエントリや広告のゴミ値を拾ってしまわないように、エントリ終了時にはSedを終了させる。 多くの場合、/^AD$/で良いかと思うが、/いいね!した人.*リブログ/というパターンも考えられる。

例えばおやつ工房 ひびのやをdumpした結果からメニューを取得するとすれば、筆者はメニューの表記前に本日のメニューは、という行を置く習慣があるようで、またブログの締めとして本日もよろしくお願いいたしますを置いている。 これに基づけば

メニューそのものを抽出するならば、で囲むようにしているようなので、そこに限定しても良い。

しかし、どうもこれは項目であるようだから、メニューとしては成立しないかもしれない。 項目終わりは空行を空けているようなので、この項目から空行まで抽出してみよう。 一応、スペースを入れていた場合にも対応しておく。

しかし次のようなケースが発見された。

*天然酵母パン*
⚫︎食パン1斤→焼き上がりました
自家製カボス酵母、春よ恋使用。
乳製品不使用のシンプルなパンです。国産のお粉の素朴な風味をそのまま味わっていただけます。 

⚫︎レーズンとくるみのパン→焼き上がりました
自家製レモン酵母、春よ恋使用。

⚫︎カンパーニュ →焼き上がりました
自家製ラ・フランス酵母使用。
ライ麦粉と全粒粉を配合。

なので、項目開始だけでなく小項目開始も受け入れることにする。

結果次のように抽出できた。

*マフィン*
プレーン⬇
ポップシュガーに戻しました。
きのこみたい

*シフォンケーキ*
プレーン小

*天然酵母パン*
⚫食パン1斤→焼き上がりました
自家製カボス酵母、春よ恋使用。
乳製品不使用のシンプルなパンです。国産のお粉の素朴な風味をそのまま味わっていた
だけます。 

⚫レーズンとくるみのパン→焼き上がりました
自家製レモン酵母、春よ恋使用。

⚫カンパーニュ →焼き上がりました
自家製ラ・フランス酵母使用。
ライ麦粉と全粒粉を配合。

日付も入れるならば、投稿時タイムスタンプの行を追加しよう。

/[0-9]\{4\}-[0-1][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-5][0-9]/ p
/*.**\|^⚫/, /^\s*$/ p
/^AD$/ q
//いいね!した人.*リブログ/ q

なお、サンプルとして「ameblo 本日のメニュー」で検索して最上位に出たおやつ工房 ひびのやさんを使わせていただいた。 埼玉県越谷市にあり、東京スカイツリーライン大袋駅、せんげん台駅が最寄りとなる、パンとお菓子のお店だそうである。11時から18時営業、金曜日休業。

更新されたかどうか

単純に考えれば取得したページをteeで保存して比較したいところだが、 広告などランダムに生成される部分があるため、これは失敗する。

amebloはDateヘッダを含み、Content-Lengthヘッダを含まないため1、ヘッダ部分だけ保存するという方法もある。 curl--dump-headerを使用し、内容は捨てるという方法が良いだろう。

もうひとつはそもそも抽出した内容を比較するという方法がある。 リクエスト回数が減るため合理的だ。

いずれにせよ、前回取得分をローテーションして、今回取得分と比較する。

diff -q ~/.cache/someameblo/header ~/.cache/someameblo/header.prev > /dev/null

通知する

今回の場合、Discordのwebhookを使用している。

Twitterという手もある。ここではtwを使用する。 セルフDMの送信自体は可能だが、Twitter webや公式アプリでは確認/通知はなくなっている。

LINE Notifyという方法もある。

メールが配送できる環境からであればMMS宛というのも悪くない

ジョブスケジューラによる定期実行

スクリプトを書いても手動実行するというのはちょっとださい。 ジョブスケジューラによって自動的に実行してほしいところだ。

anacronあたりを使うのが妥当なのだろうけれど、私はSystemd Timer (User)を使用することにした。

まずはserviceユニットを書く。 ここでは~/.config/systemd/someameblo.serviceを作成している。

[Unit]
Description=Get ameblo entry.

[Service]
Type=simple
ExecStart=/bin/zsh "/home/aki/opt/someameblo/getamebloentry.zsh"

[Install]
WantedBy=default.target

次に対応したtimerユニットを書く。 ここでは起動から10分で初回実行し、それ以降4時間ごとに起動している。

[Unit]
Description=Get ameblo entry.

[Timer]
OnBootSec=10min
OnUnitActiveSec=4hour
Unit=someameblo.service

[Install]
WantedBy=timers.target

ユーザーユニットをリロードする。

タイマーを起動&有効化

「ゆるい解決」について

このようなユーティリティは基本的に人間を補助するものである。 オートメーションに属するものの、このユーテリティが人間に無断で何かを決断するわけではない。 また、人間にとってこのユーテリティによって与えられる唯一絶対の情報というわけでもない。

できるだけ精度が高ければ嬉しいけれども、もしも例外的ケース(例えば著者が普段とはまるで違う書き方をした場合など)においてうまく機能しなかったとしたならば、それは単に普通にブログを見に行けばいいだけである。 例外的ケースにおいて問題が発生しないように設計し、成功しない可能性については許容する。

同様に情報が常に完璧なフォーマットであることを求める必要もない。 ソースが完璧なフォーマットで書かれない可能性は高いので、完璧を求めることは不毛である可能性が高い。 これもまた、不十分な情報となったときには自らブログを見に行く選択肢もあるのだ。

ただし、十分に機能させるためには、抽出条件はfalse negative(取りこぼしがある)よりはfalse positive(ゴミがはいる)ほうが好ましい。 ゴミがあっても無視すればいいが、取りこぼしがあると必要な情報が損なわれる可能性があるためだ。

プログラミングはプログラムを書くことよりも、どのようにアプローチするかということのほうが重要な場合は少なくない。 プログラミング初級者のうちは設計をないがしろにしがちだが、プログラムの出来や困難性はだいたい設計によって決まる。 今回の主題も、正攻法ではかなり難しい条件を、考え方を変えることで簡単な問題に変換しているということだ。


  1. あるいはcurlがContent-Lengthを含まないのかもしれない

シェルスクリプト Forced Todo

Forced Todoはなかなか進まない進捗に対して、一定の時間でTODOを読み上げ、進捗を迫るものである。 あまりに進捗しないので作ってみた。

Gist

Systray

Notifyを開始するとSystrayに常駐し、Systrayから終了できるようにした。

Systrayへの常駐はMailDeliver2同様、yadを使うようにした。

notifypid=$!

yad --notification --image=player-time --text="TODO NOW!" --command="yad --text-info --timeout=15 --title=TODO --width 300 --height 300 --filename=$HOME/.yek/forcedtodo/todotext" --menu='Edit!'"$task_editor $HOME/.yek/forcedtodo/todotext"'|Quit!quit' --tooptip="Immidiately Task" 
kill $!

Yadのオプションについて分析してみよう。

オプション 意味
--notification Systrayに常駐する
--image=player-time Systrayのアイコンを選択
--text="TODO NOW!" Notificationの名前(ポップアップに影響)
--command 左クリック時の実行コマンド
--menu 右クリックメニュー。項目名とコマンドを!で分けている。quitで終了

ネストされているYadは次の通り

オプション 意味
--text-info テキストダイアログ
--timeout=15 15秒でダイアログが閉じる
--title=TODO ウィンドウタイトル
--width 300 ウィンドウの幅
--height 300 ウィンドウの高さ
--filename=... テキストの内容となるファイル

Zenityとは結構な違いがあることに注意。

Yadは通常ブロックするため、Yadが終了しなければ戻らない。 そのため、Notifyするためのプログラムをサブシェルでバックグラウンドプロセスとして実行し、そのプロセスID($!)を取得し、Yadが戻ったらKillすれば良い、ということになる。

Notify (Open JTalk)

Notifyは単一のプロセスIDを振るためにサブシェルで実行し、バックグラウンドプロセスとすることでループさせる。

(
  while sleep $notify_interval
  do
  perl -pe 's/\n/。/g' ~/.yek/forcedtodo/todotext | open_jtalk "$jtalk_tuning[@]" -m $hts_voicefile -x $mecab_dictdir -ow $jtalk_tmpfile
  play $jtalk_tmpfile >| /dev/null &|
  notify-send -t $notify_time "TODO" "$(<$HOME/.yek/forcedtodo/todotext)"
  done
) &

単純なsleepによるループである。

基本的な手順としては

  1. Open JTalkで音声ファイルを作成
  2. 合成した音声ファイルをバックグラウンドプロセスとして再生
  3. Notify Sendを使って通知に表示

となる。

ここで大きな情報はOpen JTalkだろう。

Open JTalkはスピーチシンセサイザーだが、eSpeak, SVOX Pico, Festivalといったソフトウェアと比べて非常に自然で、日本語に対応している。 ただし、かなり複雑なソフトウェアだ。

Manjaro(Arch)では、AURにあるopen-jtalkパッケージをインストールする。

NAIST JdicファイルはAURにも存在していないが、辞書ディレクトリとして/usr/share/open-jtalk/dic/を使用することができる。精度は異なるかもしれない。

NAIST JDicをインストールするためには、mecabをインストールし、さらにNaist JDicで/usr/libexecが指定されている部分を/usr/libに変更しなくてはいけない。

女声を使用するためにはopen-jtalk-voice-meiをインストールする。

Open JTalkは非常に自然で、Twitterの読み上げにも使えそうだが(twとの組み合わせがいいだろう)、複数行を扱うことができないため、Perlで変換している。 また、Open JTalkは標準出力に吐くことはできないようだ。