〇〇○○!ステーキ!

今日は妙に疲れてしまってやんぬるかな,という気分になっていた.力なく昼休みぼーっとしてると,某所で「いきなり!ステーキ!」を「いきり!ステーキ!」とか「いなり!ステーキ!」とかに改変する大喜利がはじまっていて,楽しく眺めてた.

結構バリエーションあるな〜と関心してて,ボタンで生成できるようにして眺めるとよいかもしれないという気分になったので,息抜きとしてランダム生成するページを作ろうと思い,CodePen で何も気にせず思いつくがままにコードを書き,そしてすんなりできた:

See the Pen ステーキ! by マンガーノ・伊藤 (@mangano_ito) on CodePen.

こういうのが出るとうれしい:

f:id:mangano-ito:20200904204330p:plain

まったく役に立たないものを自分のためだけに作るときとか,こういう知育玩具みたいなクソダサデザインができたときとかは,すごく楽しくて満たされた気分になるということに改めて気付かされた.

先頭に小文字は来ないから除外とか,言葉的な繋がりを考えて Trie 木とかをつくるとより自然になるかもしれない.しかしながら,そこまでしないことがこのツールをこのツールたらしめてる気もしている.

昼休みはずっとカチカチ回してクスクス笑って遊んでいた.人生は不安に満ちあふれている.

ラーメン (つけ麺)

昨日,景気づけにラーメンでも食べるか,とつけ麺を頼んで食べたら異常にしょっぱくて,えっこんな感じだっけとびっくりした.

以前なにか食べようって適当に入っておいしいじゃんだった店で頼んだけどなんかめっちゃしょっぱくてなぜだろうってなっている.すだちを絞っても改善されなくて何かが違うと思っている.

間違って飲み込んだ海水レベルにしょっぱくて,もしかして宅配のつけ麺って水割りする前提があって実は原液につけて食べてるからしょっぱいとかそういうことがないだろうかと気になっている.常識がないので正しい食べ方を理解していない可能性もある.

月末に健康診断があるのも手伝って,このような高塩分のものを食べ続けると血管によくないと感じていて,反面教師的に一年に一回摂取でいいのではないかと感じはじめている.

ゲーム (Apex) をするために Windows をデュアルブートすることにした (大奮闘)

Apex Legends というゲームをやろうという話になったのだけれど,これはどうにも Windows でしか動かなさそうなので,諦めて Windows を入れることにした.

www.ea.com

Apex の感想については一番最後に書いたので,結果を知りたい場合は飛ばすことができる.

Wine

ja.wikipedia.org

最初は Wine で動かそうとしていたが,Wine では動かないことが知られている.EAC (Easy Anti-cheat) があるので Wine と相性が悪いと評判.

Wine の互換性レポートでは Garbage という評価が与えられている:

https://appdb.winehq.org/objectManager.php?sClass=application&iId=19271

自分が試したときはそもそもセットアップまでいかなくて,まず前提になっている EA が提供している Origin というプラットフォームのインストールにことごとく失敗した.

f:id:mangano-ito:20200819083211p:plain
エラーが起きる

Steam Play

ところで,Steam はマルチプラットフォームで,Windows 以外のプラットフォームにもインストールできる.しかしながら,そのゲームが Windows 以外の実行形式を提供しているかは別問題で,たいてい Windows 以外では実際は動かないというおもしろい仕組みになっている.

GNU/Linux 版には Steam Play という機能があって,Wine を使ってゲームを動かすことで Windows 用のゲームを動かす仕組みが備わっている.

steamcommunity.com

デフォルトでは確認されたタイトルしか Stream Play を有効にできないのだけれど,設定ですべてのタイトルに関して有効にすることができる.

Cluedo

また話は変わるが,Cluedo というゲームが最近流行っている (YouTuber の間で).

store.steampowered.com

このゲームは Steam で遊べるので Steam で遊んだところ,Steam Play 対象タイトルではないので全タイトル有効にすることで問題なく遊ぶことができた.

f:id:mangano-ito:20200819083425p:plain
問題ない

Apex

そして Apex に立ち返ると,Steam 上では(まだ)提供されていないようなのでうまく行かないし,Steam Play でもうまく遊べるのか疑問だ.とすると,最早 Windows を入れてしまうほうが早い.

Virtualbox でできるのならいいのだけれど,確実にパフォーマンスは落ちるのでゲームの用途は厳しいのではないかと思われる.

ところで Reddit には r/ApexLegendsOnLinux/ という subreddit があって聖杯戦争になっている:

www.reddit.com

デュアルブート (めんどい)

デュアルブートすることにした.デュアルブートするには数ステップ必要で面倒臭さと戦う必要がある.

  1. パーティションをいじって Windows をインストールできるくらい空ける
  2. Windows をインストールする
  3. インストールしたらブートローダーを戻す
  4. Windows の環境をととのえる

なんか文字で書くと楽な気がしてくる.

パーティション

パーティションが時間的には一番かかる.というのも,普通は容量フルフルで割り当てているので,どこかのパーティションを縮小してから,未確保の領域を作らないといけなくてそれなりに時間がかかる操作になる.

今回は SSDGNU/Linux/ (root) パーティションを縮小して,50 GiB ほど Windows に割り当てるための未確保領域を作ることにした:

  • EFI Partition (FAT32; 100 MiB)
  • GNU/Linux システム (ext4; 200 GiB くらい) ← 縮小 させた
  • Windows 用 (unallocated; 50 GiB くらい) ← 作った

時間は測ってないけど結構かかった気がする.HDD だとどうだろうという気分になる.

Windows をインストールしよう

できたので Windowsインストーラーを起動させてマニュアルインストールすると「システムパーティションの容量が少ないよ〜」みたいなメッセージが出てきて進めないという現象にぶち当たった.

Windows 用のパーティションは 50 GiB あるので容量的には十分だけれど,EFI パーティションが 100 MiB あって GNU/Linux 用のファイルで 88 MiB ほど使われているので,Windows ブート用のファイルがおけない状態になっていた.

EFI パーティションを拡張しよう

なので,EFI パーティションを 100 MiB から 300 MiB くらいに拡張することにした.実際には後続の Linuxパーティションを後ろにずらして縮小してから -- (1),EFI パーティションをそのスキマを埋めるように拡張する -- (2) 操作になる.

アンマウントして作業する必要があるので,Ubuntu の Live 環境で GParted を使って操作する.こういうときに 1 つ Live 環境があると便利だけど,なかったので USB メモリに焼き焼きした.

ところがどっこい,GParted が中途半端にエラーが出て (1) ができたけど (2) に失敗して嫌な感じになってしまった.ファイルシステムは 100 MiB のママで,後続に 200 MiB の確保されない領域がスキマで空くことになってしまっている:

f:id:mangano-ito:20200819093446p:plain
300 MiB だけど警告が出てる

GParted (libparted) には謎の仕様があって,256 MB 以下の FAT32 を拡縮できない:

bugzilla.gnome.org

なのでその操作だけ失敗して歯抜けの状態になってしまう

f:id:mangano-ito:20200819020958p:plain
We're working on it!

ところで Microsoft のドキュメントでは

このパーティションの最小サイズは 100 MB であり、FAT32 ファイル形式を使用してフォーマットされている必要があります。

Advanced Format 4K ネイティブ ドライブ (セクターあたり 4 KB) ドライブの場合は、FAT32 ファイル形式の制限のために、最小サイズは 260 MB です。 FAT32 ドライブの最小パーティション サイズは、セクター サイズ (4 KB) x 65527 = 256 MB で計算されます。

となっていて複雑な事情がからんでいる:

docs.microsoft.com

f:id:mangano-ito:20200819095535p:plain
300 MiB だけど 200 MiB が排他的経済水域になっている

diskpart でがんばろう

Workaround としていったん ext4 にフォーマットしなおすと拡張できるので,その後 FAT32 にフォーマットし直してファイルを戻すというのが挙げられている.

嫌なので餅は餅屋ということで Windows で操作することにした.Windows のインストール前にコマンドラインが開けるので diskpart extend で拡張しようという魂胆だ.

ところがどっこい,EFI パーティションdiskpart では操作できないようになっている:

superuser.com

再び GParted

何回やらせるねんという感じで再び Ubuntu の Live 環境を起動して GParted でやることにした.もう素直に提案されていた方法で,マウントした HDD に EFI パーティション/boot をコピーしてフォーマットすることにした.

f:id:mangano-ito:20200819092152p:plain
適当なところに一時的にコピーすることにした

これはうまくいった.ext4 にしなくても ざっくりと FAT32 でフォーマットしなおせば 300 MiB のパーティションが確保できるしファイルシステムもうまく構築される.あとはコピーしておいたファイルを戻して,eps,boot のフラグをつければ元通りの EFI パーティションになる.

f:id:mangano-ito:20200819093704p:plain
フルフルで使えるようになった

この方法の問題としては EFI パーティションの UUID がフォーマットしたら変わるので,fstab とか grub とかが UUID で参照している箇所を変更する必要がある.もしかしたら UUID を固定することができたかもしれない.

通常どおりインストール

Windows のセットアップに戻ると今度はすんなりとインストールすることができた.インストール自体は数分で終わる.パーティションもろもろの操作のほうが時間がかかって計 4 時間くらいかかっている.

あとはグラフィックボードのドライバをインストールしたりもろもろの作業があるけど,再起動したりひたすら待つのに比べれば天国的な味わいだ.

最後に grub の設定を修正する

最後にこのままだと Windowsブートローダーに乗っ取られているし,UEFI セットアップから Linuxブートローダーを起動しても UUID が違っていてロードできないエラーが出た.

ので,再び Ubuntu の Live 環境に戻って,fstab を書き換えたり,普通のインストールのように各パーティションをマウントして chroot 後に grub-mkconfig して grub の設定をアップデートすればやっと完了となる.

f:id:mangano-ito:20200819092645p:plain
/etc/fstab

wiki.archlinux.jp

Windowsブートローダーに乗っ取られているのは UEFI セットアップで Linuxブートローダーを一番初めに読むようにして基本は Linux を起動させるようにする.ゲームをしたいときは起動時に F11 を連打してブートローダーを選択して起動することにした.

最終形

最終的には以下のようなパーティションの構成になった:

f:id:mangano-ito:20200819094333p:plain
決定版

Microsoft Reserved Partition はセットアップで勝手に切られる謎パーティションだ.

Windows インストール後に EFI パーティションは 113 MiB ほどになった.13 MiB のために大奮闘したことになる.なにかファイルを削除したりすれば余裕で空いたのではという説がある.

Apex

