add interactive mode

when run with no args, interactive mode is launched
files in trash are listed, select files, and use r/c to restore/clean them
This commit is contained in:
Lilian Jónsdóttir 2024-06-19 18:24:57 -07:00
parent 5467d7a549
commit 2f56ecf40b
3 changed files with 208 additions and 81 deletions

28
internal/modes/modes.go Normal file
View file

@ -0,0 +1,28 @@
package modes
type Mode int
const (
Trashing Mode = iota + 1
Listing
Restoring
Cleaning
Interactive
)
func (m Mode) String() string {
switch m {
case Trashing:
return "Trashing"
case Listing:
return "Listing"
case Restoring:
return "Restoring"
case Cleaning:
return "Cleaning"
case Interactive:
return "Interactive"
default:
return "0"
}
}

View file

@ -9,6 +9,7 @@ import (
"git.burning.moe/celediel/gt/internal/dirs" "git.burning.moe/celediel/gt/internal/dirs"
"git.burning.moe/celediel/gt/internal/files" "git.burning.moe/celediel/gt/internal/files"
"git.burning.moe/celediel/gt/internal/modes"
"git.burning.moe/celediel/gt/internal/trash" "git.burning.moe/celediel/gt/internal/trash"
"github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/bubbles/table"
@ -22,7 +23,7 @@ const (
check string = "☑" check string = "☑"
space string = " " space string = " "
woffset int = 13 // why this number, I don't know woffset int = 13 // why this number, I don't know
hoffset int = 5 hoffset int = 6
// TODO: make these configurable or something // TODO: make these configurable or something
borderbg string = "5" borderbg string = "5"
@ -53,11 +54,12 @@ type model struct {
selected []int selected []int
readonly bool readonly bool
termheight int termheight int
mode modes.Mode
} }
// TODO: reconcile trash.Info and files.File into an interface so I can shorten this up // TODO: reconcile trash.Info and files.File into an interface so I can shorten this up
func newInfosModel(is trash.Infos, width, height int, readonly, preselected bool) model { func newInfosModel(is trash.Infos, width, height int, readonly, preselected bool, mode modes.Mode) model {
var ( var (
fwidth int = int(math.Round(float64(width-woffset) * 0.4)) fwidth int = int(math.Round(float64(width-woffset) * 0.4))
owidth int = int(math.Round(float64(width-woffset) * 0.2)) owidth int = int(math.Round(float64(width-woffset) * 0.2))
@ -70,6 +72,7 @@ func newInfosModel(is trash.Infos, width, height int, readonly, preselected bool
keys: defaultKeyMap(), keys: defaultKeyMap(),
readonly: readonly, readonly: readonly,
termheight: height, termheight: height,
mode: mode,
} }
) )
slices.SortStableFunc(is, trash.SortByTrashedReverse) slices.SortStableFunc(is, trash.SortByTrashedReverse)
@ -128,6 +131,7 @@ func newFilesModel(fs files.Files, width, height int, readonly, preselected bool
m = model{ m = model{
keys: defaultKeyMap(), keys: defaultKeyMap(),
readonly: readonly, readonly: readonly,
mode: modes.Trashing,
} }
) )
@ -181,6 +185,8 @@ type keyMap struct {
todo key.Binding todo key.Binding
nada key.Binding nada key.Binding
invr key.Binding invr key.Binding
rstr key.Binding
clen key.Binding
quit key.Binding quit key.Binding
} }
@ -206,6 +212,14 @@ func defaultKeyMap() keyMap {
key.WithKeys("i", "ctrl+i"), key.WithKeys("i", "ctrl+i"),
key.WithHelp("i", "invert selection"), key.WithHelp("i", "invert selection"),
), ),
clen: key.NewBinding(
key.WithKeys("c"),
key.WithHelp("c", "clean selection"),
),
rstr: key.NewBinding(
key.WithKeys("r"),
key.WithHelp("r", "restore selection"),
),
quit: key.NewBinding( quit: key.NewBinding(
key.WithKeys("q", "ctrl+c"), key.WithKeys("q", "ctrl+c"),
key.WithHelp("q", "quit"), key.WithHelp("q", "quit"),
@ -229,7 +243,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case key.Matches(msg, m.keys.mark): case key.Matches(msg, m.keys.mark):
m.toggle_item(m.table.Cursor()) m.toggle_item(m.table.Cursor())
case key.Matches(msg, m.keys.doit): case key.Matches(msg, m.keys.doit):
if !m.readonly { if !m.readonly && m.mode != modes.Interactive {
return m, tea.Quit return m, tea.Quit
} }
case key.Matches(msg, m.keys.nada): case key.Matches(msg, m.keys.nada):
@ -238,8 +252,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.select_all() m.select_all()
case key.Matches(msg, m.keys.invr): case key.Matches(msg, m.keys.invr):
m.invert_selection() m.invert_selection()
case key.Matches(msg, m.keys.clen):
m.mode = modes.Cleaning
return m.quit(false)
case key.Matches(msg, m.keys.rstr):
m.mode = modes.Restoring
return m.quit(false)
case key.Matches(msg, m.keys.quit): case key.Matches(msg, m.keys.quit):
return m.quit() return m.quit(true)
} }
} }
@ -262,6 +282,10 @@ func (m model) View() (out string) {
panels = append(panels, m.footer()) panels = append(panels, m.footer())
} }
if m.mode != modes.Listing {
panels = append([]string{m.header()}, panels...)
}
out = lipgloss.JoinVertical(lipgloss.Top, panels...) out = lipgloss.JoinVertical(lipgloss.Top, panels...)
return out + n return out + n
} }
@ -276,26 +300,51 @@ func (m model) showHelp() string {
fmt.Sprintf("%s %s", darktext.Render(m.keys.quit.Help().Key), darkertext.Render(m.keys.quit.Help().Desc)), fmt.Sprintf("%s %s", darktext.Render(m.keys.quit.Help().Key), darkertext.Render(m.keys.quit.Help().Desc)),
} }
if !m.readonly { if !m.readonly {
if m.mode != modes.Interactive {
keys = append([]string{ keys = append([]string{
fmt.Sprintf("%s %s", darktext.Render(m.keys.mark.Help().Key), darkertext.Render(m.keys.mark.Help().Desc)),
fmt.Sprintf("%s %s", darktext.Render(m.keys.doit.Help().Key), darkertext.Render(m.keys.doit.Help().Desc)), fmt.Sprintf("%s %s", darktext.Render(m.keys.doit.Help().Key), darkertext.Render(m.keys.doit.Help().Desc)),
}, keys...) }, keys...)
} }
keys = append([]string{
fmt.Sprintf("%s %s", darktext.Render(m.keys.mark.Help().Key), darkertext.Render(m.keys.mark.Help().Desc)),
}, keys...)
}
return strings.Join(keys, darkesttext.Render(" • ")) return strings.Join(keys, darkesttext.Render(" • "))
} }
func (m model) header() string {
var (
mode string
keys []string = []string{
fmt.Sprintf("%s %s", darktext.Render(m.keys.rstr.Help().Key), darkertext.Render(m.keys.rstr.Help().Desc)),
fmt.Sprintf("%s %s", darktext.Render(m.keys.clen.Help().Key), darkertext.Render(m.keys.clen.Help().Desc)),
}
)
switch m.mode {
case modes.Interactive:
mode = strings.Join(keys, darkesttext.Render(" • "))
default:
mode = m.mode.String()
}
return fmt.Sprintf("%s %s %d files selected", mode, darkesttext.Render("•"), len(m.selected))
}
func (m model) footer() string { func (m model) footer() string {
return regulartext.Render(m.showHelp()) return regulartext.Render(m.showHelp())
} }
func (m model) quit() (model, tea.Cmd) { func (m model) quit(unselect_all bool) (model, tea.Cmd) {
if unselect_all {
m.unselect_all() m.unselect_all()
}
m.table.SetStyles(makeUnselectedStyle()) m.table.SetStyles(makeUnselectedStyle())
return m, tea.Quit return m, tea.Quit
} }
// update_row updates row of `index` with `row` // update_row updates row of `index` with `row`
func (m *model) update_row(index int, selected bool /* , row table.Row */) { func (m *model) update_row(index int, selected bool) {
rows := m.table.Rows() rows := m.table.Rows()
row := rows[index] row := rows[index]
rows[index] = table.Row{ rows[index] = table.Row{
@ -307,7 +356,6 @@ func (m *model) update_row(index int, selected bool /* , row table.Row */) {
} }
m.table.SetRows(rows) m.table.SetRows(rows)
} }
// toggle_item toggles an item's selected state, and returns the state // toggle_item toggles an item's selected state, and returns the state
@ -361,15 +409,15 @@ func (m *model) invert_selection() {
} }
} }
func InfoTable(is trash.Infos, width, height int, readonly, preselected bool) ([]int, error) { func InfoTable(is trash.Infos, width, height int, readonly, preselected bool, mode modes.Mode) ([]int, modes.Mode, error) {
if endmodel, err := tea.NewProgram(newInfosModel(is, width, height, readonly, preselected)).Run(); err != nil { if endmodel, err := tea.NewProgram(newInfosModel(is, width, height, readonly, preselected, mode)).Run(); err != nil {
return []int{}, err return []int{}, 0, err
} else { } else {
m, ok := endmodel.(model) m, ok := endmodel.(model)
if ok { if ok {
return m.selected, nil return m.selected, m.mode, nil
} else { } else {
return []int{}, fmt.Errorf("model isn't the right type??") return []int{}, 0, fmt.Errorf("model isn't the right type??")
} }
} }
} }

