lf/nav.go

1270 lines
26 KiB
Go
Raw Normal View History

2016-08-13 12:49:04 +00:00
package main
import (
"bufio"
2016-08-13 12:49:04 +00:00
"errors"
"fmt"
"io"
2016-08-13 12:49:04 +00:00
"log"
"os"
"os/exec"
"path/filepath"
"reflect"
2016-11-10 21:18:56 +00:00
"sort"
"strconv"
2016-08-13 12:49:04 +00:00
"strings"
"time"
times "gopkg.in/djherbis/times.v1"
2016-08-13 12:49:04 +00:00
)
2016-12-18 15:01:45 +00:00
type linkState byte
const (
2016-12-17 21:47:37 +00:00
notLink linkState = iota
working
broken
)
2016-12-17 21:47:37 +00:00
type file struct {
os.FileInfo
linkState linkState
linkTarget string
path string
dirCount int
accessTime time.Time
changeTime time.Time
ext string
2016-11-10 21:18:56 +00:00
}
2016-12-17 21:47:37 +00:00
func readdir(path string) ([]*file, error) {
f, err := os.Open(path)
2016-08-13 12:49:04 +00:00
if err != nil {
return nil, err
2016-08-13 12:49:04 +00:00
}
names, err := f.Readdirnames(-1)
f.Close()
2018-05-20 17:30:41 +00:00
files := make([]*file, 0, len(names))
for _, fname := range names {
fpath := filepath.Join(path, fname)
2018-05-20 17:30:41 +00:00
lstat, err := os.Lstat(fpath)
if os.IsNotExist(err) {
continue
}
2018-05-20 17:30:41 +00:00
if err != nil {
return files, err
}
2016-12-17 21:47:37 +00:00
var linkState linkState
var linkTarget string
if lstat.Mode()&os.ModeSymlink != 0 {
2018-05-20 17:30:41 +00:00
stat, err := os.Stat(fpath)
if err == nil {
2016-12-17 21:47:37 +00:00
linkState = working
lstat = stat
} else {
2016-12-17 21:47:37 +00:00
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)
2018-05-20 17:30:41 +00:00
files = append(files, &file{
FileInfo: lstat,
linkState: linkState,
linkTarget: linkTarget,
path: fpath,
dirCount: -1,
accessTime: at,
changeTime: ct,
ext: ext,
})
}
2017-11-19 18:55:13 +00:00
2018-05-20 17:30:41 +00:00
return files, err
}
2016-08-13 12:49:04 +00:00
2016-12-17 21:47:37 +00:00
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
}
2017-11-19 18:55:13 +00:00
func newDir(path string) *dir {
time := time.Now()
2018-05-20 17:30:41 +00:00
files, err := readdir(path)
if err != nil {
log.Printf("reading directory: %s", err)
}
2017-11-19 18:55:13 +00:00
return &dir{
loadTime: time,
path: path,
2018-05-20 17:30:41 +00:00
files: files,
allFiles: files,
noPerm: os.IsPermission(err),
2016-08-13 12:49:04 +00:00
}
}
func normalize(s1, s2 string) (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
}
2017-11-19 18:55:13 +00:00
func (dir *dir) sort() {
2018-04-18 20:08:28 +00:00
dir.sortType = gOpts.sortType
dir.hiddenfiles = gOpts.hiddenfiles
dir.ignorecase = gOpts.ignorecase
dir.ignoredia = gOpts.ignoredia
2018-04-18 20:08:28 +00:00
2018-05-20 17:30:41 +00:00
dir.files = dir.allFiles
2018-04-18 20:08:28 +00:00
switch gOpts.sortType.method {
case naturalSort:
2018-05-20 17:30:41 +00:00
sort.SliceStable(dir.files, func(i, j int) bool {
s1, s2 := normalize(dir.files[i].Name(), dir.files[j].Name())
return naturalLess(s1, s2)
})
2018-04-18 20:08:28 +00:00
case nameSort:
2018-05-20 17:30:41 +00:00
sort.SliceStable(dir.files, func(i, j int) bool {
s1, s2 := normalize(dir.files[i].Name(), dir.files[j].Name())
return s1 < s2
})
2018-04-18 20:08:28 +00:00
case sizeSort:
2018-05-20 17:30:41 +00:00
sort.SliceStable(dir.files, func(i, j int) bool {
return dir.files[i].Size() < dir.files[j].Size()
})
2018-04-18 20:08:28 +00:00
case timeSort:
2018-05-20 17:30:41 +00:00
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)
// 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())
// 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 + name1) < (ext2 + name2)
})
}
2018-04-18 20:08:28 +00:00
if gOpts.sortType.option&reverseSort != 0 {
2018-05-20 17:30:41 +00:00
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]
}
}
2018-04-18 20:08:28 +00:00
if gOpts.sortType.option&dirfirstSort != 0 {
2018-05-20 17:30:41 +00:00
sort.SliceStable(dir.files, func(i, j int) bool {
if dir.files[i].IsDir() == dir.files[j].IsDir() {
return i < j
}
2018-05-20 17:30:41 +00:00
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
2018-04-18 20:08:28 +00:00
if gOpts.sortType.option&hiddenSort == 0 {
2018-05-20 17:30:41 +00:00
sort.SliceStable(dir.files, func(i, j int) bool {
if isHidden(dir.files[i], dir.path) && isHidden(dir.files[j], dir.path) {
return i < j
}
return isHidden(dir.files[i], dir.path)
})
2018-05-20 17:30:41 +00:00
for i, f := range dir.files {
if !isHidden(f, dir.path) {
2018-05-20 17:30:41 +00:00
dir.files = dir.files[i:]
return
}
}
2018-05-20 17:30:41 +00:00
dir.files = dir.files[len(dir.files):]
}
}
2017-11-19 18:55:13 +00:00
func (dir *dir) name() string {
2018-05-20 17:30:41 +00:00
if len(dir.files) == 0 {
2017-11-19 18:55:13 +00:00
return ""
}
2018-05-20 17:30:41 +00:00
return dir.files[dir.ind].Name()
2017-11-19 18:55:13 +00:00
}
func (dir *dir) sel(name string, height int) {
2018-05-20 17:30:41 +00:00
if len(dir.files) == 0 {
2016-08-13 12:49:04 +00:00
dir.ind, dir.pos = 0, 0
return
}
dir.ind = max(dir.ind, 0)
2018-05-20 17:30:41 +00:00
dir.ind = min(dir.ind, len(dir.files)-1)
2016-08-13 12:49:04 +00:00
2018-05-20 17:30:41 +00:00
if dir.files[dir.ind].Name() != name {
for i, f := range dir.files {
2016-08-13 12:49:04 +00:00
if f.Name() == name {
2017-11-19 18:55:13 +00:00
dir.ind = i
2016-08-13 12:49:04 +00:00
break
}
}
}
2018-05-20 17:30:41 +00:00
edge := min(min(height/2, gOpts.scrolloff), len(dir.files)-dir.ind-1)
dir.pos = min(dir.ind, height-edge-1)
2016-08-13 12:49:04 +00:00
}
2016-12-17 21:47:37 +00:00
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
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
2016-08-13 12:49:04 +00:00
}
2018-05-20 17:30:41 +00:00
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}
nav.dirCache[path] = d
2018-05-20 17:30:41 +00:00
go func() {
d := newDir(path)
d.sort()
d.ind, d.pos = 0, 0
nav.dirChan <- d
}()
return d
}
2020-07-16 21:33:37 +00:00
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
2018-05-20 17:30:41 +00:00
go func() {
dir.sort()
dir.loading = false
nav.dirChan <- dir
2018-05-20 17:30:41 +00:00
}()
}
}
func (nav *nav) getDirs(wd string) {
2016-12-17 21:47:37 +00:00
var dirs []*dir
2016-08-13 12:49:04 +00:00
for curr, base := wd, ""; !isRoot(base); curr, base = filepath.Dir(curr), filepath.Base(curr) {
dir := nav.loadDir(curr)
dir.sel(base, nav.height)
2016-08-13 12:49:04 +00:00
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
2016-08-13 12:49:04 +00:00
}
2018-05-20 17:30:41 +00:00
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),
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,
2018-05-20 17:30:41 +00:00
}
nav.getDirs(wd)
return nav
}
2018-04-15 15:18:39 +00:00
func (nav *nav) renew() {
2016-08-13 12:49:04 +00:00
for _, d := range nav.dirs {
nav.checkDir(d)
2016-08-13 12:49:04 +00:00
}
2018-07-09 18:22:10 +00:00
for m := range nav.selections {
if _, err := os.Lstat(m); os.IsNotExist(err) {
2018-07-09 18:22:10 +00:00
delete(nav.selections, m)
2016-08-13 12:49:04 +00:00
}
}
2018-07-09 18:22:10 +00:00
if len(nav.selections) == 0 {
nav.selectionInd = 0
}
2016-08-13 12:49:04 +00:00
}
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]
2018-05-20 17:30:41 +00:00
last.files = append(last.files, curr)
}
return nil
}
2018-05-20 17:30:41 +00:00
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)
2018-05-20 17:30:41 +00:00
path = filepath.Dir(path)
2018-04-18 20:08:28 +00:00
}
}
func (nav *nav) preview(path string) {
reg := &reg{loadTime: time.Now(), path: path}
var reader io.Reader
if len(gOpts.previewer) != 0 {
exportOpts()
cmd := exec.Command(gOpts.previewer, path, strconv.Itoa(nav.height))
out, err := cmd.StdoutPipe()
if err != nil {
log.Printf("previewing file: %s", err)
}
if err := cmd.Start(); err != nil {
log.Printf("previewing file: %s", err)
}
defer cmd.Wait()
defer out.Close()
reader = out
} else {
f, err := os.Open(path)
if err != nil {
log.Printf("opening file: %s", err)
}
defer f.Close()
reader = f
}
buf := bufio.NewScanner(reader)
for i := 0; i < nav.height && buf.Scan(); i++ {
for _, r := range buf.Text() {
if r == 0 {
reg.lines = []string{"\033[7mbinary\033[0m"}
nav.regChan <- reg
return
}
}
reg.lines = append(reg.lines, buf.Text())
}
if buf.Err() != nil {
log.Printf("loading file: %s", buf.Err())
}
nav.regChan <- reg
}
func (nav *nav) loadReg(path string) *reg {
r, ok := nav.regCache[path]
if !ok {
r := &reg{loading: true, loadTime: time.Now(), path: path}
nav.regCache[path] = r
go nav.preview(path)
return r
}
2020-07-16 21:33:37 +00:00
nav.checkReg(r)
return r
}
func (nav *nav) checkReg(reg *reg) {
s, err := os.Stat(reg.path)
if err != nil {
return
}
if s.ModTime().After(reg.loadTime) {
reg.loadTime = time.Now()
go nav.preview(reg.path)
}
}
func (nav *nav) sort() {
for _, d := range nav.dirs {
2017-11-19 18:55:13 +00:00
name := d.name()
d.sort()
d.sel(name, nav.height)
}
}
2016-12-17 21:47:37 +00:00
func (nav *nav) up(dist int) {
2016-08-14 15:39:02 +00:00
dir := nav.currDir()
if dir.ind == 0 {
if gOpts.wrapscroll {
nav.bottom()
}
2016-08-14 15:39:02 +00:00
return
}
dir.ind -= dist
dir.ind = max(0, dir.ind)
2016-08-14 15:39:02 +00:00
dir.pos -= dist
2018-04-15 15:18:39 +00:00
edge := min(min(nav.height/2, gOpts.scrolloff), dir.ind)
2016-08-14 15:39:02 +00:00
dir.pos = max(dir.pos, edge)
}
2016-12-17 21:47:37 +00:00
func (nav *nav) down(dist int) {
2016-08-13 12:49:04 +00:00
dir := nav.currDir()
2018-05-20 17:30:41 +00:00
maxind := len(dir.files) - 1
2016-08-13 12:49:04 +00:00
if dir.ind >= maxind {
if gOpts.wrapscroll {
nav.top()
}
2016-08-13 12:49:04 +00:00
return
}
dir.ind += dist
dir.ind = min(maxind, dir.ind)
2016-08-13 12:49:04 +00:00
dir.pos += dist
2018-04-15 15:18:39 +00:00
edge := min(min(nav.height/2, gOpts.scrolloff), maxind-dir.ind)
2016-09-02 19:47:11 +00:00
// 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
2016-09-02 20:06:25 +00:00
edge = min(edge, nav.height/2+nav.height%2-1)
2016-09-02 19:47:11 +00:00
2016-08-13 12:49:04 +00:00
dir.pos = min(dir.pos, nav.height-edge-1)
dir.pos = min(dir.pos, maxind)
}
2016-12-17 21:47:37 +00:00
func (nav *nav) updir() error {
2016-08-13 12:49:04 +00:00
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 {
2016-08-13 12:49:04 +00:00
return fmt.Errorf("updir: %s", err)
}
return nil
}
2016-12-17 21:47:37 +00:00
func (nav *nav) open() error {
curr, err := nav.currFile()
if err != nil {
2016-12-18 15:01:45 +00:00
return fmt.Errorf("open: %s", err)
}
2016-12-18 15:01:45 +00:00
2017-11-19 18:55:13 +00:00
path := curr.path
2016-08-13 12:49:04 +00:00
dir := nav.loadDir(path)
2016-08-13 12:49:04 +00:00
2016-08-14 12:37:22 +00:00
nav.dirs = append(nav.dirs, dir)
2016-08-13 12:49:04 +00:00
2016-08-17 20:28:42 +00:00
if err := os.Chdir(path); err != nil {
2016-08-14 12:37:22 +00:00
return fmt.Errorf("open: %s", err)
2016-08-13 12:49:04 +00:00
}
return nil
}
2016-12-17 21:47:37 +00:00
func (nav *nav) top() {
2016-08-13 12:49:04 +00:00
dir := nav.currDir()
dir.ind = 0
dir.pos = 0
}
2018-05-15 21:20:05 +00:00
func (nav *nav) bottom() {
2017-11-19 18:55:13 +00:00
dir := nav.currDir()
2018-05-20 17:30:41 +00:00
dir.ind = len(dir.files) - 1
2017-11-19 18:55:13 +00:00
dir.pos = min(dir.ind, nav.height-1)
}
2018-07-09 18:22:10 +00:00
func (nav *nav) toggleSelection(path string) {
if _, ok := nav.selections[path]; ok {
delete(nav.selections, path)
if len(nav.selections) == 0 {
nav.selectionInd = 0
}
2016-08-13 12:49:04 +00:00
} else {
2018-07-09 18:22:10 +00:00
nav.selections[path] = nav.selectionInd
nav.selectionInd++
2016-08-13 12:49:04 +00:00
}
}
2016-12-17 21:47:37 +00:00
func (nav *nav) toggle() {
curr, err := nav.currFile()
if err != nil {
return
}
2018-07-09 18:22:10 +00:00
nav.toggleSelection(curr.path)
2016-08-13 12:49:04 +00:00
}
2016-12-17 21:47:37 +00:00
func (nav *nav) invert() {
2020-07-19 23:47:33 +00:00
dir := nav.currDir()
for _, f := range dir.files {
path := filepath.Join(dir.path, f.Name())
2018-07-09 18:22:10 +00:00
nav.toggleSelection(path)
}
}
2018-07-09 18:22:10 +00:00
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
}
2016-08-13 12:49:04 +00:00
if err := saveFiles(list, cp); err != nil {
return err
}
nav.saves = make(map[string]bool)
for _, f := range list {
nav.saves[f] = cp
2016-08-13 12:49:04 +00:00
}
return nil
}
2019-02-28 18:04:38 +00:00
func (nav *nav) copyAsync(ui *ui, srcs []string, dstDir string) {
2019-02-28 19:04:57 +00:00
echo := &callExpr{"echoerr", []string{""}, 1}
_, err := os.Stat(dstDir)
if os.IsNotExist(err) {
2019-02-28 19:04:57 +00:00
echo.args[0] = err.Error()
ui.exprChan <- echo
return
}
total, err := copySize(srcs)
if err != nil {
2019-02-28 19:04:57 +00:00
echo.args[0] = err.Error()
ui.exprChan <- echo
return
}
2019-02-28 18:04:38 +00:00
nav.copyTotalChan <- total
nums, errs := copyAll(srcs, dstDir)
errCount := 0
loop:
for {
select {
case n := <-nums:
2019-02-28 18:04:38 +00:00
nav.copyBytesChan <- n
case err, ok := <-errs:
if !ok {
break loop
}
errCount++
2019-02-28 19:04:57 +00:00
echo.args[0] = fmt.Sprintf("[%d] %s", errCount, err)
ui.exprChan <- echo
}
}
2019-02-26 18:04:51 +00:00
2019-03-01 14:44:50 +00:00
nav.copyTotalChan <- -total
2019-02-26 18:27:04 +00:00
if err := remote("send load"); err != nil {
2019-02-26 18:04:51 +00:00
errCount++
2019-02-28 19:04:57 +00:00
echo.args[0] = fmt.Sprintf("[%d] %s", errCount, err)
2019-02-26 18:04:51 +00:00
ui.exprChan <- echo
}
if errCount == 0 {
2020-07-21 22:55:21 +00:00
ui.exprChan <- &callExpr{"echo", []string{"\033[0;32mCopied successfully\033[0m"}, 1}
}
}
2019-02-28 18:04:38 +00:00
func (nav *nav) moveAsync(ui *ui, srcs []string, dstDir string) {
2019-02-28 19:04:57 +00:00
echo := &callExpr{"echoerr", []string{""}, 1}
_, err := os.Stat(dstDir)
if os.IsNotExist(err) {
2019-02-28 19:04:57 +00:00
echo.args[0] = err.Error()
ui.exprChan <- echo
return
}
2019-03-01 14:44:50 +00:00
nav.moveTotalChan <- len(srcs)
errCount := 0
for _, src := range srcs {
2019-03-01 14:44:50 +00:00
nav.moveCountChan <- 1
srcStat, err := os.Stat(src)
if err != nil {
errCount++
2019-02-28 19:04:57 +00:00
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++
2019-02-28 19:04:57 +00:00
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 {
2020-07-03 21:16:16 +00:00
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
}
}
}
2019-02-26 18:04:51 +00:00
2019-03-01 14:44:50 +00:00
nav.moveTotalChan <- -len(srcs)
2019-02-26 18:27:04 +00:00
if err := remote("send load"); err != nil {
2019-02-26 18:04:51 +00:00
errCount++
2019-02-28 19:04:57 +00:00
echo.args[0] = fmt.Sprintf("[%d] %s", errCount, err)
2019-02-26 18:04:51 +00:00
ui.exprChan <- echo
}
if errCount == 0 {
2020-07-21 22:55:21 +00:00
ui.exprChan <- &callExpr{"echo", []string{"\033[0;32mMoved successfully\033[0m"}, 1}
}
}
func (nav *nav) paste(ui *ui) error {
srcs, cp, err := loadFiles()
2016-08-13 12:49:04 +00:00
if err != nil {
return err
}
if len(srcs) == 0 {
return errors.New("no file in copy/cut buffer")
2016-08-13 12:49:04 +00:00
}
dstDir := nav.currDir().path
2016-08-13 12:49:04 +00:00
if cp {
2019-02-28 18:04:38 +00:00
go nav.copyAsync(ui, srcs, dstDir)
} else {
2019-02-28 18:04:38 +00:00
go nav.moveAsync(ui, srcs, dstDir)
2016-08-13 12:49:04 +00:00
}
2018-03-22 14:54:24 +00:00
if err := saveFiles(nil, false); err != nil {
return fmt.Errorf("clearing copy/cut buffer: %s", err)
2018-03-22 14:54:24 +00:00
}
2019-02-26 18:27:04 +00:00
if err := remote("send sync"); err != nil {
2019-02-26 18:04:51 +00:00
return fmt.Errorf("paste: %s", err)
}
2016-08-13 12:49:04 +00:00
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
}
2019-07-21 17:40:57 +00:00
lstat, err := os.Lstat(newPath)
if err != nil {
return err
}
2019-07-21 17:40:57 +00:00
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
}
2016-12-17 21:47:37 +00:00
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()
}
2017-11-19 18:55:13 +00:00
func (nav *nav) cd(wd string) error {
wd = replaceTilde(wd)
2017-11-19 18:55:13 +00:00
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)
2019-05-18 15:21:16 +00:00
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 {
2018-05-20 17:30:41 +00:00
last.files = append(last.files, &file{FileInfo: lstat})
}
last.sel(base, nav.height)
return nil
}
func (nav *nav) globSel(pattern string, invert bool) error {
2020-07-19 23:47:33 +00:00
dir := nav.currDir()
anyMatched := false
2020-07-19 23:47:33 +00:00
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)
}
2020-07-19 23:47:33 +00:00
if matched {
anyMatched = true
fpath := filepath.Join(dir.path, dir.files[i].Name())
if _, ok := nav.selections[fpath]; ok == invert {
nav.toggleSelection(fpath)
}
}
}
2020-07-19 23:47:33 +00:00
if !anyMatched {
return fmt.Errorf("glob-select: pattern not found: %s", pattern)
}
2020-07-19 23:47:33 +00:00
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
2020-07-19 23:47:33 +00:00
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 {
2020-07-19 23:47:33 +00:00
if index > dir.ind {
nav.down(index - dir.ind)
} else {
2020-07-19 23:47:33 +00:00
nav.up(dir.ind - index)
}
}
return count
}
func (nav *nav) findNext() bool {
2020-07-19 23:47:33 +00:00
dir := nav.currDir()
for i := dir.ind + 1; i < len(dir.files); i++ {
if findMatch(dir.files[i].Name(), nav.find) {
nav.down(i - dir.ind)
return true
}
}
if gOpts.wrapscan {
2020-07-19 23:47:33 +00:00
for i := 0; i < dir.ind; i++ {
if findMatch(dir.files[i].Name(), nav.find) {
nav.up(dir.ind - i)
return true
}
}
}
return false
}
func (nav *nav) findPrev() bool {
2020-07-19 23:47:33 +00:00
dir := nav.currDir()
for i := dir.ind - 1; i >= 0; i-- {
if findMatch(dir.files[i].Name(), nav.find) {
nav.up(dir.ind - i)
return true
}
}
if gOpts.wrapscan {
2020-07-19 23:47:33 +00:00
for i := len(dir.files) - 1; i > dir.ind; i-- {
if findMatch(dir.files[i].Name(), nav.find) {
nav.down(i - dir.ind)
return true
}
}
}
return false
}
func searchMatch(name, pattern string) (matched bool, err error) {
2017-11-19 18:55:13 +00:00
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)
}
}
2017-11-19 18:55:13 +00:00
if gOpts.globsearch {
return filepath.Match(pattern, name)
}
return strings.Contains(name, pattern), nil
}
func (nav *nav) searchNext() error {
2020-07-19 23:47:33 +00:00
dir := nav.currDir()
for i := dir.ind + 1; i < len(dir.files); i++ {
matched, err := searchMatch(dir.files[i].Name(), nav.search)
2017-11-19 18:55:13 +00:00
if err != nil {
return err
}
if matched {
2020-07-19 23:47:33 +00:00
nav.down(i - dir.ind)
2017-11-19 18:55:13 +00:00
return nil
}
}
if gOpts.wrapscan {
2020-07-19 23:47:33 +00:00
for i := 0; i < dir.ind; i++ {
matched, err := searchMatch(dir.files[i].Name(), nav.search)
2017-11-19 18:55:13 +00:00
if err != nil {
return err
}
if matched {
2020-07-19 23:47:33 +00:00
nav.up(dir.ind - i)
2017-11-19 18:55:13 +00:00
return nil
}
}
}
return nil
}
func (nav *nav) searchPrev() error {
2020-07-19 23:47:33 +00:00
dir := nav.currDir()
for i := dir.ind - 1; i >= 0; i-- {
matched, err := searchMatch(dir.files[i].Name(), nav.search)
2017-11-19 18:55:13 +00:00
if err != nil {
return err
}
if matched {
2020-07-19 23:47:33 +00:00
nav.up(dir.ind - i)
2017-11-19 18:55:13 +00:00
return nil
}
}
if gOpts.wrapscan {
2020-07-19 23:47:33 +00:00
for i := len(dir.files) - 1; i > dir.ind; i-- {
matched, err := searchMatch(dir.files[i].Name(), nav.search)
2017-11-19 18:55:13 +00:00
if err != nil {
return err
}
if matched {
2020-07-19 23:47:33 +00:00
nav.down(i - dir.ind)
2017-11-19 18:55:13 +00:00
return nil
}
}
}
return 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) {
2020-07-19 23:47:33 +00:00
dir := nav.dirs[len(nav.dirs)-1]
2020-07-19 23:47:33 +00:00
if len(dir.files) == 0 {
return nil, fmt.Errorf("empty directory")
}
2020-07-19 23:47:33 +00:00
return dir.files[dir.ind], nil
}
2018-07-09 18:22:10 +00:00
type indexedSelections struct {
paths []string
indices []int
}
2018-07-09 18:22:10 +00:00
func (m indexedSelections) Len() int { return len(m.paths) }
2017-02-11 13:34:18 +00:00
2018-07-09 18:22:10 +00:00
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]
}
2017-02-11 13:34:18 +00:00
2018-07-09 18:22:10 +00:00
func (m indexedSelections) Less(i, j int) bool { return m.indices[i] < m.indices[j] }
2018-07-09 18:22:10 +00:00
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)
}
2018-07-09 18:22:10 +00:00
sort.Sort(indexedSelections{paths: paths, indices: indices})
return paths
2016-08-13 12:49:04 +00:00
}
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
}