refactor(tui): 重构 UI 设计
- 将 UI 组件分解为更小的模块,提高可维护性 - 优化了列表渲染和路径输入逻辑 - 改进了帮助界面的显示方式 - 调整了 UI 样式,去除了冗余代码
This commit is contained in:
parent
dd4cc42692
commit
051fa629df
57
node/utils_test.go
Normal file
57
node/utils_test.go
Normal 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
146
tui/show.go
Normal 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
|
||||||
|
}
|
242
tui/tui.go
242
tui/tui.go
@ -3,8 +3,6 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
node "git.zzyxyz.com/zzy/goncdu/node"
|
node "git.zzyxyz.com/zzy/goncdu/node"
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
@ -12,149 +10,88 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type UI struct {
|
type UI struct {
|
||||||
app *tview.Application
|
app *tview.Application
|
||||||
main *tview.Frame
|
msg string
|
||||||
msg string
|
err string
|
||||||
err string
|
|
||||||
list *tview.List
|
main *ListFrame
|
||||||
node *node.NodeManager
|
list tview.Primitive
|
||||||
nodes []*node.PathNode
|
input tview.Primitive
|
||||||
inputPath *tview.InputField
|
modal tview.Primitive
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUI(n *node.NodeManager) *UI {
|
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{
|
ui := &UI{
|
||||||
app: tview.NewApplication().SetRoot(frame, true),
|
app: tview.NewApplication(),
|
||||||
main: frame,
|
|
||||||
msg: "",
|
msg: "",
|
||||||
err: "",
|
err: "",
|
||||||
list: list,
|
main: &ListFrame{node: n},
|
||||||
node: n,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.list.SetInputCapture(
|
return ui
|
||||||
func(event *tcell.EventKey) *tcell.EventKey {
|
}
|
||||||
switch event.Key() {
|
|
||||||
case tcell.KeyRight:
|
func (ui *UI) MakeAllUI() {
|
||||||
idx := ui.list.GetCurrentItem()
|
ui.list = ui.main.MakeListFrameUI(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
if len(ui.nodes) == 0 {
|
switch event.Key() {
|
||||||
ui.err = errors.New("no children nodes").Error()
|
case tcell.KeyRight:
|
||||||
break
|
idx := ui.main.list.GetCurrentItem()
|
||||||
}
|
if len(ui.main.nodes) == 0 {
|
||||||
name := ui.nodes[idx].GetPath()
|
ui.err = errors.New("no children nodes").Error()
|
||||||
if err := ui.node.GoIn(name); err != nil {
|
break
|
||||||
ui.err = err.Error()
|
}
|
||||||
break
|
name := ui.main.nodes[idx].GetPath()
|
||||||
}
|
if err := ui.main.node.GoIn(name); err != nil {
|
||||||
case tcell.KeyLeft:
|
ui.err = err.Error()
|
||||||
if err := ui.node.GoOut(); err != nil {
|
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()
|
ui.err = err.Error()
|
||||||
}
|
}
|
||||||
case tcell.KeyRune:
|
case 'L':
|
||||||
switch event.Rune() {
|
if err := ui.main.node.LoadFromFile(); err != nil {
|
||||||
case 'q':
|
ui.err = err.Error()
|
||||||
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:
|
default:
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
// END:
|
return event
|
||||||
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
|
// END:
|
||||||
for _, line := range helpText {
|
ui.main.RenderList()
|
||||||
paddedLine := fmt.Sprintf("%-*s", maxLength, line)
|
return nil
|
||||||
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.modal = MakeHelpUI(func() {
|
||||||
ui.app.SetRoot(modal, false)
|
ui.app.SetRoot(ui.list, true)
|
||||||
}
|
})
|
||||||
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.input = MakeInputUI(func(path string) error {
|
||||||
ui.list.Clear()
|
valid := ui.main.node.SetRootPath(path)
|
||||||
for _, item := range items {
|
if valid {
|
||||||
ui.list.AddItem(item, "", 0, nil)
|
ui.app.SetRoot(ui.list, true)
|
||||||
}
|
ui.scanPath()
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return errors.New("invalid path")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UI) run() {
|
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() {
|
func (ui *UI) scanPath() {
|
||||||
err := ui.node.Scan()
|
err := ui.main.node.Scan()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ui.err = err.Error()
|
ui.err = err.Error()
|
||||||
}
|
}
|
||||||
ui.renderList()
|
ui.main.RenderList()
|
||||||
}
|
}
|
||||||
|
|
||||||
func ShowMain(entry *node.NodeManager) {
|
func ShowMain(entry *node.NodeManager) {
|
||||||
ui := NewUI(entry)
|
ui := NewUI(entry)
|
||||||
|
ui.MakeAllUI()
|
||||||
|
|
||||||
// need input path
|
// 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() {
|
if entry.VaildRoot() {
|
||||||
ui.app.SetRoot(ui.main, true)
|
ui.app.SetRoot(ui.list, true)
|
||||||
ui.scanPath()
|
ui.scanPath()
|
||||||
} else {
|
} else {
|
||||||
ui.app.SetRoot(inputPath, true)
|
ui.app.SetRoot(ui.input, true)
|
||||||
}
|
}
|
||||||
ui.run()
|
ui.run()
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user