src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/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  	"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(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  	errNamedPipe  = errors.New("no preview for named pipe")
   111  	errDevice     = errors.New("no preview for device file")
   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.ModeNamedPipe, errNamedPipe},
   122  	{os.ModeDevice, errDevice},
   123  	{os.ModeSocket, errSocket},
   124  	{os.ModeCharDevice, errCharDevice},
   125  }
   126  
   127  func (f file) Read() ([]NavigationFile, []byte, error) {
   128  	// On Unix, opening a named pipe for reading is blocking when there are no
   129  	// writers, so we need to do this check at the very beginning of this
   130  	// function.
   131  	//
   132  	// TODO: There is still a chance that the file has changed between when
   133  	// f.mode is populated and the os.Open call below, in which case the os.Open
   134  	// call can still block. This can be fixed by opening the file in async mode
   135  	// and setting a timeout on the reads. Reading the file asynchronously is
   136  	// also desirable behavior more generally for the navigation mode to remain
   137  	// usable on slower filesystems.
   138  	if f.mode&os.ModeNamedPipe != 0 {
   139  		return nil, nil, errNamedPipe
   140  	}
   141  
   142  	ff, err := os.Open(f.path)
   143  	if err != nil {
   144  		return nil, nil, err
   145  	}
   146  	defer ff.Close()
   147  
   148  	info, err := ff.Stat()
   149  	if err != nil {
   150  		return nil, 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  	if info.IsDir() {
   160  		infos, err := ff.Readdir(0)
   161  		if err != nil {
   162  			return nil, nil, err
   163  		}
   164  		files := make([]NavigationFile, len(infos))
   165  		for i, info := range infos {
   166  			files[i] = file{
   167  				info.Name(),
   168  				filepath.Join(f.path, info.Name()),
   169  				info.Mode(),
   170  				f.colorist,
   171  			}
   172  		}
   173  		return files, nil, err
   174  	}
   175  
   176  	var buf [previewBytes]byte
   177  	nr, err := ff.Read(buf[:])
   178  	if err != nil && err != io.EOF {
   179  		return nil, nil, err
   180  	}
   181  
   182  	content := buf[:nr]
   183  	if !utf8.Valid(content) {
   184  		return nil, nil, errNonUTF8
   185  	}
   186  
   187  	return nil, content, nil
   188  }