はじめに
去年暮れに話題になった、爆速でターミナルをポケモンにするの記事で、hyperについてなんとなく気になっていましたが、最近、開発環境を見直そうと思って、ちょっと触ってみました。
上の記事で紹介されているプラグイン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でアラートのメッセージが出た時、フォントの色と背景色が一緒になってしまっていて、出力されたテキストが読めない問題を解決した。
問題
読めない。空欄にあてはまる共通語クイズやってる気になってきます。
ピカチュウよ。優しかった頃のお前はどこへ行ったのだ。
悲しいことですね。
ローカルのプラグインを使う
hyperがカスタマイズしたプラグインを利用できるように、ローカルに置いたプラグインを利用するようにします。.hyper.js
のコメントには「devモードがなんとか〜」と書かれているかもしれませんが、devモードでhyperを起動しなくても、ローカルのプラグインを利用できます。
まず、プラグインをインストールします。~/.hyper_plugins
の中でnpm i hyper-pokemon
でも良いですが、せっかくなのでhyperの設定ファイルをいじってみます。plugins
というkeyがあるので、そこに'hyper-pokemon'
を追加します。
plugins: ['hyper-pokemon'],
hyperは.hyper.js
の変更をlistenしているので、ファイルを保存すると自動的にプラグインのインストールが始まって、ターミナルがピカチュウになります。かわいさに悶えながら癒やされましょう。
この状態で~/.hyper_plugins/node_modules
にhyper-pokemon
がインストールされているので、~/.hyper_plugins/local
にこのフォルダをコピーします。
cp -r ~/.hyper_plugins/node_modules/hyper-pokemon ~/.hyper_plugins/local/hyper-pokemon
~/.hyper_plugins/local/
以下のプラグインフォルダを読んでくれるように、~/.hyper.js
を変更・保存します。node_modules
からhyper-pokemon
が削除され、ターミナルからピカチュウがいなくなってしまいます。『レッドはピカチュウをにがした!ばいばい!ピカチュウ!』
plugins: [],
localPlugins: ['hyper-pokemon'],
ここで、~/.hyper_plugins
の中でプラグインをインストールすると、local
の中に置いたプラグインが使われるようになります。
npm i hyper-pokemon
hyperを再起動すると、愛しのピカチュウと再会できます。できなかったら何かエラーが起きてます。.hyper.js
の記述とかフォルダの構成とかがおかしくなってないか確認します。
他のポケモンのテーマを使いたかったり、hyper-pokemonの他のオプションの設定をしたい場合は、Github 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
の値が適用されます。
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
の全体はこんな感じです。
'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を再起動します。
ピカチュウが思いやりの心を取り戻してくれて幸せです。ぼくのピカチュウは意地悪な穴埋めクイズとか出さない。そんなの幻覚だった。
こんな素晴らしいプラグインを作ってくださったのだから何かしなければと思い、採用されようがされまいが、この修正でPRを送ってみました。(追記: 1週間後ぐらいに作者がマージしてくれました)このちゃちいコードがお礼になっているかどうかはさておき。
おわりに
hyperにはGithub zeit/hyper Unreadable nano labels if backgroundColor is transparentみたいな問題もあって(このissueはnanoについてですが、hyper-pokemonを入れた状態で、ターミナルにペーストしたテキストが読めないのもこれと同じ原因)、凝ったことしようとすると、まともに使えるようにするには色々自力で書く必要がありそうです。また、開発も止まってしまっているようで、メインで使うにはどうかなと思いました。でもhyperで遊ぶの楽しい楽しい。