lf/nav.go
Gokcehan fed8d73ffe move server file clearing to builtin 'put' command
If a user defines an asynchronous 'put' command it will fetch an empty
list from the server since server file list is immediately cleared after
every 'put' command. Now that we move the clearing to the builtin 'put'
command, users need to explicitly clear server file list in their custom
'put' commands if they like.
2017-10-26 21:16:24 +03:00

578 lines
10 KiB
Go

package main
import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strings"
)
type linkState byte
const (
notLink linkState = iota
working
broken
)
type file struct {
os.FileInfo
LinkState linkState
Path string
Count int
}
type filesSortable struct {
files []*file
less func(i, j int) bool
}
func (f filesSortable) Len() int { return len(f.files) }
func (f filesSortable) Swap(i, j int) { f.files[i], f.files[j] = f.files[j], f.files[i] }
func (f filesSortable) Less(i, j int) bool { return f.less(i, j) }
// TODO: Replace with `sort.SliceStable` once available
func sortFilesStable(files []*file, less func(i, j int) bool) {
sort.Stable(filesSortable{files: files, less: less})
}
func getFilesSorted(path string) []*file {
fi, err := readdir(path)
if err != nil {
log.Printf("reading directory: %s", err)
}
switch gOpts.sortby {
case "natural":
sortFilesStable(fi, func(i, j int) bool {
return naturalLess(strings.ToLower(fi[i].Name()), strings.ToLower(fi[j].Name()))
})
case "name":
sortFilesStable(fi, func(i, j int) bool {
return strings.ToLower(fi[i].Name()) < strings.ToLower(fi[j].Name())
})
case "size":
sortFilesStable(fi, func(i, j int) bool {
return fi[i].Size() < fi[j].Size()
})
case "time":
sortFilesStable(fi, func(i, j int) bool {
return fi[i].ModTime().Before(fi[j].ModTime())
})
default:
log.Printf("unknown sorting type: %s", gOpts.sortby)
}
if gOpts.reverse {
for i, j := 0, len(fi)-1; i < j; i, j = i+1, j-1 {
fi[i], fi[j] = fi[j], fi[i]
}
}
if gOpts.dirfirst {
sortFilesStable(fi, func(i, j int) bool {
if fi[i].IsDir() == fi[j].IsDir() {
return i < j
}
return fi[i].IsDir()
})
}
return fi
}
func readdir(path string) ([]*file, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
names, err := f.Readdirnames(-1)
f.Close()
fi := make([]*file, 0, len(names))
for _, filename := range names {
if !gOpts.hidden && filename[0] == '.' {
continue
}
fpath := filepath.Join(path, filename)
lstat, lerr := os.Lstat(fpath)
if os.IsNotExist(lerr) {
continue
}
if lerr != nil {
return fi, lerr
}
var linkState linkState
if lstat.Mode()&os.ModeSymlink != 0 {
stat, serr := os.Stat(fpath)
if serr == nil {
linkState = working
lstat = stat
} else {
linkState = broken
log.Printf("getting link destination info: %s", serr)
}
}
fi = append(fi, &file{
FileInfo: lstat,
LinkState: linkState,
Path: fpath,
Count: -1,
})
}
return fi, err
}
type dir struct {
ind int // which entry is highlighted
pos int // which line in the ui highlighted entry is
path string
fi []*file
}
func newDir(path string) *dir {
return &dir{
path: path,
fi: getFilesSorted(path),
}
}
func (dir *dir) renew(height int) {
var name string
if len(dir.fi) != 0 {
name = dir.fi[dir.ind].Name()
}
dir.fi = getFilesSorted(dir.path)
dir.load(dir.ind, dir.pos, height, name)
}
func (dir *dir) load(ind, pos, height int, name string) {
if len(dir.fi) == 0 {
dir.ind, dir.pos = 0, 0
return
}
ind = max(0, min(ind, len(dir.fi)-1))
if dir.fi[ind].Name() != name {
for i, f := range dir.fi {
if f.Name() == name {
ind = i
break
}
}
edge := min(gOpts.scrolloff, len(dir.fi)-ind-1)
pos = min(ind, height-edge-1)
}
dir.ind = ind
dir.pos = pos
}
type nav struct {
dirs []*dir
inds map[string]int
poss map[string]int
names map[string]string
marks map[string]int
saves map[string]bool
markInd int
height int
search string
}
func getDirs(wd string, height int) []*dir {
var dirs []*dir
for curr, base := wd, ""; !isRoot(base); curr, base = filepath.Dir(curr), filepath.Base(curr) {
dir := newDir(curr)
for i, f := range dir.fi {
if f.Name() == base {
dir.ind = i
edge := min(gOpts.scrolloff, len(dir.fi)-dir.ind-1)
dir.pos = min(i, height-edge-1)
break
}
}
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]
}
return dirs
}
func newNav(height int) *nav {
wd, err := os.Getwd()
if err != nil {
log.Printf("getting current directory: %s", err)
}
dirs := getDirs(wd, height)
return &nav{
dirs: dirs,
inds: make(map[string]int),
poss: make(map[string]int),
names: make(map[string]string),
marks: make(map[string]int),
saves: make(map[string]bool),
markInd: 0,
height: height,
}
}
func (nav *nav) renew(height int) {
nav.height = height
for _, d := range nav.dirs {
d.renew(nav.height)
}
for m := range nav.marks {
if _, err := os.Stat(m); os.IsNotExist(err) {
delete(nav.marks, m)
}
}
if len(nav.marks) == 0 {
nav.markInd = 0
}
}
func (nav *nav) up(dist int) {
dir := nav.currDir()
if dir.ind == 0 {
return
}
dir.ind -= dist
dir.ind = max(0, dir.ind)
dir.pos -= dist
edge := min(gOpts.scrolloff, dir.ind)
dir.pos = max(dir.pos, edge)
}
func (nav *nav) down(dist int) {
dir := nav.currDir()
maxind := len(dir.fi) - 1
if dir.ind >= maxind {
return
}
dir.ind += dist
dir.ind = min(maxind, dir.ind)
dir.pos += dist
edge := min(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.inds[dir.path] = dir.ind
nav.poss[dir.path] = dir.pos
if len(dir.fi) != 0 {
nav.names[dir.path] = dir.fi[dir.ind].Name()
}
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 := newDir(path)
dir.load(nav.inds[path], nav.poss[path], nav.height, nav.names[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) bot() {
dir := nav.currDir()
dir.ind = len(dir.fi) - 1
dir.pos = min(dir.ind, nav.height-1)
}
func (nav *nav) top() {
dir := nav.currDir()
dir.ind = 0
dir.pos = 0
}
func (nav *nav) cd(wd string) error {
wd = strings.Replace(wd, "~", gUser.HomeDir, -1)
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.dirs = getDirs(wd, nav.height)
// TODO: save/load ind and pos from the map
return nil
}
func match(pattern, name 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.globsearch {
return filepath.Match(pattern, name)
}
return strings.Contains(name, pattern), nil
}
func (nav *nav) searchNext() error {
last := nav.currDir()
for i := last.ind + 1; i < len(last.fi); i++ {
matched, err := match(nav.search, last.fi[i].Name())
if err != nil {
return err
}
if matched {
nav.down(i - last.ind)
return nil
}
}
if gOpts.wrapscan {
for i := 0; i < last.ind; i++ {
matched, err := match(nav.search, last.fi[i].Name())
if err != nil {
return err
}
if matched {
nav.up(last.ind - i)
return nil
}
}
}
return nil
}
func (nav *nav) searchPrev() error {
last := nav.currDir()
for i := last.ind - 1; i >= 0; i-- {
matched, err := match(nav.search, last.fi[i].Name())
if err != nil {
return err
}
if matched {
nav.up(last.ind - i)
return nil
}
}
if gOpts.wrapscan {
for i := len(last.fi) - 1; i > last.ind; i-- {
matched, err := match(nav.search, last.fi[i].Name())
if err != nil {
return err
}
if matched {
nav.down(i - last.ind)
return nil
}
}
}
return nil
}
func (nav *nav) toggleMark(path string) {
if _, ok := nav.marks[path]; ok {
delete(nav.marks, path)
if len(nav.marks) == 0 {
nav.markInd = 0
}
} else {
nav.marks[path] = nav.markInd
nav.markInd = nav.markInd + 1
}
}
func (nav *nav) toggle() {
curr, err := nav.currFile()
if err != nil {
return
}
nav.toggleMark(curr.Path)
nav.down(1)
}
func (nav *nav) invert() {
last := nav.currDir()
for _, f := range last.fi {
path := filepath.Join(last.path, f.Name())
nav.toggleMark(path)
}
}
func (nav *nav) save(copy bool) error {
if len(nav.marks) == 0 {
curr, err := nav.currFile()
if err != nil {
return errors.New("no file selected")
}
if err := saveFiles([]string{curr.Path}, copy); err != nil {
return err
}
nav.saves = make(map[string]bool)
nav.saves[curr.Path] = copy
} else {
var fs []string
for f := range nav.marks {
fs = append(fs, f)
}
if err := saveFiles(fs, copy); err != nil {
return err
}
nav.saves = make(map[string]bool)
for f := range nav.marks {
nav.saves[f] = copy
}
}
return nil
}
func (nav *nav) put() error {
list, copy, err := loadFiles()
if err != nil {
return err
}
if len(list) == 0 {
return errors.New("no file in yank/delete buffer")
}
dir := nav.currDir()
cmd := putCommand(list, dir, copy)
if err := cmd.Run(); err != nil {
// TODO: add name of external command to message
return fmt.Errorf("putting files: %s", err)
}
// TODO: async?
saveFiles(nil, false)
return nil
}
func (nav *nav) sync() error {
list, copy, err := loadFiles()
if err != nil {
return err
}
nav.saves = make(map[string]bool)
for _, f := range list {
nav.saves[f] = copy
}
return nil
}
func (nav *nav) currDir() *dir {
return nav.dirs[len(nav.dirs)-1]
}
func (nav *nav) currFile() (*file, error) {
last := nav.dirs[len(nav.dirs)-1]
if len(last.fi) == 0 {
return nil, fmt.Errorf("empty directory")
}
return last.fi[last.ind], nil
}
type indexedMarks struct {
paths []string
indices []int
}
func (m indexedMarks) Len() int { return len(m.paths) }
func (m indexedMarks) 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 indexedMarks) Less(i, j int) bool { return m.indices[i] < m.indices[j] }
func (nav *nav) currMarks() []string {
paths := make([]string, 0, len(nav.marks))
indices := make([]int, 0, len(nav.marks))
for path, index := range nav.marks {
paths = append(paths, path)
indices = append(indices, index)
}
sort.Sort(indexedMarks{paths: paths, indices: indices})
return paths
}