mirror of
https://github.com/wavetermdev/inshellisense.git
synced 2026-08-05 13:43:30 -07:00
feat: implement single-line passthrough ui (#90)
* feat: implement mock ui with ability to inject subtext Signed-off-by: Chapman Pendery <cpendery@vt.edu> * fix: cursor movement issues Signed-off-by: Chapman Pendery <cpendery@vt.edu> * fix: support dynamic output Signed-off-by: Chapman Pendery <cpendery@vt.edu> * feat: initial support for displaying actual suggestions Signed-off-by: Chapman Pendery <cpendery@vt.edu> * feat: enable suggestion navigation with arrow keys Signed-off-by: Chapman Pendery <cpendery@vt.edu> * fix: make suggestion loading async Signed-off-by: Chapman Pendery <cpendery@vt.edu> * fix: wrapping & coloring on single line completion Signed-off-by: Chapman Pendery <cpendery@vt.edu> * fix: support tab passthrough Signed-off-by: Chapman Pendery <cpendery@vt.edu> * fix: backspace on pwsh Signed-off-by: Chapman Pendery <cpendery@vt.edu> * fix: suggestion detection for powershell Signed-off-by: Chapman Pendery <cpendery@vt.edu> * fix: cli for ui Signed-off-by: Chapman Pendery <cpendery@vt.edu> * ci: fix linting issues Signed-off-by: Chapman Pendery <cpendery@vt.edu> --------- Signed-off-by: Chapman Pendery <cpendery@vt.edu>
This commit is contained in:
+4
-42
@@ -3,54 +3,16 @@
|
||||
|
||||
import { initRender } from "../ui/ui-init.js";
|
||||
import { render } from "../ui/ui-root.js";
|
||||
import { executeShellCommandTTY, ExecuteShellCommandTTYResult } from "../runtime/utils.js";
|
||||
import { saveCommand, loadCommand } from "../utils/cache.js";
|
||||
import { supportedShells as shells } from "../utils/bindings.js";
|
||||
import { Shell, supportedShells as shells } from "../utils/bindings.js";
|
||||
import { inferShell } from "../utils/shell.js";
|
||||
import { Command } from "commander";
|
||||
|
||||
export const supportedShells = shells.join(", ");
|
||||
|
||||
type RootCommandOptions = {
|
||||
shell: string | undefined;
|
||||
command: string | undefined;
|
||||
history: boolean | undefined;
|
||||
duration: string | undefined;
|
||||
};
|
||||
|
||||
export const action = (program: Command) => async (options: RootCommandOptions) => {
|
||||
if (options.history) {
|
||||
process.stdout.write(await loadCommand());
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const shell = options.shell ?? (await inferShell()) ?? (await initRender()) ?? "";
|
||||
export const action = (program: Command) => async () => {
|
||||
const shell = ((await inferShell()) ?? (await initRender()) ?? "") as unknown as Shell;
|
||||
if (!shells.map((s) => s.valueOf()).includes(shell)) {
|
||||
program.error(`Unsupported shell: '${shell}', supported shells: ${supportedShells}`, { exitCode: 1 });
|
||||
}
|
||||
|
||||
let executed = false;
|
||||
const commands = [];
|
||||
let result: ExecuteShellCommandTTYResult = { code: 0 };
|
||||
let startingCommand = options.command;
|
||||
while (options.duration === "session" || !executed) {
|
||||
const commandToExecute = await render(startingCommand);
|
||||
|
||||
if (commandToExecute == null || commandToExecute.trim().toLowerCase() == "exit" || commandToExecute.trim().toLowerCase() == "logout") {
|
||||
result = { code: 0 };
|
||||
break;
|
||||
}
|
||||
|
||||
commands.push(commandToExecute);
|
||||
result = await executeShellCommandTTY(shell, commandToExecute);
|
||||
executed = true;
|
||||
startingCommand = undefined;
|
||||
}
|
||||
await saveCommand(commands);
|
||||
|
||||
if (result.code) {
|
||||
process.exit(result.code);
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
await render(shell);
|
||||
};
|
||||
|
||||
+1
-5
@@ -9,7 +9,7 @@ import { Command } from "commander";
|
||||
|
||||
import bind from "./commands/bind.js";
|
||||
import uninstall from "./commands/uninstall.js";
|
||||
import { action, supportedShells } from "./commands/root.js";
|
||||
import { action } from "./commands/root.js";
|
||||
import { getVersion } from "./utils/version.js";
|
||||
|
||||
const program = new Command();
|
||||
@@ -18,10 +18,6 @@ program
|
||||
.name("inshellisense")
|
||||
.description("IDE style command line auto complete")
|
||||
.version(await getVersion(), "-v, --version", "output the current version")
|
||||
.option("-s, --shell <shell>", `shell to use for command execution, supported shells: ${supportedShells}`)
|
||||
.option("-c, --command <commmand>", "command to use as initial input")
|
||||
.option("--history", "get the last command execute")
|
||||
.option("-d, --duration <duration>", "duration of the autocomplete session, supported durations: single, session", "session")
|
||||
.action(action(program))
|
||||
.showHelpAfterError("(add --help for additional information)");
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export type CommandState = {
|
||||
promptText?: string;
|
||||
commandText?: string;
|
||||
suggestionsText?: string;
|
||||
persistentOutput?: boolean;
|
||||
hasOutput?: boolean;
|
||||
cursorTerminated?: boolean;
|
||||
};
|
||||
@@ -120,9 +121,14 @@ export class CommandManager {
|
||||
}
|
||||
|
||||
private _isSuggestion(cell: IBufferCell | undefined): boolean {
|
||||
log.debug({ msg: "suggestion detection", fgColor: cell?.getFgColor(), content: cell?.getChars() });
|
||||
const color = cell?.getFgColor();
|
||||
return color == 8 || (color ?? 0) > 235;
|
||||
const dullColor = color == 8 || (color ?? 0) > 235;
|
||||
if (this.#shell == Shell.Powershell) {
|
||||
return false;
|
||||
} else if (this.#shell == Shell.Pwsh) {
|
||||
return (color ?? 0) > 235;
|
||||
}
|
||||
return dullColor;
|
||||
}
|
||||
|
||||
getState(): CommandState {
|
||||
@@ -198,9 +204,11 @@ export class CommandManager {
|
||||
}
|
||||
}
|
||||
|
||||
const commandPostfix = this.#activeCommand.promptText.length + command.trim().length < this.#terminal.buffer.active.cursorX ? " " : "";
|
||||
this.#activeCommand.persistentOutput = this.#activeCommand.hasOutput && hasOutput;
|
||||
this.#activeCommand.hasOutput = hasOutput;
|
||||
this.#activeCommand.suggestionsText = suggestions.trim();
|
||||
this.#activeCommand.commandText = command.trim();
|
||||
this.#activeCommand.commandText = command.trim() + commandPostfix;
|
||||
this.#activeCommand.cursorTerminated = cursorAtEndOfInput;
|
||||
}
|
||||
|
||||
|
||||
+11
-21
@@ -11,7 +11,6 @@ import { IsTermOscPs, IstermOscPt, IstermPromptStart, IstermPromptEnd } from "..
|
||||
import xterm from "xterm-headless";
|
||||
import { CommandManager, CommandState } from "./commandManager.js";
|
||||
import log from "../utils/log.js";
|
||||
// import { inputModifier } from "./input.js";
|
||||
|
||||
const ISTermOnDataEvent = "data";
|
||||
|
||||
@@ -22,7 +21,7 @@ type ISTermOptions = {
|
||||
shell: Shell;
|
||||
};
|
||||
|
||||
class ISTerm implements IPty {
|
||||
export class ISTerm implements IPty {
|
||||
readonly pid: number;
|
||||
cols: number;
|
||||
rows: number;
|
||||
@@ -50,7 +49,7 @@ class ISTerm implements IPty {
|
||||
this.rows = this.#pty.rows;
|
||||
this.process = this.#pty.process;
|
||||
|
||||
this.#term = new xterm.Terminal({ allowProposedApi: true });
|
||||
this.#term = new xterm.Terminal({ allowProposedApi: true, rows, cols });
|
||||
this.#term.parser.registerOscHandler(IsTermOscPs, (data) => this._handleIsSequence(data));
|
||||
this.#commandManager = new CommandManager(this.#term, shell);
|
||||
|
||||
@@ -118,8 +117,17 @@ class ISTerm implements IPty {
|
||||
}
|
||||
|
||||
getCommandState(): CommandState {
|
||||
log.debug({ x: this.#term.buffer.active.cursorX, y: this.#term.buffer.active.baseY, lines: this.#term.buffer.active.length });
|
||||
return this.#commandManager.getState();
|
||||
}
|
||||
|
||||
getCursorState() {
|
||||
return {
|
||||
onLastLine: this.#term.buffer.active.cursorY >= this.#term.rows - 2,
|
||||
cursorX: this.#term.buffer.active.cursorX,
|
||||
cursorY: this.#term.buffer.active.cursorY,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const spawn = (options: ISTermOptions): ISTerm => {
|
||||
@@ -139,21 +147,3 @@ const convertToPtyEnv = (shell: Shell) => {
|
||||
}
|
||||
return process.env;
|
||||
};
|
||||
|
||||
// TODO bring up to higher level outside isterm
|
||||
// await log.reset();
|
||||
// const ptyProcess = spawn({ shell: Shell.Fish, rows: process.stdout.rows, cols: process.stdout.columns });
|
||||
// process.stdin.setRawMode(true);
|
||||
// ptyProcess.onData((data) => {
|
||||
// process.stdout.write(data);
|
||||
// });
|
||||
// process.stdin.on("data", (d: Buffer) => {
|
||||
// ptyProcess.write(inputModifier(d));
|
||||
// });
|
||||
|
||||
// ptyProcess.onExit(({ exitCode }) => {
|
||||
// process.exit(exitCode);
|
||||
// });
|
||||
// process.stdout.on("resize", () => {
|
||||
// ptyProcess.resize(process.stdout.columns, process.stdout.rows);
|
||||
// });
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
import { Suggestion, SuggestionBlob } from "../runtime/model.js";
|
||||
import { getSuggestions } from "../runtime/runtime.js";
|
||||
import { ISTerm } from "../isterm/pty.js";
|
||||
import { renderBox, truncateText } from "./utils.js";
|
||||
import ansi from "ansi-escapes";
|
||||
import chalk from "chalk";
|
||||
import { parseKeystroke } from "../utils/ansi.js";
|
||||
|
||||
const maxSuggestions = 5;
|
||||
const suggestionWidth = 40;
|
||||
const descriptionWidth = 30;
|
||||
const borderWidth = 2;
|
||||
const activeSuggestionBackgroundColor = "#7D56F4";
|
||||
export const MAX_LINES = borderWidth + maxSuggestions;
|
||||
type SuggestionsSequence = {
|
||||
data: string;
|
||||
columns: number;
|
||||
};
|
||||
|
||||
export class SuggestionManager {
|
||||
#term: ISTerm;
|
||||
#command: string;
|
||||
#activeSuggestionIdx: number;
|
||||
#suggestBlob?: SuggestionBlob;
|
||||
|
||||
constructor(terminal: ISTerm) {
|
||||
this.#term = terminal;
|
||||
this.#suggestBlob = { suggestions: [] };
|
||||
this.#command = "";
|
||||
this.#activeSuggestionIdx = 0;
|
||||
}
|
||||
|
||||
private async _loadSuggestions(): Promise<void> {
|
||||
const commandText = this.#term.getCommandState().commandText;
|
||||
if (!commandText) {
|
||||
this.#suggestBlob = undefined;
|
||||
return;
|
||||
}
|
||||
if (commandText == this.#command) {
|
||||
return;
|
||||
}
|
||||
this.#command = commandText;
|
||||
const suggestionBlob = await getSuggestions(commandText);
|
||||
this.#suggestBlob = suggestionBlob;
|
||||
}
|
||||
|
||||
// if I want a 30 box, this means that
|
||||
|
||||
// normalBorder = Border{
|
||||
// Top: "─",
|
||||
// Bottom: "─",
|
||||
// Left: "│",
|
||||
// Right: "│",
|
||||
// TopLeft: "┌",
|
||||
// TopRight: "┐",
|
||||
// BottomLeft: "└",
|
||||
// BottomRight: "┘",
|
||||
// MiddleLeft: "├",
|
||||
// MiddleRight: "┤",
|
||||
// Middle: "┼",
|
||||
// MiddleTop: "┬",
|
||||
// MiddleBottom: "┴",
|
||||
// }
|
||||
|
||||
private _renderSuggestions(suggestions: Suggestion[], activeSuggestionIdx: number, x: number) {
|
||||
return renderBox(
|
||||
suggestions.map((suggestion, idx) => {
|
||||
const suggestionText = `${suggestion.icon} ${suggestion.name}`.padEnd(suggestionWidth - borderWidth, " ");
|
||||
const truncatedSuggestion = truncateText(suggestionText, suggestionWidth - 2);
|
||||
return idx == activeSuggestionIdx ? chalk.bgHex(activeSuggestionBackgroundColor)(truncatedSuggestion) : truncatedSuggestion;
|
||||
}),
|
||||
suggestionWidth,
|
||||
x,
|
||||
);
|
||||
}
|
||||
|
||||
async render(): Promise<SuggestionsSequence> {
|
||||
await this._loadSuggestions();
|
||||
if (!this.#suggestBlob) return { data: "", columns: 0 };
|
||||
const { suggestions } = this.#suggestBlob;
|
||||
|
||||
const page = Math.min(Math.floor(this.#activeSuggestionIdx / maxSuggestions) + 1, Math.floor(suggestions.length / maxSuggestions) + 1);
|
||||
const pagedSuggestions = suggestions.filter((_, idx) => idx < page * maxSuggestions && idx >= (page - 1) * maxSuggestions);
|
||||
const activePagedSuggestionIndex = this.#activeSuggestionIdx % maxSuggestions;
|
||||
// const activeDescription = pagedSuggestions.at(activePagedSuggestionIndex)?.description || "";
|
||||
const activeDescription = "";
|
||||
|
||||
const wrappedPadding = this.#term.getCursorState().cursorX % this.#term.cols;
|
||||
const maxPadding = activeDescription.length !== 0 ? this.#term.cols - suggestionWidth - descriptionWidth : this.#term.cols - suggestionWidth;
|
||||
const swapDescription = wrappedPadding > maxPadding;
|
||||
const swappedPadding = swapDescription ? Math.max(wrappedPadding - descriptionWidth, 0) : wrappedPadding;
|
||||
const clampedLeftPadding = Math.min(Math.min(wrappedPadding, swappedPadding), maxPadding);
|
||||
|
||||
if (suggestions.length <= this.#activeSuggestionIdx) {
|
||||
this.#activeSuggestionIdx = Math.max(suggestions.length - 1, 0);
|
||||
}
|
||||
|
||||
if (pagedSuggestions.length == 0) {
|
||||
return { data: "", columns: 0 };
|
||||
}
|
||||
|
||||
const columnsUsed = pagedSuggestions.length + borderWidth;
|
||||
return {
|
||||
data:
|
||||
ansi.cursorHide +
|
||||
ansi.cursorUp(columnsUsed - 1) +
|
||||
ansi.cursorForward(clampedLeftPadding) +
|
||||
this._renderSuggestions(pagedSuggestions, activePagedSuggestionIndex, clampedLeftPadding) +
|
||||
ansi.cursorShow,
|
||||
columns: columnsUsed,
|
||||
};
|
||||
}
|
||||
|
||||
update(input: Buffer): "handled" | "fully-handled" | false {
|
||||
const keyStroke = parseKeystroke(input);
|
||||
if (keyStroke == null) return false;
|
||||
if (keyStroke == "up") {
|
||||
this.#activeSuggestionIdx = Math.max(0, this.#activeSuggestionIdx - 1);
|
||||
} else if (keyStroke == "down") {
|
||||
this.#activeSuggestionIdx = Math.min(this.#activeSuggestionIdx + 1, (this.#suggestBlob?.suggestions.length ?? 1) - 1);
|
||||
} else if (keyStroke == "tab") {
|
||||
const removals = "\u007F".repeat(this.#suggestBlob?.charactersToDrop ?? 0);
|
||||
const chars = this.#suggestBlob?.suggestions.at(this.#activeSuggestionIdx)?.name + " ";
|
||||
if (this.#suggestBlob == null || !chars.trim()) {
|
||||
return false;
|
||||
}
|
||||
this.#term.write(removals + chars);
|
||||
} else if (keyStroke == "ctrl-space") {
|
||||
this.#term.write("\t");
|
||||
return "fully-handled";
|
||||
}
|
||||
return "handled";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
import { inputModifier } from "./input.js";
|
||||
import log from "../utils/log.js";
|
||||
import { Shell } from "../utils/bindings.js";
|
||||
import isterm from "../isterm/index.js";
|
||||
import { eraseLinesBelow, scrollDown } from "../utils/ansi.js";
|
||||
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 suggestionManager = new SuggestionManager(term);
|
||||
let hasActiveSuggestions = false;
|
||||
let previousSuggestionsColumns = 0;
|
||||
let addedLines = 0;
|
||||
process.stdin.setRawMode(true);
|
||||
|
||||
const writeOutput = (data: string) => {
|
||||
log.debug({ msg: "writing data", data });
|
||||
process.stdout.write(data);
|
||||
};
|
||||
|
||||
writeOutput(ansi.clearTerminal);
|
||||
|
||||
term.onData((data) => {
|
||||
if (term.getCursorState().onLastLine) {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
for (const match of data.matchAll(/\x1b\[([0-9]+);([0-9]+)H/g)) {
|
||||
const [cupSequence, , cursorX] = match;
|
||||
data = data.replaceAll(cupSequence, ansi.cursorTo(parseInt(cursorX) - 1, term.rows - 1 - addedLines));
|
||||
}
|
||||
}
|
||||
|
||||
const commandState = term.getCommandState();
|
||||
if ((commandState.hasOutput || hasActiveSuggestions) && !commandState.persistentOutput) {
|
||||
writeOutput(ansi.cursorHide + ansi.cursorSavePosition + eraseLinesBelow(MAX_LINES) + ansi.cursorRestorePosition + ansi.cursorShow + data);
|
||||
} else {
|
||||
writeOutput(data);
|
||||
}
|
||||
|
||||
setImmediate(async () => {
|
||||
const suggestion = await suggestionManager.render();
|
||||
addedLines = suggestion.columns;
|
||||
const commandState = term.getCommandState();
|
||||
|
||||
if (suggestion.data != "" && commandState.cursorTerminated && !commandState.hasOutput) {
|
||||
if (hasActiveSuggestions) {
|
||||
const offset = MAX_LINES - suggestion.columns;
|
||||
writeOutput(
|
||||
ansi.cursorHide +
|
||||
ansi.cursorSavePosition +
|
||||
eraseLinesBelow(MAX_LINES) +
|
||||
(offset > 0 ? ansi.cursorUp(offset) : "") +
|
||||
suggestion.data +
|
||||
ansi.cursorRestorePosition +
|
||||
ansi.cursorShow,
|
||||
);
|
||||
} else {
|
||||
if (term.getCursorState().onLastLine) {
|
||||
writeOutput(
|
||||
ansi.cursorHide +
|
||||
ansi.cursorSavePosition +
|
||||
"\n".repeat(suggestion.columns) +
|
||||
suggestion.data +
|
||||
ansi.cursorRestorePosition +
|
||||
ansi.cursorUp(suggestion.columns) +
|
||||
ansi.cursorShow,
|
||||
);
|
||||
} else {
|
||||
writeOutput(
|
||||
ansi.cursorHide + ansi.cursorSavePosition + "\n".repeat(suggestion.columns) + suggestion.data + ansi.cursorRestorePosition + ansi.cursorShow,
|
||||
);
|
||||
}
|
||||
}
|
||||
hasActiveSuggestions = true;
|
||||
} else {
|
||||
if (hasActiveSuggestions) {
|
||||
if (term.getCursorState().onLastLine) {
|
||||
writeOutput(scrollDown(previousSuggestionsColumns) + ansi.cursorDown(previousSuggestionsColumns));
|
||||
} else {
|
||||
writeOutput(ansi.cursorHide + ansi.cursorSavePosition + eraseLinesBelow(MAX_LINES) + ansi.cursorRestorePosition + ansi.cursorShow);
|
||||
}
|
||||
}
|
||||
hasActiveSuggestions = false;
|
||||
}
|
||||
previousSuggestionsColumns = suggestion.columns;
|
||||
});
|
||||
});
|
||||
process.stdin.on("data", (d: Buffer) => {
|
||||
const suggestionResult = suggestionManager.update(d);
|
||||
if (previousSuggestionsColumns > 0 && suggestionResult == "handled") {
|
||||
term.write("\u001B[m");
|
||||
} else if (!suggestionResult) {
|
||||
term.write(inputModifier(d));
|
||||
}
|
||||
});
|
||||
|
||||
term.onExit(({ exitCode }) => {
|
||||
process.exit(exitCode);
|
||||
});
|
||||
process.stdout.on("resize", () => {
|
||||
term.resize(process.stdout.columns, process.stdout.rows);
|
||||
});
|
||||
};
|
||||
@@ -1,93 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Text, Box, render as inkRender, measureElement, DOMElement, useInput, useApp } from "ink";
|
||||
import wrapAnsi from "wrap-ansi";
|
||||
|
||||
import { getSuggestions } from "../runtime/runtime.js";
|
||||
import { Suggestion } from "../runtime/model.js";
|
||||
import Suggestions from "./suggestions.js";
|
||||
import Input from "./input.js";
|
||||
|
||||
const Prompt = "> ";
|
||||
let uiResult = undefined;
|
||||
|
||||
function UI({ startingCommand }: { startingCommand: string }) {
|
||||
const { exit } = useApp();
|
||||
const [isExiting, setIsExiting] = useState(false);
|
||||
const [command, setCommand] = useState(startingCommand);
|
||||
const [activeSuggestion, setActiveSuggestion] = useState<Suggestion>();
|
||||
const [tabCompletionDropSize, setTabCompletionDropSize] = useState(0);
|
||||
const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
|
||||
const [windowWidth, setWindowWidth] = useState(500);
|
||||
const leftPadding = getLeftPadding(windowWidth, command);
|
||||
|
||||
const measureRef = useCallback((node: DOMElement) => {
|
||||
if (node !== null) {
|
||||
const { width } = measureElement(node);
|
||||
setWindowWidth(width);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useInput((input, key) => {
|
||||
if (key.ctrl && input.toLowerCase() == "d") {
|
||||
uiResult = undefined;
|
||||
exit();
|
||||
}
|
||||
if (key.return) {
|
||||
setIsExiting(true);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isExiting) {
|
||||
uiResult = command;
|
||||
exit();
|
||||
}
|
||||
}, [isExiting]);
|
||||
|
||||
useEffect(() => {
|
||||
getSuggestions(command).then((suggestions) => {
|
||||
setSuggestions(suggestions?.suggestions ?? []);
|
||||
setTabCompletionDropSize(suggestions?.charactersToDrop ?? 0);
|
||||
});
|
||||
}, [command]);
|
||||
|
||||
if (isExiting) {
|
||||
return (
|
||||
<Text>
|
||||
{Prompt}
|
||||
{command}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" ref={measureRef}>
|
||||
<Box>
|
||||
<Text>
|
||||
<Input value={command} setValue={setCommand} prompt={Prompt} activeSuggestion={activeSuggestion} tabCompletionDropSize={tabCompletionDropSize} />
|
||||
</Text>
|
||||
</Box>
|
||||
<Suggestions leftPadding={leftPadding} setActiveSuggestion={setActiveSuggestion} suggestions={suggestions} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export const render = async (command: string | undefined): Promise<string | undefined> => {
|
||||
uiResult = undefined;
|
||||
const { waitUntilExit } = inkRender(<UI startingCommand={command ?? ""} />);
|
||||
await waitUntilExit();
|
||||
|
||||
return uiResult;
|
||||
};
|
||||
|
||||
function getLeftPadding(windowWidth: number, command: string) {
|
||||
const wrappedText = wrapAnsi(command + "", windowWidth, {
|
||||
trim: false,
|
||||
hard: true,
|
||||
});
|
||||
const lines = wrappedText.split("\n");
|
||||
return (lines.length - 1) * windowWidth + lines[lines.length - 1].length + Prompt.length;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
import ansi from "ansi-escapes";
|
||||
import chalk from "chalk";
|
||||
|
||||
/**
|
||||
* Renders a box around the given rows
|
||||
* @param rows the text content to be included in the box, must be <= width - 2
|
||||
* @param width the max width of a row
|
||||
* @param x the column to start the box at
|
||||
*/
|
||||
export const renderBox = (rows: string[], width: number, x: number, borderColor?: string) => {
|
||||
const result = [];
|
||||
const setColor = (text: string) => (borderColor ? chalk.hex(borderColor).apply(text) : text);
|
||||
result.push(setColor("┌" + "─".repeat(width - 2) + "┐") + ansi.cursorTo(x));
|
||||
rows.forEach((row) => {
|
||||
result.push(ansi.cursorDown() + setColor("│") + row + setColor("│") + ansi.cursorTo(x));
|
||||
});
|
||||
result.push(ansi.cursorDown() + setColor("└" + "─".repeat(width - 2) + "┘") + ansi.cursorTo(x));
|
||||
return result.join("");
|
||||
};
|
||||
|
||||
/**
|
||||
* Truncates the text to the given width
|
||||
*/
|
||||
export const truncateText = (text: string, width: number) => {
|
||||
const textPoints = [...text];
|
||||
const slicedText = textPoints.slice(0, width - 1);
|
||||
return slicedText.length == textPoints.length ? text : slicedText.join("") + "…";
|
||||
};
|
||||
+38
-4
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
const ESC = "\u001B[";
|
||||
const CSI = "\u001B[";
|
||||
const OSC = "\u001B]";
|
||||
const BEL = "\u0007";
|
||||
|
||||
@@ -15,6 +15,40 @@ export enum IstermOscPt {
|
||||
|
||||
export const IstermPromptStart = IS_OSC + IstermOscPt.PromptStarted + BEL;
|
||||
export const IstermPromptEnd = IS_OSC + IstermOscPt.PromptEnded + BEL;
|
||||
export const cursorHide = ESC + "?25l";
|
||||
export const cursorShow = ESC + "?25h";
|
||||
export const cursorBackward = (count = 1) => ESC + count + "D";
|
||||
export const cursorHide = CSI + "?25l";
|
||||
export const cursorShow = CSI + "?25h";
|
||||
export const cursorNextLine = CSI + "E";
|
||||
export const eraseLine = CSI + "2K";
|
||||
export const cursorBackward = (count = 1) => CSI + count + "D";
|
||||
export const cursorTo = ({ x, y }: { x?: number; y?: number }) => {
|
||||
return CSI + (y ?? "") + ";" + (x ?? "") + "H";
|
||||
};
|
||||
export const deleteLinesBelow = (count = 1) => {
|
||||
return [...Array(count).keys()].map(() => CSI + "B" + CSI + "M").join("");
|
||||
};
|
||||
export const deleteLine = (count = 1) => CSI + count + "M";
|
||||
export const scrollUp = (count = 1) => CSI + count + "S";
|
||||
export const scrollDown = (count = 1) => CSI + count + "T";
|
||||
export const eraseLinesBelow = (count = 1) => {
|
||||
return [...Array(count).keys()].map(() => cursorNextLine + eraseLine).join("");
|
||||
};
|
||||
|
||||
export const parseKeystroke = (b: Buffer): "up" | "down" | "tab" | "ctrl-space" | undefined => {
|
||||
let s: string;
|
||||
if (b[0] > 127 && b[1] === undefined) {
|
||||
b[0] -= 128;
|
||||
s = "\u001B" + String(b);
|
||||
} else {
|
||||
s = String(b);
|
||||
}
|
||||
|
||||
if (s == CSI + "A") {
|
||||
return "up";
|
||||
} else if (s == CSI + "B") {
|
||||
return "down";
|
||||
} else if (s == "\t") {
|
||||
return "tab";
|
||||
} else if (s == "\u0000") {
|
||||
return "ctrl-space";
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user