From 051fa629df9f81c5c28c1005f6b37e6a3f3e136e Mon Sep 17 00:00:00 2001 From: ZZY <2450266535@qq.com> Date: Wed, 12 Feb 2025 18:01:45 +0800 Subject: [PATCH] =?UTF-8?q?refactor(tui):=20=E9=87=8D=E6=9E=84=20UI=20?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 UI 组件分解为更小的模块,提高可维护性 - 优化了列表渲染和路径输入逻辑 - 改进了帮助界面的显示方式 - 调整了 UI 样式,去除了冗余代码 --- node/utils_test.go | 57 +++++++++++ tui/show.go | 146 +++++++++++++++++++++++++++ tui/tui.go | 242 +++++++++++++-------------------------------- 3 files changed, 271 insertions(+), 174 deletions(-) create mode 100644 node/utils_test.go create mode 100644 tui/show.go diff --git a/node/utils_test.go b/node/utils_test.go new file mode 100644 index 0000000..d4b66d6 --- /dev/null +++ b/node/utils_test.go @@ -0,0 +1,57 @@ +package node + +import ( + "fmt" + "testing" +) + +// 测试 BytesToHumanReadable 函数 +func TestBytesToHumanReadable(t *testing.T) { + tests := []struct { + num float64 + suffix string + want string + }{ + {1024, "B", "1.0 KiB"}, + {1024 * 1024, "B", "1.0 MiB"}, + {1024 * 1024 * 1024, "B", "1.0 GiB"}, + {1024 * 1024 * 1024 * 1024, "B", "1.0 TiB"}, + {1024 * 1024 * 1024 * 1024 * 1024, "B", "1.0 PiB"}, + {1024 * 1024 * 1024 * 1024 * 1024 * 1024, "B", "1.0 EiB"}, + {1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024, "B", "1.0 ZiB"}, + {1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024, "B", "1.0 YiB"}, + {512, "B", "512.0 B"}, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("%f_%s", tt.num, tt.suffix), func(t *testing.T) { + got := BytesToHumanReadable(tt.num, tt.suffix) + if got != tt.want { + t.Errorf("BytesToHumanReadable(%f, %s) = %s; want %s", tt.num, tt.suffix, got, tt.want) + } + }) + } +} + +// 测试 FormatSize 函数 +func TestFormatSize(t *testing.T) { + tests := []struct { + size int64 + want string + }{ + {1024, " 1.0 KiB"}, + {1024 * 1024, " 1.0 MiB"}, + {1024 * 1024 * 1024, " 1.0 GiB"}, + {1024 * 1024 * 1024 * 1024, " 1.0 TiB"}, + {512, " 512.0 B"}, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("%d", tt.size), func(t *testing.T) { + got := FormatSize(tt.size) + if got != tt.want { + t.Errorf("FormatSize(%d) = %s; want %s", tt.size, got, tt.want) + } + }) + } +} diff --git a/tui/show.go b/tui/show.go new file mode 100644 index 0000000..4a9e4c6 --- /dev/null +++ b/tui/show.go @@ -0,0 +1,146 @@ +package tui + +import ( + "fmt" + "strings" + + node "git.zzyxyz.com/zzy/goncdu/node" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +func (lf *ListFrame) RenderList() { + lf.nodes = lf.node.GetChildrenWithSorted() + list := lf.list + nodes := lf.nodes + + icon_type := map[node.PathNodeType]string{ + node.DIR: "📁", + node.FILE: "📄", + node.UNKNOWN: "❓", + } + + renderLine := func(entry *node.PathNode) string { + return fmt.Sprintf("%s %10s %10s %s\n", icon_type[entry.GetType()], + node.FormatSize(entry.GetSize()), + 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 + } + + drawList := func(items []string) { + list.Clear() + for _, item := range items { + list.AddItem(item, "", 0, nil) + } + } + + drawList(str_list) + lf.drawHeaderFooter() +} + +func (lf *ListFrame) drawHeaderFooter() { + curNode := lf.node.GetCurrentNode() + lf.frame.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, lf.node.GetScanTime()), + false, tview.AlignLeft, tcell.ColorWhite). + AddText(lf.err, false, tview.AlignLeft, tcell.ColorGrey) +} + +type ListFrame struct { + list *tview.List + frame *tview.Frame + nodes []*node.PathNode + node *node.NodeManager + err string +} + +func (lf *ListFrame) MakeListFrameUI(capture func(event *tcell.EventKey) *tcell.EventKey) tview.Primitive { + list := tview.NewList() + list.ShowSecondaryText(false). + SetTitleAlign(tview.AlignLeft). + SetTitleColor(tcell.ColorWhite) + frame := tview.NewFrame(list). + SetBorders(0, 0, 0, 0, 1, 1) + + list.SetInputCapture(capture) + + lf.list = list + lf.frame = frame + return frame +} + +func MakeHelpUI(quitCallback func()) tview.Primitive { + 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') { + quitCallback() + } + return event + }) + + return modal +} + +func MakeInputUI(enterCallback func(text string) error) tview.Primitive { + inputPath := tview.NewInputField() + inputPath.SetLabel("Input Path: "). + SetFieldWidth(4096). + SetDoneFunc(func(key tcell.Key) { + if key == tcell.KeyEnter { + text := inputPath.GetText() + if err := enterCallback(text); err != nil { + inputPath.SetLabel(err.Error()) + } + } + }) + return inputPath +} diff --git a/tui/tui.go b/tui/tui.go index b8775f1..3cfa692 100644 --- a/tui/tui.go +++ b/tui/tui.go @@ -3,8 +3,6 @@ package tui import ( "errors" - "fmt" - "strings" node "git.zzyxyz.com/zzy/goncdu/node" "github.com/gdamore/tcell/v2" @@ -12,149 +10,88 @@ import ( ) 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 + app *tview.Application + msg string + err string + + main *ListFrame + list tview.Primitive + input tview.Primitive + modal tview.Primitive } 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, + app: tview.NewApplication(), msg: "", err: "", - list: list, - node: n, + main: &ListFrame{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 { + return ui +} + +func (ui *UI) MakeAllUI() { + ui.list = ui.main.MakeListFrameUI(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyRight: + idx := ui.main.list.GetCurrentItem() + if len(ui.main.nodes) == 0 { + ui.err = errors.New("no children nodes").Error() + break + } + name := ui.main.nodes[idx].GetPath() + if err := ui.main.node.GoIn(name); err != nil { + ui.err = err.Error() + break + } + case tcell.KeyLeft: + if err := ui.main.node.GoOut(); err != nil { + ui.err = err.Error() + } + case tcell.KeyRune: + switch event.Rune() { + case 'q': + ui.app.Stop() + case '?': + // show help + ui.app.SetRoot(ui.modal, false) + case 'R': + ui.app.SetRoot(ui.input, true) + case 'S': + if err := ui.main.node.SaveToFile(); 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 + case 'L': + if err := ui.main.node.LoadFromFile(); err != nil { + ui.err = err.Error() } 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) + default: + return event } - } - 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 + // END: + ui.main.RenderList() + return nil }) - // 设置模态窗口为应用的根节点 - 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) -} + ui.modal = MakeHelpUI(func() { + ui.app.SetRoot(ui.list, true) + }) -func (ui *UI) drawList(items []string) { - ui.list.Clear() - for _, item := range items { - ui.list.AddItem(item, "", 0, nil) - } + ui.input = MakeInputUI(func(path string) error { + valid := ui.main.node.SetRootPath(path) + if valid { + ui.app.SetRoot(ui.list, true) + ui.scanPath() + return nil + } else { + return errors.New("invalid path") + } + }) } func (ui *UI) run() { @@ -163,67 +100,24 @@ func (ui *UI) run() { } } -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()), - 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() + err := ui.main.node.Scan() if err != nil { ui.err = err.Error() } - ui.renderList() + ui.main.RenderList() } func ShowMain(entry *node.NodeManager) { ui := NewUI(entry) + ui.MakeAllUI() // 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.app.SetRoot(ui.list, true) ui.scanPath() } else { - ui.app.SetRoot(inputPath, true) + ui.app.SetRoot(ui.input, true) } ui.run() }