tui/show.go

Summary

Maintainability
B
5 hrs
Test Coverage
package tui

import (
    "strconv"
    "strings"

    "github.com/gdamore/tcell/v2"
    "github.com/rivo/tview"
    log "github.com/sirupsen/logrus"

    "github.com/dundee/gdu/v5/build"
)

const helpText = `     [::b]up/down, k/j    [white:black:-]Move cursor up/down
  [::b]pgup/pgdn, g/G     [white:black:-]Move cursor top/bottom
 [::b]enter, right, l     [white:black:-]Go to directory/device
         [::b]left, h     [white:black:-]Go to parent directory

               [::b]r     [white:black:-]Rescan current directory
               [::b]E     [white:black:-]Export analysis data to file as JSON
               [::b]/     [white:black:-]Search items by name
               [::b]a     [white:black:-]Toggle between showing disk usage and apparent size
               [::b]B     [white:black:-]Toggle bar alignment to biggest file or directory
               [::b]c     [white:black:-]Show/hide file count
               [::b]m     [white:black:-]Show/hide latest mtime
               [::b]b     [white:black:-]Spawn shell in current directory
               [::b]q     [white:black:-]Quit gdu
               [::b]Q     [white:black:-]Quit gdu and print current directory path

Item under cursor:
               [::b]d     [white:black:-]Delete file or directory
               [::b]e     [white:black:-]Empty file or directory
               [::b]space [white:black:-]Mark file or directory for deletion
               [::b]I     [white:black:-]Ignore file or directory
               [::b]v     [white:black:-]Show content of file
               [::b]o     [white:black:-]Open file or directory in external program
               [::b]i     [white:black:-]Show info about item

Sort by (twice toggles asc/desc):
               [::b]n     [white:black:-]Sort by name (asc/desc)
               [::b]s     [white:black:-]Sort by size (asc/desc)
               [::b]C     [white:black:-]Sort by file count (asc/desc)
               [::b]M     [white:black:-]Sort by mtime (asc/desc)`

// 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 = "[#ffffff:#2479d0:b]"
        footerTextColor = blackOnBlue
    } 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 = "[#ffffff:#2479d0:b]"
        footerTextColor = blackOnBlue
    } 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")
}