lf/nav.go

558 lines
10 KiB
Go
Raw Normal View History

2016-08-13 12:49:04 +00:00
package main
import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
2016-11-10 21:18:56 +00:00
"sort"
2016-08-13 12:49:04 +00:00
"strings"
)
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
2016-12-17 21:47:37 +00:00
LinkState linkState
Path string
2017-06-03 11:12:43 +00:00
Count int
2016-08-13 12:49:04 +00:00
}
2016-12-17 21:47:37 +00:00
type filesSortable struct {
files []*file
2016-11-10 21:18:56 +00:00
less func(i, j int) bool
}
2016-12-17 21:47:37 +00:00
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) }
2016-11-10 21:18:56 +00:00
// TODO: Replace with `sort.SliceStable` once available
2016-12-17 21:47:37 +00:00
func sortFilesStable(files []*file, less func(i, j int) bool) {
sort.Stable(filesSortable{files: files, less: less})
2016-11-10 21:18:56 +00:00
}
2016-12-17 21:47:37 +00:00
func getFilesSorted(path string) []*file {
fi, err := readdir(path)
if err != nil {
log.Printf("reading directory: %s", err)
2016-08-13 12:49:04 +00:00
}
switch gOpts.sortby {
case "natural":
sortFilesStable(fi, func(i, j int) bool {
return naturalLess(strings.ToLower(fi[i].Name()), strings.ToLower(fi[j].Name()))
})
2016-08-13 12:49:04 +00:00
case "name":
sortFilesStable(fi, func(i, j int) bool {
return strings.ToLower(fi[i].Name()) < strings.ToLower(fi[j].Name())
})
2016-08-13 12:49:04 +00:00
case "size":
sortFilesStable(fi, func(i, j int) bool {
return fi[i].Size() < fi[j].Size()
})
2016-08-13 12:49:04 +00:00
case "time":
sortFilesStable(fi, func(i, j int) bool {
return fi[i].ModTime().Before(fi[j].ModTime())
})
2016-08-13 12:49:04 +00:00
default:
log.Printf("unknown sorting type: %s", gOpts.sortby)
}
2016-12-26 20:49:18 +00:00
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()
})
}
2016-08-13 12:49:04 +00:00
return fi
}
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()
2016-12-17 21:47:37 +00:00
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
}
2016-12-17 21:47:37 +00:00
var linkState linkState
if lstat.Mode()&os.ModeSymlink != 0 {
stat, serr := os.Stat(fpath)
if serr == nil {
2016-12-17 21:47:37 +00:00
linkState = working
lstat = stat
} else {
2016-12-17 21:47:37 +00:00
linkState = broken
log.Printf("getting link destination info: %s", serr)
}
}
2016-12-17 21:47:37 +00:00
fi = append(fi, &file{
FileInfo: lstat,
LinkState: linkState,
Path: fpath,
2017-06-03 11:12:43 +00:00
Count: -1,
})
}
return fi, err
}
2016-08-13 12:49:04 +00:00
2016-12-17 21:47:37 +00:00
type dir struct {
ind int // which entry is highlighted
pos int // which line in the ui highlighted entry is
path string
2016-12-17 21:47:37 +00:00
fi []*file
}
2016-12-17 21:47:37 +00:00
func newDir(path string) *dir {
return &dir{
2016-08-13 12:49:04 +00:00
path: path,
fi: getFilesSorted(path),
2016-08-13 12:49:04 +00:00
}
}
2016-12-17 21:47:37 +00:00
func (dir *dir) renew(height int) {
2016-08-13 12:49:04 +00:00
var name string
if len(dir.fi) != 0 {
name = dir.fi[dir.ind].Name()
}
dir.fi = getFilesSorted(dir.path)
2016-08-13 12:49:04 +00:00
dir.load(dir.ind, dir.pos, height, name)
}
2016-12-17 21:47:37 +00:00
func (dir *dir) load(ind, pos, height int, name string) {
2016-08-13 12:49:04 +00:00
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
}
2016-12-17 21:47:37 +00:00
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
2016-08-13 12:49:04 +00:00
}
2016-12-17 21:47:37 +00:00
func getDirs(wd string, height int) []*dir {
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) {
2016-08-13 12:49:04 +00:00
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
}
2016-12-17 21:47:37 +00:00
func newNav(height int) *nav {
2016-08-13 12:49:04 +00:00
wd, err := os.Getwd()
if err != nil {
2016-08-17 20:22:11 +00:00
log.Printf("getting current directory: %s", err)
2016-08-13 12:49:04 +00:00
}
dirs := getDirs(wd, height)
2016-12-17 21:47:37 +00:00
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,
2016-08-13 12:49:04 +00:00
}
}
2016-12-17 21:47:37 +00:00
func (nav *nav) renew(height int) {
2016-08-13 12:49:04 +00:00
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
}
2016-08-13 12:49:04 +00:00
}
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 {
return
}
dir.ind -= dist
dir.ind = max(0, dir.ind)
2016-08-14 15:39:02 +00:00
dir.pos -= dist
2016-08-14 15:39:02 +00:00
edge := min(gOpts.scrolloff, dir.ind)
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()
maxind := len(dir.fi) - 1
if dir.ind >= maxind {
return
}
dir.ind += dist
dir.ind = min(maxind, dir.ind)
2016-08-13 12:49:04 +00:00
dir.pos += dist
2016-08-13 12:49:04 +00:00
edge := min(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.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 {
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
path := curr.Path
2016-08-13 12:49:04 +00:00
2016-08-14 12:37:22 +00:00
dir := newDir(path)
2016-08-14 12:37:22 +00:00
dir.load(nav.inds[path], nav.poss[path], nav.height, nav.names[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) bot() {
2016-08-13 12:49:04 +00:00
dir := nav.currDir()
dir.ind = len(dir.fi) - 1
dir.pos = min(dir.ind, nav.height-1)
}
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
}
2016-12-17 21:47:37 +00:00
func (nav *nav) cd(wd string) error {
2016-08-14 15:38:33 +00:00
wd = strings.Replace(wd, "~", envHome, -1)
2016-10-01 21:39:03 +00:00
wd = filepath.Clean(wd)
2016-08-14 15:38:33 +00:00
if !filepath.IsAbs(wd) {
wd = filepath.Join(nav.currDir().path, wd)
2016-08-13 12:49:04 +00:00
}
2016-08-17 20:28:42 +00:00
if err := os.Chdir(wd); err != nil {
2016-08-13 12:49:04 +00:00
return fmt.Errorf("cd: %s", err)
}
nav.dirs = getDirs(wd, nav.height)
// TODO: save/load ind and pos from the map
return nil
}
func (nav *nav) searchNext() {
last := nav.currDir()
for i := last.ind + 1; i < len(last.fi); i++ {
if strings.Contains(last.fi[i].Name(), nav.search) {
nav.down(i - last.ind)
return
}
}
if gOpts.wrapscan {
for i := 0; i < last.ind; i++ {
if strings.Contains(last.fi[i].Name(), nav.search) {
nav.up(last.ind - i)
return
}
2017-01-07 14:42:33 +00:00
}
}
}
func (nav *nav) searchPrev() {
last := nav.currDir()
for i := last.ind - 1; i >= 0; i-- {
if strings.Contains(last.fi[i].Name(), nav.search) {
nav.up(last.ind - i)
return
}
}
if gOpts.wrapscan {
for i := len(last.fi) - 1; i > last.ind; i-- {
if strings.Contains(last.fi[i].Name(), nav.search) {
nav.down(i - last.ind)
return
}
2017-01-07 14:42:33 +00:00
}
}
}
2016-12-17 21:47:37 +00:00
func (nav *nav) toggleMark(path string) {
if _, ok := nav.marks[path]; ok {
2016-08-13 12:49:04 +00:00
delete(nav.marks, path)
if len(nav.marks) == 0 {
nav.markInd = 0
}
2016-08-13 12:49:04 +00:00
} else {
nav.marks[path] = nav.markInd
nav.markInd = nav.markInd + 1
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
}
nav.toggleMark(curr.Path)
2016-08-13 12:49:04 +00:00
nav.down(1)
2016-08-13 12:49:04 +00:00
}
2016-12-17 21:47:37 +00:00
func (nav *nav) invert() {
last := nav.currDir()
for _, f := range last.fi {
path := filepath.Join(last.path, f.Name())
nav.toggleMark(path)
}
}
2016-12-17 21:47:37 +00:00
func (nav *nav) save(copy bool) error {
2016-08-13 12:49:04 +00:00
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 {
2016-08-13 12:49:04 +00:00
return err
}
nav.saves = make(map[string]bool)
nav.saves[curr.Path] = copy
2016-08-13 12:49:04 +00:00
} else {
var fs []string
for f := range nav.marks {
fs = append(fs, f)
}
if err := saveFiles(fs, copy); err != nil {
2016-08-13 12:49:04 +00:00
return err
}
nav.saves = make(map[string]bool)
for f := range nav.marks {
nav.saves[f] = copy
}
2016-08-13 12:49:04 +00:00
}
return nil
}
2016-12-17 21:47:37 +00:00
func (nav *nav) put() error {
list, copy, err := loadFiles()
2016-08-13 12:49:04 +00:00
if err != nil {
return err
}
if len(list) == 0 {
return errors.New("no file in yank/delete buffer")
}
dir := nav.currDir()
var args []string
2016-08-13 12:49:04 +00:00
var sh string
if copy {
2016-08-13 12:49:04 +00:00
sh = "cp"
args = append(args, "-r")
2016-08-13 12:49:04 +00:00
} else {
sh = "mv"
}
args = append(args, "--backup=numbered")
args = append(args, list...)
args = append(args, dir.path)
2016-08-13 12:49:04 +00:00
cmd := exec.Command(sh, args...)
if err := cmd.Run(); err != nil {
return fmt.Errorf("%s: %s", sh, err)
}
// TODO: async?
return nil
}
2016-12-17 21:47:37 +00:00
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
}
2016-12-17 21:47:37 +00:00
func (nav *nav) currDir() *dir {
2016-08-13 12:49:04 +00:00
return nav.dirs[len(nav.dirs)-1]
}
2016-12-17 21:47:37 +00:00
func (nav *nav) currFile() (*file, error) {
2016-08-13 12:49:04 +00:00
last := nav.dirs[len(nav.dirs)-1]
if len(last.fi) == 0 {
return nil, fmt.Errorf("empty directory")
}
return last.fi[last.ind], nil
2016-08-13 12:49:04 +00:00
}
2017-02-11 13:34:18 +00:00
type indexedMarks struct {
paths []string
indices []int
}
2017-02-11 13:34:18 +00:00
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]
}
2017-02-11 13:34:18 +00:00
func (m indexedMarks) Less(i, j int) bool { return m.indices[i] < m.indices[j] }
2016-12-17 21:47:37 +00:00
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)
}
2017-02-11 13:34:18 +00:00
sort.Sort(indexedMarks{paths: paths, indices: indices})
return paths
2016-08-13 12:49:04 +00:00
}