├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ ├── ci.yml │ ├── github-actions-lint.yml │ └── release-please.yml ├── .gitignore ├── .goreleaser.yaml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cmd ├── clear.go ├── list.go ├── put.go ├── restore.go └── root.go ├── docs ├── clear.gif ├── list.gif ├── put.gif ├── restore-ui.gif └── restore.gif ├── go.mod ├── go.sum ├── internal ├── db │ └── db.go ├── trash │ └── trash.go └── util │ ├── contains.go │ ├── files.go │ ├── filter.go │ ├── some.go │ └── yesno.go ├── main.go ├── mise.toml ├── renovate.json └── tapes ├── clear.tape ├── list.tape ├── put.tape ├── restore-ui.tape └── restore.tape /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Setup the environment for the runner 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - uses: jdx/mise-action@13abe502c30c1559a5c37dff303831bab82c9402 # v2.2.3 8 | with: 9 | experimental: true 10 | - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 11 | with: 12 | path: | 13 | ~/.cache/go-build 14 | ~/go/pkg/mod 15 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 16 | restore-keys: | 17 | ${{ runner.os }}-go- 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | permissions: {} 4 | 5 | on: 6 | pull_request: 7 | push: 8 | branches: 9 | - main 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | build: 17 | timeout-minutes: 10 18 | permissions: 19 | contents: read 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | with: 24 | persist-credentials: false 25 | - uses: ./.github/actions/setup 26 | - run: go build . 27 | 28 | lint: 29 | timeout-minutes: 10 30 | permissions: 31 | contents: read 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 35 | with: 36 | persist-credentials: false 37 | - uses: ./.github/actions/setup 38 | - run: golangci-lint run ./... 39 | 40 | goreleaser: 41 | timeout-minutes: 10 42 | permissions: 43 | contents: read 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 47 | with: 48 | persist-credentials: false 49 | - uses: ./.github/actions/setup 50 | - run: goreleaser check 51 | -------------------------------------------------------------------------------- /.github/workflows/github-actions-lint.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Lint 2 | 3 | permissions: {} 4 | 5 | on: 6 | pull_request: 7 | paths: 8 | - ".github/**" 9 | push: 10 | branches: 11 | - main 12 | paths: 13 | - ".github/**" 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | actionlint: 21 | timeout-minutes: 5 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: read 25 | steps: 26 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 27 | with: 28 | persist-credentials: false 29 | - uses: koki-develop/github-actions-lint/actionlint@46a15ec95d25fd2fed6657d591e67d1c1cdae92b # v1.4.0 30 | 31 | ghalint: 32 | timeout-minutes: 5 33 | runs-on: ubuntu-latest 34 | permissions: 35 | contents: read 36 | steps: 37 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 38 | with: 39 | persist-credentials: false 40 | - uses: koki-develop/github-actions-lint/ghalint@46a15ec95d25fd2fed6657d591e67d1c1cdae92b # v1.4.0 41 | with: 42 | action-path: ./.github/actions/**/action.yml 43 | 44 | zizmor: 45 | timeout-minutes: 5 46 | runs-on: ubuntu-latest 47 | permissions: 48 | contents: read 49 | steps: 50 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 51 | with: 52 | persist-credentials: false 53 | - uses: koki-develop/github-actions-lint/zizmor@46a15ec95d25fd2fed6657d591e67d1c1cdae92b # v1.4.0 54 | with: 55 | github-token: ${{ github.token }} 56 | persona: auditor 57 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: Release Please 2 | 3 | permissions: {} 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: false 13 | 14 | jobs: 15 | release-please: 16 | timeout-minutes: 10 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: write # for create a release 20 | pull-requests: write # for open a pull request 21 | issues: write # for create labels 22 | outputs: 23 | should-release: ${{ steps.release-please.outputs.release_created }} 24 | steps: 25 | - uses: googleapis/release-please-action@a02a34c4d625f9be7cb89156071d8567266a2445 # v4.2.0 26 | id: release-please 27 | with: 28 | release-type: simple 29 | token: ${{ github.token }} 30 | 31 | release: 32 | if: ${{ needs.release-please.outputs.should-release == 'true' }} 33 | timeout-minutes: 10 34 | needs: release-please 35 | runs-on: ubuntu-latest 36 | permissions: 37 | contents: write # for upload release assets 38 | steps: 39 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 40 | with: 41 | fetch-depth: 0 42 | persist-credentials: false 43 | - uses: ./.github/actions/setup 44 | 45 | - run: goreleaser release --clean 46 | env: 47 | GITHUB_TOKEN: ${{ github.token }} 48 | TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /gotrash 2 | /dist/ 3 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | builds: 7 | - ldflags: 8 | - -s -w -X github.com/koki-develop/gotrash/cmd.version=v{{.Version}} 9 | env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - linux 13 | - windows 14 | - darwin 15 | 16 | archives: 17 | - formats: [tar.gz] 18 | name_template: >- 19 | {{ .ProjectName }}_ 20 | {{- title .Os }}_ 21 | {{- if eq .Arch "amd64" }}x86_64 22 | {{- else if eq .Arch "386" }}i386 23 | {{- else }}{{ .Arch }}{{ end }} 24 | {{- if .Arm }}v{{ .Arm }}{{ end }} 25 | format_overrides: 26 | - goos: windows 27 | formats: [zip] 28 | checksum: 29 | name_template: 'checksums.txt' 30 | snapshot: 31 | version_template: "{{ incpatch .Version }}-next" 32 | changelog: 33 | sort: asc 34 | filters: 35 | exclude: 36 | - '^docs:' 37 | - '^test:' 38 | 39 | brews: 40 | - repository: 41 | owner: koki-develop 42 | name: homebrew-tap 43 | token: "{{ .Env.TAP_GITHUB_TOKEN }}" 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.9.2](https://github.com/koki-develop/gotrash/compare/v0.9.1...v0.9.2) (2025-05-16) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * **deps:** update module github.com/google/uuid to v1.6.0 ([#54](https://github.com/koki-develop/gotrash/issues/54)) ([4a27fc3](https://github.com/koki-develop/gotrash/commit/4a27fc3af3eb9c889a2ad36a8c7bf3a9ef77e01b)) 9 | * **deps:** update module github.com/koki-develop/go-fzf to v0.15.0 ([#32](https://github.com/koki-develop/gotrash/issues/32)) ([81d7fd2](https://github.com/koki-develop/gotrash/commit/81d7fd2d6075f7d13b3dc5feb4185663f2c73888)) 10 | * **deps:** update module github.com/spf13/cobra to v1.9.1 ([#53](https://github.com/koki-develop/gotrash/issues/53)) ([c26912f](https://github.com/koki-develop/gotrash/commit/c26912fbd2ab3a4ec8169c1b8a65e3724c84d50f)) 11 | * **deps:** update module github.com/tidwall/buntdb to v1.3.2 ([#45](https://github.com/koki-develop/gotrash/issues/45)) ([c33238f](https://github.com/koki-develop/gotrash/commit/c33238f06884196b16a4eb1632dc8062efb73055)) 12 | 13 | ## [0.9.1](https://github.com/koki-develop/gotrash/compare/v0.9.0...v0.9.1) (2025-05-16) 14 | 15 | 16 | ### Miscellaneous Chores 17 | 18 | * Release ([c54c8a4](https://github.com/koki-develop/gotrash/commit/c54c8a49dcc568ee73944d714d1bdbea13d9ad97)) 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Koki Sato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gotrash 2 | 3 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/koki-develop/gotrash)](https://github.com/koki-develop/gotrash/releases/latest) 4 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/koki-develop/gotrash/ci.yml?logo=github)](https://github.com/koki-develop/gotrash/actions/workflows/ci.yml) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/koki-develop/gotrash)](https://goreportcard.com/report/github.com/koki-develop/gotrash) 6 | [![LICENSE](https://img.shields.io/github/license/koki-develop/gotrash)](./LICENSE) 7 | 8 | rm alternative written in Go. 9 | 10 | - [Installation](#installation) 11 | - [Usage](#usage) 12 | - [`put`](#gotrash-put) 13 | - [`list`](#gotrash-list) 14 | - [`restore`](#gotrash-restore) 15 | - [`clear`](#gotrash-clear) 16 | - [LICENSE](#license) 17 | 18 | ## Installation 19 | 20 | ### Homebrew 21 | 22 | ```console 23 | $ brew install koki-develop/tap/gotrash 24 | ``` 25 | 26 | ### `go install` 27 | 28 | ```console 29 | $ go install github.com/koki-develop/gotrash@latest 30 | ``` 31 | 32 | ### Releases 33 | 34 | Download the binary from the [releases page](https://github.com/koki-develop/gotrash/releases/latest). 35 | 36 | ## Usage 37 | 38 | ```console 39 | $ gotrash --help 40 | rm alternative written in Go. 41 | 42 | Usage: 43 | gotrash [command] 44 | 45 | Available Commands: 46 | clear Clear all trashed files or directories 47 | completion Generate the autocompletion script for the specified shell 48 | help Help about any command 49 | list List trashed flies or directories 50 | put Trash files or directories 51 | restore Restore trashed files or directories 52 | 53 | Flags: 54 | -h, --help help for gotrash 55 | -v, --version version for gotrash 56 | 57 | Use "gotrash [command] --help" for more information about a command. 58 | ``` 59 | 60 | ### `gotrash put` 61 | 62 | `gotrash put` trashes files or directories. 63 | 64 | ![](./docs/put.gif) 65 | 66 | Files and directories trashed by `gotrash put` are not deleted, but placed in the trash can ( `$GOTRASH_ROOT/can` ) . 67 | The `$GOTRASH_ROOT` environment variable ( default: `$HOME/.gotrash` ) can be rewritten to customize the trash can path. 68 | 69 | ### `gotrash list` 70 | 71 | Alias: `gotrash ls` 72 | 73 | Files and directories in the trash can can be viewed with `gotrash list`. 74 | 75 | ```console 76 | $ gotrash list 77 | ``` 78 | 79 | ![](./docs/list.gif) 80 | 81 | ### `gotrash restore` 82 | 83 | Alias: `gotrash rs` 84 | 85 | Trashed files and directories can be restored with `gotrash restore`. 86 | Check the index with `gotrash list` and pass it. 87 | 88 | ![](./docs/restore.gif) 89 | 90 | If you execute without specifying indexes, fuzzy finder will start. 91 | You can use the tab key to select multiple files or directories to restore. 92 | 93 | ![](./docs/restore-ui.gif) 94 | 95 | ### `gotrash clear` 96 | 97 | `gotrash clear` deletes all trashed files and directories. 98 | 99 | ![](./docs/clear.gif) 100 | 101 | ## LICENSE 102 | 103 | [MIT](./LICENSE) 104 | -------------------------------------------------------------------------------- /cmd/clear.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/koki-develop/gotrash/internal/db" 7 | "github.com/koki-develop/gotrash/internal/util" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var clearCmd = &cobra.Command{ 12 | Use: "clear", 13 | Short: "Clear all trashed files or directories", 14 | Long: "Clear all trashed files or directories.", 15 | Args: cobra.MaximumNArgs(0), 16 | SilenceUsage: true, 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | db, err := db.Open() 19 | if err != nil { 20 | return err 21 | } 22 | defer func() { _ = db.Close() }() 23 | 24 | if !flagClearForce { 25 | if !util.YesNo("clear all trashed files or directories?") { 26 | fmt.Println("canceled.") 27 | return nil 28 | } 29 | } 30 | 31 | if err := db.ClearAll(); err != nil { 32 | return err 33 | } 34 | 35 | return nil 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /cmd/list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/koki-develop/gotrash/internal/db" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var listCmd = &cobra.Command{ 15 | Use: "list", 16 | Short: "List trashed flies or directories", 17 | Long: "List trashed flies or directories.", 18 | Aliases: []string{"ls"}, 19 | Args: cobra.MaximumNArgs(0), 20 | SilenceUsage: true, 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | db, err := db.Open() 23 | if err != nil { 24 | return err 25 | } 26 | defer func() { _ = db.Close() }() 27 | 28 | ts, err := db.List(true) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | cwd, err := os.Getwd() 34 | if err != nil { 35 | return err 36 | } 37 | 38 | digits := len(strconv.Itoa(len(ts))) 39 | f := fmt.Sprintf("%%%dd: (%%s) %%s\n", digits) 40 | for i, t := range ts { 41 | if flagListCurrentDir { 42 | if !strings.HasPrefix(t.Path, cwd) { 43 | continue 44 | } 45 | } 46 | 47 | fmt.Printf(f, i, t.TrashedAt.Format(time.DateTime), t.Path) 48 | } 49 | 50 | return nil 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /cmd/put.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/koki-develop/gotrash/internal/db" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var putCmd = &cobra.Command{ 9 | Use: "put [file]...", 10 | Short: "Trash files or directories", 11 | Long: "Trash files or directories.", 12 | Args: cobra.MinimumNArgs(1), 13 | SilenceUsage: true, 14 | RunE: func(cmd *cobra.Command, args []string) error { 15 | db, err := db.Open() 16 | if err != nil { 17 | return err 18 | } 19 | defer func() { _ = db.Close() }() 20 | 21 | if err := db.Put(args); err != nil { 22 | return err 23 | } 24 | 25 | return nil 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /cmd/restore.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/koki-develop/go-fzf" 9 | "github.com/koki-develop/gotrash/internal/db" 10 | "github.com/koki-develop/gotrash/internal/trash" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | const ( 15 | mainColor = "#00ADD8" 16 | ) 17 | 18 | var restoreCmd = &cobra.Command{ 19 | Use: "restore [index]...", 20 | Short: "Restore trashed files or directories", 21 | Long: "Restore trashed files or directories.", 22 | Aliases: []string{"rs"}, 23 | SilenceUsage: true, 24 | RunE: func(cmd *cobra.Command, args []string) error { 25 | db, err := db.Open() 26 | if err != nil { 27 | return err 28 | } 29 | defer func() { _ = db.Close() }() 30 | 31 | if len(args) == 0 { 32 | ts, err := db.List(false) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | f, err := fzf.New( 38 | fzf.WithNoLimit(true), 39 | fzf.WithStyles( 40 | fzf.WithStyleCursor(fzf.Style{ForegroundColor: mainColor}), 41 | fzf.WithStyleCursorLine(fzf.Style{Bold: true}), 42 | fzf.WithStyleMatches(fzf.Style{ForegroundColor: mainColor}), 43 | fzf.WithStyleSelectedPrefix(fzf.Style{ForegroundColor: mainColor}), 44 | fzf.WithStyleUnselectedPrefix(fzf.Style{Faint: true}), 45 | ), 46 | ) 47 | if err != nil { 48 | return err 49 | } 50 | idxs, err := f.Find( 51 | ts, 52 | func(i int) string { return ts[i].Path }, 53 | fzf.WithItemPrefix(func(i int) string { return fmt.Sprintf("(%s) ", ts[i].TrashedAt.Format(time.DateTime)) }), 54 | ) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | var choices trash.TrashList 60 | for _, i := range idxs { 61 | choices = append(choices, ts[i]) 62 | } 63 | 64 | if err := db.Restore(choices, flagRestoreForce); err != nil { 65 | return err 66 | } 67 | } else { 68 | var is []int 69 | for _, arg := range args { 70 | i, err := strconv.Atoi(arg) 71 | if err != nil { 72 | return err 73 | } 74 | is = append(is, i) 75 | } 76 | 77 | if err := db.RestoreByIndexes(is, flagRestoreForce); err != nil { 78 | return err 79 | } 80 | } 81 | 82 | return nil 83 | }, 84 | } 85 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "runtime/debug" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | version string 12 | ) 13 | 14 | // flags 15 | var ( 16 | // list 17 | flagListCurrentDir bool 18 | 19 | // restore 20 | flagRestoreForce bool 21 | 22 | // clear 23 | flagClearForce bool 24 | ) 25 | 26 | var rootCmd = &cobra.Command{ 27 | Use: "gotrash", 28 | Long: "rm alternative written in Go.", 29 | SilenceUsage: true, 30 | } 31 | 32 | func Execute() { 33 | err := rootCmd.Execute() 34 | if err != nil { 35 | os.Exit(1) 36 | } 37 | } 38 | 39 | func init() { 40 | /* 41 | * version 42 | */ 43 | 44 | if version == "" { 45 | if info, ok := debug.ReadBuildInfo(); ok { 46 | version = info.Main.Version 47 | } 48 | } 49 | 50 | rootCmd.Version = version 51 | 52 | /* 53 | * commands 54 | */ 55 | 56 | rootCmd.AddCommand( 57 | putCmd, 58 | listCmd, 59 | restoreCmd, 60 | clearCmd, 61 | ) 62 | 63 | /* 64 | * flags 65 | */ 66 | 67 | // list 68 | listCmd.Flags().BoolVarP(&flagListCurrentDir, "current-dir", "c", false, "show only the trash in the current directory") 69 | 70 | // restore 71 | restoreCmd.Flags().BoolVarP(&flagRestoreForce, "force", "f", false, "overwrite a file or directory if it already exists") 72 | 73 | // clear 74 | clearCmd.Flags().BoolVarP(&flagClearForce, "force", "f", false, "skip confirmation before clear") 75 | } 76 | -------------------------------------------------------------------------------- /docs/clear.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koki-develop/gotrash/5d26159a0e4fad9075c5e6641444be7bcbfe0936/docs/clear.gif -------------------------------------------------------------------------------- /docs/list.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koki-develop/gotrash/5d26159a0e4fad9075c5e6641444be7bcbfe0936/docs/list.gif -------------------------------------------------------------------------------- /docs/put.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koki-develop/gotrash/5d26159a0e4fad9075c5e6641444be7bcbfe0936/docs/put.gif -------------------------------------------------------------------------------- /docs/restore-ui.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koki-develop/gotrash/5d26159a0e4fad9075c5e6641444be7bcbfe0936/docs/restore-ui.gif -------------------------------------------------------------------------------- /docs/restore.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koki-develop/gotrash/5d26159a0e4fad9075c5e6641444be7bcbfe0936/docs/restore.gif -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/koki-develop/gotrash 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/google/uuid v1.6.0 7 | github.com/koki-develop/go-fzf v0.15.0 8 | github.com/spf13/cobra v1.9.1 9 | github.com/tidwall/buntdb v1.3.2 10 | ) 11 | 12 | require ( 13 | github.com/atotto/clipboard v0.1.4 // indirect 14 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 15 | github.com/charmbracelet/bubbles v0.16.1 // indirect 16 | github.com/charmbracelet/bubbletea v0.24.2 // indirect 17 | github.com/charmbracelet/lipgloss v0.7.1 // indirect 18 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect 19 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 20 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 21 | github.com/mattn/go-isatty v0.0.18 // indirect 22 | github.com/mattn/go-localereader v0.0.1 // indirect 23 | github.com/mattn/go-runewidth v0.0.15 // indirect 24 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 25 | github.com/muesli/cancelreader v0.2.2 // indirect 26 | github.com/muesli/reflow v0.3.0 // indirect 27 | github.com/muesli/termenv v0.15.2 // indirect 28 | github.com/rivo/uniseg v0.4.4 // indirect 29 | github.com/spf13/pflag v1.0.6 // indirect 30 | github.com/tidwall/btree v1.4.2 // indirect 31 | github.com/tidwall/gjson v1.14.3 // indirect 32 | github.com/tidwall/grect v0.1.4 // indirect 33 | github.com/tidwall/match v1.1.1 // indirect 34 | github.com/tidwall/pretty v1.2.0 // indirect 35 | github.com/tidwall/rtred v0.1.2 // indirect 36 | github.com/tidwall/tinyqueue v0.1.1 // indirect 37 | golang.org/x/sync v0.3.0 // indirect 38 | golang.org/x/sys v0.7.0 // indirect 39 | golang.org/x/term v0.7.0 // indirect 40 | golang.org/x/text v0.9.0 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 2 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 3 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 4 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 5 | github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= 6 | github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= 7 | github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= 8 | github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= 9 | github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= 10 | github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= 11 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= 12 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= 13 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 16 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 17 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 18 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 19 | github.com/koki-develop/go-fzf v0.15.0 h1:M7wqkU6YtfHa5pXe3d6aWy5T5AvoGVfp78fDvp5TdkI= 20 | github.com/koki-develop/go-fzf v0.15.0/go.mod h1:qrT0S4PW4rfyxvSvQj8DbaMjTOn60KgnCyAhgryK3Z4= 21 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 22 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 23 | github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= 24 | github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 25 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 26 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 27 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 28 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 29 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 30 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 31 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 32 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 33 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 34 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 35 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 36 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 37 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 38 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 39 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 40 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 41 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 42 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 43 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 44 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 45 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 46 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 47 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 48 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 49 | github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= 50 | github.com/tidwall/btree v1.4.2 h1:PpkaieETJMUxYNADsjgtNRcERX7mGc/GP2zp/r5FM3g= 51 | github.com/tidwall/btree v1.4.2/go.mod h1:LGm8L/DZjPLmeWGjv5kFrY8dL4uVhMmzmmLYmsObdKE= 52 | github.com/tidwall/buntdb v1.3.2 h1:qd+IpdEGs0pZci37G4jF51+fSKlkuUTMXuHhXL1AkKg= 53 | github.com/tidwall/buntdb v1.3.2/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU= 54 | github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 55 | github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw= 56 | github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 57 | github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg= 58 | github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= 59 | github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= 60 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 61 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 62 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 63 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 64 | github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= 65 | github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= 66 | github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= 67 | github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= 68 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 69 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 70 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 71 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 72 | golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= 73 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 74 | golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= 75 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 76 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 77 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 78 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 79 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 80 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 81 | -------------------------------------------------------------------------------- /internal/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/koki-develop/gotrash/internal/trash" 10 | "github.com/koki-develop/gotrash/internal/util" 11 | "github.com/tidwall/buntdb" 12 | ) 13 | 14 | const ( 15 | trashDirname = ".gotrash" 16 | filesDirname = "can" 17 | dbFilename = "db" 18 | 19 | shrinkSize = 10 * 1024 * 1024 // 10MB 20 | ) 21 | 22 | type DB struct { 23 | trashDir string 24 | filesDir string 25 | dbFile string 26 | 27 | db *buntdb.DB 28 | } 29 | 30 | func Open() (*DB, error) { 31 | trashDir, err := root() 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | if err := util.CreateDir(trashDir); err != nil { 37 | return nil, err 38 | } 39 | 40 | filesDir := filepath.Join(trashDir, filesDirname) 41 | 42 | dbFile := filepath.Join(trashDir, dbFilename) 43 | db, err := buntdb.Open(dbFile) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return &DB{ 49 | trashDir: trashDir, 50 | filesDir: filesDir, 51 | dbFile: dbFile, 52 | 53 | db: db, 54 | }, nil 55 | } 56 | 57 | func root() (string, error) { 58 | r := os.Getenv("GOTRASH_ROOT") 59 | if r != "" { 60 | return r, nil 61 | } 62 | 63 | h, err := os.UserHomeDir() 64 | if err != nil { 65 | return "", err 66 | } 67 | 68 | // $HOME/.gotrash 69 | return filepath.Join(h, ".gotrash"), nil 70 | } 71 | 72 | func (db *DB) Close() error { 73 | return db.db.Close() 74 | } 75 | 76 | func (db *DB) Put(ps []string) error { 77 | if err := util.CreateDir(db.filesDir); err != nil { 78 | return err 79 | } 80 | 81 | for _, p := range ps { 82 | exists, err := util.Exists(p) 83 | if err != nil { 84 | return err 85 | } 86 | if !exists { 87 | return fmt.Errorf("%s: no such file or directory", p) 88 | } 89 | 90 | p, err = filepath.Abs(p) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | t := trash.New(p) 96 | v, err := json.Marshal(t) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | err = db.db.Update(func(tx *buntdb.Tx) error { 102 | if _, _, err := tx.Set(t.Key, string(v), nil); err != nil { 103 | return err 104 | } 105 | 106 | if err := os.Rename(p, filepath.Join(db.filesDir, t.Key)); err != nil { 107 | return err 108 | } 109 | 110 | return nil 111 | }) 112 | if err != nil { 113 | return err 114 | } 115 | } 116 | 117 | return nil 118 | } 119 | 120 | func (db *DB) List(asc bool) (trash.TrashList, error) { 121 | var ts trash.TrashList 122 | 123 | err := db.db.View(func(tx *buntdb.Tx) error { 124 | var err error 125 | if asc { 126 | err = tx.Ascend("", func(k, v string) bool { 127 | t := trash.MustFromJSON(k, []byte(v)) 128 | ts = append(ts, t) 129 | return true 130 | }) 131 | } else { 132 | err = tx.Descend("", func(k, v string) bool { 133 | t := trash.MustFromJSON(k, []byte(v)) 134 | ts = append(ts, t) 135 | return true 136 | }) 137 | } 138 | if err != nil { 139 | return err 140 | } 141 | 142 | return nil 143 | }) 144 | if err != nil { 145 | return nil, err 146 | } 147 | 148 | return ts, nil 149 | } 150 | 151 | func (db *DB) RestoreByIndexes(is []int, force bool) error { 152 | maxi := 0 153 | m := make(map[int]struct{}, len(is)) 154 | for _, i := range is { 155 | m[i] = struct{}{} 156 | if maxi < i { 157 | maxi = i 158 | } 159 | } 160 | 161 | var ts trash.TrashList 162 | 163 | err := db.db.View(func(tx *buntdb.Tx) error { 164 | i := 0 165 | err := tx.Ascend("", func(k, v string) bool { 166 | if _, ok := m[i]; ok { 167 | ts = append(ts, trash.MustFromJSON(k, []byte(v))) 168 | delete(m, i) 169 | } 170 | 171 | if i == maxi { 172 | return false 173 | } 174 | i++ 175 | return true 176 | }) 177 | if err != nil { 178 | return err 179 | } 180 | 181 | return nil 182 | }) 183 | if err != nil { 184 | return err 185 | } 186 | 187 | if len(m) > 0 { 188 | is := []int{} 189 | for i := range m { 190 | is = append(is, i) 191 | } 192 | return fmt.Errorf("%d: no such index item", is) 193 | } 194 | 195 | if err := db.Restore(ts, force); err != nil { 196 | return err 197 | } 198 | 199 | return nil 200 | } 201 | 202 | func (db *DB) Restore(ts trash.TrashList, force bool) error { 203 | for _, t := range ts { 204 | if !force { 205 | exists, err := util.Exists(t.Path) 206 | if err != nil { 207 | return err 208 | } 209 | if exists { 210 | return fmt.Errorf("%s: already exists", t.Path) 211 | } 212 | } 213 | 214 | err := db.db.Update(func(tx *buntdb.Tx) error { 215 | if _, err := tx.Delete(t.Key); err != nil { 216 | return err 217 | } 218 | 219 | if err := os.Rename(filepath.Join(db.filesDir, t.Key), t.Path); err != nil { 220 | return err 221 | } 222 | 223 | return nil 224 | }) 225 | if err != nil { 226 | return err 227 | } 228 | 229 | fmt.Printf("restored: %s\n", t.Path) 230 | } 231 | 232 | if err := db.shrink(false); err != nil { 233 | return err 234 | } 235 | 236 | return nil 237 | } 238 | 239 | func (db *DB) ClearAll() error { 240 | err := db.db.Update(func(tx *buntdb.Tx) error { 241 | if err := tx.DeleteAll(); err != nil { 242 | return err 243 | } 244 | 245 | if err := os.RemoveAll(db.filesDir); err != nil { 246 | return err 247 | } 248 | 249 | return nil 250 | }) 251 | 252 | if err := db.shrink(true); err != nil { 253 | return err 254 | } 255 | 256 | return err 257 | } 258 | 259 | func (db *DB) shrink(force bool) error { 260 | if force { 261 | if err := db.db.Shrink(); err != nil { 262 | return err 263 | } 264 | return nil 265 | } 266 | 267 | info, err := os.Stat(db.dbFile) 268 | if err != nil { 269 | return err 270 | } 271 | if info.Size() > shrinkSize { 272 | if err := db.db.Shrink(); err != nil { 273 | return err 274 | } 275 | } 276 | 277 | return nil 278 | } 279 | -------------------------------------------------------------------------------- /internal/trash/trash.go: -------------------------------------------------------------------------------- 1 | package trash 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/google/uuid" 9 | ) 10 | 11 | type Trash struct { 12 | Key string `json:"-"` 13 | Path string `json:"path"` 14 | TrashedAt time.Time `json:"trashed_at"` 15 | } 16 | 17 | type TrashList []*Trash 18 | 19 | func New(p string) *Trash { 20 | n := time.Now() 21 | 22 | return &Trash{ 23 | Key: fmt.Sprintf("%d_%s", n.Unix(), uuid.New()), 24 | Path: p, 25 | TrashedAt: n, 26 | } 27 | } 28 | 29 | func MustFromJSON(k string, b []byte) *Trash { 30 | var t *Trash 31 | if err := json.Unmarshal(b, &t); err != nil { 32 | panic(err) 33 | } 34 | 35 | t.Key = k 36 | return t 37 | } 38 | 39 | // for fuzzy 40 | func (ts TrashList) String(i int) string { 41 | return ts[i].Path 42 | } 43 | 44 | func (ts TrashList) Len() int { 45 | return len(ts) 46 | } 47 | -------------------------------------------------------------------------------- /internal/util/contains.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func Contains[T comparable](ts []T, t T) bool { 4 | for _, r := range ts { 5 | if r == t { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | -------------------------------------------------------------------------------- /internal/util/files.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func CreateDir(dir string) error { 8 | exists, err := Exists(dir) 9 | if err != nil { 10 | return err 11 | } 12 | if exists { 13 | return nil 14 | } 15 | 16 | if err := os.MkdirAll(dir, os.ModePerm); err != nil { 17 | return err 18 | } 19 | 20 | return nil 21 | } 22 | 23 | func Exists(p string) (bool, error) { 24 | if _, err := os.Stat(p); err != nil { 25 | if os.IsNotExist(err) { 26 | return false, nil 27 | } 28 | return false, err 29 | } 30 | return true, nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/util/filter.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func Filter[T any](ts []T, f func(t T) bool) []T { 4 | var rtn []T 5 | 6 | for _, t := range ts { 7 | if f(t) { 8 | rtn = append(rtn, t) 9 | } 10 | } 11 | 12 | return rtn 13 | } 14 | -------------------------------------------------------------------------------- /internal/util/some.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func Some[T any](ts []T, f func(t T) bool) bool { 4 | for _, t := range ts { 5 | if f(t) { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | -------------------------------------------------------------------------------- /internal/util/yesno.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | func YesNo(msg string) bool { 11 | sc := bufio.NewScanner(os.Stdin) 12 | 13 | fmt.Printf("%s (y/N): ", msg) 14 | _ = sc.Scan() 15 | yn := sc.Text() 16 | 17 | switch strings.ToLower(yn) { 18 | case "y", "yes": 19 | return true 20 | default: 21 | return false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/koki-develop/gotrash/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | go = "1.24.3" 3 | "go:github.com/golangci/golangci-lint/v2/cmd/golangci-lint" = "2.1.6" 4 | "go:github.com/goreleaser/goreleaser/v2" = "2.9.0" 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>koki-develop/renovate-config"] 4 | } 5 | -------------------------------------------------------------------------------- /tapes/clear.tape: -------------------------------------------------------------------------------- 1 | # configuration 2 | Output ./docs/clear.gif 3 | Set Shell "bash" 4 | Set FontSize 32 5 | Set Width 1300 6 | Set Height 600 7 | 8 | # setup 9 | Hide 10 | Type "go install" Enter 11 | Sleep 10s 12 | Type "gotrash --help" Enter 13 | Type "gotrash clear -f" Enter 14 | Type "mkdir /tmp/gotrash" Enter 15 | Type "cd /tmp/gotrash" Enter 16 | Type "touch hello.go world.ts foo.js bar.rb" Enter 17 | Ctrl+l 18 | Show 19 | 20 | # --- 21 | 22 | Type "gotrash put hello.go" Sleep 500ms Enter 23 | Sleep 1s 24 | Type "gotrash list" Sleep 500ms Enter 25 | Sleep 1s 26 | Type "gotrash clear" Sleep 500ms Enter 27 | Sleep 1s 28 | Type "y" Sleep 500ms Enter 29 | Sleep 1s 30 | Type "gotrash list" Sleep 500ms Enter 31 | 32 | Sleep 4s 33 | 34 | # cleanup 35 | Hide 36 | Type "cd ~/" Enter 37 | Type "\rm -rf /tmp/gotrash" Enter 38 | Type "gotrash clear -f" Enter 39 | -------------------------------------------------------------------------------- /tapes/list.tape: -------------------------------------------------------------------------------- 1 | # configuration 2 | Output ./docs/list.gif 3 | Set Shell "bash" 4 | Set FontSize 32 5 | Set Width 1300 6 | Set Height 600 7 | 8 | # setup 9 | Hide 10 | Type "go install" Enter 11 | Sleep 10s 12 | Type "gotrash --help" Enter 13 | Type "gotrash clear -f" Enter 14 | Type "mkdir /tmp/gotrash" Enter 15 | Type "cd /tmp/gotrash" Enter 16 | Type "touch hello.go world.ts foo.js bar.rb" Enter 17 | Ctrl+l 18 | Show 19 | 20 | # --- 21 | 22 | Type "ls" Sleep 500ms Enter 23 | Sleep 1s 24 | Type "gotrash put hello.go" Sleep 500ms Enter 25 | Sleep 1s 26 | Type "ls" Sleep 500ms Enter 27 | Sleep 1s 28 | Type "gotrash list" Sleep 500ms Enter 29 | 30 | Sleep 4s 31 | 32 | # cleanup 33 | Hide 34 | Type "cd ~/" Enter 35 | Type "\rm -rf /tmp/gotrash" Enter 36 | Type "gotrash clear -f" Enter 37 | -------------------------------------------------------------------------------- /tapes/put.tape: -------------------------------------------------------------------------------- 1 | # configuration 2 | Output ./docs/put.gif 3 | Set Shell "bash" 4 | Set FontSize 32 5 | Set Width 1300 6 | Set Height 600 7 | 8 | # setup 9 | Hide 10 | Type "go install" Enter 11 | Sleep 10s 12 | Type "gotrash --help" Enter 13 | Type "gotrash clear -f" Enter 14 | Type "mkdir /tmp/gotrash" Enter 15 | Type "cd /tmp/gotrash" Enter 16 | Type "touch hello.go world.ts foo.js bar.rb" Enter 17 | Ctrl+l 18 | Show 19 | 20 | # --- 21 | 22 | Type "ls" Sleep 500ms Enter 23 | Sleep 2s 24 | Type "gotrash put hello.go" Sleep 500ms Enter 25 | Sleep 1s 26 | Type "ls" Sleep 500ms Enter 27 | 28 | Sleep 4s 29 | 30 | # --- 31 | 32 | # cleanup 33 | Hide 34 | Type "cd ~/" Enter 35 | Type "\rm -rf /tmp/gotrash" Enter 36 | Type "gotrash clear -f" Enter 37 | -------------------------------------------------------------------------------- /tapes/restore-ui.tape: -------------------------------------------------------------------------------- 1 | # configuration 2 | Output ./docs/restore-ui.gif 3 | Set Shell "bash" 4 | Set FontSize 32 5 | Set Width 1300 6 | Set Height 600 7 | 8 | # setup 9 | Hide 10 | Type "go install" Enter 11 | Sleep 10s 12 | Type "gotrash --help" Enter 13 | Type "gotrash clear -f" Enter 14 | Type "mkdir /tmp/gotrash" Enter 15 | Type "cd /tmp/gotrash" Enter 16 | Type "touch hello.go world.ts foo.js bar.rb" Enter 17 | Ctrl+l 18 | Show 19 | 20 | # --- 21 | 22 | Type "ls" Sleep 500ms Enter 23 | Sleep 1s 24 | Type "gotrash put hello.go world.ts foo.js bar.rb" Sleep 500ms Enter 25 | Sleep 1s 26 | Type "gotrash restore" Sleep 500ms Enter 27 | Sleep 2s 28 | Type "hello.go" Sleep 500ms Tab 29 | Sleep 1s 30 | Backspace 8 31 | Sleep 1s 32 | Type "foo.js" Sleep 500ms Tab 33 | Sleep 1s 34 | Backspace 6 35 | Sleep 2s 36 | Enter 37 | Sleep 1s 38 | Type "ls" Sleep 500ms Enter 39 | 40 | Sleep 4s 41 | 42 | # cleanup 43 | Hide 44 | Type "cd ~/" Enter 45 | Type "\rm -rf /tmp/gotrash" Enter 46 | Type "gotrash clear -f" Enter 47 | -------------------------------------------------------------------------------- /tapes/restore.tape: -------------------------------------------------------------------------------- 1 | # configuration 2 | Output ./docs/restore.gif 3 | Set Shell "bash" 4 | Set FontSize 32 5 | Set Width 1300 6 | Set Height 600 7 | 8 | # setup 9 | Hide 10 | Type "go install" Enter 11 | Sleep 10s 12 | Type "gotrash --help" Enter 13 | Type "gotrash clear -f" Enter 14 | Type "mkdir /tmp/gotrash" Enter 15 | Type "cd /tmp/gotrash" Enter 16 | Type "touch hello.go world.ts foo.js bar.rb" Enter 17 | Ctrl+l 18 | Show 19 | 20 | # --- 21 | 22 | Type "ls" Sleep 500ms Enter 23 | Sleep 1s 24 | Type "gotrash put hello.go world.ts" Sleep 500ms Enter 25 | Sleep 1s 26 | Type "ls" Sleep 500ms Enter 27 | Sleep 1s 28 | Type "gotrash list" Sleep 500ms Enter 29 | Sleep 2s 30 | Type "gotrash restore 1" Sleep 500ms Enter 31 | Sleep 1s 32 | Type "ls" Sleep 500ms Enter 33 | 34 | Sleep 4s 35 | 36 | # cleanup 37 | Hide 38 | Type "cd ~/" Enter 39 | Type "\rm -rf /tmp/gotrash" Enter 40 | Type "gotrash clear -f" Enter 41 | --------------------------------------------------------------------------------