Skip to content

Commit

Permalink
Use fsnotify for watching file updates (#1667)
Browse files Browse the repository at this point in the history
* Add watch option

* Implement directory changes

* Implement file changes

* Fix for dircounts

* Remove test code

* fix dircounts

* Set watches on cd instead of select

* Set watches on startup too

* Watch newly created directories

* Add chmod events

* Avoid using multiple threads

Seems to cause issues if there are multiple threads and each one of them
is loading the same new file, in which case there is a race condition.

* Reduce logic when updating files

* Throttle write updates

* Use timer instead of ticker

* Fix file write out-of-band timing issue

* Fix cursor when renaming

* Prevent file showing at bottom during rename

* Watch new directory when created

* Revert "Watch new directory when created"

This reverts commit 6856804.

* Reapply "Watch new directory when created"

This reverts commit 1c29f34.

* Try batching renew updates

* Big rewrite

* Reload individual directories

* Change delay to 100 milliseconds

* Use separate goroutine for loading files/dirs

* Fix filter resetting

* Add documentation
  • Loading branch information
joelim-work committed Jun 22, 2024
1 parent 73030b5 commit e350e0b
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 84 deletions.
50 changes: 50 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type app struct {
menuComps []string
menuCompInd int
selectionOut []string
watch *watch
}

func newApp(ui *ui, nav *nav) *app {
Expand All @@ -44,6 +45,7 @@ func newApp(ui *ui, nav *nav) *app {
nav: nav,
ticker: new(time.Ticker),
quitChan: quitChan,
watch: newWatch(nav.dirChan, nav.fileChan),
}

sigChan := make(chan os.Signal, 1)
Expand Down Expand Up @@ -382,6 +384,8 @@ func (app *app) loop() {
if ok {
d.ind = prev.ind
d.pos = prev.pos
d.filter = prev.filter
d.sort()
d.sel(prev.name(), app.nav.height)
}

Expand Down Expand Up @@ -414,6 +418,8 @@ func (app *app) loop() {
}
}

app.setWatchPaths()

app.ui.draw(app.nav)
case r := <-app.nav.regChan:
app.nav.regCache[r.path] = r
Expand All @@ -425,6 +431,31 @@ func (app *app) loop() {
}
}

app.ui.draw(app.nav)
case f := <-app.nav.fileChan:
dirs := app.nav.dirs
if app.ui.dirPrev != nil {
dirs = append(dirs, app.ui.dirPrev)
}

for _, dir := range dirs {
if dir.path != filepath.Dir(f.path) {
continue
}

for i := range dir.allFiles {
if dir.allFiles[i].path == f.path {
dir.allFiles[i] = f
break
}
}

name := dir.name()
dir.sort()
dir.sel(name, app.nav.height)
}

app.ui.loadFile(app, false)
app.ui.draw(app.nav)
case ev := <-app.ui.evChan:
e := app.ui.readEvent(ev, app.nav)
Expand Down Expand Up @@ -591,3 +622,22 @@ func (app *app) runShell(s string, args []string, prefix string) {
}()
}
}

func (app *app) setWatchPaths() {
if !gOpts.watch || len(app.nav.dirs) == 0 {
return
}

paths := make(map[string]bool)
for _, dir := range app.nav.dirs {
paths[dir.path] = true
}

for _, file := range app.nav.currDir().allFiles {
if file.IsDir() {
paths[file.path] = true
}
}

app.watch.set(paths)
}
6 changes: 6 additions & 0 deletions doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ The following options can be used to customize the behavior of lf:
truncatechar string (default '~')
truncatepct int (default 100)
waitmsg string (default 'Press any key to continue')
watch bool (default false)
wrapscan bool (default true)
wrapscroll bool (default false)
user_{option} string (default none)
Expand Down Expand Up @@ -1016,6 +1017,11 @@ while a value of 0 will only show the end of the filename, e.g.:

String shown after commands of shell-wait type.

## watch (bool) (default false)

Watch the filesystem for changes using `fsnotify` to automatically refresh file information.
FUSE is currently not supported due to limitations in `fsnotify`.

## wrapscan (bool) (default true)

