search
LoginSignup
63

More than 3 years have passed since last update.

posted at

updated at

Hyperターミナルのプラグインを作ってみよう!

Hyperとは

Hyper(旧HyperTerm)とは、ZEITが開発しているHTML+JavaScript(Electron)で作られたクールなターミナルソフトだよ。最近、v2にバージョンアップして、描画速度や日本語対応が進み、普段使いもできそうな感じになってきてるんだ。
なんといってもHyperの魅力は、Web系の知識(React & Redux)が少々あれば、かなり自由にプラグインを作れることだね:yum:
tmuxやらvimやらpowerlineを頑張ってカスタマイズしてCUI環境をつくるのには疲れたよーという人は試してみると幸せになれるかも。

Pluginをつくってみよう

日本語の記事ではピカチュー化とか触ってみたとかコンフィグ説明はあるけど、プラグインの作り方は見つけられなかったので、簡単なプラグインを作ってわかったプラグイン作成の手順を紹介するよ。
簡単なサンプルとして、ターミナルの右下にキャラクター(アグレッシブ烈子)を表示し、コマンド入力結果に応じてキャラクターの表示内容を変化(デスボイスモード)させるというプラグインをつくってみたよ。頑張って作り込めばデスクトップマスコット伺か的なやつのターミナル版が作れるかも:wink:
HyperDemo3.gif

以下の手順は素人による「やってみた」系なので、正しい仕様は本家のドキュメントを参考にしてください :bow:
また、以下のコード例は雰囲気を伝えるのが目的なので、色々省略しています。実際のサンプルコードはこちらをみてね。

手順1:自作プラグイン読み込み

Hyperを起動すると、ホームディレクトリ以下に~/.hyper_plugins/localというディレクトリが作られるよ。その下に作りたいプラグイン名でディレクトリを作ろう。今回はプラグイン名を「mascot」にするよ。その下に、index.jsを作成し、ここにプラグインの中身を書こう。

mkdir ~/.hyper_plugins/local/mascot
vim ~/.hyper_plugins/local/mascot/index.js

index.jsでは、HyperのAPIにしたがって何かexportする必要があるようだ。とりあえず、コンフィグをPluginから設定するdecorateConfigをエクスポートして、カーソルの色を黄色にしてみよう。

// index.js
exports.decorateConfig = (config) => {
  return Object.assign({}, config, { //元のconfigを保持するため
    cursorColor: 'yellow',
  });
}

あとはこのプラグインを有効にするために~/.hyper.jsの設定でlocalPluginsに追加しよう。

// ~/hyper.js
  localPlugins: ["mascot"],

これでHyperのカーソル色が黄色になっていればOKだ。

手順2:キャラクターを表示する

HyperはReactコンポーネントの集合からできていて、各コンポーネント名の前にdecorateをつけた関数で、そのコンポーネントのHOC(Higher Order Component:コンポーネントを引数に受け取り、修飾したコンポーネントを返す関数)を定義できるんだ。Hyperのプラグインは基本的にHOCを定義して既存のコンポーネントを修飾するようだね。
コンポーネントにはTabやHeader、Notificationsなどがあるけど、今回はターミナルのメイン部分にキャラクター要素を追加したいので、Term要素のHOCを定義するよ。

// ~/hyper_plugins/local/mascot/index.js
exports.decorateTerm = (Term, { React, notify }) => {
    return class extends React.Component { // HOCを返す
        render () {
            const children = [
                 React.createElement("img", {src: "/path/img.png"}), //★画像要素を追加!
                 React.createElement(Term, this.props}), //もとの要素
            ];
            return React.createElement("div", null, children);
        }
    }
}

上の例ではTermコンポーネントのHOCを定義して、もとのTermに加えて<img src="/path/img.png" />を表示するように修飾しているよ。
babelとかで勝手に変換してくれないので、jsxとかを使うときは要注意だね。

手順3:ターミナルの入力を検知する

PluginではReduxのmiddlewareを定義でき、そこでユーザ入力などのイベント(Action)などを取得することができるんだ。ただ、ユーザの入力は1文字づつなので、これを取得してもあまり意味がない:cry: 入力の結果として画面に表示される文字列を検知して処理を開始することが一般的なようだ。画面に表示される文字列はActionTypeSESSION_ADD_DATAで呼ばれるよ。

// ~/hyper_plugins/local/mascot/index.js
exports.middleware = store => next => (action) => {
    if (action.type === 'SESSION_ADD_DATA') {
        if (/.*: .* No such file or directory/.test(action.data)) { // 検知
            store.dispatch({
                type: 'CHANGE_MASCOT_FEEL',
                skin: 'angry',
                speech: 'んなぁファイルねぇぇぇぇぇ”ぇ”!',
            });
        }
    }
    next(action);
}

上の例では、指定したファイルが存在しない時にzshで表示されるエラーメッセージを正規表現で検知して、新しいAction(CHANGE_MASCOT_FEEL)を発行しているよ。汎用的に作るにはbashやfishなんかのエラーメッセージにも対応する必要があるね。
next(action)は他のミドルウェアに処理を継続させるのに必要だけど、これを省略することで、例えばエラーメッセージの表示を抑止することもできるんだ。例えば、独自コマンドを作る場合は/zsh: command not found: .*/で検知した上でエラーメッセージを抑止することで独自コマンドを実現できるよ。

手順3:キャラクターを動かす

先の手順で発行したAction(CHANGE_MASCOT_FEEL)をReducerで受け取り、StateをPropsとしてコンポーネントに流し込むことで、HOC内でstateに応じた処理を行うよ。
Reducerの定義はreduceUIに記述するよ。stateはmapTermsStateによりTermsコンポーネントに流し込まれるため、それを孫であるTermコンポーネントに渡すにはgetTermPropsが必要になるね。

// ~/hyper_plugins/local/mascot/index.js
exports.reduceUI = (state, action) => {
  switch (action.type) {
    case 'CHANGE_MASCOT_FEEL':
      const {skin, speech} = action;
      return state.set('mascotState', { skin, speech});
    default:
      return state;
  }
};

// stateをTermsコンポーネントのpropsにマップ
exports.mapTermsState = (state, map) => Object.assign(map, {
  mascotState: state.ui.mascotState,
});

// 親コンポーネントから子(TermGroup)、孫(Term)へとpropsを渡す
const passProps = (uid, parentProps, props) => Object.assign(props, {
  mascotState: parentProps.mascotState,
});
exports.getTermGroupProps = passProps;
exports.getTermProps = passProps;

これで、HOCの中からmascotStateを参照できるようになったね。例えば下のように利用するよ。

// ~/hyper_plugins/local/mascot/index.js
exports.decorateTerm = (Term, { React, notify }) => {
    return class extends React.Component {
        render () {
            const skin = this.props.mascotState.skin == 'angry' ?
                 "/path/angry.png" : "/path/normal.png"; //★stateにより画像を決定
            const children = [
                 React.createElement("img", {src: skin}), 
                 React.createElement(Term, this.props}), 
            ];
            return React.createElement("div", null, children);
        }
    }
}

完成!

以上、こんな感じの手順でプラグインを作っていくことができるよ。すべてHTML + JavaScriptなので、例えばキャラクターをクリックしてイベントを発生させたり(onClick)とか、WebAudioでキャラクターに音声を喋らせたりすることも簡単にできそうだね。
今回つくったサンプルはこちらのGistにあげました。上記の説明ではCSSとか一切触れていないので、こちらをみてください。
ぜひみんなもいろんなプラグインをつくってHyperを盛り上げよう:rocket:

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
63