Repository: gokcehan/lf (language: go)

Issue:
setlocal: missing dircounts
Is there specific reason why dircounts cant be set using setlocal? E.g this is may be required for remote fs or network storage to get better performance. Other options like preview, ratios is also very helpful


Discussion / hints:
`setlocal` was added in #1381, and at the time only options that were deemed 'useful' were added:

> I have only added `sortby` so far for testing. In general, there are many options that are not applicable as a local option or they don't make much sense even if they are applicable. So I thought it would be better to add options selectively whenever they make sense. Currently, I'm thinking of only adding options related to sorting and maybe also things like `info` initially. I expect these options to cover most use-cases that users have in mind.

I guess it might make sense to add `dircounts` as well, it's just that so far nobody has requested such a feature. I wrote a quick patch for it and tested it very briefly, if there is enough interest from users I might consider adding it as an actual PR:

<details>
<summary>Click to expand</summary>

```diff
diff --git a/complete.go b/complete.go
index 8317ecc..f467639 100644
--- a/complete.go
+++ b/complete.go
@@ -137,6 +137,10 @@ func getLocalOptWords(localOpts any) (localOptWords []string) {
 	for i := 0; i < t.NumField(); i++ {
 		field := t.Field(i)
 		name := strings.TrimSuffix(field.Name, "s")
+		// TODO: remove the 's' suffix from all setlocal maps to simplify
+		if field.Name == "dircounts" {
+			name = "dircounts"
+		}
 		if field.Type.Kind() != reflect.Map {
 			continue
 		}
diff --git a/eval.go b/eval.go
index e222e44..f1cc76d 100644
--- a/eval.go
+++ b/eval.go
@@ -525,6 +525,8 @@ func (e *setLocalExpr) eval(app *app, args []string) {
 		gLocalOpts.locales[e.path] = localeStr
 		app.nav.sort()
 		app.ui.sort()
+	case "dircounts":
+		applyLocalBoolOpt(gLocalOpts.dircounts, gOpts.dircounts, e)
 	default:
 		err = fmt.Errorf("unknown option: %s", e.opt)
 	}
diff --git a/nav.go b/nav.go
index 940de91..08f1f9a 100644
--- a/nav.go
+++ b/nav.go
@@ -88,7 +88,7 @@ func newFile(path string) *file {
 	}
 
 	dirCount := -1
-	if lstat.IsDir() && gOpts.dircounts {
+	if lstat.IsDir() && getDirCounts(filepath.Dir(path)) {
 		d, err := os.Open(path)
 		if err != nil {
 			dirCount = -2
diff --git a/opts.go b/opts.go
index 9d1211d..965899e 100644
--- a/opts.go
+++ b/opts.go
@@ -114,6 +114,7 @@ var gLocalOpts struct {
 	reverses  map[string]bool
 	infos     map[string][]string
 	locales   map[string]string
+	dircounts map[string]bool
 }
 
 func localOptPaths(path string) []string {
@@ -187,6 +188,15 @@ func getLocale(path string) string {
 	return gOpts.locale
 }
 
+func getDirCounts(path string) bool {
+	for _, key := range localOptPaths(path) {
+		if val, ok := gLocalOpts.dircounts[key]; ok {
+			return val
+		}
+	}
+	return gOpts.dircounts
+}
+
 func init() {
 	gOpts.anchorfind = true
 	gOpts.autoquit = true
@@ -383,6 +393,7 @@ func init() {
 	gLocalOpts.reverses = make(map[string]bool)
 	gLocalOpts.infos = make(map[string][]string)
 	gLocalOpts.locales = make(map[string]string)
+	gLocalOpts.dircounts = make(map[string]bool)
 
 	setDefaults()
 }
diff --git a/ui.go b/ui.go
index 4c85aec..0d67858 100644
--- a/ui.go
+++ b/ui.go
@@ -295,7 +295,7 @@ func fileInfo(f *file, d *dir, userWidth int, groupWidth int) string {
 	for _, s := range getInfo(d.path) {
 		switch s {
 		case "size":
-			if f.IsDir() && gOpts.dircounts {
+			if f.IsDir() && getDirCounts(d.path) {
 				switch {
 				case f.dirCount < -1:
 					info.WriteString("    !")
```
</details>
thx, your implementation works perfectly. Hope you will merge this 

