|
| 1 | +package choose_files |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "io/fs" |
| 6 | + "os" |
| 7 | + "path/filepath" |
| 8 | + "slices" |
| 9 | + "strings" |
| 10 | + "sync" |
| 11 | + |
| 12 | + "github.com/kovidgoyal/kitty/tools/icons" |
| 13 | + "github.com/kovidgoyal/kitty/tools/utils" |
| 14 | + "github.com/kovidgoyal/kitty/tools/utils/humanize" |
| 15 | + "github.com/kovidgoyal/kitty/tools/utils/style" |
| 16 | + "github.com/kovidgoyal/kitty/tools/wcswidth" |
| 17 | +) |
| 18 | + |
| 19 | +var _ = fmt.Print |
| 20 | + |
| 21 | +type Preview interface { |
| 22 | + Render(h *Handler, x, y, width, height int) |
| 23 | +} |
| 24 | + |
| 25 | +type PreviewManager struct { |
| 26 | + report_errors chan error |
| 27 | + settings Settings |
| 28 | + WakeupMainThread func() bool |
| 29 | + cache map[string]Preview |
| 30 | + lock sync.Mutex |
| 31 | +} |
| 32 | + |
| 33 | +func NewPreviewManager(err_chan chan error, settings Settings, WakeupMainThread func() bool) *PreviewManager { |
| 34 | + return &PreviewManager{ |
| 35 | + report_errors: err_chan, settings: settings, WakeupMainThread: WakeupMainThread, |
| 36 | + cache: make(map[string]Preview), |
| 37 | + } |
| 38 | +} |
| 39 | + |
| 40 | +func (pm *PreviewManager) cached_preview(path string) Preview { |
| 41 | + pm.lock.Lock() |
| 42 | + defer pm.lock.Unlock() |
| 43 | + return pm.cache[path] |
| 44 | +} |
| 45 | + |
| 46 | +func (pm *PreviewManager) set_cached_preview(path string, val Preview) { |
| 47 | + pm.lock.Lock() |
| 48 | + defer pm.lock.Unlock() |
| 49 | + pm.cache[path] = val |
| 50 | +} |
| 51 | + |
| 52 | +func (h *Handler) render_wrapped_text_in_region(text string, x, y, width, height int, centered bool) int { |
| 53 | + lines := style.WrapTextAsLines(text, width, style.WrapOptions{}) |
| 54 | + for i, line := range lines { |
| 55 | + extra := 0 |
| 56 | + if centered { |
| 57 | + extra = max(0, width-wcswidth.Stringwidth(line)) / 2 |
| 58 | + } |
| 59 | + h.lp.MoveCursorTo(x+extra, y+i) |
| 60 | + h.lp.QueueWriteString(line) |
| 61 | + if i >= height { |
| 62 | + break |
| 63 | + } |
| 64 | + } |
| 65 | + return len(lines) |
| 66 | +} |
| 67 | + |
| 68 | +type MessagePreview struct { |
| 69 | + title string |
| 70 | + msg string |
| 71 | + trailers []string |
| 72 | +} |
| 73 | + |
| 74 | +func (p MessagePreview) Render(h *Handler, x, y, width, height int) { |
| 75 | + offset := 0 |
| 76 | + if p.title != "" { |
| 77 | + offset += h.render_wrapped_text_in_region(p.title, x, y, width, height, true) |
| 78 | + } |
| 79 | + offset += h.render_wrapped_text_in_region(p.msg, x, y+offset, width, height-offset, false) |
| 80 | + limit := height - offset |
| 81 | + if limit > 1 { |
| 82 | + for i, line := range p.trailers { |
| 83 | + text := wcswidth.TruncateToVisualLength(line, width-1) |
| 84 | + if len(text) < len(line) { |
| 85 | + text += "…" |
| 86 | + } |
| 87 | + h.lp.MoveCursorTo(x, y+offset+i-1) |
| 88 | + if i >= limit { |
| 89 | + h.lp.QueueWriteString("…") |
| 90 | + break |
| 91 | + } |
| 92 | + h.lp.QueueWriteString(text) |
| 93 | + } |
| 94 | + } |
| 95 | +} |
| 96 | + |
| 97 | +func NewErrorPreview(err error) Preview { |
| 98 | + sctx := style.Context{AllowEscapeCodes: true} |
| 99 | + text := fmt.Sprintf("%s: %s", sctx.SprintFunc("fg=red")("Error"), err) |
| 100 | + return &MessagePreview{msg: text} |
| 101 | +} |
| 102 | + |
| 103 | +func write_file_metadata(abspath string, metadata fs.FileInfo, entries []fs.DirEntry) (header string, trailers []string) { |
| 104 | + buf := strings.Builder{} |
| 105 | + buf.Grow(4096) |
| 106 | + add := func(key, val string) { fmt.Fprintf(&buf, "%s: %s\n", key, val) } |
| 107 | + ftype := metadata.Mode().Type() |
| 108 | + const file_icon = " " |
| 109 | + switch ftype { |
| 110 | + case 0: |
| 111 | + add("Size", humanize.Bytes(uint64(metadata.Size()))) |
| 112 | + case fs.ModeSymlink: |
| 113 | + if tgt, err := os.Readlink(abspath); err == nil { |
| 114 | + add("Target", tgt) |
| 115 | + } else { |
| 116 | + add("Target", err.Error()) |
| 117 | + } |
| 118 | + case fs.ModeDir: |
| 119 | + num_files, num_dirs := 0, 0 |
| 120 | + for _, e := range entries { |
| 121 | + if e.IsDir() { |
| 122 | + num_dirs++ |
| 123 | + } else { |
| 124 | + num_files++ |
| 125 | + } |
| 126 | + } |
| 127 | + add("Children", fmt.Sprintf("%d %s %d %s", num_dirs, icons.IconForFileWithMode("dir", fs.ModeDir, false), num_files, file_icon)) |
| 128 | + } |
| 129 | + add("Modified", humanize.Time(metadata.ModTime())) |
| 130 | + add("Mode", metadata.Mode().String()) |
| 131 | + if len(entries) > 0 { |
| 132 | + type entry struct { |
| 133 | + lname string |
| 134 | + ftype fs.FileMode |
| 135 | + } |
| 136 | + type_map := make(map[string]entry, len(entries)) |
| 137 | + for _, e := range entries { |
| 138 | + type_map[e.Name()] = entry{strings.ToLower(e.Name()), e.Type()} |
| 139 | + } |
| 140 | + names := utils.Map(func(e fs.DirEntry) string { return e.Name() }, entries) |
| 141 | + slices.SortFunc(names, func(a, b string) int { return strings.Compare(type_map[a].lname, type_map[b].lname) }) |
| 142 | + fmt.Fprintln(&buf, "Contents:") |
| 143 | + for _, n := range names { |
| 144 | + trailers = append(trailers, icons.IconForFileWithMode(n, type_map[n].ftype, false)+" "+n) |
| 145 | + } |
| 146 | + } |
| 147 | + return buf.String(), trailers |
| 148 | +} |
| 149 | + |
| 150 | +func NewDirectoryPreview(abspath string, metadata fs.FileInfo) Preview { |
| 151 | + entries, err := os.ReadDir(abspath) |
| 152 | + if err != nil { |
| 153 | + return NewErrorPreview(fmt.Errorf("failed to read the directory %s with error: %w", abspath, err)) |
| 154 | + } |
| 155 | + title := icons.IconForFileWithMode("dir", fs.ModeDir, false) + " Directory\n" |
| 156 | + header, extra := write_file_metadata(abspath, metadata, entries) |
| 157 | + return &MessagePreview{title: title, msg: header, trailers: extra} |
| 158 | +} |
| 159 | + |
| 160 | +func NewFileMetadataPreview(abspath string, metadata fs.FileInfo) Preview { |
| 161 | + title := icons.IconForFileWithMode(filepath.Base(abspath), metadata.Mode().Type(), false) + " File" |
| 162 | + h, t := write_file_metadata(abspath, metadata, nil) |
| 163 | + return &MessagePreview{title: title, msg: h, trailers: t} |
| 164 | +} |
| 165 | + |
| 166 | +func (pm *PreviewManager) preview_for(abspath string, ftype fs.FileMode) (ans Preview) { |
| 167 | + if ans = pm.cached_preview(abspath); ans != nil { |
| 168 | + return ans |
| 169 | + } |
| 170 | + defer func() { pm.set_cached_preview(abspath, ans) }() |
| 171 | + s, err := os.Lstat(abspath) |
| 172 | + if err != nil { |
| 173 | + return NewErrorPreview(err) |
| 174 | + } |
| 175 | + if s.IsDir() { |
| 176 | + return NewDirectoryPreview(abspath, s) |
| 177 | + } |
| 178 | + if ftype&fs.ModeSymlink != 0 && ftype&SymlinkToDir != 0 { |
| 179 | + s, err = os.Stat(abspath) |
| 180 | + if err != nil { |
| 181 | + return NewErrorPreview(err) |
| 182 | + } |
| 183 | + return NewDirectoryPreview(abspath, s) |
| 184 | + } |
| 185 | + return NewFileMetadataPreview(abspath, s) |
| 186 | +} |
| 187 | + |
| 188 | +func (h *Handler) draw_preview_content(x, y, width, height int) { |
| 189 | + matches, _ := h.get_results() |
| 190 | + r := matches.At(h.state.CurrentIndex()) |
| 191 | + if r == nil { |
| 192 | + h.render_wrapped_text_in_region("No preview available", x, y, width, height, false) |
| 193 | + return |
| 194 | + } |
| 195 | + abspath := filepath.Join(h.state.CurrentDir(), r.text) |
| 196 | + if p := h.preview_manager.preview_for(abspath, r.ftype); p == nil { |
| 197 | + h.render_wrapped_text_in_region("No preview available", x, y, width, height, false) |
| 198 | + } else { |
| 199 | + p.Render(h, x, y, width, height) |
| 200 | + } |
| 201 | +} |
0 commit comments