Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,26 @@ What a bar is measured against. Two modes, applying to every bar alike:

Rows the user has ignored are excluded from the denominator and draw an empty
bar.

## Projection

A way of coloring (and, for one projection, regrouping) the web UI treemap
other than by location. Area always stays the item's size; a projection only
changes what the color says.

## Cleanup candidate

An item that is likely safe to remove to free space, because a tool can
regenerate or re-download it. A directory that is a candidate is a unit: the
items inside it are not candidates of their own.

## Category

What kind of cleanup candidate an item is: build artifact, package cache, app
cache, SDK or toolchain, ML model, installer or archive, or git-ignored.

## Confidence

How safe removing a cleanup candidate is: **safe** (regenerated on demand) or
**review** (probably unneeded, but may hold the only copy of something or need
its own tool to be removed cleanly).
51 changes: 50 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,62 @@ its own reveal and delete buttons that act on that item; deletion asks for
confirmation with the same dialog. The deletion dialog can suppress further
confirmations for the current browser session, for both the treemap and the
table, so with "permanent" chosen a single click on a row's delete button
deletes immediately. The `--no-delete` flag and
deletes immediately. Deletion runs in the background: the item is drawn
hatched right away, more items can be deleted meanwhile, and a failed deletion
puts the item back with the reason. A deleted item stays in place,
cross-hatched, so the layout does not jump; the directory is reloaded when you
open another one. Opening another directory waits until the running deletions
finish. The `--no-delete` flag and
filtered-scan deletion restrictions apply to the web UI as well as the
terminal UI. Reveal remains available with `--no-delete` because it does not
modify scanned data. To deliberately allow deletion while filters are active,
set `GDU_ALLOW_DELETE_WITH_FILTER=1`; the displayed tree may then omit files
that are still present on disk.

### Cleanup projections

The treemap's projection selector (or `P`) recolors it to help free space
without touching anything important. Area always stays size.

- **Location** - the default coloring by top-level directory.
- **Cleanup** - everything grey except cleanup candidates: strong orange for
*safe* ones (regenerated by their tool, e.g. `node_modules` next to
`package.json`, `target` next to `Cargo.toml`, `__pycache__`, the Go, Gradle,
Cargo, npm, uv and Homebrew caches, app caches)
and muted gold for ones to *review first* (downloaded ML models, installed
toolchains such as `~/.rustup` or `~/.nvm`, installers,
virtualenvs, `dist`/`build`, git-ignored files). A directory is tinted by the
share of candidates inside it, including files too small to draw.
- **Age** - colored by the latest change inside an item.
- **By category** - candidates within the current directory regrouped by
category, so cache space scattered over many small directories shows up as
one block.

The legend shows how much each category takes within the current directory;
click a category to single it out: its items are drawn in the category's color
and outlined, everything else fades (`Esc` shows all again). Hovering a candidate
shows what it is and how to remove it cleanly (e.g. `ollama rm` for Ollama
models). Build-like directories only count next to a project marker, and
anything ignored by a repository's `.gitignore` is flagged for review.
Candidates are only highlighted: deleting stays a deliberate action. On macOS,
"move to trash" uses the system `trash` command, so items can be put back from
Finder.

Rules can be extended and paths excluded in the config file; user rules are
checked before the built-in ones:

```yaml
cleanup:
exclude:
- ~/work/current-project
rules:
- category: build-artifact # build-artifact, package-cache, app-cache,
confidence: safe # toolchain, ml-model, installer, git-ignored
dir: .gradle # or file: "*.ext", or path: "~/some/path"
markers: [build.gradle] # optional sibling that must exist
hint: Rebuilt by Gradle
```

By default the server binds to `localhost` on a random free port. Use
`--web-listen` (or the `web.listen` config option) to pin a fixed address.

Expand Down
7 changes: 7 additions & 0 deletions cmd/gdu/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/dundee/gdu/v5/build"
"github.com/dundee/gdu/v5/internal/common"
"github.com/dundee/gdu/v5/pkg/analyze"
"github.com/dundee/gdu/v5/pkg/cleanup"
"github.com/dundee/gdu/v5/pkg/device"
gfs "github.com/dundee/gdu/v5/pkg/fs"
"github.com/dundee/gdu/v5/pkg/timefilter"
Expand Down Expand Up @@ -119,6 +120,9 @@ type Flags struct {
BrowseParentDirs bool `yaml:"browse-parent-dirs"`
Web bool `yaml:"-"`
WebConfig WebConfig `yaml:"web"`
// Cleanup holds user rules and exclusions for the web UI's cleanup
// projections.
Cleanup cleanup.Config `yaml:"cleanup"`
}

