ATOM
AtomDay 24

5分で学ぶ!Atomテーマ(Syntax Theme)自作入門

はじめに

Atomアドベントカレンダー24日目の記事(飛び入り参加 3日ぶり2回目)です。

先日Atomパッケージの自作についての記事を書きましたが、今回はAtomテーマ(Syntax Theme)の自作方法を紹介していきます。

Atomのテーマについて

AtomのテーマにはUI ThemeSyntax Themeの2種類があります。

UI Themeはツリービューやタブなどの見た目に、Syntax Themeは主にテキストエディタ部分のコードの見た目にそれぞれ影響を与えます。
この2つの違いについては、公式ドキュメントの画像を見るとイメージしやすいかと思います。

この記事では、この2つのうちSyntax Themeの自作方法を紹介していきます。

テーマの雛形を生成

まず、テーマの雛形を生成します。

パッケージ作成の時と同様、コマンドパレットからPackage Generator: Generate Syntax Themeを実行することでSyntax Themeの雛形が作成できます。
こちらも作成するテーマのパスの入力を求められますが、今回はデフォルトの~/github/my-theme-syntaxのまま進めていきます。

公式ドキュメントによると、Syntax Themeの場合はパスの末尾を-syntaxとするべきだそうです。

Tip: Syntax themes should end with -syntax and UI themes should end with -ui.

パスを入力してテーマの雛形の生成が完了すると、~/github/my-theme-syntaxディレクトリが生成され、~/.atom/packagesにこのパスへのシンボリックリンクが貼られます。

~/.atom/packages

...

my-theme-syntax -> /PATH/TO/~/github/my-theme-syntax

...

~/.atom/packages以下にシンボリックリンクが貼られることにより、自作のテーマ(my-theme-syntax)がインストール済みのSyntax Themeとして扱われ、Settings > Themes > Choose a Theme > Syntax Themeから選択できるようになります。(my-theme-syntaxという名前の場合、Myというテーマ名として扱われるようです。)

スクリーンショット 2017-12-23 2.29.12.png

なお、作成したテーマの雛形のディレクトリ(~/github/my-theme-syntax)以下には、下記のようなファイル・ディレクトリが生成されています。

~/github/my-theme-syntax
.
├── index.less
├── package.json
└── styles
    ├── base.less
    ├── colors.less
    └── syntax-variables.less

各ファイルの役割はざっくり説明すると以下の通りです。

  • package.json: テーマの名前や説明などの情報を定義するファイル
  • index.less: メインのスタイルシート(実態はstyles/base.lessをインポートしているだけ)
  • styles/base.less: エディタ部分のスタイルを定義する、ベースとなるスタイルシート(styles/syntax-variables.lessをインポートしている)
  • styles/syntax-variables.less: styles/base.lessで使用する、構文に関する変数を定義するスタイルシート(styles/colors.lessをインポートしている)
  • styles/colors.less: styles/base.less, styles/syntax-variables.lessで使用する、色に関する変数を定義するスタイルシート

各ファイルの内容は、デフォルトではそれぞれ以下のようになっています。

package.json

package.json
{
  "name": "my-theme-syntax",
  "theme": "syntax",
  "version": "0.0.0",
  "description": "A short description of your syntax theme",
  "keywords": [
    "syntax",
    "theme"
  ],
  "repository": "https://github.com/atom/my-theme-syntax",
  "license": "MIT",
  "engines": {
    "atom": ">=1.0.0 <2.0.0"
  }
}

index.less

index.less
@import "./styles/base.less";

styles/base.less

styles/base.less
@import "syntax-variables";

atom-text-editor {
  background-color: @syntax-background-color;
  color: @syntax-text-color;

  .wrap-guide {
    background-color: @syntax-wrap-guide-color;
  }

  .indent-guide {
    color: @syntax-indent-guide-color;
  }

  .invisible-character {
    color: @syntax-invisible-character-color;
  }

  .gutter {
    background-color: @syntax-gutter-background-color;
    color: @syntax-gutter-text-color;

    .line-number {
      &.cursor-line {
        background-color: @syntax-gutter-background-color-selected;
        color: @syntax-gutter-text-color-selected;
      }

      &.cursor-line-no-selection {
        color: @syntax-gutter-text-color-selected;
      }
    }
  }

  .gutter .line-number.folded,
  .gutter .line-number:after,
  .fold-marker:after {
    color: @light-gray;
  }

  .invisible {
    color: @syntax-text-color;
  }

  .cursor {
    color: @syntax-cursor-color;
  }

  .selection .region {
    background-color: @syntax-selection-color;
  }
}


