82f03102a5
* Return early on error in nav.preview * nav.checkReg now returns a boolean instead of calling nav.preview * Pass width, height, x, and y parameters to preview script * Check previewer for exit code If non-zero the preview will be assumed to have side-effects. * Add the cleaner option This is called upon selection changes if the previous preview was volatile. To this end, volatilePreview was added to the nav struct
1319 lines
27 KiB
Go
1319 lines
27 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 + name1) < (ext2 + 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
|
|
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),
|
|
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) 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 {
|
|
nav.volatilePreview = true
|
|
reg.volatile = 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) previewClear() {
|
|
if len(gOpts.cleaner) != 0 && nav.volatilePreview {
|
|
nav.volatilePreview = false
|
|
|
|
cmd := exec.Command(gOpts.cleaner)
|
|
if err := cmd.Run(); err != nil {
|
|
log.Printf("cleaning preview: %s", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (nav *nav) loadReg(path string, win *win) *reg {
|
|
r, ok := nav.regCache[path]
|
|
if !ok || r.volatile {
|
|
r := ®{loading: true, loadTime: time.Now(), path: path, volatile: true}
|
|
nav.regCache[path] = r
|
|
go nav.preview(path, win)
|
|
return r
|
|
}
|
|
|
|
if nav.checkReg(r) {
|
|
go nav.preview(path, win)
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
func (nav *nav) checkReg(reg *reg) bool {
|
|
s, err := os.Stat(reg.path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
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 false
|
|
}
|
|
|
|
if s.ModTime().After(reg.loadTime) {
|
|
reg.loadTime = now
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
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) {
|
|
dir := nav.currDir()
|
|
|
|
if dir.ind == 0 {
|
|
if gOpts.wrapscroll {
|
|
nav.bottom()
|
|
}
|
|
return
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func (nav *nav) down(dist int) {
|
|
dir := nav.currDir()
|
|
|
|
maxind := len(dir.files) - 1
|
|
|
|
if dir.ind >= maxind {
|
|
if gOpts.wrapscroll {
|
|
nav.top()
|
|
}
|
|
return
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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() {
|
|
dir := nav.currDir()
|
|
|
|
dir.ind = 0
|
|
dir.pos = 0
|
|
}
|
|
|
|
func (nav *nav) bottom() {
|
|
dir := nav.currDir()
|
|
|
|
dir.ind = len(dir.files) - 1
|
|
dir.pos = min(dir.ind, nav.height-1)
|
|
}
|
|
|
|
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.Stat(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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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) {
|
|
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() error {
|
|
dir := nav.currDir()
|
|
for i := dir.ind + 1; i < len(dir.files); i++ {
|
|
matched, err := searchMatch(dir.files[i].Name(), nav.search)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if matched {
|
|
nav.down(i - dir.ind)
|
|
return nil
|
|
}
|
|
}
|
|
if gOpts.wrapscan {
|
|
for i := 0; i < dir.ind; i++ {
|
|
matched, err := searchMatch(dir.files[i].Name(), nav.search)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if matched {
|
|
nav.up(dir.ind - i)
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (nav *nav) searchPrev() error {
|
|
dir := nav.currDir()
|
|
for i := dir.ind - 1; i >= 0; i-- {
|
|
matched, err := searchMatch(dir.files[i].Name(), nav.search)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if matched {
|
|
nav.up(dir.ind - i)
|
|
return nil
|
|
}
|
|
}
|
|
if gOpts.wrapscan {
|
|
for i := len(dir.files) - 1; i > dir.ind; i-- {
|
|
matched, err := searchMatch(dir.files[i].Name(), nav.search)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if matched {
|
|
nav.down(i - dir.ind)
|
|
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) {
|
|
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
|
|
}
|