github.com/kolbycrouch/elvish@v0.14.1-0.20210614162631-215b9ac1c423/pkg/cli/mode/navigation_fs.go (about)

     1  package mode
     2  
     3  import (
     4  	"errors"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  	"unicode/utf8"
     9  
    10  	"src.elv.sh/pkg/cli/lscolors"
    11  	"src.elv.sh/pkg/ui"
    12  )
    13  
    14  // NavigationCursor represents a cursor for navigating in a potentially virtual
    15  // filesystem.
    16  type NavigationCursor interface {
    17  	// Current returns a File that represents the current directory.
    18  	Current() (NavigationFile, error)
    19  	// Parent returns a File that represents the parent directory. It may return
    20  	// nil if the current directory is the root of the filesystem.
    21  	Parent() (NavigationFile, error)
    22  	// Ascend navigates to the parent directory.
    23  	Ascend() error
    24  	// Descend navigates to the named child directory.
    25  	Descend(name string) error
    26  }
    27  
    28  // NavigationFile represents a potentially virtual file.
    29  type NavigationFile interface {
    30  	// Name returns the name of the file.
    31  	Name() string
    32  	// ShowName returns a styled filename.
    33  	ShowName() ui.Text
    34  	// IsDirDeep returns whether the file is itself a directory or a symlink to
    35  	// a directory.
    36  	IsDirDeep() bool
    37  	// Read returns either a list of File's if the File represents a directory,
    38  	// a (possibly incomplete) slice of bytes if the File represents a normal
    39  	// file, or an error if the File cannot be read.
    40  	Read() ([]NavigationFile, []byte, error)
    41  }
    42  
    43  // NewOSNavigationCursor returns a NavigationCursor backed by the OS.
    44  func NewOSNavigationCursor() NavigationCursor {
    45  	return osCursor{lscolors.GetColorist()}
    46  }
    47  
    48  type osCursor struct{ colorist lscolors.Colorist }
    49  
    50  func (c osCursor) Current() (NavigationFile, error) {
    51  	abs, err := filepath.Abs(".")
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  	return file{filepath.Base(abs), abs, os.ModeDir, c.colorist}, nil
    56  }
    57  
    58  func (c osCursor) Parent() (NavigationFile, error) {
    59  	if abs, _ := filepath.Abs("."); abs == "/" {
    60  		return emptyDir{}, nil
    61  	}
    62  	abs, err := filepath.Abs("..")
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  	return file{filepath.Base(abs), abs, os.ModeDir, c.colorist}, nil
    67  }
    68  
    69  func (c osCursor) Ascend() error { return os.Chdir("..") }
    70  
    71  func (c osCursor) Descend(name string) error { return os.Chdir(name) }
    72  
    73  type emptyDir struct{}
    74  
    75  func (emptyDir) Name() string                            { return "" }
    76  func (emptyDir) ShowName() ui.Text                       { return nil }
    77  func (emptyDir) IsDirDeep() bool                         { return true }
    78  func (emptyDir) Read() ([]NavigationFile, []byte, error) { return []NavigationFile{}, nil, nil }
    79  
    80  type file struct {
    81  	name     string
    82  	path     string
    83  	mode     os.FileMode
    84  	colorist lscolors.Colorist
    85  }
    86  
    87  func (f file) Name() string { return f.name }
    88  
    89  func (f file) ShowName() ui.Text {
    90  	sgrStyle := f.colorist.GetStyle(f.path)
    91  	return ui.Text{&ui.Segment{
    92  		Style: ui.StyleFromSGR(sgrStyle), Text: f.name}}
    93  }
    94  
    95  func (f file) IsDirDeep() bool {
    96  	if f.mode.IsDir() {
    97  		// File itself is a directory; return true and save a stat call.
    98  		return true
    99  	}
   100  	info, err := os.Stat(f.path)
   101  	return err == nil && info.IsDir()
   102  }
   103  
   104  const previewBytes = 64 * 1024
   105  
   106  var (
   107  	errDevice     = errors.New("no preview for device file")
   108  	errNamedPipe  = errors.New("no preview for named pipe")
   109  	errSocket     = errors.New("no preview for socket file")
   110  	errCharDevice = errors.New("no preview for char device")
   111  	errNonUTF8    = errors.New("no preview for non-utf8 file")
   112  )
   113  
   114  var specialFileModes = []struct {
   115  	mode os.FileMode
   116  	err  error
   117  }{
   118  	{os.ModeDevice, errDevice},
   119  	{os.ModeNamedPipe, errNamedPipe},
   120  	{os.ModeSocket, errSocket},
   121  	{os.ModeCharDevice, errCharDevice},
   122  }
   123  
   124  func (f file) Read() ([]NavigationFile, []byte, error) {
   125  	ff, err := os.Open(f.path)
   126  	if err != nil {
   127  		return nil, nil, err
   128  	}
   129  	defer ff.Close()
   130  
   131  	info, err := ff.Stat()
   132  	if err != nil {
   133  		return nil, nil, err
   134  	}
   135  
   136  	if info.IsDir() {
   137  		infos, err := ff.Readdir(0)
   138  		if err != nil {
   139  			return nil, nil, err
   140  		}
   141  		files := make([]NavigationFile, len(infos))
   142  		for i, info := range infos {
   143  			files[i] = file{
   144  				info.Name(),
   145  				filepath.Join(f.path, info.Name()),
   146  				info.Mode(),
   147  				f.colorist,
   148  			}
   149  		}
   150  		return files, nil, err
   151  	}
   152  
   153  	for _, special := range specialFileModes {
   154  		if info.Mode()&special.mode != 0 {
   155  			return nil, nil, special.err
   156  		}
   157  	}
   158  
   159  	var buf [previewBytes]byte
   160  	nr, err := ff.Read(buf[:])
   161  	if err != nil && err != io.EOF {
   162  		return nil, nil, err
   163  	}
   164  
   165  	content := buf[:nr]
   166  	if !utf8.Valid(content) {
   167  		return nil, nil, errNonUTF8
   168  	}
   169  
   170  	return nil, content, nil
   171  }