Repository: gokcehan/lf
Pull request #1684 (2024-04-08 11:22:41)

Taking hidden files parameter into account in windows
fixes: https://github.com/gokcehan/lf/issues/1683

On windows a file is considered hidden only if it has `FILE_ATTRIBUTE_HIDDEN` set. While this is the correct way to check if a file is hidden on windows, we should also take into account the `hiddenfiles` option so we can allow custom file patterns (ex. `__pycache__`, `obj`) as we can do on Linux.

gokcehan: Thank you @Catalyn45 for the patch and @joelim-work for the review. I like the idea of enabling the use of `hiddenfiles` on Windows, but is it a good idea to hide dot files by default on Windows? Perhaps the default for `hiddenfiles` should be `.*` on unix and empty on Windows. What do you think?

Patch:
diff --git a/app.go b/app.go
index 2f1fec9..87b7057 100644
--- a/app.go
+++ b/app.go
@@ -232,7 +232,7 @@ func (app *app) writeHistory() error {
 	}
 
 	for _, cmd := range app.cmdHistory {
-		_, err = f.WriteString(fmt.Sprintf("%s %s\n", cmd.prefix, cmd.value))
+		_, err = fmt.Fprintf(f, "%s %s\n", cmd.prefix, cmd.value)
 		if err != nil {
 			return fmt.Errorf("writing history file: %s", err)
 		}
@@ -332,8 +332,8 @@ func (app *app) loop() {
 			return
 		case n := <-app.nav.copyBytesChan:
 			app.nav.copyBytes += n
-			// n is usually 4096B so update roughly per 4096B x 1024 = 4MB copied
-			if app.nav.copyUpdate++; app.nav.copyUpdate >= 1024 {
+			// n is usually 32*1024B (default io.Copy() buffer) so update roughly per 32KB x 128 = 4MB copied
+			if app.nav.copyUpdate++; app.nav.copyUpdate >= 128 {
 				app.nav.copyUpdate = 0
 				app.ui.draw(app.nav)
 			}
@@ -585,5 +585,4 @@ func (app *app) runShell(s string, args []string, prefix string) {
 			app.ui.exprChan <- &callExpr{"load", nil, 1}
 		}()
 	}
-
 }
diff --git a/client.go b/client.go
index 559102d..52360b4 100644
--- a/client.go
+++ b/client.go
@@ -38,7 +38,7 @@ func run() {
 	}
 
 	if gLogPath != "" {
-		f, err := os.OpenFile(gLogPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
+		f, err := os.OpenFile(gLogPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
 		if err != nil {
 			panic(err)
 		}
diff --git a/colors.go b/colors.go
index 8558b3f..2d5d8c9 100644
--- a/colors.go
+++ b/colors.go
@@ -295,9 +295,9 @@ func (sm styleMap) get(f *file) tcell.Style {
 		key = "ln"
 	case f.linkState == broken:
 		key = "or"
-	case f.IsDir() && f.Mode()&os.ModeSticky != 0 && f.Mode()&0002 != 0:
+	case f.IsDir() && f.Mode()&os.ModeSticky != 0 && f.Mode()&0o002 != 0:
 		key = "tw"
-	case f.IsDir() && f.Mode()&0002 != 0:
+	case f.IsDir() && f.Mode()&0o002 != 0:
 		key = "ow"
 	case f.IsDir() && f.Mode()&os.ModeSticky != 0:
 		key = "st"
@@ -315,7 +315,7 @@ func (sm styleMap) get(f *file) tcell.Style {
 		key = "su"
 	case f.Mode()&os.ModeSetgid != 0:
 		key = "sg"
-	case f.Mode()&0111 != 0:
+	case f.Mode()&0o111 != 0:
 		key = "ex"
 	}
 
diff --git a/copy.go b/copy.go
index ea84811..336e5f9 100644
--- a/copy.go
+++ b/copy.go
@@ -11,6 +11,24 @@ import (
 	"github.com/djherbis/times"
 )
 
+type ProgressWriter struct {
+	writer io.Writer
+	nums   chan<- int64
+}
+
+func NewProgressWriter(writer io.Writer, nums chan<- int64) *ProgressWriter {
+	return &ProgressWriter{
+		writer: writer,
+		nums:   nums,
+	}
+}
+
+func (progressWriter *ProgressWriter) Write(b []byte) (int, error) {
+	n, err := progressWriter.writer.Write(b)
+	progressWriter.nums <- int64(n)
+	return n, err
+}
+
 func copySize(srcs []string) (int64, error) {
 	var total int64
 
@@ -20,14 +38,13 @@ func copySize(srcs []string) (int64, error) {
 			return total, fmt.Errorf("src does not exist: %q", src)
 		}
 
-		err = filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
+		err = filepath.Walk(src, func(_ string, info os.FileInfo, err error) error {
 			if err != nil {
 				return fmt.Errorf("walk: %s", err)
 			}
 			total += info.Size()
 			return nil
 		})
-
 		if err != nil {
 			return total, err
 		}
@@ -48,8 +65,6 @@ func copyFile(src, dst string, preserve []string, info os.FileInfo, nums chan in
 		}
 	}
 
-	buf := make([]byte, 4096)
-
 	r, err := os.Open(src)
 	if err != nil {
 		return err
@@ -61,23 +76,11 @@ func copyFile(src, dst string, preserve []string, info os.FileInfo, nums chan in
 		return err
 	}
 
-	for {
-		n, err := r.Read(buf)
-		if err != nil && err != io.EOF {
-			w.Close()
-			os.Remove(dst)
-			return err
-		}
-
-		if n == 0 {
-			break
-		}
-
-		if _, err := w.Write(buf[:n]); err != nil {
-			return err
-		}
-
-		nums <- int64(n)
+	_, err = io.Copy(NewProgressWriter(w, nums), r)
+	if err != nil {
+		w.Close()
+		os.Remove(dst)
+		return err
 	}
 
 	if err := w.Close(); err != nil {
diff --git a/doc.md b/doc.md
index d84e7bd..39abf25 100644
--- a/doc.md
+++ b/doc.md
@@ -158,7 +158,7 @@ The following options can be used to customize the behavior of lf:
 	globfilter          bool      (default false)
 	globsearch          bool      (default false)
 	hidden              bool      (default false)
-	hiddenfiles         []string  (default '.*')
+	hiddenfiles         []string  (default '.*' for Unix and '' for Windows)
 	hidecursorinactive  bool      (default false)
 	history             bool      (default true)
 	icons               bool      (default false)
@@ -765,9 +765,9 @@ Otherwise, these characters are interpreted as they are.
 
 Show hidden files.
 On Unix systems, hidden files are determined by the value of `hiddenfiles`.
-On Windows, only files with hidden attributes are considered hidden files.
+On Windows, files with hidden attributes are also considered hidden files.
 
-## hiddenfiles ([]string) (default `.*`)
+## hiddenfiles ([]string) (default `.*` for Unix and `` for Windows)
 
 List of hidden file glob patterns.
 Patterns can be given as relative or absolute paths.
@@ -1814,7 +1814,8 @@ Icons are configured using `LF_ICONS` environment variable or an icons file (ref
 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.
+The icons file (refer to the [CONFIGURATION section](https://github.com/gokcehan/lf/blob/master/doc.md#configuration)) should consist of whitespace-separated arrays with a `#` character to start comments until the end of the line.
+Each line should contain 1-3 columns, first column is filetype or filename pattern, second column is the icon, third column is an optional icon color. If there is only one column, means to disable rule for this filetype or pattern.
 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:
 
@@ -1835,3 +1836,6 @@ Default values are as follows given with their matching order in lf:
 
 A sample icons file can be found at
 https://github.com/gokcehan/lf/blob/master/etc/icons.example
+
+A sample colored icons file can be found at
+https://github.com/gokcehan/lf/blob/master/etc/icons_colored.example
diff --git a/doc.txt b/doc.txt
index 64c41a6..b21d608 100644
--- a/doc.txt
+++ b/doc.txt
@@ -2057,9 +2057,13 @@ 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
-as follows given with their matching order in lf:
+a # character to start comments until the end of the line. Each line
+should contain 1-3 columns, first column is filetype or filename
+pattern, second column is the icon, third column is an optional icon
+color. If there is only one column, means to disable rule for this
+filetype or pattern. 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:
 
     ln  l
     or  l
@@ -2078,3 +2082,6 @@ as follows given with their matching order in lf:
 
 A sample icons file can be found at
 https://github.com/gokcehan/lf/blob/master/etc/icons.example
+
+A sample colored icons file can be found at
+https://github.com/gokcehan/lf/blob/master/etc/icons_colored.example
diff --git a/etc/icons.example b/etc/icons.example
index 43dbe5d..4022691 100644
--- a/etc/icons.example
+++ b/etc/icons.example
@@ -35,6 +35,22 @@ sg      g       # SETGID
 ex             # EXEC
 fi             # FILE
 
+# disable some default filetype icons, let them choose icon by filename
+# ln             # LINK
+# or             # ORPHAN
+# tw              # STICKY_OTHER_WRITABLE
+# ow              # OTHER_WRITABLE
+# st              # STICKY
+# di             # DIR
+# pi              # FIFO
+# so              # SOCK
+# bd              # BLK
+# cd              # CHR
+# su              # SETUID
+# sg              # SETGID
+# ex              # EXEC
+# fi             # FILE
+
 # file extensions (vim-devicons)
 *.styl          
 *.sass          
diff --git a/etc/icons_colored.example b/etc/icons_colored.example
new file mode 100644
index 0000000..437e1b0
--- /dev/null
+++ b/etc/icons_colored.example
@@ -0,0 +1,377 @@
+# vim:ft=conf
+
+# These examples require Nerd Fonts or a compatible font to be used.
+# See https://www.nerdfonts.com for more information.
+
+# default values from lf (with matching order)
+# ln      l       # LINK
+# or      l       # ORPHAN
+# tw      t       # STICKY_OTHER_WRITABLE
+# ow      d       # OTHER_WRITABLE
+# st      t       # STICKY
+# di      d       # DIR
+# pi      p       # FIFO
+# so      s       # SOCK
+# bd      b       # BLK
+# cd      c       # CHR
+# su      u       # SETUID
+# sg      g       # SETGID
+# ex      x       # EXEC
+# fi      -       # FILE
+
+# file types (with matching order)
+ln             # LINK
+or             # ORPHAN
+tw      t       # STICKY_OTHER_WRITABLE
+ow             # OTHER_WRITABLE
+st      t       # STICKY
+di             # DIR
+pi      p       # FIFO
+so      s       # SOCK
+bd      b       # BLK
+cd      c       # CHR
+su      u       # SETUID
+sg      g       # SETGID
+ex             # EXEC
+fi             # FILE
+
+# disable some default filetype icons, let them choose icon by filename
+# ln             # LINK
+# or             # ORPHAN
+# tw              # STICKY_OTHER_WRITABLE
+# ow              # OTHER_WRITABLE
+# st              # STICKY
+# di             # DIR
+# pi              # FIFO
+# so              # SOCK
+# bd              # BLK
+# cd              # CHR
+# su              # SETUID
+# sg              # SETGID
+# ex              # EXEC
+# fi             # FILE
+
+# file extensions (vim-devicons)
+*.styl              00;38;2;141;193;73
+*.sass              00;38;2;245;83;133
+*.scss              00;38;2;245;83;133
+*.htm               00;38;2;227;76;38
+*.html              00;38;2;227;76;38
+*.slim              00;38;2;227;76;38
+*.haml              00;38;2;234;234;225
+*.ejs               00;38;2;203;203;65
+*.css               00;38;2;86;61;124
+*.less              00;38;2;86;61;124
+*.md                00;38;2;81;154;186
+*.mdx               00;38;2;81;154;186
+*.markdown          00;38;2;81;154;186
+*.rmd               00;38;2;81;154;186
+*.json              00;38;2;149;157;165
+*.webmanifest       00;38;2;241;224;90
+*.js                00;38;2;203;203;65
+*.mjs               00;38;2;241;224;90
+*.jsx               00;38;2;81;154;186
+*.rb                00;38;2;112;21;22
+*.gemspec           00;38;2;112;21;22
+*.rake              00;38;2;112;21;22
+*.php               00;38;2;160;116;196
+*.py                00;38;2;81;154;186
+*.pyc               00;38;2;81;154;186
+*.pyo               00;38;2;81;154;186
+*.pyd               00;38;2;81;154;186
+*.coffee            00;38;2;203;203;65
+*.mustache          00;38;2;227;121;51
+*.hbs               00;38;2;240;119;43
+*.conf              00;38;2;109;128;134
+*.ini               00;38;2;109;128;134
+*.yml               00;38;2;149;157;165
+*.yaml              00;38;2;149;157;165
+*.toml              00;38;2;109;128;134
+*.bat               00;38;2;193;241;46
+*.mk                00;38;2;109;128;134
+*.jpg               00;38;2;160;116;196
+*.jpeg              00;38;2;160;116;196
+*.bmp               00;38;2;160;116;196
+*.png               00;38;2;160;116;196
+*.webp              00;38;2;160;116;196
+*.gif               00;38;2;160;116;196
+*.ico               00;38;2;203;203;65
+*.twig              00;38;2;141;193;73
+*.cpp               00;38;2;81;154;186
+*.c++               00;38;2;89;158;255
+*.cxx               00;38;2;81;154;186
+*.cc                00;38;2;81;154;186
+*.cp                00;38;2;81;154;186
+*.c                 00;38;2;89;158;255
+*.cs            󰌛    00;38;2;89;103;6
+*.h                 00;38;2;160;116;196
+*.hh                00;38;2;160;116;196
+*.hpp               00;38;2;160;116;196
+*.hxx               00;38;2;160;116;196
+*.hs                00;38;2;160;116;196
+*.lhs               00;38;2;160;116;196
+*.nix               00;38;2;126;186;228
+*.lua               00;38;2;81;160;207
+*.java              00;38;2;204;62;68
+*.sh                00;38;2;137;224;81
+*.fish              00;38;2;137;224;81
+*.bash              00;38;2;137;224;81
+*.zsh               00;38;2;137;224;81
+*.ksh               00;38;2;137;224;81
+*.csh               00;38;2;137;224;81
+*.awk               00;38;2;137;224;81
+*.ps1               00;38;2;137;224;81
+*.ml            λ    00;38;2;227;121;51
+*.mli           λ    00;38;2;227;121;51
+*.diff              00;38;2;65;83;91
+*.db                00;38;2;218;216;216
+*.sql               00;38;2;218;216;216
+*.dump              00;38;2;218;216;216
+*.clj               00;38;2;141;193;73
+*.cljc              00;38;2;141;193;73
+*.cljs              00;38;2;81;154;186
+*.edn               00;38;2;81;154;186
+*.scala             00;38;2;204;62;68
+*.go                00;38;2;81;154;186
+*.dart              00;38;2;3;88;156
+*.xul               00;38;2;227;121;51
+*.sln               00;38;2;133;76;199
+*.suo               00;38;2;133;76;199
+*.pl                00;38;2;81;154;186
+*.pm                00;38;2;81;154;186
+*.t                 00;38;2;81;154;186
+*.rss               00;38;2;251;157;59
+'*.f#'              00;38;2;81;154;186
+*.fsscript          00;38;2;81;154;186
+*.fsx               00;38;2;81;154;186
+*.fs                00;38;2;81;154;186
+*.fsi               00;38;2;81;154;186
+*.rs                00;38;2;222;165;132
+*.rlib              00;38;2;222;165;132
+*.d                 00;38;2;66;120;25
+*.erl               00;38;2;184;57;152
+*.hrl               00;38;2;184;57;152
+*.ex                00;38;2;160;116;196
+*.exs               00;38;2;160;116;196
+*.eex               00;38;2;160;116;196
+*.leex              00;38;2;160;116;196
+*.heex              00;38;2;160;116;196
+*.vim               00;38;2;1;152;51
+*.ai                00;38;2;203;203;65
+*.psd               00;38;2;81;154;186
+*.psb               00;38;2;81;154;186
+*.ts                00;38;2;81;154;186
+*.tsx               00;38;2;81;154;186
+*.jl                00;38;2;162;112;186
+*.pp                00;38;2;255;166;26
+*.vue               00;38;2;141;193;73
+*.elm               00;38;2;81;154;186
+*.swift             00;38;2;227;121;51
+*.xcplayground      00;38;2;227;121;51
+*.tex           󰙩    00;38;2;61;97;23
+*.r             󰟔    00;38;2;53;138;91
+*.rproj         󰗆   00;38;2;53;138;91
+*.sol           󰡪    00;38;2;81;154;186
+*.pem               00;38;2;205;155;62
+
+# file names (vim-devicons) (case-insensitive not supported in lf)
+*gruntfile.coffee           00;38;2;227;121;51
+*gruntfile.js               00;38;2;227;121;51
+*gruntfile.ls               00;38;2;227;121;51
+*gulpfile.coffee            00;38;2;204;62;68
+*gulpfile.js                00;38;2;204;62;68
+*gulpfile.ls                00;38;2;204;62;68
+*mix.lock                   00;38;2;160;116;196
+*dropbox                    00;38;2;0;97;254
+*.ds_store                  00;38;2;77;90;94
+*.gitconfig                 00;38;2;65;83;91
+*.gitignore                 00;38;2;65;83;91
+*.gitattributes             00;38;2;65;83;91
+*.gitlab-ci.yml             00;38;2;226;67;41
+*.bashrc                    00;38;2;137;224;81
+*.zshrc                     00;38;2;137;224;81
+*.zshenv                    00;38;2;137;224;81
+*.zprofile                  00;38;2;137;224;81
+*.vimrc                     00;38;2;1;152;51
+*.gvimrc                    00;38;2;1;152;51
+*_vimrc                     00;38;2;1;152;51
+*_gvimrc                    00;38;2;1;152;51
+*.bashprofile               00;38;2;137;224;81
+*favicon.ico                00;38;2;203;203;65
+*license                    00;38;2;203;203;65
+*node_modules               00;38;2;232;39;75
+*react.jsx                  00;38;2;81;154;186
+*procfile                   00;38;2;160;116;196
+*dockerfile                 00;38;2;81;154;186
+*docker-compose.yml         00;38;2;81;154;186
+*docker-compose.yaml        00;38;2;81;154;186
+*compose.yml                00;38;2;81;154;186
+*compose.yaml               00;38;2;81;154;186
+*rakefile                   00;38;2;112;21;22
+*config.ru                  00;38;2;112;21;22
+*gemfile                    00;38;2;112;21;22
+*makefile                   00;38;2;109;128;134
+*cmakelists.txt             00;38;2;109;128;134
+*robots.txt             󰚩    00;38;2;109;128;134
+
+# file names (case-sensitive adaptations)
+*Gruntfile.coffee           00;38;2;227;121;51
+*Gruntfile.js               00;38;2;227;121;51
+*Gruntfile.ls               00;38;2;227;121;51
+*Gulpfile.coffee            00;38;2;204;62;68
+*Gulpfile.js                00;38;2;204;62;68
+*Gulpfile.ls                00;38;2;204;62;68
+*Dropbox                    00;38;2;0;97;254
+*.DS_Store                  00;38;2;193;241;46
+*LICENSE                    00;38;2;203;203;65
+*React.jsx                  00;38;2;81;154;186
+*Procfile                   00;38;2;160;116;196
+*Dockerfile                 00;38;2;81;154;186
+*Docker-compose.yml         00;38;2;81;154;186
+*Docker-compose.yaml        00;38;2;81;154;186
+*Rakefile                   00;38;2;112;21;22
+*Gemfile                    00;38;2;112;21;22
+*Makefile                   00;38;2;109;128;134
+*CMakeLists.txt             00;38;2;109;128;134
+
+# file patterns (vim-devicons) (patterns not supported in lf)
+# .*jquery.*\.js$             00;38;2;227;117;187
+# .*angular.*\.js$            00;38;2;226;50;55
+# .*backbone.*\.js$           00;38;2;0;113;181
+# .*require.*\.js$            00;38;2;244;74;65
+# .*materialize.*\.js$        00;38;2;238;110;115
+# .*materialize.*\.css$       00;38;2;238;110;115
+# .*mootools.*\.js$           00;38;2;236;236;236
+# .*vimrc.*                   00;38;2;1;152;51
+# Vagrantfile$                00;38;2;21;99;255
+
+# file patterns (file name adaptations)
+*jquery.min.js              00;38;2;227;117;187
+*angular.min.js             00;38;2;226;50;55
+*backbone.min.js            00;38;2;0;113;181
+*require.min.js             00;38;2;244;74;65
+*materialize.min.js         00;38;2;238;110;115
+*materialize.min.css        00;38;2;238;110;115
+*mootools.min.js            00;38;2;236;236;236
+*vimrc                      00;38;2;1;152;51
+Vagrantfile                 00;38;2;21;99;255
+
+# archives or compressed (extensions from dircolors defaults)
+*.tar   
+*.tgz   
+*.arc   
+*.arj   
+*.taz   
+*.lha   
+*.lz4   
+*.lzh   
+*.lzma  
+*.tlz   
+*.txz   
+*.tzo   
+*.t7z   
+*.zip   
+*.z     
+*.dz    
+*.gz    
+*.lrz   
+*.lz    
+*.lzo   
+*.xz    
+*.zst   
+*.tzst  
+*.bz2   
+*.bz    
+*.tbz   
+*.tbz2  
+*.tz    
+*.deb   
+*.rpm   
+*.jar   
+*.war   
+*.ear   
+*.sar   
+*.rar   
+*.alz   
+*.ace   
+*.zoo   
+*.cpio  
+*.7z    
+*.rz    
+*.cab   
+*.wim   
+*.swm   
+*.dwm   
+*.esd   
+
+# image formats (extensions from dircolors defaults)
+*.jpg   
+*.jpeg  
+*.mjpg  
+*.mjpeg 
+*.gif   
+*.bmp   
+*.pbm   
+*.pgm   
+*.ppm   
+*.tga   
+*.xbm   
+*.xpm   
+*.tif   
+*.tiff  
+*.png   
+*.svg   
+*.svgz  
+*.mng   
+*.pcx   
+*.mov   
+*.mpg   
+*.mpeg  
+*.m2v   
+*.mkv   
+*.webm  
+*.ogm   
+*.mp4   
+*.m4v   
+*.mp4v  
+*.vob   
+*.qt    
+*.nuv   
+*.wmv   
+*.asf   
+*.rm    
+*.rmvb  
+*.flc   
+*.avi   
+*.fli   
+*.flv   
+*.gl    
+*.dl    
+*.xcf   
+*.xwd   
+*.yuv   
+*.cgm   
+*.emf   
+*.ogv   
+*.ogx   
+
+# audio formats (extensions from dircolors defaults)
+*.aac   
+*.au    
+*.flac  
+*.m4a   
+*.mid   
+*.midi  
+*.mka   
+*.mp3   
+*.mpc   
+*.ogg   
+*.ra    
+*.wav   
+*.oga   
+*.opus  
+*.spx   
+*.xspf  
+
+# other formats
+*.pdf        00;38;2;179;11;0
diff --git a/etc/lfcd.nu b/etc/lfcd.nu
index 168dc92..5f7bf63 100644
--- a/etc/lfcd.nu
+++ b/etc/lfcd.nu
@@ -21,5 +21,5 @@
 
 # For nushell version >= 0.87.0
 def --env --wrapped lfcd [...args: string] { 
-  cd (lf -print-last-dir $args)
+  cd (lf -print-last-dir ...$args)
 }
diff --git a/go.mod b/go.mod
index eb3cdc4..9e88e88 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.19.0
-	golang.org/x/term v0.19.0
+	golang.org/x/sys v0.21.0
+	golang.org/x/term v0.21.0
 )
 
 require (
diff --git a/go.sum b/go.sum
index 1548183..cda2b25 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.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
-golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
+golang.org/x/sys v0.21.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.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
-golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
+golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
+golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
 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 5583864..49a522a 100644
--- a/icons.go
+++ b/icons.go
@@ -5,16 +5,32 @@ import (
 	"os"
 	"path/filepath"
 	"strings"
+
+	"github.com/gdamore/tcell/v2"
 )
 
+type iconDef struct {
+	icon     string
+	hasStyle bool
+	style    tcell.Style
+}
+
 type iconMap struct {
-	icons         map[string]string
+	icons         map[string]iconDef
 	useLinkTarget bool
 }
 
+func iconWithoutStyle(icon string) iconDef {
+	return iconDef{icon, false, tcell.StyleDefault}
+}
+
+func iconWithStyle(icon string, style tcell.Style) iconDef {
+	return iconDef{icon, true, style}
+}
+
 func parseIcons() iconMap {
 	im := iconMap{
-		icons:         make(map[string]string),
+		icons:         make(map[string]iconDef),
 		useLinkTarget: false,
 	}
 
@@ -60,14 +76,14 @@ func (im *iconMap) parseFile(path string) {
 	}
 	defer f.Close()
 
-	pairs, err := readPairs(f)
+	arrs, err := readArrays(f, 1, 3)
 	if err != nil {
 		log.Printf("reading icons file: %s", err)
 		return
 	}
 
-	for _, pair := range pairs {
-		im.parsePair(pair)
+	for _, arr := range arrs {
+		im.parseArray(arr)
 	}
 }
 
@@ -84,12 +100,12 @@ func (im *iconMap) parseEnv(env string) {
 			return
 		}
 
-		im.parsePair(pair)
+		im.parseArray(pair)
 	}
 }
 
-func (im *iconMap) parsePair(pair []string) {
-	key, val := pair[0], pair[1]
+func (im *iconMap) parseArray(arr []string) {
+	key := arr[0]
 
 	key = replaceTilde(key)
 
@@ -97,14 +113,23 @@ func (im *iconMap) parsePair(pair []string) {
 		key = filepath.Clean(key)
 	}
 
-	if key == "ln" && val == "target" {
-		im.useLinkTarget = true
+	switch len(arr) {
+	case 1:
+		delete(im.icons, key)
+	case 2:
+		icon := arr[1]
+		if key == "ln" && icon == "target" {
+			im.useLinkTarget = true
+		} else {
+			im.icons[key] = iconWithoutStyle(icon)
+		}
+	case 3:
+		icon, color := arr[1], arr[2]
+		im.icons[key] = iconWithStyle(icon, applyAnsiCodes(color, tcell.StyleDefault))
 	}
-
-	im.icons[key] = val
 }
 
-func (im iconMap) get(f *file) string {
+func (im iconMap) get(f *file) iconDef {
 	if val, ok := im.icons[f.path]; ok {
 		return val
 	}
@@ -122,9 +147,9 @@ func (im iconMap) get(f *file) string {
 		key = "ln"
 	case f.linkState == broken:
 		key = "or"
-	case f.IsDir() && f.Mode()&os.ModeSticky != 0 && f.Mode()&0002 != 0:
+	case f.IsDir() && f.Mode()&os.ModeSticky != 0 && f.Mode()&0o002 != 0:
 		key = "tw"
-	case f.IsDir() && f.Mode()&0002 != 0:
+	case f.IsDir() && f.Mode()&0o002 != 0:
 		key = "ow"
 	case f.IsDir() && f.Mode()&os.ModeSticky != 0:
 		key = "st"
@@ -142,7 +167,7 @@ func (im iconMap) get(f *file) string {
 		key = "su"
 	case f.Mode()&os.ModeSetgid != 0:
 		key = "sg"
-	case f.Mode()&0111 != 0:
+	case f.Mode()&0o111 != 0:
 		key = "ex"
 	}
 
@@ -170,5 +195,5 @@ func (im iconMap) get(f *file) string {
 		return val
 	}
 
-	return " "
+	return iconWithoutStyle(" ")
 }
diff --git a/misc.go b/misc.go
index 63b162e..d877d64 100644
--- a/misc.go
+++ b/misc.go
@@ -152,12 +152,12 @@ func splitWord(s string) (word, rest string) {
 	return
 }
 
-// This function reads whitespace separated string pairs at each line. Single
+// This function reads whitespace separated string arrays at each line. Single
 // or double quotes can be used to escape whitespaces. Hash characters can be
 // used to add a comment until the end of line. Leading and trailing space is
 // trimmed. Empty lines are skipped.
-func readPairs(r io.Reader) ([][]string, error) {
-	var pairs [][]string
+func readArrays(r io.Reader, min_cols, max_cols int) ([][]string, error) {
+	var arrays [][]string
 	s := bufio.NewScanner(r)
 	for s.Scan() {
 		line := s.Text()
@@ -182,7 +182,7 @@ func readPairs(r io.Reader) ([][]string, error) {
 		}
 
 		squote, dquote = false, false
-		pair := strings.FieldsFunc(line, func(r rune) bool {
+		arr := strings.FieldsFunc(line, func(r rune) bool {
 			if r == '\'' && !dquote {
 				squote = !squote
 			} else if r == '"' && !squote {
@@ -191,14 +191,17 @@ func readPairs(r io.Reader) ([][]string, error) {
 			return !squote && !dquote && unicode.IsSpace(r)
 		})
 
-		if len(pair) != 2 {
-			return nil, fmt.Errorf("expected pair but found: %s", s.Text())
+		if len(arr) < min_cols || len(arr) > max_cols {
+			if min_cols == max_cols {
+				return nil, fmt.Errorf("expected %d columns but found: %s", min_cols, s.Text())
+			}
+			return nil, fmt.Errorf("expected %d~%d columns but found: %s", min_cols, max_cols, s.Text())
 		}
 
-		for i := 0; i < len(pair); i++ {
+		for i := 0; i < len(arr); i++ {
 			squote, dquote = false, false
-			buf := make([]rune, 0, len(pair[i]))
-			for _, r := range pair[i] {
+			buf := make([]rune, 0, len(arr[i]))
+			for _, r := range arr[i] {
 				if r == '\'' && !dquote {
 					squote = !squote
 					continue
@@ -209,13 +212,17 @@ func readPairs(r io.Reader) ([][]string, error) {
 				}
 				buf = append(buf, r)
 			}
-			pair[i] = string(buf)
+			arr[i] = string(buf)
 		}
 
-		pairs = append(pairs, pair)
+		arrays = append(arrays, arr)
 	}
 
-	return pairs, nil
+	return arrays, nil
+}
+
+func readPairs(r io.Reader) ([][]string, error) {
+	return readArrays(r, 2, 2)
 }
 
 // This function converts a size in bytes to a human readable form using metric
diff --git a/nav.go b/nav.go
index 0a16cfb..7a5fba6 100644
--- a/nav.go
+++ b/nav.go
@@ -88,7 +88,7 @@ func readdir(path string) ([]*file, error) {
 				dirSize:    -1,
 				accessTime: time.Unix(0, 0),
 				changeTime: time.Unix(0, 0),
-				ext:        getFileExtension(lstat),
+				ext:        "",
 				err:        err,
 			})
 			continue
@@ -751,7 +751,6 @@ func (nav *nav) previewLoop(ui *ui) {
 	}
 }
 
-//lint:ignore U1000 This function is not used on Windows
 func matchPattern(pattern, name, path string) bool {
 	s := name
 
@@ -890,25 +889,29 @@ func (nav *nav) preview(path string, win *win) {
 
 	// bufio.Scanner can't handle files containing long lines if they exceed the
 	// size of its internal buffer
-	addLine := true
+	line := []byte{}
 	for len(reg.lines) < win.h {
-		line, isPrefix, err := reader.ReadLine()
+		bytes, isPrefix, err := reader.ReadLine()
 		if err != nil {
+			if len(line) > 0 {
+				reg.lines = append(reg.lines, string(line))
+			}
 			break
 		}
 
-		for _, r := range line {
-			if r == 0 {
+		for _, byte := range bytes {
+			if byte == 0 {
 				reg.lines = []string{"\033[7mbinary\033[0m"}
 				return
 			}
 		}
 
-		if addLine {
+		line = append(line, bytes...)
+
+		if !isPrefix {
 			reg.lines = append(reg.lines, string(line))
+			line = []byte{}
 		}
-
-		addLine = !isPrefix
 	}
 }
 
@@ -1884,7 +1887,7 @@ func (nav *nav) writeMarks() error {
 	sort.Strings(keys)
 
 	for _, k := range keys {
-		_, err = f.WriteString(fmt.Sprintf("%s:%s\n", k, nav.marks[k]))
+		_, err = fmt.Fprintf(f, "%s:%s\n", k, nav.marks[k])
 		if err != nil {
 			return fmt.Errorf("writing marks file: %s", err)
 		}
@@ -1945,7 +1948,7 @@ func (nav *nav) writeTags() error {
 	sort.Strings(keys)
 
 	for _, k := range keys {
-		_, err = f.WriteString(fmt.Sprintf("%s:%s\n", k, nav.tags[k]))
+		_, err = fmt.Fprintf(f, "%s:%s\n", k, nav.tags[k])
 		if err != nil {
 			return fmt.Errorf("writing tags file: %s", err)
 		}
diff --git a/opts.go b/opts.go
index 035de65..171c5d8 100644
--- a/opts.go
+++ b/opts.go
@@ -235,7 +235,7 @@ func init() {
 	gOpts.truncatechar = "~"
 	gOpts.truncatepct = 100
 	gOpts.ratios = []int{1, 2, 3}
-	gOpts.hiddenfiles = []string{".*"}
+	gOpts.hiddenfiles = gDefaultHiddenFiles
 	gOpts.history = true
 	gOpts.info = nil
 	gOpts.ruler = nil
diff --git a/os.go b/os.go
index 962bb41..ac2d2f6 100644
--- a/os.go
+++ b/os.go
@@ -24,10 +24,11 @@ var (
 )
 
 var (
-	gDefaultShell      = "sh"
-	gDefaultShellFlag  = "-c"
-	gDefaultSocketProt = "unix"
-	gDefaultSocketPath string
+	gDefaultShell       = "sh"
+	gDefaultShellFlag   = "-c"
+	gDefaultSocketProt  = "unix"
+	gDefaultSocketPath  string
+	gDefaultHiddenFiles = []string{".*"}
 )
 
 var (
@@ -177,11 +178,11 @@ func setDefaults() {
 }
 
 func setUserUmask() {
-	unix.Umask(0077)
+	unix.Umask(0o077)
 }
 
 func isExecutable(f os.FileInfo) bool {
-	return f.Mode()&0111 != 0
+	return f.Mode()&0o111 != 0
 }
 
 func isHidden(f os.FileInfo, path string, hiddenfiles []string) bool {
diff --git a/os_windows.go b/os_windows.go
index a07821f..db9ef56 100644
--- a/os_windows.go
+++ b/os_windows.go
@@ -23,10 +23,11 @@ var (
 var envPathExt = os.Getenv("PATHEXT")
 
 var (
-	gDefaultShell      = "cmd"
-	gDefaultShellFlag  = "/c"
-	gDefaultSocketProt = "unix"
-	gDefaultSocketPath string
+	gDefaultShell       = "cmd"
+	gDefaultShellFlag   = "/c"
+	gDefaultSocketProt  = "unix"
+	gDefaultSocketPath  string
+	gDefaultHiddenFiles []string
 )
 
 var (
@@ -181,7 +182,22 @@ func isHidden(f os.FileInfo, path string, hiddenfiles []string) bool {
 	if err != nil {
 		return false
 	}
-	return attrs&windows.FILE_ATTRIBUTE_HIDDEN != 0
+
+	if attrs&windows.FILE_ATTRIBUTE_HIDDEN != 0 {
+		return true
+	}
+
+	hidden := false
+	for _, pattern := range hiddenfiles {
+		matched := matchPattern(strings.TrimPrefix(pattern, "!"), f.Name(), path)
+		if strings.HasPrefix(pattern, "!") && matched {
+			hidden = false
+		} else if matched {
+			hidden = true
+		}
+	}
+
+	return hidden
 }
 
 func userName(f os.FileInfo) string {
diff --git a/server.go b/server.go
index b278b13..7bb6e73 100644
--- a/server.go
+++ b/server.go
@@ -17,7 +17,7 @@ var (
 
 func serve() {
 	if gLogPath != "" {
-		f, err := os.OpenFile(gLogPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
+		f, err := os.OpenFile(gLogPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
 		if err != nil {
 			panic(err)
 		}
diff --git a/ui.go b/ui.go
index 5aabf66..f7684ec 100644
--- a/ui.go
+++ b/ui.go
@@ -293,27 +293,27 @@ func fileInfo(f *file, d *dir) string {
 	for _, s := range getInfo(d.path) {
 		switch s {
 		case "size":
-			if !(f.IsDir() && gOpts.dircounts) {
-				var sz string
-				if f.IsDir() && f.dirSize < 0 {
-					sz = "-"
-				} else {
-					sz = humanize(f.TotalSize())
+			if f.IsDir() && gOpts.dircounts {
+				switch {
+				case f.dirCount < -1:
+					info = fmt.Sprintf("%s    !", info)
+				case f.dirCount < 0:
+					info = fmt.Sprintf("%s    ?", info)
+				case f.dirCount < 1000:
+					info = fmt.Sprintf("%s %4d", info, f.dirCount)
+				default:
+					info = fmt.Sprintf("%s 999+", info)
 				}
-				info = fmt.Sprintf("%s %4s", info, sz)
 				continue
 			}
 
-			switch {
-			case f.dirCount < -1:
-				info = fmt.Sprintf("%s    !", info)
-			case f.dirCount < 0:
-				info = fmt.Sprintf("%s    ?", info)
-			case f.dirCount < 1000:
-				info = fmt.Sprintf("%s %4d", info, f.dirCount)
-			default:
-				info = fmt.Sprintf("%s 999+", info)
+			var sz string
+			if f.IsDir() && f.dirSize < 0 {
+				sz = "-"
+			} else {
+				sz = humanize(f.TotalSize())
 			}
+			info = fmt.Sprintf("%s %4s", info, sz)
 		case "time":
 			info = fmt.Sprintf("%s %*s", info, max(len(gOpts.infotimefmtnew), len(gOpts.infotimefmtold)), infotimefmt(f.ModTime()))
 		case "atime":
@@ -450,8 +450,11 @@ func (win *win) printDir(ui *ui, dir *dir, context *dirContext, dirStyle *dirSty
 		// leave space for displaying the tag
 		s = append(s, ' ')
 
+		var icon iconDef
+
 		if gOpts.icons {
-			s = append(s, []rune(dirStyle.icons.get(f))...)
+			icon = dirStyle.icons.get(f)
+			s = append(s, []rune(icon.icon)...)
 			s = append(s, ' ')
 		}
 
@@ -471,7 +474,7 @@ func (win *win) printDir(ui *ui, dir *dir, context *dirContext, dirStyle *dirSty
 			filename = append(filename, []rune(gOpts.truncatechar)...)
 			filename = append(filename, lastPart...)
 		}
-		for i := runeSliceWidth(filename); i < maxFilenameWidth; i++ {
+		for j := runeSliceWidth(filename); j < maxFilenameWidth; j++ {
 			filename = append(filename, ' ')
 		}
 		s = append(s, filename...)
@@ -497,6 +500,10 @@ func (win *win) printDir(ui *ui, dir *dir, context *dirContext, dirStyle *dirSty
 		styledFilename := fmt.Sprintf(cursorescapefmt, string(s))
 		win.print(ui.screen, lnwidth+1, i, st, styledFilename)
 
+		if icon.hasStyle && i != dir.pos {
+			win.print(ui.screen, lnwidth+2, i, icon.style, icon.icon)
+		}
+
 		tag, ok := context.tags[path]
 		if ok {
 			if i == dir.pos {
@@ -778,11 +785,11 @@ func (ui *ui) drawPromptLine(nav *nav) {
 
 	var prompt string
 
-	prompt = strings.Replace(gOpts.promptfmt, "%u", gUser.Username, -1)
-	prompt = strings.Replace(prompt, "%h", gHostname, -1)
-	prompt = strings.Replace(prompt, "%f", fname, -1)
+	prompt = strings.ReplaceAll(gOpts.promptfmt, "%u", gUser.Username)
+	prompt = strings.ReplaceAll(prompt, "%h", gHostname)
+	prompt = strings.ReplaceAll(prompt, "%f", fname)
 
-	if printLength(strings.Replace(strings.Replace(prompt, "%w", pwd, -1), "%d", pwd, -1)) > ui.promptWin.w {
+	if printLength(strings.ReplaceAll(strings.ReplaceAll(prompt, "%w", pwd), "%d", pwd)) > ui.promptWin.w {
 		names := strings.Split(pwd, sep)
 		for i := range names {
 			if names[i] == "" {
@@ -790,23 +797,23 @@ func (ui *ui) drawPromptLine(nav *nav) {
 			}
 			r, _ := utf8.DecodeRuneInString(names[i])
 			names[i] = string(r)
-			if printLength(strings.Replace(strings.Replace(prompt, "%w", strings.Join(names, sep), -1), "%d", strings.Join(names, sep), -1)) <= ui.promptWin.w {
+			if printLength(strings.ReplaceAll(strings.ReplaceAll(prompt, "%w", strings.Join(names, sep)), "%d", strings.Join(names, sep))) <= ui.promptWin.w {
 				break
 			}
 		}
 		pwd = strings.Join(names, sep)
 	}
 
-	prompt = strings.Replace(prompt, "%w", pwd, -1)
+	prompt = strings.ReplaceAll(prompt, "%w", pwd)
 	if !strings.HasSuffix(pwd, sep) {
 		pwd += sep
 	}
-	prompt = strings.Replace(prompt, "%d", pwd, -1)
+	prompt = strings.ReplaceAll(prompt, "%d", pwd)
 
 	if len(dir.filter) != 0 {
-		prompt = strings.Replace(prompt, "%F", fmt.Sprint(dir.filter), -1)
+		prompt = strings.ReplaceAll(prompt, "%F", fmt.Sprint(dir.filter))
 	} else {
-		prompt = strings.Replace(prompt, "%F", "", -1)
+		prompt = strings.ReplaceAll(prompt, "%F", "")
 	}
 
 	// spacer
@@ -814,7 +821,7 @@ func (ui *ui) drawPromptLine(nav *nav) {
 	if avail > 0 {
 		prompt = strings.Replace(prompt, "%S", strings.Repeat(" ", avail), 1)
 	}
-	prompt = strings.Replace(prompt, "%S", "", -1)
+	prompt = strings.ReplaceAll(prompt, "%S", "")
 
 	ui.promptWin.print(ui.screen, 0, 0, st, prompt)
 }
@@ -1109,7 +1116,6 @@ func (ui *ui) draw(nav *nav) {
 		ui.sxScreen.lastFile = ui.regPrev.path
 		ui.sxScreen.showSixels()
 	}
-
 }
 
 func findBinds(keys map[string]expr, prefix string) (binds map[string]expr, ok bool) {


Test patch:
diff --git a/misc_test.go b/misc_test.go
index 52b2628..f409560 100644
--- a/misc_test.go
+++ b/misc_test.go
@@ -217,23 +217,27 @@ func TestSplitWord(t *testing.T) {
 	}
 }
 
-func TestReadPairs(t *testing.T) {
+func TestReadArrays(t *testing.T) {
 	tests := []struct {
-		s   string
-		exp [][]string
+		s        string
+		min_cols int
+		max_cols int
+		exp      [][]string
 	}{
-		{"foo bar", [][]string{{"foo", "bar"}}},
-		{"foo bar ", [][]string{{"foo", "bar"}}},
-		{" foo bar", [][]string{{"foo", "bar"}}},
-		{" foo bar ", [][]string{{"foo", "bar"}}},
-		{"foo bar#baz", [][]string{{"foo", "bar"}}},
-		{"foo bar #baz", [][]string{{"foo", "bar"}}},
-		{`'foo#baz' bar`, [][]string{{"foo#baz", "bar"}}},
-		{`"foo#baz" bar`, [][]string{{"foo#baz", "bar"}}},
+		{"foo bar", 2, 2, [][]string{{"foo", "bar"}}},
+		{"foo bar ", 2, 2, [][]string{{"foo", "bar"}}},
+		{" foo bar", 2, 2, [][]string{{"foo", "bar"}}},
+		{" foo bar ", 2, 2, [][]string{{"foo", "bar"}}},
+		{"foo bar#baz", 2, 2, [][]string{{"foo", "bar"}}},
+		{"foo bar #baz", 2, 2, [][]string{{"foo", "bar"}}},
+		{`'foo#baz' bar`, 2, 2, [][]string{{"foo#baz", "bar"}}},
+		{`"foo#baz" bar`, 2, 2, [][]string{{"foo#baz", "bar"}}},
+		{"foo bar baz", 3, 3, [][]string{{"foo", "bar", "baz"}}},
+		{`"foo bar baz"`, 1, 1, [][]string{{"foo bar baz"}}},
 	}
 
 	for _, test := range tests {
-		if got, _ := readPairs(strings.NewReader(test.s)); !reflect.DeepEqual(got, test.exp) {
+		if got, _ := readArrays(strings.NewReader(test.s), test.min_cols, test.max_cols); !reflect.DeepEqual(got, test.exp) {
 			t.Errorf("at input '%v' expected '%v' but got '%v'", test.s, test.exp, got)
 		}
 	}
@@ -326,7 +330,7 @@ func TestGetFileExtension(t *testing.T) {
 	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)
+				t.Errorf("at input '%s' expected '%s' but got '%s'", test.fileName, test.expectedExtension, got)
 			}
 		})
 	}