Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
122 changes: 80 additions & 42 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()

Expand All @@ -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()
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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()
}

Expand Down
2 changes: 1 addition & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions doc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 35 additions & 6 deletions eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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
}
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/gokcehan/lf
module github.com/Olyxz16/lf

go 1.25.0

Expand Down
7 changes: 4 additions & 3 deletions lf.1
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions nav.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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),
}
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down