さて,Apex はというとバトルロイヤルの FPS ゲームで,最初に数名のパーティで飛行機から飛び降りて,適当な領地を漁って武器を手に入れて,どんどん活動可能範囲が狭まっていくなか,他パーティを倒して生き延びよう,というゲームだ.パフォーマンスは Windows 上なので当然まったく問題ない.

僕らはというと,1 人を除いて FPS 未経験なので 1 キルもすることができず,誰かに出くわすと即銃殺されるという蜂の巣体験を展開してトラウマが植え付けられて数回プレイしてロビーに戻ると全員無言になって終わった.

IE のビジュアルリグレッションテストを BrowserStack Automate で自動化しよう

Internet Explorer をサポートしている限り動作確認をする必要がある.Mac を使っているので動作確認は Microsoft 公式の VM イメージVirtualbox を起動して確認か,BrowserStack で確認することになる.

BrowserStack は便利である.お金はかかるけれど WindowsiPadiPhone やいろんなリアルデバイス上で動かせるし,ローカルネットワークでしか繋げない開発環境もアドオンによって確認できる.

qiita.com

とはいえ毎回手動で BrowserStack でサポートブラウザを確認していくのがつらくなってきた.時間がめちゃかかる.

自動化

で,BrowserStack Automate というのがあると教えてもらったので,休みを使って自動化ツール作成を試したら,ぽいのができた:

BSBS Report: my test

f:id:mangano-ito:20200607232707p:plain
BrowserStack Automate をつかった BackstopJS もどき

github.com

BrowserStack × BackstopJS だから BSBS.いろいろと未実装が多くて余裕で未完成となっている.

BrowserStack Automate

BrowserStack Automate は Selenium でかんたんに動かすことができる:

www.browserstack.com

普通に BrowserStack を使うような WebDriver を作成すれば,通常の Selenium のテストそのまま操作できるので悩むことはない.ダッシュボードで操作ログも確認できて便利.

f:id:mangano-ito:20200607040651p:plain
BrowserStack Automate Dashboard のログ

ちなみに操作を動画で録画してくれててあとで確認できたりして楽しい:

f:id:mangano-ito:20200608010554g:plain
動画で操作が記録される

Firebase TestLab とかもこういう機能があって楽しい.

どういうデバイスで動かすかという Capabilities はページ上で設定を生成する:

www.browserstack.com

ここに BrowserStack の ID と キー も埋まってて,そのまま使うことができる.ローカルネットワークを使うかもここで設定できる:

var capabilities = {
    "os": "Windows",
    "os_version": "10",
    "browserName": "IE",
    "browser_version": "11.0",
    "browserstack.local": "false",
    "browserstack.selenium_version": "3.5.2",
    "browserstack.user": "<ID>",
    "browserstack.key": "<KEY>",
};

これさえ設定しておけば,公式のガイドに沿って実装すれば Selenium でらくらく操作ができる.おかしいときはさっきのダッシュボードのログを見てデバッグができる.便利.

ところで

今業務では BackstopJS を使って production と開発環境のスクリーンショットでビジュアルリグレッションテストをしている.これがとても便利で,幾度となく本番環境の破壊を防いでくれている.僕ではない先輩エンジニア方が導入してくださった仕組みだが,控えめに言って最高である.

で,BackstopJS のバックエンド(というべきなのエンジンなのかよくわからないが)は Puppeteer である.BackstopJS は Puppeteer でヘッドレスブラウザを操作して,テストシナリオどおりに要素の更新を待ったり,特定の要素だけをキャプチャして,最終的に reference (お手本) と test (比較したいもの) のキャプチャ画像を知覚的に比較している.

ここに Puppeteer 互換のレイヤーを差し込めるような仕組みがあれば,このドライバで動かすこともできるだろうが,詳しくは調べていない.ただエンジンは変更できるっぽい設定ファイルがあったりするので余地があるかもしれない.

ということで,逆の発想で BackstopJS 互換で BackstopJS のテストシナリオを入力すれば,BackstopJS っぽく BrowserStack Automate でキャプチャ比較してくれればいいのではないかと思った.

試す

というわけでテストしてみる.

英語版の Google と日本語版の Google を比較する.まあ,それなりに一致するだろうし,余裕でテキストの部分が不一致になるだろう.単純にトップの要素だけを比較するものと,検索欄にテキストを入れて検索ボタンを押すシナリオを準備した.

f:id:mangano-ito:20200608012604g:plain
Selenium で操作している

シナリオの操作は Selenium の操作になり,BrowserStack Automate のサーバーにコマンドが送られる.BrowserStack Automate は要求した Capabilities にあったリアルデバイス上のブラウザを起動して,コマンドどおりの操作をしてくれる.

f:id:mangano-ito:20200608012629g:plain
ダッシュボードも随時ログが記録される

これを reference と test それぞれキャプチャし,差分比較を走らせてることで冒頭のレポートが作成される:

BSBS Report: my test

試したテストシナリオは以下だ:

module.exports = {
    "id": "my test",

    "scenarios": [
        {
            "label": "google-top",
            "url": "https://www.google.com/?hl=en",
            "referenceUrl": "https://www.google.co.jp/?hl=ja",
            "readySelector": "input[name='q']",
            "selectors": [
                "#hplogo",
                "#searchform"
            ],
            "removeSelectors": [
                "input[name='btnI']"
            ]
        }, 
        {
            "label": "google-search",
            "url": "https://www.google.com/?hl=en",
            "referenceUrl": "https://www.google.co.jp/?hl=ja",
            "readySelector": "input[name='q']",
            "selectors": [
                "#top_nav",
                "#appbar",
            ],
            "keyPressSelectors": [
                {
                    "selector": "input[name='q']",
                    "keyPress": "Backstop + BrowserStack awesome"
                },
            ], 
            "clickSelector": "input[name='btnK']",
            "postInteractionWait": 100,
            "removeSelectors": [
                "#hdtb-tls",
            ]
        }, 
    ]
};

スクショを撮る

Selenium には要素のスクリーンショットを撮るコマンドがありそうだったが,実際に BrowserStack Automate でコマンドを流してみると 404 でコマンドが実装されていない感じだったので諦めた.

f:id:mangano-ito:20200606222114p:plain
要素のスクリーンショットはエラーになる

{
    "status" : 404,
    "sessionId" : "<no session>",
    "value" : "Command not found: GET /session/.../element/.../screenshot"
}

そのためページ全体のスクリーンショットを撮って,対象の要素だけを画僧処理でクロップするという代物になった.要素に他の要素がオーバーレイしている場合とかに困ったことになる.

とりあえずクロップには jimp という JS だけでできてる画像処理ライブラリを使ってみることにした.ネイティブのものにくらべパフォーマンスは劣るかもしれないが,機能が揃っていそうでお手軽である:

github.com

シナリオ

シナリオの仕様は BackstopJS と互換にする.なのでなるべくそれと同じ動作になるように実装する.

BackstopJS のシナリオを Selenium で操作する変換をすることが今回の主な仕事になっている.だから,実際に使いそうなユースケースはチェックしたけど,実は動かない,未実装のケースが余裕である.

たとえば BackstopJS では不要な要素を削除したり隠したりする機能があるけど,Selenium 側では単純に要素を取得してきて,生の JavaScript のコマンドを発行して element.style.display = "none" を実行したりしている.

readySelector という要素が DOM 上に現れるまで待つ機能は,Selenium にもある条件が満たされるかループごとにチェックして待つ機能があるので,これである要素が現れるまで待つようにしている.

あとはシナリオの実行前後に任意のスクリプトを実行したりする機能があるけど,まだ実装していなかったりする.

ローカル開発環境とクッキーと認証

セッションを実現したいときとかはクッキーを設定する必要がある.けど Selenium には Cookie を設定する API があるのでバッチリだ.実装したけど実は試してない.最悪すぎる.

BrowserStack はアドオンでローカルネットワーク経由できるので,開発環境の確認もできる,というのがある.ひとえに BrowserStackLocal を起動しておいて,ローカル経由で接続するフラグを追加して Selenium を開始することになる.

差分検知

一番肝心なのが画像の diff を撮ることだ.まあ,普通にピクセルごとの差分を撮ればいいのだけれど,BackstopJS の diff は賢くできているようだ.

github.com

BackstopJS の差分検知部分は別のモジュールになっていて,diverged というパッケージになっていることを発見した.これを見ると単純なピクセルマッチのアルゴリズムではない結果がある.移動を見ていて賢く差分を見ることができるようになっている.

https://github.com/garris/diverged/raw/master/docs/diverged%20images/changeGraphDiff.png

今回は戦術した jimp に差分の実装があったのでお手軽に使うことにした.しきい値も指定できるので便利にできている.

感想

大体は BackstopJS っぽく振る舞うように互換レイヤーもどきを書く仕事になった.Puppeteer の互換レイヤーを書いて BackstopJS に組み込むほうが早いんじゃないかって気がしてきたが,せっかく始めたのでとりあえずやることにした.

ざっくりお試して書いて興に乗ってズイズイと勧めてしまった.未実装や未確認の部分が多いので,業務で必要なくらいには実装しておけば少し助けになるかもと思った.運用はしていないのでいろいろとアラが出そうではある.

展望

BrowserStack Automate は使いまくるにはお金が必要になってくる.無料だと 300 分のクオータなので 5 時間実行したら終了となる.Pull Request ごととか git push ごとに比較を走らせる未来になったら,あっという間にクオータがやってきて比較できなくなる.

f:id:mangano-ito:20200608011615p:plain
あと 294 分しかない

お金を払うと制限がなくなったりパラレルで実行できるテストの数が増えて,今みたいに直列でやって鬼のように時間がかかる,というのも改善できるメリットもある.プランはこちらだ:

www.browserstack.com

少なからずお金がかかるけど,並列がなくてよければ月 1.3 万円ほどでリアルデバイスのテストを自動化できる可能性を秘めている.

ということで将来的に GitHub Actions とかで CI できればいいですね,みたいな世界観が広がっている.多分余裕でできると思われるが,GitHub Actions は気になっているが手を出してないのでやってみると面白そうだ.

ところで,Virtualbox をリモートで走らせて操作自動化とかできるだろうか.それができたら IE のイメージを使ってできなくもないかもしれないと思ったけど,ライセンス的にどうなのかという懸念もある.

リモートワークをふりかえる

リモートワークを開始して 2 ヶ月くらい経ってるので,これまでの生活を振り返っておこうと思った.緊急事態が解除されるところもあるので.

個人の感想です.

所感

一言でいうと気に入っているのだった.

1. 通勤しなくてすむ

筆舌に尽くしがたいほど素晴らしい.東京で通勤なんてホントにしたくないことなのだ (ハム太郎)

