mapsize 1.2.0: permanent delete and in-map reveal when headless
On a headless Linux or BSD system (no DISPLAY or WAYLAND_DISPLAY) nothing empties the freedesktop trash, so trashing never freed space. There, d now deletes permanently after an explicit confirmation, checking owner and type against the scan and removing through an os.Root on the parent so a swapped symlink cannot redirect it. o shows the folder in the map instead of launching a file manager. Desktop sessions, macOS and Windows are unchanged.
This commit is contained in:
@@ -301,4 +301,7 @@ Filenames and snapshot contents are untrusted.
|
|||||||
Aggregates are recomputed with saturating arithmetic rather than trusted.
|
Aggregates are recomputed with saturating arithmetic rather than trusted.
|
||||||
* No shell is ever invoked. "Open/reveal" and trash use direct `exec` with
|
* No shell is ever invoked. "Open/reveal" and trash use direct `exec` with
|
||||||
argument vectors or native APIs; paths are passed as single arguments.
|
argument vectors or native APIs; paths are passed as single arguments.
|
||||||
|
* Permanent delete (headless only) checks the item's owner and type against
|
||||||
|
the scan and removes it through an `os.Root` on the parent directory, so
|
||||||
|
a symlink swapped in since the scan cannot redirect it.
|
||||||
* `--read-only` disables every mutating action in the UI.
|
* `--read-only` disables every mutating action in the UI.
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.2.0 — 2026-10-03
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- On a headless Linux or BSD system (no `DISPLAY` or `WAYLAND_DISPLAY`), `d`
|
||||||
|
deletes permanently after an "are you sure" confirmation instead of moving
|
||||||
|
to a trash nobody empties, and `o` shows the folder in the map instead of
|
||||||
|
trying to open a file manager. Desktop sessions, macOS and Windows still
|
||||||
|
use the trash and the file manager.
|
||||||
|
|
||||||
## 1.1.0 — 2026-10-03
|
## 1.1.0 — 2026-10-03
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ file-manager integration have not yet been tested there.
|
|||||||
| x, g, e | file types, top lists, scan info & errors |
|
| x, g, e | file types, top lists, scan info & errors |
|
||||||
| D | find duplicates |
|
| D | find duplicates |
|
||||||
| a | toggle allocated / logical size |
|
| a | toggle allocated / logical size |
|
||||||
| c, o | copy path, reveal in file manager |
|
| c, o | copy path, reveal in file manager (headless: show the folder in the map) |
|
||||||
| d | move to trash (asks; disabled with `--read-only`) |
|
| d | move to trash, or delete permanently when headless (asks; disabled with `--read-only`) |
|
||||||
| s, r | save snapshot, rescan |
|
| s, r | save snapshot, rescan |
|
||||||
| T | cycle theme |
|
| T | cycle theme |
|
||||||
| ? | help |
|
| ? | help |
|
||||||
@@ -160,9 +160,16 @@ uses ordinary filesystem APIs, and duplicate detection reads file content
|
|||||||
|
|
||||||
`d` moves the selected item to the system trash (freedesktop.org trash on
|
`d` moves the selected item to the system trash (freedesktop.org trash on
|
||||||
Linux, `~/.Trash` on macOS, the Recycle Bin on Windows) after confirmation,
|
Linux, `~/.Trash` on macOS, the Recycle Bin on Windows) after confirmation,
|
||||||
using native file operations — never a shell. Permanent deletion is not
|
using native file operations — never a shell.
|
||||||
offered. Trash is unavailable while scanning, for snapshots, and in
|
|
||||||
read-only mode.
|
On a headless Linux or BSD system (neither `DISPLAY` nor `WAYLAND_DISPLAY`
|
||||||
|
set) nothing ever empties the trash, so trashing would not free any space.
|
||||||
|
There `d` deletes permanently instead, after a confirmation that says it
|
||||||
|
cannot be undone, and `o` shows the folder in the map rather than opening a
|
||||||
|
file manager.
|
||||||
|
|
||||||
|
Deletion is unavailable while scanning, for snapshots, and in read-only
|
||||||
|
mode.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
|||||||
+8
-4
@@ -1,5 +1,5 @@
|
|||||||
.\" Manual page for mapsize. View with: man -l docs/mapsize.1
|
.\" Manual page for mapsize. View with: man -l docs/mapsize.1
|
||||||
.TH MAPSIZE 1 "October 2026" "mapsize 1.1.0" "User Commands"
|
.TH MAPSIZE 1 "October 2026" "mapsize 1.2.0" "User Commands"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
mapsize \- interactive terminal disk usage analyser with a navigable treemap
|
mapsize \- interactive terminal disk usage analyser with a navigable treemap
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -95,7 +95,7 @@ Useful over SSH, where \fBCOLORTERM\fR is often not forwarded.
|
|||||||
Disable mouse support. Everything works from the keyboard.
|
Disable mouse support. Everything works from the keyboard.
|
||||||
.TP
|
.TP
|
||||||
.B \-\-read\-only
|
.B \-\-read\-only
|
||||||
Disable every action that modifies the filesystem (moving to trash) and
|
Disable every action that modifies the filesystem (trash and delete) and
|
||||||
show \fBREAD ONLY\fR in the header. Saving a snapshot file is still
|
show \fBREAD ONLY\fR in the header. Saving a snapshot file is still
|
||||||
allowed. This does not make
|
allowed. This does not make
|
||||||
.B mapsize
|
.B mapsize
|
||||||
@@ -223,10 +223,14 @@ Toggle allocated / logical size.
|
|||||||
Copy the selected path to the clipboard (OSC 52; terminal support varies).
|
Copy the selected path to the clipboard (OSC 52; terminal support varies).
|
||||||
.TP
|
.TP
|
||||||
.B o
|
.B o
|
||||||
Reveal the selected item in the desktop file manager.
|
Reveal the selected item in the desktop file manager. On a headless system
|
||||||
|
(no \fBDISPLAY\fR or \fBWAYLAND_DISPLAY\fR), show the folder in the map
|
||||||
|
instead.
|
||||||
.TP
|
.TP
|
||||||
.BR d ", " Delete
|
.BR d ", " Delete
|
||||||
Move the selected item to the trash after confirmation. Disabled with
|
Move the selected item to the trash after confirmation. On a headless
|
||||||
|
Linux or BSD system, where nothing would empty the trash, delete it
|
||||||
|
permanently instead after a confirmation that says so. Disabled with
|
||||||
\fB\-\-read\-only\fR, for snapshots, and while scanning.
|
\fB\-\-read\-only\fR, for snapshots, and while scanning.
|
||||||
.TP
|
.TP
|
||||||
.B s
|
.B s
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Version is stamped at release time via -ldflags -X.
|
// Version is stamped at release time via -ldflags -X.
|
||||||
var Version = "1.1.0"
|
var Version = "1.2.0"
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
//go:build unix
|
||||||
|
|
||||||
|
package platform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Delete permanently removes path (recursively for a directory) if it still
|
||||||
|
// matches want. The check and the removal both go through an os.Root on the
|
||||||
|
// parent directory, so a symlink swapped in since the scan cannot redirect
|
||||||
|
// the removal outside it.
|
||||||
|
func Delete(path string, want Expect) error {
|
||||||
|
if err := checkAbs(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dir, base := filepath.Dir(path), filepath.Base(path)
|
||||||
|
root, err := os.OpenRoot(dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer root.Close()
|
||||||
|
fi, err := root.Lstat(base)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if fi.Sys().(*syscall.Stat_t).Uid != want.UID || fi.Mode()&fs.ModeType != want.Mode&fs.ModeType {
|
||||||
|
return errors.New("item changed since the scan (owner or type differs); rescan first")
|
||||||
|
}
|
||||||
|
return root.RemoveAll(base)
|
||||||
|
}
|
||||||
@@ -75,3 +75,7 @@ func volumeOf(path string) string {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Headless is always false: macOS has Finder and the Trash even without
|
||||||
|
// $DISPLAY (which it does not use).
|
||||||
|
func Headless() bool { return false }
|
||||||
|
|||||||
@@ -123,3 +123,10 @@ func mountTop(path string, dev uint64) string {
|
|||||||
cur = parent
|
cur = parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Headless reports whether there is no graphical session (no X or Wayland
|
||||||
|
// display), so there is no file manager to open and no desktop trash anyone
|
||||||
|
// will empty.
|
||||||
|
func Headless() bool {
|
||||||
|
return os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == ""
|
||||||
|
}
|
||||||
|
|||||||
@@ -113,3 +113,43 @@ func TestTrashRefusesSwappedItem(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeletePermanently(t *testing.T) {
|
||||||
|
base := t.TempDir()
|
||||||
|
dir := filepath.Join(base, "S01")
|
||||||
|
os.MkdirAll(filepath.Join(dir, "extras"), 0o755)
|
||||||
|
os.WriteFile(filepath.Join(dir, "extras", "E01.mkv"), []byte("x"), 0o644)
|
||||||
|
// A symlink inside must be removed, not followed.
|
||||||
|
keep := filepath.Join(base, "keep")
|
||||||
|
os.WriteFile(keep, []byte("k"), 0o644)
|
||||||
|
os.Symlink(keep, filepath.Join(dir, "link"))
|
||||||
|
|
||||||
|
w := want(t, dir)
|
||||||
|
if err := Delete(dir, Expect{UID: w.UID, Mode: 0}); err == nil {
|
||||||
|
t.Fatal("type mismatch accepted")
|
||||||
|
}
|
||||||
|
if err := Delete(dir, w); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := os.Lstat(dir); !os.IsNotExist(err) {
|
||||||
|
t.Fatal("directory still present")
|
||||||
|
}
|
||||||
|
if _, err := os.Lstat(keep); err != nil {
|
||||||
|
t.Fatal("symlink target was deleted")
|
||||||
|
}
|
||||||
|
if err := Delete("relative/path", Expect{}); err == nil {
|
||||||
|
t.Fatal("relative paths must be refused")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHeadless(t *testing.T) {
|
||||||
|
t.Setenv("DISPLAY", "")
|
||||||
|
t.Setenv("WAYLAND_DISPLAY", "")
|
||||||
|
if !Headless() {
|
||||||
|
t.Fatal("no display should be headless")
|
||||||
|
}
|
||||||
|
t.Setenv("WAYLAND_DISPLAY", "wayland-0")
|
||||||
|
if Headless() {
|
||||||
|
t.Fatal("wayland session reported headless")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
package platform
|
package platform
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
// TrashName is the user-facing name of the trash.
|
// TrashName is the user-facing name of the trash.
|
||||||
const TrashName = "trash"
|
const TrashName = "trash"
|
||||||
@@ -12,3 +15,10 @@ func Reveal(string) error { return errors.New("not supported on this platform")
|
|||||||
|
|
||||||
// Trash is unsupported on this platform.
|
// Trash is unsupported on this platform.
|
||||||
func Trash(string, Expect) error { return errors.New("trash not supported on this platform") }
|
func Trash(string, Expect) error { return errors.New("trash not supported on this platform") }
|
||||||
|
|
||||||
|
// Headless reports whether there is no graphical session (no X or Wayland
|
||||||
|
// display), so there is no file manager to open and no desktop trash anyone
|
||||||
|
// will empty.
|
||||||
|
func Headless() bool {
|
||||||
|
return os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == ""
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,3 +70,9 @@ func Trash(path string, _ Expect) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Headless is always false: Windows always has Explorer and the Recycle Bin.
|
||||||
|
func Headless() bool { return false }
|
||||||
|
|
||||||
|
// Delete is not offered on Windows; items go to the Recycle Bin.
|
||||||
|
func Delete(string, Expect) error { return errors.New("permanent delete not supported on Windows") }
|
||||||
|
|||||||
+28
-11
@@ -21,10 +21,13 @@ type trashDoneMsg struct {
|
|||||||
tree *inventory.Tree // the tree id belongs to; results for a replaced tree are dropped
|
tree *inventory.Tree // the tree id belongs to; results for a replaced tree are dropped
|
||||||
id inventory.NodeID
|
id inventory.NodeID
|
||||||
err error
|
err error
|
||||||
|
done string // "Moved to trash" or "Deleted"
|
||||||
}
|
}
|
||||||
|
|
||||||
// requestTrash asks for confirmation before moving the selection to the
|
// requestTrash asks for confirmation before moving the selection to the
|
||||||
// trash. Permanent deletion is deliberately not offered.
|
// trash. On a headless system (no desktop, so nobody empties the trash and
|
||||||
|
// the space is never freed) it deletes permanently instead, after a
|
||||||
|
// confirmation that says so.
|
||||||
func (m *Model) requestTrash() tea.Cmd {
|
func (m *Model) requestTrash() tea.Cmd {
|
||||||
switch {
|
switch {
|
||||||
case m.opts.ReadOnly:
|
case m.opts.ReadOnly:
|
||||||
@@ -32,7 +35,7 @@ func (m *Model) requestTrash() tea.Cmd {
|
|||||||
case m.snapshot:
|
case m.snapshot:
|
||||||
return m.warn("Snapshot view: nothing to delete on the live filesystem")
|
return m.warn("Snapshot view: nothing to delete on the live filesystem")
|
||||||
case m.scanning:
|
case m.scanning:
|
||||||
return m.warn("Wait for the scan to finish before trashing items")
|
return m.warn("Wait for the scan to finish before deleting items")
|
||||||
}
|
}
|
||||||
id := m.selectedNode()
|
id := m.selectedNode()
|
||||||
if id == inventory.NoNode || id == m.tree.Root() {
|
if id == inventory.NoNode || id == m.tree.Root() {
|
||||||
@@ -45,15 +48,29 @@ func (m *Model) requestTrash() tea.Cmd {
|
|||||||
if n.IsDir() {
|
if n.IsDir() {
|
||||||
what = fmt.Sprintf("directory with %s files", textutil.Count(int64(n.Files)))
|
what = fmt.Sprintf("directory with %s files", textutil.Count(int64(n.Files)))
|
||||||
}
|
}
|
||||||
|
body := []string{textutil.Sanitize(path), "", fmt.Sprintf("%s · %s", what, textutil.Size(n.Total(m.sizeMode))), ""}
|
||||||
|
if platform.Headless() {
|
||||||
|
m.openModal(&confirmModal{
|
||||||
|
ttl: "Delete permanently? Are you sure?",
|
||||||
|
body: append(body, "This is a headless system (no X or Wayland display), so there is no trash:",
|
||||||
|
"it will be permanently deleted and cannot be recovered."),
|
||||||
|
yes: "delete permanently",
|
||||||
|
onYes: func(m *Model) tea.Cmd {
|
||||||
|
t := m.tree
|
||||||
|
return func() tea.Msg { return trashDoneMsg{t, id, platform.Delete(path, want), "Deleted"} }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
m.openModal(&confirmModal{
|
m.openModal(&confirmModal{
|
||||||
ttl: "Move to trash?",
|
ttl: "Move to trash?",
|
||||||
body: []string{textutil.Sanitize(path), "",
|
body: append(body, "It will be moved to the "+platform.TrashName+", not permanently deleted."),
|
||||||
fmt.Sprintf("%s · %s", what, textutil.Size(n.Total(m.sizeMode))),
|
yes: "move to " + platform.TrashName,
|
||||||
"", "It will be moved to the " + platform.TrashName + ", not permanently deleted."},
|
|
||||||
yes: "move to " + platform.TrashName,
|
|
||||||
onYes: func(m *Model) tea.Cmd {
|
onYes: func(m *Model) tea.Cmd {
|
||||||
t := m.tree
|
t := m.tree
|
||||||
return func() tea.Msg { return trashDoneMsg{t, id, platform.Trash(path, want)} }
|
return func() tea.Msg {
|
||||||
|
return trashDoneMsg{t, id, platform.Trash(path, want), "Moved to " + platform.TrashName}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
@@ -61,11 +78,11 @@ func (m *Model) requestTrash() tea.Cmd {
|
|||||||
|
|
||||||
func (m *Model) trashDone(msg trashDoneMsg) tea.Cmd {
|
func (m *Model) trashDone(msg trashDoneMsg) tea.Cmd {
|
||||||
if msg.err != nil {
|
if msg.err != nil {
|
||||||
return m.warn("Trash failed: " + textutil.Sanitize(msg.err.Error()))
|
return m.warn("Delete failed: " + textutil.Sanitize(msg.err.Error()))
|
||||||
}
|
}
|
||||||
if msg.tree != m.tree || m.scanning {
|
if msg.tree != m.tree || m.scanning {
|
||||||
// A rescan replaced the inventory; the new scan reflects the change.
|
// A rescan replaced the inventory; the new scan reflects the change.
|
||||||
return m.info("Moved to " + platform.TrashName)
|
return m.info(msg.done)
|
||||||
}
|
}
|
||||||
// The scan has finished, so the UI goroutine is the tree's only writer.
|
// The scan has finished, so the UI goroutine is the tree's only writer.
|
||||||
t := m.tree
|
t := m.tree
|
||||||
@@ -87,7 +104,7 @@ func (m *Model) trashDone(msg trashDoneMsg) tea.Cmd {
|
|||||||
name := n.Name
|
name := n.Name
|
||||||
t.Unlock()
|
t.Unlock()
|
||||||
m.invalidate()
|
m.invalidate()
|
||||||
return m.info("Moved to " + platform.TrashName + ": " + textutil.Sanitize(name))
|
return m.info(msg.done + ": " + textutil.Sanitize(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Duplicates ----------------------------------------------------------
|
// ---- Duplicates ----------------------------------------------------------
|
||||||
|
|||||||
@@ -226,6 +226,17 @@ func (m *Model) reveal() tea.Cmd {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if platform.Headless() {
|
||||||
|
// No file manager: show the folder in the map instead.
|
||||||
|
id := m.selectedNode()
|
||||||
|
if m.tree.Node(id).IsDir() && len(m.kidsOf(id).ids) > 0 {
|
||||||
|
m.zoomTo(id)
|
||||||
|
m.setViewByName("Map")
|
||||||
|
} else {
|
||||||
|
m.jumpTo(id)
|
||||||
|
}
|
||||||
|
return m.info("Showing " + textutil.Sanitize(m.tree.Path(m.zoom)))
|
||||||
|
}
|
||||||
if err := platform.Reveal(p); err != nil {
|
if err := platform.Reveal(p); err != nil {
|
||||||
return m.warn("Open failed: " + textutil.Sanitize(err.Error()))
|
return m.warn("Open failed: " + textutil.Sanitize(err.Error()))
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-5
@@ -39,9 +39,9 @@ func (*infoModal) width() int { return 72 }
|
|||||||
func (md *infoModal) title() string { return "Item information" }
|
func (md *infoModal) title() string { return "Item information" }
|
||||||
|
|
||||||
func (md *infoModal) footer(m *Model) string {
|
func (md *infoModal) footer(m *Model) string {
|
||||||
s := "Esc close · c copy path · o reveal · Space go to"
|
s := "Esc close · c copy path · o " + revealVerb() + " · Space go to"
|
||||||
if !m.opts.ReadOnly && !m.snapshot {
|
if !m.opts.ReadOnly && !m.snapshot {
|
||||||
s += " · d trash"
|
s += " · d " + deleteVerb()
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
@@ -168,7 +168,8 @@ func (md *infoModal) key(m *Model, k string) (bool, tea.Cmd) {
|
|||||||
return false, m.copyPath(m.tree.Path(md.id))
|
return false, m.copyPath(m.tree.Path(md.id))
|
||||||
case "o":
|
case "o":
|
||||||
m.sel = int64(md.id)
|
m.sel = int64(md.id)
|
||||||
return false, m.reveal()
|
// Headless, o navigates the map, which the modal would cover.
|
||||||
|
return platform.Headless(), m.reveal()
|
||||||
case "enter":
|
case "enter":
|
||||||
return true, nil
|
return true, nil
|
||||||
case "g", "right":
|
case "g", "right":
|
||||||
@@ -273,9 +274,9 @@ func (helpModal) content(m *Model, _ int) []line {
|
|||||||
sec("APPLICATION")
|
sec("APPLICATION")
|
||||||
row("a", "toggle allocated / logical (apparent) size")
|
row("a", "toggle allocated / logical (apparent) size")
|
||||||
row("c", "copy path to clipboard")
|
row("c", "copy path to clipboard")
|
||||||
row("o", "reveal in file manager")
|
row("o", revealVerb())
|
||||||
if !m.opts.ReadOnly {
|
if !m.opts.ReadOnly {
|
||||||
row("d", "move to trash (asks first)")
|
row("d", deleteVerb()+" (asks first)")
|
||||||
}
|
}
|
||||||
row("s", "save snapshot")
|
row("s", "save snapshot")
|
||||||
row("r", "rescan")
|
row("r", "rescan")
|
||||||
@@ -372,3 +373,19 @@ func (md *saveModal) key(m *Model, k string) (bool, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// revealVerb and deleteVerb describe o and d, which change meaning on a
|
||||||
|
// headless system.
|
||||||
|
func revealVerb() string {
|
||||||
|
if platform.Headless() {
|
||||||
|
return "show folder in map"
|
||||||
|
}
|
||||||
|
return "reveal in file manager"
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteVerb() string {
|
||||||
|
if platform.Headless() {
|
||||||
|
return "delete permanently"
|
||||||
|
}
|
||||||
|
return "move to " + platform.TrashName
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,3 +70,20 @@ func TestNestingReachesDeepMedia(t *testing.T) {
|
|||||||
t.Fatalf("only %d episode blocks drawn", files)
|
t.Fatalf("only %d episode blocks drawn", files)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Headless, o shows the selected folder in the map instead of launching a
|
||||||
|
// file manager.
|
||||||
|
func TestRevealHeadlessZoomsMap(t *testing.T) {
|
||||||
|
t.Setenv("DISPLAY", "")
|
||||||
|
t.Setenv("WAYLAND_DISPLAY", "")
|
||||||
|
tr := rootTree()
|
||||||
|
m := newModel(Options{Start: func(context.Context) (*scan.Scanner, *inventory.Tree, error) { return nil, tr, nil }}, nil, tr, func() {})
|
||||||
|
send(m, size(160, 45))
|
||||||
|
mnt := m.kidsOf(m.zoom).ids[0]
|
||||||
|
m.snapshot = false // reveal is refused for snapshots
|
||||||
|
m.sel, m.userSel = int64(mnt), true
|
||||||
|
send(m, key("o"))
|
||||||
|
if m.zoom != mnt {
|
||||||
|
t.Fatalf("zoom = %q, want mnt", tr.Path(m.zoom))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user