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 }