通勤中に本を読む時間がなくなるというのは,電車より歩く時間のほうが長かったので活用はできていなかった.そうなるとリモートワークでは浮いた時間で本が読めて,家事もでき,よく眠れる.

心理的なところも大きい.やはり時間を気にしつつ人々の間をくぐり抜けていくのが無意識下で疲れる.なぜかよく通勤・通学のルートが嫌いになって,定期的に別の道をわざわざ探したりする謎の行動に出たりしていた.

仕事前に通勤という大エネルギー消費イベントがあるので,仕事に入るまでに必要なエネルギーが大きくなっている.実際オフィスで仕事に入ったら慣性が働いて辛くはない,という井戸型ポテンシャルになっている.

2. ご飯や休憩の裁量

お昼はお腹が減らなければ食べなくてもよく,減ったら食べればよいというスタイルになった. (定時のお昼を否定しているわけではなく,個人のあまのじゃくなのでどちらでもいい).

休憩は個人的にあまり会社はリラックスできる場所ではない.家だとリラックスできる.頭が痛いときはベッドで少し横になれるし,なにか考え事をしてて雑な姿勢になりたいときも,同僚に迷惑をかけずに雑な姿勢でコーディングしたりできる.

3. 仕事を続けたいときは続けられる

たまに仕事のキリが悪くて遅くまで仕事をしたいときがある.あと,調べ物とか明日への準備とかそういうもろもろのことをしたいとか.

そういうとき会社だと家に帰るということを意識しないといけないし,同僚的にもなにか仕事が大変そうだな,と無意識に負担をかけてしまう.やってることは勝手に個人的に調べておきたいこととかで,今日やらなくてもいいけどキリが悪いとかいうエゴによるものだったりする.

ダラダラしてしまうという意見ももっともで,すっぱりオフにしてしまうということが必要かとも思った.

4. 会議の場所に移動しなくてすむ

これが意外とよくってビデオ通話だと URL を開くだけで会議に参加できる.なので,興味ある人だけとかそういうミーティングも参加しやすいし,傍聴もできる.(会議は発言したほうがいいという話題もある).

5. 仕事中にテンション上がっていきなり歌を歌い始めても大丈夫

これもよい.テンション上がってオリジナルソングを歌い始めても誰にも迷惑をかけない.オフィスだと多分面談が設定されるだろう.

コードレビューでなんか頭に入ってこないときは,Pull Request の説明文やコードの流れを音読したりすると入ってきたりする.入ってこないこともある.find_all_favorite_japanese_food_without_tea_by_user_id($the_most_valuable_user_in_this_world) みたいな長い名前だったりすると読んで理解したくなる.

あとは設計とかで混乱してきたときも考えてることを言葉にしたりする.昔なにかの授業で聞いた声に出すと理解しやすくなるとか言われてたメソッドを実践したりしている.信憑性と効果があるかは不明だ.

6. 休憩時に家事ができる

これも素晴らしい.洗濯物を回して,ゴミを出して,とかその他そういう細々とした雑務をすることができる.

ほかにも

このブログを書きやすくなったのもある.思いついてメモしておいた記事や試みを少しずつすすめることがやりやすい.以前は通勤前とかにやって結局まとまらないことが多かった.

あとは通勤の制約がなくなるないし頻度が少なくなって家の選択肢が広がる,とかもある.家賃をはじめとする生活費を安く抑えるとかそういうこともできるかもしれない.通勤の課題がなければ都心に住みたいとかそういうのはないので,空気と治安が良く広いところに住みたい.

課題

いいところだけではなく悪いところも.よく語り尽くされていることだけど:

  1. 運動不足になる
  2. 職場の充実した福利厚生が得られない

(1) は以前は通勤くらいしかしていなかった,数少ない運動の機会がなくなったので運動ゼロになっている.インプットはそのままでアウトプットがなくなる状態なので大変キケンだ.気休めに室内で筋トレをしたりするが続いた試しはない.

(2) はオフィスのドリンクを飲んだりとかはできなくなるという残念ポイント.一日家で集中して飲み物を飲んだりしていると,案外すぐなくなったりするものである.オフィスだと補充していただけてるのでそんな心配はない.

気にならなかったこと

一方,よく言われているので気にならないのがある:

ディスプレイとか椅子がない,というのはこれはおうちでパソコンカタカタが大好きだったので,快適になるように揃えてあったので気になっていない.ただ,別途快適グッズを購入する必要はあった (後述).

f:id:mangano-ito:20200522165055j:plain
パソコンカタカタ

コミュニケーションは疎になる.自分はそんなに口頭で話せるタイプではなくて,事前にテキストで整理したい派なのと,Slack やビデオ通話があるのでそんなに気になっていないのがあるけど,他の人はどうかわからない.

しかしながら,仮に自分以外全員オフィスだったら情報格差心理的なところが気になるかも知れない.今のところ大きな課題は出ていない,と思うが,いろいろやりようはありそう.

不明

客観的・定量的に見てパフォーマンスが下がったかはわからないところ.そして調べていない.自分ではなく他人による評価のほうが重要かもしれない.自分がパフォーマンス出せると主張していても,周囲がそう評価できなければ仕事においてよくない.

おすすめグッズ

最後にうってかわっておすすめグッズ紹介.

1. USB Type-C <=> USB Type-A, HDMI 変換ケーブル

Macbook はこれがあると外部ディスプレイ接続やキーボード等の接続が捗る.ややこしいケーブルの相性も特に憂いなく 4k 60Hz も実現できた.(2) と組み合わせると捗る.

f:id:mangano-ito:20200522134447j:plain
HDMI ケーブルは Nintendo Switch のものをつかった

たまにガンってやると接触が悪くて表示されなかったり 4k で表示できなくなったりするファミコンみたいでかわいいやつだ.

2. USB 切替器

(1) と組み合わせて,私用の PC と会社用の Macbook の USB の接続をボタンで切り替えることができる.

f:id:mangano-ito:20200522134717j:plain
きりかえ君 (写真が下手すぎる)

会社のキーボードやマウスを持って帰ることはできたのだけれど,それぞれ 2台あるのもややこしいと思ったので持って帰らなかった.ただ毎回つなぎ替えるのは煩わしいので,ボタンで切り替えられるようにした.いつものデバイスをそのままの位置で使えるし,私的なことをするときは私用 PC に切り替えることができる.

ただ切り替えに 2 秒くらいかかるのと,ワイヤレスのデバイスホットスワップできなくてワイヤードにしているという悲劇があるので,場所に余裕があれば2台使うのもいいかもしれない.

結局

リモートワークで家で仕事をすることを気に入るかは,やっている仕事や環境・個人の性格によって大きく変わると思われる.だから楽観的なことを書いてて気に障ったらごめんなさい.

実際,内向的か外交的かというある種ステレオタイプ的な分け方で捉えられ方が違うとか言われている記事がいろいろある:

gigazine.net

もちろん僕は自他ともに認める内向的性格で自分の家が最高の防衛基地になっている.

JS の日時操作ライブラリを比較する: date-fns のインターフェイスがイカす

日付を操作する必要があったので,いつものように Moment.js を使おうとした.JS ビルトインの Date は操作を行うにはあまりにも使いづらいので,補助的なライブラリを使うのが定石になっている.8 より前の Java でいう Joda-Time みたいな存在.

リファレンスを見るために Moment.js の公式ページに行ったら,なにやら Luxon という新しいライブラリがあることに気づいた.これは Next Moment.js 的な新たに書き直されたライブラリらしい:

moment.github.io

そういうことを調べているうちに他の日付操作のライブラリを見つけた.Day.jsdate-fns だ.

どう違うのか

それぞれがどう違うのか.概要で比較するとこういう特徴があるように見えた:

ライブラリ 特徴
Moment.js よく使われている.moment() という関数から日付のオブジェクトを作成して,そこからメソッドチェーンで操作ができることが特徴.jQuery Object みたいなインターフェイスになっている.タイムゾーンについてはアドオンが必要.
Day.js Moment.js と互換のインターフェイスを持っているが 2kB と軽量になっていることが特徴
Luxon Moment.js と同じところで開発されているが,インターフェイスがまったく異なっている.よくあるオブジェクト指向の日付操作のライブラリのように DateTime, Interval, Duration などの役割ごとのクラスのインスタンスから操作を行うのが特徴.Immutable になっていることも特徴.
date-fns 関数型のインターフェイスが特徴.lodash のように時刻操作に対する種々の関数があり,通常の Date オブジェクトに対するヘルパーとして働くのが特徴

ISO 8601 形式の表現 からのインスタンスの作成で比較するとわかりやすいかもしれない:

ライブラリ ISO 8601 形式からの作成 返ってくる型
Moment.js
import moment from 'moment';
moment('2020-04-01T00:00:00');
Moment
Day.js
import dayjs from 'dayjs';
dayjs('2020-04-01T00:00:00');
DayJs
Luxon
import DateTime from 'luxon/src/datetime.js';
DateTime.fromISO("2020-04-01T00:00:00");
DateTime
date-fns
import parseISO from 'date-fns/parseISO';
parseISO('2020-04-01T00:00:00');
Date (builtin)

インターフェイスの違いがよくわかる.

  1. Moment.js, Day.js はどちらも jQuery, $ 関数のようなファクトリ関数に文字列を渡すことで暗黙的にパースされる.
  2. Luxon は明示的に DateTime.fromISO という static なファクトリメソッドを経由して作成する.
  3. date-fns は parseISO というパースする関数を import して使い,ビルトインの Date インスタンスを作成する.

というものになっている.

考察

(1) はなんでも受け取るファクトリ関数内で,与えられた引数に応じて暗黙的に判断されたオーバーロード関数があるかのようにふるまい,値をラップしたオブジェクトを返すインターフェイスだ.ちょうど jQuery$(...) や lodash の _(...) のようなラッパーのライブラリにたまに見られるやつ.

(2) はごくごく標準的なオブジェクト指向のライブラリのインターフェイスだと思う.与える値の意味に応じて使用者側が明示的にクラスのファクトリメソッドを呼び出して生成するようになっている.返り値も DateTime と明示的で具体的にものになっている.

(3) はヘルパー関数を適用するだけになっている.返ってくるオブジェクトもビルトインの Date になっている.だからライブラリ特有のラッパークラスを使わないようになっている.

date-fns のなにが面白いか

この中で date-fns が面白いと思った理由は,その関数型のアプローチにある.

たとえば他のライブラリ同様に,開始日時と終了日時を持つ Interval というものがある.これはたとえば 2020-04-01 12:002020-05-01 12:00 までという日時の範囲をあらわすのに使われ,いかにも欲しくなりそうな概念だ.

