From 9b2851e3dbe6db6ba3dab36b15b3bd1c89331673 Mon Sep 17 00:00:00 2001 From: Chapman Pendery Date: Sat, 7 Oct 2023 18:49:33 -0700 Subject: [PATCH] feat: implement command execution Signed-off-by: Chapman Pendery --- src/commands/root.ts | 26 +++++++++++++++++++++-- src/index.ts | 16 +++++++------- src/runtime/suggestion.ts | 1 - src/runtime/utils.ts | 17 ++++++++++++++- src/ui/ui.tsx | 44 ++++++++++++++++++++++++++++++++------- 5 files changed, 85 insertions(+), 19 deletions(-) diff --git a/src/commands/root.ts b/src/commands/root.ts index f03ea4b..a95f9dc 100644 --- a/src/commands/root.ts +++ b/src/commands/root.ts @@ -1,5 +1,27 @@ import { render } from "../ui/ui.js"; +import { executeShellCommandTTY } from "../runtime/utils.js"; -export const action = () => { - render(); +const shells = ["bash", "powershell", "pwsh"]; +export const supportedShells = shells.join(", "); + +type RootCommandOptions = { + shell: string | undefined; + command: string | undefined; +}; + +export const action = async (options: RootCommandOptions) => { + const shell = options.shell ?? ""; + if (!shells.includes(shell)) { + console.error(`Unsupported shell: '${shell}', supported shells: ${supportedShells}`); + process.exit(1); + } + + const commandToExecute = await render(options.command); + const result = await executeShellCommandTTY(shell, commandToExecute); + if (result.code) { + process.exit(result.code); + } else { + process.exit(0); + } + // TODO: cache executed command to add to history }; diff --git a/src/index.ts b/src/index.ts index 1896b01..01fa2ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,18 @@ - import { Command } from "commander"; import bind from "./commands/bind.js"; -import { action } from "./commands/root.js"; +import { action, supportedShells } from "./commands/root.js"; const program = new Command(); -program.name("clac") -.description('IDE style command line auto complete') -.version("0.0.0", "-v, --version", "output the current version") -.action(action) +program + .name("clac") + .description("IDE style command line auto complete") + .version("0.0.0", "-v, --version", "output the current version") + .option("-s, --shell ", `shell to use for command execution, supported shells: ${supportedShells}`) + .option("-c, --command ", "command to use as initial input") + .action(action); program.addCommand(bind); -program.parse(); \ No newline at end of file +program.parse(); diff --git a/src/runtime/suggestion.ts b/src/runtime/suggestion.ts index 4a364ec..a619a4b 100644 --- a/src/runtime/suggestion.ts +++ b/src/runtime/suggestion.ts @@ -174,7 +174,6 @@ const removeDuplicateSuggestions = (suggestions: Suggestion[], acceptedTokens: C return suggestions.filter((s) => s.allNames.every((n) => !seen.has(n))); }; -// TODO: implement re-ranking globally export const getSubcommandDrivenRecommendation = async ( subcommand: Fig.Subcommand, persistentOptions: Fig.Option[], diff --git a/src/runtime/utils.ts b/src/runtime/utils.ts index d00b4a6..5b9477e 100644 --- a/src/runtime/utils.ts +++ b/src/runtime/utils.ts @@ -1,4 +1,8 @@ -import { exec } from "node:child_process"; +import { exec, spawn } from "node:child_process"; + +type ExecuteShellCommandTTYResult = { + code: number | null; +}; export const buildExecuteShellCommand = (timeout: number) => @@ -9,3 +13,14 @@ export const buildExecuteShellCommand = }); }); }; + +export const executeShellCommandTTY = async (shell: string, command: string): Promise => { + const child = spawn(shell, ["-c", command.trim()], { stdio: "inherit" }); + return new Promise((resolve) => { + child.on("close", (code) => { + resolve({ + code, + }); + }); + }); +}; diff --git a/src/ui/ui.tsx b/src/ui/ui.tsx index 947b39b..fa4ec85 100644 --- a/src/ui/ui.tsx +++ b/src/ui/ui.tsx @@ -1,16 +1,20 @@ import React, { useCallback, useEffect, useState } from "react"; -import { Text, Box, render as inkRender, measureElement, DOMElement, useApp } from "ink"; +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 = "> "; +import { executeShellCommandTTY } from "../runtime/utils.js"; -// TODO: support tab completion -function UI() { - const [command, setCommand] = useState(""); +const Prompt = "> "; +let uiResult = ""; + +function UI({ startingCommand }: { startingCommand: string }) { + const { exit } = useApp(); + const [isExiting, setIsExiting] = useState(false); + const [command, setCommand] = useState(startingCommand); const [activeSuggestion, setActiveSuggestion] = useState(); const [tabCompletionDropSize, setTabCompletionDropSize] = useState(0); const [suggestions, setSuggestions] = useState([]); @@ -24,6 +28,19 @@ function UI() { } }, []); + useInput((_, key) => { + if (key.return) { + setIsExiting(true); + } + }); + + useEffect(() => { + if (isExiting) { + uiResult = command; + exit(); + } + }, [isExiting]); + useEffect(() => { getSuggestions(command).then((suggestions) => { setSuggestions(suggestions?.suggestions ?? []); @@ -31,6 +48,15 @@ function UI() { }); }, [command]); + if (isExiting) { + return ( + + {Prompt} + {command} + + ); + } + return ( @@ -43,9 +69,11 @@ function UI() { ); } -export const render = () => { - const { waitUntilExit } = inkRender(); - return waitUntilExit(); +export const render = async (command: string | undefined) => { + const { waitUntilExit } = inkRender(); + await waitUntilExit(); + + return uiResult; }; function getLeftPadding(windowWidth: number, command: string) {