fix: improve git bash interfence & cleanup inference code (#91)

* fix: improve git bash interfence & cleanup inference code

Signed-off-by: Chapman Pendery <cpendery@vt.edu>

* fix: async issue

Signed-off-by: Chapman Pendery <cpendery@vt.edu>

* style: fix missing node

Signed-off-by: Chapman Pendery <cpendery@vt.edu>

* chore: delete dead code

Signed-off-by: Chapman Pendery <cpendery@vt.edu>

* chore: delete more dead code

Signed-off-by: Chapman Pendery <cpendery@vt.edu>

---------

Signed-off-by: Chapman Pendery <cpendery@vt.edu>
This commit is contained in:
Chapman Pendery
2023-11-24 14:22:45 -08:00
committed by GitHub
parent 6a39d0c718
commit 928dd51007
11 changed files with 145 additions and 302 deletions
+71 -20
View File
@@ -16,6 +16,7 @@
"ink": "^4.4.1",
"node-pty": "^1.0.0",
"react": "^18.2.0",
"which": "^4.0.0",
"wrap-ansi": "^8.1.0",
"xterm-headless": "^5.3.0"
},
@@ -27,6 +28,7 @@
"@tsconfig/node18": "^18.2.2",
"@types/jest": "^29.5.5",
"@types/react": "^18.2.24",
"@types/which": "^3.0.3",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@withfig/autocomplete-types": "^1.28.0",
@@ -1806,6 +1808,12 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
"dev": true
},
"node_modules/@types/which": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.3.tgz",
"integrity": "sha512-2C1+XoY0huExTbs8MQv1DuS5FS86+SEjdM9F/+GS61gg5Hqbtj8ZiDSx8MfWcyei907fIPbfPGCOrNUTnVHY1g==",
"dev": true
},
"node_modules/@types/yargs": {
"version": "17.0.26",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.26.tgz",
@@ -2959,6 +2967,27 @@
"node": ">= 8"
}
},
"node_modules/cross-spawn/node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"node_modules/cross-spawn/node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/csstype": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
@@ -4764,10 +4793,12 @@
"dev": true
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
"integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
"engines": {
"node": ">=16"
}
},
"node_modules/istanbul-lib-coverage": {
"version": "3.2.0",
@@ -7761,18 +7792,17 @@
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
"integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
"dependencies": {
"isexe": "^2.0.0"
"isexe": "^3.1.1"
},
"bin": {
"node-which": "bin/node-which"
"node-which": "bin/which.js"
},
"engines": {
"node": ">= 8"
"node": "^16.13.0 || >=18.0.0"
}
},
"node_modules/which-boxed-primitive": {
@@ -9421,6 +9451,12 @@
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
"dev": true
},
"@types/which": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.3.tgz",
"integrity": "sha512-2C1+XoY0huExTbs8MQv1DuS5FS86+SEjdM9F/+GS61gg5Hqbtj8ZiDSx8MfWcyei907fIPbfPGCOrNUTnVHY1g==",
"dev": true
},
"@types/yargs": {
"version": "17.0.26",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.26.tgz",
@@ -10211,6 +10247,23 @@
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"dependencies": {
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}
}
}
},
"csstype": {
@@ -11505,10 +11558,9 @@
"dev": true
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
"integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="
},
"istanbul-lib-coverage": {
"version": "3.2.0",
@@ -13677,12 +13729,11 @@
}
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
"integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
"requires": {
"isexe": "^2.0.0"
"isexe": "^3.1.1"
}
},
"which-boxed-primitive": {
+2
View File
@@ -40,6 +40,7 @@
"ink": "^4.4.1",
"node-pty": "^1.0.0",
"react": "^18.2.0",
"which": "^4.0.0",
"wrap-ansi": "^8.1.0",
"xterm-headless": "^5.3.0"
},
@@ -47,6 +48,7 @@
"@tsconfig/node18": "^18.2.2",
"@types/jest": "^29.5.5",
"@types/react": "^18.2.24",
"@types/which": "^3.0.3",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"@withfig/autocomplete-types": "^1.28.0",
+4 -2
View File
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { initRender } from "../ui/ui-init.js";
import { render } from "../ui/ui-root.js";
import { Shell, supportedShells as shells } from "../utils/bindings.js";
import { inferShell } from "../utils/shell.js";
@@ -10,7 +9,10 @@ import { Command } from "commander";
export const supportedShells = shells.join(", ");
export const action = (program: Command) => async () => {
const shell = ((await inferShell()) ?? (await initRender()) ?? "") as unknown as Shell;
const shell = (await inferShell()) as unknown as Shell | undefined;
if (shell == null) {
program.error(`Unable to identify shell, use the -s/--shell option to provide your shell`, { exitCode: 1 });
}
if (!shells.map((s) => s.valueOf()).includes(shell)) {
program.error(`Unsupported shell: '${shell}', supported shells: ${supportedShells}`, { exitCode: 1 });
}
+12 -6
View File
@@ -11,6 +11,7 @@ import { IsTermOscPs, IstermOscPt, IstermPromptStart, IstermPromptEnd } from "..
import xterm from "xterm-headless";
import { CommandManager, CommandState } from "./commandManager.js";
import log from "../utils/log.js";
import { gitBashPath } from "../utils/shell.js";
const ISTermOnDataEvent = "data";
@@ -36,8 +37,8 @@ export class ISTerm implements IPty {
readonly #term: xterm.Terminal;
readonly #commandManager: CommandManager;
constructor({ shell, cols, rows, env }: ISTermOptions) {
this.#pty = pty.spawn(convertToPtyTarget(shell), [], {
constructor({ shell, cols, rows, env, shellTarget }: ISTermOptions & { shellTarget: string }) {
this.#pty = pty.spawn(shellTarget, [], {
name: "xterm-256color",
cols,
rows,
@@ -130,12 +131,17 @@ export class ISTerm implements IPty {
}
}
export const spawn = (options: ISTermOptions): ISTerm => {
return new ISTerm(options);
export const spawn = async (options: ISTermOptions): Promise<ISTerm> => {
const shellTarget = await convertToPtyTarget(options.shell);
return new ISTerm({ ...options, shellTarget });
};
const convertToPtyTarget = (shell: Shell): string => {
return os.platform() == "win32" ? `${shell}.exe` : shell;
const convertToPtyTarget = async (shell: Shell): Promise<string> => {
const platform = os.platform();
if (shell == Shell.Bash && platform == "win32") {
return await gitBashPath();
}
return platform == "win32" ? `${shell}.exe` : shell;
};
const convertToPtyEnv = (shell: Shell) => {
+1 -1
View File
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import fsAsync from "fs/promises";
import fsAsync from "node:fs/promises";
import process from "node:process";
const filepathsTemplate = async (): Promise<Fig.Suggestion[]> => {
+1 -1
View File
@@ -13,7 +13,7 @@ const bashEnv = { PS1: `${IstermPromptStart}\\u$ ${IstermPromptEnd}` };
const zshEnv = { PROMPT: `%{${IstermPromptStart}%}%/ %# %{${IstermPromptEnd}%}` };
const runTerm = async (shell: Shell, input: string[], env?: { [key: string]: string | undefined }) => {
const ptyProcess = isterm.spawn({ shell, rows: process.stdout.rows, cols: process.stdout.columns, env });
const ptyProcess = await isterm.spawn({ shell, rows: process.stdout.rows, cols: process.stdout.columns, env });
await new Promise((r) => setTimeout(r, 1_000));
for (const data of input) {
ptyProcess.write(data);
-75
View File
@@ -1,75 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import React, { useState, useEffect } from "react";
import { useInput, Text } from "ink";
import chalk from "chalk";
import { Suggestion } from "../runtime/model.js";
const BlinkSpeed = 530;
const CursorColor = "#FFFFFF";
export default function Input({
value,
setValue,
prompt,
activeSuggestion,
tabCompletionDropSize,
}: {
value: string;
setValue: (_: string) => void;
prompt: string;
activeSuggestion: Suggestion | undefined;
tabCompletionDropSize: number;
}) {
const [cursorLocation, setCursorLocation] = useState(value.length);
const [cursorBlink, setCursorBlink] = useState(true);
useEffect(() => {
setTimeout(() => {
setCursorBlink(!cursorBlink);
}, BlinkSpeed);
}, [cursorBlink]);
// TODO: arrow key navigation shortcuts (emacs & vim modes)
useInput((input, key) => {
// TODO: handle delete better on unix systems: https://github.com/vadimdemedes/ink/issues/634
const windows = process.platform === "win32";
const backspaceKey = windows ? key.backspace : key.backspace || key.delete;
const deleteKey = windows ? key.delete : false;
if (backspaceKey) {
setValue([...value].slice(0, Math.max(cursorLocation - 1, 0)).join("") + [...value].slice(cursorLocation).join(""));
setCursorLocation(Math.max(cursorLocation - 1, 0));
} else if (deleteKey) {
setValue([...value].slice(0, cursorLocation).join("") + [...value].slice(Math.min(value.length, cursorLocation + 1)).join(""));
} else if (key.leftArrow) {
setCursorLocation(Math.max(cursorLocation - 1, 0));
} else if (key.rightArrow) {
setCursorLocation(Math.min(cursorLocation + 1, value.length));
} else if (key.tab) {
if (activeSuggestion) {
// TOOD: support insertValue
const newValue = [...value].slice(0, cursorLocation - tabCompletionDropSize).join("") + activeSuggestion.name + " ";
setValue(newValue);
setCursorLocation(newValue.length);
}
} else if (input) {
setValue([...value].slice(0, cursorLocation).join("") + input + [...value].slice(cursorLocation).join(""));
setCursorLocation(cursorLocation + input.length);
}
});
const cursoredCommand = value + " ";
const cursoredText =
[...cursoredCommand].slice(0, cursorLocation).join("") +
(cursorBlink ? chalk.bgHex(CursorColor).inverse([...cursoredCommand].at(cursorLocation)) : [...cursoredCommand].at(cursorLocation)) +
[...cursoredCommand].slice(cursorLocation + 1).join("");
return (
<Text>
{prompt}
{cursoredText}
</Text>
);
}
-132
View File
@@ -1,132 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import React, { useState, useCallback, useEffect } from "react";
import { Text, useInput, Box, measureElement, DOMElement } from "ink";
import wrapAnsi from "wrap-ansi";
import { Suggestion } from "../runtime/model.js";
const MaxSuggestions = 5;
const SuggestionWidth = 40;
const DescriptionWidth = 30;
const DescriptionMaxHeight = 6;
const BorderWidth = 2;
const ActiveSuggestionBackgroundColor = "#7D56F4";
function Description({ description }: { description: string }) {
wrapAnsi(description, DescriptionWidth, { hard: true });
if (description.length !== 0) {
return (
<Box flexDirection="column">
<Box borderStyle="single" width={DescriptionWidth}>
<Text>{truncateDescription(description, DescriptionMaxHeight)}</Text>
</Box>
</Box>
);
}
}
function truncateDescription(description: string, maxHeight: number) {
const wrappedText = wrapAnsi(description, DescriptionWidth - BorderWidth, {
trim: false,
hard: true,
});
const lines = wrappedText.split("\n");
const truncatedLines = lines.slice(0, maxHeight);
if (lines.length > maxHeight) {
truncatedLines[maxHeight - 1] = [...truncatedLines[maxHeight - 1]].slice(0, -1).join("") + "…";
}
return truncatedLines.join("\n");
}
function SuggestionList({ suggestions, activeSuggestionIdx }: { suggestions: Suggestion[]; activeSuggestionIdx: number }) {
return (
<Box flexDirection="column">
<Box borderStyle="single" width={SuggestionWidth} flexDirection="column">
{suggestions
.map((suggestion, idx) => {
const bgColor = idx === activeSuggestionIdx ? ActiveSuggestionBackgroundColor : undefined;
return (
<Box key={idx}>
<Text backgroundColor={bgColor} wrap="truncate-end">
{`${suggestion.icon} ${suggestion.name}`.padEnd(SuggestionWidth - BorderWidth, " ")}
</Text>
</Box>
);
})
.filter((node) => node !== undefined)}
</Box>
</Box>
);
}
export default function Suggestions({
leftPadding,
setActiveSuggestion,
suggestions,
}: {
leftPadding: number;
setActiveSuggestion: (_: Suggestion) => void;
suggestions: Suggestion[];
}) {
const [activeSuggestionIndex, setActiveSuggestionIndex] = useState(0);
const [windowWidth, setWindowWidth] = useState(500);
const page = Math.floor(activeSuggestionIndex / MaxSuggestions) + 1;
const pagedSuggestions = suggestions.filter((_, idx) => idx < page * MaxSuggestions && idx >= (page - 1) * MaxSuggestions);
const activePagedSuggestionIndex = activeSuggestionIndex % MaxSuggestions;
const activeDescription = pagedSuggestions.at(activePagedSuggestionIndex)?.description || "";
const wrappedPadding = leftPadding % windowWidth;
const maxPadding = activeDescription.length !== 0 ? windowWidth - SuggestionWidth - DescriptionWidth : windowWidth - SuggestionWidth;
const swapDescription = wrappedPadding > maxPadding;
const swappedPadding = swapDescription ? Math.max(wrappedPadding - DescriptionWidth, 0) : wrappedPadding;
const clampedLeftPadding = Math.min(Math.min(wrappedPadding, swappedPadding), maxPadding);
useEffect(() => {
setActiveSuggestion(suggestions[activeSuggestionIndex]);
}, [activeSuggestionIndex, suggestions]);
useEffect(() => {
if (suggestions.length <= activeSuggestionIndex) {
setActiveSuggestionIndex(Math.max(suggestions.length - 1, 0));
}
}, [suggestions]);
useInput((_, key) => {
if (key.upArrow) {
setActiveSuggestionIndex(Math.max(0, activeSuggestionIndex - 1));
}
if (key.downArrow) {
setActiveSuggestionIndex(Math.min(activeSuggestionIndex + 1, suggestions.length - 1));
}
});
const measureRef = useCallback((node: DOMElement) => {
if (node !== null) {
const { width } = measureElement(node);
setWindowWidth(width);
}
}, []);
if (suggestions.length === 0) return <></>;
return (
<Box ref={measureRef}>
<Box paddingLeft={clampedLeftPadding}>
{swapDescription ? (
<>
<Description description={activeDescription} />
<SuggestionList suggestions={pagedSuggestions} activeSuggestionIdx={activePagedSuggestionIndex} />
</>
) : (
<>
<SuggestionList suggestions={pagedSuggestions} activeSuggestionIdx={activePagedSuggestionIndex} />
<Description description={activeDescription} />
</>
)}
</Box>
</Box>
);
}
-64
View File
@@ -1,64 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import React, { useState } from "react";
import { Box, Text, render, useApp, useInput } from "ink";
import { supportedShells } from "../utils/bindings.js";
let uiResult = undefined;
function UI() {
const { exit } = useApp();
const [selectionIdx, setSelectionIdx] = useState(0);
const [exited, setExited] = useState(false);
useInput(async (_, key) => {
if (key.upArrow) {
setSelectionIdx(Math.max(0, selectionIdx - 1));
} else if (key.downArrow) {
setSelectionIdx(Math.min(supportedShells.length - 1, selectionIdx + 1));
} else if (key.return) {
uiResult = supportedShells[selectionIdx];
setExited(true);
setTimeout(exit, 0);
}
});
return (
<>
{exited ? null : (
<Box flexDirection="column">
<Box>
<Text bold>Select your desired shell</Text>
</Box>
<Box flexDirection="column">
{supportedShells.map((shell, idx) => {
if (idx == selectionIdx) {
return (
<Text color="cyan" underline key={idx}>
{">"} {shell}
</Text>
);
}
return (
<Text key={idx}>
{" "}
{shell}
</Text>
);
})}
</Box>
</Box>
)}
</>
);
}
export const initRender = async (): Promise<string | undefined> => {
uiResult = undefined;
const { waitUntilExit } = render(<UI />);
await waitUntilExit();
return uiResult;
};
+1 -1
View File
@@ -10,7 +10,7 @@ import ansi from "ansi-escapes";
import { SuggestionManager, MAX_LINES } from "./suggestionManager.js";
export const render = async (shell: Shell) => {
const term = isterm.spawn({ shell, rows: process.stdout.rows, cols: process.stdout.columns });
const term = await isterm.spawn({ shell, rows: process.stdout.rows, cols: process.stdout.columns });
const suggestionManager = new SuggestionManager(term);
let hasActiveSuggestions = false;
let previousSuggestionsColumns = 0;
+53
View File
@@ -3,10 +3,63 @@
import process from "node:process";
import find from "find-process";
import path from "node:path";
import { supportedShells } from "./bindings.js";
import which from "which";
import fs from "node:fs";
export const inferShell = async () => {
try {
return path.parse(process.env.SHELL ?? "").name;
} catch {
/* empty */
}
const processResult = (await find("pid", process.ppid)).at(0);
const name = processResult?.name;
return name != null ? supportedShells.find((shell) => name.includes(shell)) : undefined;
};
export const gitBashPath = async (): Promise<string> => {
const gitBashPaths = await getGitBashPaths();
for (const gitBashPath of gitBashPaths) {
if (fs.existsSync(gitBashPath)) {
return gitBashPath;
}
}
throw new Error("unable to find a git bash executable installed");
};
const getGitBashPaths = async (): Promise<string[]> => {
const gitDirs: Set<string> = new Set();
const gitExePath = await which("git.exe", { nothrow: true });
if (gitExePath) {
const gitExeDir = path.dirname(gitExePath);
gitDirs.add(path.resolve(gitExeDir, "../.."));
}
const addValid = <T>(set: Set<T>, value: T | undefined) => {
if (value) set.add(value);
};
// Add common git install locations
addValid(gitDirs, process.env["ProgramW6432"]);
addValid(gitDirs, process.env["ProgramFiles"]);
addValid(gitDirs, process.env["ProgramFiles(X86)"]);
addValid(gitDirs, `${process.env["LocalAppData"]}\\Program`);
const gitBashPaths: string[] = [];
for (const gitDir of gitDirs) {
gitBashPaths.push(
`${gitDir}\\Git\\bin\\bash.exe`,
`${gitDir}\\Git\\usr\\bin\\bash.exe`,
`${gitDir}\\usr\\bin\\bash.exe`, // using Git for Windows SDK
);
}
// Add special installs that don't follow the standard directory structure
gitBashPaths.push(`${process.env["UserProfile"]}\\scoop\\apps\\git\\current\\bin\\bash.exe`);
gitBashPaths.push(`${process.env["UserProfile"]}\\scoop\\apps\\git-with-openssh\\current\\bin\\bash.exe`);
return gitBashPaths;
};