Goでターミナルのサイズに合わせて枠を表示し続ける

## やったこと
リッチなコマンドラインツールを作るとなると常に画面に情報を出力し続けたり、ターミナルの大きさに合わせて表示も変化させたいものです。今回は以下の内容を実装しました。

 - ターミナルの大きさに合わせて枠を表示する(`|`や`-`, `+` を使用)
 - ターミナルの大きさを変更しても自動で枠が適切な(画面に沿った)大きさで表示される

以下イメージ(一番下の行はコマンドを入力するためのスペースとして使えるように敢えて残しています)
![イメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157975/c1dbe799-37b9-8ec5-60c7-7e324c452895.png)

まずは完成形から

```golang
package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"

	"golang.org/x/crypto/ssh/terminal"
)

func GetWindowSize(fd int) *Size {
	ws := &Window{}
	var err error
	ws.Row, ws.Column, err = terminal.GetSize(fd)
	if err != nil {
		fmt.Printf("get window sieze error: %v", err)
		os.Exit(1)
	}
	return &ws.Size
}

type Size struct {
	Row    int
	Column int
}

type Window struct {
	Size
}

func main() {
	signalChan := make(chan os.Signal, 1)
	// catch SIGINT(Ctrl+C), KILL signal, and window size changes
	signal.Notify(
		signalChan,
		syscall.SIGINT,
		syscall.SIGTERM,
		syscall.SIGWINCH,
	)
	ws := GetWindowSize(syscall.Stdin)

	makeWindow(ws.Row, ws.Column)

	exitChan := make(chan int)
	go func() {
		for {
			s := <-signalChan
			switch s {
			// SIGINT(Ctrl+C)
			case syscall.SIGINT:
				exitChan <- 130

			// kILL signal
			case syscall.SIGTERM:
				exitChan <- 143

			case syscall.SIGWINCH:
				ws := GetWindowSize(syscall.Stdin)
				makeWindow(ws.Row, ws.Column)

			default:
				exitChan <- 1
			}
		}
	}()

	code := <-exitChan
	os.Exit(code)
}

func makeWindow(row int, column int) {
	fmt.Print("\033[H\033[2J")
	printFilledLine(row, '+', '-')
	for i := 1; i < (column-1)-1; i++ {
		printFilledLine(row, '|', ' ')
	}
	printFilledLine(row, '+', '-')
}

func printFilledLine(row int, edgeChar byte, fillChar byte) {
	l := make([]byte, row)
	l[0] = edgeChar
	for i := 1; i < (row - 1); i++ {
		l[i] = fillChar
	}
	l[row-1] = edgeChar
	fmt.Println(string(l))
}
```

こちらの資料やコードが元になっていますので併せて見ていただけると参考になるでしょう。
[Golangにおける端末制御 リッチなターミナルUIの実現方法](https://www.slideshare.net/c-bata/golang-ui)
https://github.com/c-bata/go-prompt/blob/master/_tools/sigwinch/main.go

### 1. ターミナルのサイズの取得
ターミナルの大きさに合わせて枠を表示するためにはターミナルの大きさを知る必要があります。`golang.org/x/crypto/ssh/terminal`の`GetSize`にファイルディスクリプタの数値を入れることにより、その画面の大きさを取得することができます。
標準入力(Stdin: 0),　標準出力(Stdout: 1), 標準エラー出力(Stderr: 2)の値については`syscall`パッケージに定義されているのでその値を使うと良いでしょう。

```go
func GetWindowSize(fd int) *Size {
    ws := &Window{}
    var err error
    ws.Row, ws.Column, err = terminal.GetSize(fd)
    if err != nil {
        fmt.Printf("get window sieze error: %v", err)
        os.Exit(1)
    }
    return &ws.Size
}
```

ただし、以下のようにパイプを使用した場合は`terminal.GetSize(fd)`が`-1, -1, err`を返す点には注意が必要です。

```shell
> ls -l | go run main.go
get window sieze error: inappropriate ioctl for deviceexit status 1
```

### 2. ターミナルの画面サイズ変化の検知
ターミナルの画面サイズを取得することには成功しました。しかし常に画面のサイズに沿った枠線を表示するには画面サイズが変化したことを検知する必要があります。

実際の処理はmain関数の部分が該当しますので順に説明します。

[signal.Notify](https://golang.org/pkg/os/signal/#Notify) は第一引数にチャネルを渡すことにより、後の引数で指定してシグナルの着信をチャネルに通知します。  
本命は [SIGWINCH](https://ja.wikipedia.org/wiki/%E3%82%B7%E3%82%B0%E3%83%8A%E3%83%AB_(Unix))で画面サイズの変更を通知します。

```go
func main() {
    signalChan := make(chan os.Signal, 1)
    // catch SIGINT(Ctrl+C), KILL signal, and window size changes
    signal.Notify(
        signalChan,
        syscall.SIGINT,
        syscall.SIGTERM,
        syscall.SIGWINCH,
    )

    ・・・(略)・・・
```

### 3. 画面サイズ変更に伴う枠線の描画
最初にプログラムの終了のタイミングを制御するためのチャネルを作成します。ここでチャネルから送られる値が終了時のステータスコードになるわけですが、
`code := <-exitChan` でチャネルの待受をしていないと処理が終了してしまうので、このチャネルにはプログラムがすぐに終了してしまうことを防ぐ意味合いもあります。

`SIGWINCH`を受信した際には画面サイズの取得と画面サイズに沿った枠線の描画を行い次の通知を待つようにしています(Ctrl+Cやkillされた際には処理を終了させます)。

`makeWindow`と`printFilledLine`は文字列の整形くらいのことしかしていないので細かい説明は割愛しますが、`fmt.Print("\033[H\033[2J")`を出力することによりループごとに画面のクリアとカーソルの左上への移動を実現しています、詳しくはエスケープシーケンスについて調べてみると良いでしょう。

```go
	exitChan := make(chan int)
	go func() {
		for {
			s := <-signalChan
			switch s {
			// SIGINT(Ctrl+C)
			case syscall.SIGINT:
				exitChan <- 130

			// kILL signal
			case syscall.SIGTERM:
				exitChan <- 143

			case syscall.SIGWINCH:
				ws := GetWindowSize(syscall.Stdin)
				makeWindow(ws.Row, ws.Column)

			default:
				exitChan <- 1
			}
		}
	}()

	code := <-exitChan
	os.Exit(code)
}

```

### 4. 終わりに
今回は簡単なことしかしていませんがGoの知識に合わせてシステムコール、ファイルディスクリプタ、エスケープシーケンスについて調べるとよりリッチなコマンドラインツールの作成の一助になるでしょう。