ATOM
AtomDay 21

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

はじめに

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

Atomを使用している方であれば、普段から$ apm install {パッケージ名}というコマンドを叩いたり、Settings > Installから検索したりして、様々な便利なパッケージをインストールしているかと思います。
そんなAtomのパッケージですが、最近こちらの記事を見て、実は思っていた以上にお手軽に自作できてしまうということを今更ながら知りました!

この記事では、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以下のシンボリックリンクは削除されますが、生成したパッケージのディレクトリ自体は削除されないようです。

~/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
~/.atom/packages

...

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

...

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

スクリーンショット 2017-12-21 3.02.23.png

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

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

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

package.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

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

keymaps/my-package.json
{
  "atom-workspace": {
    "ctrl-alt-o": "my-package:toggle"
  }
}
  • atom-workspace領域内でctrl-alt-oを押下することでmy-package:toggleコマンドを実行するように定義されている

menus/my-package.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()を使用します。

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

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

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

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

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

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

...

-  toggle() {
+  countdown() {
keymaps/my-package.json
   "atom-workspace": {
-    "ctrl-alt-o": "my-package:toggle"
+    "ctrl-alt-o": "my-package:countdown"
   }
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を定義することで、ユーザ設定が有効になります。

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

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

スクリーンショット 2017-12-21 7.07.09.png

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

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を使用することでメッセージ通知が実現できます。
今回はatom.notifications.addInfo()を使用してみます。

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

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

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

完成したソースコード

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

package.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

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

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

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

menus/my-package.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パッケージの自作は意外と簡単にできてしまう」ということが伝わっていれば幸いです!

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

参考記事