Repository: gokcehan/lf
Pull request #1309 (2023-06-17 14:47:19)

Improve windows shell handling
Changes:

- Modify the `shellCommand` function in `os_windows.go` to handle quoted arguments with spaces. I got the idea from https://github.com/golang/go/issues/17149 to set the command line directly in `cmd.SysProcAttr`.
- Add a `quoteString` function which quotes strings depending if the OS is Windows or not. This now means that displaying the documentation via `<f-1>` or `:doc` now works even if the `lf` path contains spaces (see #1176).
- Refactor the `exportFiles` function so that all the main logic is done inside `nav.go`, and calls out to the above `quoteString` function to handle quoting.

---

The following commands now work, at least on my machine:

```
!dir "C:\Program Files" | %PAGER%
```

```
&lf -remote "send %id% toggle"
```

Executables with spaces in their path now also work (from #1276):

```
!"C:\Program Files\Go\bin\go.exe" version
!"C:\Program Files\Git\bin\git.exe" --version
```

`mkdir` with a quoted string now properly creates the directory in the current location instead of at `C:\` (from https://github.com/gokcehan/lf/pull/1309#issuecomment-1595856373):

```
$mkdir "foo bar"
```

---

I have also done some very brief testing with PowerShell, but I'm not so familiar with it so I would appreciate it if others helped to test it as well and make sure the changes are more stable. Hopefully we can avoid the situation where separate command-handling logic is required for CMD vs. PowerShell.

ilyagr: Thank you so much! I had a bit of experience with quoting on Windows with #1176, and it was maddening.

cc https://github.com/gokcehan/lf/discussions/1276 and @kietfsek: perhaps you could help test this out?

I haven't tried this out yet; I'd have to remember how to spin up a Windows VM to do it. Still, here are some thoughts from memory:
 
- this is likely fine, but please check that the `:doc` command still works. (it's probably OK since `%PAGER% still works). If not, you might need to revert the Windows part of #1176 (unless we come up with something better).
- OTOH, perhaps this makes `:doc` better? #1176 has a bug that if the path to `lf` contains a space, `:doc` won't work. It's possible something like this PR could fix that bug.
veltza: Sorry to intrude here. Does this fix a quoting issue that causes unexpected behavior on Windows?

For example, if you run a following command:

```
$mkdir "newdir"
```

It actually creates the new directory in root of the C drive and not the current working directory.

Whereas without quotes it properly creates a directory in the current working directory:

```
$mkdir newdir
```

This happens when I run lf with the default configuration in Powershell.

If the problem still persists, I can create a new issue about it.
kietfsek: > @kietfsek: perhaps you could help test this out?

@ilyagr: I am willing to help. However, although I am OK on technical stuff (figuring out how to download a branch and set up the build environment, I can work out them on my own), I am not familiar with GitHub interface and I have very limited experience of contributing of any form an Open Source project on GitHub. I am willing to learn in order to help the testing, but I will need guidance.
ilyagr: > I am willing to learn in order to help the testing, but I will need guidance.

Here's some guidance. Let me know if you still have questions; I'm not sure exactly what you need help with.

### Checking out the PR

There are a few different ways.

#### A. You could use the [Github Desktop](https://desktop.github.com/) tool, especially on Windows. 

#### B. You could clone Joe's repo from `https://github.com/joelim-work/lf/` and check out the `windows-shell-handling` branch.

#### C. Or you could use my favorite way, from https://gist.github.com/gnarf/5406589:

1. Add the following to your `.gitconfig`:

```
[alias]
pr  = "!f() { git fetch -fu ${2:-$(git remote |grep ^upstream || echo origin)} refs/pull/$1/head:pr/$1 && git checkout pr/$1; }; f"
```

2. Clone this repo (https://github.com/gokcehan/lf/).
3. Run `git pr 1309`. I *think* that should work even on Windows.

## Build `lf`

You'll need to install Go, see https://go.dev/doc/install. For compiling lf after that, there are instructions in the README.


    


kietfsek: @ilyagr 

I think that's good and enough information. Thanks. I will start working on it later when I have time.

joelim-work: > Sorry to intrude here. Does this fix a quoting issue that causes unexpected behavior on Windows?
> 
> For example, if you run a following command:
> 
> ```
> $mkdir "newdir"
> ```
> 
> It actually creates the new directory in root of the C drive and not the current working directory.
> 
> Whereas without quotes it properly creates a directory in the current working directory:
> 
> ```
> $mkdir newdir
> ```
> 
> This happens when I run lf with the default configuration in Powershell.
> 
> If the problem still persists, I can create a new issue about it.

@veltza I have tested `$mkdir "foo bar"` and it now creates the directory properly in the current location, and not in `C:\`. This is for the default CMD shell.

For PowerShell, I had to use single quotes (i.e. `$mkdir 'foo bar'` or `New-Item -ItemType 'directory' -Name 'foo bar'`). I don't have much experience with PowerShell, so I'm not sure how big of a problem it is - we could check whether the shell is CMD or PowerShell and handle the command differently, but I prefer not to do this if possible.
joelim-work: > Thank you so much! I had a bit of experience with quoting on Windows with #1176, and it was maddening.
> 
> cc #1276 and @kietfsek: perhaps you could help test this out?
> 
> I haven't tried this out yet; I'd have to remember how to spin up a Windows VM to do it. Still, here are some thoughts from memory:
> 
> * this is likely fine, but please check that the `:doc` command still works. (it's probably OK since `%PAGER% still works). If not, you might need to revert the Windows part of [Make the `:doc` command work even if `lf` is not in the PATH, add `lf` environment variable #1176](https://github.com/gokcehan/lf/pull/1176) (unless we come up with something better).
> * OTOH, perhaps this makes `:doc` better? [Make the `:doc` command work even if `lf` is not in the PATH, add `lf` environment variable #1176](https://github.com/gokcehan/lf/pull/1176) has a bug that if the path to `lf` contains a space, `:doc` won't work. It's possible something like this PR could fix that bug.

@ilyagr I got this to work by quoting the `lf` environment variable similar to how all the other file variables are exported, see 03f1c3b.
kietfsek: @ilyagr

I tried the PR (latest commit being 5ff3265), and the result:

* `:doc` works
* if `lf.exe` is not in `PATH`, `:doc` still doesn't work
* `map X !"c:\Program Files\WinMerge\WinMergeU.exe" d:\test.txt` works now which didn't work before the PR


joelim-work: That's strange, I found `:doc` still worked even if `lf.exe` wasn't in `PATH`. Maybe try seeing what the value of the `lf` environment variable is using `!echo %lf%`, and try printing the documentation with `!%lf% -doc`.
joelim-work: So after a bit more testing with PowerShell, I do have some slight bad news. The quoting for `cmd.SysProcAttr` is treated differently depending on whether the shell is CMD or PowerShell, so we will have to add an if statement to handle this and use the original code for PowerShell. I don't really see any other good solution - I have sort of resigned myself to the fact that Windows is just a difficult beast to deal with.
kietfsek: Update:

I realized what went wrong when testing the case that `lf.exe` is not in `PATH`, the way I tested it was incorrect -- I didn't know `cmd.exe`'s path needs to be in `PATH`. Previously I set `PATH` to empty string to do the test, and the error I saw was causing by not able to launch `cmd.exe`.

After I ruled this out, I did see `:doc` worked for the case `lf.exe` no in `PATH`.

So:

* `:doc` works
* `if lf.exe is not in PATH`, `:doc` works
* `map X !"c:\Program Files\WinMerge\WinMergeU.exe" d:\test.txt` (cc #1276) works


Patch:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index f8ae79c..7124b0f 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -8,7 +8,7 @@ on:
 
 jobs:
 
-  build:
+  tests:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
@@ -34,6 +34,21 @@ jobs:
         version: "2022.1.3"
         install-go: false
 
+  cross-compile:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+      with:
+        fetch-depth: 1
+
+    - name: Set up Go
+      uses: actions/setup-go@v4
+      with:
+        go-version: 1.18
+
+    - name: Cross-compile on all supported platforms
+      run: gen/xbuild.sh
+
   generate:
     runs-on: ubuntu-latest
     steps:
diff --git a/complete.go b/complete.go
index cae5082..f891071 100644
--- a/complete.go
+++ b/complete.go
@@ -218,6 +218,7 @@ var (
 		"infotimefmtnew",
 		"infotimefmtold",
 		"truncatechar",
+		"truncatepct",
 	}
 )
 
@@ -282,7 +283,6 @@ func matchExec(s string) (matches []string, longest []rune) {
 				continue
 			}
 
-			log.Print(finfo.Name())
 			words = append(words, finfo.Name())
 		}
 	}
diff --git a/doc.go b/doc.go
index 806b6aa..8b84bb7 100644
--- a/doc.go
+++ b/doc.go
@@ -172,6 +172,7 @@ The following options can be used to customize the behavior of lf:
 	tempmarks        string    (default '')
 	timefmt          string    (default 'Mon Jan _2 15:04:05 2006')
 	truncatechar     string    (default '~')
+	truncatepct      int       (default 100)
 	waitmsg          string    (default 'Press any key to continue')
 	wrapscan         bool      (default true)
 	wrapscroll       bool      (default false)
@@ -287,16 +288,24 @@ History file should be located at:
 	Unix     ~/.local/share/lf/history
 	Windows  C:\Users\<user>\AppData\Local\lf\history
 
-You can configure the default values of following variables to change these locations:
+You can configure these locations with the following variables given with their order of precedences and their default values:
 
 	Unix
-	    $XDG_CONFIG_HOME  ~/.config
-	    $XDG_DATA_HOME    ~/.local/share
+	    $LF_CONFIG_HOME
+	    $XDG_CONFIG_HOME
+	    ~/.config
+
+	    $LF_DATA_HOME
+	    $XDG_DATA_HOME
+	    ~/.local/share
 
 	Windows
-	    %ProgramData%     C:\ProgramData
-	    %LOCALAPPDATA%    C:\Users\<user>\AppData\Local
-	    %LF_CONFIG_HOME%  If set, use this value instead of %LOCALAPPDATA%
+	    %ProgramData%
+	    C:\ProgramData
+
+	    %LF_CONFIG_HOME%
+	    %LOCALAPPDATA%
+	    C:\Users\<user>\AppData\Local
 
 A sample configuration file can be found at
 https://github.com/gokcehan/lf/blob/master/etc/lfrc.example
@@ -941,6 +950,23 @@ Format string of the file modification time shown in the bottom line.
 
 Truncate character shown at the end when the file name does not fit to the pane.
 
+	truncatepct  int       (default 100)
+
+When a filename is too long to be shown completely, the available space is
+partitioned in two pieces. truncatepct defines a fraction (in percent
+between 0 and 100) for the size of the first piece, which will show the
+beginning of the filename. The second piece will show the end of the filename
+and will use the rest of the available space. Both pieces are separated by the
+truncation character (truncatechar).
+A value of 100 will only show the beginning of the filename,
+while a value of 0 will only show the end of the filename, e.g.:
+
+- `set truncatepct 100` -> "very-long-filename-tr~" (default)
+
+- `set truncatepct 50`  -> "very-long-f~-truncated"
+
+- `set truncatepct 0`   -> "~ng-filename-truncated"
+
 	waitmsg        string    (default 'Press any key to continue')
 
 String shown after commands of shell-wait type.
diff --git a/docstring.go b/docstring.go
index 61692de..6fb2393 100644
--- a/docstring.go
+++ b/docstring.go
@@ -175,6 +175,7 @@ The following options can be used to customize the behavior of lf:
     tempmarks        string    (default '')
     timefmt          string    (default 'Mon Jan _2 15:04:05 2006')
     truncatechar     string    (default '~')
+    truncatepct      int       (default 100)
     waitmsg          string    (default 'Press any key to continue')
     wrapscan         bool      (default true)
     wrapscroll       bool      (default false)
@@ -292,17 +293,25 @@ History file should be located at:
     Unix     ~/.local/share/lf/history
     Windows  C:\Users\<user>\AppData\Local\lf\history
 
-You can configure the default values of following variables to change these
-locations:
+You can configure these locations with the following variables given with their
+order of precedences and their default values:
 
     Unix
-        $XDG_CONFIG_HOME  ~/.config
-        $XDG_DATA_HOME    ~/.local/share
+        $LF_CONFIG_HOME
+        $XDG_CONFIG_HOME
+        ~/.config
+
+        $LF_DATA_HOME
+        $XDG_DATA_HOME
+        ~/.local/share
 
     Windows
-        %ProgramData%     C:\ProgramData
-        %LOCALAPPDATA%    C:\Users\<user>\AppData\Local
-        %LF_CONFIG_HOME%  If set, use this value instead of %LOCALAPPDATA%
+        %ProgramData%
+        C:\ProgramData
+
+        %LF_CONFIG_HOME%
+        %LOCALAPPDATA%
+        C:\Users\<user>\AppData\Local
 
 A sample configuration file can be found at
 https://github.com/gokcehan/lf/blob/master/etc/lfrc.example
@@ -1013,6 +1022,22 @@ Format string of the file modification time shown in the bottom line.
 
 Truncate character shown at the end when the file name does not fit to the pane.
 
+    truncatepct  int       (default 100)
+
+When a filename is too long to be shown completely, the available space is
+partitioned in two pieces. truncatepct defines a fraction (in percent between
+0 and 100) for the size of the first piece, which will show the beginning of
+the filename. The second piece will show the end of the filename and will use
+the rest of the available space. Both pieces are separated by the truncation
+character (truncatechar). A value of 100 will only show the beginning of the
+filename, while a value of 0 will only show the end of the filename, e.g.:
+
+- 'set truncatepct 100' -> "very-long-filename-tr~" (default)
+
+- 'set truncatepct 50' -> "very-long-f~-truncated"
+
+- 'set truncatepct 0' -> "~ng-filename-truncated"
+
     waitmsg        string    (default 'Press any key to continue')
 
 String shown after commands of shell-wait type.
diff --git a/eval.go b/eval.go
index 24975c3..7d46c18 100644
--- a/eval.go
+++ b/eval.go
@@ -831,6 +831,17 @@ func (e *setExpr) eval(app *app, args []string) {
 		}
 
 		gOpts.truncatechar = e.val
+	case "truncatepct":
+		n, err := strconv.Atoi(e.val)
+		if err != nil {
+			app.ui.echoerrf("truncatepct: %s", err)
+			return
+		}
+		if n < 0 || n > 100 {
+			app.ui.echoerrf("truncatepct: must be between 0 and 100 (both inclusive), got %d", n)
+			return
+		}
+		gOpts.truncatepct = n
 	case "waitmsg":
 		gOpts.waitmsg = e.val
 	case "wrapscan":
@@ -2142,7 +2153,7 @@ func (e *callExpr) eval(app *app, args []string) {
 		app.menuCompActive = false
 	case "cmd-enter":
 		s := string(append(app.ui.cmdAccLeft, app.ui.cmdAccRight...))
-		if len(s) == 0 && app.ui.cmdPrefix != "filter: " {
+		if len(s) == 0 && app.ui.cmdPrefix != "filter: " && app.ui.cmdPrefix != ">" {
 			return
 		}
 
diff --git a/gen/xbuild.sh b/gen/xbuild.sh
index af8c814..0dc423f 100755
--- a/gen/xbuild.sh
+++ b/gen/xbuild.sh
@@ -8,51 +8,85 @@
 # compressed in an archive form (`.zip` for windows and `.tar.gz` for the rest)
 # within a folder named `dist`.
 
-set -o verbose
+[ -z "$version" ] && version=$(git describe --tags)
 
-[ -z $version ] && version=$(git describe --tags)
+test -d dist && {
+    echo "gen/xbuild.sh: WARNING: removing preexisting subdir 'dist'"
+    rm -r dist || exit 5
+}
+mkdir dist || exit 5
 
-mkdir -p dist
+ERRORS=
 
-# https://golang.org/doc/install/source#environment
+build() {
+    # https://golang.org/doc/install/source#environment
+    echo "=== Building for GOOS=$1 and GOARCH=$2."
+    CGO_ENABLED=0 GOOS="$1" GOARCH="$2" go build -o dist/ \
+        -ldflags="-s -w -X main.gVersion=$version"
+    if test "$?" != "0"; then
+        ERRORS=1
+    else
+        package "$1" "$2" || exit 5
+    fi
+}
 
-CGO_ENABLED=0 GOOS=android   GOARCH=arm64    go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-android-arm64.tar.gz   lf --remove-files
-CGO_ENABLED=0 GOOS=darwin    GOARCH=amd64    go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-darwin-amd64.tar.gz    lf --remove-files
-CGO_ENABLED=0 GOOS=dragonfly GOARCH=amd64    go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-dragonfly-amd64.tar.gz lf --remove-files
-CGO_ENABLED=0 GOOS=freebsd   GOARCH=386      go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-freebsd-386.tar.gz     lf --remove-files
-CGO_ENABLED=0 GOOS=freebsd   GOARCH=amd64    go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-freebsd-amd64.tar.gz   lf --remove-files
-CGO_ENABLED=0 GOOS=freebsd   GOARCH=arm      go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-freebsd-arm.tar.gz     lf --remove-files
-CGO_ENABLED=0 GOOS=illumos   GOARCH=amd64    go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-illumos-amd64.tar.gz   lf --remove-files
-CGO_ENABLED=0 GOOS=linux     GOARCH=386      go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-linux-386.tar.gz       lf --remove-files
-CGO_ENABLED=0 GOOS=linux     GOARCH=amd64    go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-linux-amd64.tar.gz     lf --remove-files
-CGO_ENABLED=0 GOOS=linux     GOARCH=arm      go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-linux-arm.tar.gz       lf --remove-files
-CGO_ENABLED=0 GOOS=linux     GOARCH=arm64    go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-linux-arm64.tar.gz     lf --remove-files
-CGO_ENABLED=0 GOOS=linux     GOARCH=ppc64    go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-linux-ppc64.tar.gz     lf --remove-files
-CGO_ENABLED=0 GOOS=linux     GOARCH=ppc64le  go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-linux-ppc64le.tar.gz   lf --remove-files
-CGO_ENABLED=0 GOOS=linux     GOARCH=mips     go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-linux-mips.tar.gz      lf --remove-files
-CGO_ENABLED=0 GOOS=linux     GOARCH=mipsle   go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-linux-mipsle.tar.gz    lf --remove-files
-CGO_ENABLED=0 GOOS=linux     GOARCH=mips64   go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-linux-mips64.tar.gz    lf --remove-files
-CGO_ENABLED=0 GOOS=linux     GOARCH=mips64le go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-linux-mips64le.tar.gz  lf --remove-files
-CGO_ENABLED=0 GOOS=linux     GOARCH=s390x    go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-linux-s390x.tar.gz     lf --remove-files
-CGO_ENABLED=0 GOOS=netbsd    GOARCH=386      go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-netbsd-386.tar.gz      lf --remove-files
-CGO_ENABLED=0 GOOS=netbsd    GOARCH=amd64    go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-netbsd-amd64.tar.gz    lf --remove-files
-CGO_ENABLED=0 GOOS=netbsd    GOARCH=arm      go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-netbsd-arm.tar.gz      lf --remove-files
-CGO_ENABLED=0 GOOS=openbsd   GOARCH=386      go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-openbsd-386.tar.gz     lf --remove-files
-CGO_ENABLED=0 GOOS=openbsd   GOARCH=amd64    go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-openbsd-amd64.tar.gz   lf --remove-files
-CGO_ENABLED=0 GOOS=openbsd   GOARCH=arm      go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-openbsd-arm.tar.gz     lf --remove-files
-CGO_ENABLED=0 GOOS=openbsd   GOARCH=arm64    go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-openbsd-arm64.tar.gz   lf --remove-files
-CGO_ENABLED=0 GOOS=solaris   GOARCH=amd64    go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-solaris-amd64.tar.gz   lf --remove-files
+package() (
+    cd dist || return 1 
+    # Since the function is surrounded by (), the cd only affects a subshell
+    OUTFILE=
+    case "$1" in
+    windows)
+        OUTFILE="lf-$1-$2.zip"
+        zip "$OUTFILE" lf.exe --move || return 1
+        ;;
+    *)
+        OUTFILE="lf-$1-$2.tar.gz"
+        tar czf "$OUTFILE" lf --remove-files || return 1
+        ;;
+    esac
+    echo "dist/$OUTFILE successfully created."
+)
 
-CGO_ENABLED=0 GOOS=windows   GOARCH=386      go build -ldflags="-s -w -X main.gVersion=$version" && zip dist/lf-windows-386.zip            lf.exe --move
-CGO_ENABLED=0 GOOS=windows   GOARCH=amd64    go build -ldflags="-s -w -X main.gVersion=$version" && zip dist/lf-windows-amd64.zip          lf.exe --move
+build android arm64
+build darwin amd64
+build darwin arm64
+build dragonfly amd64
+build freebsd 386
+build freebsd amd64
+build freebsd arm
+build illumos amd64
+build linux 386
+build linux amd64
+build linux arm
+build linux arm64
+build linux ppc64
+build linux ppc64le
+build linux mips
+build linux mipsle
+build linux mips64
+build linux mips64le
+build linux s390x
+build netbsd 386
+build netbsd amd64
+build netbsd arm
+build openbsd 386
+build openbsd amd64
+build openbsd arm
+build openbsd arm64
+build solaris amd64
+build windows 386
+build windows amd64
+# Unsupported
+# build aix ppc64
+# build android 386
+# build android amd64
+# build android arm
+# build js wasm
+# build plan9 386
+# build plan9 amd64
+# build plan9 arm
 
-# not supported
-# CGO_ENABLED=0 GOOS=aix       GOARCH=ppc64    go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-aix-ppc64.tar.gz       lf --remove-files
-# CGO_ENABLED=0 GOOS=android   GOARCH=386      go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-android-386.tar.gz     lf --remove-files
-# CGO_ENABLED=0 GOOS=android   GOARCH=amd64    go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-android-amd64.tar.gz   lf --remove-files
-# CGO_ENABLED=0 GOOS=android   GOARCH=arm      go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-android-arm.tar.gz     lf --remove-files
-# CGO_ENABLED=0 GOOS=darwin    GOARCH=arm64    go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-darwin-arm64.tar.gz    lf --remove-files
-# CGO_ENABLED=0 GOOS=js        GOARCH=wasm     go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-js-wasm.tar.gz         lf --remove-files
-# CGO_ENABLED=0 GOOS=plan9     GOARCH=386      go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-plan9-386.tar.gz       lf --remove-files
-# CGO_ENABLED=0 GOOS=plan9     GOARCH=amd64    go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-plan9-amd64.tar.gz     lf --remove-files
-# CGO_ENABLED=0 GOOS=plan9     GOARCH=arm      go build -ldflags="-s -w -X main.gVersion=$version" && tar czf dist/lf-plan9-arm.tar.gz       lf --remove-files
+if test -n "$ERRORS"; then
+    printf "\ngen/xbuild.sh: some targets failed to compile.\n"
+    exit 1
+fi
diff --git a/go.mod b/go.mod
index 9e31c3a..a2298d5 100644
--- a/go.mod
+++ b/go.mod
@@ -6,8 +6,8 @@ require (
 	github.com/djherbis/times v1.5.0
 	github.com/gdamore/tcell/v2 v2.6.0
 	github.com/mattn/go-runewidth v0.0.14
-	golang.org/x/sys v0.8.0
-	golang.org/x/term v0.8.0
+	golang.org/x/sys v0.9.0
+	golang.org/x/term v0.9.0
 )
 
 require (
diff --git a/go.sum b/go.sum
index 96e8bb1..8f6e8c5 100644
--- a/go.sum
+++ b/go.sum
@@ -26,13 +26,13 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
+golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
-golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
+golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
diff --git a/lf.1 b/lf.1
index 561edc5..d821aac 100644
--- a/lf.1
+++ b/lf.1
@@ -191,6 +191,7 @@ The following options can be used to customize the behavior of lf:
     tempmarks        string    (default '')
     timefmt          string    (default 'Mon Jan _2 15:04:05 2006')
     truncatechar     string    (default '~')
+    truncatepct      int       (default 100)
     waitmsg          string    (default 'Press any key to continue')
     wrapscan         bool      (default true)
     wrapscroll       bool      (default false)
@@ -333,19 +334,31 @@ History file should be located at:
     Windows  C:\eUsers\e<user>\eAppData\eLocal\elf\ehistory
 .EE
 .PP
-You can configure the default values of following variables to change these locations:
+You can configure these locations with the following variables given with their order of precedences and their default values:
 .PP
 .EX
     Unix
-        $XDG_CONFIG_HOME  ~/.config
-        $XDG_DATA_HOME    ~/.local/share
+        $LF_CONFIG_HOME
+        $XDG_CONFIG_HOME
+        ~/.config
+.EE
+.PP
+.EX
+        $LF_DATA_HOME
+        $XDG_DATA_HOME
+        ~/.local/share
 .EE
 .PP
 .EX
     Windows
-        %ProgramData%     C:\eProgramData
-        %LOCALAPPDATA%    C:\eUsers\e<user>\eAppData\eLocal
-        %LF_CONFIG_HOME%  If set, use this value instead of %LOCALAPPDATA%
+        %ProgramData%
+        C:\eProgramData
+.EE
+.PP
+.EX
+        %LF_CONFIG_HOME%
+        %LOCALAPPDATA%
+        C:\eUsers\e<user>\eAppData\eLocal
 .EE
 .PP
 A sample configuration file can be found at https://github.com/gokcehan/lf/blob/master/etc/lfrc.example
@@ -1119,6 +1132,18 @@ Format string of the file modification time shown in the bottom line.
 .PP
 Truncate character shown at the end when the file name does not fit to the pane.
 .PP
+.EX
+    truncatepct  int       (default 100)
+.EE
+.PP
+When a filename is too long to be shown completely, the available space is partitioned in two pieces. truncatepct defines a fraction (in percent between 0 and 100) for the size of the first piece, which will show the beginning of the filename. The second piece will show the end of the filename and will use the rest of the available space. Both pieces are separated by the truncation character (truncatechar). A value of 100 will only show the beginning of the filename, while a value of 0 will only show the end of the filename, e.g.:
+.PP
+- `set truncatepct 100` -> "very-long-filename-tr~" (default)
+.PP
+- `set truncatepct 50` -> "very-long-f~-truncated"
+.PP
+- `set truncatepct 0` -> "~ng-filename-truncated"
+.PP
 .EX
     waitmsg        string    (default 'Press any key to continue')
 .EE
diff --git a/main.go b/main.go
index fa267e8..22667c9 100644
--- a/main.go
+++ b/main.go
@@ -85,7 +85,7 @@ func exportEnvVars() {
 		log.Printf("getting path to lf binary: %s", err)
 		lfPath = "lf"
 	}
-	os.Setenv("lf", lfPath)
+	os.Setenv("lf", quoteString(lfPath))
 }
 
 // used by exportOpts below
diff --git a/misc.go b/misc.go
index b0303ea..8199697 100644
--- a/misc.go
+++ b/misc.go
@@ -53,6 +53,19 @@ func runeSliceWidthRange(rs []rune, beg, end int) []rune {
 	return rs[b:]
 }
 
+// Returns the last runes of `rs` that take up at most `maxWidth` space.
+func runeSliceWidthLastRange(rs []rune, maxWidth int) []rune {
+	lastWidth := 0
+	for i := len(rs) - 1; i >= 0; i-- {
+		w := runewidth.RuneWidth(rs[i])
+		if lastWidth+w > maxWidth {
+			return rs[i+1:]
+		}
+		lastWidth += w
+	}
+	return rs
+}
+
 // This function is used to escape whitespaces and special characters with
 // backlashes in a given string.
 func escape(s string) string {
diff --git a/nav.go b/nav.go
index d491237..9322596 100644
--- a/nav.go
+++ b/nav.go
@@ -655,12 +655,24 @@ func (nav *nav) exportFiles() {
 
 	var currFile string
 	if curr, err := nav.currFile(); err == nil {
-		currFile = curr.path
+		currFile = quoteString(curr.path)
 	}
 
-	currSelections := nav.currSelections()
+	var selections []string
+	for _, selection := range nav.currSelections() {
+		selections = append(selections, quoteString(selection))
+	}
+	currSelections := strings.Join(selections, gOpts.filesep)
+
+	os.Setenv("f", currFile)
+	os.Setenv("fs", currSelections)
+	os.Setenv("PWD", quoteString(nav.currDir().path))
 
-	exportFiles(currFile, currSelections, nav.currDir().path)
+	if len(selections) == 0 {
+		os.Setenv("fx", currFile)
+	} else {
+		os.Setenv("fx", currSelections)
+	}
 }
 
 func (nav *nav) dirPreviewLoop(ui *ui) {
diff --git a/opts.go b/opts.go
index 21d624c..5b8feaa 100644
--- a/opts.go
+++ b/opts.go
@@ -72,6 +72,7 @@ var gOpts struct {
 	infotimefmtnew   string
 	infotimefmtold   string
 	truncatechar     string
+	truncatepct      int
 	ratios           []int
 	hiddenfiles      []string
 	history          bool
@@ -133,6 +134,7 @@ func init() {
 	gOpts.infotimefmtnew = "Jan _2 15:04"
 	gOpts.infotimefmtold = "Jan _2  2006"
 	gOpts.truncatechar = "~"
+	gOpts.truncatepct = 100
 	gOpts.ratios = []int{1, 2, 3}
 	gOpts.hiddenfiles = []string{".*"}
 	gOpts.history = true
diff --git a/os.go b/os.go
index 46ee694..51385ab 100644
--- a/os.go
+++ b/os.go
@@ -84,7 +84,10 @@ func init() {
 	}
 	gUser = u
 
-	config := os.Getenv("XDG_CONFIG_HOME")
+	config := os.Getenv("LF_CONFIG_HOME")
+	if config == "" {
+		config = os.Getenv("XDG_CONFIG_HOME")
+	}
 	if config == "" {
 		config = filepath.Join(gUser.HomeDir, ".config")
 	}
@@ -104,7 +107,10 @@ func init() {
 		filepath.Join(config, "lf", "icons"),
 	}
 
-	data := os.Getenv("XDG_DATA_HOME")
+	data := os.Getenv("LF_DATA_HOME")
+	if data == "" {
+		data = os.Getenv("XDG_DATA_HOME")
+	}
 	if data == "" {
 		data = filepath.Join(gUser.HomeDir, ".local", "share")
 	}
@@ -218,17 +224,6 @@ func errCrossDevice(err error) bool {
 	return err.(*os.LinkError).Err.(unix.Errno) == unix.EXDEV
 }
 
-func exportFiles(f string, fs []string, pwd string) {
-	envFile := f
-	envFiles := strings.Join(fs, gOpts.filesep)
-
-	os.Setenv("f", envFile)
-	os.Setenv("fs", envFiles)
-	os.Setenv("PWD", pwd)
-
-	if len(fs) == 0 {
-		os.Setenv("fx", envFile)
-	} else {
-		os.Setenv("fx", envFiles)
-	}
+func quoteString(s string) string {
+	return s
 }
diff --git a/os_windows.go b/os_windows.go
index ff9ac4b..ea8bd0a 100644
--- a/os_windows.go
+++ b/os_windows.go
@@ -101,10 +101,23 @@ func detachedCommand(name string, arg ...string) *exec.Cmd {
 }
 
 func shellCommand(s string, args []string) *exec.Cmd {
-	args = append([]string{gOpts.shellflag, s}, args...)
+	// Windows CMD requires special handling to deal with quoted arguments
+	if strings.ToLower(gOpts.shell) == "cmd" {
+		var builder strings.Builder
+		builder.WriteString(s)
+		for _, arg := range args {
+			fmt.Fprintf(&builder, ` "%s"`, arg)
+		}
+		shellOpts := strings.Join(gOpts.shellopts, " ")
+		cmdline := fmt.Sprintf(`%s %s %s "%s"`, gOpts.shell, shellOpts, gOpts.shellflag, builder.String())
 
-	args = append(gOpts.shellopts, args...)
+		cmd := exec.Command(gOpts.shell)
+		cmd.SysProcAttr = &windows.SysProcAttr{CmdLine: cmdline}
+		return cmd
+	}
 
+	args = append([]string{gOpts.shellflag, s}, args...)
+	args = append(gOpts.shellopts, args...)
 	return exec.Command(gOpts.shell, args...)
 }
 
@@ -168,24 +181,6 @@ func errCrossDevice(err error) bool {
 	return err.(*os.LinkError).Err.(windows.Errno) == 17
 }
 
-func exportFiles(f string, fs []string, pwd string) {
-	envFile := fmt.Sprintf(`"%s"`, f)
-
-	var quotedFiles []string
-	for _, f := range fs {
-		quotedFiles = append(quotedFiles, fmt.Sprintf(`"%s"`, f))
-	}
-	envFiles := strings.Join(quotedFiles, gOpts.filesep)
-
-	envPWD := fmt.Sprintf(`"%s"`, pwd)
-
-	os.Setenv("f", envFile)
-	os.Setenv("fs", envFiles)
-	os.Setenv("PWD", envPWD)
-
-	if len(fs) == 0 {
-		os.Setenv("fx", envFile)
-	} else {
-		os.Setenv("fx", envFiles)
-	}
+func quoteString(s string) string {
+	return fmt.Sprintf(`"%s"`, s)
 }
diff --git a/ui.go b/ui.go
index 635a428..4b8cb55 100644
--- a/ui.go
+++ b/ui.go
@@ -468,8 +468,11 @@ func (win *win) printDir(screen tcell.Screen, dir *dir, context *dirContext, dir
 
 		filename := []rune(f.Name())
 		if runeSliceWidth(filename) > maxFilenameWidth {
-			filename = runeSliceWidthRange(filename, 0, maxFilenameWidth-1)
+			truncatePos := (maxFilenameWidth - 1) * gOpts.truncatepct / 100
+			lastPart := runeSliceWidthLastRange(filename, maxFilenameWidth-truncatePos-1)
+			filename = runeSliceWidthRange(filename, 0, truncatePos)
 			filename = append(filename, []rune(gOpts.truncatechar)...)
+			filename = append(filename, lastPart...)
 		}
 		for i := runeSliceWidth(filename); i < maxFilenameWidth; i++ {
 			filename = append(filename, ' ')


Test patch:
diff --git a/misc_test.go b/misc_test.go
index aa6f6df..758e342 100644
--- a/misc_test.go
+++ b/misc_test.go
@@ -83,6 +83,47 @@ func TestRuneSliceWidthRange(t *testing.T) {
 	}
 }
 
+func TestRuneSliceWidthLastRange(t *testing.T) {
+	tests := []struct {
+		rs       []rune
+		maxWidth int
+		exp      []rune
+	}{
+		{[]rune{}, 0, []rune{}},
+		{[]rune{}, 1, []rune{}},
+		{[]rune{'a', 'ı', 'b', ' '}, 0, []rune{}},
+		{[]rune{'a', 'ı', 'b', ' '}, 1, []rune{' '}},
+		{[]rune{'a', 'ı', 'b', ' '}, 2, []rune{'b', ' '}},
+		{[]rune{'a', 'ı', 'b', ' '}, 3, []rune{'ı', 'b', ' '}},
+		{[]rune{'a', 'ı', 'b', ' '}, 4, []rune{'a', 'ı', 'b', ' '}},
+		{[]rune{'a', 'ı', 'b', ' '}, 5, []rune{'a', 'ı', 'b', ' '}},
+		{[]rune{'世', '界', '世', '界'}, 0, []rune{}},
+		{[]rune{'世', '界', '世', '界'}, 1, []rune{}},
+		{[]rune{'世', '界', '世', '界'}, 2, []rune{'界'}},
+		{[]rune{'世', '界', '世', '界'}, 3, []rune{'界'}},
+		{[]rune{'世', '界', '世', '界'}, 4, []rune{'世', '界'}},
+		{[]rune{'世', '界', '世', '界'}, 5, []rune{'世', '界'}},
+		{[]rune{'世', '界', '世', '界'}, 6, []rune{'界', '世', '界'}},
+		{[]rune{'世', '界', '世', '界'}, 7, []rune{'界', '世', '界'}},
+		{[]rune{'世', '界', '世', '界'}, 8, []rune{'世', '界', '世', '界'}},
+		{[]rune{'世', '界', '世', '界'}, 9, []rune{'世', '界', '世', '界'}},
+		{[]rune{'世', 'a', '界', 'ı'}, 0, []rune{}},
+		{[]rune{'世', 'a', '界', 'ı'}, 1, []rune{'ı'}},
+		{[]rune{'世', 'a', '界', 'ı'}, 2, []rune{'ı'}},
+		{[]rune{'世', 'a', '界', 'ı'}, 3, []rune{'界', 'ı'}},
+		{[]rune{'世', 'a', '界', 'ı'}, 4, []rune{'a', '界', 'ı'}},
+		{[]rune{'世', 'a', '界', 'ı'}, 5, []rune{'a', '界', 'ı'}},
+		{[]rune{'世', 'a', '界', 'ı'}, 6, []rune{'世', 'a', '界', 'ı'}},
+		{[]rune{'世', 'a', '界', 'ı'}, 7, []rune{'世', 'a', '界', 'ı'}},
+	}
+
+	for _, test := range tests {
+		if got := runeSliceWidthLastRange(test.rs, test.maxWidth); !reflect.DeepEqual(got, test.exp) {
+			t.Errorf("at input '%v' expected '%v' but got '%v'", test.rs, test.exp, got)
+		}
+	}
+}
+
 func TestEscape(t *testing.T) {
 	tests := []struct {
 		s   string