diff --git a/cmd/gosdn-tview/app/app.go b/cmd/gosdn-tview/app/app.go new file mode 100644 index 0000000000000000000000000000000000000000..e65af13c574abbc01551afc45e88323d4b36c2da --- /dev/null +++ b/cmd/gosdn-tview/app/app.go @@ -0,0 +1,50 @@ +package app + +import "github.com/rivo/tview" + +type view interface { + GetContent() tview.Primitive +} + +type App struct { + app *tview.Application + pages *tview.Pages +} + +func NewApp() *App { + a := &App{ + app: tview.NewApplication(), + } + return a +} + +func (a *App) SetRoot(v view) { + a.pages = v.GetContent().(*tview.Pages) + a.app.SetRoot(a.pages, true) +} + +func (a *App) SwitchPage(s string) { + if a.pages.HasPage(s) { + a.pages.SwitchToPage(s) + } +} + +func (a *App) AddPage(name string, p view) { + a.pages.AddPage(name, p.GetContent(), true, false) +} + +func (a *App) Run() error { + return a.app.Run() +} + +func (a *App) Stop() { + a.app.Stop() +} + +func (a *App) Draw() { + a.app.Draw() +} + +func (a *App) SetFocus(v tview.Primitive) { + a.app.SetFocus(v) +} diff --git a/cmd/gosdn-tview/grpc/commands.go b/cmd/gosdn-tview/grpc/commands.go new file mode 100644 index 0000000000000000000000000000000000000000..6eabf835326dcf0a08663bec21632fbc09384744 --- /dev/null +++ b/cmd/gosdn-tview/grpc/commands.go @@ -0,0 +1,125 @@ +package commands + +import ( + pb "code.fbi.h-da.de/cocsn/gosdn/api/proto" + "code.fbi.h-da.de/cocsn/gosdn/log" + "context" + grpc "google.golang.org/grpc" + "time" +) + +type commandType int + +const ( + GoSDN commandType = iota + Database +) + +const ( + defaultName = "gosdn-cli" +) + +type command struct { + Name string + Description string + //CommandType commandType + Function func(conn *grpc.ClientConn) string +} + +var CommandList = []command{ + {"hello", "test connection to goSDN controller", goSDNSayHello}, + {"shutdown", "request goSDN controller to shutdown", goSDNShutdown}, + {"testdb", "test all database connections", goSDNTestDB}, + {"tapigetedge", "get list of edges", TAPIGetEdge}, + {"tapigetedgenode", "get list of edgenodes", TAPIGetEdgeNode}, + {"tapigetlink", "get list of links", TAPIGetLink}, +} + +func Connect() (*grpc.ClientConn, error) { + address := "localhost:55055" + return grpc.Dial(address, grpc.WithInsecure(), grpc.WithTimeout(5*time.Second), grpc.WithBlock()) +} + +func goSDNSayHello(conn *grpc.ClientConn) string { + c := pb.NewGrpcCliClient(conn) + + // Contact the server and print out its response. + name := defaultName + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name}) + if err != nil { + log.Fatal(err) + } + return r.GetMessage() +} + +func goSDNShutdown(conn *grpc.ClientConn) string { + + c := pb.NewGrpcCliClient(conn) + + // Contact the server and print out its response. + name := defaultName + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + r, err := c.Shutdown(ctx, &pb.ShutdownRequest{Name: name}) + if err != nil { + log.Fatal(err) + } + return r.GetMessage() +} + +func goSDNTestDB(conn *grpc.ClientConn) string { + // TODO: fill with code and also see if grpc interface has this stub implemented. + return "not implemented yet" +} + +// TAPIGetEdge triggers the GetEdge function of the Ciena +// flavoured TAPI client +func TAPIGetEdge(conn *grpc.ClientConn) string { + + c := pb.NewGrpcCliClient(conn) + + // Contact the server and print out its response. + name := defaultName + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + r, err := c.TAPIGetEdge(ctx, &pb.TAPIRequest{Name: name}) + if err != nil { + log.Fatal(err) + } + return r.GetMessage() +} + +// TAPIGetEdgeNode triggers the GetEdgeNode function of the Ciena +// flavoured TAPI client +func TAPIGetEdgeNode(conn *grpc.ClientConn) string { + c := pb.NewGrpcCliClient(conn) + + // Contact the server and print out its response. + name := defaultName + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + r, err := c.TAPIGetEdgeNode(ctx, &pb.TAPIRequest{Name: name}) + if err != nil { + log.Fatal(err) + } + return r.GetMessage() +} + +// TAPIGetLink triggers the GetLink function of the Ciena +// flavoured TAPI client +func TAPIGetLink(conn *grpc.ClientConn) string { + + c := pb.NewGrpcCliClient(conn) + + // Contact the server and print out its response. + name := defaultName + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + r, err := c.TAPIGetLink(ctx, &pb.TAPIRequest{Name: name}) + if err != nil { + log.Fatal(err) + } + return r.GetMessage() +} diff --git a/cmd/gosdn-tview/main.go b/cmd/gosdn-tview/main.go new file mode 100644 index 0000000000000000000000000000000000000000..7f7368547959d5f92ebb922528b79d4214a2420b --- /dev/null +++ b/cmd/gosdn-tview/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "code.fbi.h-da.de/cocsn/gosdn/cmd/gosdn-tview/app" + grpc "code.fbi.h-da.de/cocsn/gosdn/cmd/gosdn-tview/grpc" + "code.fbi.h-da.de/cocsn/gosdn/cmd/gosdn-tview/views" + "code.fbi.h-da.de/cocsn/gosdn/log" +) + +func main() { + conn, err := grpc.Connect() + if err != nil { + log.Fatal(err) + } + + app := app.NewApp() + mainView := views.NewMainView(app, conn) + + app.SetRoot(mainView) + + app.Run() + defer app.Stop() +} diff --git a/cmd/gosdn-tview/views/addPNDView.go b/cmd/gosdn-tview/views/addPNDView.go new file mode 100644 index 0000000000000000000000000000000000000000..7fdf756110624cfc519a5be7596981c828e8a28c --- /dev/null +++ b/cmd/gosdn-tview/views/addPNDView.go @@ -0,0 +1,39 @@ +package views + +import ( + "code.fbi.h-da.de/cocsn/gosdn/cmd/gosdn-tview/app" + "github.com/rivo/tview" +) + +type AddPNDView struct { + addPNDView *tview.Form +} + +func NewAddPNDView(app *app.App) *AddPNDView { + pndv := &AddPNDView{ + addPNDView: tview.NewForm(), + } + pndv.addPNDView. + SetBorder(true). + SetTitle("Add new PND") + + pndv.addPNDView. + AddInputField("Name", "", 20, nil, nil). + AddInputField("description", "", 20, nil, nil). + AddDropDown("SI", []string{"Southbound 1", "Southbound 2", "Southbound 3", "Southbound 4"}, 0, nil). + AddButton("Send", func() { + //TODO: call grpc function here + }). + AddButton("Abort", func() { + app.SwitchPage("main") + }). + SetCancelFunc(func() { + app.SwitchPage("main") + }) + + return pndv +} + +func (pndv *AddPNDView) GetContent() tview.Primitive { + return pndv.addPNDView +} diff --git a/cmd/gosdn-tview/views/commandsListView.go b/cmd/gosdn-tview/views/commandsListView.go new file mode 100644 index 0000000000000000000000000000000000000000..2ab3f9452f436669a7f75c536a9775f68b29590a --- /dev/null +++ b/cmd/gosdn-tview/views/commandsListView.go @@ -0,0 +1,50 @@ +package views + +import ( + "code.fbi.h-da.de/cocsn/gosdn/cmd/gosdn-tview/app" + commands "code.fbi.h-da.de/cocsn/gosdn/cmd/gosdn-tview/grpc" + "github.com/rivo/tview" + "google.golang.org/grpc" +) + +type CommandListView struct { + commandsList *tview.List +} + +func NewCommandListView() *CommandListView { + cv := &CommandListView{ + commandsList: tview.NewList(), + } + cv.commandsList. + SetBorder(true). + SetTitle("Commands") + + return cv +} + +func (cv *CommandListView) GetContent() tview.Primitive { + return cv.commandsList +} + +func (cv *CommandListView) GetCommands(app *app.App, rv *ResultAndInputView, + conn *grpc.ClientConn) { + //TODO: create own command in grpc -> commands + cv.commandsList.AddItem("AddPND", "closes the application", '!', func() { + rv.ChangeContentView("addPND") + app.SetFocus(rv.GetContent()) + }) + + for i, command := range commands.CommandList { + f := command.Function + cv.commandsList. + AddItem(command.Name, command.Description, rune('a'+i), func() { + r := f(conn) + rv.SetContent(r) + rv.ChangeContentView("result") + }) + } + + cv.commandsList.AddItem("quit", "closes the application", 'q', func() { + app.Stop() + }) +} diff --git a/cmd/gosdn-tview/views/datbaseStatusView.go b/cmd/gosdn-tview/views/datbaseStatusView.go new file mode 100644 index 0000000000000000000000000000000000000000..2e9a268666a054fab89b5ddef1ed28dc6c65385f --- /dev/null +++ b/cmd/gosdn-tview/views/datbaseStatusView.go @@ -0,0 +1,43 @@ +package views + +import ( + "code.fbi.h-da.de/cocsn/gosdn/cmd/gosdn-tview/app" + "github.com/rivo/tview" + "time" +) + +type DatabaseStatusView struct { + databaseStatusView *tview.TextView +} + +func NewDatabaseStatusView(app *app.App) *DatabaseStatusView { + dv := &DatabaseStatusView{ + databaseStatusView: tview.NewTextView(), + } + dv.databaseStatusView. + SetDynamicColors(true). + SetTextAlign(tview.AlignCenter). + SetRegions(true). + SetBorder(true). + SetTitle("Database") + + go databaseTicker(dv) + + return dv +} + +func (dv *DatabaseStatusView) GetContent() tview.Primitive { + return dv.databaseStatusView +} + +func (dv *DatabaseStatusView) SetContent(s string) { + dv.databaseStatusView.Clear() + dv.databaseStatusView.SetText(s) +} + +func databaseTicker(dv *DatabaseStatusView) { + ticker := time.NewTicker(5 * time.Second) + for t := range ticker.C { + dv.SetContent(t.String()) + } +} diff --git a/cmd/gosdn-tview/views/footerView.go b/cmd/gosdn-tview/views/footerView.go new file mode 100644 index 0000000000000000000000000000000000000000..f998d7ba5e973a3233f0b00c1c2ff880697e531c --- /dev/null +++ b/cmd/gosdn-tview/views/footerView.go @@ -0,0 +1,35 @@ +package views + +import ( + "code.fbi.h-da.de/cocsn/gosdn/cmd/gosdn-tview/app" + "github.com/rivo/tview" + "google.golang.org/grpc" +) + +type FooterView struct { + footerView *tview.Flex + databaseStatusView *DatabaseStatusView + gRPCStatusView *GRPCStatusView +} + +func NewFooterView(app *app.App, conn *grpc.ClientConn) *FooterView { + + fw := &FooterView{ + footerView: tview.NewFlex(), + databaseStatusView: NewDatabaseStatusView(app), + gRPCStatusView: NewGRPCStatusView(app, conn), + } + fw.footerView. + SetBorder(true). + SetTitle("Status") + + fw.footerView. + AddItem(fw.gRPCStatusView.GetContent(), 0, 1, false). + AddItem(fw.databaseStatusView.GetContent(), 0, 1, false) + + return fw +} + +func (fw *FooterView) GetContent() tview.Primitive { + return fw.footerView +} diff --git a/cmd/gosdn-tview/views/gRPCStatusView.go b/cmd/gosdn-tview/views/gRPCStatusView.go new file mode 100644 index 0000000000000000000000000000000000000000..3e522db7f18237e8533970fdf29f98233b6aff0e --- /dev/null +++ b/cmd/gosdn-tview/views/gRPCStatusView.go @@ -0,0 +1,56 @@ +package views + +import ( + "code.fbi.h-da.de/cocsn/gosdn/cmd/gosdn-tview/app" + "github.com/rivo/tview" + "google.golang.org/grpc" + "time" +) + +type GRPCStatusView struct { + gRPCStatusView *tview.TextView +} + +func NewGRPCStatusView(app *app.App, conn *grpc.ClientConn) *GRPCStatusView { + sv := &GRPCStatusView{ + gRPCStatusView: tview.NewTextView(), + } + + sv.gRPCStatusView. + SetDynamicColors(true). + SetTextAlign(tview.AlignCenter). + SetRegions(true). + SetBorder(true). + SetTitle("gRPC") + + //TODO: maybe there is another way to do this. + // pretty ugly atm, since it re-draws every 5 seconds... + sv.gRPCStatusView.SetChangedFunc(func() { + app.Draw() + }) + + go gRPCTicker(sv, conn) + + return sv +} + +func (sv *GRPCStatusView) GetContent() tview.Primitive { + return sv.gRPCStatusView +} + +func (sv *GRPCStatusView) SetContent(s string) { + sv.gRPCStatusView.Clear() + sv.gRPCStatusView.SetText(s) +} + +func gRPCTicker(sv *GRPCStatusView, conn *grpc.ClientConn) { + //TODO: refactor -> get rid of hardcoded values + ticker := time.NewTicker(5 * time.Second) + for range ticker.C { + if str := conn.GetState().String(); str == "READY" || str == "IDLE" { + sv.SetContent("[green]" + "connected") + } else { + sv.SetContent("[red]" + "disconnected") + } + } +} diff --git a/cmd/gosdn-tview/views/headerView.go b/cmd/gosdn-tview/views/headerView.go new file mode 100644 index 0000000000000000000000000000000000000000..67034bb6492f01563321a8cd777323367689685b --- /dev/null +++ b/cmd/gosdn-tview/views/headerView.go @@ -0,0 +1,32 @@ +package views + +import "github.com/rivo/tview" + +var goSDNAscii = ` ____ ____ _ _ _ _ _ _ _ ____ _ _ _ + __ _ ___/ ___|| _ \| \ | | | | | | ___ ___| |__ ___ ___| |__ _ _| | ___ | _ \ __ _ _ __ _ __ ___ ___| |_ __ _ __| | |_ + / _ |/ _ \___ \| | | | \| | _____ | |_| |/ _ \ / __| '_ \/ __|/ __| '_ \| | | | |/ _ \ | | | |/ _ | '__| '_ _ \/ __| __/ _ |/ _ | __| +| (_| | (_) |__) | |_| | |\ | |_____| | _ | (_) | (__| | | \__ \ (__| | | | |_| | | __/ | |_| | (_| | | | | | | | \__ \ || (_| | (_| | |_ + \__ |\___/____/|____/|_| \_| |_| |_|\___/ \___|_| |_|___/\___|_| |_|\__,_|_|\___| |____/ \__,_|_| |_| |_| |_|___/\__\__,_|\__,_|\__| + |___/ ` + +type HeaderView struct { + headerFlex *tview.Flex + titleView *tview.TextView +} + +func NewHeaderView() *HeaderView { + //TODO: change to uses FlexBox if there is more to display in the header + hv := &HeaderView{ + titleView: tview.NewTextView(), + } + hv.titleView. + SetText(goSDNAscii). + SetTextAlign(tview.AlignCenter). + SetBorder(true) + + return hv +} + +func (hv *HeaderView) GetContent() tview.Primitive { + return hv.titleView +} diff --git a/cmd/gosdn-tview/views/mainView.go b/cmd/gosdn-tview/views/mainView.go new file mode 100644 index 0000000000000000000000000000000000000000..6c95c811cc408ee1f5e5fa480ba33e573d044555 --- /dev/null +++ b/cmd/gosdn-tview/views/mainView.go @@ -0,0 +1,45 @@ +package views + +import ( + "code.fbi.h-da.de/cocsn/gosdn/cmd/gosdn-tview/app" + "github.com/rivo/tview" + "google.golang.org/grpc" +) + +type MainView struct { + pages *tview.Pages + mainGrid *tview.Grid + commandsListView *CommandListView + resultAndInputView *ResultAndInputView + headerView *HeaderView + footerView *FooterView +} + +func NewMainView(app *app.App, conn *grpc.ClientConn) *MainView { + mv := &MainView{ + pages: tview.NewPages(), + mainGrid: tview.NewGrid(), + commandsListView: NewCommandListView(), + resultAndInputView: NewResultAndInputView(app), + headerView: NewHeaderView(), + footerView: NewFooterView(app, conn), + } + mv.commandsListView.GetCommands(app, mv.resultAndInputView, conn) + + mv.mainGrid. + SetRows(8, 0, 5). + SetColumns(40, 0). + AddItem(mv.headerView.GetContent(), 0, 0, 1, 2, 0, 0, false). + AddItem(mv.footerView.GetContent(), 2, 0, 1, 2, 0, 0, false) + + mv.mainGrid.AddItem(mv.commandsListView.GetContent(), 1, 0, 1, 1, 0, 0, true). + AddItem(mv.resultAndInputView.GetContent(), 1, 1, 1, 1, 0, 0, false) + + mv.pages.AddPage("main", mv.mainGrid, true, true) + + return mv +} + +func (mv *MainView) GetContent() tview.Primitive { + return mv.pages +} diff --git a/cmd/gosdn-tview/views/resultAndInputView.go b/cmd/gosdn-tview/views/resultAndInputView.go new file mode 100644 index 0000000000000000000000000000000000000000..99ae89654349c6347281c34c7b2149a6e4c64463 --- /dev/null +++ b/cmd/gosdn-tview/views/resultAndInputView.go @@ -0,0 +1,45 @@ +package views + +import ( + "code.fbi.h-da.de/cocsn/gosdn/cmd/gosdn-tview/app" + "github.com/rivo/tview" +) + +type ResultAndInputView struct { + pages *tview.Pages + resultView *tview.TextView + pndInputView *AddPNDView +} + +func NewResultAndInputView(app *app.App) *ResultAndInputView { + rv := &ResultAndInputView{ + pages: tview.NewPages(), + pndInputView: NewAddPNDView(app), + resultView: tview.NewTextView(), + } + rv.resultView. + SetDynamicColors(true). + SetRegions(true). + SetScrollable(true). + SetTitle("Result"). + SetBorder(true) + + rv.pages. + AddPage("result", rv.resultView, true, true). + AddPage("addPND", rv.pndInputView.GetContent(), true, false) + + return rv +} + +func (rv *ResultAndInputView) GetContent() tview.Primitive { + return rv.pages +} + +func (rv *ResultAndInputView) ChangeContentView(s string) { + rv.pages.SwitchToPage(s) +} + +func (rv *ResultAndInputView) SetContent(s string) { + rv.resultView.Clear() + rv.resultView.SetText(s) +} diff --git a/configs/gosdn.toml b/configs/gosdn.toml index 26d755b34702b3733efb841a7d0e3148e4d0a30c..c749257f4e595ddc1c2fbc7bdc1cce06742b3562 100644 --- a/configs/gosdn.toml +++ b/configs/gosdn.toml @@ -1,4 +1,7 @@ -#example gosdn.toml -CliSocket = "localhost:55055" -DatabaseSocket = "bolt://141.100.70.170:7687" -ConfigPath = "./configs/gosdn.toml" +# example config +CliSocket = "localhost:55055" +DatabaseSocket = "bolt://141.100.70.170:7687" +DatabaseUser = "" +DatabasePassword = "" +DatabaseCrypto = false +ConfigPath = "./configs/gosdn.toml" diff --git a/go.mod b/go.mod index 6dc61c2bc002bdf283ea14c6fadaa5d7794ae130..b55c84e7746a120ddb95e8d00d1776078cc7b6dc 100644 --- a/go.mod +++ b/go.mod @@ -11,8 +11,8 @@ require ( github.com/google/go-cmp v0.4.1 // indirect github.com/neo4j/neo4j-go-driver v1.8.3 github.com/onsi/ginkgo v1.13.0 // indirect + github.com/rivo/tview v0.0.0-20201018122409-d551c850a743 golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect - golang.org/x/sys v0.0.0-20200817155316-9781c653f443 // indirect google.golang.org/genproto v0.0.0-20200519141106-08726f379972 // indirect google.golang.org/grpc v1.29.1 google.golang.org/protobuf v1.23.0 diff --git a/go.sum b/go.sum index aae8fb048e6cffa8b8c77b59b035853f1417ab2c..86322fe5c669de0df7af85a81f4ccdbf4f888403 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,10 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591 h1:0WWUDZ1oxq7NxVyGo8M3KI5jbkiwNAdZFFzAdC68up4= +github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= @@ -171,6 +175,8 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -179,6 +185,9 @@ github.com/mailru/easyjson v0.7.1 h1:mdxE1MF9o53iCb2Ghj1VfWvh7ZOwHpnVG/xwXrV90U8 github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg= github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -208,6 +217,10 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rivo/tview v0.0.0-20201018122409-d551c850a743 h1:9BBjVJTRxuYBeCAv9DFH2hSzY0ujLx5sxMg5D3K/Xeg= +github.com/rivo/tview v0.0.0-20201018122409-d551c850a743/go.mod h1:t7mcA3nlK9dxD1DMoz/DQRMWFMkGBUj6rJBM5VNfLFA= +github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -282,18 +295,21 @@ golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200817155316-9781c653f443 h1:X18bCaipMcoJGm27Nv7zr4XYPKGUy92GtqboKC2Hxaw= -golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7 h1:XtNJkfEjb4zR3q20BBBcYUykVOEMgZeIUOpBPfNYgxg= +golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=