6422bd7492
* Fix replace dialog for symlinks If the oldPath is a symlink to the newPath or vice versa, than os.Stat() would resolve this symlink, and both the oldStat and newStat would be the same. Hence, the replace dialog would not appear and the newPath file would be overwritten by the oldPath file whilst the oldPath would be deleted. It is the same story when the oldPath and newPath are both symlinks to the same file. * Fix completion in the case of broken symlinks If the current directory contains broken symlinks then matchFile() would return at first broken symlink. Let's consider the following example: $ ls -F ~/ broken@ dir/ file The broken@ is a symlink to ~/foo - non existent file. If one would enter the following command in lf: :cd ~/<tab> it would not suggest possible completion options because matchFile() would return as soon as it meet the broken symlink. * Don't resolve symlinks when move files/dirs (#571) This will allow to move broken symlinks. * Fix symlinks copying/moving (#571) Copy symlinks, do not try to resolve it and copy the file pointed by this symlink. Also this allows to copy symlink to directory.
1334 lines
28 KiB
Go
1334 lines
28 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
times "gopkg.in/djherbis/times.v1"
|
|
)
|
|
|
|
type linkState byte
|
|
|
|
const (
|
|
notLink linkState = iota
|
|
working
|
|
broken
|
|
)
|
|
|
|
type file struct {
|
|
os.FileInfo
|
|
linkState linkState
|
|
linkTarget string
|
|
path string
|
|
dirCount int
|
|
accessTime time.Time
|
|
changeTime time.Time
|
|
ext string
|
|
}
|
|
|
|
func readdir(path string) ([]*file, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
names, err := f.Readdirnames(-1)
|
|
f.Close()
|
|
|
|
files := make([]*file, 0, len(names))
|
|
for _, fname := range names {
|
|
fpath := filepath.Join(path, fname)
|
|
|
|
lstat, err := os.Lstat(fpath)
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return files, err
|
|
}
|
|
|
|
var linkState linkState
|
|
var linkTarget string
|
|
|
|
if lstat.Mode()&os.ModeSymlink != 0 {
|
|
stat, err := os.Stat(fpath)
|
|
if err == nil {
|
|
linkState = working
|
|
lstat = stat
|
|
} else {
|
|
linkState = broken
|
|
}
|
|
linkTarget, err = os.Readlink(fpath)
|
|
if err != nil {
|
|
log.Printf("reading link target: %s", err)
|
|
}
|
|
}
|
|
|
|
ts := times.Get(lstat)
|
|
at := ts.AccessTime()
|
|
var ct time.Time
|
|
// from times docs: ChangeTime() panics unless HasChangeTime() is true
|
|
if ts.HasChangeTime() {
|
|
ct = ts.ChangeTime()
|
|
} else {
|
|
// fall back to ModTime if ChangeTime cannot be determined
|
|
ct = lstat.ModTime()
|
|
}
|
|
|
|
// returns an empty string if extension could not be determined
|
|
// i.e. directories, filenames without extensions
|
|
ext := filepath.Ext(fpath)
|
|
|
|
files = append(files, &file{
|
|
FileInfo: lstat,
|
|
linkState: linkState,
|
|
linkTarget: linkTarget,
|
|
path: fpath,
|
|
dirCount: -1,
|
|
accessTime: at,
|
|
changeTime: ct,
|
|
ext: ext,
|
|
})
|
|
}
|
|
|
|
return files, err
|
|
}
|
|
|
|
type dir struct {
|
|
loading bool // directory is loading from disk
|
|
loadTime time.Time // current loading or last load time
|
|
ind int // index of current entry in files
|
|
pos int // position of current entry in ui
|
|
path string // full path of directory
|
|
files []*file // displayed files in directory including or excluding hidden ones
|
|
allFiles []*file // all files in directory including hidden ones (same array as files)
|
|
sortType sortType // sort method and options from last sort
|
|
hiddenfiles []string // hiddenfiles value from last sort
|
|
ignorecase bool // ignorecase value from last sort
|
|
ignoredia bool // ignoredia value from last sort
|
|
noPerm bool // whether lf has no permission to open the directory
|
|
}
|
|
|
|
func newDir(path string) *dir {
|
|
time := time.Now()
|
|
|
|
files, err := readdir(path)
|
|
if err != nil {
|
|
log.Printf("reading directory: %s", err)
|
|
}
|
|
|
|
return &dir{
|
|
loadTime: time,
|
|
path: path,
|
|
files: files,
|
|
allFiles: files,
|
|
noPerm: os.IsPermission(err),
|
|
}
|
|
}
|
|
|
|
func normalize(s1, s2 string, ignorecase, ignoredia bool) (string, string) {
|
|
if gOpts.ignorecase {
|
|
s1 = strings.ToLower(s1)
|
|
s2 = strings.ToLower(s2)
|
|
}
|
|
if gOpts.ignoredia {
|
|
s1 = removeDiacritics(s1)
|
|
s2 = removeDiacritics(s2)
|
|
}
|
|
return s1, s2
|
|
}
|
|
|
|
func (dir *dir) sort() {
|
|
dir.sortType = gOpts.sortType
|
|
dir.hiddenfiles = gOpts.hiddenfiles
|
|
dir.ignorecase = gOpts.ignorecase
|
|
dir.ignoredia = gOpts.ignoredia
|
|
|
|
dir.files = dir.allFiles
|
|
|
|
switch dir.sortType.method {
|
|
case naturalSort:
|
|
sort.SliceStable(dir.files, func(i, j int) bool {
|
|
s1, s2 := normalize(dir.files[i].Name(), dir.files[j].Name(), dir.ignorecase, dir.ignoredia)
|
|
return naturalLess(s1, s2)
|
|
})
|
|
case nameSort:
|
|
sort.SliceStable(dir.files, func(i, j int) bool {
|
|
s1, s2 := normalize(dir.files[i].Name(), dir.files[j].Name(), dir.ignorecase, dir.ignoredia)
|
|
return s1 < s2
|
|
})
|
|
case sizeSort:
|
|
sort.SliceStable(dir.files, func(i, j int) bool {
|
|
return dir.files[i].Size() < dir.files[j].Size()
|
|
})
|
|
case timeSort:
|
|
sort.SliceStable(dir.files, func(i, j int) bool {
|
|
return dir.files[i].ModTime().Before(dir.files[j].ModTime())
|
|
})
|
|
case atimeSort:
|
|
sort.SliceStable(dir.files, func(i, j int) bool {
|
|
return dir.files[i].accessTime.Before(dir.files[j].accessTime)
|
|
})
|
|
case ctimeSort:
|
|
sort.SliceStable(dir.files, func(i, j int) bool {
|
|
return dir.files[i].changeTime.Before(dir.files[j].changeTime)
|
|
})
|
|
case extSort:
|
|
sort.SliceStable(dir.files, func(i, j int) bool {
|
|
ext1, ext2 := normalize(dir.files[i].ext, dir.files[j].ext, dir.ignorecase, dir.ignoredia)
|
|
|
|
// if the extension could not be determined (directories, files without)
|
|
// use a zero byte so that these files can be ranked higher
|
|
if ext1 == "" {
|
|
ext1 = "\x00"
|
|
}
|
|
if ext2 == "" {
|
|
ext2 = "\x00"
|
|
}
|
|
|
|
name1, name2 := normalize(dir.files[i].Name(), dir.files[j].Name(), dir.ignorecase, dir.ignoredia)
|
|
|
|
// in order to also have natural sorting with the filenames
|
|
// combine the name with the ext but have the ext at the front
|
|
return ext1 < ext2 || ext1 == ext2 && name1 < name2
|
|
})
|
|
}
|
|
|
|
if dir.sortType.option&reverseSort != 0 {
|
|
for i, j := 0, len(dir.files)-1; i < j; i, j = i+1, j-1 {
|
|
dir.files[i], dir.files[j] = dir.files[j], dir.files[i]
|
|
}
|
|
}
|
|
|
|
if dir.sortType.option&dirfirstSort != 0 {
|
|
sort.SliceStable(dir.files, func(i, j int) bool {
|
|
if dir.files[i].IsDir() == dir.files[j].IsDir() {
|
|
return i < j
|
|
}
|
|
return dir.files[i].IsDir()
|
|
})
|
|
}
|
|
|
|
// when hidden option is disabled, we move hidden files to the
|
|
// beginning of our file list and then set the beginning of displayed
|
|
// files to the first non-hidden file in the list
|
|
if dir.sortType.option&hiddenSort == 0 {
|
|
sort.SliceStable(dir.files, func(i, j int) bool {
|
|
if isHidden(dir.files[i], dir.path, dir.hiddenfiles) && isHidden(dir.files[j], dir.path, dir.hiddenfiles) {
|
|
return i < j
|
|
}
|
|
return isHidden(dir.files[i], dir.path, dir.hiddenfiles)
|
|
})
|
|
for i, f := range dir.files {
|
|
if !isHidden(f, dir.path, dir.hiddenfiles) {
|
|
dir.files = dir.files[i:]
|
|
return
|
|
}
|
|
}
|
|
dir.files = dir.files[len(dir.files):]
|
|
}
|
|
}
|
|
|
|
func (dir *dir) name() string {
|
|
if len(dir.files) == 0 {
|
|
return ""
|
|
}
|
|
return dir.files[dir.ind].Name()
|
|
}
|
|
|
|
func (dir *dir) sel(name string, height int) {
|
|
if len(dir.files) == 0 {
|
|
dir.ind, dir.pos = 0, 0
|
|
return
|
|
}
|
|
|
|
dir.ind = max(dir.ind, 0)
|
|
dir.ind = min(dir.ind, len(dir.files)-1)
|
|
|
|
if dir.files[dir.ind].Name() != name {
|
|
for i, f := range dir.files {
|
|
if f.Name() == name {
|
|
dir.ind = i
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
edge := min(min(height/2, gOpts.scrolloff), len(dir.files)-dir.ind-1)
|
|
dir.pos = min(dir.ind, height-edge-1)
|
|
}
|
|
|
|
type nav struct {
|
|
dirs []*dir
|
|
copyBytes int64
|
|
copyTotal int64
|
|
copyUpdate int
|
|
moveCount int
|
|
moveTotal int
|
|
moveUpdate int
|
|
deleteCount int
|
|
deleteTotal int
|
|
deleteUpdate int
|
|
copyBytesChan chan int64
|
|
copyTotalChan chan int64
|
|
moveCountChan chan int
|
|
moveTotalChan chan int
|
|
deleteCountChan chan int
|
|
deleteTotalChan chan int
|
|
previewChan chan string
|
|
dirChan chan *dir
|
|
regChan chan *reg
|
|
dirCache map[string]*dir
|
|
regCache map[string]*reg
|
|
saves map[string]bool
|
|
marks map[string]string
|
|
renameOldPath string
|
|
renameNewPath string
|
|
selections map[string]int
|
|
selectionInd int
|
|
height int
|
|
find string
|
|
findBack bool
|
|
search string
|
|
searchBack bool
|
|
searchInd int
|
|
searchPos int
|
|
volatilePreview bool
|
|
}
|
|
|
|
func (nav *nav) loadDir(path string) *dir {
|
|
d, ok := nav.dirCache[path]
|
|
if !ok {
|
|
d := &dir{
|
|
loading: true,
|
|
loadTime: time.Now(),
|
|
path: path,
|
|
sortType: gOpts.sortType,
|
|
hiddenfiles: gOpts.hiddenfiles,
|
|
ignorecase: gOpts.ignorecase,
|
|
ignoredia: gOpts.ignoredia,
|
|
}
|
|
nav.dirCache[path] = d
|
|
go func() {
|
|
d := newDir(path)
|
|
d.sort()
|
|
d.ind, d.pos = 0, 0
|
|
nav.dirChan <- d
|
|
}()
|
|
return d
|
|
}
|
|
|
|
nav.checkDir(d)
|
|
|
|
return d
|
|
}
|
|
|
|
func (nav *nav) checkDir(dir *dir) {
|
|
s, err := os.Stat(dir.path)
|
|
if err != nil {
|
|
log.Printf("getting directory info: %s", err)
|
|
return
|
|
}
|
|
|
|
switch {
|
|
case s.ModTime().After(dir.loadTime):
|
|
now := time.Now()
|
|
|
|
// XXX: Linux builtin exFAT drivers are able to predict modifications in the future
|
|
// https://bugs.launchpad.net/ubuntu/+source/ubuntu-meta/+bug/1872504
|
|
if s.ModTime().After(now) {
|
|
return
|
|
}
|
|
|
|
dir.loading = true
|
|
dir.loadTime = now
|
|
go func() {
|
|
nd := newDir(dir.path)
|
|
nd.sort()
|
|
nav.dirChan <- nd
|
|
}()
|
|
case dir.sortType != gOpts.sortType ||
|
|
!reflect.DeepEqual(dir.hiddenfiles, gOpts.hiddenfiles) ||
|
|
dir.ignorecase != gOpts.ignorecase ||
|
|
dir.ignoredia != gOpts.ignoredia:
|
|
dir.loading = true
|
|
go func() {
|
|
dir.sort()
|
|
dir.loading = false
|
|
nav.dirChan <- dir
|
|
}()
|
|
}
|
|
}
|
|
|
|
func (nav *nav) getDirs(wd string) {
|
|
var dirs []*dir
|
|
|
|
for curr, base := wd, ""; !isRoot(base); curr, base = filepath.Dir(curr), filepath.Base(curr) {
|
|
dir := nav.loadDir(curr)
|
|
dir.sel(base, nav.height)
|
|
dirs = append(dirs, dir)
|
|
}
|
|
|
|
for i, j := 0, len(dirs)-1; i < j; i, j = i+1, j-1 {
|
|
dirs[i], dirs[j] = dirs[j], dirs[i]
|
|
}
|
|
|
|
nav.dirs = dirs
|
|
}
|
|
|
|
func newNav(height int) *nav {
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
log.Printf("getting current directory: %s", err)
|
|
}
|
|
|
|
nav := &nav{
|
|
copyBytesChan: make(chan int64, 1024),
|
|
copyTotalChan: make(chan int64, 1024),
|
|
moveCountChan: make(chan int, 1024),
|
|
moveTotalChan: make(chan int, 1024),
|
|
deleteCountChan: make(chan int, 1024),
|
|
deleteTotalChan: make(chan int, 1024),
|
|
previewChan: make(chan string, 1024),
|
|
dirChan: make(chan *dir),
|
|
regChan: make(chan *reg),
|
|
dirCache: make(map[string]*dir),
|
|
regCache: make(map[string]*reg),
|
|
saves: make(map[string]bool),
|
|
marks: make(map[string]string),
|
|
selections: make(map[string]int),
|
|
selectionInd: 0,
|
|
height: height,
|
|
}
|
|
|
|
nav.getDirs(wd)
|
|
|
|
return nav
|
|
}
|
|
|
|
func (nav *nav) renew() {
|
|
for _, d := range nav.dirs {
|
|
nav.checkDir(d)
|
|
}
|
|
|
|
for m := range nav.selections {
|
|
if _, err := os.Lstat(m); os.IsNotExist(err) {
|
|
delete(nav.selections, m)
|
|
}
|
|
}
|
|
|
|
if len(nav.selections) == 0 {
|
|
nav.selectionInd = 0
|
|
}
|
|
}
|
|
|
|
func (nav *nav) reload() error {
|
|
nav.dirCache = make(map[string]*dir)
|
|
nav.regCache = make(map[string]*reg)
|
|
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return fmt.Errorf("getting current directory: %s", err)
|
|
}
|
|
|
|
curr, err := nav.currFile()
|
|
nav.getDirs(wd)
|
|
if err == nil {
|
|
last := nav.dirs[len(nav.dirs)-1]
|
|
last.files = append(last.files, curr)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (nav *nav) position() {
|
|
path := nav.currDir().path
|
|
for i := len(nav.dirs) - 2; i >= 0; i-- {
|
|
nav.dirs[i].sel(filepath.Base(path), nav.height)
|
|
path = filepath.Dir(path)
|
|
}
|
|
}
|
|
|
|
func (nav *nav) previewLoop(ui *ui) {
|
|
var prev string
|
|
for path := range nav.previewChan {
|
|
clear := len(path) == 0
|
|
loop:
|
|
for {
|
|
select {
|
|
case path = <-nav.previewChan:
|
|
clear = clear || len(path) == 0
|
|
default:
|
|
break loop
|
|
}
|
|
}
|
|
if clear && len(gOpts.previewer) != 0 && len(gOpts.cleaner) != 0 && nav.volatilePreview {
|
|
cmd := exec.Command(gOpts.cleaner, prev)
|
|
if err := cmd.Run(); err != nil {
|
|
log.Printf("cleaning preview: %s", err)
|
|
}
|
|
nav.volatilePreview = false
|
|
}
|
|
if len(path) != 0 {
|
|
win := ui.wins[len(ui.wins)-1]
|
|
nav.preview(path, win)
|
|
prev = path
|
|
}
|
|
}
|
|
}
|
|
|
|
func (nav *nav) preview(path string, win *win) {
|
|
reg := ®{loadTime: time.Now(), path: path}
|
|
defer func() { nav.regChan <- reg }()
|
|
|
|
var reader io.Reader
|
|
|
|
if len(gOpts.previewer) != 0 {
|
|
exportOpts()
|
|
cmd := exec.Command(gOpts.previewer, path,
|
|
strconv.Itoa(win.w),
|
|
strconv.Itoa(win.h),
|
|
strconv.Itoa(win.x),
|
|
strconv.Itoa(win.y))
|
|
|
|
out, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
log.Printf("previewing file: %s", err)
|
|
return
|
|
}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
log.Printf("previewing file: %s", err)
|
|
out.Close()
|
|
return
|
|
}
|
|
|
|
defer func() {
|
|
if err := cmd.Wait(); err != nil {
|
|
if e, ok := err.(*exec.ExitError); ok {
|
|
if e.ExitCode() != 0 {
|
|
reg.volatile = true
|
|
nav.volatilePreview = true
|
|
}
|
|
} else {
|
|
log.Printf("loading file: %s", err)
|
|
}
|
|
}
|
|
}()
|
|
defer out.Close()
|
|
reader = out
|
|
} else {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
log.Printf("opening file: %s", err)
|
|
return
|
|
}
|
|
|
|
defer f.Close()
|
|
reader = f
|
|
}
|
|
|
|
buf := bufio.NewScanner(reader)
|
|
|
|
for i := 0; i < win.h && buf.Scan(); i++ {
|
|
for _, r := range buf.Text() {
|
|
if r == 0 {
|
|
reg.lines = []string{"\033[7mbinary\033[0m"}
|
|
return
|
|
}
|
|
}
|
|
reg.lines = append(reg.lines, buf.Text())
|
|
}
|
|
|
|
if buf.Err() != nil {
|
|
log.Printf("loading file: %s", buf.Err())
|
|
}
|
|
}
|
|
|
|
func (nav *nav) loadReg(path string, volatile bool) *reg {
|
|
r, ok := nav.regCache[path]
|
|
if !ok || (volatile && r.volatile) {
|
|
r := ®{loading: true, loadTime: time.Now(), path: path, volatile: true}
|
|
nav.regCache[path] = r
|
|
nav.previewChan <- path
|
|
return r
|
|
}
|
|
|
|
nav.checkReg(r)
|
|
|
|
return r
|
|
}
|
|
|
|
func (nav *nav) checkReg(reg *reg) {
|
|
s, err := os.Stat(reg.path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
// XXX: Linux builtin exFAT drivers are able to predict modifications in the future
|
|
// https://bugs.launchpad.net/ubuntu/+source/ubuntu-meta/+bug/1872504
|
|
if s.ModTime().After(now) {
|
|
return
|
|
}
|
|
|
|
if s.ModTime().After(reg.loadTime) {
|
|
reg.loadTime = now
|
|
nav.previewChan <- reg.path
|
|
}
|
|
}
|
|
|
|
func (nav *nav) sort() {
|
|
for _, d := range nav.dirs {
|
|
name := d.name()
|
|
d.sort()
|
|
d.sel(name, nav.height)
|
|
}
|
|
}
|
|
|
|
func (nav *nav) up(dist int) bool {
|
|
dir := nav.currDir()
|
|
|
|
old := dir.ind
|
|
|
|
if dir.ind == 0 {
|
|
if gOpts.wrapscroll {
|
|
nav.bottom()
|
|
}
|
|
return old != dir.ind
|
|
}
|
|
|
|
dir.ind -= dist
|
|
dir.ind = max(0, dir.ind)
|
|
|
|
dir.pos -= dist
|
|
edge := min(min(nav.height/2, gOpts.scrolloff), dir.ind)
|
|
dir.pos = max(dir.pos, edge)
|
|
|
|
return old != dir.ind
|
|
}
|
|
|
|
func (nav *nav) down(dist int) bool {
|
|
dir := nav.currDir()
|
|
|
|
old := dir.ind
|
|
|
|
maxind := len(dir.files) - 1
|
|
|
|
if dir.ind >= maxind {
|
|
if gOpts.wrapscroll {
|
|
nav.top()
|
|
}
|
|
return old != dir.ind
|
|
}
|
|
|
|
dir.ind += dist
|
|
dir.ind = min(maxind, dir.ind)
|
|
|
|
dir.pos += dist
|
|
edge := min(min(nav.height/2, gOpts.scrolloff), maxind-dir.ind)
|
|
|
|
// use a smaller value when the height is even and scrolloff is maxed
|
|
// in order to stay at the same row as much as possible while up/down
|
|
edge = min(edge, nav.height/2+nav.height%2-1)
|
|
|
|
dir.pos = min(dir.pos, nav.height-edge-1)
|
|
dir.pos = min(dir.pos, maxind)
|
|
|
|
return old != dir.ind
|
|
}
|
|
|
|
func (nav *nav) updir() error {
|
|
if len(nav.dirs) <= 1 {
|
|
return nil
|
|
}
|
|
|
|
dir := nav.currDir()
|
|
|
|
nav.dirs = nav.dirs[:len(nav.dirs)-1]
|
|
|
|
if err := os.Chdir(filepath.Dir(dir.path)); err != nil {
|
|
return fmt.Errorf("updir: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (nav *nav) open() error {
|
|
curr, err := nav.currFile()
|
|
if err != nil {
|
|
return fmt.Errorf("open: %s", err)
|
|
}
|
|
|
|
path := curr.path
|
|
|
|
dir := nav.loadDir(path)
|
|
|
|
nav.dirs = append(nav.dirs, dir)
|
|
|
|
if err := os.Chdir(path); err != nil {
|
|
return fmt.Errorf("open: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (nav *nav) top() bool {
|
|
dir := nav.currDir()
|
|
|
|
old := dir.ind
|
|
|
|
dir.ind = 0
|
|
dir.pos = 0
|
|
|
|
return old != dir.ind
|
|
}
|
|
|
|
func (nav *nav) bottom() bool {
|
|
dir := nav.currDir()
|
|
|
|
old := dir.ind
|
|
|
|
dir.ind = len(dir.files) - 1
|
|
dir.pos = min(dir.ind, nav.height-1)
|
|
|
|
return old != dir.ind
|
|
}
|
|
|
|
func (nav *nav) toggleSelection(path string) {
|
|
if _, ok := nav.selections[path]; ok {
|
|
delete(nav.selections, path)
|
|
if len(nav.selections) == 0 {
|
|
nav.selectionInd = 0
|
|
}
|
|
} else {
|
|
nav.selections[path] = nav.selectionInd
|
|
nav.selectionInd++
|
|
}
|
|
}
|
|
|
|
func (nav *nav) toggle() {
|
|
curr, err := nav.currFile()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
nav.toggleSelection(curr.path)
|
|
}
|
|
|
|
func (nav *nav) invert() {
|
|
dir := nav.currDir()
|
|
for _, f := range dir.files {
|
|
path := filepath.Join(dir.path, f.Name())
|
|
nav.toggleSelection(path)
|
|
}
|
|
}
|
|
|
|
func (nav *nav) unselect() {
|
|
nav.selections = make(map[string]int)
|
|
nav.selectionInd = 0
|
|
}
|
|
|
|
func (nav *nav) save(cp bool) error {
|
|
list, err := nav.currFileOrSelections()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := saveFiles(list, cp); err != nil {
|
|
return err
|
|
}
|
|
|
|
nav.saves = make(map[string]bool)
|
|
for _, f := range list {
|
|
nav.saves[f] = cp
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (nav *nav) copyAsync(ui *ui, srcs []string, dstDir string) {
|
|
echo := &callExpr{"echoerr", []string{""}, 1}
|
|
|
|
_, err := os.Stat(dstDir)
|
|
if os.IsNotExist(err) {
|
|
echo.args[0] = err.Error()
|
|
ui.exprChan <- echo
|
|
return
|
|
}
|
|
|
|
total, err := copySize(srcs)
|
|
if err != nil {
|
|
echo.args[0] = err.Error()
|
|
ui.exprChan <- echo
|
|
return
|
|
}
|
|
|
|
nav.copyTotalChan <- total
|
|
|
|
nums, errs := copyAll(srcs, dstDir)
|
|
|
|
errCount := 0
|
|
loop:
|
|
for {
|
|
select {
|
|
case n := <-nums:
|
|
nav.copyBytesChan <- n
|
|
case err, ok := <-errs:
|
|
if !ok {
|
|
break loop
|
|
}
|
|
errCount++
|
|
echo.args[0] = fmt.Sprintf("[%d] %s", errCount, err)
|
|
ui.exprChan <- echo
|
|
}
|
|
}
|
|
|
|
nav.copyTotalChan <- -total
|
|
|
|
if err := remote("send load"); err != nil {
|
|
errCount++
|
|
echo.args[0] = fmt.Sprintf("[%d] %s", errCount, err)
|
|
ui.exprChan <- echo
|
|
}
|
|
|
|
if errCount == 0 {
|
|
ui.exprChan <- &callExpr{"echo", []string{"\033[0;32mCopied successfully\033[0m"}, 1}
|
|
}
|
|
}
|
|
|
|
func (nav *nav) moveAsync(ui *ui, srcs []string, dstDir string) {
|
|
echo := &callExpr{"echoerr", []string{""}, 1}
|
|
|
|
_, err := os.Stat(dstDir)
|
|
if os.IsNotExist(err) {
|
|
echo.args[0] = err.Error()
|
|
ui.exprChan <- echo
|
|
return
|
|
}
|
|
|
|
nav.moveTotalChan <- len(srcs)
|
|
|
|
errCount := 0
|
|
for _, src := range srcs {
|
|
nav.moveCountChan <- 1
|
|
|
|
srcStat, err := os.Lstat(src)
|
|
if err != nil {
|
|
errCount++
|
|
echo.args[0] = fmt.Sprintf("[%d] %s", errCount, err)
|
|
ui.exprChan <- echo
|
|
continue
|
|
}
|
|
|
|
dst := filepath.Join(dstDir, filepath.Base(src))
|
|
|
|
dstStat, err := os.Stat(dst)
|
|
if os.SameFile(srcStat, dstStat) {
|
|
errCount++
|
|
echo.args[0] = fmt.Sprintf("[%d] rename %s %s: source and destination are the same file", errCount, src, dst)
|
|
ui.exprChan <- echo
|
|
continue
|
|
} else if !os.IsNotExist(err) {
|
|
var newPath string
|
|
for i := 1; !os.IsNotExist(err); i++ {
|
|
newPath = fmt.Sprintf("%s.~%d~", dst, i)
|
|
_, err = os.Lstat(newPath)
|
|
}
|
|
dst = newPath
|
|
}
|
|
|
|
if err := os.Rename(src, dst); err != nil {
|
|
if errCrossDevice(err) {
|
|
total, err := copySize([]string{src})
|
|
if err != nil {
|
|
echo.args[0] = err.Error()
|
|
ui.exprChan <- echo
|
|
continue
|
|
}
|
|
|
|
nav.copyTotalChan <- total
|
|
|
|
nums, errs := copyAll([]string{src}, dstDir)
|
|
|
|
oldCount := errCount
|
|
loop:
|
|
for {
|
|
select {
|
|
case n := <-nums:
|
|
nav.copyBytesChan <- n
|
|
case err, ok := <-errs:
|
|
if !ok {
|
|
break loop
|
|
}
|
|
errCount++
|
|
echo.args[0] = fmt.Sprintf("[%d] %s", errCount, err)
|
|
ui.exprChan <- echo
|
|
}
|
|
}
|
|
|
|
nav.copyTotalChan <- -total
|
|
|
|
if errCount == oldCount {
|
|
if err := os.RemoveAll(src); err != nil {
|
|
errCount++
|
|
echo.args[0] = fmt.Sprintf("[%d] %s", errCount, err)
|
|
ui.exprChan <- echo
|
|
}
|
|
}
|
|
} else {
|
|
errCount++
|
|
echo.args[0] = fmt.Sprintf("[%d] %s", errCount, err)
|
|
ui.exprChan <- echo
|
|
}
|
|
}
|
|
}
|
|
|
|
nav.moveTotalChan <- -len(srcs)
|
|
|
|
if err := remote("send load"); err != nil {
|
|
errCount++
|
|
echo.args[0] = fmt.Sprintf("[%d] %s", errCount, err)
|
|
ui.exprChan <- echo
|
|
}
|
|
|
|
if errCount == 0 {
|
|
ui.exprChan <- &callExpr{"echo", []string{"\033[0;32mMoved successfully\033[0m"}, 1}
|
|
}
|
|
}
|
|
|
|
func (nav *nav) paste(ui *ui) error {
|
|
srcs, cp, err := loadFiles()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(srcs) == 0 {
|
|
return errors.New("no file in copy/cut buffer")
|
|
}
|
|
|
|
dstDir := nav.currDir().path
|
|
|
|
if cp {
|
|
go nav.copyAsync(ui, srcs, dstDir)
|
|
} else {
|
|
go nav.moveAsync(ui, srcs, dstDir)
|
|
}
|
|
|
|
if err := saveFiles(nil, false); err != nil {
|
|
return fmt.Errorf("clearing copy/cut buffer: %s", err)
|
|
}
|
|
|
|
if err := remote("send sync"); err != nil {
|
|
return fmt.Errorf("paste: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (nav *nav) del(ui *ui) error {
|
|
list, err := nav.currFileOrSelections()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
go func() {
|
|
echo := &callExpr{"echoerr", []string{""}, 1}
|
|
errCount := 0
|
|
|
|
nav.deleteTotalChan <- len(list)
|
|
|
|
for _, path := range list {
|
|
nav.deleteCountChan <- 1
|
|
|
|
if err := os.RemoveAll(path); err != nil {
|
|
errCount++
|
|
echo.args[0] = fmt.Sprintf("[%d] %s", errCount, err)
|
|
ui.exprChan <- echo
|
|
}
|
|
}
|
|
|
|
nav.deleteTotalChan <- -len(list)
|
|
|
|
if err := remote("send load"); err != nil {
|
|
errCount++
|
|
echo.args[0] = fmt.Sprintf("[%d] %s", errCount, err)
|
|
ui.exprChan <- echo
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (nav *nav) rename() error {
|
|
oldPath := nav.renameOldPath
|
|
newPath := nav.renameNewPath
|
|
|
|
if err := os.Rename(oldPath, newPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
lstat, err := os.Lstat(newPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dir := nav.loadDir(filepath.Dir(newPath))
|
|
|
|
if dir.loading {
|
|
dir.files = append(dir.files, &file{FileInfo: lstat})
|
|
}
|
|
|
|
dir.sel(lstat.Name(), nav.height)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (nav *nav) sync() error {
|
|
list, cp, err := loadFiles()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nav.saves = make(map[string]bool)
|
|
for _, f := range list {
|
|
nav.saves[f] = cp
|
|
}
|
|
|
|
return nav.readMarks()
|
|
}
|
|
|
|
func (nav *nav) cd(wd string) error {
|
|
wd = replaceTilde(wd)
|
|
wd = filepath.Clean(wd)
|
|
|
|
if !filepath.IsAbs(wd) {
|
|
wd = filepath.Join(nav.currDir().path, wd)
|
|
}
|
|
|
|
if err := os.Chdir(wd); err != nil {
|
|
return fmt.Errorf("cd: %s", err)
|
|
}
|
|
|
|
nav.getDirs(wd)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (nav *nav) sel(path string) error {
|
|
path = replaceTilde(path)
|
|
path = filepath.Clean(path)
|
|
|
|
lstat, err := os.Lstat(path)
|
|
if err != nil {
|
|
return fmt.Errorf("select: %s", err)
|
|
}
|
|
|
|
dir := filepath.Dir(path)
|
|
|
|
if err := nav.cd(dir); err != nil {
|
|
return fmt.Errorf("select: %s", err)
|
|
}
|
|
|
|
base := filepath.Base(path)
|
|
|
|
last := nav.dirs[len(nav.dirs)-1]
|
|
|
|
if last.loading {
|
|
last.files = append(last.files, &file{FileInfo: lstat})
|
|
}
|
|
|
|
last.sel(base, nav.height)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (nav *nav) globSel(pattern string, invert bool) error {
|
|
dir := nav.currDir()
|
|
anyMatched := false
|
|
|
|
for i := 0; i < len(dir.files); i++ {
|
|
matched, err := filepath.Match(pattern, dir.files[i].Name())
|
|
if err != nil {
|
|
return fmt.Errorf("glob-select: %s", err)
|
|
}
|
|
if matched {
|
|
anyMatched = true
|
|
fpath := filepath.Join(dir.path, dir.files[i].Name())
|
|
if _, ok := nav.selections[fpath]; ok == invert {
|
|
nav.toggleSelection(fpath)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !anyMatched {
|
|
return fmt.Errorf("glob-select: pattern not found: %s", pattern)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func findMatch(name, pattern string) bool {
|
|
if gOpts.ignorecase {
|
|
lpattern := strings.ToLower(pattern)
|
|
if !gOpts.smartcase || lpattern == pattern {
|
|
pattern = lpattern
|
|
name = strings.ToLower(name)
|
|
}
|
|
}
|
|
if gOpts.ignoredia {
|
|
lpattern := removeDiacritics(pattern)
|
|
if !gOpts.smartdia || lpattern == pattern {
|
|
pattern = lpattern
|
|
name = removeDiacritics(name)
|
|
}
|
|
}
|
|
if gOpts.anchorfind {
|
|
return strings.HasPrefix(name, pattern)
|
|
}
|
|
return strings.Contains(name, pattern)
|
|
}
|
|
|
|
func (nav *nav) findSingle() int {
|
|
count := 0
|
|
index := 0
|
|
dir := nav.currDir()
|
|
for i := 0; i < len(dir.files); i++ {
|
|
if findMatch(dir.files[i].Name(), nav.find) {
|
|
count++
|
|
if count > 1 {
|
|
return count
|
|
}
|
|
index = i
|
|
}
|
|
}
|
|
if count == 1 {
|
|
if index > dir.ind {
|
|
nav.down(index - dir.ind)
|
|
} else {
|
|
nav.up(dir.ind - index)
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func (nav *nav) findNext() (bool, bool) {
|
|
dir := nav.currDir()
|
|
for i := dir.ind + 1; i < len(dir.files); i++ {
|
|
if findMatch(dir.files[i].Name(), nav.find) {
|
|
return nav.down(i - dir.ind), true
|
|
}
|
|
}
|
|
if gOpts.wrapscan {
|
|
for i := 0; i < dir.ind; i++ {
|
|
if findMatch(dir.files[i].Name(), nav.find) {
|
|
return nav.up(dir.ind - i), true
|
|
}
|
|
}
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
func (nav *nav) findPrev() (bool, bool) {
|
|
dir := nav.currDir()
|
|
for i := dir.ind - 1; i >= 0; i-- {
|
|
if findMatch(dir.files[i].Name(), nav.find) {
|
|
return nav.up(dir.ind - i), true
|
|
}
|
|
}
|
|
if gOpts.wrapscan {
|
|
for i := len(dir.files) - 1; i > dir.ind; i-- {
|
|
if findMatch(dir.files[i].Name(), nav.find) {
|
|
return nav.down(i - dir.ind), true
|
|
}
|
|
}
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
func searchMatch(name, pattern string) (matched bool, err error) {
|
|
if gOpts.ignorecase {
|
|
lpattern := strings.ToLower(pattern)
|
|
if !gOpts.smartcase || lpattern == pattern {
|
|
pattern = lpattern
|
|
name = strings.ToLower(name)
|
|
}
|
|
}
|
|
if gOpts.ignoredia {
|
|
lpattern := removeDiacritics(pattern)
|
|
if !gOpts.smartdia || lpattern == pattern {
|
|
pattern = lpattern
|
|
name = removeDiacritics(name)
|
|
}
|
|
}
|
|
if gOpts.globsearch {
|
|
return filepath.Match(pattern, name)
|
|
}
|
|
return strings.Contains(name, pattern), nil
|
|
}
|
|
|
|
func (nav *nav) searchNext() (bool, error) {
|
|
dir := nav.currDir()
|
|
for i := dir.ind + 1; i < len(dir.files); i++ {
|
|
if matched, err := searchMatch(dir.files[i].Name(), nav.search); err != nil {
|
|
return false, err
|
|
} else if matched {
|
|
return nav.down(i - dir.ind), nil
|
|
}
|
|
}
|
|
if gOpts.wrapscan {
|
|
for i := 0; i < dir.ind; i++ {
|
|
if matched, err := searchMatch(dir.files[i].Name(), nav.search); err != nil {
|
|
return false, err
|
|
} else if matched {
|
|
return nav.up(dir.ind - i), nil
|
|
}
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (nav *nav) searchPrev() (bool, error) {
|
|
dir := nav.currDir()
|
|
for i := dir.ind - 1; i >= 0; i-- {
|
|
if matched, err := searchMatch(dir.files[i].Name(), nav.search); err != nil {
|
|
return false, err
|
|
} else if matched {
|
|
return nav.up(dir.ind - i), nil
|
|
}
|
|
}
|
|
if gOpts.wrapscan {
|
|
for i := len(dir.files) - 1; i > dir.ind; i-- {
|
|
if matched, err := searchMatch(dir.files[i].Name(), nav.search); err != nil {
|
|
return false, err
|
|
} else if matched {
|
|
return nav.down(i - dir.ind), nil
|
|
}
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (nav *nav) removeMark(mark string) error {
|
|
if _, ok := nav.marks[mark]; ok {
|
|
delete(nav.marks, mark)
|
|
return nil
|
|
}
|
|
return fmt.Errorf("no such mark")
|
|
}
|
|
|
|
func (nav *nav) readMarks() error {
|
|
nav.marks = make(map[string]string)
|
|
f, err := os.Open(gMarksPath)
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("opening marks file: %s", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
for scanner.Scan() {
|
|
toks := strings.SplitN(scanner.Text(), ":", 2)
|
|
if _, ok := nav.marks[toks[0]]; !ok {
|
|
nav.marks[toks[0]] = toks[1]
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return fmt.Errorf("reading marks file: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (nav *nav) writeMarks() error {
|
|
if err := os.MkdirAll(filepath.Dir(gMarksPath), os.ModePerm); err != nil {
|
|
return fmt.Errorf("creating data directory: %s", err)
|
|
}
|
|
|
|
f, err := os.Create(gMarksPath)
|
|
if err != nil {
|
|
return fmt.Errorf("creating marks file: %s", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
var keys []string
|
|
for k := range nav.marks {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
for _, k := range keys {
|
|
_, err = f.WriteString(fmt.Sprintf("%s:%s\n", k, nav.marks[k]))
|
|
if err != nil {
|
|
return fmt.Errorf("writing marks file: %s", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (nav *nav) currDir() *dir {
|
|
return nav.dirs[len(nav.dirs)-1]
|
|
}
|
|
|
|
func (nav *nav) currFile() (*file, error) {
|
|
dir := nav.dirs[len(nav.dirs)-1]
|
|
|
|
if len(dir.files) == 0 {
|
|
return nil, fmt.Errorf("empty directory")
|
|
}
|
|
return dir.files[dir.ind], nil
|
|
}
|
|
|
|
type indexedSelections struct {
|
|
paths []string
|
|
indices []int
|
|
}
|
|
|
|
func (m indexedSelections) Len() int { return len(m.paths) }
|
|
|
|
func (m indexedSelections) Swap(i, j int) {
|
|
m.paths[i], m.paths[j] = m.paths[j], m.paths[i]
|
|
m.indices[i], m.indices[j] = m.indices[j], m.indices[i]
|
|
}
|
|
|
|
func (m indexedSelections) Less(i, j int) bool { return m.indices[i] < m.indices[j] }
|
|
|
|
func (nav *nav) currSelections() []string {
|
|
paths := make([]string, 0, len(nav.selections))
|
|
indices := make([]int, 0, len(nav.selections))
|
|
for path, index := range nav.selections {
|
|
paths = append(paths, path)
|
|
indices = append(indices, index)
|
|
}
|
|
sort.Sort(indexedSelections{paths: paths, indices: indices})
|
|
return paths
|
|
}
|
|
|
|
func (nav *nav) currFileOrSelections() (list []string, err error) {
|
|
if len(nav.selections) == 0 {
|
|
curr, err := nav.currFile()
|
|
if err != nil {
|
|
return nil, errors.New("no file selected")
|
|
}
|
|
|
|
return []string{curr.path}, nil
|
|
}
|
|
|
|
return nav.currSelections(), nil
|
|
}
|