dete-fns はこの Intervalインターフェイスのみ定義している.具体的なラッパークラスではなく,あくまで要求する型特性になっている:

date-fns.org

type Interval = {
  start: Date | number
  end: Date | number
}

date-fns/typings.d.ts at 21088144134ab581ea3a1f77485c645781534198 · date-fns/date-fns · GitHub

だから,この特性をとるオブジェクトであれば Interval として与えることができるようになっている.ここが面白い点になっている.

// ok
const int1 = {
    start: parseISO('2020-05-01T00:00:00'), // Date
    end:   parseISO('2020-07-01T00:00:00'), // Date
};
const dur1 = intervalToDuration(int1);
console.log(dur1); // Object {days: 0, hours: 0, minutes: 0, months: 2, seconds: 0, years: 0}

https://runkit.com/mangano-ito/5ec2128c129f090013b3d455

同様に intervalToDuration の返り値として Interval から計算された期間をあらわす Duration も型のみを定義している.

もちろん同じインターフェイスをもつクラスインスタンスでも可だ (Object だし):

// this is also ok.
const int2 = new class {
    get start() {
        return parseISO('2020-05-01T00:00:00');
    }
    
    get end() {
        return parseISO('2020-07-01T00:00:00');
    }
};
const dur2 = intervalToDuration(int2);
console.log(dur2);

https://runkit.com/mangano-ito/5ec21304a57d1b001ab1eeca

魔改造だけど Array.prototypegetter を生やしてもいける:

const int3 = [
    parseISO('2020-05-01T00:00:00'),
    parseISO('2020-07-01T00:00:00'),
];

Object.defineProperty(
    Array.prototype,
    'start',
    {
        get: function() { return this[0]; },
    },
);
Object.defineProperty(
    Array.prototype,
    'end',
    {
        get: function() { return this[1]; },
    },
);

// this is ok, too!
const dur3 = intervalToDuration(int3);
console.log(dur3);

https://runkit.com/mangano-ito/5ec2132e25d80b001b43d325

他方,Luxon は Interval 等をライブラリのラッパークラスとしている.

const int1 = luxon.Interval.fromDateTimes(
    luxon.DateTime.fromISO('2020-05-01T00:00:00'),
    luxon.DateTime.fromISO('2020-07-01T00:00:00'),
);
const dur1 = int1.toDuration();
console.log(dur1.toFormat("M 'months'")); // "2 months"

https://runkit.com/mangano-ito/5ec211dbb51f20001a795e52

Moment.js の IntervalDuration は独特だ:

const dur1 = moment('2020-05-01T00:00:00').to(moment('2020-07-01T00:00:00'));
console.log(dur1); // "in 2 months"

https://runkit.com/mangano-ito/5ec212564de79e001ba6e774

Day.JS では RelativeTime プラグインが必要だ.(ライブラリの軽量化のためかな?)

感想

有り体に言えば,オブジェクト指向のアプローチと関数型のアプローチの違い,というだけかもしれないが,date-fns は JavaScript の特性にうまく合った lodash のようなポリシーを持つ日時操作のライブラリとして面白いと思った.

そして,ドメイン内に外部ライブラリの概念を持ち込まないでよいところに良さを感じた.扱うデータは素朴な構造体でいいことになっている.ライブラリはあくまでインターフェイスに対するユースケースだけのシンプルな設計になっている.struct, trait, impl みたいなかんじ.

しかしながらオブジェクト指向型のインターフェイスのほうが使いやすいというのはあるかもしれない.補完も効きやすいだろうし.外部のモデルを持ち込んでそのまま合成して使えるというのは楽だったりする.なので別に関数型至上主義というわけではない.

玉虫色の結論.いずれにせよ生の Date を手で計算するのは辛いのでさけたい.

CSS を JS で拡張したいという思いとアイデアと現状

CSSJavaScript で拡張できないかと思っていた.デザイナーは簡単に表現できることが増えるし,エンジニアは保守性が上がってよさそうだな,ということを考えていた.

ところで

こういう提案はすでにあって,ブラウザの API 経由で CSS を拡張できる experimental な実装があるようだ.CSS Houdini と呼ばれている.各ブラウザの実装状況は Is Houdini Ready Yet? というサイトで確認できる:

ishoudinireadyyet.com

ちなみに Houdini は フーディニ と発音するようで,どういう意味か調べたら An escape artist という意味らしい引田天功みたいな人ってことかな.信憑性が疑わしい.

CSS Houdini

CSS Houdini がどういう使い方ができるか調べてみたら,Chrome が先行していろいろ実装していてデモが確認できる https://googlechromelabs.github.io/houdini-samples/

Layout API は JS 側で要素のレイアウトを構成したり,Paint APICanvas で要素を自由にレンダーできるようになっている.特に Paint API の例にある QR コードの例はおもしろい:
https://googlechromelabs.github.io/houdini-samples/paint-worklet/qr-code/

f:id:mangano-ito:20200512204020p:plain
QR コードが表示される <textarea>

<textarea> だけど QR コードがレンダーされている謎な状況になっている.エンコードするデータは --text カスタムプロパティによって CSS に渡されている:

github.com

地味に重要な機能として Properties & Values API というのがある.CSS のカスタムプロパティ (--prop) の型をJS から定義できる.JS から CSS にデータを渡すのに使えるし,CSS アニメーションでは補完が効くようになったりするのが嬉しいポイントになっている.後に出てくる試みでもふんだんに使っている.

CSS Houdini+

ところで,CSS Houdini では関数やセレクタの拡張は現時点では特に見込みがなさそうだった.Parser API はそれを試みようとしていそうだが,ステータスを見てもわかるようにまだまだ構想段階という感じがしている.なので,CSS メタプログラミングに傾倒してめちゃくちゃカスタマイズしたいという野望は当面実現しがたい.

これを現時点でがんばるなら,CSSプリプロセスして変更した CSS + それのヘルパー JS を emit する仕組みが必要と思った.webpack とか parcel でひとまとめにできればそこまで煩わしくもない気もする.polyfill みたいにブラウザネイティブな API で同じことができるようになったとき,ネイティブ実装に移譲できるようになっているとか.と考えたところで babel を思い出して考えるのをやめた.

CSS を拡張したいという試み

さて,もし CSS Houdini のようなかたちで CSS のプロパティや関数,セレクタが拡張できるとしたらどうだろう.自分が思いついた「僕の考えたクールな CSS の拡張の提案」と現時点での実装をした.多くは CSS Houdini の機能を使っていて Chrome でないと完全な動作はしない代物になっている.

(※ 以下はすべてあったらいいなという妄想の構文なので,実際には使えないということを注意されたい)

::nth-letter(i) 疑似要素

指定した要素のある位置の文字を修飾するための疑似要素の提案:

<p id="my-paragraph">This is a paragraph.</p>
p#my-paragraph::nth-letter(2n + 1) {
    color: red;
    text-transform: uppercase;
}

たとえば この例はこういう結果を得る:

f:id:mangano-ito:20200512232408p:plain
こういう感じ

なんの役に立つのか,というとクラシックな Web サイトでよく使われていた意外とマークアップがめんどうくさい
文字を表現したい,という需要にも応えることができる.

今できる範囲で実現するとこうなる:

See the Pen :nth-letter(expression) by マンガーノ・伊藤 (@mangano_ito) on CodePen.

https://codepen.io/mangano_ito/pen/NWGMvBq

この例は適当だけど,実際は他の兄弟要素があっても影響が出ないように,Shadow DOM にしたり,innerHTML で雑に置き換えたりしないように考慮しないといけないとか,そういう課題が山積みになっている.

ちなみに Chrome だと CSS Houdini の成果によりレインボー文字がアニメーションするようになっている. Firefox では動かないのでプログレッシブ・エンハンスメントという名の未対応になっている:

f:id:mangano-ito:20200512195928g:plain
きれい

このアニメーションは先述した Properties & Values API でカスタムプロパティに型を定義することで実現している.そうすると値を @keyframes0360 に無限ループで連続的に変化させることができるので,hsl の色相が 0 度 〜 360 度でトランジションするようになっている:

:root {
    --angle: 0;
    animation: angle_around 1s linear 0s infinite;
}

@keyframes angle_around {
      0% { --angle: 0; }
    100% { --angle: 360; }
}

#letter {
    $h: calc(var(--angle) * 1deg);
    color: hsl($h, 75%, 50%);
}

dataset(selector) 関数

<element data-prop="1234" /> において rule: dataset(prop); で対応する data-prop 属性の値を使う関数.

attr(data-*) すればいいのでは,という話題があるけど,attrcontent 以外の多くのプロパティで使えない感じがしている.あまねくプロパティで属性値の変化に応じてリアクティブにスタイルが変わってほしい.型は文脈によって推論されたい.

そうすれば,たとえばプログレスバー的なコンポーネントに対して data-progress に進捗度合いを設定することによって,CSS だけでプログレスバーのアニメーションを実現できたりするだろう:

<p id="progress" data-progress="0.2">In Progress...</p>
#progress {
    $color: hsl(100, 50%, 50%);
    $progress: calc(dataset(progress) * 100%);
    background-image: linear-gradient(
        to right,
        $color      0%,
        $color      $progress,
        transparent $progress,
        transparent 100%
    );
    transition: background-image 0.25s ease-in-out 0.1s;
}

f:id:mangano-ito:20200512224031g:plain
よくあるプログレスバー

現時点でこれを実現するためには JS を経由してカスタムプロパティを設定する.例によって Properties & Values API が使えない Firefox では動かない:

See the Pen dataset(selector) by マンガーノ・伊藤 (@mangano_ito) on CodePen.

https://codepen.io/mangano_ito/pen/gOazXov

ここでは MutationObserverdata-progress の変化をウォッチして,変化があったら --prop--progress カスタムプロパティに設定するようになっている.JS → HTML → JS → CSS という遠回りな値のバケツリレーになっている.

scroll-y プロパティ

これは単にビューポートのスクロールした値が渡ってくるプロパティ.Android<CoordinatorLayout> みたいなネイティブアプリのプロフィール画面でよくあるパララックスっぽい効果とかに活用できると思った:

これは現時点でも単に --scroll-ywindow.scrollY を渡せばできる.

See the Pen CoordinatorLayout by マンガーノ・伊藤 (@mangano_ito) on CodePen.

https://codepen.io/mangano_ito/pen/PoPeear

例の理由で Firefox ではパララックスにはならないけど読める.requestAnimationFrame でガチャガチャやっているので重い.ちなみに画像は CodePen で用意されている Assets 使っている.

