Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: copy to clipboard #234

Merged
merged 5 commits into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions gptme/clipboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import platform
import subprocess


text = ""


def set_copytext(new_text: str):
global text
text = new_text


def copy() -> bool:
"""return True if successful"""
from .util import get_installed_programs

global text
if platform.system() == "Linux":
# check if xclip or wl-clipboard is installed
installed = get_installed_programs(("xclip", "wl-copy"))
if "wl-copy" in installed:
output = subprocess.run(["wl-copy"], input=text, text=True, check=True)
if output.returncode != 0:
print("wl-copy failed to copy to clipboard.")
return False
return True
elif "xclip" in installed:
output = subprocess.run(
["xclip", "-selection", "clipboard"], input=text, text=True
)
if output.returncode != 0:
print("xclip failed to copy to clipboard.")
return False
return True
else:
print("No clipboard utility found. Please install xclip or wl-clipboard.")
return False
elif platform.system() == "Darwin":
output = subprocess.run(["pbcopy"], input=text, text=True)
if output.returncode != 0:
print("pbcopy failed to copy to clipboard.")
return False
return True

return False
ErikBjare marked this conversation as resolved.
Show resolved Hide resolved
45 changes: 19 additions & 26 deletions gptme/tools/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,40 @@
"""

import atexit
import functools
import logging
import os
import re
import select
import shutil
import subprocess
import sys
from collections.abc import Generator

import bashlex

from ..message import Message
from ..util import get_tokenizer, print_preview
from ..util import get_tokenizer, print_preview, get_installed_programs
from .base import ConfirmFunc, ToolSpec, ToolUse

logger = logging.getLogger(__name__)


@functools.lru_cache
def get_installed_programs() -> set[str]:
candidates = [
# platform-specific
"brew",
"apt-get",
"pacman",
# common and useful
"ffmpeg",
"magick",
"pandoc",
"git",
"docker",
]
installed = set()
for candidate in candidates:
if shutil.which(candidate) is not None:
installed.add(candidate)
return installed


shell_programs_str = "\n".join(f"- {prog}" for prog in get_installed_programs())
candidates = (
# platform-specific
"brew",
"apt-get",
"pacman",
# common and useful
"ffmpeg",
"magick",
"pandoc",
"git",
"docker",
)


shell_programs_str = "\n".join(
f"- {prog}" for prog in get_installed_programs(candidates)
)
is_macos = sys.platform == "darwin"

instructions = f"""
Expand Down Expand Up @@ -260,7 +253,7 @@ def execute_shell(
break

if not allowlisted:
print_preview(cmd, "bash")
print_preview(cmd, "bash", True)
if not confirm("Run command?"):
yield Message("system", "User chose not to run command.")
return
Expand Down
2 changes: 1 addition & 1 deletion gptme/tools/tmux.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def execute_tmux(
assert not args
cmd = code.strip()

print_preview(f"Command: {cmd}", "bash")
print_preview(f"Command: {cmd}", "bash", copy=True)
ErikBjare marked this conversation as resolved.
Show resolved Hide resolved
if not confirm(f"Execute command: {cmd}?"):
yield Message("system", "Command execution cancelled.")
return
Expand Down
44 changes: 42 additions & 2 deletions gptme/util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import io
import logging
import random
import shutil
import functools
import re
import sys
import termios
Expand All @@ -10,6 +12,8 @@
from pathlib import Path
from typing import Any

from .clipboard import copy, set_copytext

import tiktoken
from rich import print
from rich.console import Console
Expand Down Expand Up @@ -132,9 +136,27 @@ def epoch_to_age(epoch, incl_date=False):
)


def print_preview(code: str, lang: str): # pragma: no cover
copiable = False


def set_copiable():
global copiable
copiable = True


def clear_copiable():
global copiable
copiable = False


def print_preview(code: str, lang: str, copy: bool = False): # pragma: no cover
print()
print("[bold white]Preview[/bold white]")

if copy:
set_copiable()
set_copytext(code)

# NOTE: we can set background_color="default" to remove background
print(Syntax(code.strip("\n"), lang))
print()
Expand All @@ -148,10 +170,19 @@ def ask_execute(question="Execute code?", default=True) -> bool: # pragma: no c
termios.tcflush(sys.stdin, termios.TCIFLUSH) # flush stdin

choicestr = f"[{'Y' if default else 'y'}/{'n' if default else 'N'}]"
copystr = r"\[c] to copy" if copiable else ""
answer = console.input(
f"[bold bright_yellow on red] {question} {choicestr} [/] ",
f"[bold bright_yellow on red] {question} {choicestr}{copystr} [/] ",
)

global override_auto

if not override_auto and copiable and "c" == answer.lower().strip():
if copy():
print("Copied to clipboard.")
return False
clear_copiable()

if answer.lower() in [
"auto"
]: # secret option to stop asking for the rest of the session
Expand Down Expand Up @@ -309,3 +340,12 @@ def path_with_tilde(path: Path) -> str:
if path_str.startswith(home):
return path_str.replace(home, "~", 1)
return path_str


@functools.lru_cache
def get_installed_programs(candidates: tuple[str, ...]) -> set[str]:
ErikBjare marked this conversation as resolved.
Show resolved Hide resolved
installed = set()
for candidate in candidates:
if shutil.which(candidate) is not None:
installed.add(candidate)
return installed
Loading