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

Add an ability to query files in the current directory as displayed in lf #1949

Merged
merged 1 commit into from
Apr 9, 2025

Conversation

MahouShoujoMivutilde
Copy link
Contributor

@MahouShoujoMivutilde MahouShoujoMivutilde commented Apr 6, 2025

What it does

Implements #1084 as lf -remote "query $id files".

It outputs \n separated list of full file (dir/link/etc) paths of all files in lf's current directory visible at the moment with the current settings. (But it doesn't care about your scroll position)

The benefit compared to using external sorting tools is that files maintains the same sorting order and filter as used in lf at the moment.

This means that you can continue to view your files in some external program, and the "next" and "previous" will mean the same thing as in lf, opening the same files as "j" and "k" in lf.

Example

Example usage with nsxiv (image viewer):

lfrc open nsxiv

set shell bash
set shellopts '-eu'
set ifs "\n"

cmd on-init &{{
    # show id at bottom right corner
    rfmt="  %a|  %p|  \033[7;31m %m \033[0m|  \033[7;33m %c \033[0m|  \033[7;35m %s \033[0m|  \033[7;34m %f \033[0m|  %i/%t [id = $id]"
    lf -remote "send $id set rulerfmt \"$rfmt\""
}}

cmd open &{{
    case $(file -b -L --mime-type -- $f) in
        text/* | */xml | application/json)
            lf -remote "send $id editor $f"
            ;;
        image/*)
            images="$(lf -remote "query $id files" |
                grep -Ei "\.(png|jpg|jpeg|gif|webp|tif|tiff|bmp|heic|heif|avif|jxl|svg)(_large)*$")"
            num_current="$(echo "$images" | nl | grep -F -- "$(basename $f)" | awk '{print $1}')"
            echo "$images" | nsxiv -io -n $num_current 2>/dev/null | while read -r file; do
                [ -z "$file" ] && continue
                # select "marked" [press m in nsxiv] files
                lf -remote "send $id toggle \"$file\""
            done
            ;;
        *)
            setsid -f $OPENER $f >/dev/null
            ;;
    esac
}}

map l open

The current state

So far, it works as expected, that is:

  1. exports (read: updates gState.data["files"]) the correct file paths
  2. maintains the correct order with the correct content (e.g. dironly, filter).
  3. does it before you run shell commands, assuming the directory is done loading:
    • so it won't work is some cases in custom on-... commands, typically just after launch

Basically, it should look like what you see in lf, if you scrolled all the way and copied everything, and resolved full path for everything. If it doesn't, it's a bug.

TODO - this needs fixing or at least being addressed

Implementation

Old

It works by calling app.nav.exportFilesInCurrDir() in a few key places, which updates gState.data["files"] if nav initialized and the current directory is done loading.

For convenience, exportFilesInCurrDir takes origin of call as the argument that you can pass to logs, but I don't expect we keep it (too noisy).

To choose the places of calls, I've gone over references of dir.sort(), avoiding exporting right after, and instead trying to find some higher level function where possible to avoid needless exports.

It seems like I could almost solely rely on export where dirChan is consumed. I explicitly added some exports just in case there are some corner cases I didn't account for, but it seems like rename, onChdir, nav.sort are covered by loop.case app.nav.dirChan export.

The only thing it doesn't cover is setFilter, which I added too.

Original implementation mentioned the above that always maintains files in sync with what is displayed in lf can be found here:

https://github.com/MahouShoujoMivutilde/lf/tree/query-files-always-in-sync

(force-pushed out of this branch to not clutter history)

Replaced with the proposed export in runShell(), as it works fine, and when it has problems (set dirpreviews true) - mine does too.


So, this is where we're at. It works fine for me. At this point it will be more productive to work on it in pr, should more changes be needed.

Copy link
Collaborator

@joelim-work joelim-work left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way query works by design is that the gState.data is only evaluated when running a shell command (e.g. !lf -remote "query $id files"). Otherwise if you try to keep the gState.data up to date at all times as you've done here, it becomes very messy and error prone - when making changes in the future, it's very easy to write code that will change the internal state but then forget to update gState.data as well.

This does mean that there is extra overhead whenever running a shell command, but I haven't noticed any slowdown in performance.

So for the record I managed to get it working myself, something like below:

diff --git a/app.go b/app.go
index eaa913c..e56cae0 100644
--- a/app.go
+++ b/app.go
@@ -552,6 +552,7 @@ func (app *app) runShell(s string, args []string, prefix string) {
 	gState.data["cmds"] = listCmds().String()
 	gState.data["jumps"] = listJumps(app.nav.jumpList, app.nav.jumpListInd).String()
 	gState.data["history"] = listHistory(app.cmdHistory).String()
+	gState.data["files"] = listFiles(app.nav.currDir()).String()
 	gState.mutex.Unlock()
 
 	cmd := shellCommand(s, args)
diff --git a/ui.go b/ui.go
index 54dd103..67c48a9 100644
--- a/ui.go
+++ b/ui.go
@@ -1204,6 +1204,16 @@ func listHistory(history []cmdItem) *bytes.Buffer {
 	return b
 }
 
+func listFiles(dir *dir) *strings.Builder {
+	b := new(strings.Builder)
+
+	for _, file := range dir.files {
+		fmt.Fprintln(b, file.path)
+	}
+
+	return b
+}
+
 func listMarks(marks map[string]string) *bytes.Buffer {
 	t := new(tabwriter.Writer)
 	b := new(bytes.Buffer)

@MahouShoujoMivutilde
Copy link
Contributor Author

MahouShoujoMivutilde commented Apr 6, 2025

Thanks for the feedback!

Yes, I understand. Your implementation is the first thing I tried after looking at how the other query variables are saved, but there were some issues that forced me to abandon it.

After some testing, I see that it panics in on-cd on start:

cmd on-cd &{{
    echo noop
}}

# this causes the panic
on-cd
Details

panic: runtime error: index out of range [-1]

goroutine 1 [running]:
main.(*nav).currDir(...)
/home/witch/projects/forked/elefu-his/nav.go:1982
main.(*app).runShell(0xc0000c8dd0, {0xc00067c000, 0x2b8}, {0x0, 0x0, 0x0}, {0xc0006766c0, 0x1})
/home/witch/projects/forked/elefu-his/app.go:555 +0xca8
main.(*execExpr).eval(0xc000270740, 0xc0000c8dd0, {0x0, 0x0, 0x0})
/home/witch/projects/forked/elefu-his/eval.go:2250 +0x3b9
main.(*callExpr).eval(0xc000678180, 0xc0000c8dd0, {0x0?, 0xc000000000?, 0x4cdf27?})
/home/witch/projects/forked/elefu-his/eval.go:2233 +0x7e5f
main.(*app).readFile(0xc0000c8dd0, {0xc0000183a0, 0x1b})
/home/witch/projects/forked/elefu-his/app.go:110 +0x194
main.(*app).loop(0xc0000c8dd0)
/home/witch/projects/forked/elefu-his/app.go:279 +0x1746
main.run()
/home/witch/projects/forked/elefu-his/client.go:65 +0x357
main.main()
/home/witch/projects/forked/elefu-his/main.go:357 +0x819

So at it should include the same guards my export has.

E.g. like this

diff --git a/app.go b/app.go
index eaa913c..a383bbb 100644
--- a/app.go
+++ b/app.go
@@ -552,6 +552,8 @@ func (app *app) runShell(s string, args []string, prefix string) {
 	gState.data["cmds"] = listCmds().String()
 	gState.data["jumps"] = listJumps(app.nav.jumpList, app.nav.jumpListInd).String()
 	gState.data["history"] = listHistory(app.cmdHistory).String()
+	// gState.data["files"] = listFiles(app.nav.currDir()).String()
+	app.nav.exportFilesInCurrDir()
 	gState.mutex.Unlock()

 	cmd := shellCommand(s, args)
diff --git a/nav.go b/nav.go
index 0935ff0..5baffac 100644
--- a/nav.go
+++ b/nav.go
@@ -718,6 +718,18 @@ func (nav *nav) exportFiles() {
 	}
 }

+// Updates gState.data["files"], doesn't lock
+func (nav *nav) exportFilesInCurrDir() {
+	if !nav.init {
+		return
+	}
+	dir := nav.currDir()
+	if dir.loading {
+		return
+	}
+	gState.data["files"] = listFiles(dir).String()
+}
+
 func (nav *nav) dirPreviewLoop(ui *ui) {
 	var prevPath string
 	for dir := range nav.dirPreviewChan {
diff --git a/ui.go b/ui.go
index 54dd103..67c48a9 100644
--- a/ui.go
+++ b/ui.go
@@ -1204,6 +1204,16 @@ func listHistory(history []cmdItem) *bytes.Buffer {
 	return b
 }

+func listFiles(dir *dir) *strings.Builder {
+	b := new(strings.Builder)
+
+	for _, file := range dir.files {
+		fmt.Fprintln(b, file.path)
+	}
+
+	return b
+}
+
 func listMarks(marks map[string]string) *bytes.Buffer {
 	t := new(tabwriter.Writer)
 	b := new(bytes.Buffer)

I don't remember precisely where else it fails, but I remember there was something, which is why I went with backtracking from dir.sort() calls method. (I was struggling to get it to work consistently, didn't exactly write everything down)

The basic testing shows that it works, including the filter which I initially suspected would fail.

I'll see if I can spot anything else.


In regards to the added latency, it seems fine.

  • for the directory with 6500 photos - exportFilesInCurrDir() took 4.632001ms; all runShell() exports took 5.755784ms (includes files).
  • For 50 files it's exportFilesInCurrDir() took 14.76µs; all runShell() exports took 575.982µs.
Details

diff --git a/app.go b/app.go
index eaa913c..9657948 100644
--- a/app.go
+++ b/app.go
@@ -541,6 +541,7 @@ func (app *app) runCmdSync(cmd *exec.Cmd, pause_after bool) {
 //	!       Yes   No     Yes    Yes     Yes     Pause and then resume
 //	&       No    Yes    No     No      No      Do nothing
 func (app *app) runShell(s string, args []string, prefix string) {
+	startRS := time.Now()
 	app.nav.exportFiles()
 	app.ui.exportSizes()
 	app.ui.exportMode()
@@ -552,6 +553,10 @@ func (app *app) runShell(s string, args []string, prefix string) {
 	gState.data["cmds"] = listCmds().String()
 	gState.data["jumps"] = listJumps(app.nav.jumpList, app.nav.jumpListInd).String()
 	gState.data["history"] = listHistory(app.cmdHistory).String()
+	startFiles := time.Now()
+	app.nav.exportFilesInCurrDir()
+	log.Printf("exportFilesInCurrDir() took %s", time.Since(startFiles))
+	log.Printf("all runShell() exports took %s", time.Since(startRS))
 	gState.mutex.Unlock()

 	cmd := shellCommand(s, args)
diff --git a/nav.go b/nav.go
index 0935ff0..5baffac 100644
--- a/nav.go
+++ b/nav.go
@@ -718,6 +718,18 @@ func (nav *nav) exportFiles() {
 	}
 }

+// Updates gState.data["files"], doesn't lock
+func (nav *nav) exportFilesInCurrDir() {
+	if !nav.init {
+		return
+	}
+	dir := nav.currDir()
+	if dir.loading {
+		return
+	}
+	gState.data["files"] = listFiles(dir).String()
+}
+
 func (nav *nav) dirPreviewLoop(ui *ui) {
 	var prevPath string
 	for dir := range nav.dirPreviewChan {
diff --git a/ui.go b/ui.go
index 54dd103..67c48a9 100644
--- a/ui.go
+++ b/ui.go
@@ -1204,6 +1204,16 @@ func listHistory(history []cmdItem) *bytes.Buffer {
 	return b
 }

+func listFiles(dir *dir) *strings.Builder {
+	b := new(strings.Builder)
+
+	for _, file := range dir.files {
+		fmt.Fprintln(b, file.path)
+	}
+
+	return b
+}
+
 func listMarks(marks map[string]string) *bytes.Buffer {
 	t := new(tabwriter.Writer)
 	b := new(bytes.Buffer)

@joelim-work
Copy link
Collaborator

@MahouShoujoMivutilde Good catch with finding the panic, I forgot that functions like app.nav.currDir() assume that lf has already been initialized. I think it's fine to keep all the code in a single function, just change the structure so it's like this:

func listFiles(nav *nav) string {
	// return "" if there's an error
}

Also just out of interest, what are the implications if the directory is loading? Would it be better to return nothing at all, or just proceed anyway and return potentially outdated information?

presented in lf (including filter/sorting)

`lf -remote "query $id files"`

This version updates `files` list only before running shell commands.
@MahouShoujoMivutilde
Copy link
Contributor Author

I can't find what else was wrong with the export in runShell.

Maybe that's all it was, just various custom on- functions running with no nav ready. on-init; on-redraw functions don't update files on launch too, because the directory isn't loaded yet.

So, I switch implementations in favor of yours.


A bug I discovered is that with set dirpreviews true and

#!/usr/bin/env bash
# previewer script

echo "Started slow preview demo..."
sleep 5s
echo "Done!"

loading won't be set false (and thus files won't be updated) until the previewer for the currently selected file is done, even though the current directory is done loading.

This is not the case with set dirpreviews false.

(and to be fair, my initial implementation has the same problem, too)

Looking at the places where loading is changed, it seems to be a kind of mutex without being sync.Mutex, intended to stop anything from reading the dir until list of files is done updating via newDir() (or manually after renames) and then sorted with the current display options.

I don't understand why this is needed at all

lf/nav.go

Line 190 in 6718a36

loading: gOpts.dirpreviews, // Directory is loaded after previewer function exits.

Ideally, loading for the current dir should have nothing to do with the preview of whatever other files/directories are inside it.

If it's a file under cursor - we don't care, because we already display its name and user can interact with it. And the preview script shouldn't prevent interaction with files. Preview is just a nice to have thing, but not at all necessary for renaming/deleting etc.

And if it's a directory - why should we treat it differently? It doesn't matter if the preview is done. You already can do anything you'd want with it, and if you care about the preview - you'll just wait for it.

Looking at blame #842 comes up. It sets the current dir to loading when running the previewer. I don't understand why because the file previewer seems to do fine without it...? And it's not like the content/sorting of the current directory should be changed.

Why doesn't it just use file previewer logic? Like, yes, in this implementation, you can't just comment out

lf/nav.go

Line 190 in 6718a36

loading: gOpts.dirpreviews, // Directory is loaded after previewer function exits.

and expect the dir previewer script to display fine. Makes sense. But why was it even implemented like that?


Other than that loading shouldn't be a problem.

I think it's lesser of two evils to return empty, because then user sees it immediately instead of "wait, I know this file should be here" some time after opening the external app - and it would just be pressing l another time to open a file. And it's rare when lf is even interactive during such loading, it's either we change directory into something slow, like a NAS, or there's a lot of files and we just renamed something. Even with

while true; do touch /tmp/lf-test/$RANDOM.blob ; done
while true; do touch /tmp/lf-test ; done

and 32k files, it's really hard trigger.

@joelim-work
Copy link
Collaborator

joelim-work commented Apr 8, 2025

@MahouShoujoMivutilde I don't use the dirpreviews feature myself, and given that it was added before I started contributing to lf, I never really bothered to look at the implementation of it in detail. That being said, after reading through the code and testing it out briefly I can't say that I'm a big fan of it from a maintenance perspective. I find it strange that a second goroutine/channel is created to separately handle directory previews when there is already one for regular previews, and it looks like the code was just copied/pasted without much thought about keeping the design clean.

As for the definition of loading. I only realized it just now after seeing your response, but it looks like it's being used to signal two different things:

  1. If the directory is currently being loaded (via the readdir function), and then stored in an internal data structure within lf.
  2. If a preview is currently being generated for the directory. By default dirpreviews is disabled, and this condition is satisfied as soon as the directory has finished loading as mentioned above. But if dirpreviews is enabled, then this depends on running the external previewer script (actually this doesn't depend on the directory being loaded at all).

The purpose of 1) is for performing some initialization on a dir object if it hasn't been loaded yet. For example, when selecting a file in a different directory that hasn't been loaded yet, a dummy entry is created with that filename so that the cursor will remain pointing to that file (as opposed to pointing to the first entry) when that directory is actually loaded and sorted:

lf/nav.go

Lines 1658 to 1660 in 6718a36

if last.loading {
last.files = append(last.files, &file{FileInfo: lstat})
}

The purpose of 2) is to simply display loading... if the preview is not ready:

lf/ui.go

Lines 371 to 376 in 6718a36

if (dir.loading && fileslen == 0) || (dirStyle.role == Preview && dir.loading && gOpts.dirpreviews) {
if dirStyle.role != Preview || previewLoading {
win.print(ui.screen, 2, 0, messageStyle, "loading...")
}
return
}

Originally before dirpreviews was added, both of these requirements were essentially the same, and could be represented by the same variable. But now this is no longer the case - it's possibly fine to just add another variable like previewLoading to track this additional requirement, but I feel like it's a band-aid fix that ends up hiding the original design problem.

Another weird thing I found with the dirpreviews implementation is that every directory gets passed to dirPreviewChan after being loaded, despite the fact that at most only one of these directories should actually be previewed. Given the following previewer script:

#!/bin/sh

printf "[%s]: previewing %s: start\n" "$(date)" "$1" >> /tmp/lf.log
sleep 1
printf "[%s]: previewing %s: end\n" "$(date)" "$1" >> /tmp/lf.log

I get the following output:

[Tue 08 Apr 2025 14:11:01 AEST]: previewing /: start
[Tue 08 Apr 2025 14:11:02 AEST]: previewing /: end
[Tue 08 Apr 2025 14:11:02 AEST]: previewing /home/joelim/lf: start
[Tue 08 Apr 2025 14:11:03 AEST]: previewing /home/joelim/lf: end
[Tue 08 Apr 2025 14:11:03 AEST]: previewing /home: start
[Tue 08 Apr 2025 14:11:04 AEST]: previewing /home: end
[Tue 08 Apr 2025 14:11:04 AEST]: previewing /home/joelim/lf/etc: start
[Tue 08 Apr 2025 14:11:05 AEST]: previewing /home/joelim/lf/etc: end
[Tue 08 Apr 2025 14:11:05 AEST]: previewing /home/joelim: start
[Tue 08 Apr 2025 14:11:06 AEST]: previewing /home/joelim: end

This presumably also means that a directory won't be marked as loaded unless all the directories before it have as well, which is counterintuitive as directories should be loaded independently of each other. Although in practice it probably doesn't matter much since the previewer script should run quickly, so the user doesn't notice anything.

EDIT: I hacked around with the existing code a bit and managed to get dirpreviews working by just leveraging the existing preview goroutine/channel.

Diff (click to expand)
diff --git a/app.go b/app.go
index 5c246bd..e6df3c2 100644
--- a/app.go
+++ b/app.go
@@ -258,7 +258,6 @@ func (app *app) writeHistory() error {
 // separate goroutines and sent here for update.
 func (app *app) loop() {
 	go app.nav.previewLoop(app.ui)
-	go app.nav.dirPreviewLoop(app.ui)
 
 	var serverChan <-chan expr
 	if !gSingleMode {
@@ -336,7 +335,6 @@ func (app *app) loop() {
 			app.quit()
 
 			app.nav.previewChan <- ""
-			app.nav.dirPreviewChan <- nil
 
 			log.Print("bye!")
 
@@ -510,7 +508,6 @@ func (app *app) loop() {
 
 func (app *app) runCmdSync(cmd *exec.Cmd, pause_after bool) {
 	app.nav.previewChan <- ""
-	app.nav.dirPreviewChan <- nil
 
 	if err := app.ui.suspend(); err != nil {
 		log.Printf("suspend: %s", err)
diff --git a/nav.go b/nav.go
index 0935ff0..d275329 100644
--- a/nav.go
+++ b/nav.go
@@ -175,7 +175,6 @@ type dir struct {
 	ignoredia   bool       // ignoredia value from last sort
 	locale      string     // locale value from last sort
 	noPerm      bool       // whether lf has no permission to open the directory
-	lines       []string   // lines of text to display if directory previews are enabled
 }
 
 func newDir(path string) *dir {
@@ -187,7 +186,6 @@ func newDir(path string) *dir {
 	}
 
 	return &dir{
-		loading:  gOpts.dirpreviews, // Directory is loaded after previewer function exits.
 		loadTime: time,
 		path:     path,
 		files:    files,
@@ -450,7 +448,6 @@ type nav struct {
 	deleteCountChan chan int
 	deleteTotalChan chan int
 	previewChan     chan string
-	dirPreviewChan  chan *dir
 	dirChan         chan *dir
 	regChan         chan *reg
 	fileChan        chan *file
@@ -480,8 +477,6 @@ type nav struct {
 }
 
 func (nav *nav) loadDirInternal(path string) *dir {
-	nav.startPreview()
-
 	d := &dir{
 		loading:     true,
 		loadTime:    time.Now(),
@@ -500,9 +495,6 @@ func (nav *nav) loadDirInternal(path string) *dir {
 		d := newDir(path)
 		d.sort()
 		d.ind, d.pos = 0, 0
-		if gOpts.dirpreviews {
-			nav.dirPreviewChan <- d
-		}
 		nav.dirChan <- d
 	}()
 	return d
@@ -541,14 +533,10 @@ func (nav *nav) checkDir(dir *dir) {
 			return
 		}
 
-		nav.startPreview()
 		dir.loading = true
 		dir.loadTime = now
 		go func() {
 			nd := newDir(dir.path)
-			if gOpts.dirpreviews {
-				nav.dirPreviewChan <- nd
-			}
 			nav.dirChan <- nd
 		}()
 	case dir.sortby != getSortBy(dir.path) ||
@@ -599,7 +587,6 @@ func newNav(height int) *nav {
 		deleteCountChan: make(chan int, 1024),
 		deleteTotalChan: make(chan int, 1024),
 		previewChan:     make(chan string, 1024),
-		dirPreviewChan:  make(chan *dir, 1024),
 		dirChan:         make(chan *dir),
 		regChan:         make(chan *reg),
 		fileChan:        make(chan *file),
@@ -718,23 +705,6 @@ func (nav *nav) exportFiles() {
 	}
 }
 
-func (nav *nav) dirPreviewLoop(ui *ui) {
-	var prevPath string
-	for dir := range nav.dirPreviewChan {
-		if dir == nil && len(gOpts.previewer) != 0 && len(gOpts.cleaner) != 0 && nav.volatilePreview {
-			cmd := exec.Command(gOpts.cleaner, prevPath)
-			if err := cmd.Run(); err != nil {
-				log.Printf("cleaning preview: %s", err)
-			}
-			nav.volatilePreview = false
-		} else if dir != nil {
-			win := ui.wins[len(ui.wins)-1]
-			nav.previewDir(dir, win)
-			prevPath = dir.path
-		}
-	}
-}
-
 func (nav *nav) previewLoop(ui *ui) {
 	var prev string
 	for path := range nav.previewChan {
@@ -783,64 +753,6 @@ func matchPattern(pattern, name, path string) bool {
 	return matched
 }
 
-func (nav *nav) previewDir(dir *dir, win *win) {
-	defer func() {
-		dir.loading = false
-		nav.dirChan <- dir
-	}()
-
-	var reader io.Reader
-
-	if len(gOpts.previewer) != 0 {
-		cmd := exec.Command(gOpts.previewer, dir.path,
-			strconv.Itoa(win.w),
-			strconv.Itoa(win.h),
-			strconv.Itoa(win.x),
-			strconv.Itoa(win.y))
-
-		out, err := cmd.StdoutPipe()
-		if err != nil {
-			log.Printf("previewing dir: %s", err)
-			return
-		}
-
-		if err := cmd.Start(); err != nil {
-			log.Printf("previewing dir: %s", err)
-			out.Close()
-			return
-		}
-
-		defer func() {
-			if err := cmd.Wait(); err != nil {
-				if e, ok := err.(*exec.ExitError); ok {
-					if e.ExitCode() != 0 {
-						nav.volatilePreview = true
-					}
-				} else {
-					log.Printf("loading dir: %s", err)
-				}
-			}
-		}()
-		defer out.Close()
-		reader = out
-		buf := bufio.NewScanner(reader)
-
-		for i := 0; i < win.h && buf.Scan(); i++ {
-			for _, r := range buf.Text() {
-				if r == 0 {
-					dir.lines = []string{"\033[7mbinary\033[0m"}
-					return
-				}
-			}
-			dir.lines = append(dir.lines, buf.Text())
-		}
-
-		if buf.Err() != nil {
-			log.Printf("loading dir: %s", buf.Err())
-		}
-	}
-}
-
 func (nav *nav) preview(path string, win *win) {
 	reg := &reg{loadTime: time.Now(), path: path}
 	defer func() { nav.regChan <- reg }()
diff --git a/ui.go b/ui.go
index eeae0cf..365d1f9 100644
--- a/ui.go
+++ b/ui.go
@@ -356,7 +356,7 @@ type dirStyle struct {
 	role   dirRole
 }
 
-func (win *win) printDir(ui *ui, dir *dir, context *dirContext, dirStyle *dirStyle, previewLoading bool) {
+func (win *win) printDir(ui *ui, dir *dir, context *dirContext, dirStyle *dirStyle) {
 	if win.w < 5 || dir == nil {
 		return
 	}
@@ -368,25 +368,11 @@ func (win *win) printDir(ui *ui, dir *dir, context *dirContext, dirStyle *dirSty
 		return
 	}
 	fileslen := len(dir.files)
-	if (dir.loading && fileslen == 0) || (dirStyle.role == Preview && dir.loading && gOpts.dirpreviews) {
-		if dirStyle.role != Preview || previewLoading {
-			win.print(ui.screen, 2, 0, messageStyle, "loading...")
-		}
+	if dir.loading && fileslen == 0 {
+		win.print(ui.screen, 2, 0, messageStyle, "loading...")
 		return
 	}
 
-	if dirStyle.role == Preview && gOpts.dirpreviews && len(gOpts.previewer) > 0 {
-		// Print previewer result instead of default directory print operation.
-		st := tcell.StyleDefault
-		for i, l := range dir.lines {
-			if i > win.h-1 {
-				break
-			}
-
-			st = win.print(ui.screen, 2, i, st, l)
-		}
-		return
-	}
 	if fileslen == 0 {
 		win.print(ui.screen, 2, 0, messageStyle, "empty")
 		return
@@ -767,10 +753,10 @@ func (ui *ui) loadFile(app *app, volatile bool) {
 		return
 	}
 
-	if curr.IsDir() {
-		ui.dirPrev = app.nav.loadDir(curr.path)
-	} else if curr.Mode().IsRegular() {
+	if curr.Mode().IsRegular() || (curr.IsDir() && gOpts.dirpreviews) {
 		ui.regPrev = app.nav.loadReg(curr.path, volatile)
+	} else if curr.IsDir() {
+		ui.dirPrev = app.nav.loadDir(curr.path)
 	}
 }
 
@@ -1052,8 +1038,7 @@ func (ui *ui) draw(nav *nav) {
 		}
 		if dir := ui.dirOfWin(nav, i); dir != nil {
 			ui.wins[i].printDir(ui, dir, &context,
-				&dirStyle{colors: ui.styles, icons: ui.icons, role: role},
-				nav.previewLoading)
+				&dirStyle{colors: ui.styles, icons: ui.icons, role: role})
 		}
 	}
 
@@ -1081,12 +1066,11 @@ func (ui *ui) draw(nav *nav) {
 		if err == nil {
 			preview := ui.wins[len(ui.wins)-1]
 
-			if curr.IsDir() {
-				preview.printDir(ui, ui.dirPrev, &context,
-					&dirStyle{colors: ui.styles, icons: ui.icons, role: Preview},
-					nav.previewLoading)
-			} else if curr.Mode().IsRegular() {
+			if curr.Mode().IsRegular() || (curr.IsDir() && gOpts.dirpreviews) {
 				preview.printReg(ui.screen, ui.regPrev, nav.previewLoading, &ui.sxScreen)
+			} else if curr.IsDir() {
+				preview.printDir(ui, ui.dirPrev, &context,
+					&dirStyle{colors: ui.styles, icons: ui.icons, role: Preview})
 			}
 		}
 	}

And it works with the following previewer script, although I only did very minimal testing:

#!/bin/sh

if [ -d "$1" ]; then
    tree "$1"
else
    nl -ba "$1"
fi

Anyway all of this discussion on dirpreviews and loading probably belongs in a separate issue, and is largely irrelevant from this PR.

So I'm still not sure whether lf -remote "query $id files" should return nothing if the directory is still loading, it's probably just a matter of weighing up the pros and cons:

Pros:

  • The user will immediately notice if the directory is still loading, since nothing is returned as opposed to receiving an outdated/partial list and finding out later

Cons:

  • Slightly more complex logic involved
  • loading has issues if dirpreviews is enabled as mentioned above
  • An empty list can be confused with an empty directory

Personally, I still lean towards returning an outdated list just because it's cleaner, and a user can always just run the query again if the result turned out be outdated. Although I agree that it's quite rare for this situation to come up in practice, since directories generally load sufficiently fast enough.

@MahouShoujoMivutilde
Copy link
Contributor Author

Okay, that's a number of strange design decisions for that one option in particular, so I agree let's not deal with it here.

And interesting find with dirpreviews running on all directories from / to the viewed, I've noticed that it takes much longer than 5 seconds on my sleep in the previous comment, but didn't measure it.

Your patch seems to work okay. The only problem I see is some symbols not clearing up if emojis are in file names. But that's a problem with regular file preview too, so not really different.

e.g.

aaaaaaaaaaaaaaa☄️aaaaaaaaaaaaa

it's not too bad anyway, here one a at the end wont clear until something else appear over it... And it seems like I can reproduce in file list, so probably tcell problem or my font's.

Ah, never mind, original dirpreviews has this problem too.

Tested via

if [ -d "$1" ]; then
    echo "It's a dir!😃"
    exa --color=always -1 "$1"
fi

#... do the file preview

What if we just return e.g. [loading]?

That way, user can distinguish empty dir with loading in scripts too. Because otherwise e.g. on-cd would get wrong list of files on loading large dir, and it would be harder to determine if it's right or not instead of just knowing.

@joelim-work
Copy link
Collaborator

I wouldn't worry about emojis, it's probably an issue with their width being reported incorrectly, possibly related to #1366.

I'm tempted to commit that patch at some time in the future anyway (if I can't find any issues from testing), the implementation is much cleaner than the current one from #842.


What if we just return e.g. [loading]?

That way, user can distinguish empty dir with loading in scripts too. Because otherwise e.g. on-cd would get wrong list of files on loading large dir, and it would be harder to determine if it's right or not instead of just knowing.

If lf -remote "query $id files" returns an identifier like [loading], it could still be interpreted as a filename. Typically the result won't be displayed and read by the user - it will be passed as input to another program, which would then treat it as a filename to open.

Regarding on-cd. I think it naturally doesn't work well with query anyway. on-cd is triggered immediately upon entering a new directory, and at this point the new directory won't have had time to load. Usually you would cd into a directory, optionally do something like applying a filter, and only then call open the files using query (e.g. by pressing a keybinding).

So after thinking about it some more, I guess it's fine to keep the current behavior you have for now. I think it's unlikely to matter either way in practice, and there isn't all that much extra complexity from a maintenance perspective. We can always revisit this later if a user asks about it.


So yeah the changes look fine I will approve it. Thanks for your contribution!

@joelim-work joelim-work merged commit 1878faa into gokcehan:master Apr 9, 2025
4 checks passed
@joelim-work joelim-work added the new Pull requests that add new behavior label Apr 9, 2025
@joelim-work joelim-work added this to the r35 milestone Apr 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new Pull requests that add new behavior
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants