github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/cli/modes/navigation_fs.go (about)

     1  package modes
     2  
     3  import (
     4  	"errors"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  	"unicode/utf8"
     9  
    10  	"github.com/markusbkk/elvish/pkg/cli/lscolors"
    11  	"github.com/markusbkk/elvish/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(chdir func(string) error) NavigationCursor {
    45  	return osCursor{chdir, lscolors.GetColorist()}
    46  }
    47  
    48  type osCursor struct {
    49  	chdir    func(string) error
    50  	colorist lscolors.Colorist
    51  }
    52  
    53  func (c osCursor) Current() (NavigationFile, error) {
    54  	abs, err := filepath.Abs(".")
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  	return file{filepath.Base(abs), abs, os.ModeDir, c.colorist}, nil
    59  }
    60  
    61  func (c osCursor) Parent() (NavigationFile, error) {
    62  	if abs, _ := filepath.Abs("."); abs == "/" {
    63  		return emptyDir{}, nil
    64  	}
    65  	abs, err := filepath.Abs("..")
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  	return file{filepath.Base(abs), abs, os.ModeDir, c.colorist}, nil
    70  }
    71  
    72  func (c osCursor) Ascend() error { return c.chdir("..") }
    73  
    74  func (c osCursor) Descend(name string) error { return c.chdir(name) }
    75  
    76  type emptyDir struct{}
    77  
    78  func (emptyDir) Name() string                            { return "" }
    79  func (emptyDir) ShowName() ui.Text                       { return nil }
    80  func (emptyDir) IsDirDeep() bool                         { return true }
    81  func (emptyDir) Read() ([]NavigationFile, []byte, error) { return []NavigationFile{}, nil, nil }
    82  
    83  type file struct {
    84  	name     string
    85  	path     string
    86  	mode     os.FileMode
    87  	colorist lscolors.Colorist
    88  }
    89  
    90  func (f file) Name() string { return f.name }
    91  
    92  func (f file) ShowName() ui.Text {
    93  	sgrStyle := f.colorist.GetStyle(f.path)
    94  	return ui.Text{&ui.Segment{
    95  		Style: ui.StyleFromSGR(sgrStyle), Text: f.name}}
    96  }
    97  
    98  func (f file) IsDirDeep() bool {
    99  	if f.mode.IsDir() {
   100  		// File itself is a directory; return true and save a stat call.
   101  		return true
   102  	}
   103  	info, err := os.Stat(f.path)
   104  	return err == nil && info.IsDir()
   105  }
   106  
   107  const previewBytes = 64 * 1024
   108  
   109  var (
   110  	errDevice     = errors.New("no preview for device file")
   111  	errNamedPipe  = errors.New("no preview for named pipe")
   112  	errSocket     = errors.New("no preview for socket file")
   113  	errCharDevice = errors.New("no preview for char device")
   114  	errNonUTF8    = errors.New("no preview for non-utf8 file")
   115  )
   116  
   117  var specialFileModes = []struct {
   118  	mode os.FileMode
   119  	err  error
   120  }{
   121  	{os.ModeDevice, errDevice},
   122  	{os.ModeNamedPipe, errNamedPipe},
   123  	{os.ModeSocket, errSocket},
   124  	{os.ModeCharDevice, errCharDevice},
   125  }
   126  
   127  func (f file) Read() ([]NavigationFile, []byte, error) {
   128  	ff, err := os.Open(f.path)
   129  	if err != nil {
   130  		return nil, nil, err
   131  	}
   132  	defer ff.Close()
   133  
   134  	info, err := ff.Stat()
   135  	if err != nil {
   136  		return nil, nil, err
   137  	}
   138  
   139  	if info.IsDir() {
   140  		infos, err := ff.Readdir(0)
   141  		if err != nil {
   142  			return nil, nil, err
   143  		}
   144  		files := make([]NavigationFile, len(infos))
   145  		for i, info := range infos {
   146  			files[i] = file{
   147  				info.Name(),
   148  				filepath.Join(f.path, info.Name()),
   149  				info.Mode(),
   150  				f.colorist,
   151  			}
   152  		}
   153  		return files, nil, err
   154  	}
   155  
   156  	for _, special := range specialFileModes {
   157  		if info.Mode()&special.mode != 0 {
   158  			return nil, nil, special.err
   159  		}
   160  	}
   161  
   162  	var buf [previewBytes]byte
   163  	nr, err := ff.Read(buf[:])
   164  	if err != nil && err != io.EOF {
   165  		return nil, nil, err
   166  	}
   167  
   168  	content := buf[:nr]
   169  	if !utf8.Valid(content) {
   170  		return nil, nil, errNonUTF8
   171  	}
   172  
   173  	return nil, content, nil
   174  }