diff --git a/Dockerfile b/Dockerfile index b7c6c959..9f87b7b9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,10 @@ FROM python:3-slim -ENV PYTHONDONTWRITEBYTECODE 1 -ENV PYTHONUNBUFFERED 1 -ENV PIP_ROOT_USER_ACTION ignore WORKDIR /app COPY . /app -RUN pip install --no-cache --upgrade pip \ - && pip install --no-cache /app \ - && addgroup --system app && adduser --system --group --home /home/app app \ - && mkdir -p /tmp/shell_gpt \ - && chown -R app:app /tmp/shell_gpt - -USER app +RUN apt-get update && apt-get install -y gcc +RUN pip install --no-cache /app && mkdir -p /tmp/shell_gpt VOLUME /tmp/shell_gpt diff --git a/README.md b/README.md index b44b5a55..1311b137 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,19 @@ Error Detected: Memory allocation failed at line 12. Possible Solution: Consider increasing memory allocation or optimizing application memory usage. ``` +You can also use all kind of redirection operators to pass input: +```shell +sgpt "summarise" < document.txt +# -> The document discusses the impact... +sgpt << EOF +What is the best way to lear Golang. +Provide simple hello world example. +EOF +# -> The best way to learn Golang... +sgpt <<< "What is the best way to learn shell redirects?" +# -> The best way to learn shell redirects is through... +``` + ### Shell commands Have you ever found yourself forgetting common shell commands, such as `find`, and needing to look up the syntax online? With `--shell` or shortcut `-s` option, you can quickly generate and execute the commands you need right in the terminal. @@ -65,14 +78,14 @@ sgpt -s "start nginx container, mount ./index.html" # -> [E]xecute, [D]escribe, [A]bort: e ``` -We can still use pipes to pass input to `sgpt` and get generate shell commands: +We can still use pipes to pass input to `sgpt` and generate shell commands: ```shell -cat data.json | sgpt -s "POST localhost with json" +sgpt -s "POST localhost with" < data.json # -> curl -X POST -H "Content-Type: application/json" -d '{"a": 1, "b": 2}' http://localhost # -> [E]xecute, [D]escribe, [A]bort: e ``` -Applying additional shell magic in our prompt, in this example passing file names to ffmpeg: +Applying additional shell magic in our prompt, in this example passing file names to `ffmpeg`: ```shell ls # -> 1.mp4 2.mp4 3.mp4 @@ -81,6 +94,11 @@ sgpt -s "ffmpeg combine $(ls -m) into one video file without audio." # -> [E]xecute, [D]escribe, [A]bort: e ``` +If you would like to pass generated shell command using pipe, you can use `--no-interaction` option. This will disable interactive mode and will print generated command to stdout. In this example we are using `pbcopy` to copy generated command to clipboard: +```shell +sgpt -s "find all json files in current folder" --no-interaction | pbcopy +``` + ### Shell integration Shell integration enables the use of ShellGPT with hotkeys in your terminal, supported by both Bash and ZSH shells. This feature puts `sgpt` completions directly into terminal buffer (input line), allowing for immediate editing of suggested commands. @@ -392,6 +410,7 @@ Possible options for `CODE_THEME`: https://pygments.org/styles/ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Assistance Options ─────────────────────────────────────────────────────────────────────────────────────╮ │ --shell -s Generate and execute shell commands. │ +│ --interaction --no-interaction Interactive mode for --shell option. [default: interaction] │ │ --describe-shell -d Describe a shell command. │ │ --code -c Generate only code. │ │ --functions --no-functions Allow function calls. [default: functions] │ diff --git a/sgpt/app.py b/sgpt/app.py index a9a2eaf1..da565843 100644 --- a/sgpt/app.py +++ b/sgpt/app.py @@ -1,10 +1,11 @@ -# To allow users to use arrow keys in the REPL. import os + +# To allow users to use arrow keys in the REPL. import readline # noqa: F401 import sys import typer -from click import BadArgumentUsage, MissingParameter +from click import BadArgumentUsage from click.types import Choice from sgpt.config import cfg @@ -24,7 +25,7 @@ def main( prompt: str = typer.Argument( - None, + "", show_default=False, help="The prompt to generate completions for.", ), @@ -51,6 +52,11 @@ def main( help="Generate and execute shell commands.", rich_help_panel="Assistance Options", ), + interaction: bool = typer.Option( + True, + help="Interactive mode for --shell option.", + rich_help_panel="Assistance Options", + ), describe_shell: bool = typer.Option( False, "--describe-shell", @@ -156,20 +162,22 @@ def main( # but rest of the stdin to be used as a inputs. For example: # echo "hello\n__sgpt__eof__\nThis is input" | sgpt --repl temp # In this case, "hello" will be used as a init prompt, and - # "This is input" will be used as a input to the REPL. + # "This is input" will be used as "interactive" input to the REPL. + # This is useful to test REPL with some initial context. for line in sys.stdin: if "__sgpt__eof__" in line: break stdin += line prompt = f"{stdin}\n\n{prompt}" if prompt else stdin - # Switch to stdin for interactive input. - if os.name == "posix": - sys.stdin = open("/dev/tty", "r") - elif os.name == "nt": - sys.stdin = open("CON", "r") - - if not prompt and not editor and not repl: - raise MissingParameter(param_hint="PROMPT", param_type="string") + try: + # Switch to stdin for interactive input. + if os.name == "posix": + sys.stdin = open("/dev/tty", "r") + elif os.name == "nt": + sys.stdin = open("CON", "r") + except OSError: + # Non-interactive shell. + pass if sum((shell, describe_shell, code)) > 1: raise BadArgumentUsage( @@ -225,7 +233,7 @@ def main( functions=function_schemas, ) - while shell: + while shell and interaction: option = typer.prompt( text="[E]xecute, [D]escribe, [A]bort", type=Choice(("e", "d", "a", "y"), case_sensitive=False), diff --git a/sgpt/handlers/handler.py b/sgpt/handlers/handler.py index 75773be6..39afc811 100644 --- a/sgpt/handlers/handler.py +++ b/sgpt/handlers/handler.py @@ -35,7 +35,6 @@ def _handle_with_markdown(self, prompt: str, **kwargs: Any) -> str: with Live( Markdown(markup="", code_theme=self.theme_name), console=Console(), - refresh_per_second=8, ) as live: if self.disable_stream: live.update( @@ -45,7 +44,7 @@ def _handle_with_markdown(self, prompt: str, **kwargs: Any) -> str: for word in self.get_completion(messages=messages, **kwargs): full_completion += word live.update( - Markdown(full_completion, code_theme=self.theme_name), + Markdown(markup=full_completion, code_theme=self.theme_name), refresh=not self.disable_stream, ) return full_completion diff --git a/sgpt/integration.py b/sgpt/integration.py new file mode 100644 index 00000000..fd19fc6d --- /dev/null +++ b/sgpt/integration.py @@ -0,0 +1,27 @@ +bash_integration = """ +# Shell-GPT integration BASH v0.2 +_sgpt_bash() { +if [[ -n "$READLINE_LINE" ]]; then + READLINE_LINE=$(sgpt --shell <<< "$READLINE_LINE" --no-interaction) + READLINE_POINT=${#READLINE_LINE} +fi +} +bind -x '"\\C-l": _sgpt_bash' +# Shell-GPT integration BASH v0.2 +""" + +zsh_integration = """ +# Shell-GPT integration ZSH v0.2 +_sgpt_zsh() { +if [[ -n "$BUFFER" ]]; then + _sgpt_prev_cmd=$BUFFER + BUFFER+="⌛" + zle -I && zle redisplay + BUFFER=$(sgpt --shell <<< "$_sgpt_prev_cmd" --no-interaction) + zle end-of-line +fi +} +zle -N _sgpt_zsh +bindkey ^l _sgpt_zsh +# Shell-GPT integration ZSH v0.2 +""" diff --git a/sgpt/utils.py b/sgpt/utils.py index 6dbb0b2d..f33430e7 100644 --- a/sgpt/utils.py +++ b/sgpt/utils.py @@ -5,9 +5,10 @@ from typing import Any, Callable import typer -from click import BadParameter +from click import BadParameter, UsageError from sgpt.__version__ import __version__ +from sgpt.integration import bash_integration, zsh_integration def get_edited_prompt() -> str: @@ -65,17 +66,25 @@ def wrapper(cls: Any, value: str) -> None: @option_callback def install_shell_integration(*_args: Any) -> None: """ - Installs shell integration. Currently only supports Linux. + Installs shell integration. Currently only supports ZSH and Bash. Allows user to get shell completions in terminal by using hotkey. - Allows user to edit shell command right away in terminal. + Replaces current "buffer" of the shell with the completion. """ # TODO: Add support for Windows. # TODO: Implement updates. - if platform.system() == "Windows": - typer.echo("Windows is not supported yet.") + shell = os.getenv("SHELL", "") + if shell == "/bin/zsh": + typer.echo("Installing ZSH integration...") + with open(os.path.expanduser("~/.zshrc"), "a", encoding="utf-8") as file: + file.write(zsh_integration) + elif shell == "/bin/bash": + typer.echo("Installing Bash integration...") + with open(os.path.expanduser("~/.bashrc"), "a", encoding="utf-8") as file: + file.write(bash_integration) else: - url = "https://raw.githubusercontent.com/TheR1D/shell_gpt/shell-integrations/install.sh" - os.system(f'sh -c "$(curl -fsSL {url})"') + raise UsageError("ShellGPT integrations only available for ZSH and Bash.") + + typer.echo("Done! Restart your shell to apply changes.") @option_callback diff --git a/tests/test_shell.py b/tests/test_shell.py index 43afde80..346d4b42 100644 --- a/tests/test_shell.py +++ b/tests/test_shell.py @@ -159,3 +159,21 @@ def test_shell_and_describe_shell(completion): completion.assert_not_called() assert result.exit_code == 2 assert "Error" in result.stdout + + +@patch("openai.resources.chat.Completions.create") +def test_shell_no_interaction(completion): + completion.return_value = comp_chunks("git commit -m test") + role = SystemRole.get(DefaultRoles.SHELL.value) + + args = { + "prompt": "make a commit using git", + "--shell": True, + "--no-interaction": True, + } + result = runner.invoke(app, cmd_args(**args)) + + completion.assert_called_once_with(**comp_args(role, args["prompt"])) + assert result.exit_code == 0 + assert "git commit" in result.stdout + assert "[E]xecute" not in result.stdout