Repository: gokcehan/lf
Pull request #1286 (2023-06-04 03:44:09)

Add `clearmaps` command
- cc #1161 

Add a `clearmap` command for clearing the current `maps`. See https://github.com/gokcehan/lf/issues/1161#issuecomment-1574654016 and below for more details.


Patch:
diff --git a/app.go b/app.go
index f05a7a3..4bfaea6 100644
--- a/app.go
+++ b/app.go
@@ -602,26 +602,16 @@ func (app *app) runShell(s string, args []string, prefix string) {
 
 }
 
-func (app *app) runPagerOnText(text io.Reader) {
+func (app *app) runPagerOn(stdin io.Reader) {
 	app.nav.exportFiles()
 	app.ui.exportSizes()
 	exportOpts()
 
 	cmd := shellCommand(envPager, nil)
 
+	cmd.Stdin = stdin
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
 
-	stdin, err := cmd.StdinPipe()
-	if err != nil {
-		app.ui.echoerrf("obtaining stdin pipe: %s", err)
-		return
-	}
-
-	go func() {
-		io.Copy(stdin, text)
-		stdin.Close()
-	}()
-
 	app.runCmdSync(cmd, false)
 }
diff --git a/complete.go b/complete.go
index d2e04dc..024da3a 100644
--- a/complete.go
+++ b/complete.go
@@ -16,6 +16,7 @@ var (
 		"cmap",
 		"cmaps",
 		"cmd",
+		"cmds",
 		"quit",
 		"up",
 		"half-up",
@@ -42,6 +43,7 @@ var (
 		"glob-select",
 		"glob-unselect",
 		"calcdirsize",
+		"clearmaps",
 		"copy",
 		"cut",
 		"paste",
diff --git a/doc.go b/doc.go
index 096cf5f..bdafebe 100644
--- a/doc.go
+++ b/doc.go
@@ -41,6 +41,7 @@ The following commands are provided by lf:
 	glob-select
 	glob-unselect
 	calcdirsize
+	clearmaps
 	copy                     (default 'y')
 	cut                      (default 'd')
 	paste                    (default 'p')
@@ -81,6 +82,7 @@ The following commands are provided by lf:
 	tag-toggle               (default 't')
 	maps
 	cmaps
+	cmds
 	jumps
 
 The following command line commands are provided by lf:
@@ -208,11 +210,13 @@ The following special shell commands are used to customize the behavior of lf wh
 
 The following commands/keybindings are provided by default:
 
-	Unix                     Windows
-	cmd open &$OPENER "$f"   cmd open &%OPENER% %f%
-	map e $$EDITOR "$f"      map e $%EDITOR% %f%
-	map i $$PAGER "$f"       map i !%PAGER% %f%
-	map w $$SHELL            map w $%SHELL%
+	Unix                          Windows
+	cmd open &$OPENER "$f"        cmd open &%OPENER% %f%
+	map e $$EDITOR "$f"           map e $%EDITOR% %f%
+	map i $$PAGER "$f"            map i !%PAGER% %f%
+	map w $$SHELL                 map w $%SHELL%
+	cmd doc $$lf -doc | $PAGER    cmd doc !%lf% -doc | %PAGER%
+	map <f-1> doc                 map <f-1> doc
 
 The following additional keybindings are provided by default:
 
@@ -379,6 +383,12 @@ Calculate the total size for each of the selected directories.
 Option 'info' should include 'size' and option 'dircounts' should be disabled to show this size.
 If the total size of a directory is not calculated, it will be shown as '-'.
 
+	clearmaps
+
+Remove all keybindings associated with the `map` command.
+This command can be used in the config file to remove the default keybindings.
+For safety purposes, `:` is left mapped to the `read` command, and `cmap` keybindings are retained so that it is still possible to exit `lf` using `:quit`.
+
 	copy                     (default 'y')
 
 If there are no selections, save the path of the current file to the copy buffer, otherwise, copy the paths of selected files.
@@ -620,6 +630,10 @@ Capitalize/uppercase/lowercase the current word and jump to the next word.
 
 List all key mappings in normal mode or command-line editing mode.
 
+	cmds
+
+List all custom commands defined using the `cmd` command
+
 	jumps
 
 List the contents of the jump list, in order of the most recently visited locations.
diff --git a/docstring.go b/docstring.go
index f716670..8947671 100644
--- a/docstring.go
+++ b/docstring.go
@@ -44,6 +44,7 @@ The following commands are provided by lf:
     glob-select
     glob-unselect
     calcdirsize
+    clearmaps
     copy                     (default 'y')
     cut                      (default 'd')
     paste                    (default 'p')
@@ -84,6 +85,7 @@ The following commands are provided by lf:
     tag-toggle               (default 't')
     maps
     cmaps
+    cmds
     jumps
 
 The following command line commands are provided by lf:
@@ -212,11 +214,13 @@ when defined:
 
 The following commands/keybindings are provided by default:
 
-    Unix                     Windows
-    cmd open &$OPENER "$f"   cmd open &%OPENER% %f%
-    map e $$EDITOR "$f"      map e $%EDITOR% %f%
-    map i $$PAGER "$f"       map i !%PAGER% %f%
-    map w $$SHELL            map w $%SHELL%
+    Unix                          Windows
+    cmd open &$OPENER "$f"        cmd open &%OPENER% %f%
+    map e $$EDITOR "$f"           map e $%EDITOR% %f%
+    map i $$PAGER "$f"            map i !%PAGER% %f%
+    map w $$SHELL                 map w $%SHELL%
+    cmd doc $$lf -doc | $PAGER    cmd doc !%lf% -doc | %PAGER%
+    map <f-1> doc                 map <f-1> doc
 
 The following additional keybindings are provided by default:
 
@@ -395,6 +399,13 @@ should include 'size' and option 'dircounts' should be disabled to show this
 size. If the total size of a directory is not calculated, it will be shown as
 '-'.
 
+    clearmaps
+
+Remove all keybindings associated with the 'map' command. This command can be
+used in the config file to remove the default keybindings. For safety purposes,
+':' is left mapped to the 'read' command, and 'cmap' keybindings are retained so
+that it is still possible to exit 'lf' using ':quit'.
+
     copy                     (default 'y')
 
 If there are no selections, save the path of the current file to the copy
@@ -648,6 +659,10 @@ Capitalize/uppercase/lowercase the current word and jump to the next word.
 
 List all key mappings in normal mode or command-line editing mode.
 
+    cmds
+
+List all custom commands defined using the 'cmd' command
+
     jumps
 
 List the contents of the jump list, in order of the most recently visited
diff --git a/etc/icons.example b/etc/icons.example
index 8039c93..404f9e5 100644
--- a/etc/icons.example
+++ b/etc/icons.example
@@ -87,7 +87,7 @@ fi             # FILE
 *.cc            
 *.cp            
 *.c             
-*.cs            
+*.cs            󰌛
 *.h             
 *.hh            
 *.hpp           
@@ -152,10 +152,10 @@ fi             # FILE
 *.elm           
 *.swift         
 *.xcplayground  
-*.tex           ﭨ
-*.r             ﳒ
-*.rproj         鉶
-*.sol           ﲹ
+*.tex           󰙩
+*.r             󰟔
+*.rproj         󰗆
+*.sol           󰡪
 *.pem           
 
 # file names (vim-devicons) (case-insensitive not supported in lf)
@@ -193,7 +193,7 @@ fi             # FILE
 *gemfile                
 *makefile               
 *cmakelists.txt         
-*robots.txt             ﮧ
+*robots.txt             󰚩
 
 # file names (case-sensitive adaptations)
 *Gruntfile.coffee       
diff --git a/eval.go b/eval.go
index 0ddf107..048ee92 100644
--- a/eval.go
+++ b/eval.go
@@ -1614,6 +1614,10 @@ func (e *callExpr) eval(app *app, args []string) {
 		app.ui.loadFileInfo(app.nav)
 		app.nav.sort()
 		app.ui.sort()
+	case "clearmaps":
+		// leave `:` and cmaps bound so the user can still exit using `:quit`
+		gOpts.keys = make(map[string]expr)
+		gOpts.keys[":"] = &callExpr{"read", nil, 1}
 	case "copy":
 		if !app.nav.init {
 			return
@@ -2244,64 +2248,60 @@ func (e *callExpr) eval(app *app, args []string) {
 			}
 		case "rename: ":
 			app.ui.cmdPrefix = ""
-			if curr, err := app.nav.currFile(); err != nil {
-				app.ui.echoerrf("rename: %s", err)
-			} else {
-				wd, err := os.Getwd()
-				if err != nil {
-					log.Printf("getting current directory: %s", err)
-					return
-				}
-
-				oldPath := filepath.Join(wd, curr.Name())
-
-				newPath := filepath.Clean(replaceTilde(s))
-				if !filepath.IsAbs(newPath) {
-					newPath = filepath.Join(wd, newPath)
-				}
 
-				if oldPath == newPath {
-					return
-				}
+			curr, err := app.nav.currFile()
+			if err != nil {
+				app.ui.echoerrf("rename: %s", err)
+				return
+			}
+			wd, err := os.Getwd()
+			if err != nil {
+				log.Printf("getting current directory: %s", err)
+				return
+			}
 
-				app.nav.renameOldPath = oldPath
-				app.nav.renameNewPath = newPath
+			oldPath := filepath.Join(wd, curr.Name())
+			newPath := filepath.Clean(replaceTilde(s))
+			if !filepath.IsAbs(newPath) {
+				newPath = filepath.Join(wd, newPath)
+			}
+			if oldPath == newPath {
+				return
+			}
+			app.nav.renameOldPath = oldPath
+			app.nav.renameNewPath = newPath
 
-				newDir := filepath.Dir(newPath)
-				if _, err := os.Stat(newDir); os.IsNotExist(err) {
-					app.ui.cmdPrefix = "create '" + newDir + "' ? [y/N] "
-					return
-				}
+			newDir := filepath.Dir(newPath)
+			if _, err := os.Stat(newDir); os.IsNotExist(err) {
+				app.ui.cmdPrefix = "create '" + newDir + "' ? [y/N] "
+				return
+			}
 
-				oldStat, err := os.Lstat(oldPath)
-				if err != nil {
-					app.ui.echoerrf("rename: %s", err)
-					return
-				}
+			oldStat, err := os.Lstat(oldPath)
+			if err != nil {
+				app.ui.echoerrf("rename: %s", err)
+				return
+			}
+			if newStat, err := os.Lstat(newPath); !os.IsNotExist(err) && !os.SameFile(oldStat, newStat) {
+				app.ui.cmdPrefix = "replace '" + newPath + "' ? [y/N] "
+				return
+			}
 
-				if newStat, err := os.Lstat(newPath); !os.IsNotExist(err) && !os.SameFile(oldStat, newStat) {
-					app.ui.cmdPrefix = "replace '" + newPath + "' ? [y/N] "
-					return
-				}
+			if err := app.nav.rename(); err != nil {
+				app.ui.echoerrf("rename: %s", err)
+				return
+			}
 
-				if err := app.nav.rename(); err != nil {
+			if gSingleMode {
+				app.nav.renew()
+			} else {
+				if err := remote("send load"); err != nil {
 					app.ui.echoerrf("rename: %s", err)
 					return
 				}
-
-				if gSingleMode {
-					app.nav.renew()
-					app.ui.loadFile(app, true)
-				} else {
-					if err := remote("send load"); err != nil {
-						app.ui.echoerrf("rename: %s", err)
-						return
-					}
-				}
-
-				app.ui.loadFile(app, true)
-				app.ui.loadFileInfo(app.nav)
 			}
+			app.ui.loadFile(app, true)
+			app.ui.loadFileInfo(app.nav)
 		default:
 			log.Printf("entering unknown execution prefix: %q", app.ui.cmdPrefix)
 		}
@@ -2347,9 +2347,10 @@ func (e *callExpr) eval(app *app, args []string) {
 			switch app.ui.cmdPrefix {
 			case "!", "$", "%", "&":
 				app.ui.cmdPrefix = ":"
-			case ">", "rename: ":
+			case ">", "rename: ", "filter: ":
 				// Don't mess with programs waiting for input.
-				// Exiting on backspace is also inconvenient for renames since the text field starts out nonempty.
+				// Exiting on backspace is also inconvenient for 'rename' and 'filter',
+				// since the text field can start out nonempty.
 			default:
 				normal(app)
 			}
@@ -2525,12 +2526,14 @@ func (e *callExpr) eval(app *app, args []string) {
 
 		app.ui.cmdAccLeft = acc
 		update(app)
+	case "cmds":
+		app.runPagerOn(listCmds())
 	case "maps":
-		app.runPagerOnText(listBinds(gOpts.keys))
+		app.runPagerOn(listBinds(gOpts.keys))
 	case "cmaps":
-		app.runPagerOnText(listBinds(gOpts.cmdkeys))
+		app.runPagerOn(listBinds(gOpts.cmdkeys))
 	case "jumps":
-		app.runPagerOnText(listJumps(app.nav.jumpList, app.nav.jumpListInd))
+		app.runPagerOn(listJumps(app.nav.jumpList, app.nav.jumpListInd))
 	default:
 		cmd, ok := gOpts.cmds[e.name]
 		if !ok {
diff --git a/lf.1 b/lf.1
index 6de2b89..7b59b1c 100644
--- a/lf.1
+++ b/lf.1
@@ -56,6 +56,7 @@ The following commands are provided by lf:
     glob-select
     glob-unselect
     calcdirsize
+    clearmaps
     copy                     (default 'y')
     cut                      (default 'd')
     paste                    (default 'p')
@@ -96,6 +97,7 @@ The following commands are provided by lf:
     tag-toggle               (default 't')
     maps
     cmaps
+    cmds
     jumps
 .EE
 .PP
@@ -233,11 +235,13 @@ The following special shell commands are used to customize the behavior of lf wh
 The following commands/keybindings are provided by default:
 .PP
 .EX
-    Unix                     Windows
-    cmd open &$OPENER "$f"   cmd open &%OPENER% %f%
-    map e $$EDITOR "$f"      map e $%EDITOR% %f%
-    map i $$PAGER "$f"       map i !%PAGER% %f%
-    map w $$SHELL            map w $%SHELL%
+    Unix                          Windows
+    cmd open &$OPENER "$f"        cmd open &%OPENER% %f%
+    map e $$EDITOR "$f"           map e $%EDITOR% %f%
+    map i $$PAGER "$f"            map i !%PAGER% %f%
+    map w $$SHELL                 map w $%SHELL%
+    cmd doc $$lf -doc | $PAGER    cmd doc !%lf% -doc | %PAGER%
+    map <f-1> doc                 map <f-1> doc
 .EE
 .PP
 The following additional keybindings are provided by default:
@@ -441,6 +445,12 @@ Select/unselect files that match the given glob.
 .PP
 Calculate the total size for each of the selected directories. Option 'info' should include 'size' and option 'dircounts' should be disabled to show this size. If the total size of a directory is not calculated, it will be shown as '-'.
 .PP
+.EX
+    clearmaps
+.EE
+.PP
+Remove all keybindings associated with the `map` command. This command can be used in the config file to remove the default keybindings. For safety purposes, `:` is left mapped to the `read` command, and `cmap` keybindings are retained so that it is still possible to exit `lf` using `:quit`.
+.PP
 .EX
     copy                     (default 'y')
 .EE
@@ -764,6 +774,12 @@ Capitalize/uppercase/lowercase the current word and jump to the next word.
 .PP
 List all key mappings in normal mode or command-line editing mode.
 .PP
+.EX
+    cmds
+.EE
+.PP
+List all custom commands defined using the `cmd` command
+.PP
 .EX
     jumps
 .EE
diff --git a/misc.go b/misc.go
index ea33664..b0303ea 100644
--- a/misc.go
+++ b/misc.go
@@ -31,23 +31,26 @@ func runeSliceWidth(rs []rune) int {
 }
 
 func runeSliceWidthRange(rs []rune, beg, end int) []rune {
+	if beg == end {
+		return []rune{}
+	}
+
 	curr := 0
 	b := 0
+	foundb := false
 	for i, r := range rs {
 		w := runewidth.RuneWidth(r)
-		switch {
-		case curr == beg:
+		if curr >= beg && !foundb {
 			b = i
-		case curr < beg && curr+w > beg:
-			b = i + 1
-		case curr == end:
+			foundb = true
+		}
+		if curr == end || curr+w > end {
 			return rs[b:i]
-		case curr > end:
-			return rs[b : i-1]
 		}
 		curr += w
 	}
-	return nil
+
+	return rs[b:]
 }
 
 // This function is used to escape whitespaces and special characters with
diff --git a/ui.go b/ui.go
index 4c01391..1f8af2c 100644
--- a/ui.go
+++ b/ui.go
@@ -441,8 +441,16 @@ func (win *win) printDir(screen tcell.Screen, dir *dir, context *dirContext, dir
 			}
 		}
 
+		// make space for select marker, and leave another space at the end
+		maxWidth := win.w - lnwidth - 2
+		// make extra space to separate windows if drawbox is not enabled
+		if !gOpts.drawbox {
+			maxWidth -= 1
+		}
+
 		var s []rune
 
+		// leave space for displaying the tag
 		s = append(s, ' ')
 
 		if gOpts.icons {
@@ -450,40 +458,26 @@ func (win *win) printDir(screen tcell.Screen, dir *dir, context *dirContext, dir
 			s = append(s, ' ')
 		}
 
-		for _, r := range f.Name() {
-			s = append(s, r)
-		}
-
-		w := runeSliceWidth(s)
+		maxFilenameWidth := maxWidth - runeSliceWidth(s)
 
-		// make space for select marker, and leave another space at the end
-		maxlength := win.w - lnwidth - 2
-		// make extra space to separate windows if drawbox is not enabled
-		if !gOpts.drawbox {
-			maxlength -= 1
+		info := fileInfo(f, dir)
+		showInfo := len(info) > 0 && 2*len(info) < maxWidth
+		if showInfo {
+			maxFilenameWidth -= len(info)
 		}
 
-		if w > maxlength {
-			s = runeSliceWidthRange(s, 0, maxlength-1)
-			s = append(s, []rune(gOpts.truncatechar)...)
-		} else {
-			for i := 0; i < maxlength-w; i++ {
-				s = append(s, ' ')
-			}
+		filename := []rune(f.Name())
+		if runeSliceWidth(filename) > maxFilenameWidth {
+			filename = runeSliceWidthRange(filename, 0, maxFilenameWidth-1)
+			filename = append(filename, []rune(gOpts.truncatechar)...)
 		}
+		for i := runeSliceWidth(filename); i < maxFilenameWidth; i++ {
+			filename = append(filename, ' ')
+		}
+		s = append(s, filename...)
 
-		info := fileInfo(f, dir)
-
-		if len(info) > 0 && 2*len(info) < maxlength {
-			if w+len(info) > maxlength {
-				s = runeSliceWidthRange(s, 0, maxlength-len(info)-1)
-				s = append(s, []rune(gOpts.truncatechar)...)
-			} else {
-				s = runeSliceWidthRange(s, 0, maxlength-len(info))
-			}
-			for _, r := range info {
-				s = append(s, r)
-			}
+		if showInfo {
+			s = append(s, []rune(info)...)
 		}
 
 		ce := ""
@@ -1052,7 +1046,7 @@ func findBinds(keys map[string]expr, prefix string) (binds map[string]expr, ok b
 	return
 }
 
-func listBinds(binds map[string]expr) *bytes.Buffer {
+func listExprMap(binds map[string]expr, title string) *bytes.Buffer {
 	t := new(tabwriter.Writer)
 	b := new(bytes.Buffer)
 
@@ -1063,7 +1057,7 @@ func listBinds(binds map[string]expr) *bytes.Buffer {
 	sort.Strings(keys)
 
 	t.Init(b, 0, gOpts.tabstop, 2, '\t', 0)
-	fmt.Fprintln(t, "keys\tcommand")
+	fmt.Fprintf(t, "%s\tcommand\n", title)
 	for _, k := range keys {
 		fmt.Fprintf(t, "%s\t%v\n", k, binds[k])
 	}
@@ -1072,6 +1066,14 @@ func listBinds(binds map[string]expr) *bytes.Buffer {
 	return b
 }
 
+func listBinds(binds map[string]expr) *bytes.Buffer {
+	return listExprMap(binds, "keys")
+}
+
+func listCmds() *bytes.Buffer {
+	return listExprMap(gOpts.cmds, "name")
+}
+
 func listJumps(jumps []string, ind int) *bytes.Buffer {
 	t := new(tabwriter.Writer)
 	b := new(bytes.Buffer)


Test patch:
diff --git a/misc_test.go b/misc_test.go
index 3568fa2..aa6f6df 100644
--- a/misc_test.go
+++ b/misc_test.go
@@ -55,21 +55,30 @@ func TestRuneSliceWidthRange(t *testing.T) {
 		end int
 		exp []rune
 	}{
+		{[]rune{}, 0, 0, []rune{}},
 		{[]rune{'a', 'b', 'c', 'd'}, 1, 3, []rune{'b', 'c'}},
 		{[]rune{'a', 'ı', 'b', 'ş'}, 1, 3, []rune{'ı', 'b'}},
 		{[]rune{'世', '界', '世', '界'}, 2, 6, []rune{'界', '世'}},
 		{[]rune{'世', '界', '世', '界'}, 3, 6, []rune{'世'}},
 		{[]rune{'世', '界', '世', '界'}, 2, 5, []rune{'界'}},
 		{[]rune{'世', '界', '世', '界'}, 3, 5, []rune{}},
+		{[]rune{'世', '界', '世', '界'}, 4, 4, []rune{}},
+		{[]rune{'世', '界', '世', '界'}, 5, 5, []rune{}},
+		{[]rune{'世', '界', '世', '界'}, 4, 7, []rune{'世'}},
+		{[]rune{'世', '界', '世', '界'}, 4, 8, []rune{'世', '界'}},
 		{[]rune{'世', 'a', '界', 'ı'}, 2, 5, []rune{'a', '界'}},
 		{[]rune{'世', 'a', '界', 'ı'}, 2, 4, []rune{'a'}},
 		{[]rune{'世', 'a', '界', 'ı'}, 3, 5, []rune{'界'}},
 		{[]rune{'世', 'a', '界', 'ı'}, 3, 4, []rune{}},
+		{[]rune{'世', 'a', '界', 'ı'}, 3, 3, []rune{}},
+		{[]rune{'世', 'a', '界', 'ı'}, 4, 4, []rune{}},
+		{[]rune{'世', 'a', '界', 'ı'}, 4, 6, []rune{'ı'}},
+		{[]rune{'世', 'a', '界', 'ı'}, 5, 6, []rune{'ı'}},
 	}
 
 	for _, test := range tests {
 		if got := runeSliceWidthRange(test.rs, test.beg, test.end); !reflect.DeepEqual(got, test.exp) {
-			t.Errorf("at input '%v' expected '%v' but got '%v'", test.rs, test.rs, got)
+			t.Errorf("at input '%v' expected '%v' but got '%v'", test.rs, test.exp, got)
 		}
 	}
 }