github.com/elves/elvish@v0.15.0/pkg/cli/addons/navigation/cursor.go (about)

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