gt/main.go

497 lines
13 KiB
Go
Raw Normal View History

2024-07-31 03:13:48 -04:00
// Package main does the thing
2024-06-18 18:21:03 -04:00
package main
import (
"fmt"
2024-06-20 14:38:00 -04:00
"io/fs"
2024-06-18 18:21:03 -04:00
"os"
"path/filepath"
"slices"
"time"
"git.burning.moe/celediel/gt/internal/filemode"
2024-06-18 18:21:03 -04:00
"git.burning.moe/celediel/gt/internal/files"
"git.burning.moe/celediel/gt/internal/filter"
2024-07-28 21:06:41 -04:00
"git.burning.moe/celediel/gt/internal/interactive"
"git.burning.moe/celediel/gt/internal/interactive/modes"
2024-06-18 18:21:03 -04:00
"github.com/adrg/xdg"
"github.com/charmbracelet/log"
"github.com/urfave/cli/v2"
)
const (
appname string = "gt"
2024-07-30 16:18:15 -04:00
appsubtitle string = "xdg trash cli"
appversion string = "v0.0.1"
2024-07-30 16:18:15 -04:00
appdesc string = `A small command line program to interface with the
Freedesktop.org / XDG trash specification.
Run with no command or filename(s) to start interactive mode.
See gt(1) for more information.`
executePerm = fs.FileMode(0755)
2024-06-18 18:21:03 -04:00
)
var (
2024-07-28 22:02:11 -04:00
loglvl string
fltr *filter.Filter
onArg, beforeArg, afterArg string
globArg, patternArg string
unGlobArg, unPatternArg string
modeArg, minArg, maxArg string
filesOnlyArg, dirsOnlyArg bool
hiddenArg, noInterArg bool
askconfirm, all bool
workdir, ogdir cli.Path
recursive bool
2024-06-18 18:21:03 -04:00
trashDir = filepath.Join(xdg.DataHome, "Trash")
beforeAll = func(_ *cli.Context) error {
2024-06-18 18:21:03 -04:00
// setup log
log.SetReportTimestamp(true)
log.SetTimeFormat(time.TimeOnly)
if level, err := log.ParseLevel(loglvl); err == nil {
log.SetLevel(level)
2024-06-18 18:21:03 -04:00
// Some extra info for debug level
if log.GetLevel() == log.DebugLevel {
log.SetReportCaller(true)
}
} else {
log.Errorf("unknown log level '%s' (possible values: debug, info, warn, error, fatal, default: warn)", loglvl)
2024-06-18 18:21:03 -04:00
}
2024-06-20 14:38:00 -04:00
// ensure trash directories exist
if _, e := os.Stat(trashDir); os.IsNotExist(e) {
if err := os.Mkdir(trashDir, executePerm); err != nil {
2024-06-20 14:38:00 -04:00
return err
}
}
if _, e := os.Stat(filepath.Join(trashDir, "info")); os.IsNotExist(e) {
if err := os.Mkdir(filepath.Join(trashDir, "info"), executePerm); err != nil {
2024-06-20 14:38:00 -04:00
return err
}
}
if _, e := os.Stat(filepath.Join(trashDir, "files")); os.IsNotExist(e) {
if err := os.Mkdir(filepath.Join(trashDir, "files"), executePerm); err != nil {
2024-06-20 14:38:00 -04:00
return err
}
}
return nil
2024-06-18 18:21:03 -04:00
}
// action launches interactive mode if run without args, or trashes files as args.
2024-06-25 00:54:46 -04:00
action = func(ctx *cli.Context) error {
var (
err error
)
2024-07-28 22:02:11 -04:00
if fltr == nil {
md, e := filemode.Parse(modeArg)
2024-07-16 00:05:21 -04:00
if e != nil {
return e
}
2024-07-28 22:02:11 -04:00
fltr, err = filter.New(onArg, beforeArg, afterArg, globArg, patternArg, unGlobArg, unPatternArg, filesOnlyArg, dirsOnlyArg, false, minArg, maxArg, md)
2024-06-25 00:54:46 -04:00
}
if err != nil {
return err
}
if len(ctx.Args().Slice()) == 0 {
// no ags, so do interactive mode
var (
infiles files.Files
selected files.Files
mode modes.Mode
err error
)
2024-07-28 22:02:11 -04:00
infiles, err = files.FindTrash(trashDir, ogdir, fltr)
if err != nil {
return err
}
if len(infiles) <= 0 {
var msg string
2024-07-28 22:02:11 -04:00
if fltr.Blank() {
msg = "trash is empty"
} else {
msg = "no files to show"
}
2024-07-30 16:03:58 -04:00
fmt.Fprintln(os.Stdout, msg)
return nil
}
2024-07-31 03:53:02 -04:00
selected, mode, err = interactive.Select(infiles, false, false, workdir, modes.Interactive)
if err != nil {
return err
}
switch mode {
case modes.Cleaning:
for _, file := range selected {
log.Debugf("gonna clean %s", file.Name())
}
if err := files.ConfirmClean(askconfirm, selected); err != nil {
return err
}
case modes.Restoring:
for _, file := range selected {
log.Debugf("gonna restore %s", file.Name())
}
if err := files.ConfirmRestore(askconfirm, selected); err != nil {
return err
}
case modes.Interactive:
return nil
default:
return fmt.Errorf("got bad mode %s", mode)
}
return nil
2024-06-25 00:54:46 -04:00
}
// args, so try to trash files
var filesToTrash files.Files
for _, arg := range ctx.Args().Slice() {
file, e := files.NewDisk(arg)
if e != nil {
log.Errorf("cannot trash '%s': No such file or directory", arg)
continue
}
filesToTrash = append(filesToTrash, file)
}
return files.ConfirmTrash(askconfirm, filesToTrash, trashDir)
2024-06-25 00:54:46 -04:00
}
beforeCommands = func(ctx *cli.Context) (err error) {
2024-06-18 18:21:03 -04:00
// setup filter
2024-07-28 22:02:11 -04:00
if fltr == nil {
md, e := filemode.Parse(modeArg)
2024-07-16 00:05:21 -04:00
if e != nil {
return e
}
2024-07-28 22:02:11 -04:00
fltr, err = filter.New(onArg, beforeArg, afterArg, globArg, patternArg, unGlobArg, unPatternArg, filesOnlyArg, dirsOnlyArg, false, minArg, maxArg, md, ctx.Args().Slice()...)
2024-06-18 18:21:03 -04:00
}
2024-07-28 22:02:11 -04:00
log.Debugf("filter: %s", fltr.String())
2024-06-18 18:21:03 -04:00
return
}
2024-06-25 00:54:46 -04:00
beforeTrash = func(_ *cli.Context) (err error) {
2024-07-28 22:02:11 -04:00
if fltr == nil {
md, e := filemode.Parse(modeArg)
2024-07-16 00:05:21 -04:00
if e != nil {
return e
}
2024-07-28 22:02:11 -04:00
fltr, err = filter.New(onArg, beforeArg, afterArg, globArg, patternArg, unGlobArg, unPatternArg, filesOnlyArg, dirsOnlyArg, !hiddenArg, minArg, maxArg, md)
}
2024-07-28 22:02:11 -04:00
log.Debugf("filter: %s", fltr.String())
return
}
after = func(_ *cli.Context) error {
2024-06-18 18:21:03 -04:00
return nil
}
2024-06-25 00:54:46 -04:00
doTrash = &cli.Command{
2024-07-30 17:31:45 -04:00
Name: "trash",
Aliases: []string{"tr"},
Usage: "Trash a file or files",
UsageText: "[command options] [filename(s)]",
Flags: slices.Concat(trashingFlags, filterFlags),
Before: beforeTrash,
2024-06-18 18:21:03 -04:00
Action: func(ctx *cli.Context) error {
var filesToTrash files.Files
for _, arg := range ctx.Args().Slice() {
file, e := files.NewDisk(arg)
if e != nil || workdir != "" {
log.Debugf("%s wasn't really a file", arg)
2024-07-28 22:02:11 -04:00
fltr.AddFileName(arg)
continue
}
filesToTrash = append(filesToTrash, file)
2024-06-18 18:21:03 -04:00
}
2024-07-30 22:42:59 -04:00
// if none of the args were files, then find files based on filter
if len(filesToTrash) == 0 {
2024-07-28 22:02:11 -04:00
fls, err := files.FindDisk(workdir, recursive, fltr)
if err != nil {
return err
}
if len(fls) == 0 {
2024-07-30 16:03:58 -04:00
fmt.Fprintln(os.Stdout, "no files to trash")
return nil
}
filesToTrash = append(filesToTrash, fls...)
2024-06-18 18:21:03 -04:00
}
2024-07-31 03:53:02 -04:00
selected, _, err := interactive.Select(filesToTrash, false, false, workdir, modes.Trashing)
if err != nil {
return err
}
if len(selected) <= 0 {
return nil
}
return files.ConfirmTrash(askconfirm, selected, trashDir)
2024-06-18 18:21:03 -04:00
},
}
2024-06-25 00:54:46 -04:00
doList = &cli.Command{
2024-06-18 18:21:03 -04:00
Name: "list",
Aliases: []string{"ls"},
2024-06-20 00:54:50 -04:00
Usage: "List trashed files",
2024-07-30 16:18:15 -04:00
Flags: slices.Concat(listFlags, trashedFlags, filterFlags),
2024-06-25 00:54:46 -04:00
Before: beforeCommands,
Action: func(_ *cli.Context) error {
2024-06-18 18:21:03 -04:00
log.Debugf("searching in directory %s for files", trashDir)
2024-07-28 22:02:11 -04:00
fls, err := files.FindTrash(trashDir, ogdir, fltr)
2024-06-18 18:21:03 -04:00
var msg string
log.Debugf("filter '%s' is blank? %t in %s", fltr, fltr.Blank(), ogdir)
2024-07-28 22:02:11 -04:00
if fltr.Blank() && ogdir == "" {
2024-06-18 18:21:03 -04:00
msg = "trash is empty"
} else {
msg = "no files to show"
}
if len(fls) == 0 {
2024-07-30 16:03:58 -04:00
fmt.Fprintln(os.Stdout, msg)
2024-06-18 18:21:03 -04:00
return nil
} else if err != nil {
return err
}
2024-07-31 03:53:02 -04:00
return interactive.Show(fls, noInterArg, workdir)
2024-06-18 18:21:03 -04:00
},
}
2024-06-25 00:54:46 -04:00
doRestore = &cli.Command{
2024-07-30 17:31:45 -04:00
Name: "restore",
Aliases: []string{"re"},
Usage: "Restore a trashed file or files",
UsageText: "[command options] [filename(s)]",
Flags: slices.Concat(cleanRestoreFlags, trashedFlags, filterFlags),
Before: beforeCommands,
Action: func(_ *cli.Context) error {
2024-06-18 18:21:03 -04:00
log.Debugf("searching in directory %s for files", trashDir)
2024-07-28 22:02:11 -04:00
fls, err := files.FindTrash(trashDir, ogdir, fltr)
if len(fls) == 0 {
2024-07-30 16:03:58 -04:00
fmt.Fprintln(os.Stdout, "no files to restore")
2024-06-18 18:21:03 -04:00
return nil
} else if err != nil {
return err
}
2024-07-31 03:53:02 -04:00
selected, _, err := interactive.Select(fls, all, all, workdir, modes.Restoring)
if err != nil {
return err
}
if len(selected) <= 0 {
return nil
}
return files.ConfirmRestore(askconfirm || all, selected)
2024-06-18 18:21:03 -04:00
},
}
2024-06-25 00:54:46 -04:00
doClean = &cli.Command{
2024-07-30 17:31:45 -04:00
Name: "clean",
Aliases: []string{"cl"},
Usage: "Clean files from trash",
UsageText: "[command options] [filename(s)]",
Flags: slices.Concat(cleanRestoreFlags, trashedFlags, filterFlags),
Before: beforeCommands,
Action: func(_ *cli.Context) error {
2024-07-28 22:02:11 -04:00
fls, err := files.FindTrash(trashDir, ogdir, fltr)
if len(fls) == 0 {
2024-07-30 16:03:58 -04:00
fmt.Fprintln(os.Stdout, "no files to clean")
2024-06-18 18:21:03 -04:00
return nil
} else if err != nil {
return err
}
2024-07-31 03:53:02 -04:00
selected, _, err := interactive.Select(fls, all, all, workdir, modes.Cleaning)
if err != nil {
return err
}
if len(selected) <= 0 {
return nil
}
return files.ConfirmClean(askconfirm, selected)
2024-06-18 18:21:03 -04:00
},
}
2024-06-25 00:54:46 -04:00
globalFlags = []cli.Flag{
2024-06-18 18:21:03 -04:00
&cli.StringFlag{
Name: "log",
2024-07-30 16:18:15 -04:00
Usage: "set log level",
2024-06-18 18:21:03 -04:00
Value: "warn",
Aliases: []string{"l"},
Destination: &loglvl,
},
&cli.BoolFlag{
Name: "confirm",
2024-06-20 00:59:04 -04:00
Usage: "ask for confirmation before executing any action",
Value: false,
Aliases: []string{"c"},
DisableDefaultText: true,
Destination: &askconfirm,
},
2024-06-18 18:21:03 -04:00
}
2024-06-25 00:54:46 -04:00
filterFlags = []cli.Flag{
2024-06-18 18:21:03 -04:00
&cli.StringFlag{
Name: "match",
2024-06-20 00:59:04 -04:00
Usage: "operate on files matching regex `PATTERN`",
2024-06-18 18:21:03 -04:00
Aliases: []string{"m"},
2024-07-28 22:02:11 -04:00
Destination: &patternArg,
2024-06-18 18:21:03 -04:00
},
&cli.StringFlag{
Name: "glob",
2024-06-20 00:59:04 -04:00
Usage: "operate on files matching `GLOB`",
2024-06-18 18:21:03 -04:00
Aliases: []string{"g"},
2024-07-28 22:02:11 -04:00
Destination: &globArg,
2024-06-18 18:21:03 -04:00
},
2024-06-18 18:56:55 -04:00
&cli.StringFlag{
Name: "not-match",
2024-06-20 00:59:04 -04:00
Usage: "operate on files not matching regex `PATTERN`",
2024-06-18 18:56:55 -04:00
Aliases: []string{"M"},
2024-07-28 22:02:11 -04:00
Destination: &unPatternArg,
2024-06-18 18:56:55 -04:00
},
&cli.StringFlag{
Name: "not-glob",
2024-06-20 00:59:04 -04:00
Usage: "operate on files not matching `GLOB`",
2024-06-18 18:56:55 -04:00
Aliases: []string{"G"},
2024-07-28 22:02:11 -04:00
Destination: &unGlobArg,
2024-06-18 18:56:55 -04:00
},
2024-06-18 18:21:03 -04:00
&cli.StringFlag{
Name: "on",
2024-06-20 00:59:04 -04:00
Usage: "operate on files modified on `DATE`",
Aliases: []string{"O"},
2024-07-28 22:02:11 -04:00
Destination: &onArg,
2024-06-18 18:21:03 -04:00
},
&cli.StringFlag{
Name: "after",
2024-06-20 00:59:04 -04:00
Usage: "operate on files modified before `DATE`",
Aliases: []string{"A"},
2024-07-28 22:02:11 -04:00
Destination: &afterArg,
2024-06-18 18:21:03 -04:00
},
&cli.StringFlag{
Name: "before",
2024-06-20 00:59:04 -04:00
Usage: "operate on files modified after `DATE`",
Aliases: []string{"B"},
2024-07-28 22:02:11 -04:00
Destination: &beforeArg,
2024-06-18 18:21:03 -04:00
},
&cli.BoolFlag{
2024-06-20 00:59:04 -04:00
Name: "files-only",
Usage: "operate on files only",
Aliases: []string{"F"},
2024-06-20 00:59:04 -04:00
DisableDefaultText: true,
2024-07-28 22:02:11 -04:00
Destination: &filesOnlyArg,
},
&cli.BoolFlag{
2024-06-20 00:59:04 -04:00
Name: "dirs-only",
Usage: "operate on directories only",
Aliases: []string{"D"},
2024-06-20 00:59:04 -04:00
DisableDefaultText: true,
2024-07-28 22:02:11 -04:00
Destination: &dirsOnlyArg,
},
&cli.StringFlag{
Name: "min-size",
Usage: "operate on files larger than `SIZE`",
Aliases: []string{"N"},
2024-07-28 22:02:11 -04:00
Destination: &minArg,
},
&cli.StringFlag{
Name: "max-size",
Usage: "operate on files smaller than `SIZE`",
Aliases: []string{"X"},
2024-07-28 22:02:11 -04:00
Destination: &maxArg,
},
2024-07-16 00:05:21 -04:00
&cli.StringFlag{
Name: "mode",
Usage: "operate on files matching mode `MODE`",
Aliases: []string{"x"},
2024-07-28 22:02:11 -04:00
Destination: &modeArg,
2024-07-16 00:05:21 -04:00
},
2024-06-18 18:21:03 -04:00
}
2024-07-30 16:18:15 -04:00
trashingFlags = []cli.Flag{
2024-06-18 18:21:03 -04:00
&cli.BoolFlag{
Name: "recursive",
2024-06-20 00:59:04 -04:00
Usage: "operate on files recursively",
2024-06-18 18:21:03 -04:00
Aliases: []string{"r"},
Destination: &recursive,
Value: false,
DisableDefaultText: true,
},
&cli.PathFlag{
Name: "work-dir",
2024-06-20 00:59:04 -04:00
Usage: "operate on files in this `DIRECTORY`",
2024-06-18 18:21:03 -04:00
Aliases: []string{"w"},
Destination: &workdir,
},
&cli.BoolFlag{
Name: "hidden",
Usage: "operate on hidden files",
Aliases: []string{"H"},
DisableDefaultText: true,
2024-07-28 22:02:11 -04:00
Destination: &hiddenArg,
},
2024-06-18 18:21:03 -04:00
}
2024-07-30 16:18:15 -04:00
trashedFlags = []cli.Flag{
&cli.PathFlag{
Name: "original-path",
2024-06-20 00:59:04 -04:00
Usage: "operate on files trashed from this `DIRECTORY`",
Aliases: []string{"o"},
Destination: &ogdir,
},
}
listFlags = []cli.Flag{
&cli.BoolFlag{
2024-07-16 14:20:49 -04:00
Name: "non-interactive",
Usage: "list files and quit",
Aliases: []string{"n"},
2024-07-28 22:02:11 -04:00
Destination: &noInterArg,
2024-07-16 14:20:49 -04:00
DisableDefaultText: true,
},
}
cleanRestoreFlags = []cli.Flag{
&cli.BoolFlag{
Name: "all",
Usage: "operate on all files in trash",
Aliases: []string{"a"},
Destination: &all,
DisableDefaultText: true,
},
}
2024-06-18 18:21:03 -04:00
)
func main() {
app := &cli.App{
2024-06-20 01:44:07 -04:00
Name: appname,
2024-07-30 16:18:15 -04:00
Usage: appsubtitle,
2024-06-20 01:44:07 -04:00
Version: appversion,
2024-06-25 00:54:46 -04:00
Before: beforeAll,
2024-06-20 01:44:07 -04:00
After: after,
Action: action,
2024-06-25 00:54:46 -04:00
Commands: []*cli.Command{doTrash, doList, doRestore, doClean},
Flags: globalFlags,
2024-07-30 16:18:15 -04:00
UsageText: appname + " [global options] [command [command options] / filename(s)]",
Description: appdesc,
EnableBashCompletion: true,
2024-06-20 01:44:07 -04:00
UseShortOptionHandling: true,
2024-06-18 18:21:03 -04:00
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}