slコマンド作った (Nim版のを)

これを作りました。

![nmi_1](https://user-images.githubusercontent.com/13825004/83327472-1e57b200-a2b7-11ea-80f3-025d5eba2102.gif)
![nmi_2](https://user-images.githubusercontent.com/13825004/83327473-20ba0c00-a2b7-11ea-8fe9-a8f4e6862d01.gif)

# 概要

- Nimで作ったslコマンド(nmi)の話をします
- 仕組みの話をします

# slコマンドって？

slは、lsコマンドを実行しようとして誤ってslとタイプしたときに、機関車がターミナルを横切るアニメーションを表示するコマンドです。

slコマンドはジョーク系コマンドの筆頭です。

slコマンドはミスタイプした時に次ミスタイプしないようにと、あらゆるキー操作を受け付けず、アニメーションが終了するまで待機することを強制されるという~~悪質さ~~優しさを持っています。

# ソースコード

ソースコードは以下。

https://github.com/jiro4989/nmi

nimble packageにも登録ずみですので、Nimユーザであれば以下のコマンドでインストールできます。

```bash
nimble install nmi
```

# 仕組み

## アニメーション

Nimの標準パッケージ([terminal](https://nim-lang.org/docs/terminal.html))でターミナルの幅を取得し、アスキーアートの行の一部を徐々に表示するように幅を計算してスライドしているだけです。
アニメーションは本家のように車輪が回転したりはせず、1枚のアスキーアートを横にスライドするだけにしています。

以下の`traverse`プロシージャにアスキーアートの文字列を渡すと、横からスライドするようにアニメーションしながら出力します。
`color`は表示したアニメーションに色をつけたい時に指定します。

```nim
proc traverse(text: string, color: bool) =
  # ターミナルの幅を取得
  let width = terminalWidth() - 5
  let asciiHeight = text.split("\n").len
  let lines = text.split("\n")
  var i: int

  # --------------------------------------------------------------
  # 1. ロゴが右からぬーっと出てきて左端に到達するまでのアニメーション
  # --------------------------------------------------------------

  # 画面の横幅分、横切るようにアニメーションするため
  # 横幅回数分ループする
  for leftPadWidth in countdown(width, 0):
    var buf: seq[string]
    # ロゴのAAを行単位で出力
    for line in lines:
      if line.len < 1: continue
      # アニメーションの左側の空白文字を生成
      let leftPad = " ".repeat(leftPadWidth)
      if line.len <= i: i = line.len - 1
      # アニメーションで描画された回数分、ロゴの左側を取得
      let logoPart = line[0..i]
      # 左側の空白と、ロゴの描画された部分を結合
      buf.add(leftPad & logoPart)

    # 行単位ループで標準出力すると
    # アニメーションがちらつく可能性があるので
    # 一回のechoで出力する
    var t = buf.join("\n")
    if color:
      t = t.colorful
    echo t

    # アニメーションするためにカーソル位置を元の位置に戻す
    cursorUp(asciiHeight - 1)
    sleep 50
    inc i

  # --------------------------------------------------------------
  # 2. 左端に到達したロゴが徐々に左側に消えていくアニメーション
  # --------------------------------------------------------------

  # 1行目の文字数をロゴの横幅とみなす。
  # 徐々に消えていくので横幅分ループ。
  for x in 0..lines[0].len:
    var buf: seq[string]
    for line in lines:
      if line.len < 1: continue
      # ループ回数が、ターミナルの左外に隠れた幅になるので
      # ループ変数のインデックスから終端までを取得
      buf.add(line[x..^1])

    # 行単位ループで標準出力すると
    # アニメーションがちらつく可能性があるので
    # 一回のechoで出力する
    var t = buf.join("\n")
    if color:
      t = t.colorful
    echo t

    # アニメーションするためにカーソル位置を元の位置に戻す
    cursorUp(asciiHeight - 1)
    sleep 50
```

## キー入力の無効化

Nimの標準パッケージ([posix](https://nim-lang.org/docs/posix.html))で、Control+Cの入力があったとき(SIGINTシグナルが送信された時)に特定の処理を呼び出すプロシージャが提供されています。

これでCtrl+Cの入力時に「何も処理をしない」プロシージャを呼び出すように上書きすることで、本家の「キー入力無視」を実現しました。

```nim
  when not defined windows:
    from posix import onSignal, SIGTERM, SIGINT
    onSignal(SIGTERM, SIGINT):
      discard
```

posixモジュールはWindows環境では利用不可能なので、
windows以外の時だけCtrl+Cを無視するようにしています。
なのでWindowsの時はCtrl+Cで普通に停止します。

# まとめ

以下の内容について説明しました。

- slコマンドのこと
- nmiコマンドの作り
  - スライドアニメーションの実装
  - キー入力無視の方法

nimを誤ってnmiとタイプしてしまった時は、落ち着いてNimのロゴを眺めましょう。

# 余談

実は初期リリース時はキー入力を無視する機能は実装していませんでした。

本家slにキー入力を無視する機能が存在することは知っていたのですが、無くてもいいかな、と思い。

ですが、以下のissuesで熱い要望があり、これは実装せざるを得ないと思い至り、実装しました。

https://github.com/jiro4989/nmi/issues/1

> As we we all know, one of the most important features of sl - which is clearly the inspiration for nmi - is that it is not uninterruptible by the user.

> I think nmi should also ignore SIGTERM and SIGINT, turning it into the true torture it is ment to be. You mistype? You WAIT!

> 訳: nmiもSIGTERMとSIGINTを無視して、それを真の拷問に変えるべきだと思います。タイプミスした？待ちなさい！

遊びで作ったコマンドだったのですが、何がきっかけで発展するかわからないものですね。

以上です。