f:id:mangano-ito:20200512230700g:plain
Jane さんのプロフィール

しかしながら HTML と TypeScript はシンプルだが,まあ SCSS はとても汚い書き方になっている.Dev Tools のアニメーションのタイムラインを見るといかにも重そうな感じになっている:

f:id:mangano-ito:20200512234256p:plain
Dev Tools > Animations

CSS 側でいろいろ計算をやろうとするとカオスになってくる.スクロールの振る舞いはやっぱり JS で計算したほうがいいのでは,となって議論が一周してきたりした.

randomOf(values...) 関数

これは与えられた values... からランダムに値を選択するための関数:

p {
    color: randomOf(red, blue, yellow);
}

こうすると #my-class はページをリロードするたびに red, blue, yellow のどれかが毎回ランダムに選択されるようになる.こういう感じ:

See the Pen random_of(...values) by マンガーノ・伊藤 (@mangano_ito) on CodePen.

めちゃくちゃ地味だけど,これは業務で実際に実装する必要があったから欲しいと思っている人はいるはず.JS 側で動的に style を変更したり,パターンごとに複数のクラスを生やして JS 側でクラスを与える必要がなくなる.それだけといえばそれだけ.

:within-viewport 疑似セレクタ

この疑似セレクタは 要素がビューポートに入っていれば有効になるセレクタだ.現時点では InsersectionObserver を使ってクラスを当てることで実現している:

See the Pen :within-viewport by マンガーノ・伊藤 (@mangano_ito) on CodePen.

https://codepen.io/mangano_ito/pen/oNjdore

またもや地味.これくらいはわざわざ専用のセレクタにしなくても,普通に JS で class を設定されるほうが素直な気がしている.

感想

意見がまとまらなかったので箇条書きにした:

  • CSS でデータからプレゼンテーションの要素を分離するコンセプトは素晴らしい
  • CSS を拡張できたら,JS はビジネスロジックのデータだけを扱えば済むし,デザインを変えたいときは CSS だけを変更するので済むのでは.(そんなに甘くないかな)
  • 結局,現時点でも JS で style をゴリゴリすれば大抵のことは実現できるので充分とも言える
  • カオスにならない程度に CSS を拡張できると面白そう

リモートワークだしマイスーパーリモートコントロールつくりませんか

リモートワークしてるからリモコンつくろうかなと思った.数年前に Arduino で赤外線リモコン作って塩漬けにしてたので,そのときのコードを使ってなにかできそうだ,という感じのモチベーションだ.

できあがったもの

表現しづらいので動画を撮ることにした:

f:id:mangano-ito:20200507025822g:plain
こんな感じでブラウザから電灯が操作できた

コードはこちら:

github.com

概要

f:id:mangano-ito:20200505155503p:plain
全体像

Arduino 側で赤外線通信し,PC 側で それとシリアル通信する API サーバーをたてることにより,適当な REST API リクエストを送って赤外線操作できるようになった.クライアントサイドはただリクエストをなげればいい.

用意

用意するのは

  • Arduino (なんでもいい)
  • 赤外線受光センサー (なんでもいい)
  • 赤外線 LED (なんでもいい)
  • ブレッドボード (なんでもいい)

ホントになんでもいい.共立電子とか秋月電子とかでパーツを買ってくればいい.どこの電気街にもパーツ屋は存在するし,ステイホームということで通販をつかって待ち遠しいのも楽しい.

Arduino もなんでもよくて,なんなら Arduino でなくてもいい.GPIO があればいい.使ったのは Arduino Duemilanove というやつで,2009 年 大学 1 年のときに買ったもの.ちなみに Duemilanove とは 2009 という意味.

ブレッドボードもなんでもいい.なんでもいいって言ったけど,昔安いのを買ったら配線が微妙につながってなくて原因不明のデバッグに長時間要したことがあったので,それなりのものを選ぶのがよいのかも.やる気があればユニバーサル基板ではんだ付けしてもいいが,今回その価値はなさそうだ.

電子回路の知識はないので信憑性に自信はなく,語れることはない.

赤外線センサーを接続する

赤外線センサーは適当にぶっさす.大抵同じだと思うので自分の例で解説.

f:id:mangano-ito:20200507073934j:plain
赤外線センサー

データシートを見てピンの用途を確認する.左のピンから (1) Vout の検出した信号,真ん中のピンが (2) GND,右のピンが (3) Vcc となっていた.昔授業で素子の Vcc と GND を間違えた結果,触ったらやけどするほど熱くなって煙を出したことがあるので注意が必要.ハードウェアは厳しい.

f:id:mangano-ito:20200507074339j:plain
ぶっさす

なので Arduino の +5V を (3) に,GND を (2) に接続する.(1) は適当にデジタル 4 とかに接続する.

赤外線 LED を接続する

赤外線 LED も適当にぶっさす.これも普通の LED と同じで長い足がアノードで,短い足がカソード.これも Arduino のデジタル 3 とかをアノードに,カソードを GND に接続する.

f:id:mangano-ito:20200507074245j:plain
赤外線 LED とたぶんそれのデータシート

昔何個か買って数個ぶっ壊したのでこのデータシートが本当にこの LED のものかは不明だけど,グッズのなかに残ってたのでこれだと思うことにする.

f:id:mangano-ito:20200505162933j:plain
アソート

完全余談で,潰さないためと確認用にデバッグ中は適当な LED をつけておくとよい.赤外線 LED は不可視なのでカメラを使わないと本当に発してるのかわからない.適当な LED 詰め合わせを買ってたけどこんなには要らない.

f:id:mangano-ito:20200505162755j:plain
雑にぶっさす

抵抗は? となるのだけれど,入れたらなんか光量が足りなすぎたので直接接続することにした.素子にダメージを与えるのでよくないかもしれない.計算して必要に応じて適切な抵抗をいれるのが本当はよいでしょう.

Arduino でコーディングする

Arduino 側の作業になる.やることは C 言語でコーディングするだけだ (ArduinoC++ でもいいが STL もないしパワーもないので結局 Better C になる).

  1. 入力/出力ピンやシリアルポートの設定
  2. 赤外線の入力をバッファに入れる
  3. 赤外線 LED にバッファの内容を出力
  4. シリアル通信で入力や出力を操作できるようにする

なぜシリアル通信するのかというと, PC で操作したいという理由がまずある.そしてArduino 側でいろいろするのはスペック的にも言語的にも厳しい.ので,Arduino はただの赤外線入力/出力に徹してもらって,PC 側でいい感じの言語で計算をやってシリアル通信でやりとりしよう,というモチベーションになる.

赤外線リモコンの仕様については微妙にややこしいが,以下のページがズバリ参考になる:

310web.ddo.jp

赤外線センサーの入力は各値が変化するまでの時間をバッファに記録する.値は 0, 1, 0, 1 と交互に来て,その長さが異なるだけなので長さだけを記録しておけばいいからだ.

赤外線 LED の出力時のポイントは,リモコンの赤外線は 38 kHz で変調されているということ.漫然と 1 で光らせて,0 で引かせない,では反応しない.なので 38 kHz の周期で 0 / 1 をパタパタさせればいい.出したい値が 1 (HIGH) のときは変調した信号を送り続け,0 (LOW) のときは何も出さない,という実装になる.

さて,HIGH と LOW の継続させる長さがデータの 0 / 1 を表すのだが,学習リモコンであれば何も考えずにセンサーの入力をそのままフォワードすればいい.つまりバッファに記録しておいた各値の長さで信号を流し続ける感じになる.(センサーからの出力は復調後のものになっている).

ちなみに ArduinodigitalWrite 関数は結構遅いので,レジスタを直接いじらないとマイクロ秒オーダーではタイミングが合わない現象に見舞われた.

my-super-remote/arduino.ino at master · mangano-ito/my-super-remote · GitHub

久しぶりに温かみのある感じのコテコテの C っぽいコードを書いた. ところで Arduino には赤外線のデコードやエンコードをするライブラリがあるようで,自分で実装する必要はない.

API サーバー

シリアル通信ができればなんでもいい,ので大抵の言語で可能だ (ホントかな). 今回は PythonFastAPI + pySerial というのを使った.理由は昔 pySerial を使ったことがあるから.

my-super-remote/api.py at master · mangano-ito/my-super-remote · GitHub

curlPOST すると電灯がピッっていうのでおもしろい.

作ってからおもったのは,普通に node.js で NestJSnode-serialport とかを使って実現するとよいと思った. TypeScript で isomorphic にサーバーサイドとクライアントサイドが構築できてオシャレかも.

クライアントサイド

やることは REST のリクエストを送るだけなのでなんでもいい.とりあえず Web ベースの簡易的なダッシュボードを作った. View 側は Hooks を使ってみたかったので React を使ってみることにした.

my-super-remote/app.tsx at master · mangano-ito/my-super-remote · GitHub

これでリモコンの電池が切れても安心だ.

ボタンを押すと該当の信号のリクエストが飛んでピッってなるしくみ. ボタンのアニメーションは SoundCloud を意識しているが失敗だろう.

その他

応用として cron で定時に電灯やエアコンをつけたり消したりできる. けど,最近の電灯は普通にタイマーがあったりするのであまり意味はない. 光センサーと組み合わせて,暗くなったら電灯をつける,とかはできそうだ.

外出先から遠隔操作できるようにするには,普通にサーバーを公開するか, Firebase Realtime Database みたいなのをリクエストのキューにして, listen していたローカルのサービスがイベントが来たら操作することもできそう.

最近名探偵コナンをよく観ているのでトリックに使えるとかくだらないことを考えていた.

最後に

自分の家は東京の狭い 1K 8畳だし,リモコンは手を伸ばせば届くし, リモコンを使う機器も電灯とエアコンしかないし,リモートワークで更に外に出なくなったしで, 活用できる予定はいまのところない.

シーケンスをプログラムして踊ることはできる

夏休みの自由研究や工作のネタにはできそうだ.

あなたの window.open はなぜ開かないのか,Chrome で

先日 window.open をしようとしたらポップアップブロッカーに阻まれて open することができなかった.

f:id:mangano-ito:20200430132359p:plain
Blocked

まあ,これならよくあることなのだが,いかんせん自分の記憶では onClick のようなユーザーのアクション内で開かれた window.open は阻まれないことになってると思っていた.だからそのときも onClickイベントハンドラ内で window.open したから大丈夫だろう,と思っていたら,見事にブロックされてしまったのでなぜだろう,となっていた.

検証

なので,検証するために 3 つのケースを用意してみた:

検証ページを用意したのであなたの環境でも試してみてね♥

