lf/nav.go
2017-04-09 16:15:06 +03:00

556 lines
10 KiB
Go

package main
import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
)
type linkState byte
const (
notLink linkState = iota
working
broken
)
type file struct {
os.FileInfo
LinkState linkState
Path string
}
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,
})
}
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, "~", envHome, -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 (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
}
}
}
}
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
}
}
}
}
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()
var args []string
var sh string
if copy {
sh = "cp"
args = append(args, "-r")
} else {
sh = "mv"
}
args = append(args, "--backup=numbered")
args = append(args, list...)
args = append(args, dir.path)
cmd := exec.Command(sh, args...)
if err := cmd.Run(); err != nil {
return fmt.Errorf("%s: %s", sh, err)
}
// TODO: async?
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
}