Searching can wrap around the file list.
Expand Down
11 changes: 11 additions & 0 deletions eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,16 @@ func (e *setExpr) eval(app *app, args []string) {
app.ui.sort()
app.ui.loadFile(app, true)
}
case "watch", "nowatch", "watch!":
err = applyBoolOpt(&gOpts.watch, e)
if err == nil {
if gOpts.watch {
app.watch.start()
app.setWatchPaths()
} else {
app.watch.stop()
}
}
case "wrapscan", "nowrapscan", "wrapscan!":
err = applyBoolOpt(&gOpts.wrapscan, e)
case "wrapscroll", "nowrapscroll", "wrapscroll!":
Expand Down Expand Up @@ -563,6 +573,7 @@ func preChdir(app *app) {

func onChdir(app *app) {
app.nav.addJumpList()
app.setWatchPaths()
if cmd, ok := gOpts.cmds["on-cd"]; ok {
cmd.eval(app, nil)
}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.18

require (
github.com/djherbis/times v1.6.0
github.com/fsnotify/fsnotify v1.7.0
github.com/gdamore/tcell/v2 v2.7.4
github.com/mattn/go-runewidth v0.0.15
golang.org/x/sys v0.21.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
Expand Down
172 changes: 88 additions & 84 deletions nav.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,84 @@ type file struct {
err error
}

func newFile(path string) *file {
lstat, err := os.Lstat(path)

if err != nil {
log.Printf("getting file information: %s", err)
return &file{
FileInfo: &fakeStat{name: filepath.Base(path)},
linkState: notLink,
linkTarget: "",
path: path,
dirCount: -1,
dirSize: -1,
accessTime: time.Unix(0, 0),
changeTime: time.Unix(0, 0),
ext: "",
err: err,
}
}

var linkState linkState
var linkTarget string

if lstat.Mode()&os.ModeSymlink != 0 {
stat, err := os.Stat(path)
if err == nil {
linkState = working
lstat = stat
} else {
linkState = broken
}
linkTarget, err = os.Readlink(path)
if err != nil {
log.Printf("reading link target: %s", err)
}
}

ts := times.Get(lstat)
at := ts.AccessTime()
var ct time.Time
// from times docs: ChangeTime() panics unless HasChangeTime() is true
if ts.HasChangeTime() {
ct = ts.ChangeTime()
} else {
// fall back to ModTime if ChangeTime cannot be determined
ct = lstat.ModTime()
}

dirCount := -1
if lstat.IsDir() && gOpts.dircounts {
d, err := os.Open(path)
if err != nil {
dirCount = -2
} else {
names, err := d.Readdirnames(1000)
d.Close()

if names == nil && err != io.EOF {
dirCount = -2
} else {
dirCount = len(names)
}
}
}

return &file{
FileInfo: lstat,
linkState: linkState,
linkTarget: linkTarget,
path: path,
dirCount: dirCount,
dirSize: -1,
accessTime: at,
changeTime: ct,
ext: getFileExtension(lstat),
err: nil,
}
}

func (file *file) TotalSize() int64 {
if file.IsDir() {
if file.dirSize >= 0 {
Expand Down Expand Up @@ -70,87 +148,7 @@ func readdir(path string) ([]*file, error) {

files := make([]*file, 0, len(names))
for _, fname := range names {
fpath := filepath.Join(path, fname)

lstat, err := os.Lstat(fpath)

if os.IsNotExist(err) {
continue
}
if err != nil {
log.Printf("getting file information: %s", err)
files = append(files, &file{
FileInfo: &fakeStat{name: fname},
linkState: notLink,
linkTarget: "",
path: fpath,
dirCount: -1,
dirSize: -1,
accessTime: time.Unix(0, 0),
changeTime: time.Unix(0, 0),
ext: "",
err: err,
})
continue
}

var linkState linkState
var linkTarget string

if lstat.Mode()&os.ModeSymlink != 0 {
stat, err := os.Stat(fpath)
if err == nil {
linkState = working
lstat = stat
} else {
linkState = broken
}
linkTarget, err = os.Readlink(fpath)
if err != nil {
log.Printf("reading link target: %s", err)
}
}

ts := times.Get(lstat)
at := ts.AccessTime()
var ct time.Time
// from times docs: ChangeTime() panics unless HasChangeTime() is true
if ts.HasChangeTime() {
ct = ts.ChangeTime()
} else {
// fall back to ModTime if ChangeTime cannot be determined
ct = lstat.ModTime()
}

dirCount := -1
if lstat.IsDir() && gOpts.dircounts {
d, err := os.Open(fpath)
if err != nil {
dirCount = -2
} else {
names, err := d.Readdirnames(1000)
d.Close()

if names == nil && err != io.EOF {
dirCount = -2
} else {
dirCount = len(names)
}
}
}

files = append(files, &file{
FileInfo: lstat,
linkState: linkState,
linkTarget: linkTarget,
path: fpath,
dirCount: dirCount,
dirSize: -1,
accessTime: at,
changeTime: ct,
ext: getFileExtension(lstat),
err: nil,
})
files = append(files, newFile(filepath.Join(path, fname)))
}

return files, err
Expand Down Expand Up @@ -440,6 +438,7 @@ type nav struct {
dirPreviewChan chan *dir
dirChan chan *dir
regChan chan *reg
fileChan chan *file
dirCache map[string]*dir
regCache map[string]*reg
saves map[string]bool
Expand Down Expand Up @@ -530,8 +529,6 @@ func (nav *nav) checkDir(dir *dir) {
dir.loadTime = now
go func() {
nd := newDir(dir.path)
nd.filter = dir.filter
nd.sort()
if gOpts.dirpreviews {
nav.dirPreviewChan <- nd
}
Expand Down Expand Up @@ -587,6 +584,7 @@ func newNav(height int) *nav {
dirPreviewChan: make(chan *dir, 1024),
dirChan: make(chan *dir),
regChan: make(chan *reg),
fileChan: make(chan *file),
dirCache: make(map[string]*dir),
regCache: make(map[string]*reg),
saves: make(map[string]bool),
Expand Down Expand Up @@ -1564,7 +1562,13 @@ func (nav *nav) rename() error {
dir := nav.loadDir(filepath.Dir(newPath))

if dir.loading {
dir.files = append(dir.files, &file{FileInfo: lstat})
for i := range dir.allFiles {
if dir.allFiles[i].path == oldPath {
dir.allFiles[i] = &file{FileInfo: lstat}
break
}
}
dir.sort()
}

dir.sel(lstat.Name(), nav.height)
Expand Down
2 changes: 2 additions & 0 deletions opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ var gOpts struct {
smartcase bool
smartdia bool
waitmsg string
watch bool
wrapscan bool
wrapscroll bool
findlen int
Expand Down Expand Up @@ -213,6 +214,7 @@ func init() {
gOpts.smartcase = true
gOpts.smartdia = false
gOpts.waitmsg = "Press any key to continue"
gOpts.watch = false
gOpts.wrapscan = true
gOpts.wrapscroll = false
gOpts.findlen = 1
Expand Down
Loading

0 comments on commit e350e0b

Please sign in to comment.