複数のコマンドを画面分割してwatchするwatchコマンド作った

# 概要

複数のコマンドを同時にwatchしたかったのでvhwatchというコマンドをGoで作った

https://github.com/jiro4989/vhwatch

# 使い方

```bash
vhwatch 'ls -1' 'echo test' 'date'
```

![sample1.png](https://qiita-image-store.s3.amazonaws.com/0/69665/9ae17243-2c72-a4e5-28b3-c3dde99cda6f.png)

```bash
vhwatch -c 1 'ls -1' 'echo test' 'date'
```

![sample2.png](https://qiita-image-store.s3.amazonaws.com/0/69665/a6514af0-0a75-af57-b2df-31c09a518a0e.png)

```bash
vhwatch -c 3 'echo 1' 'echo 2' 'echo 3' 'echo 4' 'echo 5' 'echo 6' 'echo 7'
```

![sample3.png](https://qiita-image-store.s3.amazonaws.com/0/69665/2db91b72-7edc-1d21-dd6e-106314592734.png)

# インストール

```bash
go get -u github.com/jiro4989/vhwatch
```

あるいは[GitHub Release](https://github.com/jiro4989/vhwatch/releases)からバイナリをダウンロード

# 実装

ロジックはわりと単純で、引数（実行させたいコマンド文字列）の数だけ
分割スクリーンの矩形構造体を生成して、逐次コマンドを実行してその結果を画面に描画しているだけです。
コマンド実行はそのうち並列実行にする予定です。

画面描画には https://github.com/nsf/termbox-go を使用しています。

```pane.go
func NewPanes(c, tw, th int, cmds []string) (ret Panes) {
	cmdLen := len(cmds)
	for i, cmd := range cmds {
		pw := paneWidth(c, tw)          // 1ペインあたりの幅
		ph := paneHeight(c, th, cmdLen) // 1ペインあたりの幅

		x := paneX(c, pw, i)
		y := paneY(c, ph, i)
		// ペインの下のペインが空いていたら縦幅を拡張
		h := ph
		if cmdLen-1 < i+c && (cmdLen-1)/c != i/c {
			h += ph
		}
		p := Pane{
			Name:    cmd,
			X:       x,
			Y:       y,
			Width:   pw,
			Height:  h,
			Command: cmd,
		}
		ret = append(ret, p)
	}
	return
}

func paneWidth(c, w int) int {
	return w / c
}

func paneHeight(c, h, cnt int) int {
	mod := cnt % c
	div := cnt / c
	if 0 < mod {
		div++
	}
	return h / div
}

func paneX(c, w, i int) int {
	return w * (i % c)
}

func paneY(c, h, i int) int {
	return h * (i / c)
}
```

コマンドラインオプションパーサーには https://github.com/spf13/cobra を使用しています。

```root.go
func init() {
	cobra.OnInitialize()
	RootCommand.Flags().IntP("col", "c", 2, "column count")
	RootCommand.Flags().IntP("interval", "n", 2, "seconds to wait between updates")
}

var RootCommand = &cobra.Command{
	Use:     "vhwatch",
	Short:   "vhwatch is Vertical/Horizontal Watch",
	Example: "vhwatch -c 3 'echo test' 'date' 'ls -1' 'ls -lah'",
	Version: Version,
	Long: `
vhwatch provides watching multiple commands execution.

Repository: https://github.com/jiro4989/vhwatch
    Author: jiro4989
	`,
	Run: func(cmd *cobra.Command, args []string) {
		if len(args) < 1 {
			cmd.Help()
			return
		}

		f := cmd.Flags()

		col, err := f.GetInt("col")
		if err != nil {
			panic(err)
		}

		interval, err := f.GetInt("interval")
		if err != nil {
			panic(err)
		}

		// termboxの初期化
		if err := termbox.Init(); err != nil {
			panic(err)
		}
		defer termbox.Close()
		termbox.SetInputMode(termbox.InputEsc)
		termbox.Flush()

		// 各ペイン毎にコマンドを定期実行
		go mainloop(col, args, time.Duration(interval))

		// Ctrl-Cで終了されるまで待機
		waitKeyInput()
	},
}

func mainloop(col int, args []string, interval time.Duration) {
	const fc = termbox.ColorDefault
	const bc = termbox.ColorDefault
	for {
		// 端末の幅を取得
		w, h := termbox.Size()
		// コマンドを描画するペインを取得
		panes := NewPanes(col, w, h, args)
		for _, p := range panes {
			var out []byte
			c, err := shellwords.Parse(p.Command)
			if err != nil {
				panic(err)
			}
			switch len(c) {
			case 0:
				// 空の文字列が渡された場合
				return
			case 1:
				// コマンドのみを渡された場合
				out, err = exec.Command(c[0]).Output()
			default:
				// コマンド+オプションを渡された場合
				// オプションは可変長でexec.Commandに渡す
				out, err = exec.Command(c[0], c[1:]...).Output()
			}
			if err != nil {
				panic(err)
			}
			p.DrawHeader()
			p.DrawText(out, Offset{Y: 1}, fc, bc)
		}
		termbox.Flush()
		time.Sleep(interval * time.Second)
		termbox.Clear(fc, bc)
	}
}

func waitKeyInput() {
	for {
		switch ev := termbox.PollEvent(); ev.Type {
		case termbox.EventKey:
			switch ev.Key {
			case termbox.KeyCtrlC, termbox.KeyCtrlD:
				return
			}
		}
	}
}
```

# 実装の問題点

## メモリ消費

普通に逐次実行してその結果をメモリに全部蓄えていて一気に描画してるので
コマンド実行結果が巨大だとメモリ消費がヤバそう。
この辺もうちょい省メモリに並列でいい感じにしたい。

## マルチバイト文字がずれる

~~`echo ああ`とかをvhwatchしたらずれた・・・~~
~~近いうちになおします。~~
直しました。
https://github.com/jiro4989/vhwatch/issues/1

# まとめ

わりと以前から自分が欲しかった機能なので、
ドッグフーディングしつつ改善していこうと思ってます。
(screenとかのコマンドをいちいち使うのは面倒）

もし使ってくださる方がいらっしゃって
バグとか機能要望ありましたらissuesまでお願いします:pray: