refactor(tui): 重构 UI 设计

- 将 UI 组件分解为更小的模块,提高可维护性
- 优化了列表渲染和路径输入逻辑
- 改进了帮助界面的显示方式
- 调整了 UI 样式,去除了冗余代码
This commit is contained in:
ZZY 2025-02-12 18:01:45 +08:00
parent dd4cc42692
commit 051fa629df
3 changed files with 271 additions and 174 deletions

57
node/utils_test.go Normal file
View File

@ -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)
}
})
}
}

146
tui/show.go Normal file
View File

@ -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
}

View File

@ -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()
}