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 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 { fi, err := readdir(path) if err != nil { log.Printf("reading directory: %s", err) } return &dir{ path: path, fi: fi, } } func (dir *dir) renew() { fi, err := readdir(dir.path) if err != nil { log.Printf("reading directory: %s", err) } dir.fi = fi } func (dir *dir) sort() { switch gOpts.sortby { case "natural": sortFilesStable(dir.fi, func(i, j int) bool { return naturalLess(strings.ToLower(dir.fi[i].Name()), strings.ToLower(dir.fi[j].Name())) }) case "name": sortFilesStable(dir.fi, func(i, j int) bool { return strings.ToLower(dir.fi[i].Name()) < strings.ToLower(dir.fi[j].Name()) }) case "size": sortFilesStable(dir.fi, func(i, j int) bool { return dir.fi[i].Size() < dir.fi[j].Size() }) case "time": sortFilesStable(dir.fi, func(i, j int) bool { return dir.fi[i].ModTime().Before(dir.fi[j].ModTime()) }) default: log.Printf("unknown sorting type: %s", gOpts.sortby) } if gOpts.reverse { for i, j := 0, len(dir.fi)-1; i < j; i, j = i+1, j-1 { dir.fi[i], dir.fi[j] = dir.fi[j], dir.fi[i] } } if gOpts.dirfirst { sortFilesStable(dir.fi, func(i, j int) bool { if dir.fi[i].IsDir() == dir.fi[j].IsDir() { return i < j } return dir.fi[i].IsDir() }) } } func (dir *dir) name() string { if len(dir.fi) == 0 { return "" } return dir.fi[dir.ind].Name() } func (dir *dir) find(name string, height int) { if len(dir.fi) == 0 { dir.ind, dir.pos = 0, 0 return } dir.ind = max(0, min(dir.ind, len(dir.fi)-1)) if dir.fi[dir.ind].Name() != name { for i, f := range dir.fi { if f.Name() == name { dir.ind = i break } } edge := min(gOpts.scrolloff, len(dir.fi)-dir.ind-1) dir.pos = min(dir.ind, height-edge-1) } } type nav struct { dirs []*dir dirCache map[string]*dir saves map[string]bool marks map[string]int markInd int height int search string } func newNav(height int) *nav { wd, err := os.Getwd() if err != nil { log.Printf("getting current directory: %s", err) } nav := &nav{ dirCache: make(map[string]*dir), marks: make(map[string]int), saves: make(map[string]bool), markInd: 0, height: height, } nav.getDirs(wd) return nav } 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.load(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, nav.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] } nav.dirs = dirs } func (nav *nav) renew(height int) { nav.height = height for _, d := range nav.dirs { name := d.name() d.renew() d.sort() d.find(name, 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) load(path string) *dir { dir, ok := nav.dirCache[path] if !ok { dir = newDir(path) dir.sort() dir.ind, dir.pos = 0, 0 nav.dirCache[path] = dir } return dir } func (nav *nav) sort() { for _, d := range nav.dirs { name := d.name() d.sort() d.find(name, nav.height) } } 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.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.load(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) bot() { dir := nav.currDir() dir.ind = len(dir.fi) - 1 dir.pos = min(dir.ind, nav.height-1) } 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 { marks := nav.currMarks() if err := saveFiles(marks, 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 { return fmt.Errorf("putting files: %s", err) } 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) 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.getDirs(wd) 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) 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 }