10分で学ぶ！Atomパッケージ自作入門

## はじめに

[Atomアドベントカレンダー](https://qiita.com/advent-calendar/2017/atom)21日目の記事（飛び入り参加）です。

Atomを使用している方であれば、普段から`$ apm install {パッケージ名}`というコマンドを叩いたり、`Settings > Install`から検索したりして、様々な便利なパッケージをインストールしているかと思います。
そんなAtomのパッケージですが、最近[こちら](https://engineer.dena.jp/2016/12/dena-engineer-advent-calendar-2016-2-atom-package.html)の記事を見て、実は思っていた以上にお手軽に自作できてしまうということを今更ながら知りました！

この記事では、Atomパッケージ自作の入門として「コマンド実行後、一定時間が経過したら`It's Time!`というメッセージを表示する」という簡単なパッケージを作成する手順を紹介していきます。

なお、開発環境は以下の通りです。

* `OS`: `macOS Sierra 10.12.6`
* `Atom`: `1.22.1`

## パッケージの雛形を生成

まずはパッケージの雛形を生成します。

コマンドパレットを表示（Macの場合：`cmd-shift-p`）して`Package Generator: Generate Package`を実行すると、生成するパッケージのパスの入力（デフォルトでは`~/github/my-package`）を求められます。

※本来であれば適切な名前をつけるべきですが、ここではデフォルトの`~/github/my-package`のままパッケージを生成したものとします。

パッケージのパスを入力すると、`~/github/my-package`以下に下記のようなファイルやディレクトリが生成され、同時に`~/.atom/packages`以下に生成したパッケージ（`~/github/my-package`）へのシンボリックリンクが貼られます。

余談ですが、自作パッケージをアンインストールすると`~/.atom/packages`以下のシンボリックリンクは削除されますが、生成したパッケージのディレクトリ自体は削除されないようです。

```console:~/github/my-package
.
├── keymaps
│   └── my-package.json
├── lib
│   ├── my-package-view.js
│   └── my-package.js
├── menus
│   └── my-package.json
├── package.json
├── spec
│   ├── my-package-spec.js
│   └── my-package-view-spec.js
└── styles
    └── my-package.less
```

```console:~/.atom/packages

...

my-package -> /PATH/TO/~/github/my-package

...

```

雛形の状態では`my-package:toggle`というコマンドが定義されていて、このコマンドを実行すると以下のようなモーダルが表示されます。

![スクリーンショット 2017-12-21 3.02.23.png](https://qiita-image-store.s3.amazonaws.com/0/125531/79abc68a-17af-ad8b-006e-ede777649fd5.png)

主要なファイルについては、ざっくりと説明すると以下の通りです。

* `package.json`: パッケージの説明や依存ライブラリなどを定義する
* `lib/my-package.js`: パッケージのエントリポイント
* `keymaps/my-package.json`: パッケージのショートカットキーを定義する
* `menus/my-package.json`: メニューバーやコンテキストメニュー（右クリックで表示するメニュー）の表示内容を定義する

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

### package.json

```json:package.json
{
  "name": "my-package",
  "main": "./lib/my-package",
  "version": "0.0.0",
  "description": "A short description of your package",
  "keywords": [
  ],
  "activationCommands": {
    "atom-workspace": "my-package:toggle"
  },
  "repository": "https://github.com/atom/my-package",
  "license": "MIT",
  "engines": {
    "atom": ">=1.0.0 <2.0.0"
  },
  "dependencies": {
  }
}
```

* `main`: エントリポイントへのパスの指定
* `activationCommands`: パッケージを有効化するためのコマンドの指定
    * `my-package:toggle`コマンドを実行するまでパッケージの読み込み（有効化）を遅延させるように定義されている

`activationCommands`については、定義自体を削除するとAtomの起動と同時にパッケージが有効化されるようになります。
ただし、**起動と同時にパッケージを有効化しようとするとその分起動時の動作が重くなってしまう**ので、コマンド実行型のパッケージであれば`activationCommands`を定義しておいた方がよさそうです。


### lib/my-package.js

```js:lib/my-package.js
'use babel';

import MyPackageView from './my-package-view';
import { CompositeDisposable } from 'atom';

export default {

  myPackageView: null,
  modalPanel: null,
  subscriptions: null,

  activate(state) {
    this.myPackageView = new MyPackageView(state.myPackageViewState);
    this.modalPanel = atom.workspace.addModalPanel({
      item: this.myPackageView.getElement(),
      visible: false
    });

    // Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
    this.subscriptions = new CompositeDisposable();

    // Register command that toggles this view
    this.subscriptions.add(atom.commands.add('atom-workspace', {
      'my-package:toggle': () => this.toggle()
    }));
  },

  deactivate() {
    this.modalPanel.destroy();
    this.subscriptions.dispose();
    this.myPackageView.destroy();
  },

  serialize() {
    return {
      myPackageViewState: this.myPackageView.serialize()
    };
  },

  toggle() {
    console.log('MyPackage was toggled!');
    return (
      this.modalPanel.isVisible() ?
      this.modalPanel.hide() :
      this.modalPanel.show()
    );
  }

};
```

* `activate(state)`: パッケージ起動時（パッケージが有効化された際）の処理
    * `this.subscriptions.add(atom.commands.add(...))`: `my-package:toggle`コマンドを実行した際に`toggle()`メソッドを呼び出すように定義されている
* `deactivate()`:  パッケージ終了時の処理

### keymaps/my-package.json

```json:keymaps/my-package.json
{
  "atom-workspace": {
    "ctrl-alt-o": "my-package:toggle"
  }
}
```

* `atom-workspace`領域内で`ctrl-alt-o`を押下することで`my-package:toggle`コマンドを実行するように定義されている

### menus/my-package.json

```json:menus/my-package.json
{
  "context-menu": {
    "atom-text-editor": [
      {
        "label": "Toggle my-package",
        "command": "my-package:toggle"
      }
    ]
  },
  "menu": [
    {
      "label": "Packages",
      "submenu": [
        {
          "label": "my-package",
          "submenu": [
            {
              "label": "Toggle",
              "command": "my-package:toggle"
            }
          ]
        }
      ]
    }
  ]
}
```

* `context-menu`: `atom-text-editor`領域でのコンテキストメニューに`Toggle my-package`というラベルで`my-package:toggle`コマンドの実行処理を定義されている
* `menu`: メニューバー > `Packages` > `my-package` > `Toggle`というラベルで`my-package:toggle`コマンドの実行処理を定義されている

## 機能実装

ここからは、今回作成するパッケージの機能を実装していきます。

### 1. メッセージを表示する

まずはコマンド実行時に`It's Time!`というメッセージを表示させます。現時点では、とりあえずメッセージの表示には`window.alert()`を使用します。

```diff:lib/my-package.js
   toggle() {
-    console.log('MyPackage was toggled!');
-    return (
-      this.modalPanel.isVisible() ?
-      this.modalPanel.hide() :
-      this.modalPanel.show()
-    );
+    window.alert("It's Time!");
   }
```

ソースコードの修正後、変更内容を反映させるにはAtomを再起動する必要があります。
メニューから`View` > `Developer` > `Reload Window`を選択することで再起動できますが、キーボードショートカット（Macの場合：`ctrl-alt-cmd-l`）を覚えておくと便利です。

再起動後、`my-package:toggle`コマンドを実行すると以下のような形式でメッセージが表示されます。

![スクリーンショット 2017-12-21 4.10.47.png](https://qiita-image-store.s3.amazonaws.com/0/125531/90e441e2-88ed-f939-68d4-72eda7d6e81f.png)

### 2. 一定時間が経過したらメッセージを表示する

次に、コマンド実行してから一定時間が経過してからメッセージを表示するように修正します。`setTimeout()`を使用して5秒後にメッセージを表示させます。

```diff:lib/my-package.js
   toggle() {
-    window.alert("It's Time!");
+    setTimeout(() => {
+      window.alert("It's Time!");
+    }, 5000);
   }
```

また、メソッド名やコマンドの名称、メニューのラベルを`toggle`から`countdown`に変更しておきます。

```diff:package.json
   "activationCommands": {
-    "atom-workspace": "my-package:toggle"
+    "atom-workspace": "my-package:countdown"
   },
```

```diff:lib/my-package.js
     this.subscriptions.add(atom.commands.add('atom-workspace', {
-      'my-package:toggle': () => this.toggle()
+      'my-package:countdown': () => this.countdown()
     }));

...

-  toggle() {
+  countdown() {
```

```diff:keymaps/my-package.json
   "atom-workspace": {
-    "ctrl-alt-o": "my-package:toggle"
+    "ctrl-alt-o": "my-package:countdown"
   }
```

```diff:menus/my-package.json
   "context-menu": {
     "atom-text-editor": [
       {
-        "label": "Toggle my-package",
-        "command": "my-package:toggle"
+        "label": "Countdown",
+        "command": "my-package:countdown"
       }
     ]
   },

...

           "submenu": [
             {
-              "label": "Toggle",
-              "command": "my-package:toggle"
+              "label": "Countdown",
+              "command": "my-package:countdown"
             }
           ]
```

### 3. カウントダウンの秒数をユーザが設定できるようにする

これまでの修正によりコマンド実行の5秒後にメッセージが表示されるようになりましたが、この秒数をユーザが設定できるようにします。
エントリポイントに設定しているメインのスクリプト（`lib/my-package.js`）に`config`を定義することで、ユーザ設定が有効になります。

```diff:lib/my-package.js
 export default {
-
+  config: {
+    "countdownSeconds": {
+      "type": "integer",
+      "default": 5
+    }
+  },
   myPackageView: null,
```

上記の定義を追加することで、パッケージ設定画面にユーザ設定の項目（`Countdown Seconds`）が追加されます。

![スクリーンショット 2017-12-21 7.07.09.png](https://qiita-image-store.s3.amazonaws.com/0/125531/300a2548-22b4-e2e1-e52f-6c5967288bd7.png)

[atom.config](https://atom.io/docs/api/v1.2.0/Config)を参照してユーザ設定の値を取得し、任意の秒数が経過したらメッセージを表示するように修正します。`atom.config.get()`の第一引数は`{パッケージ名}.{configのキー名}`の形式で指定します。

```diff:lib/my-package.js
   countdown() {
+    const countdown_seconds = atom.config.get('my-package.countdownSeconds');
     setTimeout(() => {
       window.alert("It's Time!");
-    }, 5000);
+    }, countdown_seconds * 1000);
   }
```

これで「コマンド実行後、一定時間が経過したら`It's Time!`というメッセージを表示する」という機能を持ったパッケージが自作できました！

### 4. おまけ：メッセージの表示形式を変更

これで必要な機能は実現できましたが、せっかくなので`window.alert()`の代わりにAtomに用意されているメッセージ通知機能を利用してみます。

[atom.notifications](https://atom.io/docs/api/v1.23.1/NotificationManager)を使用することでメッセージ通知が実現できます。
今回は[atom.notifications.addInfo()](https://atom.io/docs/api/v1.23.1/NotificationManager#instance-addInfo)を使用してみます。

```diff:lib/my-package.js
     setTimeout(() => {
-      window.alert("It's Time!");
+      atom.notifications.addInfo("It's Time!");
     }, countdown_seconds * 1000);
```

また、オプションとして`dismissable: true`を指定することでユーザが任意のタイミングで通知メッセージを閉じることができるようになります。

```diff:lib/my-package.js
     setTimeout(() => {
-      atom.notifications.addInfo("It's Time!");
+      atom.notifications.addInfo("It's Time!", {dismissable: true});
     }, countdown_seconds * 1000);
```

これらの修正を加えることで、以下のような形式で通知メッセージが表示されるようになります。

![スクリーンショット 2017-12-21 8.15.55.png](https://qiita-image-store.s3.amazonaws.com/0/125531/fe4a55e8-bdfd-b557-e72d-a96a31f12a25.png)

## 完成したソースコード

機能実装後の最終的なソースコードは以下の通りです。

### package.json

```json:package.json
{
  "name": "my-package",
  "main": "./lib/my-package",
  "version": "0.0.0",
  "description": "A short description of your package",
  "keywords": [
  ],
  "activationCommands": {
    "atom-workspace": "my-package:countdown"
  },
  "repository": "https://github.com/atom/my-package",
  "license": "MIT",
  "engines": {
    "atom": ">=1.0.0 <2.0.0"
  },
  "dependencies": {
  }
}
```

### lib/my-package.js

※これまでの機能実装の部分では特に取り上げていませんでしたが、不要なコードは削除しています。

```js:lib/my-package.js
'use babel';

import { CompositeDisposable } from 'atom';

export default {
  config: {
    "countdownSeconds": {
      "type": "integer",
      "default": 5
    }
  },
  subscriptions: null,

  activate(state) {
    this.subscriptions = new CompositeDisposable();
    this.subscriptions.add(atom.commands.add('atom-workspace', {
      'my-package:countdown': () => this.countdown()
    }));
  },

  deactivate() {
    this.subscriptions.dispose();
  },

  countdown() {
    const countdown_seconds = atom.config.get('my-package.countdownSeconds');
    setTimeout(() => {
      atom.notifications.addInfo("It's Time!", {dismissable: true});
    }, countdown_seconds * 1000);
  }
};
```

### keymaps/my-package.json

```json:keymaps/my-package.json
{
  "atom-workspace": {
    "ctrl-alt-o": "my-package:countdown"
  }
}
```

### menus/my-package.json

```json:menus/my-package.json
{
  "context-menu": {
    "atom-text-editor": [
      {
        "label": "Countdown",
        "command": "my-package:countdown"
      }
    ]
  },
  "menu": [
    {
      "label": "Packages",
      "submenu": [
        {
          "label": "my-package",
          "submenu": [
            {
              "label": "Countdown",
              "command": "my-package:countdown"
            }
          ]
        }
      ]
    }
  ]
}
```

## おわりに

簡単な実装例を取り入れつつ、Atomのパッケージを自作する手順について紹介しました。表題通り**10分で学べる**程度のボリュームに上手くまとまっているかは微妙なところですが、この記事を通して「Atomパッケージの自作は意外と簡単にできてしまう」ということが伝わっていれば幸いです！

時間があれば、いずれ今回作ったパッケージを実際に公開してみようと思います。

## 参考記事

* [Atom Documentation > Atom Flight Manual > Chapter 3: Hacking Atom > Package: Word Count](http://flight-manual.atom.io/hacking-atom/sections/package-word-count/)
* [Atomパッケージを作ってみよう - Technology of DeNA](https://engineer.dena.jp/2016/12/dena-engineer-advent-calendar-2016-2-atom-package.html)
* [自作のAtomパッケージでユーザー設定を有効にする | 開発 / note.nkmk.me](https://note.nkmk.me/dev-atom-package-user-setting/)
* [Atom Documentation > Essential Classes: Config](https://atom.io/docs/api/v1.23.1/Config)
* [Atom Documentation > Essential Classes: NotificationManager](https://atom.io/docs/api/v1.23.1/NotificationManager)