| // 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/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", cc: |
| 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) |
| } |