Repository: gokcehan/lf (go)

Issue:
on-quit command is not run when terminal is closed
Due to this, some cleanup commands are not run.

I tried moving these lines
https://github.com/gokcehan/lf/blob/36a7a18316580864f2f2ebd79e9ddae4858f6b60/app.go#L314-L316

into app.quit() method (because it is called on SIGHUP, SIGTERM and other signals) but it did not work to some reason.

Pull request description:
Run `on-quit` when terminal closes
- Fixes #847

Move `on-quit` hook command to inside `app.quit` so that it is executed even when terminated via a signel (e.g. terminal is closed). Based on https://github.com/gokcehan/lf/issues/847#issuecomment-1158119232.

Patch:
diff --git a/app.go b/app.go
index f48bfce..2f1fec9 100644
--- a/app.go
+++ b/app.go
@@ -64,6 +64,8 @@ func newApp(ui *ui, nav *nav) *app {
 }
 
 func (app *app) quit() {
+	onQuit(app)
+
 	if gOpts.history {
 		if err := app.writeHistory(); err != nil {
 			log.Printf("writing history file: %s", err)
@@ -320,10 +322,6 @@ func (app *app) loop() {
 				continue
 			}
 
-			if cmd, ok := gOpts.cmds["on-quit"]; ok {
-				cmd.eval(app, nil)
-			}
-
 			app.quit()
 
 			app.nav.previewChan <- ""
diff --git a/client.go b/client.go
index db60df4..559102d 100644
--- a/client.go
+++ b/client.go
@@ -36,7 +36,6 @@ func run() {
 	if gOpts.mouse {
 		screen.EnableMouse()
 	}
-	screen.EnableFocus()
 
 	if gLogPath != "" {
 		f, err := os.OpenFile(gLogPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
diff --git a/colors.go b/colors.go
index 9520480..8558b3f 100644
--- a/colors.go
+++ b/colors.go
@@ -10,10 +10,16 @@ import (
 	"github.com/gdamore/tcell/v2"
 )
 
-type styleMap map[string]tcell.Style
+type styleMap struct {
+	styles        map[string]tcell.Style
+	useLinkTarget bool
+}
 
 func parseStyles() styleMap {
-	sm := make(styleMap)
+	sm := styleMap{
+		styles:        make(map[string]tcell.Style),
+		useLinkTarget: false,
+	}
 
 	// Default values from dircolors
 	//
@@ -191,20 +197,12 @@ func (sm styleMap) parseFile(path string) {
 	}
 
 	for _, pair := range pairs {
-		key, val := pair[0], pair[1]
-
-		key = replaceTilde(key)
-
-		if filepath.IsAbs(key) {
-			key = filepath.Clean(key)
-		}
-
-		sm[key] = applyAnsiCodes(val, tcell.StyleDefault)
+		sm.parsePair(pair)
 	}
 }
 
 // This function parses $LS_COLORS environment variable.
-func (sm styleMap) parseGNU(env string) {
+func (sm *styleMap) parseGNU(env string) {
 	for _, entry := range strings.Split(env, ":") {
 		if entry == "" {
 			continue
@@ -217,16 +215,24 @@ func (sm styleMap) parseGNU(env string) {
 			return
 		}
 
-		key, val := pair[0], pair[1]
+		sm.parsePair(pair)
+	}
+}
 
-		key = replaceTilde(key)
+func (sm *styleMap) parsePair(pair []string) {
+	key, val := pair[0], pair[1]
 
-		if filepath.IsAbs(key) {
-			key = filepath.Clean(key)
-		}
+	key = replaceTilde(key)
 
-		sm[key] = applyAnsiCodes(val, tcell.StyleDefault)
+	if filepath.IsAbs(key) {
+		key = filepath.Clean(key)
 	}
+
+	if key == "ln" && val == "target" {
+		sm.useLinkTarget = true
+	}
+
+	sm.styles[key] = applyAnsiCodes(val, tcell.StyleDefault)
 }
 
 // This function parses $LSCOLORS environment variable.
@@ -267,17 +273,17 @@ func (sm styleMap) parseBSD(env string) {
 	}
 
 	for i, key := range colorNames {
-		sm[key] = getStyle(env[i*2], env[i*2+1])
+		sm.styles[key] = getStyle(env[i*2], env[i*2+1])
 	}
 }
 
 func (sm styleMap) get(f *file) tcell.Style {
-	if val, ok := sm[f.path]; ok {
+	if val, ok := sm.styles[f.path]; ok {
 		return val
 	}
 
 	if f.IsDir() {
-		if val, ok := sm[f.Name()+"/"]; ok {
+		if val, ok := sm.styles[f.Name()+"/"]; ok {
 			return val
 		}
 	}
@@ -285,7 +291,7 @@ func (sm styleMap) get(f *file) tcell.Style {
 	var key string
 
 	switch {
-	case f.linkState == working:
+	case f.linkState == working && !sm.useLinkTarget:
 		key = "ln"
 	case f.linkState == broken:
 		key = "or"
@@ -313,27 +319,27 @@ func (sm styleMap) get(f *file) tcell.Style {
 		key = "ex"
 	}
 
-	if val, ok := sm[key]; ok {
+	if val, ok := sm.styles[key]; ok {
 		return val
 	}
 
-	if val, ok := sm[f.Name()+"*"]; ok {
+	if val, ok := sm.styles[f.Name()+"*"]; ok {
 		return val
 	}
 
-	if val, ok := sm["*"+f.Name()]; ok {
+	if val, ok := sm.styles["*"+f.Name()]; ok {
 		return val
 	}
 
-	if val, ok := sm[filepath.Base(f.Name())+".*"]; ok {
+	if val, ok := sm.styles[filepath.Base(f.Name())+".*"]; ok {
 		return val
 	}
 
-	if val, ok := sm["*"+strings.ToLower(f.ext)]; ok {
+	if val, ok := sm.styles["*"+strings.ToLower(f.ext)]; ok {
 		return val
 	}
 
-	if val, ok := sm["fi"]; ok {
+	if val, ok := sm.styles["fi"]; ok {
 		return val
 	}
 
diff --git a/copy.go b/copy.go
index 8127a08..ea84811 100644
--- a/copy.go
+++ b/copy.go
@@ -37,7 +37,7 @@ func copySize(srcs []string) (int64, error) {
 }
 
 func copyFile(src, dst string, preserve []string, info os.FileInfo, nums chan int64) error {
-	var dst_mode os.FileMode = 0666
+	var dst_mode os.FileMode = 0o666
 	preserve_timestamps := false
 	for _, s := range preserve {
 		switch s {
@@ -107,9 +107,9 @@ func copyAll(srcs []string, dstDir string, preserve []string) (nums chan int64,
 			file := filepath.Base(src)
 			dst := filepath.Join(dstDir, file)
 
-			_, err := os.Lstat(dst)
+			lstat, err := os.Lstat(dst)
 			if !os.IsNotExist(err) {
-				ext := filepath.Ext(file)
+				ext := getFileExtension(lstat)
 				basename := file[:len(file)-len(ext)]
 				var newPath string
 				for i := 1; !os.IsNotExist(err); i++ {
diff --git a/doc.md b/doc.md
index 92dee46..d84e7bd 100644
--- a/doc.md
+++ b/doc.md
@@ -155,6 +155,7 @@ The following options can be used to customize the behavior of lf:
 	errorfmt            string    (default "\033[7;31;47m")
 	filesep             string    (default "\n")
 	findlen             int       (default 1)
+	globfilter          bool      (default false)
 	globsearch          bool      (default false)
 	hidden              bool      (default false)
 	hiddenfiles         []string  (default '.*')
@@ -180,6 +181,7 @@ The following options can be used to customize the behavior of lf:
 	ratios              []int     (default '1:2:3')
 	relativenumber      bool      (default false)
 	reverse             bool      (default false)
+	roundbox            bool      (default false)
 	ruler               []string  (default 'acc:progress:selection:filter:ind')
 	rulerfmt            string    (default "  %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")
 	scrolloff           int       (default 0)
@@ -749,6 +751,10 @@ File separator used in environment variables `fs` and `fx`.
 Number of characters prompted for the find command.
 When this value is set to 0, find command prompts until there is only a single match left.
 
+## globfilter (bool) (default false)
+
+Patterns are treated as globs for the filter command, see `globsearch` for more details.
+
 ## globsearch (bool) (default false)
 
 When this option is enabled, search command patterns are considered as globs, otherwise, they are literals.
@@ -884,6 +890,10 @@ When `number` is enabled, the current line shows the absolute position, otherwis
 
 Reverse the direction of sort.
 
+## roundbox (bool) (default false)
+
+Draw rounded outer corners when the `drawbox` option is enabled.
+
 ## ruler ([]string) (default `acc:progress:selection:filter:ind`)
 
 This option is deprecated in favor of using the `rulerfmt` option (see below).
@@ -1790,6 +1800,7 @@ You may instead divide it into multiple lines in between double quotes by escapi
 	ex=01;32:\
 	"
 
+The `ln` entry supports the special value `target`, which will use the link target to select a style. File name rules will still apply based on the link's name -- this mirrors GNU's `ls` and `dircolors` behavior.
 Having such a long variable definition in a shell configuration file might be undesirable.
 You may instead use the colors file (refer to the [CONFIGURATION section](https://github.com/gokcehan/lf/blob/master/doc.md#configuration)) for configuration.
 A sample colors file can be found at
@@ -1802,6 +1813,7 @@ https://en.wikipedia.org/wiki/ANSI_escape_code
 Icons are configured using `LF_ICONS` environment variable or an icons file (refer to the [CONFIGURATION section](https://github.com/gokcehan/lf/blob/master/doc.md#configuration)).
 The variable uses the same syntax as `LS_COLORS/LF_COLORS`.
 Instead of colors, you should put a single characters as values of entries.
+The `ln` entry supports the special value `target`, which will use the link target to select a icon. File name rules will still apply based on the link's name -- this mirrors GNU's `ls` and `dircolors` behavior.
 The icons file (refer to the [CONFIGURATION section](https://github.com/gokcehan/lf/blob/master/doc.md#configuration)) should consist of whitespace-separated pairs with a `#` character to start comments until the end of the line.
 Do not forget to add `set icons true` to your `lfrc` to see the icons.
 Default values are as follows given with their matching order in lf:
diff --git a/doc.txt b/doc.txt
index f932ec1..64c41a6 100644
--- a/doc.txt
+++ b/doc.txt
@@ -145,6 +145,7 @@ The following options can be used to customize the behavior of lf:
     errorfmt            string    (default "\033[7;31;47m")
     filesep             string    (default "\n")
     findlen             int       (default 1)
+    globfilter          bool      (default false)
     globsearch          bool      (default false)
     hidden              bool      (default false)
     hiddenfiles         []string  (default '.*')
@@ -170,6 +171,7 @@ The following options can be used to customize the behavior of lf:
     ratios              []int     (default '1:2:3')
     relativenumber      bool      (default false)
     reverse             bool      (default false)
+    roundbox            bool      (default false)
     ruler               []string  (default 'acc:progress:selection:filter:ind')
     rulerfmt            string    (default "  %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")
     scrolloff           int       (default 0)
@@ -797,6 +799,11 @@ findlen (int) (default 1)
 Number of characters prompted for the find command. When this value is
 set to 0, find command prompts until there is only a single match left.
 
+globfilter (bool) (default false)
+
+Patterns are treated as globs for the filter command, see globsearch for
+more details.
+
 globsearch (bool) (default false)
 
 When this option is enabled, search command patterns are considered as
@@ -953,6 +960,10 @@ reverse (bool) (default false)
 
 Reverse the direction of sort.
 
+roundbox (bool) (default false)
+
+Draw rounded outer corners when the drawbox option is enabled.
+
 ruler ([]string) (default acc:progress:selection:filter:ind)
 
 This option is deprecated in favor of using the rulerfmt option (see
@@ -2026,8 +2037,11 @@ escaping newlines with backslashes as follows:
     ex=01;32:\
     "
 
-Having such a long variable definition in a shell configuration file
-might be undesirable. You may instead use the colors file (refer to the
+The ln entry supports the special value target, which will use the link
+target to select a style. File name rules will still apply based on the
+link's name -- this mirrors GNU's ls and dircolors behavior. Having such
+a long variable definition in a shell configuration file might be
+undesirable. You may instead use the colors file (refer to the
 CONFIGURATION section) for configuration. A sample colors file can be
 found at https://github.com/gokcehan/lf/blob/master/etc/colors.example
 You may also see the wiki page for ANSI escape codes
@@ -2038,7 +2052,10 @@ ICONS
 Icons are configured using LF_ICONS environment variable or an icons
 file (refer to the CONFIGURATION section). The variable uses the same
 syntax as LS_COLORS/LF_COLORS. Instead of colors, you should put a
-single characters as values of entries. The icons file (refer to the
+single characters as values of entries. The ln entry supports the
+special value target, which will use the link target to select a icon.
+File name rules will still apply based on the link's name -- this
+mirrors GNU's ls and dircolors behavior. The icons file (refer to the
 CONFIGURATION section) should consist of whitespace-separated pairs with
 a # character to start comments until the end of the line. Do not forget
 to add set icons true to your lfrc to see the icons. Default values are
diff --git a/eval.go b/eval.go
index bdb7ff8..02d1dda 100644
--- a/eval.go
+++ b/eval.go
@@ -96,14 +96,16 @@ func (e *setExpr) eval(app *app, args []string) {
 			}
 			app.ui.loadFile(app, true)
 		}
-	case "globsearch", "noglobsearch", "globsearch!":
-		err = applyBoolOpt(&gOpts.globsearch, e)
+	case "globfilter", "noglobfilter", "globfilter!":
+		err = applyBoolOpt(&gOpts.globfilter, e)
 		if err == nil {
 			app.nav.sort()
 			app.nav.position()
 			app.ui.sort()
 			app.ui.loadFile(app, true)
 		}
+	case "globsearch", "noglobsearch", "globsearch!":
+		err = applyBoolOpt(&gOpts.globsearch, e)
 	case "hidden", "nohidden", "hidden!":
 		err = applyBoolOpt(&gOpts.hidden, e)
 		if err == nil {
@@ -114,6 +116,13 @@ func (e *setExpr) eval(app *app, args []string) {
 		}
 	case "hidecursorinactive", "nohidecursorinactive", "hidecursorinactive!":
 		err = applyBoolOpt(&gOpts.hidecursorinactive, e)
+		if err == nil {
+			if gOpts.hidecursorinactive {
+				app.ui.screen.EnableFocus()
+			} else {
+				app.ui.screen.DisableFocus()
+			}
+		}
 	case "history", "nohistory", "history!":
 		err = applyBoolOpt(&gOpts.history, e)
 	case "icons", "noicons", "icons!":
@@ -167,6 +176,8 @@ func (e *setExpr) eval(app *app, args []string) {
 			app.nav.sort()
 			app.ui.sort()
 		}
+	case "roundbox", "noroundbox", "roundbox!":
+		err = applyBoolOpt(&gOpts.roundbox, e)
 	case "sixel", "nosixel", "sixel!":
 		err = applyBoolOpt(&gOpts.sixel, e)
 	case "smartcase", "nosmartcase", "smartcase!":
@@ -569,6 +580,12 @@ func onSelect(app *app) {
 	}
 }
 
+func onQuit(app *app) {
+	if cmd, ok := gOpts.cmds["on-quit"]; ok {
+		cmd.eval(app, nil)
+	}
+}
+
 func splitKeys(s string) (keys []string) {
 	for i := 0; i < len(s); {
 		r, w := utf8.DecodeRuneInString(s[i:])
@@ -1611,8 +1628,8 @@ func (e *callExpr) eval(app *app, args []string) {
 			}
 			normal(app)
 			app.ui.cmdPrefix = "rename: "
-			extension := filepath.Ext(curr.Name())
-			if len(extension) == 0 || extension == curr.Name() || curr.IsDir() {
+			extension := getFileExtension(curr)
+			if len(extension) == 0 {
 				// no extension or .hidden or is directory
 				app.ui.cmdAccLeft = append(app.ui.cmdAccLeft, []rune(curr.Name())...)
 			} else {
diff --git a/go.mod b/go.mod
index 10886b4..eb3cdc4 100644
--- a/go.mod
+++ b/go.mod
@@ -6,8 +6,8 @@ require (
 	github.com/djherbis/times v1.6.0
 	github.com/gdamore/tcell/v2 v2.7.4
 	github.com/mattn/go-runewidth v0.0.15
-	golang.org/x/sys v0.18.0
-	golang.org/x/term v0.18.0
+	golang.org/x/sys v0.19.0
+	golang.org/x/term v0.19.0
 )
 
 require (
diff --git a/go.sum b/go.sum
index b10dc49..1548183 100644
--- a/go.sum
+++ b/go.sum
@@ -31,14 +31,14 @@ golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBc
 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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
-golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
-golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
+golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
+golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
 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/icons.go b/icons.go
index ac4230f..5583864 100644
--- a/icons.go
+++ b/icons.go
@@ -7,10 +7,16 @@ import (
 	"strings"
 )
 
-type iconMap map[string]string
+type iconMap struct {
+	icons         map[string]string
+	useLinkTarget bool
+}
 
 func parseIcons() iconMap {
-	im := make(iconMap)
+	im := iconMap{
+		icons:         make(map[string]string),
+		useLinkTarget: false,
+	}
 
 	defaultIcons := []string{
 		"ln=l",
@@ -44,7 +50,7 @@ func parseIcons() iconMap {
 	return im
 }
 
-func (im iconMap) parseFile(path string) {
+func (im *iconMap) parseFile(path string) {
 	log.Printf("reading file: %s", path)
 
 	f, err := os.Open(path)
@@ -61,19 +67,11 @@ func (im iconMap) parseFile(path string) {
 	}
 
 	for _, pair := range pairs {
-		key, val := pair[0], pair[1]
-
-		key = replaceTilde(key)
-
-		if filepath.IsAbs(key) {
-			key = filepath.Clean(key)
-		}
-
-		im[key] = val
+		im.parsePair(pair)
 	}
 }
 
-func (im iconMap) parseEnv(env string) {
+func (im *iconMap) parseEnv(env string) {
 	for _, entry := range strings.Split(env, ":") {
 		if entry == "" {
 			continue
@@ -86,25 +84,33 @@ func (im iconMap) parseEnv(env string) {
 			return
 		}
 
-		key, val := pair[0], pair[1]
+		im.parsePair(pair)
+	}
+}
 
-		key = replaceTilde(key)
+func (im *iconMap) parsePair(pair []string) {
+	key, val := pair[0], pair[1]
 
-		if filepath.IsAbs(key) {
-			key = filepath.Clean(key)
-		}
+	key = replaceTilde(key)
 
-		im[key] = val
+	if filepath.IsAbs(key) {
+		key = filepath.Clean(key)
 	}
+
+	if key == "ln" && val == "target" {
+		im.useLinkTarget = true
+	}
+
+	im.icons[key] = val
 }
 
 func (im iconMap) get(f *file) string {
-	if val, ok := im[f.path]; ok {
+	if val, ok := im.icons[f.path]; ok {
 		return val
 	}
 
 	if f.IsDir() {
-		if val, ok := im[f.Name()+"/"]; ok {
+		if val, ok := im.icons[f.Name()+"/"]; ok {
 			return val
 		}
 	}
@@ -112,7 +118,7 @@ func (im iconMap) get(f *file) string {
 	var key string
 
 	switch {
-	case f.linkState == working:
+	case f.linkState == working && !im.useLinkTarget:
 		key = "ln"
 	case f.linkState == broken:
 		key = "or"
@@ -140,27 +146,27 @@ func (im iconMap) get(f *file) string {
 		key = "ex"
 	}
 
-	if val, ok := im[key]; ok {
+	if val, ok := im.icons[key]; ok {
 		return val
 	}
 
-	if val, ok := im[f.Name()+"*"]; ok {
+	if val, ok := im.icons[f.Name()+"*"]; ok {
 		return val
 	}
 
-	if val, ok := im["*"+f.Name()]; ok {
+	if val, ok := im.icons["*"+f.Name()]; ok {
 		return val
 	}
 
-	if val, ok := im[filepath.Base(f.Name())+".*"]; ok {
+	if val, ok := im.icons[filepath.Base(f.Name())+".*"]; ok {
 		return val
 	}
 
-	if val, ok := im["*"+strings.ToLower(f.ext)]; ok {
+	if val, ok := im.icons["*"+strings.ToLower(f.ext)]; ok {
 		return val
 	}
 
-	if val, ok := im["fi"]; ok {
+	if val, ok := im.icons["fi"]; ok {
 		return val
 	}
 
diff --git a/lf.1 b/lf.1
index 1d0c45e..cff175a 100644
--- a/lf.1
+++ b/lf.1
@@ -163,6 +163,7 @@ dupfilefmt          string    (default \[aq]%f.\[ti]%n\[ti]\[aq])
 errorfmt            string    (default \[dq]\[rs]033[7;31;47m\[dq])
 filesep             string    (default \[dq]\[rs]n\[dq])
 findlen             int       (default 1)
+globfilter          bool      (default false)
 globsearch          bool      (default false)
 hidden              bool      (default false)
 hiddenfiles         []string  (default \[aq].*\[aq])
@@ -188,6 +189,7 @@ promptfmt           string    (default \[dq]\[rs]033[32;1m%u\[at]%h\[rs]033[0m:\
 ratios              []int     (default \[aq]1:2:3\[aq])
 relativenumber      bool      (default false)
 reverse             bool      (default false)
+roundbox            bool      (default false)
 ruler               []string  (default \[aq]acc:progress:selection:filter:ind\[aq])
 rulerfmt            string    (default \[dq]  %a|  %p|  \[rs]033[7;31m %m \[rs]033[0m|  \[rs]033[7;33m %c \[rs]033[0m|  \[rs]033[7;35m %s \[rs]033[0m|  \[rs]033[7;34m %f \[rs]033[0m|  %i/%t\[dq])
 scrolloff           int       (default 0)
@@ -815,6 +817,10 @@ File separator used in environment variables \f[C]fs\f[R] and
 Number of characters prompted for the find command.
 When this value is set to 0, find command prompts until there is only a
 single match left.
+.SS globfilter (bool) (default false)
+.PP
+Patterns are treated as globs for the filter command, see
+\f[C]globsearch\f[R] for more details.
 .SS globsearch (bool) (default false)
 .PP
 When this option is enabled, search command patterns are considered as
@@ -969,6 +975,9 @@ position, otherwise nothing is shown.
 .SS reverse (bool) (default false)
 .PP
 Reverse the direction of sort.
+.SS roundbox (bool) (default false)
+.PP
+Draw rounded outer corners when the \f[C]drawbox\f[R] option is enabled.
 .SS ruler ([]string) (default \f[C]acc:progress:selection:filter:ind\f[R])
 .PP
 This option is deprecated in favor of using the \f[C]rulerfmt\f[R]
@@ -2363,6 +2372,10 @@ ex=01;32:\[rs]
 \f[R]
 .fi
 .PP
+The \f[C]ln\f[R] entry supports the special value \f[C]target\f[R],
+which will use the link target to select a style.
+File name rules will still apply based on the link\[aq]s name -- this
+mirrors GNU\[aq]s \f[C]ls\f[R] and \f[C]dircolors\f[R] behavior.
 Having such a long variable definition in a shell configuration file
 might be undesirable.
 You may instead use the colors file (refer to the CONFIGURATION
@@ -2380,6 +2393,10 @@ section (https://github.com/gokcehan/lf/blob/master/doc.md#configuration)).
 The variable uses the same syntax as \f[C]LS_COLORS/LF_COLORS\f[R].
 Instead of colors, you should put a single characters as values of
 entries.
+The \f[C]ln\f[R] entry supports the special value \f[C]target\f[R],
+which will use the link target to select a icon.
+File name rules will still apply based on the link\[aq]s name -- this
+mirrors GNU\[aq]s \f[C]ls\f[R] and \f[C]dircolors\f[R] behavior.
 The icons file (refer to the CONFIGURATION
 section (https://github.com/gokcehan/lf/blob/master/doc.md#configuration))
 should consist of whitespace-separated pairs with a \f[C]#\f[R]
diff --git a/misc.go b/misc.go
index b38dab2..63b162e 100644
--- a/misc.go
+++ b/misc.go
@@ -4,6 +4,7 @@ import (
 	"bufio"
 	"fmt"
 	"io"
+	"io/fs"
 	"path/filepath"
 	"regexp"
 	"strconv"
@@ -291,12 +292,30 @@ func naturalLess(s1, s2 string) bool {
 	}
 }
 
-var reModKey = regexp.MustCompile(`<(c|s|a)-(.+)>`)
-var reRulerSub = regexp.MustCompile(`%[apmcsfithd]|%\{[^}]+\}`)
+// This function returns the extension of a file with a leading dot
+// it returns an empty string if extension could not be determined
+// i.e. directories, filenames without extensions
+func getFileExtension(file fs.FileInfo) string {
+	if file.IsDir() {
+		return ""
+	}
+	if strings.Count(file.Name(), ".") == 1 && file.Name()[0] == '.' {
+		// hidden file without extension
+		return ""
+	}
+	return filepath.Ext(file.Name())
+}
 
-var reWord = regexp.MustCompile(`(\pL|\pN)+`)
-var reWordBeg = regexp.MustCompile(`([^\pL\pN]|^)(\pL|\pN)`)
-var reWordEnd = regexp.MustCompile(`(\pL|\pN)([^\pL\pN]|$)`)
+var (
+	reModKey   = regexp.MustCompile(`<(c|s|a)-(.+)>`)
+	reRulerSub = regexp.MustCompile(`%[apmcsfithd]|%\{[^}]+\}`)
+)
+
+var (
+	reWord    = regexp.MustCompile(`(\pL|\pN)+`)
+	reWordBeg = regexp.MustCompile(`([^\pL\pN]|^)(\pL|\pN)`)
+	reWordEnd = regexp.MustCompile(`(\pL|\pN)([^\pL\pN]|$)`)
+)
 
 func min(a, b int) int {
 	if a < b {
diff --git a/nav.go b/nav.go
index f620222..0a16cfb 100644
--- a/nav.go
+++ b/nav.go
@@ -55,7 +55,7 @@ type fakeStat struct {
 
 func (fs *fakeStat) Name() string       { return fs.name }
 func (fs *fakeStat) Size() int64        { return 0 }
-func (fs *fakeStat) Mode() os.FileMode  { return os.FileMode(0000) }
+func (fs *fakeStat) Mode() os.FileMode  { return os.FileMode(0o000) }
 func (fs *fakeStat) ModTime() time.Time { return time.Unix(0, 0) }
 func (fs *fakeStat) IsDir() bool        { return false }
 func (fs *fakeStat) Sys() any           { return nil }
@@ -88,7 +88,7 @@ func readdir(path string) ([]*file, error) {
 				dirSize:    -1,
 				accessTime: time.Unix(0, 0),
 				changeTime: time.Unix(0, 0),
-				ext:        filepath.Ext(fpath),
+				ext:        getFileExtension(lstat),
 				err:        err,
 			})
 			continue
@@ -122,10 +122,6 @@ func readdir(path string) ([]*file, error) {
 			ct = lstat.ModTime()
 		}
 
-		// returns an empty string if extension could not be determined
-		// i.e. directories, filenames without extensions
-		ext := filepath.Ext(fpath)
-
 		dirCount := -1
 		if lstat.IsDir() && gOpts.dircounts {
 			d, err := os.Open(fpath)
@@ -152,7 +148,7 @@ func readdir(path string) ([]*file, error) {
 			dirSize:    -1,
 			accessTime: at,
 			changeTime: ct,
-			ext:        ext,
+			ext:        getFileExtension(lstat),
 			err:        nil,
 		})
 	}
@@ -492,7 +488,6 @@ func (nav *nav) loadDirInternal(path string) *dir {
 			nav.dirPreviewChan <- d
 		}
 		nav.dirChan <- d
-
 	}()
 	return d
 }
@@ -773,7 +768,6 @@ func matchPattern(pattern, name, path string) bool {
 }
 
 func (nav *nav) previewDir(dir *dir, win *win) {
-
 	defer func() {
 		dir.loading = false
 		nav.dirChan <- dir
@@ -829,11 +823,9 @@ func (nav *nav) previewDir(dir *dir, win *win) {
 			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 }()
 
@@ -1408,7 +1400,7 @@ func (nav *nav) moveAsync(app *app, srcs []string, dstDir string) {
 			app.ui.exprChan <- echo
 			continue
 		} else if !os.IsNotExist(err) {
-			ext := filepath.Ext(file)
+			ext := getFileExtension(dstStat)
 			basename := file[:len(file)-len(ext)]
 			var newPath string
 			for i := 1; !os.IsNotExist(err); i++ {
@@ -1756,7 +1748,7 @@ func (nav *nav) findPrev() (bool, bool) {
 	return false, false
 }
 
-func searchMatch(name, pattern string) (matched bool, err error) {
+func searchMatch(name, pattern string, glob bool) (matched bool, err error) {
 	if gOpts.ignorecase {
 		lpattern := strings.ToLower(pattern)
 		if !gOpts.smartcase || lpattern == pattern {
@@ -1771,7 +1763,7 @@ func searchMatch(name, pattern string) (matched bool, err error) {
 			name = removeDiacritics(name)
 		}
 	}
-	if gOpts.globsearch {
+	if glob {
 		return filepath.Match(pattern, name)
 	}
 	return strings.Contains(name, pattern), nil
@@ -1780,7 +1772,7 @@ func searchMatch(name, pattern string) (matched bool, err error) {
 func (nav *nav) searchNext() (bool, error) {
 	dir := nav.currDir()
 	for i := dir.ind + 1; i < len(dir.files); i++ {
-		if matched, err := searchMatch(dir.files[i].Name(), nav.search); err != nil {
+		if matched, err := searchMatch(dir.files[i].Name(), nav.search, gOpts.globsearch); err != nil {
 			return false, err
 		} else if matched {
 			return nav.down(i - dir.ind), nil
@@ -1788,7 +1780,7 @@ func (nav *nav) searchNext() (bool, error) {
 	}
 	if gOpts.wrapscan {
 		for i := 0; i < dir.ind; i++ {
-			if matched, err := searchMatch(dir.files[i].Name(), nav.search); err != nil {
+			if matched, err := searchMatch(dir.files[i].Name(), nav.search, gOpts.globsearch); err != nil {
 				return false, err
 			} else if matched {
 				return nav.up(dir.ind - i), nil
@@ -1801,7 +1793,7 @@ func (nav *nav) searchNext() (bool, error) {
 func (nav *nav) searchPrev() (bool, error) {
 	dir := nav.currDir()
 	for i := dir.ind - 1; i >= 0; i-- {
-		if matched, err := searchMatch(dir.files[i].Name(), nav.search); err != nil {
+		if matched, err := searchMatch(dir.files[i].Name(), nav.search, gOpts.globsearch); err != nil {
 			return false, err
 		} else if matched {
 			return nav.up(dir.ind - i), nil
@@ -1809,7 +1801,7 @@ func (nav *nav) searchPrev() (bool, error) {
 	}
 	if gOpts.wrapscan {
 		for i := len(dir.files) - 1; i > dir.ind; i-- {
-			if matched, err := searchMatch(dir.files[i].Name(), nav.search); err != nil {
+			if matched, err := searchMatch(dir.files[i].Name(), nav.search, gOpts.globsearch); err != nil {
 				return false, err
 			} else if matched {
 				return nav.down(i - dir.ind), nil
@@ -1821,7 +1813,7 @@ func (nav *nav) searchPrev() (bool, error) {
 
 func isFiltered(f os.FileInfo, filter []string) bool {
 	for _, pattern := range filter {
-		matched, err := searchMatch(f.Name(), strings.TrimPrefix(pattern, "!"))
+		matched, err := searchMatch(f.Name(), strings.TrimPrefix(pattern, "!"), gOpts.globfilter)
 		if err != nil {
 			log.Printf("Filter Error: %s", err)
 			return false
@@ -1991,7 +1983,6 @@ func (m indexedSelections) Swap(i, j int) {
 func (m indexedSelections) Less(i, j int) bool { return m.indices[i] < m.indices[j] }
 
 func (nav *nav) currSelections() []string {
-
 	currDirOnly := gOpts.selmode == "dir"
 	currDirPath := ""
 	if currDirOnly {
diff --git a/opts.go b/opts.go
index fc211e0..035de65 100644
--- a/opts.go
+++ b/opts.go
@@ -46,6 +46,7 @@ var gOpts struct {
 	dirpreviews        bool
 	drawbox            bool
 	dupfilefmt         string
+	globfilter         bool
 	globsearch         bool
 	hidden             bool
 	icons              bool
@@ -58,6 +59,7 @@ var gOpts struct {
 	preview            bool
 	relativenumber     bool
 	reverse            bool
+	roundbox           bool
 	selectfmt          string
 	sixel              bool
 	sortby             sortMethod
@@ -191,6 +193,7 @@ func init() {
 	gOpts.cursorpreviewfmt = "\033[4m"
 	gOpts.cutfmt = "\033[7;31m"
 	gOpts.hidecursorinactive = false
+	gOpts.globfilter = false
 	gOpts.globsearch = false
 	gOpts.hidden = false
 	gOpts.icons = false
@@ -203,6 +206,7 @@ func init() {
 	gOpts.preview = true
 	gOpts.relativenumber = false
 	gOpts.reverse = false
+	gOpts.roundbox = false
 	gOpts.selectfmt = "\033[7;35m"
 	gOpts.sixel = false
 	gOpts.sortby = naturalSort
diff --git a/os_windows.go b/os_windows.go
index 4eefe83..a07821f 100644
--- a/os_windows.go
+++ b/os_windows.go
@@ -8,6 +8,7 @@ import (
 	"os/user"
 	"path/filepath"
 	"strings"
+	"syscall"
 
 	"golang.org/x/sys/windows"
 )
@@ -24,8 +25,8 @@ var envPathExt = os.Getenv("PATHEXT")
 var (
 	gDefaultShell      = "cmd"
 	gDefaultShellFlag  = "/c"
-	gDefaultSocketProt = "tcp"
-	gDefaultSocketPath = "127.0.0.1:12345"
+	gDefaultSocketProt = "unix"
+	gDefaultSocketPath string
 )
 
 var (
@@ -98,6 +99,16 @@ func init() {
 	gMarksPath = filepath.Join(data, "lf", "marks")
 	gTagsPath = filepath.Join(data, "lf", "tags")
 	gHistoryPath = filepath.Join(data, "lf", "history")
+
+	socket, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
+	if err != nil {
+		gDefaultSocketProt = "tcp"
+		gDefaultSocketPath = "127.0.0.1:12345"
+	} else {
+		runtime := os.TempDir()
+		gDefaultSocketPath = filepath.Join(runtime, fmt.Sprintf("lf.%s.sock", gUser.Username))
+		syscall.Close(socket)
+	}
 }
 
 func detachedCommand(name string, arg ...string) *exec.Cmd {
diff --git a/ui.go b/ui.go
index 10c0f90..5aabf66 100644
--- a/ui.go
+++ b/ui.go
@@ -980,10 +980,17 @@ func (ui *ui) drawBox() {
 		ui.screen.SetContent(w-1, i, tcell.RuneVLine, nil, st)
 	}
 
-	ui.screen.SetContent(0, 1, tcell.RuneULCorner, nil, st)
-	ui.screen.SetContent(w-1, 1, tcell.RuneURCorner, nil, st)
-	ui.screen.SetContent(0, h-2, tcell.RuneLLCorner, nil, st)
-	ui.screen.SetContent(w-1, h-2, tcell.RuneLRCorner, nil, st)
+	if gOpts.roundbox {
+		ui.screen.SetContent(0, 1, '╭', nil, st)
+		ui.screen.SetContent(w-1, 1, '╮', nil, st)
+		ui.screen.SetContent(0, h-2, '╰', nil, st)
+		ui.screen.SetContent(w-1, h-2, '╯', nil, st)
+	} else {
+		ui.screen.SetContent(0, 1, tcell.RuneULCorner, nil, st)
+		ui.screen.SetContent(w-1, 1, tcell.RuneURCorner, nil, st)
+		ui.screen.SetContent(0, h-2, tcell.RuneLLCorner, nil, st)
+		ui.screen.SetContent(w-1, h-2, tcell.RuneLRCorner, nil, st)
+	}
 
 	wacc := 0
 	for wind := 0; wind < len(ui.wins)-1; wind++ {


Test patch:
diff --git a/diacritics_test.go b/diacritics_test.go
index 3e29cf2..1d71eea 100644
--- a/diacritics_test.go
+++ b/diacritics_test.go
@@ -61,7 +61,7 @@ func runSearch(t *testing.T, ignorecase, smartcase, ignorediacritics, smartdiacr
 	gOpts.smartcase = smartcase
 	gOpts.ignoredia = ignorediacritics
 	gOpts.smartdia = smartdiacritics
-	matched, _ := searchMatch(base, pattern)
+	matched, _ := searchMatch(base, pattern, false)
 	if matched != expected {
 		t.Errorf("False search for" +
 			" ignorecase = " + strconv.FormatBool(gOpts.ignorecase) + ", " +
diff --git a/misc_test.go b/misc_test.go
index 758e342..52b2628 100644
--- a/misc_test.go
+++ b/misc_test.go
@@ -5,6 +5,7 @@ import (
 	"reflect"
 	"strings"
 	"testing"
+	"time"
 )
 
 func TestIsRoot(t *testing.T) {
@@ -293,3 +294,40 @@ func TestNaturalLess(t *testing.T) {
 		}
 	}
 }
+
+type fakeFileInfo struct {
+	name  string
+	isDir bool
+}
+
+func (fileinfo fakeFileInfo) Name() string       { return fileinfo.name }
+func (fileinfo fakeFileInfo) Size() int64        { return 0 }
+func (fileinfo fakeFileInfo) Mode() os.FileMode  { return os.FileMode(0o000) }
+func (fileinfo fakeFileInfo) ModTime() time.Time { return time.Unix(0, 0) }
+func (fileinfo fakeFileInfo) IsDir() bool        { return fileinfo.isDir }
+func (fileinfo fakeFileInfo) Sys() any           { return nil }
+
+func TestGetFileExtension(t *testing.T) {
+	tests := []struct {
+		name              string
+		fileName          string
+		isDir             bool
+		expectedExtension string
+	}{
+		{"normal file", "file.txt", false, ".txt"},
+		{"file without extension", "file", false, ""},
+		{"hidden file", ".gitignore", false, ""},
+		{"hidden file with extension", ".file.txt", false, ".txt"},
+		{"directory", "dir", true, ""},
+		{"hidden directory", ".git", true, ""},
+		{"directory with dot", "profile.d", true, ""},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			if got := getFileExtension(fakeFileInfo{test.fileName, test.isDir}); got != test.expectedExtension {
+				t.Errorf("at input %q expected %q but got %q", test.fileName, test.expectedExtension, got)
+			}
+		})
+	}
+}