[Go言語]ターミナルの入力をリアルタイムに取得する方法


<a id="はじめに"></a>

## はじめに
本記事では、Go言語を使用してターミナルの入力をリアルタイムで取得する方法をご紹介します。
今回ご紹介するのは、UNIX系OSに限った話です。
Windowsの場合は、Microsoftが用意してるAPIで実現できるらしいが......(調査不足)

<a id="目次"></a>

## 目次
<!-- TOC -->

- [はじめに](#はじめに)
- [目次](#目次)
- [結論](#結論)
- [動作環境](#動作環境)
- [カノニカルモードとは](#カノニカルモードとは)
- [非カノニカルモードとは](#非カノニカルモードとは)
    - [サンプルコード](#サンプルコード)
- [Rawモードとは](#rawモードとは)
    - [サンプルコード](#サンプルコード-1)
- [参考サイト](#参考サイト)
- [おわりに](#おわりに)

<!-- /TOC -->

<a id="結論"></a>

## 結論
ターミナルを非カノニカルモードにすること、即時に入力を取得可能です。
非カノニカルモードにするには、termios構造体のフィールドの値を変更しなければなりません。
以下では、サンプルコードとLinuxのマニュアルを交えて、それぞれのモードを説明します。

<a id="動作環境"></a>

## 動作環境
- macOS Catalina
- CentOS 7
- go version go1.12.6

<a id="カノニカルモードとは"></a>

## カノニカルモードとは
非カノニカルモードを説明する前に、まずカノニカルモードについてです。
わかりやすく言えば、デフォルトの状態ですね。Enterが押下されるまで入力を受け付けます。

> 引用 [Linuxの日本語翻訳マニュアル](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/termios.3.html)
>
> - 入力は行単位に行われる。 行区切り文字が打ち込まれた時点で、入力行が利用可能となる。行区切り文字は NL, EOL, EOL2 および行頭での EOF である。 EOF 以外の場合、 read(2) が返すバッファーに行区切り文字も含められる
>
> - 行編集が有効となる (ERASE, KILL が効果を持つ。 IEXTEN フラグが設定された場合は、 WERASE, REPRINT, LNEXT も効果を持つ)。 read(2) は最大でも 1行の入力しか返さない。 read(2) が要求したバイト数が現在の入力行のバイト数よりも少ない場合、 要求したのと同じバイト数だけが読み込まれ、 残りの文字は次回の read(2) で読み込まれる


<a id="非カノニカルモードとは"></a>

## 非カノニカルモードとは
非カノニカルモード、リアルタイムに入力を受け付け可能なのがこちらのモードです。
MIN (c_cc[VMIN])で受付文字数を設定できます。
例えば、c_cc[VMIN] = 3とした場合、3文字入力した時点で、入力完了となりプログラムに読み込まれます。
TIME (c_cc[VTIME])でタイムアウト時間を設定でき、タイムアウトを必要としない場合は0にします。

> 引用 [Linuxの日本語翻訳マニュアル](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/termios.3.html)
>
> - 非カノニカルモードでは、入力は即座に利用可能となり (ユーザーは行区切り文字を打ち込む必要はない)、入力処理は実行されず、行編集は無効となる。 MIN (c_cc[VMIN]) と TIME (c_cc[VTIME]) の設定により、 read(2) が完了する条件が決定される

<a id="サンプルコード"></a>

### サンプルコード
C言語の場合、termios構造体にアクセスする際には、「termios.h」の関数群を使用します。
これをGo言語で同じように使用できるよう、有志が作成した[pkg/termパッケージ](https://github.com/pkg/term)をお借りします。 
termios構造体のパラメータを変更するときは、Tcgetattr()で現在の設定を取得してから、ビット演算などで必要な設定をしていきます。
設定できるパラメータは、[Linuxの日本語翻訳マニュアル](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/termios.3.html)を参考にしてください。
ちなみに、C言語で使用できるフラグは、基本的にはpkg/termパッケージでも使用できました。
必要な設定が完了したら、Tcsetattr()で反映できます。

```go:sample.go
package main

import (
	"fmt"
	"syscall"

	"github.com/pkg/term/termios"
)

func main() {
	var ttystate syscall.Termios

	// ターミナルの設定を取得
	termios.Tcgetattr(uintptr(syscall.Stdin), &ttystate)

    // ターミナルの設定変更
    setNonCanonicalMode(&ttystate)

    // 標準入力を取得
	bufCh := make(chan []byte, 1)
	go readBuffer(bufCh)

	for {
		fmt.Printf(" あなたが入力したのは: %c\n", <-bufCh)
	}
}

// 非カノニカルモードに設定する
func setNonCanonicalMode(attr *syscall.Termios) {

	// カノニカルモードを無効に設定 （&^ AND NOT)
	attr.Lflag &^= syscall.ICANON

	// 読み込み時の最小文字数 = 1文字
	attr.Cc[syscall.VMIN] = 1

	//非カノニカル読み込み時のタイムアウト時間 = 0
	attr.Cc[syscall.VTIME] = 0

	// 変更した設定を反映
	termios.Tcsetattr(uintptr(syscall.Stdin), termios.TCSANOW, attr)
}

// バッファの値を取得する
func readBuffer(bufCh chan []byte) {
	buf := make([]byte, 1024)

	for {
		if n, err := syscall.Read(syscall.Stdin, buf); err == nil {
			bufCh <- buf[:n]
		}
	}
}
```

上記コードを実行すると、以下のような挙動になります。
入力したキーが画面に表示された後に、Printfが実行されます。
ちなみにVMINの数値を増やせば、複数の値を取得可能です。

![Demo01](https://grimoire.f5.si/wp-content/uploads/2020/05/sample_demo01.gif?raw=true)

<a id="Rawモードとは"></a>

## Rawモードとは
非カノニカルモードの特徴に加えて、入力したキーが画面に表示されません。
また特殊処理が効かないので、Ctrl + C で強制終了ができなくなります。

> 引用 [Linuxの日本語翻訳マニュアル](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/termios.3.html)
>
>- 入力は文字単位に可能であり、エコーが無効となり、 端末の入出力文字に対する特殊処理はすべて無効となる

<a id="サンプルコード-1"></a>

### サンプルコード
[Linuxの日本語翻訳マニュアル](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/termios.3.html)に示す通りにRawモードに設定したパターンです。(※ 不必要だと判断した箇所はコメントアウトしてます。)   
下記コードのように何も考えずにマニュアル通りに設定してもいいが、必要に応じて設定パラメータを選択した方がいい気がしますね。

```go:sample.go
package main

import (
	"fmt"
	"syscall"

	"github.com/pkg/term/termios"
)

func main() {
	var ttystate syscall.Termios

	// ターミナルの設定を取得
	termios.Tcgetattr(uintptr(syscall.Stdin), &ttystate)

	// ターミナルの設定変更　※ここを変更
	//setNonCanonicalMode(&ttystate)
	setRawMode(&ttystate)

	bufCh := make(chan []byte, 1)
	go readBuffer(bufCh)

	for {
		fmt.Printf(" あなたが入力したのは: %c\n", <-bufCh)
	}
}

// Rawモードに設定する
func setRawMode(attr *syscall.Termios) {

	// 設定値は、[https://linuxjm.osdn.jp/html/LDP_man-pages/man3/termios.3.html]から引用
	// OPOSTをビットクリアすると、自分の環境では出力がおかしくなるのでコメントアウト
	// ISIGを無効にすると、Ctrl+Cが押せなくてデバッグがしづらいのでコメントアウト
	// マニュアルでは、文字サイズをCS8に指定しているが、今回の検証では不必要と判断してコメントアウト

    attr.Iflag &^= syscall.BRKINT | syscall.ICRNL | syscall.INPCK | syscall.ISTRIP | syscall.IXON
  
	//attr.Oflag &^= syscall.OPOST
  
    attr.Cflag &^= syscall.CSIZE | syscall.PARENB
  
    //attr.Cflag |= syscall.CS8
  
    attr.Lflag &^= syscall.ECHO | syscall.ICANON | syscall.IEXTEN //| syscall.ISIG
  
    attr.Cc[syscall.VMIN] = 1
	attr.Cc[syscall.VTIME] = 0

	// 変更した設定を反映
	termios.Tcsetattr(uintptr(syscall.Stdin), termios.TCSANOW, attr)
}

// バッファの値を取得する
func readBuffer(bufCh chan []byte) {
	// 省略
}
```

![Demo02](https://grimoire.f5.si/wp-content/uploads/2020/05/sample_demo02.gif?raw=true)
上記の非カノニカルモードと違って、押下したキーが画面に表示されないですね。

<a id="参考サイト"></a>

## 参考サイト

https://linuxjm.osdn.jp/html/LDP_man-pages/man3/termios.3.html
https://www.slideshare.net/c-bata/golang-ui
https://grimoire.f5.si/archives/125

<a id="おわりに"></a>

## おわりに
termiosについてC言語で扱う情報は、たくさんあるのですが、Go言語でやってる事例が少なかったです。
[ジョークコマンド](https://github.com/yuki9431/ihacker)を作成するために調べ始めたのですが、思ったより時間を取られてしまいました。
未来の自分のため、そして同じように詰まってる人のために記事にまとめました。
普段意識しない低レイヤーに触れることができて、面白かったですね。