今回試すブラウザは Google Chrome を前提にしてます

ケース1

const immediate = () => {
    window.open('https://www.google.co.jp/');
};

document.getElementById('button_1').addEventListener('click', immediate);

これはユーザーの click 経由だから素直に開く.

ケース2

次にこれを非同期にして (async),6 秒待ってから window.open するようにしてみる:

const deferred = async () => {
    await new Promise((resolve) => {
        setTimeout(resolve, 6000);
    });
    window.open('https://www.google.co.jp/');
};

document.getElementById('button_2').addEventListener('click', deferred);

f:id:mangano-ito:20200430132508g:plain
じっとまってたらブロックされた

これは開かない.ポップアップブロックされる.しかし,たまに開くケースもある.

ケース3

任意の秒数を与えられるようにしてみる:

const deferred = async (waitSeconds: number = 6) => {
    await new Promise((resolve) => {
        setTimeout(resolve, 1000 * waitSeconds);
    });
    window.open('https://www.google.co.jp/');
};

document.getElementById('button_3').addEventListener('click', () => deferred(3));

3 秒にするといつも開く.

意味がわからなかったので

Chromiumソースコードからポップアップブロッカーの実装にあたってみた.ポップアップブロッカーのコードはここだ:

github.com

ところでポップアップがブロックされる理由は 2 種類ある:

// Classifies what caused a popup to be blocked.
enum class PopupBlockType {
  kNotBlocked,

  // Popup blocked due to no user gesture.
  kNoGesture,
  // Popup blocked due to the abusive popup blocker.
  kAbusive,
};

kNoGesture はユーザーの操作なくスクリプトが勝手にポップアップしたとみなされた場合 (ユーザーの意図,とも訳せるかも知れない).kAbusive は特に不正とされたページでポップアップが開いてブロックされた場合だ.3 種類あるやん,って思うかもしれないですが,kNotBlocked はそのとおり not blocked である.

デバッグ

実際にさっきのページのケースがどうだったか,Chromiumデバッグビルドしてデバッガーで確認してみることにした.

f:id:mangano-ito:20200429211020p:plain
kNoGesture

結果としては kNoGesture のケースだったことになる.そもそも不正なページではないので残ったケースといえばそうなる.

しかし,この部分からスタックトレースを追っていくと,ユーザー操作があったかのフラグである user_gesture の真偽値はどこから来たかがわかる.

f:id:mangano-ito:20200429211129p:plain
スタックトレースをたどっていく

RenderFrameHostImpl::CreateNewWindow のあたりに面白いコードがある:

  bool effective_transient_activation_state =
      params->allow_popup || frame_tree_node_->HasTransientUserActivation();

  // Ignore window creation when sent from a frame that's not current or
  // created.
  bool can_create_window =
      IsCurrent() && render_frame_created_ &&
      GetContentClient()->browser()->CanCreateWindow(
          this, GetLastCommittedURL(), GetMainFrame()->GetLastCommittedURL(),
          last_committed_origin_, params->window_container_type,
          params->target_url, params->referrer.To<Referrer>(),
          params->frame_name, params->disposition, *params->features,
          effective_transient_activation_state, params->opener_suppressed,
          &no_javascript_access);

chromium/render_frame_host_impl.cc at master · chromium/chromium · GitHub

ここの effective_transient_activation_state がここからの呼び出しにおける user_gesture フラグの真偽値になっている.frame_tree_node_->HasTransientUserActivation() という関数がポイントだ.UserActivationState というクラスの状態がユーザーの操作があったかどうかを管理している.

User Activation

ところで,このユーザーの操作があったか,というコンセプトは User Activation v2 と呼ばれているようで,コメントに詳細な解説がある (下のコメントは長いので中略している):

// This class represents the user activation state of a frame.  It maintains two
// bits of information: whether this frame has ever seen an activation in its
// lifetime (the sticky bit), and whether this frame has a current activation
// that is neither expired nor consumed (the transient bit).  See User
// Activation v2 (UAv2) links below for more info.
//
// More Info
// =========
//
// - UAv2 explainer: https://mustaqahmed.github.io/user-activation-v2
// - Main design:
//   https://docs.google.com/a/chromium.org/document/d/1erpl1yqJlc1pH0QvVVmi1s3WzqQLsEXTLLh6VuYp228
// - Browser-side replication for OOPIFs:
//   https://docs.google.com/document/d/1XL3vCedkqL65ueaGVD-kfB5RnnrnTaxLc7kmU91oerg
class BLINK_COMMON_EXPORT UserActivationState {

chromium/user_activation_state.h at 7709ebb8b7d8b373a1f9cc7ab3b7ecf6a1474f81 · chromium/chromium · GitHub

DesignDoc と解説のページがあるので,詳細なところはそこを見てもらうとして,「ユーザーが意図的ななにかをしたか」というのが管理されるようになっている:

mustaqahmed.github.io

今回のケースにおいて,このモデルが関与しているのは次の二点:

  1. ユーザーはそのページを見たか?
  2. ユーザーはそのページでクリック等の操作をしたか?

となっていて

