Terminalで「すぐに消せ」ごっこ (Mac, JXA)

JXAでTerminalを操作する方法を調べたので、メモするために書きました。
間違った記述があれば指摘していただけるとありがたいです。
全部読めばTerminal以外の使い方も何となく分かるかもしれません。

JXAを知らない方は以下の記事をどうぞ。

[知らないうちにMacがシステム標準でJavaScriptで操作できるようになってた (JXA) - Qiita](http://qiita.com/zakuroishikuro/items/1b02378bf9e940602d87)

# 「すぐに消せ」について

[すぐにけせとは (スグニケセとは) [単語記事] - ニコニコ大百科](http://dic.nicovideo.jp/a/%E3%81%99%E3%81%90%E3%81%AB%E3%81%91%E3%81%9B)

> SFC版「真・女神転生」で、電源を入れた際、ごくたまに上記のように画面いっぱいに「すぐにけせ」と表示されるという都市伝説（？）

例の都市伝説です。
1/65536の確率で発生すると言われています。

今回は確率は考慮しません。必ず発生するようにします。
音も鳴らしません。

## やること

- ターミナルのウィンドウを取得する
- 表示を変更する (黒背景 & 赤文字)
- コマンドを実行する (画面いっぱいに「すぐに消せ」を表示)

これだけです。
(というか、Terminalの機能はこれでほぼ全てですね。)
「すぐに消せ」ごっこに使わない機能もついでに紹介していきます。

※簡単にコードを試すには、`osascript`のインタラクティブモードが便利です。

```text:インタラクティブモード起動
$ osascript -l JavaScript -i
```

# Terminalの操作方法

Terminalの参照を取得します。

```js:Terminalの参照を取得
Terminal = Application("Terminal");
```

**以下の全てのコードにおいて、変数`Terminal`は`Application("Terminal")`を参照しているものとします。**

Terminalからウィンドウを取得します。

```js:ウィンドウ、タブの参照を取得
var win = Terminal.windows[0]; //最前面のウィンドウを取得

var tab = win.selectedTab(); //選択されているタブを取得
```

ウィンドウは最前面から順番に`.windows`に格納されています。
なので`.windows[0]`で最前面のウィンドウを取得できます。
(背面のウィンドウをクリックすると瞬時に0番目になります。)

Terminalはタブ表示に対応したアプリケーションです。(`command + t`で新しいタブを開ける)
表示中の情報にアクセスするには、ウィンドウからタブを取得する必要があります。

選択されているタブを取得するにはウィンドウの`.selectedTab()`を呼びます。
選択されていないタブを参照したい場合は`.tabs`から取得します。
`.tabs`の順番は`.windows`と違い、前後の並び順ではなく作成順なので注意してください。

## コマンドを実行する

コマンド実行には`.doScript`を使います。

```js:コマンドを実行
var newTab = Terminal.doScript("echo 'hi.'");
```
Terminalから普通に呼ぶと新しいウィンドウが開き、そのタブの参照が返ってきます。
既存のウィンドウで実行したい場合、タブかウィンドウをオプションとして渡します。

```js:指定したタブ内でコマンドを実行
var tab = Terminal.windows[0].selectedTab();

Terminal.doScript("echo = '好きなタブで実行'", {in:tab});
```
(osascriptのインタラクティブモード内で実行しても大丈夫なよう、`echo`の後に「=」をつけてます。ただし変数「echo」に文字列が代入されます。)

**Terminalを操作するメソッドは、この`.doScript()`だけです。**
指定したタブの中で「文字列 + 改行」が入力されるだけっぽいです。

その他に何かさせたいなら`System Events`を使ってください。
詳しくはこちら→ [Macのキーボード入力、マウスクリックをJavaScriptで (JXA) - Qiita](http://qiita.com/zakuroishikuro/items/afab0e33ad2030ba2f92)

ちなみに`Standard Additions`の`doShellScript`を使うとコマンドの戻り値を得ることが出来ます。
こちらはTerminalとは関係なく、どのアプリケーションでも使用可能です。

```js:.doShellScript
var app = Application.currentApplication();
app.includeStandardAdditions = true;

app.doShellScript("date"); //=> "2015年 2月17日 火曜日 01時20分01秒 JST"
```

## ログ内容を取得する

タブの`.contents()`か`.history()`を呼ぶとログを文字列として取得できます。
読み取り専用であり、書き換え不可です。

```js:ログ内容の取得
tab.contents(); //画面に表示されているログを取得
tab.history(); //スクロールバーで隠れているログも全部取得
```

ちなみに、ここから下の説明はほぼ全てTerminalの見た目の変更についてです。

## 表示色を変更する

タブの参照から、表示設定を変更することができます。

**一時的なものであり、新しくウィンドウを開けば戻るので安心してください。**

色情報は`red` `green` `blue`プロパティを持ったオブジェクトで表現します。
アルファ値は存在しません。
各数値は0.0〜1.0のfloatです。

変更可能な色設定は以下の4つです。

```js:色の変更
tab.normalTextColor = {red:1}; //文字色を赤に
tab.boldTextColor = {red:1}; //太字の文字色も赤に

tab.backgroundColor = {}; //背景色を黒に
tab.cursorColor = {red:0.2, green:0.2, blue:0.2}; //カーソルを暗い灰色に
```

省略したプロパティは0になります。1以上は1になります。

色情報はColorオブジェクトと呼ばれ、カラーピッカーから選択して作成することもできます。
`Standard Additions`の`.chooseColor()`を使ってください。

## フォントを変更する

フォントの変更可能な設定は以下の3つです。

```js:フォントの設定
tab.fontName = "PCMyungjo"; //フォント設定
tab.fontSize = 20; //文字サイズ設定

tab.fontAntialiasing = true; //アンチエイリアスを有効にする
```
ターミナルのフォントが明朝体だと色んな意味で怖いので明朝体にしました。
「消」の上の点々がハの字になってる辺りが胡散臭くていい感じだったのでこのフォントにしました。

アンチエイリアスの設定は、Retinaディスプレイだとfalseにしても常に有効ぽいです。
(それか、アンチエイリアスが無効でも視認できてない？)

ここまでの全てのコードを実行するとこんな感じになってるはずです。

![スクリーンショット 2015-02-15 23.55.17.png](https://qiita-image-store.s3.amazonaws.com/0/18902/04eee4ed-1b6a-b83e-b917-41623ca3e020.png "スクリーンショット 2015-02-15 23.55.17.png")

# 「すぐに消せ」ごっこ

完成したソースがこれ。
上で説明していても不要なものは省いてます。
保存して`osascript -l JavaScript SHUT_YOU_DOWN.js`で実行してください。

念の為に書きますが **自己責任でお願いします。**
何か起きた場合に責任は持てません。何も起きませんが。

```js:SHUT_YOU_DOWN.js
MESSAGE = "すぐに消せ";

Terminal = Application("Terminal");
Terminal.activate();

Terminal.windows().forEach(function(win){
  var tab = win.selectedTab();
  
  tab.backgroundColor = {};
  tab.normalTextColor = {red:1, blue:0.1, green:0.1};
  tab.fontName = "PCMyungjo";
  
  Terminal.doScript("ruby -e 's=%|" + MESSAGE + "　|;v,h=`stty size`.split.map(&:to_i);puts [s.ljust(h/2,s)]*v'", {in:tab});
});
```

全てのウィンドウで実行するようにしてみました。
えぇ、肝心な部分はRubyです。ごめんなさい。
Rubyの部分は、画面と同じ幅の文字列を作成し、画面の行数分だけ表示してるだけなので気にしないでください。

実行前。
![スクリーンショット 2015-02-16 15.20.40.png](https://qiita-image-store.s3.amazonaws.com/0/18902/c4cc7743-5a27-c2e6-60dd-6c064c11b481.png)


実行後。
![スクリーンショット 2015-02-16 15.15.09.png](https://qiita-image-store.s3.amazonaws.com/0/18902/a5ecfc59-506d-5a27-9c89-fd584b40d604.png)

文字サイズいじらないほうがいい感じかもです。

**おわり。**

# 補足

冗長すぎる説明文は以下に隔離しました。

## Terminalに限らない豆知識

### .get() と .set()

プロパティへの代入文は`.set()`のシンタックスシュガーです。

```js:プロパティへの値の代入文は.set()を呼び出している
tab.fontName = "Impact";
tab.fontName.set("Impact"); //上に同じ
```
プロパティ名に`()`をつけて値を取り出す構文は`.get()`を呼び出しています。

```js:プロパティの値の取り出しは.get()を呼び出している
fontName = tab.fontName();
fontName = tab.fontName.get(); //上に同じ
```


### .properties()
JXAのだいたいのオブジェクトは`.properties()`でプロパティを取得できます。(たぶん)
困ったら用語説明と一緒に見てみましょう。

`JSON.stringity`と一緒に使うと見やすいです。
3つ目の引数にスペースを渡せばインデントして表示できます。

```js:タブのプロパティ一覧を表示
var props = Terminal.windows[0].selectedTab.properties();

console.log(JSON.stringify(props, null, "  "));
```

```js:表示例
{
  "fontName": "Monaco",
  "titleDisplaysDeviceName": false,
  "cursorColor": {
    "red": 0.3024185597896576,
    "green": 0.3024185597896576,
    "blue": 0.3024185597896576
  },
  "titleDisplaysShellPath": false,
  "tty": "/dev/ttys000",
  "normalTextColor": {
    "red": 0.9475700259208679,
    "green": 0.9475700259208679,
    "blue": 0.9475700259208679
  },
  "titleDisplaysWindowSize": true,
  "titleDisplaysCustomTitle": true,
  "contents": "XXXX", //見せられないよ
  "numberOfRows": 24,
  "processes": [
    "login",
    "bash",
    "osascript"
  ],
  "cleanCommands": [
    "screen",
    "tmux"
  ],
  "fontAntialiasing": true,
  "backgroundColor": {
    "red": 0,
    "green": 0,
    "blue": 0
  },
  "customTitle": "ターミナル",
  "pcls": "tab",
  "titleDisplaysFileName": false,
  "history": "XXXX", //見せられないよ
  "selected": true,
  "fontSize": 10,
  "boldTextColor": {
    "red": 1,
    "green": 1,
    "blue": 1
  },
  "busy": true,
  "numberOfColumns": 80
}
```

ふつうのJavaScriptみたいにプロパティを得ようとしても無駄です。

```js:普通にはプロパティ取得できない
Object.keys(Terminal); //=> []
Object.getOwnPropertyNames(Terminal) //=> ["__private__"]

Terminal.hasOwnProperty("a"); //=> true
Terminal.hasOwnProperty("ab"); //=> true
Terminal.hasOwnProperty("abc"); //=> true //なわけないじゃん
```

## Terminalの表示設定

今回はタブ自体の表示設定をいじりました。

用語説明を見ると、この方法は **非推奨(deprecated)** になっています。
「タブから`.currentSettings()`を参照してそっちいじれ」とのこと。

`.currentSettings()`を呼ぶと、そのタブが使用しているプロファイルを取得できます。
これを変更した場合は一時的な変更でなく、その後ずっとその設定のままになります。

なので、今回のようなイタズラをしたいだけの場合は新たにプロファイルを作成し、終了時に削除する必要があります。

今回はイタズラ程度でそんなめんどくさい処理を含めるのはごめんなので省きました。
強制終了させられたらプロファイル一覧にゴミが残ってしまいますし。殺されそう。

非推奨と言っても、将来的に削除されるかもしれないというだけだと思います。
非推奨な方法は嫌な方は以下の補足をどうぞ。

## SettingsSet (プロファイル)

ターミナルのプロファイルは`SettingsSet`というクラスになっています。

プロファイルを知らない方はターミナルの環境設定を見てください。
色やフォントの他、動作の設定が出来ます。
スクリプトから変更できるのはこれらの設定の一部です。

この`SettingsSet`を取得・作成・編集・削除する方法を書きます。

### 既存プロファイルの取得

Terminalから直接プロファイルを得る方法は以下の3つです。

```js:既存プロファイルの取得
var defaultProfile = Terminal.defaultSettings(); //デフォルトのプロファイル
var startupProfile = Terminal.startupSettings(); //起動時に一回だけ使用するプロファイル

var allProfiles = Terminal.settingsSets(); //全てのプロファイルの配列
```

`.startupProfile`は環境設定を見てもらえれば一発で分かりますが、起動後の一回のみ使うプロファイルです。
大体の人は`.defaultProfile`と一緒だと思います。
デフォルトのプロファイルを変更した場合、一緒に変更されるので。


または、タブからも取得できます。

```js:タブのプロファイルを取得
var tab = Terminal.windows[0].selectedTab();

var profile = tab.currentSettings();
```

プロファイルを変更するには、逆にタブのcurrentSettingsに代入します。

```js:プロファイルを適用
tab.currentSettings = profile;
```

### 新規プロファイルの作成

新しくプロファイルを作成するには以下のようにします。

```js:新規プロファイルの作成
var newProfile = Terminal.SettingsSet().make();

newProfile.name = "ぷろふぁいる";
```

プロパティを渡して作成することもできます。
この方法が一番好きです。

```js:上に同じ
var newProfile = Terminal.SettingsSet({name:"ぷろふぁいる"}).make();
```

その他の方法。

既存のプロファイルに`.push()`で追加すると新規作成されます。
`.push()`はインデックスを返すので、それを使ってプロファイルを取得します。

```js:上に同じ
var klass = Terminal.SettingsSet();
var index = Terminal.settingsSets.push(klass);
var newProfile = Terminal.settingsSets[index];

newProfile.name = "ぷろふぁいるy";
```
最初から名前をつけておく場合。

```js:上に同じ
var klass = Terminal.SettingsSet({name:"ぷろふぁいるx"});
Terminal.settingsSets.push(klass);

var newProfile = Terminal.settingsSets["ぷろふぁいるx"];
```

※以下のように`Terminal#make()`にプロパティを渡すことでも出来るそうなのですが、方法が分かりませんでした。
方法が分かる方、教えてください。

```js:fail
profile = Terminal.make({new:Terminal.SettingsSet, withProperties:{name:"ぷろふぁいる"}});
// !! Error on line 1: Error: Can't convert types.
```

### プロファイルの編集

今までやってきたように、プロパティに値を代入するだけです。

```js:プロファイルを編集
profile.name = "お前は知りすぎた";

profile.backgroundColor = {red:0.523307};
```

以下、`SettingsSet`の用語説明から引用。
idのみ読み取り専用(r/o)なので変更できません。

>id (integer, r/o) : The unique identifier of the settings set.
name (text) : The name of the settings set.
numberOfRows (integer) : The number of rows displayed in the tab.
numberOfColumns (integer) : The number of columns displayed in the tab.
cursorColor (Color) : The cursor color for the tab.
backgroundColor (Color) : The background color for the tab.
normalTextColor (Color) : The normal text color for the tab.
boldTextColor (Color) : The bold text color for the tab.
fontName (text) : The name of the font used to display the tab’s contents.
fontSize (integer) : The size of the font used to display the tab’s contents.
fontAntialiasing (boolean) : Whether the font used to display the tab’s contents is antialiased.
cleanCommands (list of text) : The processes which will be ignored when checking whether a tab can be closed without showing a prompt.
titleDisplaysDeviceName (boolean) : Whether the title contains the device name.
titleDisplaysShellPath (boolean) : Whether the title contains the shell path.
titleDisplaysWindowSize (boolean) : Whether the title contains the tab’s size, in rows and columns.
titleDisplaysSettingsName (boolean) : Whether the title contains the settings name.
titleDisplaysCustomTitle (boolean) : Whether the title contains a custom title.
customTitle (text) : The tab’s custom title.

上のほうでも書きましたが、Colorにはアルファ値が設定できません。
環境設定からはアルファ値を設定することができるので、半透明にしたい方はJXAを使わずに変更してください。

### プロファイルの削除

単純に`.delete()`を呼びます。
**取り返しがつかないので注意してください。**
怖かったら、普通にTerminalの環境設定を開いて消せばいいだけです。

```js:プロファイルの削除
profile.delete();
```