/ src / utils / pathSelector.js
pathSelector.js
  1  export class PathSelector {
  2    constructor(uiService, menuLoop, fsService) {
  3      this.ui = uiService;
  4      this.loop = menuLoop;
  5      this.fs = fsService;
  6  
  7      this.pathMustExist = true;
  8      this.loop.initialize(this.showPathSelector);
  9    }
 10  
 11    show = async (startingPath, pathMustExist) => {
 12      this.startingPath = startingPath;
 13      this.pathMustExist = pathMustExist;
 14      this.roots = this.fs.getAvailableRoots();
 15      this.currentPath = this.splitPath(startingPath);
 16      if (!this.hasValidRoot(this.currentPath)) {
 17        this.currentPath = [this.roots[0]];
 18      }
 19  
 20      await this.loop.showLoop();
 21  
 22      return this.resultingPath;
 23    };
 24  
 25    showPathSelector = async () => {
 26      this.showCurrent();
 27      await this.ui.askMultipleChoice("Select an option:", [
 28        {
 29          label: "Enter path",
 30          action: this.enterPath,
 31        },
 32        {
 33          label: "Go up one",
 34          action: this.upOne,
 35        },
 36        {
 37          label: "Go down one",
 38          action: this.downOne,
 39        },
 40        {
 41          label: "Create new folder here",
 42          action: this.createSubDir,
 43        },
 44        {
 45          label: "Select this path",
 46          action: this.selectThisPath,
 47        },
 48        {
 49          label: "Cancel",
 50          action: this.cancel,
 51        },
 52      ]);
 53    };
 54  
 55    splitPath = (str) => {
 56      var result = this.dropEmptyParts(str.replaceAll("\\", "/").split("/"));
 57      if (str.startsWith("/") && this.roots.includes("/")) {
 58        result = ["/", ...result];
 59      }
 60      return result;
 61    };
 62  
 63    dropEmptyParts = (parts) => {
 64      return parts.filter((part) => part.length > 0);
 65    };
 66  
 67    combine = (parts) => {
 68      const toJoin = this.dropEmptyParts(parts);
 69      if (toJoin.length == 1) return toJoin[0];
 70      var result = this.fs.pathJoin(toJoin);
 71      if (result.startsWith("//")) {
 72        result = result.substring(1);
 73      }
 74      return result;
 75    };
 76  
 77    combineWith = (parts, extra) => {
 78      const toJoin = this.dropEmptyParts(parts);
 79      if (toJoin.length == 1) return this.fs.pathJoin([toJoin[0], extra]);
 80      return this.fs.pathJoin([...toJoin, extra]);
 81    };
 82  
 83    showCurrent = () => {
 84      const len = this.currentPath.length;
 85      this.ui.showInfoMessage(
 86        `Current path: [${len}]\n` + this.combine(this.currentPath),
 87      );
 88  
 89      if (len < 2) {
 90        this.ui.showInfoMessage(
 91          "Warning - Known issue:\n" +
 92            "Path selection does not work in root paths on some platforms.\n" +
 93            'Use "Enter path" or "Create new folder" to navigate and create folders\n' +
 94            "if this is the case for you.",
 95        );
 96      }
 97    };
 98  
 99    hasValidRoot = (checkPath) => {
100      if (checkPath.length < 1) return false;
101      var result = false;
102      this.roots.forEach(function (root) {
103        if (root.toLowerCase() == checkPath[0].toLowerCase()) {
104          result = true;
105        }
106      });
107      return result;
108    };
109  
110    updateCurrentIfValidFull = (newFullPath) => {
111      if (this.pathMustExist && !this.fs.isDir(newFullPath)) {
112        this.ui.showErrorMessage("The path does not exist.");
113        return;
114      }
115      this.updateCurrentIfValidParts(this.splitPath(newFullPath));
116    };
117  
118    updateCurrentIfValidParts = (newParts) => {
119      if (!this.hasValidRoot(newParts)) {
120        this.ui.showErrorMessage("The path has no valid root.");
121        return;
122      }
123      this.currentPath = newParts;
124    };
125  
126    enterPath = async () => {
127      const newPath = await this.ui.askPrompt("Enter Path:");
128      this.updateCurrentIfValidFull(newPath);
129    };
130  
131    upOne = () => {
132      const newParts = this.currentPath.slice(0, this.currentPath.length - 1);
133      this.updateCurrentIfValidParts(newParts);
134    };
135  
136    isSubDir = (entry) => {
137      const newPath = this.combineWith(this.currentPath, entry);
138      return this.fs.isDir(newPath);
139    };
140  
141    getSubDirOptions = () => {
142      const fullPath = this.combine(this.currentPath);
143      try {
144        const entries = this.fs.readDir(fullPath);
145        return entries.filter((entry) => this.isSubDir(entry));
146      } catch {
147        return [];
148      }
149    };
150  
151    downOne = async () => {
152      const options = this.getSubDirOptions();
153      if (options.length == 0) {
154        this.ui.showInfoMessage("There are no subdirectories here.");
155        return;
156      }
157  
158      var selected = "";
159      var uiOptions = [];
160      options.forEach(function (option) {
161        uiOptions.push({
162          label: option,
163          action: () => {
164            selected = option;
165          },
166        });
167      });
168  
169      await this.ui.askMultipleChoice("Select an subdir", uiOptions);
170  
171      if (selected.length < 1) return;
172      this.updateCurrentIfValidParts([...this.currentPath, selected]);
173    };
174  
175    createSubDir = async () => {
176      const name = await this.ui.askPrompt("Enter name:");
177      if (name.length < 1) return;
178      const newPath = [...this.currentPath, name];
179      if (this.pathMustExist) {
180        this.fs.makeDir(this.combine(newPath));
181      }
182      this.updateCurrentIfValidParts(newPath);
183    };
184  
185    selectThisPath = async () => {
186      this.resultingPath = this.combine(this.currentPath);
187      this.loop.stopLoop();
188    };
189  
190    cancel = async () => {
191      this.resultingPath = this.startingPath;
192      this.loop.stopLoop();
193    };
194  }