【1日目】graqt開発日誌 ~GolangでリッチなCLIを作る~

最近ブログ書いてないので、graqtの進捗を1mmでも進んだら書くことにする。

graqtについては、以下の記事を参考にされたし。 一言で言うと、バカでも使えるGolang用のWebアプリのボトルネックを調査するソフトウェアです。主にISUCONをターゲットにしています。

serinuntius.hatenablog.jp github.com

進捗

現在、CLI部分を作っていて、見た目だけ少し動くようになった。 まだ、ログ部分とつなぎこみはしていない。

別日だけど、sketchでワイヤー作ったりもした。

golangでリッチなCLIを作るには

jroimartin/gocuiというOSSがすごく便利です。

_exampleに参考になるコードが結構ある。 github.com

今日のコードを貼っておきます。 汚いのは自覚してるし、graqtに持っていくときは清書しますw

github.com

package main

import (
    "log"

    "github.com/jroimartin/gocui"
    "fmt"
    "text/tabwriter"
    "github.com/pkg/errors"
)

type RequestWidget struct {
    name    string
    x, y    int
    w, h    int
    counter int
    v       *gocui.View
    tw      *tabwriter.Writer
}

func NewRequestWidget(name string, x, y, w, h int) *RequestWidget {
    // add initialize
    return &RequestWidget{name: name, x: x, y: y, w: w, h: h, counter: 0}
}

func (w *RequestWidget) Layout(g *gocui.Gui) error {
    // _ => vgit
    v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+w.h)
    if err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = "Request Index"
        v.Highlight = true
        v.SelBgColor = gocui.ColorGreen
        v.SelFgColor = gocui.ColorBlack

        v.SetCursor(0, 1)

        if err := w.KeyBindings(g); err != nil {
            return errors.Wrap(err, "Failed to Set KeyBindings")
        }

        if _, err := g.SetCurrentView("request"); err != nil {
            log.Panicln(err)
        }

        w.InitTabWriter(v)

        if _, err := w.PrintHeader(); err != nil {
            return err
        }

        // TODO テスト用
        for i := 0; i < 10; i++ {
            fmt.Fprintln(w.tw, "\t300\tGET\t/hoge\t10\t0.3\t5\t3000\t3\t8\t9\t8\t300mb\t50mb\t30mb\t30000mb")
        }

        w.tw.Flush()
        fmt.Fprintln(w.v)
    }

    return nil
}

func (w *RequestWidget) InitTabWriter(v *gocui.View) error {
    w.v = v
    w.tw = tabwriter.NewWriter(v, 0, 8, 2, ' ', 0)
    return nil
}

func (w *RequestWidget) PrintHeader() (int, error) {
    return fmt.Fprintln(w.tw, "\tCount\tMethod\tPath\tMax\tMin\tAvg\tSum\tP1\tP50\tP99\tStddev\tMaxBody\tMinBody\tAvgBody\tSumBody")
}

func (w *RequestWidget) enter(g *gocui.Gui, v *gocui.View) error {
    w.v.Clear()
    w.PrintHeader()
    w.counter++
    for i := 0; i < w.counter; i++ {
        fmt.Fprintln(w.tw, "\taaaaaaaaaa\tbbb")
    }

    w.tw.Flush()
    fmt.Fprintln(w.v)

    return nil
}

func (w *RequestWidget) cursorUp(g *gocui.Gui, v *gocui.View) error {
    if v != nil {
        ox, oy := v.Origin()
        cx, cy := v.Cursor()

        // cy == 0 is header
        if cy == 1 {
            return nil
        }
        if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
            if err := v.SetOrigin(ox, oy-1); err != nil {
                return err
            }
        }
    }
    return nil
}

func (w *RequestWidget) cursorDown(g *gocui.Gui, v *gocui.View) error {
    if v != nil {
        cx, cy := v.Cursor()
        if err := v.SetCursor(cx, cy+1); err != nil {
            ox, oy := v.Origin()
            if err := v.SetOrigin(ox, oy+1); err != nil {
                return err
            }
        }
    }
    return nil
}

func (w *RequestWidget) Printf(format string, a ...interface{}) {
    fmt.Fprintf(w.tw, format, a)
    w.tw.Flush()
    fmt.Fprintln(w.v)
}

type HelpWidget struct {
    name string
    x, y int
    w, h int
    body string
}

func NewHelpWidget(name string, x, y, w, h int) *HelpWidget {
    // add initialize
    return &HelpWidget{name: name, x: x, y: y, w: w, h: h}
}

func (w *HelpWidget) Layout(g *gocui.Gui) error {
    // _ => v
    v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+w.h)
    if err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = "Help"
        fmt.Fprintf(v, "%d:%d:%d:%d", w.x, w.y, w.x+w.w, w.y+w.h)
    }
    return nil
}

type QueryWidget struct {
    name string
    x, y int
    w,

    h int
}

func NewQueryWidget(name string, x, y, w, h int) *QueryWidget {
    // add initialize
    return &QueryWidget{name: name, x: x, y: y, w: w, h: h}
}

func (w *QueryWidget) Layout(g *gocui.Gui) error {
    // _ => v
    v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+w.h)
    if err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = "Query Index"
    }
    return nil
}

type ParameterWidget struct {
    name string
    x, y int
    w, h int
}

func NewParameterWidget(name string, x, y, w, h int) *ParameterWidget {
    // add initialize
    return &ParameterWidget{name: name, x: x, y: y, w: w, h: h}
}

func (w *ParameterWidget) Layout(g *gocui.Gui) error {
    // _ => v
    v, err := g.SetView(w.name, w.x, w.y, w.x+w.w, w.y+w.h)
    if err != nil {
        if err != gocui.ErrUnknownView {
            return err
        }
        v.Title = "Query Parameter"
    }
    return nil
}

func quit(g *gocui.Gui, v *gocui.View) error {
    return gocui.ErrQuit
}

func (w *RequestWidget) KeyBindings(g *gocui.Gui) error {
    if err := g.SetKeybinding("request", gocui.KeyEnter, gocui.ModNone, w.enter); err != nil {
        return err
    }
    if err := g.SetKeybinding("request", gocui.KeyArrowDown, gocui.ModNone, w.cursorDown); err != nil {
        return err
    }
    if err := g.SetKeybinding("request", gocui.KeyArrowUp, gocui.ModNone, w.cursorUp); err != nil {
        return err
    }
    if err := g.SetKeybinding("request", gocui.KeyCtrlN, gocui.ModNone, w.cursorDown); err != nil {
        return err
    }
    if err := g.SetKeybinding("request", gocui.KeyCtrlP, gocui.ModNone, w.cursorUp); err != nil {
        return err
    }

    return nil
}

func main() {
    g, err := gocui.NewGui(gocui.OutputNormal)
    if err != nil {
        log.Panicln(err)
    }
    defer g.Close()

    g.Highlight = true
    g.SelFgColor = gocui.ColorBlue

    winX, winY := g.Size()
    fmt.Println(winX, winY)

    request := NewRequestWidget("request", 0, 0, winX-1, winY-3)
    help := NewHelpWidget("help", 0, winY-3, winX-1, 2)
    //query := NewQueryWidget("query", winX/2, 0, winX/2-1, winY-3)
    //parameter := NewParameterWidget("parameter", winX/2, winY/2, winX/2-1, winY/2-2)

    g.SetManager(help, request) //, query, parameter)

    if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
        log.Panicln(err)
    }

    if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
        log.Panicln(err)
    }
}