From 7ad4c94735d4cacceade94378a3f511142f951ae Mon Sep 17 00:00:00 2001 From: Brayo Date: Sun, 10 Nov 2024 20:41:59 +0300 Subject: [PATCH] feat: copy to clipboard (#234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: copy to clipboard feat: add copiable variable fix: update the copystr refactor: revert to "[c] to copy" feat: copy to clipboard refactor: revert to [c] to copy. handle circular import feat: simplify the input handling. * refactor: move get_installed_programs to utils * feat: stop for copying only if override auto is not set * Update gptme/clipboard.py Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * format: fixed formatting --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: Erik Bjäreholt --- gptme/clipboard.py | 45 ++++++++++++++++++++++++++++++++++++++++++++ gptme/tools/shell.py | 45 +++++++++++++++++++------------------------- gptme/tools/tmux.py | 2 +- gptme/util.py | 44 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 107 insertions(+), 29 deletions(-) create mode 100644 gptme/clipboard.py diff --git a/gptme/clipboard.py b/gptme/clipboard.py new file mode 100644 index 00000000..412b71e5 --- /dev/null +++ b/gptme/clipboard.py @@ -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 diff --git a/gptme/tools/shell.py b/gptme/tools/shell.py index 93156552..2c341871 100644 --- a/gptme/tools/shell.py +++ b/gptme/tools/shell.py @@ -3,12 +3,10 @@ """ import atexit -import functools import logging import os import re import select -import shutil import subprocess import sys from collections.abc import Generator @@ -16,34 +14,29 @@ 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""" @@ -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 diff --git a/gptme/tools/tmux.py b/gptme/tools/tmux.py index 15749eb7..5a4664f5 100644 --- a/gptme/tools/tmux.py +++ b/gptme/tools/tmux.py @@ -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) if not confirm(f"Execute command: {cmd}?"): yield Message("system", "Command execution cancelled.") return diff --git a/gptme/util.py b/gptme/util.py index f76af1a3..179a0b2d 100644 --- a/gptme/util.py +++ b/gptme/util.py @@ -1,6 +1,8 @@ import io import logging import random +import shutil +import functools import re import sys import termios @@ -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 @@ -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() @@ -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 @@ -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]: + installed = set() + for candidate in candidates: + if shutil.which(candidate) is not None: + installed.add(candidate) + return installed