ぼくのピカチュウに少しだけ思いやりの心を持ってもらう

## はじめに
去年暮れに話題になった、[爆速でターミナルをポケモンにする](https://qiita.com/chanuu/items/b5c3d11e089ad0c7356d)の記事で、[hyper](https://github.com/zeit/hyper)についてなんとなく気になっていましたが、最近、開発環境を見直そうと思って、ちょっと触ってみました。

上の記事で紹介されているプラグイン[hyper-pokemon](https://github.com/klauscfhq/hyper-pokemon)を入れたところ、`grep`したとき、ヒットした文字列の部分が読めない！　vimでアラートのメッセージが読めない！　ぼくのピカチュウ全然優しくない！　ってなりました。（**追記: 作者の方が私のPRをマージしてくださったみたいで、下記の問題はもうありません**）

その問題は修正したものの、他にも問題があったりして、結局hyperをメインで使うのは見送ったわけですが、触った記録として修正したところについて書いておきます。本当に2, 3行のちゃちい修正です。hyperおもしろいし、遊ぶには良いですが、もう少しな部分があったり、開発が止まっていたりして、惜しいなと思います。

## 結論
既存のプラグインをカスタマイズして使うには、

- カスタマイズしたプラグインのフォルダを `~/.hyper_plugins/local` に置く
- `~/.hyper.js` の `plugins` からプラグイン名を消す
- `~/.hyper.js` の `localPlugins` にプラグイン名を入れる
- `~/.hyper_plugins`の中で `npm i プラグイン名`
- hyperを再起動

hyper-pokemonを入れた状態で、`grep`でヒットしたときや、vimでアラートのメッセージが出た時、フォントの色と背景色が一緒になってしまっていて、出力されたテキストが読めない問題を解決した。

## 問題
読めない。空欄にあてはまる共通語クイズやってる気になってきます。
![Screenshot 2018-08-23 02.14.25.png](https://qiita-image-store.s3.amazonaws.com/0/119298/6091c8d5-280b-b9e8-a3fd-21d9a77d2fa4.png)
ピカチュウよ。優しかった頃のお前はどこへ行ったのだ。
![Screenshot 2018-08-23 02.11.12.png](https://qiita-image-store.s3.amazonaws.com/0/119298/0fa113f3-3f4d-08c9-a1e8-d7583a18c7c1.png)
悲しいことですね。

## ローカルのプラグインを使う
hyperがカスタマイズしたプラグインを利用できるように、ローカルに置いたプラグインを利用するようにします。`.hyper.js`のコメントには「devモードがなんとか〜」と書かれているかもしれませんが、devモードでhyperを起動しなくても、ローカルのプラグインを利用できます。

まず、プラグインをインストールします。`~/.hyper_plugins`の中で`npm i hyper-pokemon`でも良いですが、せっかくなのでhyperの設定ファイルをいじってみます。`plugins`というkeyがあるので、そこに`'hyper-pokemon'`を追加します。

```javascript:~/.hyper.js
plugins: ['hyper-pokemon'],
```

hyperは`.hyper.js`の変更をlistenしているので、ファイルを保存すると自動的にプラグインのインストールが始まって、ターミナルがピカチュウになります。かわいさに悶えながら癒やされましょう。

この状態で`~/.hyper_plugins/node_modules`に`hyper-pokemon`がインストールされているので、`~/.hyper_plugins/local`にこのフォルダをコピーします。

```bash:~
cp -r ~/.hyper_plugins/node_modules/hyper-pokemon ~/.hyper_plugins/local/hyper-pokemon
```

`~/.hyper_plugins/local/`以下のプラグインフォルダを読んでくれるように、`~/.hyper.js`を変更・保存します。`node_modules`から`hyper-pokemon`が削除され、ターミナルからピカチュウがいなくなってしまいます。『レッドはピカチュウをにがした！ばいばい！ピカチュウ！』

```javascript:~/.hyper.js
plugins: [],
localPlugins: ['hyper-pokemon'],
```

ここで、`~/.hyper_plugins`の中でプラグインをインストールすると、`local`の中に置いたプラグインが使われるようになります。

```bash:~/.hyper_plugins
npm i hyper-pokemon
```

hyperを再起動すると、愛しのピカチュウと再会できます。できなかったら何かエラーが起きてます。`.hyper.js`の記述とかフォルダの構成とかがおかしくなってないか確認します。

他のポケモンのテーマを使いたかったり、hyper-pokemonの他のオプションの設定をしたい場合は、[Github klauscfhq/hyper-pokemon Usage](https://github.com/klauscfhq/hyper-pokemon#usage)を見ます。

## 色の指定を修正する
`~/hyper_plugins/local/hyper-pokemon/index.js`を変更します。変更したところはこれだけです。

- `const isSecondaryDark = color(secondary).isDark();`
- `const highlight = isSecondaryDark ? '#FFFFFF' : '#000000';`
- `const secondHighlight = isSecondaryDark ? '#C7C7C7' : '#686868';`
- `white: secondHighlight,`
- `lightWhite: highlight`

なぜテキスト読めない問題が起きているのかというと、テキストの色とその背景色が同一の値になってしまっていて、テキストが背景に同化してしまっているためです。

ここでhyperでのテキストの色や背景色の指定について話すと、色はCSSを挿入して指定することもできますが、基本的に`~/.hyper.js`の`colors`などの値をいじって行います。例えば、`grep`でヒットした文字列の背景色には、colors中の`magenta`というキーの値にセットされた色が適用されるので、下のようにmagentaの値を変更すると、背景色は白になります。また、vimのアラートメッセージの背景色には`red`の値が適用されます。

```javascript:~/.hyper.js
    colors: {
      black: '#000000',
      red: '#C51E14',
      green: '#1DC121',
      yellow: '#C7C329',
      blue: '#0A2FC4',
      magenta: '#FFFFFF', // デフォルトでは '#C839C5'
      cyan: '#20C5C6',
      white: '#C7C7C7',
      lightBlack: '#686868',
      lightRed: '#FD6F6B',
      lightGreen: '#67F86F',
      lightYellow: '#FFFA72',
      lightBlue: '#6A76FB',
      lightMagenta: '#FD7CFC',
      lightCyan: '#68FDFE',
      lightWhite: '#FFFFFF',
    },
```

「magentaなのに`#FFFFFF`！」とかややこしくなるし、magentaが使われている箇所が複数あって、それぞれ色を分けたいときなどにも不便なので、どういう理由でこういう実装になっているのか知りたいです。

話を戻して、hyper-pokemonプラグインには151+ものテーマがあり、それぞれのテーマでprimary・secondary・tertiary...という風に、配色が設定されていますが、前述のmagentaとredに設定されているのはsecondaryです。そして、`grep`でヒットした文字列のテキストの色は`white`、vimのアラートメッセージのテキストの色は`lightWhite`の値で指定されていて、こちらもsecondaryが割り当てられているために、背景に同化してテキストが読めなくなっています。そのため、このwhiteとlightWhiteに割り当てられている色を変えることで解決を試みます。

secondaryの値はテーマによって大きく異なるため、それによってテキストの色も変える必要があります。`const isSecondaryDark = color(secondary).isDark();`で、使用しているテーマでのsecondaryが暗色かどうかをチェックし、暗色の場合は`highlight`の値は`#FFFFFF`、`secondHighlight`の値は`#C7C7C7`。明色の場合は`highlight`は`#000000`、`secondHighlight`は`#686868`。どれもhyperのcolorsのデフォルト値です。

`index.js`の全体はこんな感じです。

```javascript:~/.hyper_plugins/local/hyper-pokemon/index.js
'use strict';
const fs = require('fs');
const path = require('path');
const color = require('color');
const yaml = require('js-yaml');

const filepaths = {
  backgrounds: path.resolve(__dirname, 'backgrounds'),
  gifs: path.resolve(__dirname, 'pokecursors')
};

const colorSchemes = {
  types: path.resolve(__dirname, 'themes', 'types.yml'),
  pokemon: path.resolve(__dirname, 'themes', 'pokemon.yml'),
  trainers: path.resolve(__dirname, 'themes', 'trainers.yml')
};

function getUserOptions(configObj) {
  return Object.assign({}, {
    get pokemon() {
      if (Array.isArray(configObj.pokemon)) {
        return configObj.pokemon[Math.floor(Math.random() * configObj.pokemon.length)];
      }
      return configObj.pokemon || 'pikachu';
    },
    get poketab() {
      return (configObj.poketab || 'false') === 'true';
    },
    get unibody() {
      return (configObj.unibody || 'true') !== 'false';
    }
  });
}

function getRandomTheme(category) {
  const index = Math.floor(Math.random() * (Object.keys(category).length));
  const name = Object.keys(category)[index];
  return [name, category[name]];
}

function getThemes() {
  const themes = {};
  Object.keys(colorSchemes).forEach(category => {
    Object.assign(themes, yaml.safeLoad(fs.readFileSync(colorSchemes[category], 'utf8')));
  });
  return themes;
}

function getThemeColors(theme) {
  const themes = getThemes();
  const name = theme.trim().toLowerCase();
  if (name === 'random') {
    return getRandomTheme(themes.pokemon);
  }
  if (Object.prototype.hasOwnProperty.call(themes, name)) {
    // Choose a random theme from the given category -- i.e. `fire`
    return getRandomTheme(themes[name]);
  }
  if (Object.prototype.hasOwnProperty.call(themes.pokemon, name)) {
    // Return the requested pokemon theme -- i.e. `lapras`
    return [name, themes.pokemon[name]];
  }
  // Got non-existent theme name thus resolve to default
  return ['pikachu', themes.pokemon.pikachu];
}

function getMediaPaths(theme) {
  const [imagePath, gifPath] = [[], []];
  imagePath.push(...[path.join(filepaths.backgrounds, theme), '.png']);
  gifPath.push(...[path.join(filepaths.gifs, theme), '.gif']);
  if (process.platform === 'win32') {
    return [imagePath, gifPath].map(item => item.join('').replace(/\\/g, '/'));
  }
  return [imagePath.join(''), gifPath.join('')];
}

exports.decorateConfig = config => {
  // Get user options
  const options = getUserOptions(config);
  const [themeName, colors] = getThemeColors(options.pokemon);
  const [imagePath, gifPath] = getMediaPaths(themeName);

  // Set theme colors
  const {primary, secondary, tertiary, unibody} = colors;
  const background = options.unibody ? unibody : primary;
  const selection = color(primary).alpha(0.3).string();
  const transparent = color(secondary).alpha(0).string();
  const header = color(background).isDark() ? '#FAFAFA' : '#010101';

  // ここを変更しました。
  const isSecondaryDark = color(secondary).isDark();
  const activeTab = isSecondaryDark ? '#FAFAFA' : '#383A42';
  const highlight = isSecondaryDark ? '#FFFFFF' : '#000000';
  const secondHighlight = isSecondaryDark ? '#C7C7C7' : '#686868';
　// 変更ここまで。下にも変更箇所があります。

  const tab = color(activeTab).darken(0.1);

  // Set poketab
  const tabContent = options.poketab ? gifPath : '';

  const syntax = {
    backgroundColor: transparent,
    borderColor: background,
    cursorColor: secondary,
    foregroundColor: secondary,
    selectionColor: selection,
    colors: {
      black: tertiary,
      red: secondary,
      green: tertiary,
      yellow: secondary,
      blue: secondary,
      magenta: secondary,
      cyan: secondary,
　　　 // ここも変更。
      white: secondHighlight,
      // 変更ここまで。
      lightBlack: tertiary,
      lightRed: secondary,
      lightGreen: secondary,
      lightYellow: secondary,
      lightBlue: secondary,
      lightMagenta: secondary,
      lightCyan: secondary,
　　　 // ここも変更。
      lightWhite: highlight
　　　 // 変更ここまで。
    }
  };

  return Object.assign({}, config, syntax, {
    termCSS: config.termCSS || '',
    css: `
      ${config.css || ''}
      .terms_terms {
        background: url("file://${imagePath}") center;
        background-size: cover;
      }
      .header_shape, .header_appTitle {
        color: ${header};
      }
      .header_header, .header_windowHeader {
        background-color: ${background} !important;
      }
      .hyper_main {
        background-color: ${background};
      }
      .tab_textActive .tab_textInner::before {
        content: url("file://${tabContent}");
        position: absolute;
        right: 0;
        top: -4px;
      }
      .tabs_nav .tabs_list {
        border-bottom: 0;
      }
      .tabs_nav .tabs_title,
      .tabs_nav .tabs_list .tab_tab {
        color: ${secondary};
        border: 0;
      }
      .tab_icon {
        color: ${background};
        width: 15px;
        height: 15px;
      }
      .tab_icon:hover {
        background-color: ${background};
      }
      .tab_shape {
        color: ${secondary};
        width: 7px;
        height: 7px;
      }
      .tab_shape:hover {
        color: ${secondary};
      }
      .tab_active {
        background-color: ${activeTab};
      }
      .tabs_nav .tabs_list .tab_tab:not(.tab_active) {
        background-color: ${tab};
      }
      .tabs_nav .tabs_list {
        color: ${background};
      }
      .tab_tab::before {
        content: '';
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        height: 4px;
        background-color: ${secondary};
        transform: scaleX(0);
        transition: none;
      }
      .tab_tab.tab_active::before {
        transform: scaleX(1);
        transition: all 400ms cubic-bezier(0.0, 0.0, 0.2, 1)
      }
      .terms_terms .terms_termGroup .splitpane_panes .splitpane_divider {
        background-color: ${secondary} !important;
      }
    `
  });
};
```

hyperを再起動します。

ピカチュウが思いやりの心を取り戻してくれて幸せです。ぼくのピカチュウは意地悪な穴埋めクイズとか出さない。そんなの幻覚だった。
![Screenshot 2018-08-22 22.14.48.png](https://qiita-image-store.s3.amazonaws.com/0/119298/35f63428-8269-7688-ec03-f8bc9f29dd30.png)

こんな素晴らしいプラグインを作ってくださったのだから何かしなければと思い、採用されようがされまいが、この修正でPRを送ってみました。（追記: 1週間後ぐらいに作者がマージしてくれました）このちゃちいコードがお礼になっているかどうかはさておき。

## おわりに
hyperには[Github zeit/hyper Unreadable nano labels if backgroundColor is transparent](https://github.com/zeit/hyper/issues/2984)みたいな問題もあって（このissueはnanoについてですが、hyper-pokemonを入れた状態で、ターミナルにペーストしたテキストが読めないのもこれと同じ原因）、凝ったことしようとすると、まともに使えるようにするには色々自力で書く必要がありそうです。また、開発も止まってしまっているようで、メインで使うにはどうかなと思いました。でもhyperで遊ぶの楽しい楽しい。