  • (2) は 一定時間経ってリセットされる
  • (2) は window.open のような操作後もリセットされる

Chromium の実装に戻ると.UserActivationState では (2) の一定時間が定数で定義されている.それは次のようになっている:

// The expiry time should be long enough to allow network round trips even in a
// very slow connection (to support xhr-like calls with user activation), yet
// not too long to make an "unattneded" page feel activated.
constexpr base::TimeDelta kActivationLifespan = base::TimeDelta::FromSeconds(5);

chromium/user_activation_state.cc at 78f0e8103e36a5126a9ce0ac134f2f9d4d5d25e0 · chromium/chromium · GitHub

5 秒である.コメントにあるように,5 秒あれば大体のアクションは完了するだろう,と見做されている.

だから,今回のケースでは (2) に該当して「なにも操作をせずに 5 秒待っているとポップアップブロックされる」という結果になった.そのため,カチカチと適当にクリックをして待っているとポップアップは開く:

f:id:mangano-ito:20200430155528g:plain
カチカチしてると開く

ポップアップブロッカー

ポップアップブロッカーのテストコードを見てみると,とてもおもしろいものになっている.というのも,ありとあらゆるポップアップの試みのテストケースがあるからだ.めちゃくちゃポップアップ開くとか,不可視の <a> を踏ませるとか,そういう想定されるケースがブロックされるかが書かれている:

github.com

だから,target="_blank"<a>document.createElement して click とか,よくあるやつはブロックされるかチェックされている:

<script>
function test() {
  document.querySelector("a").dispatchEvent(new MouseEvent("click", {ctrlKey: true, metaKey: true}));
}
</script>
</head>
<body onload="test()">
If the fake click was not blocked then there will be a warning message displayed in a new tab. Otherwise, the test passes.
<a href="popup-success.html" target="tab">link</a>
</body>

chromium/popup-fake-click-on-anchor.html at ccd149af47315e4c6f2fc45d55be1b271f39062c · chromium/chromium · GitHub

IN_PROC_BROWSER_TEST_F(PopupBlockerBrowserTest, PopupBlockedFakeClickOnAnchor) {
  RunCheckTest(browser(), "/popup_blocker/popup-fake-click-on-anchor.html",
               WindowOpenDisposition::CURRENT_TAB, kExpectForegroundTab,
               kDontCheckTitle);
}

chromium/popup_blocker_browsertest.cc at ccd149af47315e4c6f2fc45d55be1b271f39062c · chromium/chromium · GitHub

さらに,

開いたウィンドウを opener が操作するためには,postMessage してやりとりする方法がある:

developer.mozilla.org

window.addEventListener('message', (e) => {
    location.href = e.data;
}, false);

const deferred2 = async () => {
    const uri = location.href;
    const win = window.open(uri);
    await new Promise((resolve) => {
        setTimeout(resolve, 6000);
    });
    if (win) {
        win.postMessage('https://www.google.co.jp/', '*');
    } else {
        console.error('You have already closed the popup. Sheesh!');
    }
};

(※ この例では楽してるけど,本当はちゃんとメッセージの正当性を確認するために origin を指定&チェックしたりする必要がある)

こうすれば window.open しておいて,データが来た後に操作をすることができる.

さいごに

調べてみると時間がかかったわりに,へぇ〜…という感想しか抱かないものになっている.貴重な時間が消えて切ない.Firefox の実装は調べてないが,同じようなものと想像している.

1995〜2000年代の忌み嫌われた広告ウィンドウがビュンビュン飛び交うインターネットの反省で,window.open は実質封印されたも同然になっている.スマホの現代,window.open はもう @deprecated にしてもいいのではないか.大言壮語.

拡張を作ろうとする症候群

VSCode 拡張を作ろうとしたとき,実は最初 Perl のテストを CodeLens からやる,という拡張をつくろうとしていた.

どういうものか

Perl のテスト(Test::Class) で sub some_test_1 : Tests の行の上に CodeLens で Run Test... っていうのつけて,簡単にテスト実行できるようにしよう,というやつだった.

なぜかというと,以前までテストを実行するために iTerm2 から

docker-compose run -e TEST_METHOD=some_test docker_app carton exec -- prove -lr t/Some/Test.t

とかやっていて,超効率が悪かったからだ.

そんなとき JUnit とかだと VSCode の CodeLens から実行できるのを見た:

https://www.publickey1.jp/blog/17/visual_studio_codejunit.html

なので,VSCode からそのままテスト実行できたら,ものすごい楽だろうな,という気持ちになっていた.

CodeLens

CodeLens を任意の行に追加するのは非常に楽で,vscode.languages.registerCodeLensProvider というメソッドで,vscode.TextDocument を受け取って,vscode.CodeLens オブジェクトの配列を返すような関数を登録すればいいのだった.

Perl のパーサーはどうした,という話だが,そのときはスーパー適当で,正規表現でマッチした行でいいでしょう! ってザックリ実装したのだった:

import * as vscode from 'vscode';

export default function findTestMethodRanges(document: vscode.TextDocument): vscode.Range[] {
    const REGEX = /sub\s+(.+?)\s*:\s*Tests?/mg;
    const ranges = [];

    for (let i = 0; i < document.lineCount; ++i) {
        const text = document.lineAt(i).text;
        const matches = REGEX.exec(text);
        if (!matches) {
            continue;
        }
        const range = new vscode.Range(i, 0, i, matches[1].length);
        ranges.push(range);
    }

    return ranges;
}

そして,

それはそれっぽく動いて,sub great_test : Tests のような行の上に Run Test... が出て,クリックしたら VSCode のビルトイン Terminal に config で設定したテストコマンドが流し込まれて,いいんじゃないか,となっていた.

f:id:mangano-ito:20200426001653p:plain
それっぽい

しかしながら,

すこし欲が出て,サイドバーにテストケースのツリービューを表示して,テストの成功・失敗が一覧できると楽しいだろうな,という気持ちになってきた (IntelliJIDE みたいに).

しかも,そこで Rerun All Tests... とか Rerun Failed Tests... とかできたら気分がいいだろう…とも思っていた.

https://blog.jetbrains.com/idea/2013/03/how-to-write-automated-tests-for-plugins/

こういうのです.

PHPStorm ではテストが通った行がマーカーついてカバレッジこれくらいですよ,っていうのが視覚的に見えたりする機能もあった.

そうすると,

テストは入れ子にできるので,subtest のツリーとかをパースする必要が出てくるわけだが,これは正規表現でザックリできるほど生易しいものではない.

というのも,{, } のマッチとかになってくると,コンテキストなしにパースするのは難しいというか,多いに間違ってしまうわけで,簡単にはできないだろうな,と思って諦めた.

最終的に,

でもターミナルから毎回やるのは面倒だな〜と思っていたら,普通に業務のレポジトリでは「カーソル行のテスト実行」と「すべてのテスト実行」がタスクとして .vscode/tasks.json に定義されていて,単に ⌘ + P> Run Task から便利にテスト実行できるようになっていたのだった!:

f:id:mangano-ito:20200427194525p:plain
求めていたものが身近にあった

こんな感じでカーソル行とファイルを指定してタスク定義できるのだった:

{
    "tasks": [
        {
            "label": "My Test",
            "type": "shell",
            "command": "path-to-test-helper ${relativeFile} ${lineNumber}",
            "group": "test"
        }
    ]
}

だとしたら,僕の悩みというのは 9 割解決で,別に CodeLens でポチポチする必要はないな,と思い直した.

しかも Problem Matcher が定義されているので,テストに Fail した行が波線も引かれていて,エラー行を調べるとかもしなくてよかったので,10 割解決して満足したのでこの拡張はボツとなった.

つまり,

頑張らなくても意外と素直に簡単な方法で実現できることは多くて,みんな経験することならなおさらステキな解決案が出ているので,先に探したほうがよかったりするのだった.

今回は CodeLens や VSCode の勉強だったと思うことにして,勉強のために車輪の再発明をしたと認識を改めることで後ろ向きに前進した.

ブラウザでカーテンをバックに石畳と木組みの街に行ってみませんか

週末,ブラウザ上でバーチャル背景合成できるものを作った.

mangano-ito.github.io

f:id:mangano-ito:20200420044736p:plain

github.com

これで任意のビデオ通話ソフトウェアで合成ができるようになった.よくつかうのは Google Meet なのだけれど,画面取り込みすれば簡易的にバーチャル背景機能を使えるかもしれないし,使えないかも知れない.

活用

よくこういう背景合成に「グリーンバック」とかいう緑一色(リューイーソーではない)の布を背景にしてくり抜くのが行われている.一昔前はブルーバックだった気がする.なぜ変わったのかは知らない.

幸いにして自分の家のカーテンが緑色だったのでこれで代用することにした.そうしたら思いの外うまくいった.

配布されているバーチャル背景をつかうことで,あの石畳と木組みの街にも行ける:

f:id:mangano-ito:20200420045920p:plain
不審者

シュールですね.

ちなみに合成をやめるとこんな感じで悲壮感が漂う:

f:id:mangano-ito:20200420050330p:plain
夜に自撮りする未来の暗い中年男性

YouTube も流せるようにしてみたら幅が広がり始めた:

f:id:mangano-ito:20200420042100p:plain
日本の絶景

www.youtube.com

寝間着姿で日本の絶景を訪れることも可能となった.(4k で GPU をいじめてるからかアドレスバーの一部が壊れ始めた).

動画で我が家に天使が舞い降りさせることもできるけど,普通に起こられそうなので個人的に楽しむことにした.

明るい色ですべてのパラメーターを過剰気味にすると,暗い部分が残って一昔前の iPod CM 風の映像が得られたりもする:

f:id:mangano-ito:20200420043315p:plain
懐かしい iPod CM 風

dic.pixiv.net

くり抜き機能のあるビデオ通話ソフトウェアに比べれば当然品質は良くはないが,この週末に作ったものにしてはそこそこ活用できるものになっただろう.


ここからは技術の話.

しくみ

まず,カメラの映像は Media Streams API で普通に映像をキャプチャする.カメラの画像をテクスチャにアップロードすれば,WebGL でレンダーできる.

そうすれば GLSL でフラグメントシェーダーを使って,特定の色範囲をマスクすれば,うまくいけば背景を切り抜くことができる.

JavaScript 側でゴリゴリピクセル単位の操作なんてしようものならブラウザが即固まって論外だけど,GLSL なら GPU でやってくれるので問題ない.

そして普通にやると OpenGLAPI をがそのまんまで面倒な WebGL まわりも Three.js を使えば簡単に扱える.

くふう

まず,背景として単色が求められる (「マスク色」と呼ぶことにする).

単純にこの 1 色を指定して切り抜くか判断するとしたら到底無理なので,ある程度あいまいに判定する必要がある.とすると,普通にある程度幅をもたせて範囲内だったら背景と判断して切り抜くようなロジックになる.

イメージ的には「マスク色」と「ある任意のピクセルの色」の距離が「しきい値」以下だったら,背景と判定するような単純なロジック.

ところで単純に色の値を比較するにしてもいろいろな要素が出てくる.まず「色空間」の違いがあるので無視できなさそうである.RGB で 2 色の距離を取ったとしても,知覚的に近いとはあまり思えないかもしれない.

なので,シェーダー側で RGB から HSV に色空間を変換する.HSV 色空間になると「色相」「彩度」「明度」の 3 要素が色の構成要素になった.

ここで距離を考えるときに単純にベクトルの距離をとればいいかというと,やってみて思ったのは,ケースバイケースでいい感じになるパラメーターが異なるので,「しきい値」はその 3 要素それぞれで設定できるのがよさそうと思った.

カーテンみたいにのっぺり単色だと「色相」の差は小さく,「彩度」や「明度」の差を中くらいにすればある程度いい感じになる.

さらなるくふう

さて,これでいいかというと微妙だった.細かなノイズがパラパラと散らばり出て煩わしい感じになってしまう.

空間方向のノイズを低減するというと,ガウシアンをかけるとかそういうぼかし系の処理をかけて平滑化すればなんとかなるだろうと,とりあえず周囲の色を適当な個数サンプリングしてみることにしたら,ある程度マシになった.

時間方向のノイズも積み重ねるバッファを設けて判定を行えばよりチラチラするノイズを低減することができるだろう,と思ったけど面倒なのでしなかった.

ところで

Chrome だと document.createElement('video'); でつくった不可視の要素からはテクスチャをつくることができなかった.(サイズが 0 になってしまってエラーになる).

style="width: 0; height: 0;" にして document.body.appendChild することで動作したので,この Workaround でヨシ,としたけど,これもいつか通用しなくなりそうな気がする.

Firefox では問題なく動作する.Chrome に比べて WebGL の動作もスムースな気がする.ファンも回りにくいし,プチフリーズ的なのもない.

Uber Eats で頼んだ温泉卵が入ってなかったらどうなるでしょう

Uber Eats で まぐろローストビーフ丼というおいしいものだけミックスしました,みたいなご飯をしばしば頼む.

先日頼んだところ,意気揚々とフタを開けたらいつもプラスしている温泉卵が入ってなかった.

よく頼んでるので,別添のカップに入ってるとか,そういうわけではなくて直接入ってることを熟知している.けれど,もしかしたらと思ってガサゴソしたけど入ってなかった.

指定しなくても入ってるものなら諦めてたかもしれない.しかしながら,僕は温泉卵を任意で60円でプラスしているし(ケチ),温泉卵が好き(まろやかになる)という事実があるので,勇気を出して食べ終わったあとに問い合わせをしようと思った.

Uber Eats で問い合わせをするのは,過去の注文履歴から数ステップの選択肢沿うだけだった.「注文の一部が入ってない」という項目を選んで,「温泉卵」の項目を選択して完了した.

そうすると,即「全額返金します」と画面に表示され面食らってしまった.いくばくかやりとりがあっての 60 円返金があるといいなと思ってた僕は,たいそう驚いてしまった.

f:id:mangano-ito:20200417135736p:plain
訂正された

過去に何度も注文した履歴があったからだろうか.投機的に全額返金すればたしかに不満に思う人間はそうそういないと思う.サポートのコストのほうが高いと判断されているのかもしれない.これが舶来の効率主義.

むしろ,食べちゃったんですけどなんか申し訳ないっていう気分になった.

いまさら csvq が便利という話

最近の便利だったツール小話.csvq が便利という話.

csvq

mithrandie.github.io

csvqCSVSQL で操作できるツールで jqCSV 版 (変な表現). 有名なのに使ったことがなかった.

具体的にどう便利だったかというと, したごしらえしておいた CSV を手元でいじくり回すのに使っていて, 公開日のセルを一律 +1 ヶ月したりとか,なにがしを GROUP BY して SUM して集計するみたいな,ごく簡単なもの.

mithrandie.github.io

cwdmy_data.csv があるとする:

id,date1_from,date2_from
0,2020-04-01T00:00:00+09:00,2020-11-05T10:00:00+09:00
1,2020-06-05T12:34:56+09:00,2051-01-12T12:12:12+09:00

これに対して操作するときは,

UPDATE `my_data`
SET    date1_from = ADD_MONTH(date1_from, 1),
       date2_from = ADD_MONTH(date2_from, 1)
;
COMMIT;
$ csvq -s my_update.sql

とできる.オプションをつけなければインタラクティブシェルになるので,こっちのほうが楽.

CSV はファイル名をテーブルっぽく指定して扱える.スキーマはいらず 1 行目がカラム名になる.オンメモリで処理されて,2万行は少なくとも問題ない.

大した操作ではないのだけれど,大したことない処理だからこそスクリプト書きたくもないし,かといって手で変更するのも疲れるケースで活躍する.Excel のマクロは書けないが SQL が書ける人は多いし,それは僕.

直接 DB 操作はできないし,かといってローカルの MySQLCREATE TABLE して LOAD DATA INFILE... するもの面倒だし,Excel でマクロとかもちょっとパッと書けないし,スクリプトと書くのもちょっとみたいな限定的なケースで便利.

ところで,

てっきり実装として :memory:SQLite のデータベースに CSV をインポートしてやりとりしているものかと思ったが,どうやらそういうわけではなく丁寧なもののようだった.

なにかと,

CSV がポータブルなデータ入稿フォーマットになっているのは,