183
main.go
View file

@ -10,6 +10,7 @@ import (
"git.burning.moe/celediel/gt/internal/files" "git.burning.moe/celediel/gt/internal/files"
"git.burning.moe/celediel/gt/internal/filter" "git.burning.moe/celediel/gt/internal/filter"
"git.burning.moe/celediel/gt/internal/modes"
"git.burning.moe/celediel/gt/internal/tables" "git.burning.moe/celediel/gt/internal/tables"
"git.burning.moe/celediel/gt/internal/trash" "git.burning.moe/celediel/gt/internal/trash"
@ -23,6 +24,8 @@ const (
appname string = "gt" appname string = "gt"
appdesc string = "xdg trash cli" appdesc string = "xdg trash cli"
appversion string = "v0.0.1" appversion string = "v0.0.1"
yes rune = 'y'
no rune = 'n'
) )
var ( var (
@ -103,59 +106,29 @@ var (
return nil return nil
} }
if confirm(fmt.Sprintf("trash %d selected files?", len(selected))) { return confirm_trash(selected)
tfs := make([]string, 0, len(selected))
for _, file := range selected {
log.Debugf("gonna trash %s", file.Filename())
tfs = append(tfs, file.Filename())
}
trashed, err := trash.TrashFiles(trashDir, tfs...)
if err != nil {
return err
}
fmt.Printf("trashed %d files\n", trashed)
} else {
fmt.Printf("not doing anything\n")
return nil
}
return nil
}, },
} }
// action launches interactive mode if run without args, or trashes files as args
action = func(ctx *cli.Context) error { action = func(ctx *cli.Context) error {
var ( var (
fls trash.Infos
indicies []int
err error err error
) )
if f == nil { if f == nil {
f, err = filter.New(o, b, a, g, p, ung, unp, ctx.Args().Slice()...) f, err = filter.New(o, b, a, g, p, ung, unp)
} }
if err != nil { if err != nil {
return err return err
} }
fls, err = trash.FindFiles(trashDir, ogdir, f) if len(ctx.Args().Slice()) != 0 {
if err != nil { f.AddFileNames(ctx.Args().Slice()...)
return err return do_trash.Action(ctx)
} else {
return interactive_mode()
} }
if len(fls) <= 0 {
log.Printf("no files to show")
return nil
}
indicies, err = tables.InfoTable(fls, termwidth, termheight, false, true)
if err != nil {
return err
}
for _, i := range indicies {
log.Printf("gonna do something with %s", fls[i].Name())
}
return nil
} }
do_list = &cli.Command{ do_list = &cli.Command{
@ -185,7 +158,7 @@ var (
} }
// display them // display them
_, err = tables.InfoTable(fls, termwidth, termheight, true, false) _, _, err = tables.InfoTable(fls, termwidth, termheight, true, false, modes.Listing)
return err return err
}, },
@ -209,7 +182,7 @@ var (
return err return err
} }
indices, err := tables.InfoTable(fls, termwidth, termheight, false, !f.Blank()) indices, _, err := tables.InfoTable(fls, termwidth, termheight, false, !f.Blank(), modes.Restoring)
if err != nil { if err != nil {
return err return err
} }
@ -223,18 +196,7 @@ var (
return nil return nil
} }
if confirm(fmt.Sprintf("restore %d selected files?", len(selected))) { return confirm_restore(selected)
log.Info("doing the thing")
restored, err := trash.Restore(selected)
if err != nil {
return fmt.Errorf("restored %d files before error %s", restored, err)
}
fmt.Printf("restored %d files\n", restored)
} else {
fmt.Printf("not doing anything\n")
}
return nil
}, },
} }
@ -253,7 +215,7 @@ var (
return err return err
} }
indices, err := tables.InfoTable(fls, termwidth, termheight, false, !f.Blank()) indices, _, err := tables.InfoTable(fls, termwidth, termheight, false, !f.Blank(), modes.Cleaning)
if err != nil { if err != nil {
return err return err
} }
@ -267,18 +229,7 @@ var (
return nil return nil
} }
if confirm(fmt.Sprintf("remove %d selected files permanently from the trash?", len(selected))) && return confirm_clean(selected)
confirm(fmt.Sprintf("really remove all these %d selected files permanently from the trash forever??", len(selected))) {
log.Info("gonna remove some files forever")
removed, err := trash.Remove(selected)
if err != nil {
return fmt.Errorf("removed %d files before error %s", removed, err)
}
fmt.Printf("removed %d files\n", removed)
} else {
fmt.Printf("not doing anything\n")
}
return nil
}, },
} }
@ -381,6 +332,106 @@ func main() {
} }
} }
func interactive_mode() error {
var (
fls trash.Infos
indicies []int
mode modes.Mode
err error
)
fls, err = trash.FindFiles(trashDir, ogdir, f)
if err != nil {
return err
}
if len(fls) <= 0 {
log.Printf("no files to show")
return nil
}
indicies, mode, err = tables.InfoTable(fls, termwidth, termheight, false, false, modes.Interactive)
if err != nil {
return err
}
var selected trash.Infos
for _, i := range indicies {
selected = append(selected, fls[i])
}
switch mode {
case modes.Cleaning:
for _, file := range selected {
log.Debugf("gonna clean %s", file.Name())
}
if err := confirm_clean(selected); err != nil {
return err
}
case modes.Restoring:
for _, file := range selected {
log.Debugf("gonna restore %s", file.Name())
}
if err := confirm_restore(selected); err != nil {
return err
}
case modes.Interactive:
return nil
default:
return fmt.Errorf("got bad mode %s", mode)
}
return nil
}
func confirm_restore(is trash.Infos) error {
if confirm(fmt.Sprintf("restore %d selected files?", len(is))) {
log.Info("doing the thing")
restored, err := trash.Restore(is)
if err != nil {
return fmt.Errorf("restored %d files before error %s", restored, err)
}
fmt.Printf("restored %d files\n", restored)
} else {
fmt.Printf("not doing anything\n")
}
return nil
}
func confirm_clean(is trash.Infos) error {
if confirm(fmt.Sprintf("remove %d selected files permanently from the trash?", len(is))) &&
confirm(fmt.Sprintf("really remove all these %d selected files permanently from the trash forever??", len(is))) {
log.Info("gonna remove some files forever")
removed, err := trash.Remove(is)
if err != nil {
return fmt.Errorf("removed %d files before error %s", removed, err)
}
fmt.Printf("removed %d files\n", removed)
} else {
fmt.Printf("not doing anything\n")
}
return nil
}
func confirm_trash(fs files.Files) error {
if confirm(fmt.Sprintf("trash %d selected files?", len(fs))) {
tfs := make([]string, 0, len(fs))
for _, file := range fs {
log.Debugf("gonna trash %s", file.Filename())
tfs = append(tfs, file.Filename())
}
trashed, err := trash.TrashFiles(trashDir, tfs...)
if err != nil {
return err
}
fmt.Printf("trashed %d files\n", trashed)
} else {
fmt.Printf("not doing anything\n")
return nil
}
return nil
}
func confirm(prompt string) bool { func confirm(prompt string) bool {
// TODO: handle errors better // TODO: handle errors better
// switch stdin into 'raw' mode // switch stdin into 'raw' mode
@ -394,7 +445,7 @@ func confirm(prompt string) bool {
} }
}() }()
fmt.Printf("%s [y/n]: ", prompt) fmt.Printf("%s [%s/%s]: ", prompt, string(yes), string(no))
// read one byte from stdin // read one byte from stdin
b := make([]byte, 1) b := make([]byte, 1)