lf icon indicating copy to clipboard operation
lf copied to clipboard

Git status indicator like ranger

Open anurag3301 opened this issue 7 months ago • 13 comments

Hey I really liked this git status indicators in ranger, wondering if lf has something similar implemented in it.

Image

If not then can it be implemented with some custom scripts.

anurag3301 avatar May 21 '25 07:05 anurag3301

Nope, currently there is no way to display a git column in lf. The best thing you can do is display the git status inside the prompt, as described in integrations.

There seems to be some interest in this feature though as seen in https://github.com/gokcehan/lf/issues/83, https://github.com/gokcehan/lf/issues/360 and https://github.com/gokcehan/lf/issues/843.

I might give it a try and try to implement it myself.

CatsDeservePets avatar May 24 '25 04:05 CatsDeservePets

The reason why Git status indicators is not included as a feature is because lf is intended to be a file (system) manager, and not a Git repo manager. This means that the information provided by lf largely comes from readdir (list of directory entries) and stat (information about files) system calls, which have nothing to do with Git at all.


That being said, I'm not opposed to adding some user-defined type for set info like below:

# display both size and some custom user information for each file
set info size:user

And then the user would be responsible for populating this info through configuration via a command in lf:

# <command_name> <file_name> <value>
userinfo foo.txt "\033[32mA\033[0m" # Added
userinfo bar.txt "\033[33mM\033[0m" # Modified

