Refactor Nav API around symlinks handling (mostly) (#33)

* gofmt ui.go

* Refactor Nav API around symlinks handling (mostly)

1. Get both `Lstat` and `Stat` while reading the files. Stat info is
   used by default, but we still can tell it's a link if needed.
2. Remove all other `Stat` calls from different places.
3. Handle hidden files while reading.
4. `currFile` now returns error if there are no files.
5. `currEmpty` and `currPath` not longer necessary, removed.
6. `open` now returns a named error ErrNotDir, where expected.

7. Side effect: Links that point at directories are now sorted among
   "actual" directories.
This commit is contained in:
Karol Woźniak 2016-10-24 21:18:31 +02:00 committed by gokcehan
parent 880615ddd0
commit c9b4389c65
4 changed files with 127 additions and 111 deletions

6
app.go
View File

@ -76,11 +76,9 @@ func (app *App) handleInp() {
} }
func (app *App) exportVars() { func (app *App) exportVars() {
dir := app.nav.currDir()
var envFile string var envFile string
if len(dir.fi) != 0 { if f, err := app.nav.currFile(); err == nil {
envFile = app.nav.currPath() envFile = f.Path
} }
marks := app.nav.currMarks() marks := app.nav.currMarks()

28
eval.go
View File

@ -178,31 +178,16 @@ func (e *CallExpr) eval(app *App, args []string) {
} }
app.ui.loadFile(app.nav) app.ui.loadFile(app.nav)
case "open": case "open":
dir := app.nav.currDir() err := app.nav.open()
if err == nil {
if len(dir.fi) == 0 { app.ui.loadFile(app.nav)
return return
} }
if err != ErrNotDir {
path := app.nav.currPath()
f, err := os.Stat(path)
if err != nil {
msg := fmt.Sprintf("open: %s", err)
app.ui.message = msg
log.Print(msg)
return
}
if f.IsDir() {
if err := app.nav.open(); err != nil {
app.ui.message = err.Error() app.ui.message = err.Error()
log.Print(err) log.Print(err)
return return
} }
app.ui.loadFile(app.nav)
return
}
if gSelectionPath != "" { if gSelectionPath != "" {
out, err := os.Create(gSelectionPath) out, err := os.Create(gSelectionPath)
@ -211,9 +196,14 @@ func (e *CallExpr) eval(app *App, args []string) {
} }
defer out.Close() defer out.Close()
var path string
if len(app.nav.marks) != 0 { if len(app.nav.marks) != 0 {
marks := app.nav.currMarks() marks := app.nav.currMarks()
path = strings.Join(marks, "\n") path = strings.Join(marks, "\n")
} else if curr, err := app.nav.currFile(); err == nil {
path = curr.Path
} else {
return
} }
_, err = out.WriteString(path) _, err = out.WriteString(path)

144
nav.go
View File

@ -3,7 +3,6 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os" "os"
"os/exec" "os/exec"
@ -12,14 +11,21 @@ import (
"strings" "strings"
) )
type Dir struct { type LinkState int8
ind int // which entry is highlighted
pos int // which line in the ui highlighted entry is const (
path string NotLink LinkState = iota
fi []os.FileInfo Working
Broken
)
type File struct {
os.FileInfo
LinkState LinkState
Path string
} }
type ByName []os.FileInfo type ByName []*File
func (a ByName) Len() int { return len(a) } func (a ByName) Len() int { return len(a) }
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
@ -28,19 +34,19 @@ func (a ByName) Less(i, j int) bool {
return strings.ToLower(a[i].Name()) < strings.ToLower(a[j].Name()) return strings.ToLower(a[i].Name()) < strings.ToLower(a[j].Name())
} }
type BySize []os.FileInfo type BySize []*File
func (a BySize) Len() int { return len(a) } func (a BySize) Len() int { return len(a) }
func (a BySize) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a BySize) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a BySize) Less(i, j int) bool { return a[i].Size() < a[j].Size() } func (a BySize) Less(i, j int) bool { return a[i].Size() < a[j].Size() }
type ByTime []os.FileInfo type ByTime []*File
func (a ByTime) Len() int { return len(a) } func (a ByTime) Len() int { return len(a) }
func (a ByTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByTime) Less(i, j int) bool { return a[i].ModTime().Before(a[j].ModTime()) } func (a ByTime) Less(i, j int) bool { return a[i].ModTime().Before(a[j].ModTime()) }
type ByDir []os.FileInfo type ByDir []*File
func (a ByDir) Len() int { return len(a) } func (a ByDir) Len() int { return len(a) }
func (a ByDir) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByDir) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
@ -52,7 +58,7 @@ func (a ByDir) Less(i, j int) bool {
return a[i].IsDir() return a[i].IsDir()
} }
type ByNum []os.FileInfo type ByNum []*File
func (a ByNum) Len() int { return len(a) } func (a ByNum) Len() int { return len(a) }
func (a ByNum) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByNum) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
@ -85,15 +91,10 @@ func (a ByNum) Less(i, j int) bool {
return i < j return i < j
} }
func organizeFiles(fi []os.FileInfo) []os.FileInfo { func getFilesSorted(path string) []*File {
if !gOpts.hidden { fi, err := readdir(path)
var tmp []os.FileInfo if err != nil {
for _, f := range fi { log.Printf("reading directory: %s", err)
if f.Name()[0] != '.' {
tmp = append(tmp, f)
}
}
fi = tmp
} }
switch gOpts.sortby { switch gOpts.sortby {
@ -116,34 +117,72 @@ func organizeFiles(fi []os.FileInfo) []os.FileInfo {
return fi return fi
} }
func newDir(path string) *Dir { func readdir(path string) ([]*File, error) {
fi, err := ioutil.ReadDir(path) f, err := os.Open(path)
if err != nil { if err != nil {
log.Printf("reading directory: %s", err) return nil, err
} }
fi = organizeFiles(fi) names, err := f.Readdirnames(-1)
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{ return &Dir{
path: path, path: path,
fi: fi, fi: getFilesSorted(path),
} }
} }
func (dir *Dir) renew(height int) { func (dir *Dir) renew(height int) {
fi, err := ioutil.ReadDir(dir.path)
if err != nil {
log.Printf("reading directory: %s", err)
}
fi = organizeFiles(fi)
var name string var name string
if len(dir.fi) != 0 { if len(dir.fi) != 0 {
name = dir.fi[dir.ind].Name() name = dir.fi[dir.ind].Name()
} }
dir.fi = fi dir.fi = getFilesSorted(dir.path)
dir.load(dir.ind, dir.pos, height, name) dir.load(dir.ind, dir.pos, height, name)
} }
@ -296,8 +335,17 @@ func (nav *Nav) updir() error {
return nil return nil
} }
var ErrNotDir = fmt.Errorf("not a directory")
func (nav *Nav) open() error { func (nav *Nav) open() error {
path := nav.currPath() curr, err := nav.currFile()
if err != nil {
return err
}
if !curr.IsDir() {
return ErrNotDir
}
path := curr.Path
dir := newDir(path) dir := newDir(path)
@ -354,11 +402,12 @@ func (nav *Nav) toggleMark(path string) {
} }
func (nav *Nav) toggle() { func (nav *Nav) toggle() {
if nav.currEmpty() { curr, err := nav.currFile()
if err != nil {
return return
} }
nav.toggleMark(nav.currPath()) nav.toggleMark(curr.Path)
nav.down(1) nav.down(1)
} }
@ -373,13 +422,12 @@ func (nav *Nav) invert() {
func (nav *Nav) save(keep bool) error { func (nav *Nav) save(keep bool) error {
if len(nav.marks) == 0 { if len(nav.marks) == 0 {
if nav.currEmpty() { curr, err := nav.currFile()
if err != nil {
return errors.New("no file selected") return errors.New("no file selected")
} }
path := nav.currPath() if err := saveFiles([]string{curr.Path}, keep); err != nil {
if err := saveFiles([]string{path}, keep); err != nil {
return err return err
} }
} else { } else {
@ -433,23 +481,17 @@ func (nav *Nav) paste() error {
return nil return nil
} }
func (nav *Nav) currEmpty() bool {
return len(nav.dirs[len(nav.dirs)-1].fi) == 0
}
func (nav *Nav) currDir() *Dir { func (nav *Nav) currDir() *Dir {
return nav.dirs[len(nav.dirs)-1] return nav.dirs[len(nav.dirs)-1]
} }
func (nav *Nav) currFile() os.FileInfo { func (nav *Nav) currFile() (*File, error) {
last := nav.dirs[len(nav.dirs)-1] last := nav.dirs[len(nav.dirs)-1]
return last.fi[last.ind]
}
func (nav *Nav) currPath() string { if len(last.fi) == 0 {
last := nav.dirs[len(nav.dirs)-1] return nil, fmt.Errorf("empty directory")
curr := last.fi[last.ind] }
return filepath.Join(last.path, curr.Name()) return last.fi[last.ind], nil
} }
func (nav *Nav) currMarks() []string { func (nav *Nav) currMarks() []string {

54
ui.go
View File

@ -236,6 +236,15 @@ func (win *Win) printd(dir *Dir, marks map[string]bool) {
for i, f := range dir.fi[beg:end] { for i, f := range dir.fi[beg:end] {
switch { switch {
case f.LinkState != NotLink:
if f.LinkState == Working {
fg = termbox.ColorCyan
if f.Mode().IsDir() {
fg |= termbox.AttrBold
}
} else {
fg = termbox.ColorMagenta
}
case f.Mode().IsRegular(): case f.Mode().IsRegular():
if f.Mode()&0111 != 0 { if f.Mode()&0111 != 0 {
fg = termbox.AttrBold | termbox.ColorGreen fg = termbox.AttrBold | termbox.ColorGreen
@ -244,8 +253,6 @@ func (win *Win) printd(dir *Dir, marks map[string]bool) {
} }
case f.Mode().IsDir(): case f.Mode().IsDir():
fg = termbox.AttrBold | termbox.ColorBlue fg = termbox.AttrBold | termbox.ColorBlue
case f.Mode()&os.ModeSymlink != 0:
fg = termbox.ColorCyan
case f.Mode()&os.ModeNamedPipe != 0: case f.Mode()&os.ModeNamedPipe != 0:
fg = termbox.ColorRed fg = termbox.ColorRed
case f.Mode()&os.ModeSocket != 0: case f.Mode()&os.ModeSocket != 0:
@ -390,39 +397,26 @@ func (ui *UI) renew() {
} }
func (ui *UI) loadFile(nav *Nav) { func (ui *UI) loadFile(nav *Nav) {
dir := nav.currDir() curr, err := nav.currFile()
if err != nil {
if len(dir.fi) == 0 {
return return
} }
curr := nav.currFile()
ui.message = fmt.Sprintf("%v %v %v", curr.Mode(), humanize(curr.Size()), curr.ModTime().Format(time.ANSIC)) ui.message = fmt.Sprintf("%v %v %v", curr.Mode(), humanize(curr.Size()), curr.ModTime().Format(time.ANSIC))
if !gOpts.preview { if !gOpts.preview {
return return
} }
path := nav.currPath() if curr.IsDir() {
dir := newDir(curr.Path)
f, err := os.Stat(path) dir.load(nav.inds[curr.Path], nav.poss[curr.Path], nav.height, nav.names[curr.Path])
if err != nil {
msg := fmt.Sprintf("getting file information: %s", err)
ui.message = msg
log.Print(msg)
return
}
if f.IsDir() {
dir := newDir(path)
dir.load(nav.inds[path], nav.poss[path], nav.height, nav.names[path])
ui.dirprev = dir ui.dirprev = dir
} else if f.Mode().IsRegular() { } else if curr.Mode().IsRegular() {
var reader io.Reader var reader io.Reader
if len(gOpts.previewer) != 0 { if len(gOpts.previewer) != 0 {
cmd := exec.Command(gOpts.previewer, path, strconv.Itoa(nav.height)) cmd := exec.Command(gOpts.previewer, curr.Path, strconv.Itoa(nav.height))
out, err := cmd.StdoutPipe() out, err := cmd.StdoutPipe()
if err != nil { if err != nil {
@ -441,7 +435,7 @@ func (ui *UI) loadFile(nav *Nav) {
defer out.Close() defer out.Close()
reader = out reader = out
} else { } else {
f, err := os.Open(path) f, err := os.Open(curr.Path)
if err != nil { if err != nil {
msg := fmt.Sprintf("opening file: %s", err) msg := fmt.Sprintf("opening file: %s", err)
ui.message = msg ui.message = msg
@ -489,7 +483,7 @@ func (ui *UI) draw(nav *Nav) {
// leave the cursor at the beginning of the current file for screen readers // leave the cursor at the beginning of the current file for screen readers
var length, woff, doff int var length, woff, doff int
defer func() { defer func() {
fmt.Printf("[%d;%dH", ui.wins[woff+length-1].y + nav.dirs[doff+length-1].pos + 1, ui.wins[woff+length-1].x + 1) fmt.Printf("[%d;%dH", ui.wins[woff+length-1].y+nav.dirs[doff+length-1].pos+1, ui.wins[woff+length-1].x+1)
}() }()
defer termbox.Flush() defer termbox.Flush()
@ -519,20 +513,12 @@ func (ui *UI) draw(nav *Nav) {
defer ui.msgwin.print(0, 0, fg, bg, ui.message) defer ui.msgwin.print(0, 0, fg, bg, ui.message)
if gOpts.preview { if gOpts.preview {
if len(dir.fi) == 0 { f, err := nav.currFile()
if err != nil {
return return
} }
preview := ui.wins[len(ui.wins)-1] preview := ui.wins[len(ui.wins)-1]
path := nav.currPath()
f, err := os.Stat(path)
if err != nil {
msg := fmt.Sprintf("getting file information: %s", err)
ui.message = msg
log.Print(msg)
return
}
if f.IsDir() { if f.IsDir() {
preview.printd(ui.dirprev, nav.marks) preview.printd(ui.dirprev, nav.marks)