// go get github.com/rivo/tview@master package tui import ( "errors" "fmt" "strings" node "git.zzyxyz.com/zzy/goncdu/node" "git.zzyxyz.com/zzy/goncdu/utils" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) type UI struct { app *tview.Application main *tview.Frame msg string err string list *tview.List node *node.NodeManager nodes []*node.PathNode inputPath *tview.InputField } func NewUI(n *node.NodeManager) *UI { list := tview.NewList() list.ShowSecondaryText(false). SetTitleAlign(tview.AlignLeft). SetTitleColor(tcell.ColorWhite) frame := tview.NewFrame(list). SetBorders(0, 0, 0, 0, 1, 1) ui := &UI{ app: tview.NewApplication().SetRoot(frame, true), main: frame, msg: "", err: "", list: list, node: n, } ui.list.SetInputCapture( func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyRight: idx := ui.list.GetCurrentItem() if len(ui.nodes) == 0 { ui.err = errors.New("no children nodes").Error() break } name := ui.nodes[idx].GetPath() if err := ui.node.GoIn(name); err != nil { ui.err = err.Error() break } case tcell.KeyLeft: if err := ui.node.GoOut(); err != nil { ui.err = err.Error() } case tcell.KeyRune: switch event.Rune() { case 'q': ui.app.Stop() case '?': // show help ui.showHelp() case 'R': ui.app.SetRoot(ui.inputPath, true) case 'S': if err := ui.node.SaveToFile(); err != nil { ui.err = err.Error() } case 'L': if err := ui.node.LoadFromFile(); err != nil { ui.err = err.Error() } default: return event } default: return event } // END: ui.renderList() return nil }) return ui } func (ui *UI) showHelp() { helpText := []string{ "goncdu Help", "-------------------------", "Use the arrow keys to navigate:", " ↑ (up) : Move up", " ↓ (down) : Move down", " → (right) : Enter directory", " ← (left) : Go up one directory", "-------------------------", "Other commands:", " R : ReInput scan path", " f : Refresh cache", " q : Quit", " ? : Show this help message", "-------------------------", "Press q, Enter, Esc key to return", } // 调整字符串长度使其一致 maxLength := 0 for _, line := range helpText { if len(line) > maxLength { maxLength = len(line) } } var helpContent strings.Builder for _, line := range helpText { paddedLine := fmt.Sprintf("%-*s", maxLength, line) helpContent.WriteString(paddedLine) helpContent.WriteString("\n") } // 创建一个模态窗口并添加 TextView modal := tview.NewModal(). SetText(helpContent.String()) modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyEscape || event.Key() == tcell.KeyEnter || (event.Key() == tcell.KeyRune && event.Rune() == 'q') { ui.app.SetRoot(ui.main, true) } return event }) // 设置模态窗口为应用的根节点 ui.app.SetRoot(modal, false) } func (ui *UI) drawHeaderFooter() { curNode := ui.node.GetCurrentNode() ui.main.Clear(). AddText("goncdu test ~ Use the arrow keys to navigate, press ? for help", true, tview.AlignLeft, tcell.ColorWhite). AddText(fmt.Sprintf("--- %s", curNode.GetPath()), true, tview.AlignLeft, tcell.ColorWhite). AddText(fmt.Sprintf("Total Size = %s Items = %d --- Scaning time = %s", node.FormatSize(curNode.GetSize()), curNode.GetCount()-1, ui.node.GetScanTime()), false, tview.AlignLeft, tcell.ColorWhite). AddText(ui.err, false, tview.AlignLeft, tcell.ColorGrey) } func (ui *UI) drawList(items []string) { ui.list.Clear() for _, item := range items { ui.list.AddItem(item, "", 0, nil) } } func (ui *UI) run() { if err := ui.app.Run(); err != nil { panic(err) } } func (ui *UI) renderList() { icon_type := map[node.PathNodeType]string{ node.DIR: "📁", node.FILE: "📄", node.UNKNOWN: "❓", } nodes := ui.node.GetChildrenWithSorted() ui.nodes = nodes renderLine := func(entry *node.PathNode) string { return fmt.Sprintf("%s %10s %10s %s\n", icon_type[entry.GetType()], node.FormatSize(entry.GetSize()), utils.FormatProgressBar(entry.GetSize(), nodes[0].GetSize(), 10, []string{" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"}), entry.GetPath()) } str_list := make([]string, len(nodes)) for idx, child := range nodes { ret := renderLine(child) str_list[idx] = ret } ui.drawList(str_list) ui.drawHeaderFooter() } func (ui *UI) scanPath() { err := ui.node.Scan() if err != nil { ui.err = err.Error() } ui.renderList() } func ShowMain(entry *node.NodeManager) { ui := NewUI(entry) // need input path inputPath := tview.NewInputField() ui.inputPath = inputPath inputPath.SetLabel("Input Path: "). SetFieldWidth(4096). SetDoneFunc(func(key tcell.Key) { if key == tcell.KeyEnter { path := inputPath.GetText() valid := ui.node.SetRootPath(path) if valid { ui.app.SetRoot(ui.main, true) ui.scanPath() } else { inputPath.SetLabel("Invalid Path") } } }) if entry.VaildRoot() { ui.app.SetRoot(ui.main, true) ui.scanPath() } else { ui.app.SetRoot(inputPath, true) } ui.run() }