2024-07-30 14:43:54 -04:00
|
|
|
// Package interactive implements a charm-powered table to display files.
|
2024-07-28 21:06:41 -04:00
|
|
|
package interactive
|
2024-06-19 18:55:01 -04:00
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"math"
|
2024-07-31 03:53:02 -04:00
|
|
|
"os"
|
2024-06-19 18:55:01 -04:00
|
|
|
"path/filepath"
|
|
|
|
"slices"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"git.burning.moe/celediel/gt/internal/dirs"
|
|
|
|
"git.burning.moe/celediel/gt/internal/files"
|
2024-07-28 21:06:41 -04:00
|
|
|
"git.burning.moe/celediel/gt/internal/interactive/modes"
|
|
|
|
"git.burning.moe/celediel/gt/internal/interactive/sorting"
|
2024-07-31 03:53:02 -04:00
|
|
|
"golang.org/x/term"
|
2024-07-03 18:05:17 -04:00
|
|
|
|
2024-06-19 18:55:01 -04:00
|
|
|
"github.com/charmbracelet/bubbles/key"
|
|
|
|
"github.com/charmbracelet/bubbles/table"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
|
|
"github.com/dustin/go-humanize"
|
2024-07-30 21:32:34 -04:00
|
|
|
"github.com/lithammer/fuzzysearch/fuzzy"
|
2024-06-19 18:55:01 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
uncheck string = "☐"
|
|
|
|
check string = "☑"
|
|
|
|
space string = " "
|
|
|
|
woffset int = 13 // why this number, I don't know
|
2024-06-19 21:24:57 -04:00
|
|
|
hoffset int = 6
|
2024-07-30 16:03:33 -04:00
|
|
|
poffset int = 2
|
|
|
|
|
2024-07-30 21:25:48 -04:00
|
|
|
filenameColumn string = "filename"
|
|
|
|
pathColumn string = "path"
|
|
|
|
modifiedColumn string = "modified"
|
|
|
|
trashedColumn string = "trashed"
|
|
|
|
sizeColumn string = "size"
|
2024-07-30 21:48:49 -04:00
|
|
|
bar string = "───"
|
2024-07-30 21:25:48 -04:00
|
|
|
|
2024-07-30 16:03:33 -04:00
|
|
|
// TODO: figure these out dynamically based on longest of each
|
2024-07-30 21:25:48 -04:00
|
|
|
filenameColumnW float64 = 0.46
|
|
|
|
pathColumnW float64 = 0.25
|
|
|
|
dateColumnW float64 = 0.15
|
|
|
|
sizeColumnW float64 = 0.12
|
|
|
|
checkColumnW float64 = 0.02
|
2024-06-19 18:55:01 -04:00
|
|
|
|
|
|
|
// TODO: make these configurable or something
|
|
|
|
borderbg string = "5"
|
|
|
|
hoveritembg string = "13"
|
|
|
|
black string = "0"
|
|
|
|
darkblack string = "8"
|
|
|
|
white string = "7"
|
|
|
|
darkgray string = "15"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
style = lipgloss.NewStyle().
|
|
|
|
BorderStyle(lipgloss.RoundedBorder()).
|
|
|
|
BorderForeground(lipgloss.Color(borderbg))
|
|
|
|
regulartext = lipgloss.NewStyle().
|
2024-07-30 16:03:33 -04:00
|
|
|
Padding(0, poffset)
|
2024-06-19 18:55:01 -04:00
|
|
|
darktext = lipgloss.NewStyle().
|
|
|
|
Foreground(lipgloss.Color(darkgray))
|
|
|
|
darkertext = lipgloss.NewStyle().
|
|
|
|
Foreground(lipgloss.Color(darkblack))
|
|
|
|
darkesttext = lipgloss.NewStyle().
|
|
|
|
Foreground(lipgloss.Color(black))
|
|
|
|
)
|
|
|
|
|
|
|
|
type model struct {
|
2024-07-15 18:32:33 -04:00
|
|
|
table table.Model
|
|
|
|
keys keyMap
|
|
|
|
selected map[string]bool
|
2024-07-21 23:54:22 -04:00
|
|
|
selectsize int64
|
2024-07-15 18:32:33 -04:00
|
|
|
readonly bool
|
2024-07-15 18:33:37 -04:00
|
|
|
once bool
|
2024-07-30 21:32:34 -04:00
|
|
|
filtering bool
|
|
|
|
filter string
|
2024-07-15 18:32:33 -04:00
|
|
|
termheight int
|
2024-07-28 21:18:16 -04:00
|
|
|
termwidth int
|
2024-07-15 18:32:33 -04:00
|
|
|
mode modes.Mode
|
|
|
|
sorting sorting.Sorting
|
|
|
|
workdir string
|
|
|
|
files files.Files
|
2024-07-30 21:32:34 -04:00
|
|
|
fltrfiles files.Files
|
2024-06-19 18:55:01 -04:00
|
|
|
}
|
|
|
|
|
2024-08-06 01:34:24 -04:00
|
|
|
func newModel(fls []files.File, selectall, readonly, once bool, workdir string, mode modes.Mode) model {
|
|
|
|
m := model{
|
2024-07-31 03:53:02 -04:00
|
|
|
keys: defaultKeyMap(),
|
|
|
|
readonly: readonly,
|
|
|
|
once: once,
|
|
|
|
mode: mode,
|
|
|
|
selected: map[string]bool{},
|
|
|
|
selectsize: 0,
|
|
|
|
files: fls,
|
2024-07-15 19:31:58 -04:00
|
|
|
}
|
|
|
|
|
2024-07-31 03:53:02 -04:00
|
|
|
m.termwidth, m.termheight = termSizes()
|
2024-06-19 18:55:01 -04:00
|
|
|
|
2024-07-31 03:53:02 -04:00
|
|
|
if workdir != "" {
|
|
|
|
m.workdir = filepath.Clean(workdir)
|
2024-07-30 14:51:12 -04:00
|
|
|
}
|
|
|
|
|
2024-07-31 03:53:02 -04:00
|
|
|
rows := m.freshRows()
|
|
|
|
columns := m.freshColumns()
|
2024-06-19 18:55:01 -04:00
|
|
|
|
2024-07-31 03:53:02 -04:00
|
|
|
theight := min(m.termheight-hoffset, len(fls))
|
|
|
|
m.table = createTable(columns, rows, theight)
|
2024-06-19 18:55:01 -04:00
|
|
|
|
2024-07-31 03:53:02 -04:00
|
|
|
m.sorting = sorting.Name
|
|
|
|
m.sort()
|
2024-07-03 18:05:17 -04:00
|
|
|
|
2024-07-30 22:42:59 -04:00
|
|
|
if selectall {
|
2024-07-31 03:53:02 -04:00
|
|
|
m.selectAll()
|
2024-07-30 22:42:59 -04:00
|
|
|
}
|
|
|
|
|
2024-08-06 01:34:24 -04:00
|
|
|
return m
|
2024-06-19 18:55:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
type keyMap struct {
|
|
|
|
mark key.Binding
|
|
|
|
doit key.Binding
|
2024-06-19 19:36:12 -04:00
|
|
|
todo key.Binding
|
|
|
|
nada key.Binding
|
|
|
|
invr key.Binding
|
2024-06-19 21:24:57 -04:00
|
|
|
rstr key.Binding
|
|
|
|
clen key.Binding
|
2024-07-03 18:05:17 -04:00
|
|
|
sort key.Binding
|
|
|
|
rort key.Binding
|
2024-07-30 21:32:34 -04:00
|
|
|
fltr key.Binding
|
|
|
|
clfl key.Binding
|
|
|
|
apfl key.Binding
|
|
|
|
bksp key.Binding
|
2024-06-19 18:55:01 -04:00
|
|
|
quit key.Binding
|
|
|
|
}
|
|
|
|
|
|
|
|
func defaultKeyMap() keyMap {
|
|
|
|
return keyMap{
|
|
|
|
mark: key.NewBinding(
|
|
|
|
key.WithKeys(space),
|
2024-06-19 19:36:12 -04:00
|
|
|
key.WithHelp("space", "toggle"),
|
2024-06-19 18:55:01 -04:00
|
|
|
),
|
|
|
|
doit: key.NewBinding(
|
|
|
|
key.WithKeys("enter", "y"),
|
|
|
|
key.WithHelp("enter/y", "confirm"),
|
|
|
|
),
|
2024-06-19 19:36:12 -04:00
|
|
|
todo: key.NewBinding(
|
|
|
|
key.WithKeys("a", "ctrl+a"),
|
2024-07-28 21:07:23 -04:00
|
|
|
key.WithHelp("a", "all"),
|
2024-06-19 19:36:12 -04:00
|
|
|
),
|
|
|
|
nada: key.NewBinding(
|
|
|
|
key.WithKeys("n", "ctrl+n"),
|
2024-06-20 00:36:13 -04:00
|
|
|
key.WithHelp("n", "none"),
|
2024-06-19 19:36:12 -04:00
|
|
|
),
|
|
|
|
invr: key.NewBinding(
|
|
|
|
key.WithKeys("i", "ctrl+i"),
|
2024-06-20 00:36:13 -04:00
|
|
|
key.WithHelp("i", "invert"),
|
2024-06-19 19:36:12 -04:00
|
|
|
),
|
2024-06-19 21:24:57 -04:00
|
|
|
clen: key.NewBinding(
|
|
|
|
key.WithKeys("c"),
|
2024-06-20 00:36:13 -04:00
|
|
|
key.WithHelp("c", "clean"),
|
2024-06-19 21:24:57 -04:00
|
|
|
),
|
|
|
|
rstr: key.NewBinding(
|
|
|
|
key.WithKeys("r"),
|
2024-06-20 00:36:13 -04:00
|
|
|
key.WithHelp("r", "restore"),
|
2024-06-19 21:24:57 -04:00
|
|
|
),
|
2024-07-03 18:05:17 -04:00
|
|
|
sort: key.NewBinding(
|
|
|
|
key.WithKeys("s"),
|
2024-07-28 21:07:23 -04:00
|
|
|
key.WithHelp("s/S", "sort"),
|
2024-07-03 18:05:17 -04:00
|
|
|
),
|
|
|
|
rort: key.NewBinding(
|
|
|
|
key.WithKeys("S"),
|
2024-07-28 21:07:23 -04:00
|
|
|
key.WithHelp("S", "sort (reverse)"),
|
2024-07-03 18:05:17 -04:00
|
|
|
),
|
2024-07-30 21:32:34 -04:00
|
|
|
fltr: key.NewBinding(
|
|
|
|
key.WithKeys("/"),
|
|
|
|
key.WithHelp("/", "filter"),
|
|
|
|
),
|
|
|
|
apfl: key.NewBinding(
|
|
|
|
key.WithKeys("enter"),
|
|
|
|
key.WithHelp("enter", "apply filter"),
|
|
|
|
),
|
|
|
|
clfl: key.NewBinding(
|
|
|
|
key.WithKeys("esc"),
|
|
|
|
key.WithHelp("esc", "clear filter"),
|
|
|
|
),
|
|
|
|
bksp: key.NewBinding(
|
|
|
|
key.WithKeys("backspace"),
|
|
|
|
key.WithHelp("backspace", "backspace"),
|
|
|
|
),
|
2024-06-19 18:55:01 -04:00
|
|
|
quit: key.NewBinding(
|
|
|
|
key.WithKeys("q", "ctrl+c"),
|
2024-06-19 19:36:12 -04:00
|
|
|
key.WithHelp("q", "quit"),
|
2024-06-19 18:55:01 -04:00
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m model) Init() tea.Cmd {
|
2024-07-15 18:33:37 -04:00
|
|
|
/* if m.onePage() {
|
|
|
|
m.table.SetStyles(makeUnselectedStyle())
|
|
|
|
m.unselectAll()
|
|
|
|
return tea.Quit
|
|
|
|
} */
|
2024-06-19 18:55:01 -04:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
|
var cmd tea.Cmd
|
|
|
|
|
2024-07-15 18:33:37 -04:00
|
|
|
if m.once {
|
2024-07-16 14:12:37 -04:00
|
|
|
return m.quit(m.readonly)
|
2024-07-15 18:33:37 -04:00
|
|
|
}
|
|
|
|
|
2024-06-19 18:55:01 -04:00
|
|
|
switch msg := msg.(type) {
|
2024-07-31 03:53:02 -04:00
|
|
|
case tea.WindowSizeMsg:
|
|
|
|
m.updateTableSize()
|
2024-06-19 18:55:01 -04:00
|
|
|
case tea.KeyMsg:
|
2024-07-30 21:32:34 -04:00
|
|
|
if m.filtering {
|
|
|
|
switch {
|
|
|
|
case key.Matches(msg, m.keys.clfl):
|
|
|
|
m.filter = ""
|
|
|
|
m.filtering = false
|
|
|
|
case key.Matches(msg, m.keys.apfl):
|
|
|
|
m.filtering = false
|
|
|
|
case key.Matches(msg, m.keys.bksp):
|
|
|
|
if len(m.filter) > 0 {
|
|
|
|
m.filter = m.filter[:len(m.filter)-1]
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
m.filter += msg.String()
|
|
|
|
}
|
|
|
|
m.applyFilter()
|
|
|
|
return m, cmd
|
|
|
|
}
|
|
|
|
|
2024-06-19 18:55:01 -04:00
|
|
|
switch {
|
|
|
|
case key.Matches(msg, m.keys.mark):
|
2024-06-29 22:54:15 -04:00
|
|
|
m.toggleItem(m.table.Cursor())
|
2024-06-19 18:55:01 -04:00
|
|
|
case key.Matches(msg, m.keys.doit):
|
2024-07-30 22:44:02 -04:00
|
|
|
if !m.readonly && m.mode != modes.Interactive && len(m.fltrfiles) > 1 {
|
2024-06-29 22:56:51 -04:00
|
|
|
return m.quit(false)
|
2024-06-19 18:55:01 -04:00
|
|
|
}
|
2024-06-19 19:36:12 -04:00
|
|
|
case key.Matches(msg, m.keys.nada):
|
2024-06-29 22:54:15 -04:00
|
|
|
m.unselectAll()
|
2024-06-19 19:36:12 -04:00
|
|
|
case key.Matches(msg, m.keys.todo):
|
2024-06-29 22:54:15 -04:00
|
|
|
m.selectAll()
|
2024-06-19 19:36:12 -04:00
|
|
|
case key.Matches(msg, m.keys.invr):
|
2024-06-29 22:54:15 -04:00
|
|
|
m.invertSelection()
|
2024-06-19 21:24:57 -04:00
|
|
|
case key.Matches(msg, m.keys.clen):
|
2024-07-29 02:31:38 -04:00
|
|
|
return m.execute(modes.Cleaning)
|
2024-06-19 21:24:57 -04:00
|
|
|
case key.Matches(msg, m.keys.rstr):
|
2024-07-29 02:31:38 -04:00
|
|
|
return m.execute(modes.Restoring)
|
2024-07-03 18:05:17 -04:00
|
|
|
case key.Matches(msg, m.keys.sort):
|
|
|
|
m.sorting = m.sorting.Next()
|
|
|
|
m.sort()
|
|
|
|
case key.Matches(msg, m.keys.rort):
|
|
|
|
m.sorting = m.sorting.Prev()
|
|
|
|
m.sort()
|
2024-07-30 21:32:34 -04:00
|
|
|
case key.Matches(msg, m.keys.fltr):
|
|
|
|
m.filtering = true
|
|
|
|
case key.Matches(msg, m.keys.clfl):
|
2024-07-30 22:44:02 -04:00
|
|
|
if m.filter != "" {
|
|
|
|
m.filter = ""
|
|
|
|
m.applyFilter()
|
|
|
|
}
|
2024-06-19 18:55:01 -04:00
|
|
|
case key.Matches(msg, m.keys.quit):
|
2024-06-19 21:24:57 -04:00
|
|
|
return m.quit(true)
|
2024-06-19 18:55:01 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// pass events along to the table
|
|
|
|
m.table, cmd = m.table.Update(msg)
|
|
|
|
return m, cmd
|
|
|
|
}
|
|
|
|
|
2024-07-29 02:36:40 -04:00
|
|
|
func (m model) View() string {
|
2024-07-30 22:44:02 -04:00
|
|
|
var panels []string
|
|
|
|
if !m.once {
|
|
|
|
panels = append(panels, m.header())
|
|
|
|
}
|
|
|
|
|
|
|
|
panels = append(panels, style.Render(m.table.View()), m.footer())
|
|
|
|
|
2024-07-30 21:32:34 -04:00
|
|
|
return lipgloss.JoinVertical(lipgloss.Top,
|
2024-07-30 22:44:02 -04:00
|
|
|
panels...,
|
2024-07-30 21:32:34 -04:00
|
|
|
)
|
2024-06-19 18:55:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func (m model) showHelp() string {
|
2024-07-30 21:32:34 -04:00
|
|
|
var filterText string
|
|
|
|
if m.filter != "" {
|
|
|
|
filterText = fmt.Sprintf(" (%s)", m.filter)
|
|
|
|
}
|
|
|
|
|
|
|
|
keys := []string{
|
|
|
|
fmt.Sprintf("%s %s%s", darktext.Render(m.keys.fltr.Help().Key), darkertext.Render(m.keys.fltr.Help().Desc), filterText),
|
2024-07-03 18:05:17 -04:00
|
|
|
fmt.Sprintf("%s %s (%s)", darktext.Render(m.keys.sort.Help().Key), darkertext.Render(m.keys.sort.Help().Desc), m.sorting.String()),
|
2024-07-30 21:28:14 -04:00
|
|
|
styleKey(m.keys.quit),
|
2024-06-19 18:55:01 -04:00
|
|
|
}
|
2024-07-30 21:32:34 -04:00
|
|
|
|
2024-06-19 18:55:01 -04:00
|
|
|
if !m.readonly {
|
2024-06-19 21:24:57 -04:00
|
|
|
if m.mode != modes.Interactive {
|
2024-07-31 00:28:09 -04:00
|
|
|
keys = append([]string{styleKey(m.keys.doit)}, keys...)
|
2024-06-19 21:24:57 -04:00
|
|
|
}
|
2024-07-31 00:28:09 -04:00
|
|
|
keys = append([]string{styleKey(m.keys.mark)}, keys...)
|
2024-06-19 18:55:01 -04:00
|
|
|
}
|
|
|
|
return strings.Join(keys, darkesttext.Render(" • "))
|
|
|
|
}
|
|
|
|
|
2024-06-19 21:24:57 -04:00
|
|
|
func (m model) header() string {
|
|
|
|
var (
|
2024-07-30 14:43:54 -04:00
|
|
|
right, left string
|
|
|
|
spacerWidth int
|
|
|
|
keys = []string{
|
2024-07-30 21:28:14 -04:00
|
|
|
styleKey(m.keys.rstr),
|
|
|
|
styleKey(m.keys.clen),
|
2024-06-19 21:24:57 -04:00
|
|
|
}
|
2024-07-30 14:43:54 -04:00
|
|
|
selectKeys = []string{
|
2024-07-30 21:28:14 -04:00
|
|
|
styleKey(m.keys.todo),
|
|
|
|
styleKey(m.keys.nada),
|
|
|
|
styleKey(m.keys.invr),
|
2024-06-20 00:36:13 -04:00
|
|
|
}
|
2024-07-30 21:32:34 -04:00
|
|
|
filterKeys = []string{
|
|
|
|
styleKey(m.keys.clfl),
|
|
|
|
styleKey(m.keys.apfl),
|
|
|
|
}
|
2024-07-31 00:28:09 -04:00
|
|
|
dot = darkesttext.Render("•")
|
|
|
|
wideDot = darkesttext.Render(" • ")
|
|
|
|
keysFmt = strings.Join(keys, wideDot)
|
|
|
|
selectFmt = strings.Join(selectKeys, wideDot)
|
|
|
|
filterFmt = strings.Join(filterKeys, wideDot)
|
2024-06-19 21:24:57 -04:00
|
|
|
)
|
|
|
|
|
2024-07-31 00:28:09 -04:00
|
|
|
switch {
|
|
|
|
case m.filtering:
|
|
|
|
right = fmt.Sprintf(" Filtering %s %s", dot, filterFmt)
|
|
|
|
case m.mode == modes.Interactive:
|
|
|
|
right = fmt.Sprintf(" %s %s %s", keysFmt, dot, selectFmt)
|
|
|
|
left = fmt.Sprintf("%d/%d %s %s", len(m.selected), len(m.fltrfiles), dot, humanize.Bytes(uint64(m.selectsize)))
|
|
|
|
case m.mode == modes.Listing:
|
|
|
|
var filtered string
|
|
|
|
if m.filter != "" || m.filtering {
|
|
|
|
filtered = " (filtered)"
|
|
|
|
}
|
|
|
|
right = fmt.Sprintf(" Showing%s %d files in trash", filtered, len(m.fltrfiles))
|
2024-06-19 21:24:57 -04:00
|
|
|
default:
|
2024-07-31 00:28:09 -04:00
|
|
|
var wd string
|
2024-07-03 18:05:17 -04:00
|
|
|
if m.workdir != "" {
|
2024-07-31 00:28:09 -04:00
|
|
|
wd = " in " + dirs.UnExpand(m.workdir, "")
|
2024-06-30 23:01:36 -04:00
|
|
|
}
|
2024-07-31 00:28:09 -04:00
|
|
|
right = fmt.Sprintf(" %s%s %s %s", m.mode.String(), wd, dot, selectFmt)
|
|
|
|
left = fmt.Sprintf("%d/%d %s %s", len(m.selected), len(m.fltrfiles), dot, humanize.Bytes(uint64(m.selectsize)))
|
2024-07-30 21:32:34 -04:00
|
|
|
}
|
|
|
|
|
2024-07-28 21:18:16 -04:00
|
|
|
// offset of 2 again because of table border
|
2024-07-30 16:03:33 -04:00
|
|
|
spacerWidth = m.termwidth - lipgloss.Width(right) - lipgloss.Width(left) - poffset
|
2024-07-30 14:43:54 -04:00
|
|
|
if spacerWidth <= 0 {
|
|
|
|
spacerWidth = 1 // always at least one space
|
2024-07-28 21:18:16 -04:00
|
|
|
}
|
|
|
|
|
2024-07-30 14:43:54 -04:00
|
|
|
return fmt.Sprintf("%s%s%s", right, strings.Repeat(" ", spacerWidth), left)
|
2024-06-19 21:24:57 -04:00
|
|
|
}
|
|
|
|
|
2024-06-19 18:55:01 -04:00
|
|
|
func (m model) footer() string {
|
|
|
|
return regulartext.Render(m.showHelp())
|
|
|
|
}
|
|
|
|
|
2024-07-30 14:43:54 -04:00
|
|
|
func (m model) quit(unselectAll bool) (model, tea.Cmd) {
|
|
|
|
if unselectAll {
|
2024-06-29 22:54:15 -04:00
|
|
|
m.unselectAll()
|
2024-06-29 22:56:51 -04:00
|
|
|
} else {
|
|
|
|
m.onlySelected()
|
2024-06-19 21:24:57 -04:00
|
|
|
}
|
2024-06-19 19:36:12 -04:00
|
|
|
m.table.SetStyles(makeUnselectedStyle())
|
|
|
|
return m, tea.Quit
|
|
|
|
}
|
|
|
|
|
2024-07-29 02:31:38 -04:00
|
|
|
func (m model) execute(mode modes.Mode) (model, tea.Cmd) {
|
2024-07-30 22:44:02 -04:00
|
|
|
if m.mode != modes.Interactive || len(m.selected) <= 0 || len(m.fltrfiles) <= 0 {
|
2024-07-29 02:31:38 -04:00
|
|
|
var cmd tea.Cmd
|
|
|
|
return m, cmd
|
|
|
|
}
|
|
|
|
|
|
|
|
m.mode = mode
|
|
|
|
m.onlySelected()
|
|
|
|
m.table.SetStyles(makeUnselectedStyle())
|
|
|
|
return m, tea.Quit
|
|
|
|
}
|
|
|
|
|
2024-07-15 18:32:33 -04:00
|
|
|
func (m model) selectedFiles() (outfile files.Files) {
|
2024-07-30 22:44:02 -04:00
|
|
|
for _, file := range m.fltrfiles {
|
2024-07-15 18:32:33 -04:00
|
|
|
if m.selected[file.String()] {
|
|
|
|
outfile = append(outfile, file)
|
2024-07-03 18:05:17 -04:00
|
|
|
}
|
2024-07-15 18:32:33 -04:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-07-15 18:33:37 -04:00
|
|
|
/* func (m model) onePage() bool {
|
|
|
|
x := m.termheight
|
|
|
|
y := len(m.table.Rows()) + hoffset
|
|
|
|
if x > y && m.readonly {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
} */
|
|
|
|
|
2024-07-30 16:12:00 -04:00
|
|
|
func (m *model) freshRows() (rows []table.Row) {
|
2024-07-30 14:43:54 -04:00
|
|
|
for _, file := range m.files {
|
|
|
|
row := newRow(file, m.workdir)
|
2024-07-03 18:05:17 -04:00
|
|
|
|
|
|
|
if !m.readonly {
|
2024-07-30 16:12:00 -04:00
|
|
|
row = append(row, getCheck(false))
|
2024-07-03 18:05:17 -04:00
|
|
|
}
|
2024-07-30 14:43:54 -04:00
|
|
|
rows = append(rows, row)
|
2024-07-03 18:05:17 -04:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-06-29 22:56:51 -04:00
|
|
|
func (m *model) onlySelected() {
|
|
|
|
var rows = make([]table.Row, 0)
|
|
|
|
for _, row := range m.table.Rows() {
|
|
|
|
if row[4] == check {
|
|
|
|
rows = append(rows, row)
|
|
|
|
} else {
|
|
|
|
rows = append(rows, table.Row{})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
m.table.SetRows(rows)
|
|
|
|
}
|
|
|
|
|
2024-07-30 14:43:54 -04:00
|
|
|
// updateRow updates row of provided index with provided row.
|
2024-06-29 22:54:15 -04:00
|
|
|
func (m *model) updateRow(index int, selected bool) {
|
2024-06-19 19:36:12 -04:00
|
|
|
rows := m.table.Rows()
|
|
|
|
row := rows[index]
|
|
|
|
rows[index] = table.Row{
|
|
|
|
row[0],
|
|
|
|
row[1],
|
|
|
|
row[2],
|
|
|
|
row[3],
|
|
|
|
getCheck(selected),
|
|
|
|
}
|
|
|
|
|
|
|
|
m.table.SetRows(rows)
|
|
|
|
}
|
|
|
|
|
2024-06-29 22:54:15 -04:00
|
|
|
func (m *model) updateRows(selected bool) {
|
2024-07-30 14:43:54 -04:00
|
|
|
var newrows = []table.Row{}
|
2024-06-19 22:33:01 -04:00
|
|
|
|
|
|
|
for _, row := range m.table.Rows() {
|
2024-07-30 14:43:54 -04:00
|
|
|
newRow := table.Row{
|
2024-06-19 22:33:01 -04:00
|
|
|
row[0],
|
|
|
|
row[1],
|
|
|
|
row[2],
|
|
|
|
row[3],
|
|
|
|
getCheck(selected),
|
|
|
|
}
|
2024-07-30 14:43:54 -04:00
|
|
|
newrows = append(newrows, newRow)
|
2024-06-19 22:33:01 -04:00
|
|
|
}
|
|
|
|
m.table.SetRows(newrows)
|
|
|
|
}
|
|
|
|
|
2024-06-29 22:54:15 -04:00
|
|
|
func (m *model) toggleItem(index int) (selected bool) {
|
2024-07-30 21:48:49 -04:00
|
|
|
if m.readonly || len(m.fltrfiles) == 0 {
|
2024-06-19 18:55:01 -04:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2024-07-30 22:44:02 -04:00
|
|
|
name := m.fltrfiles[index].String()
|
|
|
|
size := m.fltrfiles[index].Filesize()
|
2024-07-15 18:32:33 -04:00
|
|
|
|
2024-06-19 18:55:01 -04:00
|
|
|
// select the thing
|
2024-07-15 18:32:33 -04:00
|
|
|
if v, ok := m.selected[name]; v && ok {
|
2024-06-19 18:55:01 -04:00
|
|
|
// already selected
|
2024-07-15 18:32:33 -04:00
|
|
|
delete(m.selected, name)
|
2024-06-19 18:55:01 -04:00
|
|
|
selected = false
|
2024-07-21 23:54:22 -04:00
|
|
|
m.selectsize -= size
|
2024-06-19 18:55:01 -04:00
|
|
|
} else {
|
|
|
|
// not selected
|
2024-07-15 18:32:33 -04:00
|
|
|
m.selected[name] = true
|
2024-06-19 18:55:01 -04:00
|
|
|
selected = true
|
2024-07-21 23:54:22 -04:00
|
|
|
m.selectsize += size
|
2024-06-19 18:55:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// update the rows with the state
|
2024-06-29 22:54:15 -04:00
|
|
|
m.updateRow(index, selected)
|
2024-06-19 19:36:12 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-06-29 22:54:15 -04:00
|
|
|
func (m *model) selectAll() {
|
2024-07-30 21:48:49 -04:00
|
|
|
if m.readonly || len(m.fltrfiles) == 0 {
|
2024-06-19 19:36:12 -04:00
|
|
|
return
|
2024-06-19 18:55:01 -04:00
|
|
|
}
|
|
|
|
|
2024-07-15 18:32:33 -04:00
|
|
|
m.selected = map[string]bool{}
|
2024-07-21 23:54:22 -04:00
|
|
|
m.selectsize = 0
|
2024-07-15 20:55:51 -04:00
|
|
|
for i := range m.table.Rows() {
|
2024-07-30 21:32:34 -04:00
|
|
|
m.selected[m.fltrfiles[i].String()] = true
|
|
|
|
m.selectsize += m.fltrfiles[i].Filesize()
|
2024-06-19 19:36:12 -04:00
|
|
|
}
|
2024-06-29 22:54:15 -04:00
|
|
|
m.updateRows(true)
|
2024-06-19 19:36:12 -04:00
|
|
|
}
|
2024-06-19 18:55:01 -04:00
|
|
|
|
2024-06-29 22:54:15 -04:00
|
|
|
func (m *model) unselectAll() {
|
2024-06-19 19:36:12 -04:00
|
|
|
if m.readonly {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-07-15 18:32:33 -04:00
|
|
|
m.selected = map[string]bool{}
|
2024-07-21 23:54:22 -04:00
|
|
|
m.selectsize = 0
|
2024-06-29 22:54:15 -04:00
|
|
|
m.updateRows(false)
|
2024-06-19 19:36:12 -04:00
|
|
|
}
|
|
|
|
|
2024-06-29 22:54:15 -04:00
|
|
|
func (m *model) invertSelection() {
|
2024-07-30 21:48:49 -04:00
|
|
|
if m.readonly || len(m.fltrfiles) == 0 {
|
2024-07-15 18:32:33 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-06-19 22:33:01 -04:00
|
|
|
var newrows []table.Row
|
|
|
|
|
|
|
|
for index, row := range m.table.Rows() {
|
2024-07-30 22:44:02 -04:00
|
|
|
name := m.fltrfiles[index].String()
|
|
|
|
size := m.fltrfiles[index].Filesize()
|
2024-07-15 18:32:33 -04:00
|
|
|
if v, ok := m.selected[name]; v && ok {
|
|
|
|
delete(m.selected, name)
|
2024-07-21 23:54:22 -04:00
|
|
|
m.selectsize -= size
|
2024-06-19 22:33:01 -04:00
|
|
|
newrows = append(newrows, table.Row{
|
|
|
|
row[0],
|
|
|
|
row[1],
|
|
|
|
row[2],
|
|
|
|
row[3],
|
|
|
|
getCheck(false),
|
|
|
|
})
|
|
|
|
} else {
|
2024-07-15 18:32:33 -04:00
|
|
|
m.selected[name] = true
|
2024-07-21 23:54:22 -04:00
|
|
|
m.selectsize += size
|
2024-06-19 22:33:01 -04:00
|
|
|
newrows = append(newrows, table.Row{
|
|
|
|
row[0],
|
|
|
|
row[1],
|
|
|
|
row[2],
|
|
|
|
row[3],
|
|
|
|
getCheck(true),
|
|
|
|
})
|
|
|
|
}
|
2024-06-19 19:36:12 -04:00
|
|
|
}
|
2024-06-19 22:33:01 -04:00
|
|
|
|
|
|
|
m.table.SetRows(newrows)
|
2024-06-19 18:55:01 -04:00
|
|
|
}
|
|
|
|
|
2024-07-03 18:05:17 -04:00
|
|
|
func (m *model) sort() {
|
|
|
|
slices.SortStableFunc(m.files, m.sorting.Sorter())
|
2024-07-30 21:32:34 -04:00
|
|
|
m.applyFilter()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *model) applyFilter() {
|
|
|
|
m.fltrfiles = m.filteredFiles()
|
2024-07-30 14:43:54 -04:00
|
|
|
var rows = []table.Row{}
|
2024-07-30 21:32:34 -04:00
|
|
|
for _, file := range m.fltrfiles {
|
2024-07-30 22:44:02 -04:00
|
|
|
row := newRow(file, m.workdir)
|
2024-07-15 18:32:33 -04:00
|
|
|
if !m.readonly {
|
2024-07-30 22:44:02 -04:00
|
|
|
row = append(row, getCheck(m.selected[file.String()]))
|
2024-07-15 18:32:33 -04:00
|
|
|
}
|
2024-07-30 22:44:02 -04:00
|
|
|
rows = append(rows, row)
|
2024-07-15 18:32:33 -04:00
|
|
|
}
|
2024-07-03 18:05:17 -04:00
|
|
|
|
2024-07-30 22:44:02 -04:00
|
|
|
if len(rows) < 1 {
|
|
|
|
row := table.Row{"no files matched filter!", bar, bar, bar}
|
|
|
|
if !m.readonly {
|
|
|
|
row = append(row, uncheck)
|
|
|
|
}
|
|
|
|
rows = append(rows, row)
|
2024-07-30 21:48:49 -04:00
|
|
|
}
|
2024-07-15 18:32:33 -04:00
|
|
|
m.table.SetRows(rows)
|
2024-07-30 21:32:34 -04:00
|
|
|
m.updateTableHeight()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *model) filteredFiles() (filteredFiles files.Files) {
|
|
|
|
for _, file := range m.files {
|
2024-07-30 21:53:44 -04:00
|
|
|
if isMatch(m.filter, file.Name()) {
|
2024-07-30 21:32:34 -04:00
|
|
|
filteredFiles = append(filteredFiles, file)
|
|
|
|
} else {
|
|
|
|
if _, ok := m.selected[file.String()]; ok {
|
|
|
|
delete(m.selected, file.String())
|
|
|
|
m.selectsize -= file.Filesize()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-07-31 03:53:02 -04:00
|
|
|
func (m *model) freshColumns() []table.Column {
|
|
|
|
var (
|
|
|
|
fwidth = int(math.Round(float64(m.termwidth-woffset) * filenameColumnW))
|
|
|
|
owidth = int(math.Round(float64(m.termwidth-woffset) * pathColumnW))
|
|
|
|
dwidth = int(math.Round(float64(m.termwidth-woffset) * dateColumnW))
|
|
|
|
swidth = int(math.Round(float64(m.termwidth-woffset) * sizeColumnW))
|
|
|
|
cwidth = int(math.Round(float64(m.termwidth-woffset) * checkColumnW))
|
|
|
|
datecolumn string
|
|
|
|
)
|
|
|
|
|
|
|
|
switch m.mode {
|
|
|
|
case modes.Trashing:
|
|
|
|
datecolumn = modifiedColumn
|
|
|
|
default:
|
|
|
|
datecolumn = trashedColumn
|
|
|
|
}
|
|
|
|
|
|
|
|
columns := []table.Column{
|
|
|
|
{Title: filenameColumn, Width: fwidth},
|
|
|
|
{Title: pathColumn, Width: owidth},
|
|
|
|
{Title: datecolumn, Width: dwidth},
|
|
|
|
{Title: sizeColumn, Width: swidth},
|
|
|
|
}
|
|
|
|
|
|
|
|
if !m.readonly {
|
|
|
|
columns = append(columns, table.Column{Title: uncheck, Width: cwidth})
|
|
|
|
} else {
|
|
|
|
columns[0].Width += cwidth
|
|
|
|
}
|
|
|
|
|
|
|
|
return columns
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *model) updateTableSize() {
|
|
|
|
width, height := termSizes()
|
|
|
|
m.termheight = height
|
|
|
|
m.termwidth = width - poffset
|
|
|
|
m.table.SetWidth(m.termwidth)
|
|
|
|
m.updateTableHeight()
|
|
|
|
m.table.SetColumns(m.freshColumns())
|
|
|
|
}
|
|
|
|
|
2024-07-30 21:32:34 -04:00
|
|
|
func (m *model) updateTableHeight() {
|
|
|
|
h := min(m.termheight-hoffset, len(m.table.Rows()))
|
|
|
|
m.table.SetHeight(h)
|
|
|
|
if m.table.Cursor() >= h {
|
|
|
|
m.table.SetCursor(h - 1)
|
|
|
|
}
|
2024-07-15 18:32:33 -04:00
|
|
|
}
|
2024-06-20 00:17:55 -04:00
|
|
|
|
2024-07-31 03:53:02 -04:00
|
|
|
func Select(fls files.Files, selectall, once bool, workdir string, mode modes.Mode) (files.Files, modes.Mode, error) {
|
|
|
|
mdl := newModel(fls, selectall, false, once, workdir, mode)
|
2024-07-15 18:32:33 -04:00
|
|
|
endmodel, err := tea.NewProgram(mdl).Run()
|
|
|
|
if err != nil {
|
2024-07-30 14:43:54 -04:00
|
|
|
return fls, 0, err
|
2024-07-15 18:32:33 -04:00
|
|
|
}
|
|
|
|
m, ok := endmodel.(model)
|
|
|
|
if !ok {
|
2024-07-30 14:43:54 -04:00
|
|
|
return fls, 0, fmt.Errorf("model isn't the right type?? what has happened")
|
2024-06-19 18:55:01 -04:00
|
|
|
}
|
2024-07-15 18:32:33 -04:00
|
|
|
return m.selectedFiles(), m.mode, nil
|
2024-06-19 18:55:01 -04:00
|
|
|
}
|
|
|
|
|
2024-07-31 03:53:02 -04:00
|
|
|
func Show(fls files.Files, once bool, workdir string) error {
|
|
|
|
mdl := newModel(fls, false, true, once, workdir, modes.Listing)
|
2024-07-30 22:42:59 -04:00
|
|
|
if _, err := tea.NewProgram(mdl).Run(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-07-28 21:07:07 -04:00
|
|
|
func newRow(file files.File, workdir string) table.Row {
|
2024-07-30 21:48:49 -04:00
|
|
|
var time, size string
|
2024-07-30 14:43:54 -04:00
|
|
|
time = humanize.Time(file.Date())
|
2024-07-28 21:07:07 -04:00
|
|
|
if file.IsDir() {
|
2024-07-30 21:48:49 -04:00
|
|
|
size = bar
|
2024-07-28 21:07:07 -04:00
|
|
|
} else {
|
2024-07-30 21:48:49 -04:00
|
|
|
size = humanize.Bytes(uint64(file.Filesize()))
|
2024-07-28 21:07:07 -04:00
|
|
|
}
|
|
|
|
return table.Row{
|
2024-08-06 02:09:00 -04:00
|
|
|
dirs.PercentDecode(file.Name()),
|
2024-07-28 21:07:07 -04:00
|
|
|
dirs.UnExpand(filepath.Dir(file.Path()), workdir),
|
2024-07-30 14:43:54 -04:00
|
|
|
time,
|
2024-07-30 21:48:49 -04:00
|
|
|
size,
|
2024-07-28 21:07:07 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-19 18:55:01 -04:00
|
|
|
func getCheck(selected bool) (ourcheck string) {
|
|
|
|
if selected {
|
|
|
|
ourcheck = check
|
|
|
|
} else {
|
|
|
|
ourcheck = uncheck
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-07-15 18:32:33 -04:00
|
|
|
func createTable(columns []table.Column, rows []table.Row, height int) table.Model {
|
2024-07-30 14:43:54 -04:00
|
|
|
tbl := table.New(
|
2024-06-19 18:55:01 -04:00
|
|
|
table.WithColumns(columns),
|
|
|
|
table.WithRows(rows),
|
|
|
|
table.WithFocused(true),
|
|
|
|
table.WithHeight(height),
|
|
|
|
)
|
2024-07-30 14:43:54 -04:00
|
|
|
tbl.KeyMap = fixTableKeymap()
|
|
|
|
tbl.SetStyles(makeStyle())
|
|
|
|
return tbl
|
2024-06-19 18:55:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func fixTableKeymap() table.KeyMap {
|
2024-07-30 16:12:00 -04:00
|
|
|
keys := table.DefaultKeyMap()
|
2024-06-19 18:55:01 -04:00
|
|
|
|
|
|
|
// remove spacebar from default page down keybind, but keep the rest
|
2024-07-30 16:12:00 -04:00
|
|
|
keys.PageDown.SetKeys(
|
|
|
|
slices.DeleteFunc(keys.PageDown.Keys(), func(s string) bool {
|
2024-06-19 18:55:01 -04:00
|
|
|
return s == space
|
|
|
|
})...,
|
|
|
|
)
|
|
|
|
|
2024-07-30 16:12:00 -04:00
|
|
|
return keys
|
2024-06-19 18:55:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func makeStyle() table.Styles {
|
2024-07-30 14:43:54 -04:00
|
|
|
style := table.DefaultStyles()
|
|
|
|
style.Header = style.Header.
|
2024-06-19 18:55:01 -04:00
|
|
|
BorderStyle(lipgloss.NormalBorder()).
|
|
|
|
BorderForeground(lipgloss.Color(black)).
|
|
|
|
BorderBottom(true).
|
|
|
|
Bold(false)
|
2024-07-30 14:43:54 -04:00
|
|
|
style.Selected = style.Selected.
|
2024-06-19 18:55:01 -04:00
|
|
|
Foreground(lipgloss.Color(white)).
|
|
|
|
Background(lipgloss.Color(hoveritembg)).
|
|
|
|
Bold(false)
|
|
|
|
|
2024-07-30 14:43:54 -04:00
|
|
|
return style
|
2024-06-19 18:55:01 -04:00
|
|
|
}
|
2024-06-19 19:36:12 -04:00
|
|
|
|
|
|
|
func makeUnselectedStyle() table.Styles {
|
|
|
|
style := makeStyle()
|
|
|
|
style.Selected = style.Selected.
|
|
|
|
Foreground(lipgloss.NoColor{}).
|
|
|
|
Background(lipgloss.NoColor{}).
|
|
|
|
Bold(false)
|
|
|
|
return style
|
|
|
|
}
|
2024-07-30 21:28:14 -04:00
|
|
|
|
|
|
|
func styleKey(key key.Binding) string {
|
|
|
|
return fmt.Sprintf("%s %s", darktext.Render(key.Help().Key), darkertext.Render(key.Help().Desc))
|
|
|
|
}
|
2024-07-30 21:53:44 -04:00
|
|
|
|
|
|
|
func isMatch(pattern, filename string) bool {
|
|
|
|
p := strings.ToLower(pattern)
|
|
|
|
f := strings.ToLower(filename)
|
|
|
|
return fuzzy.Match(p, f)
|
|
|
|
}
|
2024-07-31 03:53:02 -04:00
|
|
|
|
|
|
|
func termSizes() (width int, height int) {
|
|
|
|
// read the term height and width for tables
|
|
|
|
var err error
|
|
|
|
width, height, err = term.GetSize(int(os.Stdout.Fd()))
|
|
|
|
if err != nil {
|
|
|
|
width = 80
|
|
|
|
height = 24
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|