pkg/display/graph/tree/tree.go (173 lines of code) (raw):

// Licensed to Apache Software Foundation (ASF) under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Apache Software Foundation (ASF) licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package tree import ( "fmt" "strings" ui "github.com/gizak/termui/v3" "github.com/gizak/termui/v3/widgets" "github.com/apache/skywalking-cli/pkg/logger" ) type Node struct { Children []*Node Detail string Value fmt.Stringer } var extra = make(map[*widgets.TreeNode]*Node) func Display(roots []*Node, serviceNames []string) error { if err := ui.Init(); err != nil { logger.Log.Fatalf("failed to initialize termui: %v", err) } defer ui.Close() nodes := make([]*widgets.TreeNode, len(roots)) for i := range nodes { nodes[i] = &widgets.TreeNode{} } for i, root := range roots { adapt(root, nodes[i]) } tree := widgets.NewTree() tree.TextStyle = ui.Style{ Fg: ui.ColorWhite, Bg: ui.ColorClear, Modifier: 0, } tree.SelectedRowStyle = ui.Style{ Fg: ui.ColorBlack, Bg: ui.ColorWhite, Modifier: ui.ModifierBold, } tree.WrapText = false tree.SetNodes(nodes) tree.Title = fmt.Sprintf("[ %s ] [%s]", strings.Join(serviceNames, "->"), " Press ? to show help ") tree.TitleStyle.Modifier = ui.ModifierBold tree.TitleStyle.Fg = ui.ColorRed x, y := ui.TerminalDimensions() tree.SetRect(0, 0, x, y) detail := widgets.NewParagraph() detail.Title = Detail detail.WrapText = false detail.SetRect(x, 0, x, y) help := widgets.NewParagraph() help.WrapText = false help.SetRect(x, 0, x, y) help.Title = KeyMap help.Text = ` [? ](fg:red,mod:bold) Toggle this help [k ](fg:red,mod:bold) Scroll Up [<Up> ](fg:red,mod:bold) Scroll Up [j ](fg:red,mod:bold) Scroll Down [<Down> ](fg:red,mod:bold) Scroll Down [<Ctr-b> ](fg:red,mod:bold) Scroll Page Up [<Ctr-u> ](fg:red,mod:bold) Scroll Half Page Up [<Ctr-f> ](fg:red,mod:bold) Scroll Page Down [<Ctr-d> ](fg:red,mod:bold) Scroll Half Page Down [<Home> ](fg:red,mod:bold) Scroll to Top [gg ](fg:red,mod:bold) Scroll to Top [<Enter> ](fg:red,mod:bold) Show Trace Detail [<Space> ](fg:red,mod:bold) Show Trace Detail [o ](fg:red,mod:bold) Toggle Expand [G ](fg:red,mod:bold) Scroll to Bottom [<End> ](fg:red,mod:bold) Scroll to Bottom [E ](fg:red,mod:bold) Expand All [C ](fg:red,mod:bold) Collapse All [q ](fg:red,mod:bold) Quit [<Ctr-c> ](fg:red,mod:bold) Quit ` ui.Render(tree, detail, help) listenKeyboard(tree, detail, help) return nil } func adapt(n1 *Node, n2 *widgets.TreeNode) { if n1 == nil || n2 == nil { return } n2.Expanded = true n2.Value = n1.Value n2.Nodes = []*widgets.TreeNode{} extra[n2] = n1 for _, child := range n1.Children { node := &widgets.TreeNode{} n2.Nodes = append(n2.Nodes, node) adapt(child, node) } } func actions(key string, tree *widgets.Tree) func() { // mostly vim style actions := map[string]func(){ "k": tree.ScrollUp, "<Up>": tree.ScrollUp, "j": tree.ScrollDown, "<Down>": tree.ScrollDown, "<C-b>": tree.ScrollPageUp, "<C-u>": tree.ScrollHalfPageUp, "<C-f>": tree.ScrollPageDown, "<C-d>": tree.ScrollHalfPageDown, "<Home>": tree.ScrollTop, "o": tree.ToggleExpand, "G": tree.ScrollBottom, "<End>": tree.ScrollBottom, "E": tree.ExpandAll, "C": tree.CollapseAll, "<Resize>": func() { x, y := ui.TerminalDimensions() tree.SetRect(0, 0, x, y) }, } return actions[key] } func listenKeyboard(tree *widgets.Tree, detail, help *widgets.Paragraph) { var previousKey string var previousSelected *Node visibilities := make(map[interface{}]bool) uiEvents := ui.PollEvents() for { e := <-uiEvents switch e.ID { case "q", Quit: return case "g": if previousKey == "g" { tree.ScrollTop() } case "<Enter>", "<Space>": selected := extra[tree.SelectedNode()] detail.Text = selected.Detail selectionChanged := previousSelected != selected visibilities[detail] = selectionChanged || !visibilities[detail] previousSelected = selected case "?": visibilities[help] = !visibilities[help] default: if action := actions(e.ID, tree); action != nil { action() } } if previousKey == "g" { previousKey = "" } else { previousKey = e.ID } redraw(visibilities, tree, detail, help) } } func redraw(shouldShow map[interface{}]bool, tree *widgets.Tree, detail, help *widgets.Paragraph) { x, y := ui.TerminalDimensions() shouldDisplaySideBar := shouldShow[detail] || shouldShow[help] if shouldDisplaySideBar { tree.SetRect(0, 0, x*2/3, y) } else { tree.SetRect(0, 0, x, y) } if shouldShow[detail] && shouldShow[help] { detail.SetRect(x*2/3, 0, x, y/2) help.SetRect(x*2/3, y/2+1, x, y) } else if shouldShow[help] { detail.SetRect(0, 0, 0, 0) help.SetRect(x*2/3, 0, x, y) } else if shouldShow[detail] { detail.SetRect(x*2/3, 0, x, y) help.SetRect(0, 0, 0, 0) } else { help.SetRect(0, 0, 0, 0) detail.SetRect(0, 0, 0, 0) } ui.Render(tree, detail, help) }