Repository: gokcehan/lf
Pull request #849 (2022-06-01 19:53:08)

Add a selection mode
When working with lf I prefer to have it use only the selections I made in the current directory.

```
    selmode        string    (default 'all')

Selection mode for commands.
When set to 'all' it will use the selected files from all directories.
When set to 'dir' it will only use the selected files in the current directory.
```

Use case: I often forget to clear selections before selecting files in the current directory and have to start over or (worst case) deleted some files by mistake.


rafcon-dev: This is sorely needed, I just tested this, wasn't even aware of this weird behaviour, selection mode should be 'dir' by default indeed. This can really lead to some really nasty surprises.
laktak: @rafcon-dev as I understand it gokcehan is busy with things in real life ... just fyi because I saw you commented on other issues as well.


rafcon-dev: Thank you for the fyi mate. Yeah, such is life indeed, hopefully they'll find some time once things are sorted out!

Patch:
diff --git a/README.md b/README.md
index 70e705a..4e4e19d 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ See [faq](https://github.com/gokcehan/lf/wiki/FAQ) for more information and [tut
 
 ## Features
 
-- Cross-platform (Linux, MacOS, BSDs, Windows)
+- Cross-platform (Linux, macOS, BSDs, Windows)
 - Single binary without any runtime dependencies
 - Fast startup and low memory footprint due to native code and static binaries
 - Asynchronous IO operations to avoid UI locking
diff --git a/app.go b/app.go
index 5ccab80..b77d146 100644
--- a/app.go
+++ b/app.go
@@ -239,6 +239,7 @@ func (app *app) writeHistory() error {
 // separate goroutines and sent here for update.
 func (app *app) loop() {
 	go app.nav.previewLoop(app.ui)
+	go app.nav.dirPreviewLoop(app.ui)
 
 	var serverChan <-chan expr
 	if !gSingleMode {
@@ -320,6 +321,7 @@ func (app *app) loop() {
 			app.quit()
 
 			app.nav.previewChan <- ""
+			app.nav.dirPreviewChan <- nil
 
 			log.Print("bye!")
 
@@ -384,6 +386,7 @@ func (app *app) loop() {
 			}
 			app.ui.draw(app.nav)
 		case d := <-app.nav.dirChan:
+
 			app.nav.checkDir(d)
 
 			if gOpts.dircache {
@@ -484,6 +487,8 @@ func (app *app) runShell(s string, args []string, prefix string) {
 		cmd.Stderr = os.Stderr
 
 		app.nav.previewChan <- ""
+		app.nav.dirPreviewChan <- nil
+
 		if err := app.ui.suspend(); err != nil {
 			log.Printf("suspend: %s", err)
 		}
diff --git a/colors.go b/colors.go
index 949a98e..e76e5d8 100644
--- a/colors.go
+++ b/colors.go
@@ -110,6 +110,8 @@ func applyAnsiCodes(s string, st tcell.Style) tcell.Style {
 			st = st.Bold(true)
 		case n == 2:
 			st = st.Dim(true)
+		case n == 3:
+			st = st.Italic(true)
 		case n == 4:
 			st = st.Underline(true)
 		case n == 5 || n == 6:
@@ -319,7 +321,7 @@ func (sm styleMap) get(f *file) tcell.Style {
 		return val
 	}
 
-	if val, ok := sm["*"+f.ext]; ok {
+	if val, ok := sm["*"+strings.ToLower(f.ext)]; ok {
 		return val
 	}
 
diff --git a/complete.go b/complete.go
index cad63f8..c3b965e 100644
--- a/complete.go
+++ b/complete.go
@@ -77,6 +77,33 @@ var (
 		"mark-remove",
 		"tag",
 		"tag-toggle",
+		"cmd-escape",
+		"cmd-complete",
+		"cmd-menu-complete",
+		"cmd-menu-complete-back",
+		"cmd-menu-accept",
+		"cmd-enter",
+		"cmd-interrupt",
+		"cmd-history-next",
+		"cmd-history-prev",
+		"cmd-left",
+		"cmd-right",
+		"cmd-home",
+		"cmd-end",
+		"cmd-delete",
+		"cmd-delete-back",
+		"cmd-delete-home",
+		"cmd-delete-end",
+		"cmd-delete-unix-word",
+		"cmd-yank",
+		"cmd-transpose",
+		"cmd-transpose-word",
+		"cmd-word",
+		"cmd-word-back",
+		"cmd-delete-word",
+		"cmd-capitalize-word",
+		"cmd-uppercase-word",
+		"cmd-lowercase-word",
 	}
 
 	gOptWords = []string{
@@ -96,6 +123,7 @@ var (
 		"nodirfirst",
 		"dirfirst!",
 		"dironly",
+		"dirpreviews",
 		"nodironly",
 		"dironly!",
 		"drawbox",
@@ -164,6 +192,7 @@ var (
 		"cleaner",
 		"promptfmt",
 		"ratios",
+		"selmode",
 		"shell",
 		"shellflag",
 		"shellopts",
@@ -177,7 +206,7 @@ var (
 	}
 )
 
-func matchLongest(s1, s2 string) string {
+func matchLongest(s1, s2 []rune) []rune {
 	i := 0
 	for ; i < len(s1) && i < len(s2); i++ {
 		if s1[i] != s2[i] {
@@ -187,28 +216,28 @@ func matchLongest(s1, s2 string) string {
 	return s1[:i]
 }
 
-func matchWord(s string, words []string) (matches []string, longest string) {
+func matchWord(s string, words []string) (matches []string, longest []rune) {
 	for _, w := range words {
 		if !strings.HasPrefix(w, s) {
 			continue
 		}
 
 		matches = append(matches, w)
-		if longest != "" {
-			longest = matchLongest(longest, w)
+		if len(longest) != 0 {
+			longest = matchLongest(longest, []rune(w))
 		} else if s != "" {
-			longest = w + " "
+			longest = []rune(w + " ")
 		}
 	}
 
-	if longest == "" {
-		longest = s
+	if len(longest) == 0 {
+		longest = []rune(s)
 	}
 
 	return
 }
 
-func matchExec(s string) (matches []string, longest string) {
+func matchExec(s string) (matches []string, longest []rune) {
 	var words []string
 
 	paths := strings.Split(envPath, string(filepath.ListSeparator))
@@ -258,7 +287,8 @@ func matchExec(s string) (matches []string, longest string) {
 	return matchWord(s, words)
 }
 
-func matchFile(s string) (matches []string, longest string) {
+func matchFile(s string) (matches []string, longest []rune) {
+	s = unescape(s)
 	dir := replaceTilde(s)
 
 	if !filepath.IsAbs(dir) {
@@ -270,7 +300,7 @@ func matchFile(s string) (matches []string, longest string) {
 		}
 	}
 
-	dir = unescape(filepath.Dir(dir))
+	dir = filepath.Dir(dir)
 
 	files, err := ioutil.ReadDir(dir)
 	if err != nil {
@@ -279,25 +309,20 @@ func matchFile(s string) (matches []string, longest string) {
 
 	for _, f := range files {
 		name := filepath.Join(dir, f.Name())
-		f, err := os.Stat(name)
+		f, err := os.Lstat(name)
 		if err != nil {
-			fl, err := os.Lstat(name)
-			if err == nil && fl.Mode()&os.ModeSymlink != 0 {
-				continue
-			} else {
-				log.Printf("getting file information: %s", err)
-				return
-			}
+			log.Printf("getting file information: %s", err)
+			continue
 		}
 
 		_, last := filepath.Split(s)
-		if !strings.HasPrefix(escape(f.Name()), last) {
+		if !strings.HasPrefix(f.Name(), last) {
 			continue
 		}
 
 		name = f.Name()
 		if isRoot(s) || filepath.Base(s) != s {
-			name = filepath.Join(filepath.Dir(unescape(s)), f.Name())
+			name = filepath.Join(filepath.Dir(s), f.Name())
 		}
 		name = escape(name)
 
@@ -307,66 +332,79 @@ func matchFile(s string) (matches []string, longest string) {
 		}
 		matches = append(matches, item)
 
-		if longest != "" {
-			longest = matchLongest(longest, name)
+		if len(longest) != 0 {
+			longest = matchLongest(longest, []rune(name))
 		} else if s != "" {
 			if f.Mode().IsRegular() {
-				longest = name + " "
+				longest = []rune(name + " ")
 			} else {
-				longest = name + escape(string(filepath.Separator))
+				longest = []rune(name + escape(string(filepath.Separator)))
 			}
 		}
 	}
 
-	if longest == "" {
-		longest = s
+	if len(longest) == 0 {
+		longest = []rune(s)
 	}
 
 	return
 }
 
+func matchCmd(s string) (matches []string, longest []rune) {
+	words := gCmdWords
+	for c := range gOpts.cmds {
+		words = append(words, c)
+	}
+	sort.Strings(words)
+	j := 0
+	for i := 1; i < len(words); i++ {
+		if words[j] == words[i] {
+			continue
+		}
+		j++
+		words[i], words[j] = words[j], words[i]
+	}
+	words = words[:j+1]
+	matches, longest = matchWord(s, words)
+	return
+}
+
 func completeCmd(acc []rune) (matches []string, longestAcc []rune) {
 	s := string(acc)
 	f := tokenize(s)
 
-	var longest string
+	var longest []rune
 
 	switch len(f) {
 	case 1:
-		words := gCmdWords
-		for c := range gOpts.cmds {
-			words = append(words, c)
-		}
-		sort.Strings(words)
-		j := 0
-		for i := 1; i < len(words); i++ {
-			if words[j] == words[i] {
-				continue
-			}
-			j++
-			words[i], words[j] = words[j], words[i]
-		}
-		words = words[:j+1]
-		matches, longest = matchWord(s, words)
-		longestAcc = []rune(longest)
+		matches, longestAcc = matchCmd(s)
 	case 2:
 		switch f[0] {
 		case "set":
 			matches, longest = matchWord(f[1], gOptWords)
-			longestAcc = append(acc[:len(acc)-len(f[len(f)-1])], []rune(longest)...)
-		case "map", "cmd":
+			longestAcc = append(acc[:len(acc)-len([]rune(f[len(f)-1]))], longest...)
+		case "map", "cmap", "cmd":
 			longestAcc = acc
 		default:
 			matches, longest = matchFile(f[len(f)-1])
-			longestAcc = append(acc[:len(acc)-len(f[len(f)-1])], []rune(longest)...)
+			longestAcc = append(acc[:len(acc)-len([]rune(f[len(f)-1]))], longest...)
+		}
+	case 3:
+		switch f[0] {
+		case "map", "cmap":
+			matches, longest = matchCmd(f[2])
+			longestAcc = append(acc[:len(acc)-len([]rune(f[len(f)-1]))], longest...)
+		default:
+			matches, longest = matchFile(f[len(f)-1])
+			longestAcc = append(acc[:len(acc)-len([]rune(f[len(f)-1]))], longest...)
 		}
 	default:
 		switch f[0] {
-		case "set", "map", "cmd":
+		case "set", "map", "cmap", "cmd":
 			longestAcc = acc
 		default:
 			matches, longest = matchFile(f[len(f)-1])
-			longestAcc = append(acc[:len(acc)-len(f[len(f)-1])], []rune(longest)...)
+			longestAcc = append(acc[:len(acc)-len([]rune(f[len(f)-1]))], longest...)
 		}
 	}
 
@@ -386,8 +424,6 @@ func completeFile(acc []rune) (matches []string, longestAcc []rune) {
 		log.Printf("reading directory: %s", err)
 	}
 
-	var longest string
-
 	for _, f := range files {
 		if !strings.HasPrefix(f.Name(), s) {
 			continue
@@ -395,19 +431,17 @@ func completeFile(acc []rune) (matches []string, longestAcc []rune) {
 
 		matches = append(matches, f.Name())
 
-		if longest != "" {
-			longest = matchLongest(longest, f.Name())
+		if len(longestAcc) != 0 {
+			longestAcc = matchLongest(longestAcc, []rune(f.Name()))
 		} else if s != "" {
-			longest = f.Name()
+			longestAcc = []rune(f.Name())
 		}
 	}
 
-	if longest == "" {
-		longest = s
+	if len(longestAcc) == 0 {
+		longestAcc = acc
 	}
 
-	longestAcc = []rune(longest)
-
 	return
 }
 
@@ -415,15 +449,14 @@ func completeShell(acc []rune) (matches []string, longestAcc []rune) {
 	s := string(acc)
 	f := tokenize(s)
 
-	var longest string
+	var longest []rune
 
 	switch len(f) {
 	case 1:
-		matches, longest = matchExec(s)
-		longestAcc = []rune(longest)
+		matches, longestAcc = matchExec(s)
 	default:
 		matches, longest = matchFile(f[len(f)-1])
-		longestAcc = append(acc[:len(acc)-len([]rune(f[len(f)-1]))], []rune(longest)...)
+		longestAcc = append(acc[:len(acc)-len([]rune(f[len(f)-1]))], longest...)
 	}
 
 	return
diff --git a/doc.go b/doc.go
index 86e0c42..50868ad 100644
--- a/doc.go
+++ b/doc.go
@@ -85,6 +85,7 @@ The following command line commands are provided by lf:
     cmd-complete             (default '<tab>')
     cmd-menu-complete
     cmd-menu-complete-back
+    cmd-menu-accept
     cmd-enter                (default '<c-j>' and '<enter>')
     cmd-interrupt            (default '<c-c>')
     cmd-history-next         (default '<c-n>')
@@ -117,6 +118,7 @@ The following options can be used to customize the behavior of lf:
     dircounts      bool      (default off)
     dirfirst       bool      (default on)
     dironly        bool      (default off)
+    dirpreviews    bool      (default off)
     drawbox        bool      (default off)
     errorfmt       string    (default "\033[7;31;47m%s\033[0m")
     filesep        string    (default "\n")
@@ -144,6 +146,7 @@ The following options can be used to customize the behavior of lf:
     relativenumber bool      (default off)
     reverse        bool      (default off)
     scrolloff      int       (default 0)
+    selmode        string    (default 'all')
     shell          string    (default 'sh' for Unix and 'cmd' for Windows)
     shellflag      string    (default '-c' for Unix and '/c' for Windows)
     shellopts      []string  (default '')
@@ -492,6 +495,10 @@ Autocomplete the current word with menu selection.
 You need to assign keys to these commands (e.g. 'cmap <tab> cmd-menu-complete; cmap <backtab> cmd-menu-complete-back').
 You can use the assigned keys assigned to display the menu and then cycle through completion options.
 
+    cmd-menu-accept
+
+Accept the currently selected match in menu completion and close the menu.
+
     cmd-enter                (default '<c-j>' and '<enter>')
 
 Execute the current line.
@@ -571,7 +578,7 @@ Automatically quit server when there are no clients left connected.
 Set the path of a cleaner file.
 The file should be executable.
 This file is called if previewing is enabled, the previewer is set, and the previously selected file had its preview cache disabled.
-One argument is passed to the file, path to the file whose preview should be cleaned.
+Five arguments are passed to the file, (1) current file name, (2) width, (3) height, (4) horizontal position, and (5) vertical position of preview pane respectively.
 Preview clearing is disabled when the value of this option is left empty.
 
     dircache       bool      (default on)
@@ -592,6 +599,10 @@ Show directories first above regular files.
 
     dironly        bool      (default off)
 
+If enabled, directories will also be passed to the previewer script. This allows custom previews for directories.
+
+    dirpreviews    bool      (default off)
+
 Show only directories.
 
     drawbox        bool      (default off)
@@ -630,6 +641,10 @@ Patterns can be given as relative or absolute paths.
 Globbing supports the usual special characters, '*' to match any sequence, '?' to match any character, and '[...]' or '[^...] to match character sets or ranges.
 In addition, if a pattern starts with '!', then its matches are excluded from hidden files.
 
+    history        bool      (default on)
+
+Save command history.
+
     icons          bool      (default off)
 
 Show icons before each item in the list.
@@ -728,6 +743,12 @@ When 'number' is enabled, current line shows the absolute position, otherwise no
 
 Reverse the direction of sort.
 
+    selmode        string    (default 'all')
+
+Selection mode for commands.
+When set to 'all' it will use the selected files from all directories.
+When set to 'dir' it will only use the selected files in the current directory.
+
     scrolloff      int       (default 0)
 
 Minimum number of offset lines shown at all times in the top and the bottom of the screen when scrolling.
@@ -863,7 +884,7 @@ This shell command can be defined to override the default 'paste' command.
 
     rename
 
-This shell command can be defined to override the default 'paste' command.
+This shell command can be defined to override the default 'rename' command.
 
     delete
 
@@ -1048,7 +1069,7 @@ A first attempt to write such a command may look like this:
         if [ -z "$fs" ]; then
             mv "$f" ~/.trash
         else
-            IFS="`printf '\n\t'`"; mv $fs ~/.trash
+            IFS="$(printf '\n\t')"; mv $fs ~/.trash
         fi
     }}
 
@@ -1059,7 +1080,7 @@ We can use this variable to get rid of the conditional:
 
     cmd trash ${{
         mkdir -p ~/.trash
-        IFS="`printf '\n\t'`"; mv $fx ~/.trash
+        IFS="$(printf '\n\t')"; mv $fx ~/.trash
     }}
 
 The trash directory is checked each time the command is executed.
@@ -1067,13 +1088,13 @@ We can move it outside of the command so it would only run once at startup:
 
     ${{ mkdir -p ~/.trash }}
 
-    cmd trash ${{ IFS="`printf '\n\t'`"; mv $fx ~/.trash }}
+    cmd trash ${{ IFS="$(printf '\n\t')"; mv $fx ~/.trash }}
 
 Since these are one liners, we can drop '{{' and '}}':
 
     $mkdir -p ~/.trash
 
-    cmd trash $IFS="`printf '\n\t'`"; mv $fx ~/.trash
+    cmd trash $IFS="$(printf '\n\t')"; mv $fx ~/.trash
 
 Finally note that we set 'IFS' variable manually in these commands.
 Instead we could use the 'ifs' option to set it for all shell commands (i.e. 'set ifs "\n"').
@@ -1219,14 +1240,10 @@ You can enable 'globsearch' option to match with a glob pattern.
 Globbing supports '*' to match any sequence, '?' to match any character, and '[...]' or '[^...] to match character sets or ranges.
 You can enable 'incsearch' option to jump to the current match at each keystroke while typing.
 In this mode, you can either use 'cmd-enter' to accept the search or use 'cmd-escape' to cancel the search.
-Alternatively, you can also map some other commands with 'cmap' to accept the search and execute the command immediately afterwards.
-Possible candidates are 'up', 'down' and their variants, 'top', 'bottom', 'updir', and 'open' commands.
-For example, you can use arrow keys to finish the search with the following mappings:
+You can also map some other commands with 'cmap' to accept the search and execute the command immediately afterwards.
+For example, you can use the right arrow key to finish the search and open the selected file with the following mapping:
 
-    cmap <up>    up
-    cmap <down>  down
-    cmap <left>  updir
-    cmap <right> open
+    cmap <right> :cmd-enter; open
 
 Finding mechanism is implemented with commands 'find' (default 'f'), 'find-back' (default 'F'), 'find-next' (default ';'), 'find-prev' (default ',').
 You can disable 'anchorfind' option to match a pattern at an arbitrary position in the filename instead of the beginning.
@@ -1256,8 +1273,7 @@ It is possible to use different command types:
 You may want to use either file extensions or mime types from 'file' command:
 
     cmd open ${{
-        test -L $f && f=$(readlink -f $f)
-        case $(file --mime-type $f -b) in
+        case $(file --mime-type -Lb $f) in
             text/*) vi $fx;;
             *) for f in $fx; do xdg-open $f > /dev/null 2> /dev/null & done;;
         esac
@@ -1276,7 +1292,7 @@ Previewing Files
 
 lf previews files on the preview pane by printing the file until the end or the preview pane is filled.
 This output can be enhanced by providing a custom preview script for filtering.
-This can be used to highlight source codes, list contents of archive files or view pdf or image files to name few.
+This can be used to highlight source codes, list contents of archive files or view pdf or image files to name a few.
 For coloring lf recognizes ansi escape codes.
 
 In order to use this feature you need to set the value of 'previewer' option to the path of an executable file.
diff --git a/docstring.go b/docstring.go
index c0e933f..e108cad 100644
--- a/docstring.go
+++ b/docstring.go
@@ -89,6 +89,7 @@ The following command line commands are provided by lf:
     cmd-complete             (default '<tab>')
     cmd-menu-complete
     cmd-menu-complete-back
+    cmd-menu-accept
     cmd-enter                (default '<c-j>' and '<enter>')
     cmd-interrupt            (default '<c-c>')
     cmd-history-next         (default '<c-n>')
@@ -148,6 +149,7 @@ The following options can be used to customize the behavior of lf:
     relativenumber bool      (default off)
     reverse        bool      (default off)
     scrolloff      int       (default 0)
+    selmode        string    (default 'all')
     shell          string    (default 'sh' for Unix and 'cmd' for Windows)
     shellflag      string    (default '-c' for Unix and '/c' for Windows)
     shellopts      []string  (default '')
@@ -520,6 +522,10 @@ to these commands (e.g. 'cmap <tab> cmd-menu-complete; cmap <backtab>
 cmd-menu-complete-back'). You can use the assigned keys assigned to display
 the menu and then cycle through completion options.
 
+    cmd-menu-accept
+
+Accept the currently selected match in menu completion and close the menu.
+
     cmd-enter                (default '<c-j>' and '<enter>')
 
 Execute the current line.
@@ -601,9 +607,10 @@ Automatically quit server when there are no clients left connected.
 
 Set the path of a cleaner file. The file should be executable. This file is
 called if previewing is enabled, the previewer is set, and the previously
-selected file had its preview cache disabled. One argument is passed to the
-file, path to the file whose preview should be cleaned. Preview clearing is
-disabled when the value of this option is left empty.
+selected file had its preview cache disabled. Five arguments are passed to
+the file, (1) current file name, (2) width, (3) height, (4) horizontal
+position, and (5) vertical position of preview pane respectively. Preview
+clearing is disabled when the value of this option is left empty.
 
     dircache       bool      (default on)
 
@@ -666,6 +673,10 @@ any sequence, '?' to match any character, and '[...]' or '[^...] to match
 character sets or ranges. In addition, if a pattern starts with '!', then
 its matches are excluded from hidden files.
 
+    history        bool      (default on)
+
+Save command history.
+
     icons          bool      (default off)
 
 Show icons before each item in the list.
@@ -777,6 +788,12 @@ shown.
 
 Reverse the direction of sort.
 
+    selmode        string    (default 'all')
+
+Selection mode for commands. When set to 'all' it will use the selected
+files from all directories. When set to 'dir' it will only use the selected
+files in the current directory.
+
     scrolloff      int       (default 0)
 
 Minimum number of offset lines shown at all times in the top and the bottom
@@ -930,7 +947,7 @@ This shell command can be defined to override the default 'paste' command.
 
     rename
 
-This shell command can be defined to override the default 'paste' command.
+This shell command can be defined to override the default 'rename' command.
 
     delete
 
@@ -1138,7 +1155,7 @@ this:
         if [ -z "$fs" ]; then
             mv "$f" ~/.trash
         else
-            IFS="'printf '\n\t''"; mv $fs ~/.trash
+            IFS="$(printf '\n\t')"; mv $fs ~/.trash
         fi
     }}
 
@@ -1149,7 +1166,7 @@ conditional:
 
     cmd trash ${{
         mkdir -p ~/.trash
-        IFS="'printf '\n\t''"; mv $fx ~/.trash
+        IFS="$(printf '\n\t')"; mv $fx ~/.trash
     }}
 
 The trash directory is checked each time the command is executed. We can
@@ -1157,13 +1174,13 @@ move it outside of the command so it would only run once at startup:
 
     ${{ mkdir -p ~/.trash }}
 
-    cmd trash ${{ IFS="'printf '\n\t''"; mv $fx ~/.trash }}
+    cmd trash ${{ IFS="$(printf '\n\t')"; mv $fx ~/.trash }}
 
 Since these are one liners, we can drop '{{' and '}}':
 
     $mkdir -p ~/.trash
 
-    cmd trash $IFS="'printf '\n\t''"; mv $fx ~/.trash
+    cmd trash $IFS="$(printf '\n\t')"; mv $fx ~/.trash
 
 Finally note that we set 'IFS' variable manually in these commands. Instead
 we could use the 'ifs' option to set it for all shell commands (i.e. 'set
@@ -1348,16 +1365,12 @@ pattern. Globbing supports '*' to match any sequence, '?' to match any
 character, and '[...]' or '[^...] to match character sets or ranges. You can
 enable 'incsearch' option to jump to the current match at each keystroke
 while typing. In this mode, you can either use 'cmd-enter' to accept the
-search or use 'cmd-escape' to cancel the search. Alternatively, you can also
-map some other commands with 'cmap' to accept the search and execute the
-command immediately afterwards. Possible candidates are 'up', 'down' and
-their variants, 'top', 'bottom', 'updir', and 'open' commands. For example,
-you can use arrow keys to finish the search with the following mappings:
+search or use 'cmd-escape' to cancel the search. You can also map some other
+commands with 'cmap' to accept the search and execute the command
+immediately afterwards. For example, you can use the right arrow key to
+finish the search and open the selected file with the following mapping:
 
-    cmap <up>    up
-    cmap <down>  down
-    cmap <left>  updir
-    cmap <right> open
+    cmap <right> :cmd-enter; open
 
 Finding mechanism is implemented with commands 'find' (default 'f'),
 'find-back' (default 'F'), 'find-next' (default ';'), 'find-prev' (default
@@ -1393,8 +1406,7 @@ You may want to use either file extensions or mime types from 'file'
 command:
 
     cmd open ${{
-        test -L $f && f=$(readlink -f $f)
-        case $(file --mime-type $f -b) in
+        case $(file --mime-type -Lb $f) in
             text/*) vi $fx;;
             *) for f in $fx; do xdg-open $f > /dev/null 2> /dev/null & done;;
         esac
@@ -1417,7 +1429,7 @@ Previewing Files
 lf previews files on the preview pane by printing the file until the end or
 the preview pane is filled. This output can be enhanced by providing a
 custom preview script for filtering. This can be used to highlight source
-codes, list contents of archive files or view pdf or image files to name
+codes, list contents of archive files or view pdf or image files to name a
 few. For coloring lf recognizes ansi escape codes.
 
 In order to use this feature you need to set the value of 'previewer' option
diff --git a/etc/lfrc.example b/etc/lfrc.example
index 510f14d..1f96c22 100644
--- a/etc/lfrc.example
+++ b/etc/lfrc.example
@@ -36,8 +36,7 @@ map O $mimeopen --ask $f
 # use either file extensions and/or mime types here. Below uses an editor for
 # text files and a file opener for the rest.
 cmd open ${{
-    test -L $f && f=$(readlink -f $f)
-    case $(file --mime-type $f -b) in
+    case $(file --mime-type -Lb $f) in
         text/*) $EDITOR $fx;;
         *) for f in $fx; do setsid $OPENER $f > /dev/null 2> /dev/null & done;;
     esac
diff --git a/eval.go b/eval.go
index af3671d..71bccbc 100644
--- a/eval.go
+++ b/eval.go
@@ -44,6 +44,8 @@ func (e *setExpr) eval(app *app, args []string) {
 		app.nav.position()
 		app.ui.sort()
 		app.ui.loadFile(app.nav, true)
+	case "dirpreviews":
+		gOpts.dirpreviews = true
 	case "nodironly":
 		gOpts.dironly = false
 		app.nav.sort()
@@ -384,6 +386,8 @@ func (e *setExpr) eval(app *app, args []string) {
 		gOpts.ratios = rats
 		app.ui.wins = getWins(app.ui.screen)
 		app.ui.loadFile(app.nav, true)
+	case "selmode":
+		gOpts.selmode = e.val
 	case "shell":
 		gOpts.shell = e.val
 	case "shellflag":
@@ -511,6 +515,9 @@ func update(app *app) {
 	switch {
 	case gOpts.incsearch && app.ui.cmdPrefix == "/":
 		app.nav.search = string(app.ui.cmdAccLeft) + string(app.ui.cmdAccRight)
+		if app.nav.search == "" {
+			return
+		}
 
 		dir := app.nav.currDir()
 		old := dir.ind
@@ -525,6 +532,9 @@ func update(app *app) {
 		}
 	case gOpts.incsearch && app.ui.cmdPrefix == "?":
 		app.nav.search = string(app.ui.cmdAccLeft) + string(app.ui.cmdAccRight)
+		if app.nav.search == "" {
+			return
+		}
 
 		dir := app.nav.currDir()
 		old := dir.ind
@@ -594,6 +604,8 @@ func normal(app *app) {
 	app.ui.cmdAccRight = nil
 	app.ui.cmdTmp = nil
 	app.ui.cmdPrefix = ""
+
+	app.cmdHistoryInd = 0
 }
 
 func insert(app *app, arg string) {
@@ -1587,7 +1599,7 @@ func (e *callExpr) eval(app *app, args []string) {
 			return
 		}
 
-		// Reset the completition menu as in bash/vim
+		// Reset the completion menu as in bash/vim
 		// and update the pertinent variables
 		if app.ui.menuBuf != nil {
 			app.ui.menuBuf = nil
@@ -1628,27 +1640,21 @@ func (e *callExpr) eval(app *app, args []string) {
 			app.ui.menuBuf = b
 		}
 	case "cmd-menu-complete":
-		var target []rune
-
-		// target will store the current menu query
 		// note that, as we will store the current selected value
 		// in cmdAccLeft, we can not call the complete function with its
-		// value, as it is already a final completition result
+		// value, as it is already a final completion result
 		if app.ui.menuBuf == nil {
-			target = app.ui.cmdAccLeft
 			app.ui.cmdTmp = app.ui.cmdAccLeft
-		} else {
-			target = app.ui.cmdTmp
 		}
 
 		var matches []string
 		switch app.ui.cmdPrefix {
 		case ":":
-			matches, app.ui.cmdAccLeft = completeCmd(target)
+			matches, app.ui.cmdAccLeft = completeCmd(app.ui.cmdTmp)
 		case "/", "?":
-			matches, app.ui.cmdAccLeft = completeFile(target)
+			matches, app.ui.cmdAccLeft = completeFile(app.ui.cmdTmp)
 		case "$", "%", "!", "&":
-			matches, app.ui.cmdAccLeft = completeShell(target)
+			matches, app.ui.cmdAccLeft = completeShell(app.ui.cmdTmp)
 		default:
 			return
 		}
@@ -1656,13 +1662,12 @@ func (e *callExpr) eval(app *app, args []string) {
 		app.ui.draw(app.nav)
 
 		if len(matches) > 1 {
-			step := 1
-
-			// Check if the tab-selecttion was inited
+			// Check if the tab-selection was inited
 			if app.ui.menuSelected == -2 {
 				app.ui.menuSelected = -1
 			} else {
-				app.ui.menuSelected = mod(app.ui.menuSelected+step, len(matches))
+				app.ui.menuSelected++
+				app.ui.menuSelected %= len(matches)
 			}
 
 			if err := listMatchesMenu(app.ui, matches); err != nil {
@@ -1673,23 +1678,18 @@ func (e *callExpr) eval(app *app, args []string) {
 			app.ui.menuSelected = -2
 		}
 	case "cmd-menu-complete-back":
-		var target []rune
-
 		if app.ui.menuBuf == nil {
-			target = app.ui.cmdAccLeft
 			app.ui.cmdTmp = app.ui.cmdAccLeft
-		} else {
-			target = app.ui.cmdTmp
 		}
 
 		var matches []string
 		switch app.ui.cmdPrefix {
 		case ":":
-			matches, app.ui.cmdAccLeft = completeCmd(target)
+			matches, app.ui.cmdAccLeft = completeCmd(app.ui.cmdTmp)
 		case "/", "?":
-			matches, app.ui.cmdAccLeft = completeFile(target)
+			matches, app.ui.cmdAccLeft = completeFile(app.ui.cmdTmp)
 		case "$", "%", "!", "&":
-			matches, app.ui.cmdAccLeft = completeShell(target)
+			matches, app.ui.cmdAccLeft = completeShell(app.ui.cmdTmp)
 		default:
 			return
 		}
@@ -1697,13 +1697,13 @@ func (e *callExpr) eval(app *app, args []string) {
 		app.ui.draw(app.nav)
 
 		if len(matches) > 1 {
-			step := -1
-
-			// Check if the tab-selecttion was inited
+			// Check if the tab-selection was inited
 			if app.ui.menuSelected == -2 {
 				app.ui.menuSelected = -1
+			} else if app.ui.menuSelected <= 0 {
+				app.ui.menuSelected = len(matches) - 1
 			} else {
-				app.ui.menuSelected = mod(app.ui.menuSelected+step, len(matches))
+				app.ui.menuSelected--
 			}
 
 			if err := listMatchesMenu(app.ui, matches); err != nil {
@@ -1713,6 +1713,9 @@ func (e *callExpr) eval(app *app, args []string) {
 			app.ui.menuBuf = nil
 			app.ui.menuSelected = -2
 		}
+	case "cmd-menu-accept":
+		app.ui.menuBuf = nil
+		app.ui.menuSelected = -2
 	case "cmd-enter":
 		s := string(append(app.ui.cmdAccLeft, app.ui.cmdAccRight...))
 		if len(s) == 0 && app.ui.cmdPrefix != "filter: " {
@@ -1887,31 +1890,27 @@ func (e *callExpr) eval(app *app, args []string) {
 			app.cmdHistoryInd--
 		}
 		if app.cmdHistoryInd == 0 {
-			app.ui.menuBuf = nil
-			app.ui.menuSelected = -2
-			app.ui.cmdAccLeft = nil
-			app.ui.cmdAccRight = nil
-			app.ui.cmdTmp = nil
+			normal(app)
 			app.ui.cmdPrefix = ":"
 			return
 		}
-		cmd := app.cmdHistory[len(app.cmdHistory)-app.cmdHistoryInd]
+		historyInd := app.cmdHistoryInd
+		cmd := app.cmdHistory[len(app.cmdHistory)-historyInd]
 		normal(app)
+		app.cmdHistoryInd = historyInd
 		app.ui.cmdPrefix = cmd.prefix
 		app.ui.cmdAccLeft = []rune(cmd.value)
 	case "cmd-history-prev":
 		if app.ui.cmdPrefix == ">" {
 			return
 		}
-		if app.ui.cmdPrefix == "" {
-			app.cmdHistoryInd = 0
-		}
 		if app.cmdHistoryInd == len(app.cmdHistory) {
 			return
 		}
-		app.cmdHistoryInd++
-		cmd := app.cmdHistory[len(app.cmdHistory)-app.cmdHistoryInd]
+		historyInd := app.cmdHistoryInd + 1
+		cmd := app.cmdHistory[len(app.cmdHistory)-historyInd]
 		normal(app)
+		app.cmdHistoryInd = historyInd
 		app.ui.cmdPrefix = cmd.prefix
 		app.ui.cmdAccLeft = []rune(cmd.value)
 	case "cmd-delete":
diff --git a/icons.go b/icons.go
index 3b56293..347645f 100644
--- a/icons.go
+++ b/icons.go
@@ -156,7 +156,7 @@ func (im iconMap) get(f *file) string {
 		return val
 	}
 
-	if val, ok := im["*"+f.ext]; ok {
+	if val, ok := im["*"+strings.ToLower(f.ext)]; ok {
 		return val
 	}
 
diff --git a/lf.1 b/lf.1
index 77c58d1..9e288ae 100644
--- a/lf.1
+++ b/lf.1
@@ -102,6 +102,7 @@ The following command line commands are provided by lf:
     cmd-complete             (default '<tab>')
     cmd-menu-complete
     cmd-menu-complete-back
+    cmd-menu-accept
     cmd-enter                (default '<c-j>' and '<enter>')
     cmd-interrupt            (default '<c-c>')
     cmd-history-next         (default '<c-n>')
@@ -163,6 +164,7 @@ The following options can be used to customize the behavior of lf:
     relativenumber bool      (default off)
     reverse        bool      (default off)
     scrolloff      int       (default 0)
+    selmode        string    (default 'all')
     shell          string    (default 'sh' for Unix and 'cmd' for Windows)
     shellflag      string    (default '-c' for Unix and '/c' for Windows)
     shellopts      []string  (default '')
@@ -600,6 +602,12 @@ Autocomplete the current word.
 .PP
 Autocomplete the current word with menu selection. You need to assign keys to these commands (e.g. 'cmap <tab> cmd-menu-complete; cmap <backtab> cmd-menu-complete-back'). You can use the assigned keys assigned to display the menu and then cycle through completion options.
 .PP
+.EX
+    cmd-menu-accept
+.EE
+.PP
+Accept the currently selected match in menu completion and close the menu.
+.PP
 .EX
     cmd-enter                (default '<c-j>' and '<enter>')
 .EE
@@ -705,7 +713,7 @@ Automatically quit server when there are no clients left connected.
     cleaner        string    (default '') (not called if empty)
 .EE
 .PP
-Set the path of a cleaner file. The file should be executable. This file is called if previewing is enabled, the previewer is set, and the previously selected file had its preview cache disabled. One argument is passed to the file, path to the file whose preview should be cleaned. Preview clearing is disabled when the value of this option is left empty.
+Set the path of a cleaner file. The file should be executable. This file is called if previewing is enabled, the previewer is set, and the previously selected file had its preview cache disabled. Five arguments are passed to the file, (1) current file name, (2) width, (3) height, (4) horizontal position, and (5) vertical position of preview pane respectively. Preview clearing is disabled when the value of this option is left empty.
 .PP
 .EX
     dircache       bool      (default on)
@@ -773,6 +781,12 @@ Show hidden files. On Unix systems, hidden files are determined by the value of
 .PP
 List of hidden file glob patterns. Patterns can be given as relative or absolute paths. Globbing supports the usual special characters, '*' to match any sequence, '?' to match any character, and '[...]' or '[^...] to match character sets or ranges. In addition, if a pattern starts with '!', then its matches are excluded from hidden files.
 .PP
+.EX
+    history        bool      (default on)
+.EE
+.PP
+Save command history.
+.PP
 .EX
     icons          bool      (default off)
 .EE
@@ -881,6 +895,12 @@ Show the position number relative to the current line. When 'number' is enabled,
 .PP
 Reverse the direction of sort.
 .PP
+.EX
+    selmode        string    (default 'all')
+.EE
+.PP
+Selection mode for commands. When set to 'all' it will use the selected files from all directories. When set to 'dir' it will only use the selected files in the current directory.
+.PP
 .EX
     scrolloff      int       (default 0)
 .EE
@@ -1061,7 +1081,7 @@ This shell command can be defined to override the default 'paste' command.
     rename
 .EE
 .PP
-This shell command can be defined to override the default 'paste' command.
+This shell command can be defined to override the default 'rename' command.
 .PP
 .EX
     delete
@@ -1278,7 +1298,7 @@ Regular shell commands are the most basic command type that is useful for many p
         if [ -z "$fs" ]; then
             mv "$f" ~/.trash
         else
-            IFS="`printf '\en\et'`"; mv $fs ~/.trash
+            IFS="$(printf '\en\et')"; mv $fs ~/.trash
         fi
     }}
 .EE
@@ -1288,7 +1308,7 @@ We check '$fs' to see if there are any selected files. Otherwise we just delete
 .EX
     cmd trash ${{
         mkdir -p ~/.trash
-        IFS="`printf '\en\et'`"; mv $fx ~/.trash
+        IFS="$(printf '\en\et')"; mv $fx ~/.trash
     }}
 .EE
 .PP
@@ -1299,7 +1319,7 @@ The trash directory is checked each time the command is executed. We can move it
 .EE
 .PP
 .EX
-    cmd trash ${{ IFS="`printf '\en\et'`"; mv $fx ~/.trash }}
+    cmd trash ${{ IFS="$(printf '\en\et')"; mv $fx ~/.trash }}
 .EE
 .PP
 Since these are one liners, we can drop '{{' and '}}':
@@ -1309,7 +1329,7 @@ Since these are one liners, we can drop '{{' and '}}':
 .EE
 .PP
 .EX
-    cmd trash $IFS="`printf '\en\et'`"; mv $fx ~/.trash
+    cmd trash $IFS="$(printf '\en\et')"; mv $fx ~/.trash
 .EE
 .PP
 Finally note that we set 'IFS' variable manually in these commands. Instead we could use the 'ifs' option to set it for all shell commands (i.e. 'set ifs "\en"'). This can be especially useful for interactive use (e.g. '$rm $f' or '$rm $fs' would simply work). This option is not set by default as it can behave unexpectedly for new users. However, use of this option is highly recommended and it is assumed in the rest of the documentation.
@@ -1413,13 +1433,10 @@ By default, lf does not assign 'delete' command to a key to protect new users. Y
 .SH SEARCHING FILES
 There are two mechanisms implemented in lf to search a file in the current directory. Searching is the traditional method to move the selection to a file matching a given pattern. Finding is an alternative way to search for a pattern possibly using fewer keystrokes.
 .PP
-Searching mechanism is implemented with commands 'search' (default '/'), 'search-back' (default '?'), 'search-next' (default 'n'), and 'search-prev' (default 'N'). You can enable 'globsearch' option to match with a glob pattern. Globbing supports '*' to match any sequence, '?' to match any character, and '[...]' or '[^...] to match character sets or ranges. You can enable 'incsearch' option to jump to the current match at each keystroke while typing. In this mode, you can either use 'cmd-enter' to accept the search or use 'cmd-escape' to cancel the search. Alternatively, you can also map some other commands with 'cmap' to accept the search and execute the command immediately afterwards. Possible candidates are 'up', 'down' and their variants, 'top', 'bottom', 'updir', and 'open' commands. For example, you can use arrow keys to finish the search with the following mappings:
+Searching mechanism is implemented with commands 'search' (default '/'), 'search-back' (default '?'), 'search-next' (default 'n'), and 'search-prev' (default 'N'). You can enable 'globsearch' option to match with a glob pattern. Globbing supports '*' to match any sequence, '?' to match any character, and '[...]' or '[^...] to match character sets or ranges. You can enable 'incsearch' option to jump to the current match at each keystroke while typing. In this mode, you can either use 'cmd-enter' to accept the search or use 'cmd-escape' to cancel the search. You can also map some other commands with 'cmap' to accept the search and execute the command immediately afterwards. For example, you can use the right arrow key to finish the search and open the selected file with the following mapping:
 .PP
 .EX
-    cmap <up>    up
-    cmap <down>  down
-    cmap <left>  updir
-    cmap <right> open
+    cmap <right> :cmd-enter; open
 .EE
 .PP
 Finding mechanism is implemented with commands 'find' (default 'f'), 'find-back' (default 'F'), 'find-next' (default ';'), 'find-prev' (default ','). You can disable 'anchorfind' option to match a pattern at an arbitrary position in the filename instead of the beginning. You can set the number of keys to match using 'findlen' option. If you set this value to zero, then the the keys are read until there is only a single match. Default values of these two options are set to jump to the first file with the given initial.
@@ -1442,8 +1459,7 @@ You may want to use either file extensions or mime types from 'file' command:
 .PP
 .EX
     cmd open ${{
-        test -L $f && f=$(readlink -f $f)
-        case $(file --mime-type $f -b) in
+        case $(file --mime-type -Lb $f) in
             text/*) vi $fx;;
             *) for f in $fx; do xdg-open $f > /dev/null 2> /dev/null & done;;
         esac
@@ -1460,7 +1476,7 @@ Following command is provided by default:
 .PP
 You may also use any other existing file openers as you like. Possible options are 'libfile-mimeinfo-perl' (executable name is 'mimeopen'), 'rifle' (ranger's default file opener), or 'mimeo' to name a few.
 .SH PREVIEWING FILES
-lf previews files on the preview pane by printing the file until the end or the preview pane is filled. This output can be enhanced by providing a custom preview script for filtering. This can be used to highlight source codes, list contents of archive files or view pdf or image files to name few. For coloring lf recognizes ansi escape codes.
+lf previews files on the preview pane by printing the file until the end or the preview pane is filled. This output can be enhanced by providing a custom preview script for filtering. This can be used to highlight source codes, list contents of archive files or view pdf or image files to name a few. For coloring lf recognizes ansi escape codes.
 .PP
 In order to use this feature you need to set the value of 'previewer' option to the path of an executable file. Five arguments are passed to the file, (1) current file name, (2) width, (3) height, (4) horizontal position, and (5) vertical position of preview pane respectively. Output of the execution is printed in the preview pane. You may also want to use the same script in your pager mapping as well:
 .PP
diff --git a/misc.go b/misc.go
index 7083cd7..451b760 100644
--- a/misc.go
+++ b/misc.go
@@ -91,7 +91,7 @@ func unescape(s string) string {
 }
 
 // This function splits the given string by whitespaces. It is aware of escaped
-// whitespaces so that they are not splitted unintentionally.
+// whitespaces so that they are not split unintentionally.
 func tokenize(s string) []string {
 	esc := false
 	var buf []rune
@@ -295,10 +295,6 @@ func max(a, b int) int {
 	return b
 }
 
-func mod(a, b int) int {
-	return (a%b + b) % b
-}
-
 // We don't need no generic code
 // We don't need no type control
 // No dark templates in compiler
diff --git a/nav.go b/nav.go
index 00ed77d..2d7f0b1 100644
--- a/nav.go
+++ b/nav.go
@@ -150,6 +150,7 @@ type dir struct {
 	ignorecase  bool      // ignorecase value from last sort
 	ignoredia   bool      // ignoredia value from last sort
 	noPerm      bool      // whether lf has no permission to open the directory
+	lines       []string  // lines of text to display if directory previews are enabled
 }
 
 func newDir(path string) *dir {
@@ -161,6 +162,7 @@ func newDir(path string) *dir {
 	}
 
 	return &dir{
+		loading:  gOpts.dirpreviews, // Directory is loaded after previewer function exits.
 		loadTime: time,
 		path:     path,
 		files:    files,
@@ -365,6 +367,7 @@ type nav struct {
 	deleteCountChan chan int
 	deleteTotalChan chan int
 	previewChan     chan string
+	dirPreviewChan  chan *dir
 	dirChan         chan *dir
 	regChan         chan *reg
 	dirCache        map[string]*dir
@@ -403,7 +406,11 @@ func (nav *nav) loadDirInternal(path string) *dir {
 		d := newDir(path)
 		d.sort()
 		d.ind, d.pos = 0, 0
+		if gOpts.dirpreviews {
+			nav.dirPreviewChan <- d
+		}
 		nav.dirChan <- d
+
 	}()
 	return d
 }
@@ -447,6 +454,9 @@ func (nav *nav) checkDir(dir *dir) {
 			nd := newDir(dir.path)
 			nd.filter = dir.filter
 			nd.sort()
+			if gOpts.dirpreviews {
+				nav.dirPreviewChan <- nd
+			}
 			nav.dirChan <- nd
 		}()
 	case dir.sortType != gOpts.sortType ||
@@ -490,6 +500,7 @@ func newNav(height int) *nav {
 		deleteCountChan: make(chan int, 1024),
 		deleteTotalChan: make(chan int, 1024),
 		previewChan:     make(chan string, 1024),
+		dirPreviewChan:  make(chan *dir, 1024),
 		dirChan:         make(chan *dir),
 		regChan:         make(chan *reg),
 		dirCache:        make(map[string]*dir),
@@ -599,6 +610,27 @@ func (nav *nav) exportFiles() {
 	exportFiles(currFile, currSelections, nav.currDir().path)
 }
 
+func (nav *nav) dirPreviewLoop(ui *ui) {
+	var prevPath string
+	for {
+		select {
+		case dir := <-nav.dirPreviewChan:
+
+			if dir == nil && len(gOpts.previewer) != 0 && len(gOpts.cleaner) != 0 && nav.volatilePreview {
+				cmd := exec.Command(gOpts.cleaner, prevPath)
+				if err := cmd.Run(); err != nil {
+					log.Printf("cleaning preview: %s", err)
+				}
+				nav.volatilePreview = false
+			} else if dir != nil {
+				win := ui.wins[len(ui.wins)-1]
+				nav.previewDir(dir, win)
+				prevPath = dir.path
+			}
+		}
+	}
+}
+
 func (nav *nav) previewLoop(ui *ui) {
 	var prev string
 	for path := range nav.previewChan {
@@ -612,17 +644,21 @@ func (nav *nav) previewLoop(ui *ui) {
 				break loop
 			}
 		}
+		win := ui.wins[len(ui.wins)-1]
 		if clear && len(gOpts.previewer) != 0 && len(gOpts.cleaner) != 0 && nav.volatilePreview {
 			nav.exportFiles()
 			exportOpts()
-			cmd := exec.Command(gOpts.cleaner, prev)
+			cmd := exec.Command(gOpts.cleaner, prev,
+				strconv.Itoa(win.w),
+				strconv.Itoa(win.h),
+				strconv.Itoa(win.x),
+				strconv.Itoa(win.y))
 			if err := cmd.Run(); err != nil {
 				log.Printf("cleaning preview: %s", err)
 			}
 			nav.volatilePreview = false
 		}
 		if len(path) != 0 {
-			win := ui.wins[len(ui.wins)-1]
 			nav.preview(path, win)
 			prev = path
 		}
@@ -644,7 +680,70 @@ func matchPattern(pattern, name, path string) bool {
 	return matched
 }
 
+func (nav *nav) previewDir(dir *dir, win *win) {
+
+	defer func() {
+		dir.loading = false
+		nav.dirChan <- dir
+	}()
+
+	var reader io.Reader
+
+	if len(gOpts.previewer) != 0 {
+		nav.exportFiles()
+		exportOpts()
+		cmd := exec.Command(gOpts.previewer, dir.path,
+			strconv.Itoa(win.w),
+			strconv.Itoa(win.h),
+			strconv.Itoa(win.x),
+			strconv.Itoa(win.y))
+
+		out, err := cmd.StdoutPipe()
+		if err != nil {
+			log.Printf("previewing dir: %s", err)
+			return
+		}
+
+		if err := cmd.Start(); err != nil {
+			log.Printf("previewing dir: %s", err)
+			out.Close()
+			return
+		}
+
+		defer func() {
+			if err := cmd.Wait(); err != nil {
+				if e, ok := err.(*exec.ExitError); ok {
+					if e.ExitCode() != 0 {
+						nav.volatilePreview = true
+					}
+				} else {
+					log.Printf("loading dir: %s", err)
+				}
+			}
+		}()
+		defer out.Close()
+		reader = out
+		buf := bufio.NewScanner(reader)
+
+		for i := 0; i < win.h && buf.Scan(); i++ {
+			for _, r := range buf.Text() {
+				if r == 0 {
+					dir.lines = []string{"\033[7mbinary\033[0m"}
+					return
+				}
+			}
+			dir.lines = append(dir.lines, buf.Text())
+		}
+
+		if buf.Err() != nil {
+			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 }()
 
@@ -1739,27 +1838,37 @@ 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 {
+		// select only from this directory
+		currDirPath = nav.currDir().path
+	}
+
 	paths := make([]string, 0, len(nav.selections))
 	indices := make([]int, 0, len(nav.selections))
 	for path, index := range nav.selections {
-		paths = append(paths, path)
-		indices = append(indices, index)
+		if !currDirOnly || filepath.Dir(path) == currDirPath {
+			paths = append(paths, path)
+			indices = append(indices, index)
+		}
 	}
 	sort.Sort(indexedSelections{paths: paths, indices: indices})
 	return paths
 }
 
 func (nav *nav) currFileOrSelections() (list []string, err error) {
-	if len(nav.selections) == 0 {
+	sel := nav.currSelections()
+
+	if len(sel) == 0 {
 		curr, err := nav.currFile()
 		if err != nil {
 			return nil, errors.New("no file selected")
 		}
-
 		return []string{curr.path}, nil
 	}
-
-	return nav.currSelections(), nil
+	return sel, nil
 }
 
 func (nav *nav) calcDirSize() error {
diff --git a/opts.go b/opts.go
index 3b7121e..cd2f890 100644
--- a/opts.go
+++ b/opts.go
@@ -33,6 +33,7 @@ var gOpts struct {
 	dircache       bool
 	dircounts      bool
 	dironly        bool
+	dirpreviews    bool
 	drawbox        bool
 	globsearch     bool
 	icons          bool
@@ -59,6 +60,7 @@ var gOpts struct {
 	previewer      string
 	cleaner        string
 	promptfmt      string
+	selmode        string
 	shell          string
 	shellflag      string
 	timefmt        string
@@ -84,6 +86,7 @@ func init() {
 	gOpts.dircache = true
 	gOpts.dircounts = false
 	gOpts.dironly = false
+	gOpts.dirpreviews = false
 	gOpts.drawbox = false
 	gOpts.globsearch = false
 	gOpts.icons = false
@@ -110,6 +113,7 @@ func init() {
 	gOpts.previewer = ""
 	gOpts.cleaner = ""
 	gOpts.promptfmt = "\033[32;1m%u@%h\033[0m:\033[34;1m%d\033[0m\033[1m%f\033[0m"
+	gOpts.selmode = "all"
 	gOpts.shell = gDefaultShell
 	gOpts.shellflag = gDefaultShellFlag
 	gOpts.timefmt = time.ANSIC
diff --git a/ui.go b/ui.go
index e287235..5faccc6 100644
--- a/ui.go
+++ b/ui.go
@@ -324,7 +324,8 @@ func fileInfo(f *file, d *dir) string {
 	return info
 }
 
-func (win *win) printDir(screen tcell.Screen, dir *dir, selections map[string]int, saves map[string]bool, tags map[string]string, colors styleMap, icons iconMap) {
+func (win *win) printDir(screen tcell.Screen, dir *dir, selections map[string]int, saves map[string]bool, tags map[string]string, colors styleMap, icons iconMap, previewAllowed bool) {
+
 	if win.w < 5 || dir == nil {
 		return
 	}
@@ -334,11 +335,25 @@ func (win *win) printDir(screen tcell.Screen, dir *dir, selections map[string]in
 		return
 	}
 
-	if dir.loading && len(dir.files) == 0 {
+	if (dir.loading && len(dir.files) == 0) || (previewAllowed && dir.loading && gOpts.dirpreviews) {
 		win.print(screen, 2, 0, tcell.StyleDefault.Reverse(true), "loading...")
 		return
 	}
 
+	if previewAllowed && gOpts.dirpreviews && len(gOpts.previewer) > 0 {
+
+		// Print previewer result instead of default directory print operation.
+		st := tcell.StyleDefault
+		for i, l := range dir.lines {
+			if i > win.h-1 {
+				break
+			}
+
+			st = win.print(screen, 2, i, st, l)
+		}
+		return
+	}
+
 	if len(dir.files) == 0 {
 		win.print(screen, 2, 0, tcell.StyleDefault.Reverse(true), "empty")
 		return
@@ -774,8 +789,9 @@ func (ui *ui) drawStatLine(nav *nav) {
 		}
 	}
 
-	if len(nav.selections) > 0 {
-		selection += fmt.Sprintf("  \033[35;7m %d \033[0m", len(nav.selections))
+	currSelections := nav.currSelections()
+	if len(currSelections) > 0 {
+		selection += fmt.Sprintf("  \033[35;7m %d \033[0m", len(currSelections))
 	}
 
 	if len(dir.filter) != 0 {
@@ -855,7 +871,7 @@ func (ui *ui) draw(nav *nav) {
 
 	doff := len(nav.dirs) - length
 	for i := 0; i < length; i++ {
-		ui.wins[woff+i].printDir(ui.screen, nav.dirs[doff+i], nav.selections, nav.saves, nav.tags, ui.styles, ui.icons)
+		ui.wins[woff+i].printDir(ui.screen, nav.dirs[doff+i], nav.selections, nav.saves, nav.tags, ui.styles, ui.icons, false)
 	}
 
 	switch ui.cmdPrefix {
@@ -889,7 +905,7 @@ func (ui *ui) draw(nav *nav) {
 			preview := ui.wins[len(ui.wins)-1]
 
 			if curr.IsDir() {
-				preview.printDir(ui.screen, ui.dirPrev, nav.selections, nav.saves, nav.tags, ui.styles, ui.icons)
+				preview.printDir(ui.screen, ui.dirPrev, nav.selections, nav.saves, nav.tags, ui.styles, ui.icons, true)
 			} else if curr.Mode().IsRegular() {
 				preview.printReg(ui.screen, ui.regPrev)
 			}
@@ -1265,13 +1281,14 @@ func listMatchesMenu(ui *ui, matches []string) error {
 
 			// Handle menu tab match only if wanted
 			if ui.menuSelected == i {
+				comp := []rune(escape(target))
 				toks := tokenize(string(ui.cmdAccLeft))
-				last := toks[len(toks)-1]
+				_, last := filepath.Split(toks[len(toks)-1])
 
-				if strings.Contains(target, last) {
-					ui.cmdAccLeft = append(ui.cmdAccLeft[:len(ui.cmdAccLeft)-len(last)], []rune(target)...)
+				if last == "" {
+					ui.cmdAccLeft = append(ui.cmdAccLeft, comp...)
 				} else {
-					ui.cmdAccLeft = append(ui.cmdAccLeft, []rune(target)...)
+					ui.cmdAccLeft = append(ui.cmdAccLeft[:len(ui.cmdAccLeft)-len([]rune(last))], comp...)
 				}
 
 				target = fmt.Sprintf("\033[7m%s\033[0m%*s", target, wcol-len(target), "")


Test patch:
diff --git a/complete_test.go b/complete_test.go
index b5044c2..36994dd 100644
--- a/complete_test.go
+++ b/complete_test.go
@@ -18,10 +18,11 @@ func TestMatchLongest(t *testing.T) {
 		{"foo", "foobar", "foo"},
 		{"foo", "barfoo", ""},
 		{"foobar", "foobaz", "fooba"},
+		{"год", "гол", "го"},
 	}
 
 	for _, test := range tests {
-		if got := matchLongest(test.s1, test.s2); got != test.exp {
+		if got := string(matchLongest([]rune(test.s1), []rune(test.s2))); got != test.exp {
 			t.Errorf("at input '%s' and '%s' expected '%s' but got '%s'", test.s1, test.s2, test.exp, got)
 		}
 	}
@@ -48,8 +49,8 @@ func TestMatchWord(t *testing.T) {
 			t.Errorf("at input '%s' with '%s' expected '%s' but got '%s'", test.s, test.words, test.matches, m)
 		}
 
-		if l != test.longest {
-			t.Errorf("at input '%s' with '%s' expected '%s' but got '%s'", test.s, test.words, test.longest, l)
+		if ls := string(l); ls != test.longest {
+			t.Errorf("at input '%s' with '%s' expected '%s' but got '%s'", test.s, test.words, test.longest, ls)
 		}
 	}
 }