// Syntax styles

.syntax--comment {
  color: @light-gray;
}

.syntax--keyword {
  color: @purple;

  &.syntax--control {
    color: @purple;
  }

  &.syntax--operator {
    color: @syntax-text-color;
  }

  &.syntax--other.syntax--special-method {
    color: @blue;
  }

  &.syntax--other.syntax--unit {
    color: @orange;
  }
}

.syntax--storage {
  color: @purple;
}

.syntax--constant {
  color: @orange;

  &.syntax--character.syntax--escape {
    color: @cyan;
  }

  &.syntax--numeric {
    color: @orange;
  }

  &.syntax--other.syntax--color {
    color: @cyan;
  }

  &.syntax--other.syntax--symbol {
    color: @green;
  }
}

.syntax--variable {
  color: @red;

  &.syntax--interpolation {
    color: darken(@red, 10%);
  }

  &.syntax--parameter.syntax--function {
    color: @syntax-text-color;
  }
}

.syntax--invalid.syntax--illegal {
  background-color: @red;
  color: @syntax-background-color;
}

.syntax--string {
  color: @green;


  &.syntax--regexp {
    color: @cyan;

    .syntax--source.syntax--ruby.syntax--embedded {
      color: @orange;
    }
  }

  &.syntax--other.syntax--link {
    color: @red;
  }
}

.syntax--punctuation {
  &.syntax--definition {
    &.syntax--comment {
      color: @light-gray;
    }

    &.syntax--string,
    &.syntax--variable,
    &.syntax--parameters,
    &.syntax--array {
      color: @syntax-text-color;
    }

    &.syntax--heading,
    &.syntax--identity {
      color: @blue;
    }

    &.syntax--bold {
      color: @light-orange;
      font-weight: bold;
    }

    &.syntax--italic {
      color: @purple;
      font-style: italic;
    }
  }

  &.syntax--section.syntax--embedded {
    color: darken(@red, 10%);
  }

}

.syntax--support {
  &.syntax--class {
    color: @light-orange;
  }

  &.syntax--function  {
    color: @cyan;

    &.syntax--any-method {
      color: @blue;
    }
  }
}

.syntax--entity {
  &.syntax--name.syntax--function {
    color: @blue;
  }
  &.syntax--name.syntax--type {
    color: @light-orange;
    text-decoration: underline;
  }

  &.syntax--other.syntax--inherited-class {
    color: @green;
  }
  &.syntax--name.syntax--class, &.syntax--name.syntax--type.syntax--class {
    color: @light-orange;
  }

  &.syntax--name.syntax--section {
    color: @blue;
  }

  &.syntax--name.syntax--tag {
    color: @red;
    text-decoration: underline;
  }

  &.syntax--other.syntax--attribute-name {
    color: @orange;

    &.syntax--id {
      color: @blue;
    }
  }
}

.syntax--meta {
  &.syntax--class {
    color: @light-orange;
  }

  &.syntax--link {
    color: @orange;
  }

  &.syntax--require {
    color: @blue;
  }

  &.syntax--selector {
    color: @purple;
  }

  &.syntax--separator {
    background-color: @gray;
    color: @syntax-text-color;
  }
}

.syntax--none {
  color: @syntax-text-color;
}

.syntax--markup {
  &.syntax--bold {
    color: @orange;
    font-weight: bold;
  }

  &.syntax--changed {
    color: @purple;
  }

  &.syntax--deleted {
    color: @red;
  }

  &.syntax--italic {
    color: @purple;
    font-style: italic;
  }

  &.syntax--heading .syntax--punctuation.syntax--definition.syntax--heading {
    color: @blue;
  }

  &.syntax--inserted {
    color: @green;
  }

  &.syntax--list {
    color: @red;
  }

  &.syntax--quote {
    color: @orange;
  }

  &.syntax--raw.syntax--inline {
    color: @green;
  }
}

