diff --git a/internal/modes/modes.go b/internal/modes/modes.go new file mode 100644 index 0000000..f79679a --- /dev/null +++ b/internal/modes/modes.go @@ -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" + } +} diff --git a/internal/tables/tables.go b/internal/tables/tables.go index e235245..ce55435 100644 --- a/internal/tables/tables.go +++ b/internal/tables/tables.go @@ -9,6 +9,7 @@ import ( "git.burning.moe/celediel/gt/internal/dirs" "git.burning.moe/celediel/gt/internal/files" + "git.burning.moe/celediel/gt/internal/modes" "git.burning.moe/celediel/gt/internal/trash" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/table" @@ -22,7 +23,7 @@ const ( check string = "☑" space string = " " woffset int = 13 // why this number, I don't know - hoffset int = 5 + hoffset int = 6 // TODO: make these configurable or something borderbg string = "5" @@ -53,11 +54,12 @@ type model struct { selected []int readonly bool termheight int + mode modes.Mode } // 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 ( fwidth int = int(math.Round(float64(width-woffset) * 0.4)) 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(), readonly: readonly, termheight: height, + mode: mode, } ) slices.SortStableFunc(is, trash.SortByTrashedReverse) @@ -128,6 +131,7 @@ func newFilesModel(fs files.Files, width, height int, readonly, preselected bool m = model{ keys: defaultKeyMap(), readonly: readonly, + mode: modes.Trashing, } ) @@ -181,6 +185,8 @@ type keyMap struct { todo key.Binding nada key.Binding invr key.Binding + rstr key.Binding + clen key.Binding quit key.Binding } @@ -206,6 +212,14 @@ func defaultKeyMap() keyMap { key.WithKeys("i", "ctrl+i"), 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( key.WithKeys("q", "ctrl+c"), 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): m.toggle_item(m.table.Cursor()) case key.Matches(msg, m.keys.doit): - if !m.readonly { + if !m.readonly && m.mode != modes.Interactive { return m, tea.Quit } 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() case key.Matches(msg, m.keys.invr): 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): - return m.quit() + return m.quit(true) } } @@ -262,6 +282,10 @@ func (m model) View() (out string) { panels = append(panels, m.footer()) } + if m.mode != modes.Listing { + panels = append([]string{m.header()}, panels...) + } + out = lipgloss.JoinVertical(lipgloss.Top, panels...) 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)), } if !m.readonly { + if m.mode != modes.Interactive { + keys = append([]string{ + fmt.Sprintf("%s %s", darktext.Render(m.keys.doit.Help().Key), darkertext.Render(m.keys.doit.Help().Desc)), + }, keys...) + } 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)), }, keys...) } 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 { return regulartext.Render(m.showHelp()) } -func (m model) quit() (model, tea.Cmd) { - m.unselect_all() +func (m model) quit(unselect_all bool) (model, tea.Cmd) { + if unselect_all { + m.unselect_all() + } m.table.SetStyles(makeUnselectedStyle()) return m, tea.Quit } // 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() row := rows[index] rows[index] = table.Row{ @@ -307,7 +356,6 @@ func (m *model) update_row(index int, selected bool /* , row table.Row */) { } m.table.SetRows(rows) - } // 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) { - if endmodel, err := tea.NewProgram(newInfosModel(is, width, height, readonly, preselected)).Run(); err != nil { - return []int{}, err +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, mode)).Run(); err != nil { + return []int{}, 0, err } else { m, ok := endmodel.(model) if ok { - return m.selected, nil + return m.selected, m.mode, nil } else { - return []int{}, fmt.Errorf("model isn't the right type??") + return []int{}, 0, fmt.Errorf("model isn't the right type??") } } } diff --git a/main.go b/main.go index 1252cc2..d32e9d5 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "git.burning.moe/celediel/gt/internal/files" "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/trash" @@ -23,6 +24,8 @@ const ( appname string = "gt" appdesc string = "xdg trash cli" appversion string = "v0.0.1" + yes rune = 'y' + no rune = 'n' ) var ( @@ -103,59 +106,29 @@ var ( return nil } - if confirm(fmt.Sprintf("trash %d selected files?", len(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 + return confirm_trash(selected) }, } + // action launches interactive mode if run without args, or trashes files as args action = func(ctx *cli.Context) error { var ( - fls trash.Infos - indicies []int - err error + err error ) 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 { return err } - fls, err = trash.FindFiles(trashDir, ogdir, f) - if err != nil { - return err + if len(ctx.Args().Slice()) != 0 { + f.AddFileNames(ctx.Args().Slice()...) + 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{ @@ -185,7 +158,7 @@ var ( } // display them - _, err = tables.InfoTable(fls, termwidth, termheight, true, false) + _, _, err = tables.InfoTable(fls, termwidth, termheight, true, false, modes.Listing) return err }, @@ -209,7 +182,7 @@ var ( 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 { return err } @@ -223,18 +196,7 @@ var ( return nil } - if confirm(fmt.Sprintf("restore %d selected files?", len(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 + return confirm_restore(selected) }, } @@ -253,7 +215,7 @@ var ( 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 { return err } @@ -267,18 +229,7 @@ var ( return nil } - if confirm(fmt.Sprintf("remove %d selected files permanently from the trash?", len(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 + return confirm_clean(selected) }, } @@ -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 { // TODO: handle errors better // 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 b := make([]byte, 1)