This is probably how the dircounts feature could have been implemented (though I don't have any plans to change this), as the stat of a directory does not relate to how many files it contains.

In terms of implementing this, there's a few things to consider off the top of my head:

  • info columns should be aligned, so the size of each column is dynamic and is equal to the length of the longest string, see the implementation for user and group: https://github.com/gokcehan/lf/blob/2494b3f632a9d735286f22b763a01fa20e81fdfa/ui.go#L400-L415
  • It should be possible to add color using escape sequences, however they should be stripped for the file currently under the cursor (like the screenshot in the OP)
  • There should be certain points in time where this has to be updated (e.g. when the file is modified). Maybe it's possible to add hook commands like on-load for when directories/files are loaded, but I haven't given this too much thought yet. Running a shell command for every file when loaded might also cause performance issues.

joelim-work avatar May 24 '25 10:05 joelim-work

I am working on it. This is my progress so far. I used https://github.com/gokcehan/lf/issues/83#issuecomment-1030013438 as an orientation.

I update the column in newDir(). This way it gets updates when entering/reentering a directory, or automatically with set watch enabled. https://github.com/CatsDeservePets/lf/blob/ed3767c67f3148cd5e1651b745010e529c968273/nav.go#L180C1-L191C3

However, I do notice a small delay when opening huge directories with many subdirs at the moment.

Right now, lf executes gOpts.infoScript which is a script that takes all file names of a directory as its parameter, iterates over them and for each file either returns a new value or an empty line. lf then stores the output inside a map which uses the file names as keys and the returned lines as values. I think that is more efficient than calling gOpts.infoScript for every single file.

I am not sure whether I should introduce a new new property for

type file struct {
    os.FileInfo
    ...
}

instead of this map. Or putting it into dirContext.

Also, escape codes inside the script currently break the highlighting, I will work on this.

CatsDeservePets avatar May 24 '25 17:05 CatsDeservePets

OK so I had a look at your implementation, I have some notes:

  • For consistency, option names are just simply all lowercase, so it should be infoscript, not infoScript.
  • If this script is called when loading a directory, I would still prefer this to be a hook command like on-load (files to be loaded can still be passed as parameters). This is more flexible as it can be used for other purposes like generating file previews (e.g. see #1537). lf -remote "send $id :<command1>; <command2>; ..." can still be used to output the actual custom info.
  • Currently the script only runs when a directory is loaded, if a file is modified (as opposed to added/deleted) then the changes won't be reflected.
  • I think it's fine to store the custom info as a map. This can either be per dir object, or global in which case it should be passed via dirContext.
  • I suspect escape codes is going to be somewhat problematic, as the info was originally intended to just use the same style as the filename. They will have to be stripped out for the file under the cursor (you can have a look at the printLength function for ideas). Furthermore printf doesn't understand escape sequences, so they will actually be counted when specifying the width. The resulting code might be a bit complex as a result.

Also what is the script you are currently using to test with?

joelim-work avatar May 26 '25 01:05 joelim-work

I introduced a function stripAnsi() based on printLength(). As of right now, escape codes get fully removed before printing the custom column. They also no longer influence the calculation of infolen. https://github.com/CatsDeservePets/lf/commit/9052fcc10e0575d6b0da37587d1bdb740eaffc78

The script I use for testing is the following:

#!/bin/sh

git rev-parse >/dev/null 2>&1 || exit 0

for file in "$@"; do
	status=$(git status --porcelain -- "$file" | cut -c1-2 | head -n1)
	if [ -n "$status" ]; then
		printf "\033[1;35m%s\033[0m\n" "$status"
	else
		printf "  \n"
	fi
done

If we want to allow escape codes to modify the design of the custom column, I think we first have to separate the different parts of info. Some approaches:

  • make fileInfo() return a slice instead of a string
  • keep returning a string but replace
fmt.Fprintf(&info, " %-*s", customWidth, d.customInfo[f.Name()])

with something like

fmt.Fprintf(&info, "\x01%-*s\x02", customWidth, d.customInfo[f.Name()])

and later extract the parts using strings.SplitN() or strings.Cut()

Then depending of whether the cursor is on the current dir:

  • concat the string and strip all escape sequences
  • concat the string but make sure to put a reset sequence + st after the custom part
    • or replace the custom part of info with spaces before printing and print the custom part separately like tag

CatsDeservePets avatar May 26 '25 14:05 CatsDeservePets

Then depending of whether the cursor is on the current dir:

  • concat the string and strip all escape sequences
  • concat the string but make sure to put a reset sequence + st after the custom part
    • or replace the custom part of info with spaces before printing and print the custom part separately like tag

I don't recall any existing functionality for converting st into an escape sequence. It's probably easier to just reserve space for the custom info field (i.e. use space characters) and then print it out separately like tag. This means that fileInfo would additionally have to return the value of the custom field and also the print offset.

Another idea could be to just print everything in their defined color, and then for handling the cursor, add a utility function (similar to win.print) which applies a (cursor) style over the defined area. But that might end up being just as complex as the options already discussed so far.

joelim-work avatar May 27 '25 05:05 joelim-work

I initially tried writing a function to convert from tcell.Style to escape sequences, but it seemed too error-prone. After experimenting with several approaches, I settled on returning the custom field and print offset in fileInfo while trying to introduce as few changes as possible. https://github.com/CatsDeservePets/lf/commit/a5974d7905312ff5e2f3223c0836e52c0637ee12

Next step is making it a command.

CatsDeservePets avatar May 29 '25 03:05 CatsDeservePets

Also, I am not fully understanding how the onLoad hook should work.

I created the hook command on-load like this (not committed yet):

func onLoad(app *app, path string) {
	log.Println("onLoad: " + path)
	if cmd, ok := gOpts.cmds["on-load"]; ok {
		cmd.eval(app, []string{path})
	}
}

It gets called inside the (app *app) loop():

case d := <-app.nav.dirChan:
	onLoad(app, d.path)

Its this to correct way so far? Passing the path so the script can iterate over the files in there (CWD or just * is not possible when iterating over sub directories)?

Inside the script, how should I modify the map?

My previews attempt was calling the script inside readCustomInfo() and iterating over the command output to fill the map.

lines := strings.Split(string(out), "\n")
infoCol := make(map[string]string, len(names))

for i, line := range lines {
	if i < len(names) {
		infoCol[names[i]] = line
	}
}

Should I expose a function like addInfo() that gets called by the script for every file like lf -remote "send $id addInfo '$file' '$info'"?

CatsDeservePets avatar May 30 '25 00:05 CatsDeservePets

The overall idea looks correct to me, you can trigger on-load for dirChan (directory is loaded), and also fileChan (file is updated). I was thinking that you could just pass the list of files as arguments to the script as you originally intended, I'm not sure why you need to iterate over subdirectories though, each dir object only store information about their immediate children.

For outputting the data, I think it's fine to create a command for this. It should be all in lowercase, so maybe call it something like addcustominfo? Then the script should be able to invoke it via lf -remote "send $id ...". It's also possible to chain multiple commands using :<command1>; <command2> syntax to avoid calling lf -remote multiple times.

joelim-work avatar May 30 '25 01:05 joelim-work

I'm not sure why you need to iterate over subdirectories though, each dir object only store information about their immediate children.

What I meant by this when the contents of a directory get shown inside the preview column and one would simply iterate over $PWD inside the script (in order to also have a custom info on those files like on the screenshot), it would not work because the $PWD would still be the other path.

It would only be a problem if we did not pass full filenames or the directory path to on-load.

CatsDeservePets avatar May 30 '25 02:05 CatsDeservePets

If multiple directories are being loaded, the on-load hook would be invoked for each one. The value of $PWD would just be the current directory lf is in (since child processes inherit this from their parent), and not the actual directory being loaded.

Up until now, I don't think there are any hook commands that provide arguments - in theory this should be possible though but I haven't actually tried.

joelim-work avatar May 30 '25 05:05 joelim-work

Ok, It mostly works now.

I removed the infoscript option in favor of the command addcustominfo. I decided on making it take an argument in the format of $LS_COLORS which makes it easy for on-load commmands to return multiple values at once. I also moved the customInfo map from the dir struct to dirContext (and therefore, also nav) because I could not find a way to modify the correct dir object from inside addcustominfo.

case "addcustominfo":
	if len(e.args) != 1 {
		app.ui.echoerr("addcustominfo: requires an argument formatted like $LS_COLORS")
		return
	}

	for _, entry := range strings.Split(e.args[0], ":") {
		if entry == "" {
			continue
		}

		pair := strings.Split(entry, "=")

		if len(pair) != 2 {
			log.Printf("invalid custominfo entry: %s", entry)
			return
		}

		k, v := pair[0], pair[1]
		if len(strings.Trim(v, " ")) == 0 {
			delete(app.nav.customInfo, k)
		} else {
			app.nav.customInfo[k] = v
		}
	}

In my lfrc I added a slightly modified version of my previous infoscript as an on-load cmd:

cmd on-load &{{
	cd "$(dirname "$1")" || exit 1
	git rev-parse >/dev/null 2>&1 || exit 0

	info=""

	for file in "$@"; do
		status=$(git status --porcelain --ignored -- "$file" | cut -c1-2 | head -n1)

		if [ -n "$status" ]; then
			 info="${info}${file}=$(printf '\033[1;35m%s\033[0m' $status):"
		else
			info="${info}${file}=:"
		fi
	done

	lf -remote "send $id addcustominfo '$info'"
}}

Some things I noticed, I would appreciate your input here:

  • There is still is a really short but noticeable delay before the custom info is shown but I don't think there is anything I can do about it. This can cause weird situations where the info is visible for a split second but then gets hidden because the custominfo makes it too long.
  • When setting nodircache, having an on-load hook configured (even an empty one) does cause lf to constantly reload the preview window. Perhaps I have to change the placement of onLoad inside the channel receivers.
  • Without watch enabled, the custom column still only updates when entering a directory as the case f := <-app.nav.fileChan: doesn't seem to trigger otherwise.
  • Having an on-load hook configured constantly causes the logs to write "listFilesInCurrDir(): [path] is still loading, files isn't ready for remote query", even if the cmd is completely is completely empty.

Here s short screencast demonstrating the loading delay and the problems with nodircache:

https://github.com/user-attachments/assets/e3116e43-f860-4a85-83a5-28f9a220f607

Here you can see all the changes I did: https://github.com/gokcehan/lf/compare/master...CatsDeservePets:lf:feat/1994-custom-info-column

Are there any other things to consider? What are thoughts on the overall implementation?

CatsDeservePets avatar May 31 '25 15:05 CatsDeservePets

Right the code looks better now, I still have the following feedback:

I prefer to keep the original syntax of addcustominfo (i.e. addcustominfo <path> <value>) to maintain consistency with other commands. Since you are still building the command string in a while loop, is there really all that much benefit to using LS_COLORS syntax? I found that it was possible to just build a chain of commands with the original syntax like so:

cmd on-load &{{
	cd "$(dirname "$1")" || exit 1
	git rev-parse >/dev/null 2>&1 || exit 0

	cmds=""

	for file in "$@"; do
		status=$(git status --porcelain --ignored -- "$file" | cut -c1-2 | head -n1)

		if [ -n "$status" ]; then
			cmds="${cmds}addcustominfo ${file} \"\033[1;35m$status\033[0m\"; "
		else
			cmds="${cmds}addcustominfo ${file} ''; "
		fi
	done

	lf -remote "send $id :$cmds"
}}

There is still is a really short but noticeable delay before the custom info is shown but I don't think there is anything I can do about it. This can cause weird situations where the info is visible for a split second but then gets hidden because the custominfo makes it too long.

I'm not really sure if there's much that can be done about the delay either. If the delay is caused by the fact that a new shell process is being launched, then that is the price of using a shell-based user configuration system. It is possible that there is also some delay from calling git status once for each file instead of passing in all the files at once, but I'm not sure if the latter approach works or not.

When setting nodircache, having an on-load hook configured (even an empty one) does cause lf to constantly reload the preview window. Perhaps I have to change the placement of onLoad inside the channel receivers.

This is caused by an infinite loop, somewhat by design. When a shell command is run, it calls ui.loadFile to reflect any new changes to the filesystem. If the directory cache is disabled, then the current file will have to be loaded since it is not cached. And if the current file is a directory, then it will trigger app.nav.dirChan, thereby completing the loop. I guess one solution is to ensure that dircache is enabled inside onLoad before actually calling the on-load command.

Going off on a tangent, I think support for nodircache should be removed - the reason it existed in the first place was because updating the directory cache was unreliable early on during development, so users were given an option to disable it altogether. You can see #1414 for more details. but that is another story, and so far I decided to just leave it in because it normally doesn't cause much harm.

  • Without watch enabled, the custom column still only updates when entering a directory as the case f := <-app.nav.fileChan: doesn't seem to trigger otherwise.

This is expected. Support for automatically picking up changes to individual files was practically non-existent until I implemented watch in #1667. It isn't enabled by default though, as I'm not sure if it causes many issues for users or not.

  • Having an on-load hook configured constantly causes the logs to write "listFilesInCurrDir(): [path] is still loading, files isn't ready for remote query", even if the cmd is completely is completely empty.

I couldn't reproduce this, but I think this might be caused by the fact that the dir object passed to the app.nav.dirChan (with loading set to true) is stored after calling on-load. I would suggest to put the onLoad call towards the very end of the handling block, just before app.ui.draw is called.

Also some other things:

  • The addcustominfo command needs to be added to complete.go so that the user can manually tab-complete it on the command line.
  • Should the customInfo map be cleared during something like reload?
  • stripAnsi is a utility function and should have some unit tests added

So in terms of my overall thoughts of this implementation, there's actually two different features that are being added here:

  • Addition of on-load hook command
  • Support for custom info fields

I think it is OK to add as the patch doesn't look too complex, but at the same time I'm not particularly excited about it either. I can see some potential use for on-load, but as for custom info fields, I think every time someone has mentioned this the use case is just for displaying Git statuses. Is it really all that useful to display the Git status for each file in a file manager? For me personally I think this is better handled inside a text editor (e.g. Vim plugins) since I view text editors to be more closely tied to repo development than file managers. I worry that one day users will start treating lf like a Git client and request features associated with it.

joelim-work avatar Jun 02 '25 05:06 joelim-work

I prefer to keep the original syntax of addcustominfo (i.e. addcustominfo <path> <value>) to maintain consistency with other commands. Since you are still building the command string in a while loop, is there really all that much benefit to using LS_COLORS syntax?

I only did it as a workaround because I was not able to find a way to execute multiple commands as a chain. Your approach works, so I changed it back. addcustominfo now takes either both <path> and <value> or just <path>. In the latter case, it treats <value> as empty and removes the entry.

I would suggest to put the onLoad call towards the very end of the handling block, just before app.ui.draw is called.

Done! Doesn’t entirely eliminate the issue, but I see it much less now.

  • The addcustominfo command needs to be added to complete.go so that the user can manually tab-complete it on the command line.

Done! Also, when only given a filename as <path>, addcustominfo assumes relative to CWD for easier completion. This is not how most commands work but I thought it made sense here. If not wanted, I can change this again.

  • Should the customInfo map be cleared during something like reload?

Sounds plausible to me but I don't want to be the one judging here. What do you think?

  • stripAnsi is a utility function and should have some unit tests added

Done! While doing that I noticed some quirks when passing strings containing broken escape sequences. Changing this also requires us updating printLength to avoid mismatches and misalignment (for me details see my code comments).

So in terms of my overall thoughts of this implementation, there's actually two different features that are being added here:

  • Addition of on-load hook command
  • Support for custom info fields

Should I create two separate pull requests for this with independent and cleaned-up git history?

Depending on your reply, the only thing that is still left is updating the help.

Again, here are my changes. https://github.com/CatsDeservePets/lf/commits/feat/1994-custom-info-column/

For me personally I think this is better handled inside a text editor (e.g. Vim plugins) since I view text editors to be more closely tied to repo development than file managers. I worry that one day users will start treating lf like a Git client and request features associated with it.

Personally, I think this generic approach is good. It does not suggest git client to me but rather Unix philosophy. We could also add other use cases for the info column to the docs.

CatsDeservePets avatar Jun 10 '25 01:06 CatsDeservePets

OK so I took a look at your branch again, most of it looks fine. I think I lean towards having separate PRs for on-load and addcustominfo since they are technically two different features that happen to be useful when used together. If there are any other use cases apart from Git markers for these features, feel free to mention them in the docs.

Once you submit the PRs I will go through them in more detail (will take some time).

To respond to your other points:

I only did it as a workaround because I was not able to find a way to execute multiple commands as a chain. Your approach works, so I changed it back. addcustominfo now takes either both <path> and <value> or just <path>. In the latter case, it treats <value> as empty and removes the entry.

I think it's fine to treat the <value> argument as an empty string if not provided.

Done! Also, when only given a filename as <path>, addcustominfo assumes relative to CWD for easier completion. This is not how most commands work but I thought it made sense here. If not wanted, I can change this again.

I think addcustominfo should allow both absolute and relative paths as you have done. The semantics and completion behavior should be similar to other commands like source and toggle.

Sounds plausible to me but I don't want to be the one judging here. What do you think?

Actually now that I think about it, the custom info probably should be stored as an additional field in the file object instead of a global map. That way it will automatically be cleared during a reload, and it is more intuitive since the fileInfo function extracts information from a file object. This does mean that addcustominfo will have to look up the directory cache for the relevant dir and then file object, but it sounds doable to me.

Done! While doing that I noticed some quirks when passing strings containing broken escape sequences. Changing this also requires us updating printLength to avoid mismatches and misalignment (for me details see my code comments).

Don't worry about this for now. It's important to keep the stripAnsi function similar to printLength, and I don't particularly want to have to make changes to printLength. In any case printLength typically only operates on input from the user (configuration settings involving escape sequences), so they should be able to fix any issues relating to broken escape sequences.

joelim-work avatar Jun 13 '25 08:06 joelim-work

is there a working script for the git status indicator? would be nice to add to git integration section in the wiki :)

horriblename avatar Jun 20 '25 23:06 horriblename

@horriblename You can find a simple example inside the docs.

Keep in mind this currently only works when compiling lf yourself from master as the features required to make this work were just merged and there isn't a new release yet. Also, in my pull request I was made aware of potential problems with the example when having the watch option enabled.

I will update the example inside the docs soon and also add it to the wiki.

CatsDeservePets avatar Jun 20 '25 23:06 CatsDeservePets

Speaking of new releases, it has been about a couple of months since r35 was released. There are already a number of upcoming changes, I think we should make a new release soon before that list piles up too much.

joelim-work avatar Jun 21 '25 00:06 joelim-work

Right the code looks better now, I still have the following feedback:

I prefer to keep the original syntax of addcustominfo (i.e. addcustominfo <path> <value>) to maintain consistency with other commands. Since you are still building the command string in a while loop, is there really all that much benefit to using LS_COLORS syntax? I found that it was possible to just build a chain of commands with the original syntax like so:

cmd on-load &{{
	cd "$(dirname "$1")" || exit 1
	git rev-parse >/dev/null 2>&1 || exit 0

	cmds=""

	for file in "$@"; do
		status=$(git status --porcelain --ignored -- "$file" | cut -c1-2 | head -n1)

		if [ -n "$status" ]; then
			cmds="${cmds}addcustominfo ${file} \"\033[1;35m$status\033[0m\"; "
		else
			cmds="${cmds}addcustominfo ${file} ''; "
		fi
	done

	lf -remote "send $id :$cmds"
}}

There is still is a really short but noticeable delay before the custom info is shown but I don't think there is anything I can do about it. This can cause weird situations where the info is visible for a split second but then gets hidden because the custominfo makes it too long.

So this command will call git status for each file, which is costly. It could be optimized to query git status once and parse the results with bash into associative array. Eg.

cmd on-load &{{
    cd "$(dirname "$1")" || exit 1
    git rev-parse >/dev/null 2>&1 || exit 0

    prefix=$(git rev-parse --show-prefix 2>/dev/null)
    declare -A statusarray

    while read -r line; do
        # Extract status and file name
        status="${line:0:2}"
        file="${line:3}"
        # Remove prefix if it exists
        file="${file#"$prefix"}"
        file="${file%%/*}" # Treat nested files as one
        # Store in associative array
        statusarray[$file]="$status"
    done < <(git status --porcelain --ignored -- .)

    cmds=""
    for file in "$@"; do
        filename="${file##*/}"
        if [[ -v statusarray[$filename] ]]; then
            cmds+="addcustominfo '$file' '\033[1;35m${statusarray[$filename]}\033[0m';"
        else
            cmds+="addcustominfo '$file' '';"
        fi
    done

    lf -remote "send $id :$cmds"
}}

I've made a simple test to compare times and outputs of solution and it's about 3 times faster on a folder with 8 files:

The new one:

real	0m0.011s
user	0m0.004s
sys	0m0.008s
send addcustominfo /home/user/r/app/rust/.cargo '';addcustominfo /home/user/r/app/rust/test.sh '??';addcustominfo /home/user/r/app/rust/target '!!';addcustominfo /home/user/r/app/rust/Cargo.lock '!!';addcustominfo /home/user/r/app/rust/src 'M ';addcustominfo /home/user/r/app/rust/README.md '';addcustominfo /home/user/r/app/rust/Makefile '';addcustominfo /home/user/r/app/rust/cliff.toml '';addcustominfo /home/user/r/app/rust/Cargo.toml.in '';addcustominfo /home/user/r/app/rust/Cargo.toml '!!';

The original one:

real	0m0.035s
user	0m0.045s
sys	0m0.011s
send addcustominfo /home/user/r/app/rust/.cargo ''; addcustominfo /home/user/r/app/rust/test.sh "??"; addcustominfo /home/user/r/app/rust/target "!!"; addcustominfo /home/user/r/app/rust/Cargo.lock "!!"; addcustominfo /home/user/r/app/rust/src "A "; addcustominfo /home/user/r/app/rust/README.md ''; addcustominfo /home/user/r/app/rust/Makefile ''; addcustominfo /home/user/r/app/rust/cliff.toml ''; addcustominfo /home/user/r/app/rust/Cargo.toml.in ''; addcustominfo /home/user/r/app/rust/Cargo.toml "!!"

Using a faster solution obviously provides to a better, snappier experience. 😎

mibli avatar Jun 22 '25 10:06 mibli

Hello, @mibli.

As someone who is not that deep into unix shell scripting, it is nice to see people coming up with more efficient solutions. Faster is always better!

Those are just my thoughts about this implementation:

  • First of all, the speed gain is really noticeable!
  • While my version runs fine just using sh, yours specifically requires bash 4+ which makes it less portable (for example, MacOS only comes with bash 3.2 preinstalled) and requires either set shell "bash" or wrapping the command inside a bash -c call with more verbose escaping. If your version gets merged, we should mention this in a sentence as it silently fails without the user noticing.
  • Your version doesn't handle the ANSI escape sequences correctly. Here is a simple fix:
cmds+="addcustominfo '$file' \"\033[1;35m${statusarray[$filename]}\033[0m\"; "
  • You might want to slightly update your version to align with the latest version of the docs as that one ignores/skips .git directories. For more information, read the comments inside the pull request (also the version in the docs no longer includes those ANSI codes, I only used them during testing).
cmd on-load &{{
	cd "$(dirname "$1")" || exit 1
	[ "$(git rev-parse --is-inside-git-dir 2>/dev/null)" = false ] || exit 0

	cmds=""
	for file in "$@"; do
		case "$file" in
			*/.git|*/.git/*) continue;;
		esac
		status=$(git status --porcelain --ignored -- "$file" | cut -c1-2 | head -n1)
		if [ -n "$status" ]; then
			cmds="${cmds}addcustominfo ${file} \"$status\"; "
		else
			cmds="${cmds}addcustominfo ${file} ''; "
		fi
	done

	lf -remote "send $id :$cmds"
}}

CatsDeservePets avatar Jun 22 '25 22:06 CatsDeservePets

Hey @CatsDeservePets

First of all, thanks for amazing feedback. To be honest I didn't take bash variations into consideration, I'm so used to these features being present on linux for so long, that one can forget about the rest of the world :D

  • While my version runs fine just using sh, yours specifically requires bash 4+ which makes it less portable (for example, MacOS only comes with bash 3.2 preinstalled) and requires either set shell "bash" or wrapping the command inside a bash -c call with more verbose escaping. If your version gets merged, we should mention this in a sentence as it silently fails without the user noticing.

I have to give it to You that if we wanted to use pure sh the original proposition couldn't be made much faster, because we still would need to make a bunch of system calls (and I've tested it).

  • Your version doesn't handle the ANSI escape sequences correctly. Here is a simple fix:

cmds+="addcustominfo '$file' "\033[1;35m${statusarray[$filename]}\033[0m"; "

Yeah this is weird because it worked for me, so I guess the double quotes really do matter! Good catch!

So Yeah I've spent a little time this morning to prepare a version that's for bash 3.2, and tested a couple of approaches (mostly using command line utils, and they were slow). The resulting mostly pure bash solution it's actually still pretty damn fast 😎. It gets close to O(n * log(n)) complexity of the original proposition, by reducing the size of the array for each match. It doesn't do any system calls aside from the necessary ones. It could be added as an option, although it is noticably more complex, it could be a good boilerplate for advanced parsing (if you have different statuses for a directory for example).

cmd on-load &{{
    cd "$(dirname "$1")" || exit 1
    [ "$(git rev-parse --is-inside-git-dir 2>/dev/null)" = false ] || exit 0

    prefix=$(git rev-parse --show-prefix 2>/dev/null)
    array=()
    lastfile=""
    while read -r line; do
        status="${line:0:2}"
        file="${line:3}"
        file="${file#"$prefix"}"
        file="${file%%/*}"
        if [ "$status" = "  " ]; then
            continue
        fi
        if [ "$file" = "$lastfile" ]; then
            continue
        fi
        array+=( "$status $file" )
    done < <(git status --porcelain --ignored -- .)

    cmds=""
    for file in "$@"; do
        case "$file" in
            */.git|*/.git/*) continue;;
        esac
        filename="${file##*/}"
        status=""
        idx=0
        for line in "${array[@]}"; do
            if [[ "${line:3}" == "$filename" ]]; then
                status="\\033[1;35m${line:0:2}\\033[0m"
                break
            fi
            idx=$((idx + 1))
        done
        if [ -n "$status" ]; then
            unset "array[$idx]"
        fi
        cmds+="addcustominfo '$file' \"$status\";"
    done

    lf -remote "send $id :$cmds"
}}

And then again, thanks for coming up with this amazing feature!

mibli avatar Jun 23 '25 08:06 mibli