Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use fsnotify for watching file updates #1667

Merged
merged 31 commits into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7c3cfa8
Add watch option
joelim-work Mar 30, 2024
20646bf
Implement directory changes
joelim-work Mar 30, 2024
574513a
Implement file changes
joelim-work Mar 30, 2024
7ce0756
Fix for dircounts
joelim-work Mar 30, 2024
54c1c9c
Remove test code
joelim-work Mar 31, 2024
4ec8ba5
fix dircounts
joelim-work Mar 31, 2024
e9ff541
Set watches on cd instead of select
joelim-work Mar 31, 2024
ad38761
Set watches on startup too
joelim-work Mar 31, 2024
bdc9ba0
Watch newly created directories
joelim-work Mar 31, 2024
ced735a
Add chmod events
joelim-work Apr 1, 2024
76aacc7
Avoid using multiple threads
joelim-work Apr 4, 2024
4f898cd
Reduce logic when updating files
joelim-work Apr 4, 2024
33f7204
Throttle write updates
joelim-work Apr 5, 2024
be9113e
Use timer instead of ticker
joelim-work Apr 5, 2024
9c0afc5
Merge branch 'master' into fsnotify
joelim-work Apr 8, 2024
089c540
Fix file write out-of-band timing issue
joelim-work Apr 10, 2024
d3f7c08
Fix cursor when renaming
joelim-work Apr 11, 2024
c695904
Prevent file showing at bottom during rename
joelim-work Apr 12, 2024
6856804
Watch new directory when created
joelim-work Apr 22, 2024
1c29f34
Revert "Watch new directory when created"
joelim-work Apr 22, 2024
fb99853
Reapply "Watch new directory when created"
joelim-work Apr 22, 2024
e0dfe94
Merge branch 'master' into fsnotify
joelim-work Apr 22, 2024
f9a51ad
Try batching renew updates
joelim-work Apr 23, 2024
edcc1a5
Big rewrite
joelim-work Apr 23, 2024
c441aa5
Reload individual directories
joelim-work Apr 23, 2024
b3b1a4b
Change delay to 100 milliseconds
joelim-work Apr 24, 2024
b4eb749
Use separate goroutine for loading files/dirs
joelim-work Apr 27, 2024
3902642
Merge branch 'master' into fsnotify
joelim-work May 6, 2024
d3f7c6a
Merge branch 'master' into fsnotify
joelim-work Jun 22, 2024
4196bda
Fix filter resetting
joelim-work Jun 22, 2024
800cf3a
Add documentation
joelim-work Jun 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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