テンキーレスミニキーボード「Razer HUNTSMAN MINI」をmacOSで使う

 長年仕事用キーボードとしてHappy Hacking Keyboard(HHKB)Lite 2を使っていたのですが、だいぶタッチが渋くなっていること、そしてこの製品がすでに廃盤になっていることから将来的なmacOSサポートが期待できない点を考慮して、代替品としてRazerのHUNTSMAN MINIという製品を導入してみました。テンキーレスのコンパクトサイズキーボードの選択肢はあまり多くはないのですが、HUNTSMAN MINIでは日本語配列モデルも提供されており、かつキー配列的にも素直な感じです。macOSのでは英数キーとかなキーで日本語入力のON/OFFを切り替えるため、スペースバーが長かったりすると違和感を感じるのですが、本製品はスペースバーの幅がAppleの純正キーボードとほぼ同じなのが決め手でした。

Apple純正キーボードとHUNTSMAN MINIの比較

 HHKB Lite2と比較すると、サイズ的にはほぼ同じです。スペースキーが若干長く、また矢印キーがない点が大きな違いです。

HHKB Lite2とHUNTSMAN MINIの比較

 このキーボードは一般的なWindowsキーボードと同様、最上段の「1」キーの左には半角/全角キーがり、また「A」キーの左はCaps Lockキーがあるのですが、これらは純正のキー配列カスタマイズツールで変更可能で、かつ変更した内容はキーボード自体に保存されます。ただ、このカスタマイズツールは現状Windows版しか提供されていません。以前はmacOS版も提供されていたのですが、数年前にリリースされた最新バージョンからはmacOSのサポートが廃止されたようです。そのためキー配列のカスタマイズにはWindowsマシンが必要ですが、一度設定さえしてしまえば、Macでも特別なドライバなしで同じキー配列で利用可能です。

設定ツールRazer Synapse 3

 ただし、このカスタマイズツールではWindowsキーに関してはカスタマイズできず、また無変換キーや変換キーをmacOSで使われる英数キーやかなキーに割り当てることもできません。WindowsキーについてはmacOSではCommandキー、AltキーはOptionキーとして認識されるのですが、Apple純正キーボードではこれらがちょうど逆の位置にあります。これについてはmacOS側で入れ替えを行うことができるので、そちらで対応可能です。また、英数キーやかなキーが使えない問題は、無変換キーにCtrl+Shift+:、変換キーにCtrl+Shift+Jというショートカットキーを割り当てることで一般的には対応が可能です(参考資料:Macの日本語入力ソースを設定する/切り替える))。

macOSのキーボード設定