.syntax--source.syntax--gfm .syntax--markup {
  -webkit-font-smoothing: auto;
  &.syntax--heading {
    color: @green;
  }
}


// Mini editor

atom-text-editor[mini] .scroll-view {
  padding-left: 1px;
}

styles/syntax-variables.less

styles/syntax-variables.less
@import "colors";

// This defines all syntax variables that syntax themes must implement when they
// include a syntax-variables.less file.

// General colors
@syntax-text-color: @very-light-gray;
@syntax-cursor-color: white;
@syntax-selection-color: lighten(@dark-gray, 10%);
@syntax-background-color: @very-dark-gray;

// Guide colors
@syntax-wrap-guide-color: @dark-gray;
@syntax-indent-guide-color: @gray;
@syntax-invisible-character-color: @gray;

// For find and replace markers
@syntax-result-marker-color: @light-gray;
@syntax-result-marker-color-selected: white;

// Gutter colors
@syntax-gutter-text-color: @very-light-gray;
@syntax-gutter-text-color-selected: @syntax-gutter-text-color;
@syntax-gutter-background-color: @dark-gray;
@syntax-gutter-background-color-selected: @gray;

// For git diff info. i.e. in the gutter
@syntax-color-renamed: @blue;
@syntax-color-added: @green;
@syntax-color-modified: @orange;
@syntax-color-removed: @red;

styles/colors.less

styles/colors.less
// These colors are specific to the theme. Do not use in a package!

@very-light-gray: #c5c8c6;
@light-gray: #969896;
@gray: #373b41;
@dark-gray: #282a2e;
@very-dark-gray: #1d1f21;

@cyan: #8abeb7;
@blue: #81a2be;
@purple: #b294bb;
@green: #b5bd68;
@red: #cc6666;
@orange: #de935f;
@light-orange: #f0c674;

テーマの作成

ここからは実際にテーマを作成していきます。

…といっても、テーマ(Syntax Theme)の作成については

  1. 自分が作りたいテーマのイメージカラーをそのままLESS(CSS)に落とし込み
  2. 適当なコード(後述)に対する見た目がいい感じになるまで試行錯誤を繰り返す

というイメージです。

今回は例として以下のようなカラーリングのテーマを作成してみます。

  • コメント:
  • function(アロー関数): ピンク
  • class: 臙脂
  • クラス: 黄色
  • get, new: 黄緑
  • 関数:
  • return:
  • 文字列:
  • 変数:
  • static, const:
  • 標準関数: 浅葱
  • 定数: オレンジ

参考までに、テーマの見た目の確認用として以下のようなjsのコードを使用しました。

/**
 * Syntax Theme Test
 */
(() => {
    class Hoge {
        static get STATIC_MESSAGE() {
            return 'HOGEHOGE';
        }

        constructor(msg) {
            this.msg = msg;
        }

        get replacedMessage() {
            return this.msg.replace(/^(.)(.+)(.)$/, (match, p1, p2, p3) => {
                return `${p1.toUpperCase()}${p2}${p3.toUpperCase()}`;
            });
        }
    }

    const hoge = new Hoge('hogehoge');

    console.log(Hoge.STATIC_MESSAGE);
    // HOGEHOGE
    console.log(hoge.replacedMessage());
    // HogehogE
})();

各ファイルの修正内容はそれぞれ以下の通りです。

※LESS(CSS)のお作法に詳しくないため、変数定義が非常に雑です。「もっといいやり方があるよ!」とか「ここはこうした方がいいよ!」といったコメント大歓迎です。

styles/colors.less

まずはstyles/colors.lessに色に関する変数を定義していきます。
@red@blueなどの変数名は既に使用されていたため、自作テーマの変数にはmy-という接頭辞を付与して競合を回避しました。

styles/colors.less
 @light-orange: #f0c674;
+
+@my-red: #f70f1f;
+@my-blue: #0775c4;
+@my-white: #aececb;
+@my-orange: #f29047;
+@my-green: #00a752;
+@my-purple: #7e51a6;
+@my-pink: #fa98bf;
+@my-black: #464b4f;
+@my-yellow: #fcd424;
+@my-fresh-green: #a1ca62;
+@my-pale-blue-green: #00b1bb;
+@my-carmine: #b51d66;

