diff --git a/app.go b/app.go index 1ee75766..1f18d6c5 100644 --- a/app.go +++ b/app.go @@ -19,24 +19,25 @@ import ( ) type app struct { - ui *ui // ui state (screen, windows, input) - nav *nav // navigation state (dirs, cursor, selections, preview, caches) - ticker *time.Ticker // refresh ticker if `period` > 0 - quitChan chan struct{} // signals main loop to exit - cmd *exec.Cmd // currently running % (shell-pipe) command - cmdIn io.WriteCloser // stdin writer for running % command - cmdOutBuf []byte // output of running % command - cmdHistory []string // command history entries - cmdHistoryBeg int // index where commands from this session start in cmdHistory - cmdHistoryInd int // history navigation offset from most recent - cmdHistoryInput *string // initial input used as prefix filter while browsing history - menuCompActive bool // whether completion cycling is active - menuCompTmp []string // token snapshot taken when completion cycling starts, used for `cmd-menu-discard` - menuComps []compMatch // completion candidates for active prompt - menuCompInd int // index of selected completion candidate (-1: none selected) - selectionOut []string // paths to output on exit, used for `-print-selection` and `-selection-path` - watch *watch // fs watcher if `watch` is enabled - quitting bool // guard to prevent re-entering quit logic + ui *ui // ui state (screen, windows, input) + nav *nav // navigation state (dirs, cursor, selections, preview, caches) + ticker *time.Ticker // refresh ticker if `period` > 0 + quitChan chan struct{} // signals main loop to exit + cmd *exec.Cmd // currently running % (shell-pipe) command + cmdIn io.WriteCloser // stdin writer for running % command + cmdOutBuf []byte // output of running % command + cmdHistory []string // command history entries + cmdHistoryBeg int // index where commands from this session start in cmdHistory + cmdHistoryInd int // history navigation offset from most recent + cmdHistoryInput *string // initial input used as prefix filter while browsing history + menuCompActive bool // whether completion cycling is active + menuCompTmp []string // token snapshot taken when completion cycling starts, used for `cmd-menu-discard` + menuComps []compMatch // completion candidates for active prompt + menuCompInd int // index of selected completion candidate (-1: none) + selectionOut []string // paths to output on exit, used for `-print-selection` and `-selection-path` + watch *watch // fs watcher if `watch` is enabled + quitting bool // guard to prevent re-entering quit logic + noSuspend bool // suppress terminal suspend/resume and "Press any key" for sync hooks } func newApp(ui *ui, nav *nav) *app { @@ -395,18 +396,53 @@ func (app *app) loop() { oldCurrPath = curr.path } - if prev, ok := app.nav.dirCache[d.path]; ok { - d.ind = prev.ind - d.pos = prev.pos - d.visualAnchor = min(prev.visualAnchor, len(d.files)-1) - d.visualWrap = prev.visualWrap - d.filter = prev.filter - d.sort() - d.sel(prev.name(), app.nav.height) - } else { - d.sort() + // Avoid flickering UI and multiple, unnecessary `on-load` calls + // triggered by Git commands executed inside the users `on-load` + // command (often used to add git symbols using `addcustominfo`). + // TODO: Should `watch` also ignore `.git` directories? + // Only run on-load for the currently focused directory. + // Parent directories are loaded too but skipping their hooks saves + // ~20ms each, eliminating the main-loop blocking that causes preview lag. + // Parent panes will use global hiddenfiles (hides dotfiles) instead of + // gitignore-aware local settings — a reasonable tradeoff for speed. + if filepath.Base(d.path) != ".git" && d.path == app.nav.currDir().path { + paths := make([]string, len(d.allFiles)) + for i, file := range d.allFiles { + paths[i] = file.path + } + onLoad(app, paths) + } + + var prevName string + if prev, ok := app.nav.dirCache[d.path]; ok { + d.ind = prev.ind + d.pos = prev.pos + d.visualAnchor = min(prev.visualAnchor, len(d.files)-1) + d.visualWrap = prev.visualWrap + d.filter = prev.filter + prevName = prev.name() + } + + // Put loaded directory into cache BEFORE draining server commands. + // This ensures commands like addcustominfo (sent by lf -remote from + // on-load) can find files in d.allFiles. + app.nav.dirCache[d.path] = d + + // Drain pending server commands (e.g. setlocal) so local options apply + // before the first sort and draw. + if !gSingleMode { + for drained := true; drained; { + select { + case e := <-serverChan: + e.eval(app, nil) + default: + drained = false + } } - app.nav.dirCache[d.path] = d + } + + d.sort() + d.sel(prevName, app.nav.height) app.nav.position() @@ -418,18 +454,6 @@ func (app *app) loop() { app.watchDir(d) - // Avoid flickering UI and multiple, unnecessary `on-load` calls - // triggered by Git commands executed inside the users `on-load` - // command (often used to add git symbols using `addcustominfo`). - // TODO: Should `watch` also ignore `.git` directories? - if filepath.Base(d.path) != ".git" { - paths := make([]string, len(d.allFiles)) - for i, file := range d.allFiles { - paths[i] = file.path - } - onLoad(app, paths) - } - if d.path == app.nav.currDir().path { app.nav.preload() } @@ -536,6 +560,20 @@ func (app *app) loop() { func (app *app) runCmdSync(cmd *exec.Cmd, pauseAfter bool) { app.nav.previewChan <- "" + if app.noSuspend { + cmd.Stdout = io.Discard + cmd.Stderr = io.Discard + cmd.Stdin = nil + + if err := cmd.Run(); err != nil { + app.ui.echoerrf("running shell: %s", err) + } + + app.ui.loadFile(app, true) + app.nav.renew() + return + } + if err := app.ui.suspend(); err != nil { log.Printf("suspend: %s", err) } @@ -549,7 +587,7 @@ func (app *app) runCmdSync(cmd *exec.Cmd, pauseAfter bool) { if err := cmd.Run(); err != nil { app.ui.echoerrf("running shell: %s", err) } - if pauseAfter { + if pauseAfter && !app.noSuspend { anyKey() } diff --git a/client.go b/client.go index cae82ea4..7e01efda 100644 --- a/client.go +++ b/client.go @@ -146,7 +146,7 @@ func writeSelection(filename string, selection []string) { } func readExpr() <-chan expr { - ch := make(chan expr) + ch := make(chan expr, 32) go func() { duration := 100 * time.Millisecond diff --git a/doc.md b/doc.md index aed366bb..c216f96e 100644 --- a/doc.md +++ b/doc.md @@ -1504,7 +1504,7 @@ Command `set` is used to set an option which can be a boolean, integer, or strin set sortby "time" # string value with double quotes (backslash escapes) Command `setlocal` is used to set a local option for a directory which can be a boolean or string. -Currently supported local options are `dircounts`, `dirfirst`, `dironly`, `hidden`, `info`, `reverse`, `sortby`, `sortignorecase` and `sortignoredia`. +Currently supported local options are `dircounts`, `dirfirst`, `dironly`, `hidden`, `hiddenfiles`, `info`, `reverse`, `sortby`, `sortignorecase` and `sortignoredia`. setlocal /foo/bar hidden # boolean enable setlocal /foo/bar hidden true # boolean enable diff --git a/doc.txt b/doc.txt index 65b71afa..d07cd9e9 100644 --- a/doc.txt +++ b/doc.txt @@ -1655,8 +1655,8 @@ string: Command setlocal is used to set a local option for a directory which can be a boolean or string. Currently supported local options are dircounts, -dirfirst, dironly, hidden, info, reverse, sortby, sortignorecase and -sortignoredia. +dirfirst, dironly, hidden, hiddenfiles, info, reverse, sortby, +sortignorecase and sortignoredia. setlocal /foo/bar hidden # boolean enable setlocal /foo/bar hidden true # boolean enable diff --git a/eval.go b/eval.go index 07b15709..f7671160 100644 --- a/eval.go +++ b/eval.go @@ -522,36 +522,63 @@ func (e *setLocalExpr) eval(app *app, _ []string) { err = applyLocalBoolOpt(gLocalOpts.dircounts, gOpts.dircounts, e) case "dirfirst", "nodirfirst", "dirfirst!": err = applyLocalBoolOpt(gLocalOpts.dirfirst, gOpts.dirfirst, e) - if err == nil { + if err == nil && !app.noSuspend { app.nav.sort() } case "dironly", "nodironly", "dironly!": err = applyLocalBoolOpt(gLocalOpts.dironly, gOpts.dironly, e) - if err == nil { + if err == nil && !app.noSuspend { app.nav.sort() app.nav.position() app.ui.loadFile(app, true) } case "hidden", "nohidden", "hidden!": err = applyLocalBoolOpt(gLocalOpts.hidden, gOpts.hidden, e) - if err == nil { + if err == nil && !app.noSuspend { + app.nav.sort() + app.nav.position() + app.ui.loadFile(app, true) + } + case "hiddenfiles": + if e.val == "" { + gLocalOpts.hiddenfiles[e.path] = nil + } else { + toks := strings.Split(e.val, ":") + for _, s := range toks { + if s == "" { + app.ui.echoerr("hiddenfiles: glob should be non-empty") + return + } + _, err := filepath.Match(s, "a") + if err != nil { + app.ui.echoerrf("hiddenfiles: %s", err) + return + } + } + gLocalOpts.hiddenfiles[e.path] = toks + } + if !app.noSuspend { app.nav.sort() app.nav.position() app.ui.loadFile(app, true) } case "reverse", "noreverse", "reverse!": err = applyLocalBoolOpt(gLocalOpts.reverse, gOpts.reverse, e) - if err == nil { + if err == nil && !app.noSuspend { app.nav.sort() } case "sortignorecase", "nosortignorecase", "sortignorecase!": err = applyLocalBoolOpt(gLocalOpts.sortignorecase, gOpts.sortignorecase, e) - if err == nil { + if err == nil && !app.noSuspend { + app.nav.sort() + } + err = applyLocalBoolOpt(gLocalOpts.sortignorecase, gOpts.sortignorecase, e) + if err == nil && !app.noSuspend { app.nav.sort() } case "sortignoredia", "nosortignoredia", "sortignoredia!": err = applyLocalBoolOpt(gLocalOpts.sortignoredia, gOpts.sortignoredia, e) - if err == nil { + if err == nil && !app.noSuspend { app.nav.sort() } case "info": @@ -654,7 +681,9 @@ func onChdir(app *app) { func onLoad(app *app, files []string) { if cmd, ok := gOpts.cmds["on-load"]; ok { + app.noSuspend = true cmd.eval(app, files) + app.noSuspend = false } } diff --git a/go.mod b/go.mod index 4ae0edef..897ba50f 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/gokcehan/lf +module github.com/Olyxz16/lf go 1.25.0 diff --git a/lf.1 b/lf.1 index b1ae2691..a5b9f3d7 100644 --- a/lf.1 +++ b/lf.1 @@ -1,6 +1,6 @@ .\" Automatically generated by Pandoc 3.7.0.2 .\" -.TH "LF" "1" "2026\-05\-30" "r42" "DOCUMENTATION" +.TH "LF" "1" "2026\-06\-25" "r1" "DOCUMENTATION" .SH NAME lf \- terminal file manager .SH SYNOPSIS @@ -1451,8 +1451,9 @@ Command \f[CR]setlocal\f[R] is used to set a local option for a directory which can be a boolean or string. Currently supported local options are \f[CR]dircounts\f[R], \f[CR]dirfirst\f[R], \f[CR]dironly\f[R], \f[CR]hidden\f[R], -\f[CR]info\f[R], \f[CR]reverse\f[R], \f[CR]sortby\f[R], -\f[CR]sortignorecase\f[R] and \f[CR]sortignoredia\f[R]. +\f[CR]hiddenfiles\f[R], \f[CR]info\f[R], \f[CR]reverse\f[R], +\f[CR]sortby\f[R], \f[CR]sortignorecase\f[R] and +\f[CR]sortignoredia\f[R]. .IP .EX setlocal /foo/bar hidden # boolean enable diff --git a/nav.go b/nav.go index 397aab35..08ca92a9 100644 --- a/nav.go +++ b/nav.go @@ -207,7 +207,7 @@ func (dir *dir) sort() { dir.dironly = getDirOnly(dir.path) dir.hidden = getHidden(dir.path) dir.reverse = getReverse(dir.path) - dir.hiddenfiles = gOpts.hiddenfiles + dir.hiddenfiles = getHiddenFiles(dir.path) dir.sortignorecase = getSortIgnoreCase(dir.path) dir.sortignoredia = getSortIgnoreDia(dir.path) @@ -501,7 +501,7 @@ func (nav *nav) getDir(path string) *dir { hidden: getHidden(path), reverse: getReverse(path), visualAnchor: -1, - hiddenfiles: gOpts.hiddenfiles, + hiddenfiles: getHiddenFiles(path), sortignorecase: getSortIgnoreCase(path), sortignoredia: getSortIgnoreDia(path), } @@ -545,7 +545,7 @@ func (nav *nav) checkDir(dir *dir) { dir.dironly != getDirOnly(dir.path) || dir.hidden != getHidden(dir.path) || dir.reverse != getReverse(dir.path) || - !slices.Equal(dir.hiddenfiles, gOpts.hiddenfiles) || + !slices.Equal(dir.hiddenfiles, getHiddenFiles(dir.path)) || dir.sortignorecase != getSortIgnoreCase(dir.path) || dir.sortignoredia != getSortIgnoreDia(dir.path): dir.loading = true diff --git a/opts.go b/opts.go index afcee68f..010deddc 100644 --- a/opts.go +++ b/opts.go @@ -171,6 +171,7 @@ var gLocalOpts struct { dirfirst map[string]bool dironly map[string]bool hidden map[string]bool + hiddenfiles map[string][]string info map[string][]string reverse map[string]bool sortby map[string]sortMethod @@ -206,6 +207,13 @@ func getHidden(path string) bool { return gOpts.hidden } +func getHiddenFiles(path string) []string { + if val, ok := gLocalOpts.hiddenfiles[path]; ok { + return val + } + return gOpts.hiddenfiles +} + func getInfo(path string) []string { if val, ok := gLocalOpts.info[path]; ok { return val @@ -455,6 +463,7 @@ func init() { gLocalOpts.dirfirst = make(map[string]bool) gLocalOpts.dironly = make(map[string]bool) gLocalOpts.hidden = make(map[string]bool) + gLocalOpts.hiddenfiles = make(map[string][]string) gLocalOpts.info = make(map[string][]string) gLocalOpts.reverse = make(map[string]bool) gLocalOpts.sortby = make(map[string]sortMethod)