Fix patch:
diff --git a/complete.go b/complete.go
index 8317ecca..1e8a4a17 100644
--- a/complete.go
+++ b/complete.go
@@ -136,7 +136,7 @@ func getLocalOptWords(localOpts any) (localOptWords []string) {
 	t := reflect.TypeOf(localOpts)
 	for i := 0; i < t.NumField(); i++ {
 		field := t.Field(i)
-		name := strings.TrimSuffix(field.Name, "s")
+		name := field.Name
 		if field.Type.Kind() != reflect.Map {
 			continue
 		}
diff --git a/doc.md b/doc.md
index dd3e3dd1..90637399 100644
--- a/doc.md
+++ b/doc.md
@@ -1196,7 +1196,7 @@ Command `set` is used to set an option which can be a boolean, integer, or strin
 	set sortby "time"  # string value with double quotes (backslash escapes)
 
 Command `setlocal` is used to set a local option for a directory which can be a boolean or string.
-Currently supported local options are `dirfirst`, `dironly`, `hidden`, `info`, `reverse`, `sortby` and `locale`.
+Currently supported local options are `dircounts`, `dirfirst`, `dironly`, `hidden`, `info`, `reverse`, `sortby` and `locale`.
 Adding a trailing path separator (i.e. `/` for Unix and `\` for Windows) sets the option for the given directory along with its subdirectories:
 
 	setlocal /foo/bar hidden         # boolean enable
diff --git a/doc.txt b/doc.txt
index 5a97f2b5..72d9f2cd 100644
--- a/doc.txt
+++ b/doc.txt
@@ -1320,10 +1320,10 @@ string:
     set sortby "time"  # string value with double quotes (backslash escapes)
 
 Command setlocal is used to set a local option for a directory which can
-be a boolean or string. Currently supported local options are dirfirst,
-dironly, hidden, info, reverse, sortby and locale. Adding a trailing
-path separator (i.e. / for Unix and \ for Windows) sets the option for
-the given directory along with its subdirectories:
+be a boolean or string. Currently supported local options are dircounts,
+dirfirst, dironly, hidden, info, reverse, sortby and locale. Adding a
+trailing path separator (i.e. / for Unix and \ for Windows) sets the
+option for the given directory along with its subdirectories:
 
     setlocal /foo/bar hidden         # boolean enable
     setlocal /foo/bar hidden true    # boolean enable
diff --git a/eval.go b/eval.go
index e222e44d..b266975b 100644
--- a/eval.go
+++ b/eval.go
@@ -462,14 +462,16 @@ func (e *setLocalExpr) eval(app *app, args []string) {
 
 	var err error
 	switch e.opt {
+	case "dircounts", "nodircounts", "dircounts!":
+		err = applyLocalBoolOpt(gLocalOpts.dircounts, gOpts.dircounts, e)
 	case "dirfirst", "nodirfirst", "dirfirst!":
-		err = applyLocalBoolOpt(gLocalOpts.dirfirsts, gOpts.dirfirst, e)
+		err = applyLocalBoolOpt(gLocalOpts.dirfirst, gOpts.dirfirst, e)
 		if err == nil {
 			app.nav.sort()
 			app.ui.sort()
 		}
 	case "dironly", "nodironly", "dironly!":
-		err = applyLocalBoolOpt(gLocalOpts.dironlys, gOpts.dironly, e)
+		err = applyLocalBoolOpt(gLocalOpts.dironly, gOpts.dironly, e)
 		if err == nil {
 			app.nav.sort()
 			app.nav.position()
@@ -477,7 +479,7 @@ func (e *setLocalExpr) eval(app *app, args []string) {
 			app.ui.loadFile(app, true)
 		}
 	case "hidden", "nohidden", "hidden!":
-		err = applyLocalBoolOpt(gLocalOpts.hiddens, gOpts.hidden, e)
+		err = applyLocalBoolOpt(gLocalOpts.hidden, gOpts.hidden, e)
 		if err == nil {
 			app.nav.sort()
 			app.nav.position()
@@ -485,14 +487,14 @@ func (e *setLocalExpr) eval(app *app, args []string) {
 			app.ui.loadFile(app, true)
 		}
 	case "reverse", "noreverse", "reverse!":
-		err = applyLocalBoolOpt(gLocalOpts.reverses, gOpts.reverse, e)
+		err = applyLocalBoolOpt(gLocalOpts.reverse, gOpts.reverse, e)
 		if err == nil {
 			app.nav.sort()
 			app.ui.sort()
 		}
 	case "info":
 		if e.val == "" {
-			gLocalOpts.infos[e.path] = nil
+			gLocalOpts.info[e.path] = nil
 			return
 		}
 		toks := strings.Split(e.val, ":")
@@ -504,14 +506,14 @@ func (e *setLocalExpr) eval(app *app, args []string) {
 				return
 			}
 		}
-		gLocalOpts.infos[e.path] = toks
+		gLocalOpts.info[e.path] = toks
 	case "sortby":
 		method := sortMethod(e.val)
 		if !isValidSortMethod(method) {
 			app.ui.echoerr(invalidSortErrorMessage)
 			return
 		}
-		gLocalOpts.sortbys[e.path] = method
+		gLocalOpts.sortby[e.path] = method
 		app.nav.sort()
 		app.ui.sort()
 	case "locale":
@@ -522,7 +524,7 @@ func (e *setLocalExpr) eval(app *app, args []string) {
 				return
 			}
 		}
-		gLocalOpts.locales[e.path] = localeStr
+		gLocalOpts.locale[e.path] = localeStr
 		app.nav.sort()
 		app.ui.sort()
 	default:
diff --git a/lf.1 b/lf.1
index 94a83251..8ae32bfd 100644
--- a/lf.1
+++ b/lf.1
@@ -1,6 +1,6 @@
 .\" Automatically generated by Pandoc 2.11.4
 .\"
-.TH "LF" "1" "2025-02-09" "" "DOCUMENTATION"
+.TH "LF" "1" "2025-02-18" "" "DOCUMENTATION"
 .hy
 .SH NAME
 .PP
@@ -1329,9 +1329,9 @@ set sortby \[dq]time\[dq]  # string value with double quotes (backslash escapes)
 .PP
 Command \f[C]setlocal\f[R] is used to set a local option for a directory
 which can be a boolean or string.
-Currently supported local options are \f[C]dirfirst\f[R],
-\f[C]dironly\f[R], \f[C]hidden\f[R], \f[C]info\f[R], \f[C]reverse\f[R],
-\f[C]sortby\f[R] and \f[C]locale\f[R].
+Currently supported local options are \f[C]dircounts\f[R],
+\f[C]dirfirst\f[R], \f[C]dironly\f[R], \f[C]hidden\f[R], \f[C]info\f[R],
+\f[C]reverse\f[R], \f[C]sortby\f[R] and \f[C]locale\f[R].
 Adding a trailing path separator (i.e.
 \f[C]/\f[R] for Unix and \f[C]\[rs]\f[R] for Windows) sets the option
 for the given directory along with its subdirectories:
diff --git a/nav.go b/nav.go
index 940de91b..08f1f9ae 100644
--- a/nav.go
+++ b/nav.go
@@ -88,7 +88,7 @@ func newFile(path string) *file {
 	}
 
 	dirCount := -1
-	if lstat.IsDir() && gOpts.dircounts {
+	if lstat.IsDir() && getDirCounts(filepath.Dir(path)) {
 		d, err := os.Open(path)
 		if err != nil {
 			dirCount = -2
diff --git a/opts.go b/opts.go
index 9d1211d2..32b36b19 100644
--- a/opts.go
+++ b/opts.go
@@ -107,13 +107,14 @@ var gOpts struct {
 }
 
 var gLocalOpts struct {
-	sortbys   map[string]sortMethod
-	dirfirsts map[string]bool
-	dironlys  map[string]bool
-	hiddens   map[string]bool
-	reverses  map[string]bool
-	infos     map[string][]string
-	locales   map[string]string
+	sortby    map[string]sortMethod
+	dircounts map[string]bool
+	dirfirst  map[string]bool
+	dironly   map[string]bool
+	hidden    map[string]bool
+	reverse   map[string]bool
+	info      map[string][]string
+	locale    map[string]string
 }
 
 func localOptPaths(path string) []string {
@@ -124,9 +125,18 @@ func localOptPaths(path string) []string {
 	return list
 }
 
+func getDirCounts(path string) bool {
+	for _, key := range localOptPaths(path) {
+		if val, ok := gLocalOpts.dircounts[key]; ok {
+			return val
+		}
+	}
+	return gOpts.dircounts
+}
+
 func getDirFirst(path string) bool {
 	for _, key := range localOptPaths(path) {
-		if val, ok := gLocalOpts.dirfirsts[key]; ok {
+		if val, ok := gLocalOpts.dirfirst[key]; ok {
 			return val
 		}
 	}
@@ -135,7 +145,7 @@ func getDirFirst(path string) bool {
 
 func getDirOnly(path string) bool {
 	for _, key := range localOptPaths(path) {
-		if val, ok := gLocalOpts.dironlys[key]; ok {
+		if val, ok := gLocalOpts.dironly[key]; ok {
 			return val
 		}
 	}
@@ -144,7 +154,7 @@ func getDirOnly(path string) bool {
 
 func getHidden(path string) bool {
 	for _, key := range localOptPaths(path) {
-		if val, ok := gLocalOpts.hiddens[key]; ok {
+		if val, ok := gLocalOpts.hidden[key]; ok {
 			return val
 		}
 	}
@@ -153,7 +163,7 @@ func getHidden(path string) bool {
 
 func getInfo(path string) []string {
 	for _, key := range localOptPaths(path) {
-		if val, ok := gLocalOpts.infos[key]; ok {
+		if val, ok := gLocalOpts.info[key]; ok {
 			return val
 		}
 	}
@@ -162,7 +172,7 @@ func getInfo(path string) []string {
 
 func getReverse(path string) bool {
 	for _, key := range localOptPaths(path) {
-		if val, ok := gLocalOpts.reverses[key]; ok {
+		if val, ok := gLocalOpts.reverse[key]; ok {
 			return val
 		}
 	}
@@ -171,7 +181,7 @@ func getReverse(path string) bool {
 
 func getSortBy(path string) sortMethod {
 	for _, key := range localOptPaths(path) {
-		if val, ok := gLocalOpts.sortbys[key]; ok {
+		if val, ok := gLocalOpts.sortby[key]; ok {
 			return val
 		}
 	}
@@ -180,7 +190,7 @@ func getSortBy(path string) sortMethod {
 
 func getLocale(path string) string {
 	for _, key := range localOptPaths(path) {
-		if val, ok := gLocalOpts.locales[key]; ok {
+		if val, ok := gLocalOpts.locale[key]; ok {
 			return val
 		}
 	}
@@ -376,13 +386,14 @@ func init() {
 	gOpts.cmds = make(map[string]expr)
 	gOpts.user = make(map[string]string)
 
-	gLocalOpts.sortbys = make(map[string]sortMethod)
-	gLocalOpts.dirfirsts = make(map[string]bool)
-	gLocalOpts.dironlys = make(map[string]bool)
-	gLocalOpts.hiddens = make(map[string]bool)
-	gLocalOpts.reverses = make(map[string]bool)
-	gLocalOpts.infos = make(map[string][]string)
-	gLocalOpts.locales = make(map[string]string)
+	gLocalOpts.sortby = make(map[string]sortMethod)
+	gLocalOpts.dircounts = make(map[string]bool)
+	gLocalOpts.dirfirst = make(map[string]bool)
+	gLocalOpts.dironly = make(map[string]bool)
+	gLocalOpts.hidden = make(map[string]bool)
+	gLocalOpts.reverse = make(map[string]bool)
+	gLocalOpts.info = make(map[string][]string)
+	gLocalOpts.locale = make(map[string]string)
 
 	setDefaults()
 }
diff --git a/ui.go b/ui.go
index 4c85aecd..0d678583 100644
--- a/ui.go
+++ b/ui.go
@@ -295,7 +295,7 @@ func fileInfo(f *file, d *dir, userWidth int, groupWidth int) string {
 	for _, s := range getInfo(d.path) {
 		switch s {
 		case "size":
-			if f.IsDir() && gOpts.dircounts {
+			if f.IsDir() && getDirCounts(d.path) {
 				switch {
 				case f.dirCount < -1:
 					info.WriteString("    !")


Test patch:
diff --git a/complete_test.go b/complete_test.go
index 51896b97..58f45327 100644
--- a/complete_test.go
+++ b/complete_test.go
@@ -79,10 +79,10 @@ func TestGetLocalOptWords(t *testing.T) {
 		localOpts any
 		exp       []string
 	}{
-		{struct{ features map[string]bool }{}, []string{"feature", "feature!", "nofeature"}},
-		{struct{ features map[string]int }{}, []string{"feature"}},
-		{struct{ features map[string]string }{}, []string{"feature"}},
-		{struct{ features map[string][]string }{}, []string{"feature"}},
+		{struct{ feature map[string]bool }{}, []string{"feature", "feature!", "nofeature"}},
+		{struct{ feature map[string]int }{}, []string{"feature"}},
+		{struct{ feature map[string]string }{}, []string{"feature"}},
+		{struct{ feature map[string][]string }{}, []string{"feature"}},
 	}
 
 	for _, test := range tests {