  • たいていの人にとって扱いやすい (Excel)
  • プレーンテキスト

というみなまでいわずともわかる利点から,どこの現場にいっても人々のデータのやりとりに使われているのを目撃している.管理ツールとかも CSV を食べさせるものも多いのだった.

なので,プログラマーでなくとも CSV と楽に付き合えるようになるのが汎用らくらくポイントなのだと思った.

Contoso から Microsoft に思いを馳せる

先日 VSCode 拡張を作って Marketplace に公開したとき,Microsoft 公式のガイド読んでいた.

code.visualstudio.com

公開するためには Azure DevOps というサービスでアカウントを作って organization を作る必要があるらしいので,結局のところそっちのガイドを読むことになった.

文字が多すぎてすべて読める気がしないので,重要そうな部分だけガイドをななめ読みしていたらこのセクションにぶちあたった:

docs.microsoft.com

ここに CONTOSO という企業の例が図で出てくるのだけれど,久々にこの Microsoft ワード (ワープロソフトではない) をひさびさに見てなんだか嬉しい気分になった.

https://docs.microsoft.com/en-us/azure/devops/user-guide/media/contoso-organization-with-projects.png?view=azure-devops

CONTOSO

Contoso についてはこの Wikipedia のページが詳しい:

ja.wikipedia.org

有り体に言えば,Microsoft のリファレンスでよく例につかわれる架空の企業の名前ってだけなのだけれど,こんなに脈々と 2020 年の今も受け継がれてるのがすごくおもしろい.

自分が Contoso を目撃したのはいつだったか覚えてない.わりと昔だと思う.同様になにかのリファレンスを読んでいて触れた.それがずっと使われているところにシャレが利いているというか,良さがあるなと思った.

Microsoft

Microsoft の従業員でもないのに謎目線で Microsoft の面白いと思っている部分は,後方互換性を大事にしているところとか,歴史や資産を重要視しているところ.

といっても,それは The Old New Thing という本の受け売りなところが大きい:

https://www.amazon.co.jp/dp/0321440307

f:id:mangano-ito:20200415133802j:plain

著者は Microsoft の古参で,この本では「Windows のこれはなぜこうなっているのか」という理由が歴史的事情から解説されている.

f:id:mangano-ito:20200415133642j:plain
タスクバーがタスクトレイと呼ばれている理由

ペーパーバックだと超分厚くてちょっとした辞典くらいあるので,なかなかボリュームがあって面白い本.

Windows のソフトウェアへの互換性の情熱はすごいみたいな話は結構いろんなところで語られている:

xtech.nikkei.com

Excel

著名なのが Excel のエポックの事情である.

Excel には 1900 年システムと 1904 年システムがあって,紀元が異なる 2 つのシステムがある.この事情については公式の説明がある:

docs.microsoft.com

これと似たようなことが Joel on SoftwareMy First BillG Review でも触れられている:

www.joelonsoftware.com

(※ 前までは日本語訳のページがあったのだけれど,今調べても見つからなかった.書籍版に日本語訳があるようだった: https://www.amazon.co.jp/dp/4798118923/)

さて,この話のおもしろポイントをまとめると,Excel1900/02/2859, 1900/03/0161 として番号を振っていて,601900/02/29 のうるう年の日としている.ところが,実際は 1900 年はうるう年ではない.

これは当時主流のスプレッドシートLotus 1-2-3 がそういう仕様になっていてインポートできるようにしたため.そもそも Lotus 1-2-3 は当時のコンピュータの性能から計算を省略するために意図的にそうしたと推測されている.

一方 Visual Basic では 1899/12/31 がエポックになっていて,これに符号するように -1 日されている.

誤解を恐れずにいうと,Lotus 1-2-3 の仕様に ExcelVisual Basic が合わせようとしてた,という話だった.そして 1904 年システムは 1900 年を回避するためにエポックが 4 年ズレている.

これを現実を歪曲させた設計で負債と取るか,当時の要求に見事に答えた仕様と見るか.


あとは A1 参照形式R1C1 参照形式 の話もある:

www.relief.jp

これも Lotus 1-2-3Microsoft Multiplan の仕様の違いから選択可能になっているオプションで,似たような事情を抱えている.

伝統

Excel の例を伝統というかは置いておいて,伝統を踏襲しているデザインはおもしろい.ガワはモダンになっていってるけど,コアの部分とかでファンサービスを忘れていないようなものはいくつかある.

ドラクエは今でも魔法の SE はあの 8-bit な音だとか,ポケモンモンスターボールの落ちる SE は初代ゲームボーイ版の音だったりするみたいなのがある.

マリオでも,ヨッシーに乗ると BGM にパーカッションが足されるのが,ヨッシー初登場の「スーパーマリオワールド」から続いているとか,2D マリオではジャンプのときの SE がマリオワールドと同じパンフルートの音とかがある.

ゲームはこういう体験の例が多い気がしている.

こういう

おもしろ歴史のバックグラウンドがあるから納得できるけど,仮に新しくつくるものに何の経緯も理由もなく採用したら意味不明のものとして評価されるかもしれない.

ところで,

Google Spreadsheet はどうなってるのかといえば,

f:id:mangano-ito:20200415134146p:plain

気にされていないのだった.


我ながら Contoso でこれだけポエムが書けるのに驚いた.

VSCode でブランチごとにタブ状態を復元する拡張

gitswitch <branch> したときに VSCode のタブ状態がブランチごとに保存/復元できたら,切替時に楽だな〜と思った.のでそういう拡張機能を作ろうと思った.

たとえば

  • feature-add-user ブランチでは: User.pm, User.t, UserRepository.pm
  • feature-add-work ブランチでは: Work.pm, Work.t, WorkRepository.pm

を開いて作業するのが心地よい,とかザラにあると思われる.

そんなときに毎回切替時に全タブ閉じて,必要なファイルを Cmd+P で開いてとか面倒の極みだな,と思っていた.開きっぱだとごちゃごちゃしてきて精神衛生上よくない.

git でファイルの状態は変わるのなら,タブの状態も変わっていいだろうと思った.

結果

できあがった拡張機能がこちら:

marketplace.visualstudio.com

github.com

ブランチごとにタブ状態の保存/復元機能

現在いるブランチのタブ状態のセッションをコマンドから保存/復元できる機能:

https://raw.githubusercontent.com/mangano-ito/git-branch-wise-session/master/assets/save-and-restore.gif

これがないと話にならない.

(UPDATE) コマンドはこの記事を書いたあとで Git Branch-Wise Session: の prefix をつけるようにしたので,とりあえず Ctrl + P or ⌘ + P して > Git Branch とか入れれば候補が出てくるようになった.

  • セッションの保存: Ctrl + P / ⌘ + P → 入力 「> Save Session for Current Branch
  • 今いるブランチのセッションの復元: Ctrl + P / ⌘ + P → 入力 「> Restore Saved Session for Current Branch

セッションは > Clear Session とか打つと出てくるコマンドで消せる.

ブランチ切り替わったら自動で復元してくれる機能

git switch でブランチが切り替わったら,保存したセッションがあれば自動復元してくれる機能:

https://raw.githubusercontent.com/mangano-ito/git-branch-wise-session/master/assets/auto-restore.gif

要・不要わかれそうなのでオプションでデフォルト OFF にした.

Ctrl + P / ⌘ + P → 「> Open Settings」 でワークスペースごと or グローバルで設定変更できる.

なやみポイント 1

VSCodeAPI には今開いてる全タブ (vscode.TextEditor) を取得する API がないということ.

なので,「次のタブに切り替え」アクション (workbench.action.nextEditor) を発行しまくって,今のタブっぽいのに戻ってくるまで繰り返して,全部のタブを入れるという激ゴリハックが提案されていた:

github.com

こういう感じになる:

import * as vscode from 'vscode';

async function *getOpenedTabs(): AsyncGenerator<vscode.TextEditor> {
    const active = vscode.window.activeTextEditor;
    if (!active) {
        return;
    }
    yield active;

    const equals = (lhs: vscode.TextEditor, rhs: vscode.TextEditor) => {
        return lhs.document.uri === rhs.document.uri
            && lhs.viewColumn === rhs.viewColumn;
    };

    while (true) {
        await vscode.commands.executeCommand<vscode.TextEditor>('workbench.action.nextEditor');
        const editor = vscode.window.activeTextEditor;
        if (!editor || equals(active, editor)) {
            break;
        }
        yield editor;
    }
}

嘘やろ,って思いますよね.この Issue 2016 年 からあるというので謎です.

なやみポイント 2

VSCodevscode.Memento というインタフェースがあって,これはワークスペース (or グローバル)に設定を保存しておける.(Memento パターンとかの Memento ですね).

これは Map<string, any> みたいな感じで KVS 感覚で保存できる.ので楽だけど,保存されているすべてのキーを enumerate することはできない.

なので,結局のところ適当な単位でキーに複数の値を詰め込むように JSON.stringify して保存することになる.

なやみポイント 3

この拡張は別のビルトイン拡張 vscode.git に依存しているので,vscode.git の初期化を待たないといけない.

最初は vscode.extension.onDidChange で初期化のイベントこないじゃん…って絶望して,

await new Promise(
    (resolve) => {
        const check = () => {
            setTimeout(() => {
                if (vscode.extensions.getExtension('vscode.git')) {
                    return resolve();
                }
                check();
            }, 100);
        };
        check();
    }
);

みたいなすごくダサいゴリゴリ待つコードを書いていた.

その後調べたら,package.json の manifest で依存している拡張を指定できることに気づいて解決した:

{
    "extensionDependencies": [
        "vscode.git"
    ]
}

なやみポイント4

VSCodeAPI ではタブがどのカラムに表示されているかは vscode.TextEditor.viewColumn: vscode.ViewColumn で取得できる.

が,この vscode.ViewColumnnumberエイリアスであって,普通に番号なのだった.

で,この番号は絶対的なペインの番号とかではなくて,左から 1, 2, 3, ... みたいに振られているということにデバッグしていて気づいた.

だから,そのタブが Window 下部のペインにアタッチされていたとしても 2 だし,右側にアタッチされていても 2 みたいなケースがあって,復元時にペインの状態を完全に元通りにできないという課題がある.

もしかしたら別の API でレイアウト状態を取れるかもしれない.

しかしながら,

まあいいか,と思った.ブランチごとに復元できることが目的であって,ペインの状態はめんどうだけど手で戻すこともできる.

そんなこといったらこの拡張機能の意義は,となって,なにもかも破壊したい気分になってきた.