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 }