// WebConfig defines the web UI options that can be set from the config file.
Expand Down Expand Up @@ -496,6 +500,9 @@ func (a *App) createUI(outputAttributes gfs.JSONAttributes) (UI, error) {
if a.Flags.TrashCommand != "" {
webUI.SetTrashCommand(a.Flags.TrashCommand)
}
if err := webUI.SetCleanupConfig(a.Flags.Cleanup); err != nil {
return nil, fmt.Errorf("cleanup config: %w", err)
}
ui = webUI
case a.Flags.OutputFile != "":
var output io.Writer
Expand Down
41 changes: 41 additions & 0 deletions cmd/gdu/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import (
"time"

log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"

"github.com/dundee/gdu/v5/internal/common"
"github.com/dundee/gdu/v5/internal/testapp"
"github.com/dundee/gdu/v5/internal/testdev"
"github.com/dundee/gdu/v5/internal/testdir"
"github.com/dundee/gdu/v5/pkg/cleanup"
"github.com/dundee/gdu/v5/pkg/device"
gfs "github.com/dundee/gdu/v5/pkg/fs"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -872,6 +874,45 @@ func TestWebFlagCreatesWebUIAndHonorsNoDelete(t *testing.T) {
assert.ErrorContains(t, err, "binding")
}

func TestWebFlagRejectsInvalidCleanupRule(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()

_, err := runApp(
&Flags{
LogFile: "/dev/null",
Web: true,
Cleanup: cleanup.Config{Rules: []cleanup.Rule{{Category: "junk", Dir: "x"}}},
},
[]string{"test_dir"},
false,
testdev.DevicesInfoGetterMock{},
)

assert.ErrorContains(t, err, `cleanup config: cleanup rule 1: unknown category "junk"`)
}

func TestCleanupConfigFromYAML(t *testing.T) {
var f Flags
err := yaml.Unmarshal([]byte(`
cleanup:
exclude: [~/work]
rules:
- category: build-artifact
confidence: safe
dir: .gradle
markers: [build.gradle]
`), &f)
assert.NoError(t, err)
assert.Equal(t, cleanup.Config{
Exclude: []string{"~/work"},
Rules: []cleanup.Rule{{
Category: cleanup.BuildArtifact, Confidence: cleanup.Safe,
Dir: ".gradle", Markers: []string{"build.gradle"},
}},
}, f.Cleanup)
}

func TestListDevices(t *testing.T) {
fin := testdir.CreateTestDir()
defer fin()
Expand Down
41 changes: 41 additions & 0 deletions docs/adr/0003-macos-trash-uses-system-trash-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 3. The macOS trash runs the system trash command

Date: 2026-10-19

## Status

Accepted

## Context

The built-in trash targets the FreeDesktop trash, which Finder ignores, so on
darwin it was a stub that returned "not supported". ADR 0002 left macOS users a
workaround (`--trash-command`) and noted that probing for a trash CLI remained
open.

The web UI's cleanup projections make moving many items to the trash a common
action, so it has to work without configuration on the platform most of its
users run.

Current macOS ships `/usr/bin/trash`, which moves items into the Finder trash,
where they can be put back.

## Decision

On darwin, `MoveItemToTrash` runs `/usr/bin/trash <abspath>` directly, without a
shell. `trash` does not understand `--`, but the path is always absolute, so it
can never be read as an option.

As with the trash command, the tree is updated from the filesystem: the item is
dropped only when its path is really gone.

When `/usr/bin/trash` does not exist, the error points the user at
`trash-command`, which still takes precedence when set.

## Consequences

- `D` in the terminal UI and "Move to trash" in the web UI work on macOS out of
the box, and trashed items can be restored from Finder.
- Older macOS releases without `/usr/bin/trash` keep the previous behavior, an
error, with a hint towards the workaround.
- No Objective-C bridge or cgo is needed.
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/ebitengine/purego v0.10.2
github.com/fatih/color v1.19.0
github.com/gdamore/tcell/v2 v2.13.10
github.com/go-git/go-git/v5 v5.19.3
github.com/h2non/filetype v1.1.3
github.com/maruel/natural v1.3.0
github.com/mattn/go-isatty v0.0.24
Expand All @@ -27,11 +28,14 @@ require (
github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gdamore/encoding v1.0.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/flatbuffers v25.9.23+incompatible // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
Expand All @@ -44,8 +48,10 @@ require (
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/term v0.43.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/term v0.45.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
modernc.org/libc v1.75.7 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.12.1 // indirect
Expand Down
16 changes: 14 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uh
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
github.com/gdamore/tcell/v2 v2.13.10 h1:Afs3JKt83HnhuUKdZ3MnxUgOqQRWftj5JyDqv1LLynA=
github.com/gdamore/tcell/v2 v2.13.10/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.9.2 h1:OXFSRyz4g20upsGDJgQG9Bak1l/ZEv8GHVYB52O71sE=
github.com/go-git/go-billy/v5 v5.9.2/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
github.com/go-git/go-git/v5 v5.19.3 h1:qHttbZ+Am7wEjdTpR1XMQ4rl42FK9SIXzZ4o9x7s6M4=
github.com/go-git/go-git/v5 v5.19.3/go.mod h1:Ye4C8dVigkqrv9UsB4NMdSLV4yAIkLiIhW61gcOyhl4=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
Expand All @@ -36,6 +42,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down Expand Up @@ -96,6 +104,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand All @@ -112,8 +122,8 @@ golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
Expand All @@ -133,6 +143,8 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.29.2 h1:h6+9ciCnPKutf4I03CvheAvDLX7+IHlqR6Iy6J+cgd8=
Expand Down
Loading
Loading