Karabiner Elementsを導入する

 ひとまずはこの設定で利用していたのですが、なんとEmacsではCtrl+Shift+:やCtrl+Shift+Jというショートカットキーを使えない(Emacsのショートカットキーとして認識されてしまう)ことが判明。一応Emacs側で無理やりシステムイベントを発生させることで対応は可能ではあるのですが(参考資料:OSXにおけるIMEの変更 #10)、これは内部でコマンドを実行している関係で微妙に切り替えにタイムラグが発生するため、頻繁に入力モードの切り替えを行う使い方には向いていないと感じました。

 ということで、最終的にはmacOSでWindows向けキーボードを使う際の定番ツールであるKarabiner-Elementsを導入して無変換キーを英数キーに、変換キーをかなキーに割り当てることにしました。Karabiner-Elementsを使うとmacOS標準設定でのCommandキーとOptionキーの入れ替えが効かなくなるようなので、その設定もKarabiner-Elements側で行なっています。

Karabiner-Elementsの設定

 このように設定関係で紆余曲折はありますが、Razer HUNTSMAN MINIはキーボードとしての機能自体はよくできており、キータッチにも不満はありません。有線キーボードではありますが、お値段的にはHHKBと比べて1.5〜2万円ほどお安いので、キーボードに3万円近く出すのはちょっと……という方にはおすすめです。

Emacsでcmark-gfmを使ったGitHub互換Markdownプレビューを実現する

 最近Markdownで文章を書くことが増えているので色々と環境を整えたのですが、GitHubが独自に拡張した文法をサポートするMarkdownコンパイラのcmark-gfmとEmacsのmarkdown-modeを組み合わせようとしたら色々とハマったので対処方法をメモしておきます。

macOS環境でのcmark-gfmのビルドとインストール

 cmark-gfmのリポジトリではバイナリは提供されていないので、自前でビルドする必要があります。macOS環境(Xcode)でのビルドにはCMakeが必要となるので、まずこちらの準備を行います。CMakeは公式にmacOS向けのバイナリが提供されているので、dmgファイルもしくはtar.gzファイルをダウンロードしてその中身を/ApplicationsディレクトリにコピーすればOKです。cmakeコマンド自体はアプリケーションバンドル中のContents/binディレクトリ内に入っているので、適当にパスを通すなり直接フルパスで実行するなりします。

 cmark-gfmのビルドは次のような感じで実行します。

$ git clone https://github.com/github/cmark-gfm
$ cd cmark-gfm
$ mkdir build
$ cd build
$ cmake .. -DCMAKE_INSTALL_PREFIX=~/local -DCMAKE_INSTALL_RPATH=$HOME/local/lib
$ make test
$ make install

 システムツール以外はできるだけ~/local以下にインストールしたい勢なので、cmake実行時に-DCMAKE_INSTALL_PREFIX=~/localを指定してインストール先をデフォルトの/usr/local/以下から変更しています。ただしこの場合、rpathの自動設定はしてくれないようなので、追加で-DCMAKE_INSTALL_RPATH=$HOME/local/libオプションでrpathを指定します。このオプションでは指定したパスを直接rpathとしてバイナリに書き込むので、~/表記は利用できないようです。このオプションを指定しないと、インストール後に次のようなエラーが出ます

$ cmark-gfm 
dyld: Library not loaded: @rpath/libcmark-gfm-extensions.0.29.0.gfm.0.dylib
  Referenced from: /Users/hylom/local/bin/cmark-gfm
  Reason: image not found
Abort trap: 6

 なぜかというと、cmark-gfmlibcmark-gfmlibcmark-gfm-extensionsと言った動的ライブラリを生成して使用するのですが、デフォルトではこれらのロードパスが@rpathを使って指定されている一方、上記の設定をしないとバイナリにrpathが書き込まれず、その結果これらライブラリのロードに失敗するためです。

$ otool -L cmark-gfm 
cmark-gfm:
	@rpath/libcmark-gfm-extensions.0.29.0.gfm.0.dylib (compatibility version 0.29.0, current version 0.29.0)
	@rpath/libcmark-gfm.0.29.0.gfm.0.dylib (compatibility version 0.29.0, current version 0.29.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.0.0)

markdown-modeのインストールと設定

 Emacsにmarkdown-modeをインストールすると、Markdown向けのシンタックスハイライトが利用できるようになったり、プレビュー関連のコマンドが利用できるようになったりして便利です。こちらはM-x package-list-packagesで簡単にインストールできます。

 markdown-modeではMarkdown形式ファイルのコンパイルに使用するコマンドをmarkdown-command変数で指定できます。これはおなじみcustomizeコマンドで簡単に設定できます。M-x customize実行後、markdownキーワードで設定パラメータを検索して変更しておきましょう。なお、GitHub向け拡張はエクステンションとして実装されているので、これらを利用するには-eオプションで明示的に有効にする必要があります。たとえば|を使ったテーブル表記を利用するには、cmark-gfm -e tableのようにオプション付きの値をmarkdown-commandパラメータに指定する必要があります

 また、cmark-gfmはUTF-8以外の文字コードに対応していないようです。なぜかUTF-8以外でエンコードされた日本語の場合、-e tableオプションを有効にしても|によるテーブル表記だけがピンポイントで無視されてハマりました。

 markdown-modemarkdown-commandで指定したコマンドを実行する際に、elispのcall-process-regionを使ってEmacsバッファの内容をコマンドに与えているので、.emacsに下記の内容を追加して、markdown-modeではUTF-8でバッファの入出力を行うよう指定します。

;; markdown-modeでmarkdownコマンドにUTF-8でテキストを渡す
(add-hook 'markdown-mode-hook (lambda () (set-buffer-process-coding-system 'utf-8-unix 'utf-8-unix)))

markdown-preview-modeを使う

markdown-preview-mode

 ついでにmarkdown-preview-modeを利用すると、こんな感じでリアルタイムでレンダリング結果をプレビューできてとても便利です。こちらもM-x package-list-packagesでインストールできます。

SVG+Web Componentsで再利用可能なGUIコンポーネントを作る

昨今のWeb開発の現場では、Vue.jsやReactといったページ内の要素をコンポーネント化して再利用しやすくするフレームワークが多く利用されています。しかし、これらのフレームワークのコンポーネントは相互運用性がありません。たとえばVue.js向けに作ったコンポーネントはReactでは利用できませんし、その逆も同様です。

一方で、特に外部のフレームワークなどを利用せずに、JavaScriptだけで自作のコンポーネントを作成できる「Web Components」という技術もあります。Web ComponentではVue.jsやReactのような、DOMとデータの高度な紐づけ機能は提供されませんが、データをビジュアライズするだけであれば十分な機能を備えています。さらに、Web Componentsで作成したコンポーネントはVue.jsやReactでも使えます。

ということで、Web ComponentsとSVGを使って、ダイアル型のメーター(名前は「dial-meter」)というUIコンポーネントを作成してみました。ソースはGitHubのhylom/web-component-demoリポジトリで公開しています。シンプルなデモページも用意しました。

Web Componentsの概要と情報源

Web Componentsを利用すると、次のような手順で独自のタグ(カスタム要素)を実装できます。

  1. HTMLElementクラスを継承したクラスを定義する
  2. 定義するタグ名を第1引数、紐付けるクラス(1.で定義したクラス)を第2引数として与えてcustomElements.define()メソッドを実行する

たとえば<dial-meter>というカスタム要素を実装する場合、まずHTMLElementを継承したDialMeterというクラスを作成し、customElements.define('dial-meter', DialMeter)を実行して登録します。

class DialMeter extends HTMLElement {
  constructor() {
    super();
  }
}
customElements.define("dial-meter", DialMeter);

このJavaScriptコードをWebブラウザ内で実行すると、<dial-meter>という要素がページ内で利用できるようになります。

Web Componentsについて詳しくはMDNの「Web Components」ドキュメントに一通りの基本的な内容がまとめられているので、本記事では基本的な解説はそちらに譲り、ハマったところ、注意点などについて簡単にまとめておきます。

既存の要素を拡張して独自コンポーネントを作ることはできない

Web Componentsでは、完全に新たなHTML要素を実装するだけでなく、既存のHTML要素を拡張する(機能を追加する)という機能も提供されています。この場合、拡張したいHTML要素に対応するクラスを継承したクラスを作成し、さらにcustomElements.define()の第3引数として{ extends: <要素名> }というオブジェクトを与えます。

たとえば<div>要素を拡張したmy-new-elementという要素を作成する場合なら、HTMLDivElementを継承したMyNewElementクラスを作成し、次のようにcustomElements.define()を実行します。

customElements.define(`my-new-element`, MyNewElement, { extends: 'div' });

このようにして定義したカスタム要素は、拡張元の要素に「is」属性を付与した要素を作成することでドキュメント内で利用できます。

<div is="my-new-element"></div>

一方で、このようにして定義したコンポーネントは<my-new-element></my-new-element>のような形では利用できません。

作成するコンポーネント名には必ず「-」が含まれている必要がある

既存のHTMLタグとの競合を回避するため、作成するコンポーネントの名前には必ず「-」が含まれている必要があります。

shadow DOMのルート要素として挿入できる要素の制約

Web Componentsでは、shadow DOMというドキュメントとは隔離されたDOMを使って要素をコンポーネント化します。たとえばこのshadow DOM内に<div>要素を挿入する場合、次のような処理を行います。

const shadow = this.attachShadow({mode: 'open'});

const div = document.createElement('div');
shadow.appendChild(div);

このようにして作成したshadow DOMは、ドキュメント内でそのカスタム要素が存在する位置にアタッチされるのですが、一方でshadow DOMのルート要素直下に挿入できる子要素には制約があり、<div><p>など、限られたものしか挿入できません。

利用できる要素はMDNのドキュメントに記載されていますが、たとえば<img><svg>タグは許可されていません。ただし、この制約はあくまでshadow DOMのルート要素直下にのみ適用されるため、shadow DOMのルート要素直下に<div>要素を挿入すれば、その<div>要素内には任意の要素を挿入できます。

今回作成したカスタム要素は<svg>要素を使ってUIを作成しているので、shadow DOMのルート直下には<div>を挿入して使用しています。

属性へのアクセス

Web Componentsで実装したカスタム要素は、DOM上ではcustomElements.define()メソッドに与えたクラスのインスタンスになります。たとえば今回の例では、<dial-meter>要素はDialMeterクラスのインスタンスとなります。このとき、<dial-meter>に与えた属性は、DialMeterクラス内からはthis.getAttribute()を使ってアクセスできます。

たとえば、<dial-meter value="100">のように記述した場合、このvalue属性の値はthis.getAttribute('value')のようにして取得できます。

一方で、クラス内で毎回getAttribute()を使用するのはやや面倒です。そのため、クラス内から頻繁にアクセスする属性については次のようにgetter/setterを定義すると直感的にアクセスできるようになります。

get value() {
  return this.getAttribute('value');
}

set value(val) {
  this.setAttribute('value', val);
}

属性が変更された場合の対応

Web Components技術を使って実装されたカスタム要素は、一般的なHTML要素と同様に扱えます。つまり、createElement()で新規作成してappendChild()append()prepend()といったメソッドで追加したり、replaceChildren()で削除したり、DOM経由で属性を操作する、といった操作が行えます。こういった操作が行われた際には、カスタム要素に紐づけられたクラスの次のメソッドが実行されます。

  • カスタム要素がノードに追加された場合:connectedCallback()
  • カスタム要素がノードから削除された場合:disconnectedCallback()
  • カスタム要素が移動された場合:adoptedCallback()
  • カスタム要素の属性値が変化した場合:attributeChangedCallback()

なお、属性値に関してはあらかじめobservedAttributes()メソッドを定義し、このメソッドの戻り値で監視する属性名の配列を返すよう実装しておく必要があります。

たとえば「value」および「class」、「style」属性が変化したときにattributeChangedCallback()が呼び出されるようにする場合、次のようにobservedAttributes()メソッドを定義しておきます。

static get observedAttributes() {
  return ['value', 'class', 'style'];
}

また、connectedCallback()およびdisconnectedCallback()adoptedCallback()には引数が渡されませんが、attributeChangedCallback()には次のように3つの引数が与えられます。

attributeChangedCallback(name, oldValue, newValue)

ここでnameは変化した属性名、oldValueは変化前の値、newValueは変化後の値です。

DOMとカスタム要素の構築タイミング

HTML内にカスタム要素を記述していた場合、connectedCallback()はそのカスタム要素のタグがパースされたタイミング(DOMContentLoadedイベントの発生前)に実行されます。

また、属性が指定されていた場合、まずattributeChangedCallback()が実行され、続いてconnectedCallback()が実行されます。

DOMの構築後にcustomElements.define()でカスタム要素が登録された場合、そのタイミングでattributeChangedCallback()connectedCallback()が実行されます。

shadow DOM内の要素に適用されるスタイルシートの定義

Shadow DOM内に、適用したいCSSをtextContentとして持つ<style>要素を挿入することでスタイルシートを適用できます。

const style = document.createElement('style');
style.textContent = `<適用したいCSS>`;
shadow.appendChild(style);

<link>要素を挿入して外部のスタイルシートを読み込ませることも可能です。

const link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', '<CSSファイルのURL>');
shadow.appendChild(link);

カスタム要素自体のstyleを指定する

上記の方法で読み込ませたスタイルシート中では、いくつか特殊な擬似クラスが利用できます。

  • :host:そのカスタム要素自体(shadow DOMのroot)のスタイル
  • :host():引数で指定したセレクタがそのカスタム要素に適用されているに適用されるスタイル。たとえば:host(.foo)とすると、そのカスタム要素に「foo」と言うクラスが指定されていた場合のみに指定したクラスが適用される
  • :host-content():引数で指定したセレクタに合致する要素内にそのカスタム要素が存在する場合に適用されるスタイル。たとえば:host-content(h1)とすると、h1要素の中に存在するそのカスタム要素のみに指定したスタイルが適用される。

ただし、:host-content()は現状Chrome系ブラウザでのみサポートされているようです。

また、Web Componentsに関連する擬似要素として、:defined::part()の2つがあります。

まず:definedですが、これはそのカスタム要素が定義されている(customElements.define()で定義されている)場合のみ適用されるスタイルを指定するものです。たとえば次のコードはfoo-barと言うカスタム要素がcustomElements.define()で定義されている場合にインライン要素として表示し、そうでない場合は非表示にすると言うものです。

foo-bar:not(:defined) {
  display: none;
}

foo-bar:defined {
  display: inline;
}

::part()は、:カスタム要素のshadow DOM内でpart属性が指定されている要素を対象として選択するものです。たとえばfoo-barカスタム要素のshadow DOM内にpart="hoge"と言う属性が指定された要素が存在する場合、次のようにしてその属性のみを対象にスタイルを適用できます。

foo-bar::part(hoge) {
  ...
}

shadow DOM内の要素は通常は外部のスタイルシートの影響を受けませんが、part属性とこの::part()セレクタを組み合わせることで、一部の要素のみ外部のスタイルシートでスタイルを変更できるようにすることが可能になります。

スロット

shadow DOM内に<slot name="<スロット名>">と言う要素を挿入すると、この要素はそのカスタム要素内に囲まれた要素で、かつslot="<スロット名>"という属性が指定された要素に置き換えられます。

たとえばfoo-barというカスタム要素のshadow DOMが次のようになっていたとします。

<p><slot name="hoge">blah blah blah</slot></p>

このとき、<foo-bar>カスタム要素を次のようにマークアップしてみます。

<foo-bar><i slot="hoge">wryyy</i></foo-bar>

すると、表示されるshadow DOMは次のように<slot name="hoge">要素が<i slot="hoge">要素に置き換えられたものになります。

<p><i>wryyy</i></p>

なお、name属性を指定せずに<slot>要素を使用すると、この<slot>要素はカスタム要素内の最初の子要素に置き換えられます。

SVGの要素をSVGタグ内に挿入する

HTML内に直接<svg>要素を書く場合にはあまり意識しませんが、実は<svg>要素やその子要素として指定する<path><circle>といった要素は、HTMLの要素ではありません。そのため、document.createElement()メソッドでは作成できず、代わりにdocument.createElementNS()メソッドを使用して作成します。このメソッドは第1引数としてネームスペースを指定する必要があり、<svg>要素やその子要素を作成する場合にはhttp://www.w3.org/2000/svgと言うネームスペースを指定します。

this._svg = document.createElementNS(`http://www.w3.org/2000/svg`, "svg");

macOSの最近のEmacsで日本語フォントがいわゆる中華フォントになる問題の解決方法

Emacsをアップデートしたところ、日本語(というか漢字)がいわゆる「中華フォント」になってしまった。中華フォントといっても繁体字なので問題なく読めるのだが、たとえば「化」の3画目が4画目に対して突き抜ける感じになったりするので、文章を書くときに気が乗らない。解決方法を試行錯誤したところ、これはset-language-environment"Japanese"に設定するだけで簡単に解決した。

(set-language-environment "Japanese")

あとついでにフォント設定も微調整。

(create-fontset-from-ascii-font "menlo-14" nil "menlo14")
(set-fontset-font "fontset-menlo14" 'unicode "menlo-14" nil 'append)
(add-to-list 'default-frame-alist '(font . "fontset-menlo14"))

Emacs 27.1ではいつの間にか日本語変換時のちらつき問題も解決していた。素晴らしい。

npmで「npm install -g」ではなくnpxをおすすめする理由

npmではライブラリだけでなく、Node.jsで実装されたコマンドラインツールも多数配布されています。こういったツールをインストールする際に「npm install -g」のように-gオプション付きでnpm installコマンドを実行すると、そのコマンドがパスの通った場所にインストールされ、カレントディレクトリがどのディレクトリであってもコマンド名だけでそのコマンドを実行できるようになります。

たとえばこの方法でvueコマンド(vue-cli)をインストールする場合、次の例のようになります。

# npm intall -g @vue/cli

こうやってインストールすると、次のようにvueコマンドだけでvue-cliを実行可能になります。

$ vue

この方法は一見便利なように見えますが、npm install -gコマンドの実行時にroot権限が必要になるという問題があります。さらに、インストールしたコマンドは全ユーザーが利用できるようになるため、意図せずに他のコマンドと競合するといった副作用が発生する可能性もあります。

そのためおすすめしたいのが、-gコマンドを利用せずにローカルディレクトリにインストールし、npxコマンドを使ってインストールしたコマンドラインツールを利用するというやり方です。

npxコマンドは、カレントディレクトリを起点としてnode_modulesディレクトリを探索し、引数で指定したコマンドを見つけたらそれを実行する、という処理を行うコマンドで、2017年にリリースされたnpm v5.2.0からnpmに同梱されています(リリースノート)。

たとえば先のvue-cliを利用したい場合、まずこのツールを利用したいプロジェクトのディレクトリ内で次のように-gオプションなしでインストールを行います(ちなみに-DはインストールするモジュールをdevDependenciesに追加する処理を行うオプションです)。

$ npm install -D @vue/cli

こうやってインストールしたvueコマンドを使って、たとえば「vue create」コマンドを実行したい場合、次のように実行します。

$ npx vue create .

ちなみにここではプロジェクト名の代わりに「.」を指定していますが、こうするとvue-cliはカレントディレクトリ内にプロジェクトを作成してくれます。

この手法には、root権限が不要であるというだけでなく、プロジェクトごとに異なるバージョンのツールを簡単に利用できるようになるというメリットもあります。一方でデメリットとしては、プロジェクトごとにインストールが必要になり、またそのためディスク容量を余分に使用するというものがあります。とは言え、昨今のツールでそのデメリットはあまり大きくないでしょう。

ここではvueコマンドを例にしましたが、tscコマンドやwebpack-cliなどでも同様に使えます。