styles/syntax-variables.less

次にstyles/syntax-variables.lessに構文に関する変数を定義していきます。

実際に進めていった手順としては

  1. Atom上で表示されるソースコードについて、デベロッパーツールでDOM構造を眺めつつ
  2. 任意色を反映させたい要素に対してstyles/base.lessの定義に合わせてそれっぽい変数名を付けていく

といった感じでした。

 @syntax-background-color: @very-dark-gray;

+@syntax-comment-color: @my-black;
+@syntax-keyword-control-color: @my-purple;
+@syntax-keyword-operator-color: @my-fresh-green;
+@syntax-storage-color: @my-white;
+@syntax-storage-class-color: @my-carmine;
+@syntax-storage-function-rolor: @my-pink;
+@syntax-constant-color: @my-orange;
+@syntax-variable-rolor: @my-red;
+@syntax-string-color: @my-green;
+@syntax-support-function-color: @my-pale-blue-green;
+@syntax-entity-name-function-color: @my-blue;
+@syntax-entity-name-class-color: @my-yellow;
+
 // Guide colors

styles/base.less

そして、styles/base.lessにスタイルの定義を反映させていきます。

styles/base.less
 .syntax--comment {
-  color: @light-gray;
+  color: @syntax-comment-color;
 }

 .syntax--keyword {
   color: @purple;

   &.syntax--control {
-    color: @purple;
+    color: @syntax-keyword-control-color;
   }

   &.syntax--operator {
     color: @syntax-text-color;
+
+    &.syntax--new, &.syntax--getter {
+        color: @syntax-keyword-operator-color;
+    }
   }

...

 .syntax--storage {
-  color: @purple;
+  color: @syntax-storage-color;
+
+  &.syntax--class {
+    color: @syntax-storage-class-color;
+  }
+
+  &.syntax--function {
+    color: @syntax-storage-function-rolor;
+  }
 }

 .syntax--constant {
-  color: @orange;
+  color: @syntax-constant-color;

...

 .syntax--variable {
-  color: @red;
+  color: @syntax-variable-rolor;

   &.syntax--interpolation {
     color: darken(@red, 10%);

...

 .syntax--string {
-  color: @green;
+  color: @syntax-string-color;

...

 .syntax--punctuation {
   &.syntax--definition {
     &.syntax--comment {
-      color: @light-gray;
+      color: @syntax-comment-color;
     }

...

   &.syntax--function  {
-    color: @cyan;
+    color: @syntax-support-function-color;

...

 .syntax--entity {
   &.syntax--name.syntax--function {
-    color: @blue;
+    color: @syntax-entity-name-function-color;
   }

...

-  &.syntax--name.syntax--class, &.syntax--name.syntax--type.syntax--class {
-    color: @light-orange;
+  &.syntax--name.syntax--class, &.syntax--name.syntax--type.syntax--class, &.syntax--name.syntax--instance {
+    color: @syntax-entity-name-class-color;
   }

テーマ修正前後でのコードの見た目

テーマを修正する前後でのコードの見た目はそれぞれ以下のような感じです。

テーマ修正前

スクリーンショット 2017-12-23 7.42.01.png

全体的に落ち着いた色合いでまとまっている印象です。

テーマ修正後

スクリーンショット 2017-12-23 7.40.58.png

とてもカラフルな見た目に変わりました!

おわりに

駆け足でしたが、Atomのテーマ(Syntax Theme)の自作方法について紹介していきました。

やってみた感想としては、コードの見た目を確認しつつ適宜スタイル定義を変更していくだけなので、パッケージ自作の時と同様「思っていた以上にお手軽に自作できてしまう」という印象でした!
(やろうと思えば非常に細かくスタイル定義ができるので、凝ったものを作ろうと思えば、もちろんそれ相応に大変だとは思います。)

「Atomの見た目をこんな風に変えてみたい!」というイメージさえあれば、あとはLESS(CSS)をガリガリ書いていくだけなので、もし興味がある方は試してみてはいかがでしょうか?

いずれちゃんとしたテーマを作成して公開できればと思っております。
あと、時間に余裕があればUI Themeの方の自作にも挑戦してみたいです。

参考記事