star/gdu/tui/show.go
2025-04-26 19:33:14 +08:00

333 lines
9.0 KiB
Go

package tui
import (
"fmt"
"strconv"
"strings"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
log "github.com/sirupsen/logrus"
"b612.me/apps/b612/gdu/build"
)
const helpText = ` [::b]上/下, k/j [white:black:-]光标上/下移动
[::b]pgup/pgdn, g/G [white:black:-]光标跳转至顶部/底部
[::b]enter, 右, l [white:black:-]进入目录/设备
[::b]左, h [white:black:-]返回上级目录
[::b]r [white:black:-]重新扫描当前目录
[::b]E [white:black:-]导出分析数据为JSON文件
[::b]/ [white:black:-]按名称搜索项目
[::b]a [white:black:-]切换磁盘用量与表观大小显示
[::b]B [white:black:-]切换进度条对齐方式(最大文件/目录)
[::b]c [white:black:-]显示/隐藏文件数量统计
[::b]m [white:black:-]显示/隐藏最新修改时间
[::b]b [white:black:-]在当前目录打开Shell
[::b]q [white:black:-]退出程序
[::b]Q [white:black:-]退出并输出当前目录路径
光标所在项目操作:
[::b]d [white:black:-]删除文件或目录
[::b]e [white:black:-]清空文件或目录
[::b]space [white:black:-]标记待删除文件/目录
[::b]I [white:black:-]忽略当前文件或目录
[::b]v [white:black:-]查看文件内容
[::b]o [white:black:-]使用外部程序打开
[::b]i [white:black:-]显示项目详细信息
排序方式(再次按键切换升序/降序):
[::b]n [white:black:-]按名称排序
[::b]s [white:black:-]按大小排序
[::b]C [white:black:-]按文件数量排序
[::b]M [white:black:-]按修改时间排序`
// nolint: funlen // Why: complex function
func (ui *UI) showDir() {
var (
totalUsage int64
totalSize int64
maxUsage int64
maxSize int64
itemCount int
)
ui.currentDirPath = ui.currentDir.GetPath()
if ui.changeCwdFn != nil {
err := ui.changeCwdFn(ui.currentDirPath)
if err != nil {
log.Printf("error setting cwd: %s", err.Error())
}
log.Printf("changing cwd to %s", ui.currentDirPath)
}
ui.currentDirLabel.SetText("[::b] --- " +
tview.Escape(
strings.TrimPrefix(ui.currentDirPath, build.RootPathPrefix),
) +
" ---").SetDynamicColors(true)
ui.table.Clear()
rowIndex := 0
if ui.currentDirPath != ui.topDirPath {
prefix := " "
if len(ui.markedRows) > 0 {
prefix += " "
}
cell := tview.NewTableCell(prefix + "[::b]/..")
cell.SetReference(ui.currentDir.GetParent())
cell.SetStyle(tcell.Style{}.Foreground(tcell.ColorDefault))
ui.table.SetCell(0, 0, cell)
rowIndex++
}
ui.sortItems()
unlock := ui.currentDir.RLock()
defer unlock()
i := rowIndex
maxUsage = 0
maxSize = 0
for _, item := range ui.currentDir.GetFiles() {
if _, ignored := ui.ignoredRows[i]; ignored {
i++
continue
}
if ui.ShowRelativeSize {
if item.GetUsage() > maxUsage {
maxUsage = item.GetUsage()
}
if item.GetSize() > maxSize {
maxSize = item.GetSize()
}
} else {
maxSize += item.GetSize()
maxUsage += item.GetUsage()
}
i++
}
for i, item := range ui.currentDir.GetFiles() {
if ui.filterValue != "" && !strings.Contains(
strings.ToLower(item.GetName()),
strings.ToLower(ui.filterValue),
) {
continue
}
_, ignored := ui.ignoredRows[rowIndex]
if !ignored {
totalUsage += item.GetUsage()
totalSize += item.GetSize()
itemCount += item.GetItemCount()
}
_, marked := ui.markedRows[rowIndex]
cell := tview.NewTableCell(ui.formatFileRow(item, maxUsage, maxSize, marked, ignored))
cell.SetReference(ui.currentDir.GetFiles()[i])
switch {
case ignored:
cell.SetStyle(tcell.Style{}.Foreground(tview.Styles.SecondaryTextColor))
case marked:
cell.SetStyle(tcell.Style{}.Foreground(tview.Styles.PrimaryTextColor))
cell.SetBackgroundColor(tview.Styles.ContrastBackgroundColor)
default:
cell.SetStyle(tcell.Style{}.Foreground(tcell.ColorDefault))
}
ui.table.SetCell(rowIndex, 0, cell)
rowIndex++
}
var footerNumberColor, footerTextColor string
if ui.UseColors {
footerNumberColor = fmt.Sprintf(
"[%s:%s:b]",
ui.footerNumberColor,
ui.footerBackgroundColor,
)
footerTextColor = fmt.Sprintf(
"[%s:%s:-]",
ui.footerTextColor,
ui.footerBackgroundColor,
)
} else {
footerNumberColor = "[black:white:b]"
footerTextColor = blackOnWhite
}
selected := ""
if len(ui.markedRows) > 0 {
selected = " Selected items: " + footerNumberColor +
strconv.Itoa(len(ui.markedRows)) + footerTextColor
}
ui.footerLabel.SetText(
selected + footerTextColor +
" Total disk usage: " +
footerNumberColor +
ui.formatSize(totalUsage, true, false) +
" Apparent size: " +
footerNumberColor +
ui.formatSize(totalSize, true, false) +
" Items: " + footerNumberColor + strconv.Itoa(itemCount) +
footerTextColor +
" Sorting by: " + ui.sortBy + " " + ui.sortOrder)
ui.table.Select(0, 0)
ui.table.ScrollToBeginning()
if !ui.filtering {
ui.app.SetFocus(ui.table)
}
}
func (ui *UI) showDevices() {
var totalUsage int64
ui.table.Clear()
ui.table.SetCell(0, 0, tview.NewTableCell("Device name").SetSelectable(false))
ui.table.SetCell(0, 1, tview.NewTableCell("Size").SetSelectable(false))
ui.table.SetCell(0, 2, tview.NewTableCell("Used").SetSelectable(false))
ui.table.SetCell(0, 3, tview.NewTableCell("Used part").SetSelectable(false))
ui.table.SetCell(0, 4, tview.NewTableCell("Free").SetSelectable(false))
ui.table.SetCell(0, 5, tview.NewTableCell("Mount point").SetSelectable(false))
var textColor, sizeColor string
if ui.UseColors {
textColor = "[#3498db:-:b]"
sizeColor = "[#edb20a:-:b]"
} else {
textColor = "[white:-:b]"
sizeColor = "[white:-:b]"
}
ui.sortDevices()
for i, device := range ui.devices {
totalUsage += device.GetUsage()
ui.table.SetCell(i+1, 0, tview.NewTableCell(textColor+device.Name).SetReference(ui.devices[i]))
ui.table.SetCell(i+1, 1, tview.NewTableCell(ui.formatSize(device.Size, false, true)))
ui.table.SetCell(i+1, 2, tview.NewTableCell(sizeColor+ui.formatSize(device.Size-device.Free, false, true)))
ui.table.SetCell(i+1, 3, tview.NewTableCell(getDeviceUsagePart(device, ui.useOldSizeBar)))
ui.table.SetCell(i+1, 4, tview.NewTableCell(ui.formatSize(device.Free, false, true)))
ui.table.SetCell(i+1, 5, tview.NewTableCell(textColor+device.MountPoint).SetReference(ui.devices[i]))
}
var footerNumberColor, footerTextColor string
if ui.UseColors {
footerNumberColor = fmt.Sprintf(
"[%s:%s:b]",
ui.footerNumberColor,
ui.footerBackgroundColor,
)
footerTextColor = fmt.Sprintf(
"[%s:%s:-]",
ui.footerTextColor,
ui.footerBackgroundColor,
)
} else {
footerNumberColor = "[black:white:b]"
footerTextColor = blackOnWhite
}
ui.footerLabel.SetText(
" Total usage: " +
footerNumberColor +
ui.formatSize(totalUsage, true, false) +
footerTextColor +
" Sorting by: " + ui.sortBy + " " + ui.sortOrder)
ui.table.Select(1, 0)
ui.table.SetSelectedFunc(ui.deviceItemSelected)
if ui.topDirPath != "" {
for i, device := range ui.devices {
if device.MountPoint == ui.topDirPath {
ui.table.Select(i+1, 0)
break
}
}
}
}
func (ui *UI) showErr(msg string, err error) {
modal := tview.NewModal().
SetText(msg + ": " + err.Error()).
AddButtons([]string{"ok"}).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
ui.pages.RemovePage("error")
})
if !ui.UseColors {
modal.SetBackgroundColor(tcell.ColorGray)
}
ui.pages.AddPage("error", modal, true, true)
ui.app.SetFocus(modal)
}
func (ui *UI) showErrFromGo(msg string, err error) {
ui.app.QueueUpdateDraw(func() {
ui.showErr(msg, err)
})
}
func (ui *UI) showHelp() {
text := tview.NewTextView().SetDynamicColors(true)
text.SetBorder(true).SetBorderPadding(2, 2, 2, 2)
text.SetBorderColor(tcell.ColorDefault)
text.SetTitle(" gdu help ")
text.SetScrollable(true)
formattedHelpText := ui.formatHelpTextFor()
text.SetText(formattedHelpText)
maxHeight := strings.Count(formattedHelpText, "\n") + 7
_, height := ui.screen.Size()
if height > maxHeight {
height = maxHeight
}
flex := tview.NewFlex().
AddItem(nil, 0, 1, false).
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(nil, 0, 1, false).
AddItem(text, height, 1, false).
AddItem(nil, 0, 1, false), 80, 1, false).
AddItem(nil, 0, 1, false)
ui.help = flex
ui.pages.AddPage("help", flex, true, true)
ui.app.SetFocus(text)
}
func (ui *UI) formatHelpTextFor() string {
lines := strings.Split(helpText, "\n")
for i, line := range lines {
if ui.UseColors {
lines[i] = strings.ReplaceAll(
strings.ReplaceAll(line, defaultColorBold, "[red]"),
whiteOnBlack,
"[white]",
)
}
if ui.noDelete && (strings.Contains(line, "Empty file or directory") ||
strings.Contains(line, "Delete file or directory")) {
lines[i] += " (disabled)"
}
}
return strings.Join(lines, "\n")
}