src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/cli/modes/location.go (about) 1 package modes 2 3 import ( 4 "errors" 5 "fmt" 6 "math" 7 "path/filepath" 8 "regexp" 9 "strings" 10 11 "src.elv.sh/pkg/cli" 12 "src.elv.sh/pkg/cli/tk" 13 "src.elv.sh/pkg/fsutil" 14 "src.elv.sh/pkg/store/storedefs" 15 "src.elv.sh/pkg/ui" 16 ) 17 18 // Location is a mode for viewing location history and changing to a selected 19 // directory. It is based on the ComboBox widget. 20 type Location interface { 21 tk.ComboBox 22 } 23 24 // LocationSpec is the configuration to start the location history feature. 25 type LocationSpec struct { 26 // Key bindings. 27 Bindings tk.Bindings 28 // Store provides the directory history and the function to change directory. 29 Store LocationStore 30 // IteratePinned specifies pinned directories by calling the given function 31 // with all pinned directories. 32 IteratePinned func(func(string)) 33 // IterateHidden specifies hidden directories by calling the given function 34 // with all hidden directories. 35 IterateHidden func(func(string)) 36 // IterateWorksapce specifies workspace configuration. 37 IterateWorkspaces LocationWSIterator 38 // Configuration for the filter. 39 Filter FilterSpec 40 } 41 42 // LocationStore defines the interface for interacting with the directory history. 43 type LocationStore interface { 44 Dirs(blacklist map[string]struct{}) ([]storedefs.Dir, error) 45 Chdir(dir string) error 46 Getwd() (string, error) 47 } 48 49 // A special score for pinned directories. 50 var pinnedScore = math.Inf(1) 51 52 var errNoDirectoryHistoryStore = errors.New("no directory history store") 53 54 // NewLocation creates a new location mode. 55 func NewLocation(app cli.App, cfg LocationSpec) (Location, error) { 56 if cfg.Store == nil { 57 return nil, errNoDirectoryHistoryStore 58 } 59 60 dirs := []storedefs.Dir{} 61 blacklist := map[string]struct{}{} 62 wsKind, wsRoot := "", "" 63 64 if cfg.IteratePinned != nil { 65 cfg.IteratePinned(func(s string) { 66 blacklist[s] = struct{}{} 67 dirs = append(dirs, storedefs.Dir{Score: pinnedScore, Path: s}) 68 }) 69 } 70 if cfg.IterateHidden != nil { 71 cfg.IterateHidden(func(s string) { blacklist[s] = struct{}{} }) 72 } 73 wd, err := cfg.Store.Getwd() 74 if err == nil { 75 blacklist[wd] = struct{}{} 76 if cfg.IterateWorkspaces != nil { 77 wsKind, wsRoot = cfg.IterateWorkspaces.Parse(wd) 78 } 79 } 80 storedDirs, err := cfg.Store.Dirs(blacklist) 81 if err != nil { 82 return nil, fmt.Errorf("db error: %v", err) 83 } 84 for _, dir := range storedDirs { 85 if filepath.IsAbs(dir.Path) { 86 dirs = append(dirs, dir) 87 } else if wsKind != "" && hasPathPrefix(dir.Path, wsKind) { 88 dirs = append(dirs, dir) 89 } 90 } 91 92 l := locationList{dirs} 93 94 w := tk.NewComboBox(tk.ComboBoxSpec{ 95 CodeArea: tk.CodeAreaSpec{ 96 Prompt: modePrompt(" LOCATION ", true), 97 Highlighter: cfg.Filter.Highlighter, 98 }, 99 ListBox: tk.ListBoxSpec{ 100 Bindings: cfg.Bindings, 101 OnAccept: func(it tk.Items, i int) { 102 path := it.(locationList).dirs[i].Path 103 if strings.HasPrefix(path, wsKind) { 104 path = wsRoot + path[len(wsKind):] 105 } 106 err := cfg.Store.Chdir(path) 107 if err != nil { 108 app.Notify(ErrorText(err)) 109 } 110 app.PopAddon() 111 }, 112 }, 113 OnFilter: func(w tk.ComboBox, p string) { 114 w.ListBox().Reset(l.filter(cfg.Filter.makePredicate(p)), 0) 115 }, 116 }) 117 return w, nil 118 } 119 120 func hasPathPrefix(path, prefix string) bool { 121 return path == prefix || 122 strings.HasPrefix(path, prefix+string(filepath.Separator)) 123 } 124 125 // LocationWSIterator is a function that iterates all workspaces by calling 126 // the passed function with the name and pattern of each kind of workspace. 127 // Iteration should stop when the called function returns false. 128 type LocationWSIterator func(func(kind, pattern string) bool) 129 130 // Parse returns whether the path matches any kind of workspace. If there is 131 // a match, it returns the kind of the workspace and the root. It there is no 132 // match, it returns "", "". 133 func (ws LocationWSIterator) Parse(path string) (kind, root string) { 134 var foundKind, foundRoot string 135 ws(func(kind, pattern string) bool { 136 if !strings.HasPrefix(pattern, "^") { 137 pattern = "^" + pattern 138 } 139 re, err := regexp.Compile(pattern) 140 if err != nil { 141 // TODO(xiaq): Surface the error. 142 return true 143 } 144 if root := re.FindString(path); root != "" { 145 foundKind, foundRoot = kind, root 146 return false 147 } 148 return true 149 }) 150 return foundKind, foundRoot 151 } 152 153 type locationList struct { 154 dirs []storedefs.Dir 155 } 156 157 func (l locationList) filter(p func(string) bool) locationList { 158 var filteredDirs []storedefs.Dir 159 for _, dir := range l.dirs { 160 if p(fsutil.TildeAbbr(dir.Path)) { 161 filteredDirs = append(filteredDirs, dir) 162 } 163 } 164 return locationList{filteredDirs} 165 } 166 167 func (l locationList) Show(i int) ui.Text { 168 return ui.T(fmt.Sprintf("%s %s", 169 showScore(l.dirs[i].Score), fsutil.TildeAbbr(l.dirs[i].Path))) 170 } 171 172 func (l locationList) Len() int { return len(l.dirs) } 173 174 func showScore(f float64) string { 175 if f == pinnedScore { 176 return " *" 177 } 178 return fmt.Sprintf("%3.0f", f) 179 }