From 8fd70f37bdd054263c4467cff019622ff89ce0d3 Mon Sep 17 00:00:00 2001 From: henk717 Date: Fri, 11 Apr 2025 12:23:23 +0200 Subject: [PATCH] Zentk integration (Zenity/yad support) (#1475) * Zentk integration (Zenity/yad support) * Escape incompatible dependencies in zentk * Properly clean env --- koboldcpp.py | 133 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 118 insertions(+), 15 deletions(-) diff --git a/koboldcpp.py b/koboldcpp.py index 46ead67c7..1e2b8b846 100644 --- a/koboldcpp.py +++ b/koboldcpp.py @@ -29,6 +29,7 @@ import html import urllib.parse as urlparse from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timezone +from typing import Tuple # constants sampler_order_max = 7 @@ -615,9 +616,8 @@ def unpack_to_dir(destpath = ""): print("Attempt to unpack KoboldCpp into directory...") if not cliunpack: - from tkinter.filedialog import askdirectory from tkinter import messagebox - destpath = askdirectory(title='Select an empty folder to unpack KoboldCpp') + destpath = zentk_askdirectory(title='Select an empty folder to unpack KoboldCpp') if not destpath: return @@ -3455,19 +3455,123 @@ def RunServerMultiThreaded(addr, port, server_handler): threadArr[i].stop() sys.exit(0) +# Based on https://github.com/mathgeniuszach/xdialog/blob/main/xdialog/zenity_dialogs.py - MIT license | - Expanded version by Henk717 + +def zenity_clean(txt: str): + return txt\ + .replace("\\", "\\\\")\ + .replace("$", "\\$")\ + .replace("!", "\\!")\ + .replace("*", "\\*")\ + .replace("?", "\\?")\ + .replace("&", "&")\ + .replace("|", "|")\ + .replace("<", "<")\ + .replace(">", ">")\ + .replace("(", "\\(")\ + .replace(")", "\\)")\ + .replace("[", "\\[")\ + .replace("]", "\\]")\ + .replace("{", "\\{")\ + .replace("}", "\\}")\ + +def zenity(typ, filetypes=None, initialdir="", initialfile="", **kwargs) -> Tuple[int, str]: + import shutil, subprocess, os, platform + if not platform.system() == "Linux": + raise Exception("This feature should only be used on Linux, if you see this error there is no TK fallback implemented in the code.") + zenity_bin = shutil.which("zenity") + if not zenity_bin: + zenity_bin = shutil.which("yad") + if not zenity_bin: + raise Exception("Zenity not present") + + # Build args based on keywords + args = ['/usr/bin/env', zenity_bin, '--'+typ] + for k, v in kwargs.items(): + if v is True: + args.append(f'--{k.replace("_", "-").strip("-")}') + elif isinstance(v, str): + cv = zenity_clean(v) if k != "title" else v + args.append(f'--{k.replace("_", "-").strip("-")}={cv}') + + # Build filetypes specially if specified + if filetypes: + for name, globs in filetypes: + if name: + globlist = globs.split() + args.append(f'--file-filter={name.replace("|", "")} ({", ".join(t for t in globlist)})|{globs}') + + # Default filename and folder + if initialdir is None: + initialdir=os.getcwd() + if initialfile is None: + initialfile="" + initialpath = os.path.join(initialdir, initialfile) + args.append(f'--filename={initialpath}') + + clean_env = os.environ.copy() + clean_env.pop("LD_LIBRARY_PATH", None) + clean_env["PATH"] = "/usr/bin:/bin" + + proc = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + shell=False, + env=clean_env + ) + stdout, _ = proc.communicate() + + return (proc.returncode, stdout.decode('utf-8').strip()) + +# note: In this section we wrap around file dialogues to allow for zenity + +def zentk_askopenfilename(**options): + try: + result = zenity('file-selection', filetypes=options.get("filetypes"), initialdir=options.get("initialdir"), title=options.get("title"))[1] + except: + from tkinter.filedialog import askopenfilename + result = askopenfilename(**options) + return result + +def zentk_askopenmultiplefilenames(**options): + try: + from os.path import isfile + files = zenity('file-selection', filetypes=options.get("filetypes"), initialdir=options.get("initialdir"), title=options.get("title"), multiple=True, separator="\n")[1].splitlines() + result = tuple(filter(isfile, files)) + except: + from tkinter.filedialog import askopenfilenames + result = askopenfilenames(**options) + return result + +def zentk_askdirectory(**options): + try: + result = zenity('file-selection', initialdir=options.get("initialdir"), title=options.get("title"), directory=True)[1] + except: + from tkinter.filedialog import askdirectory + result = askdirectory(**options) + return result + +def zentk_asksaveasfilename(**options): + try: + result = zenity('file-selection', filetypes=options.get("filetypes"), initialdir=options.get("initialdir"), initialfile=options.get("initialfile"), title=options.get("title"), save=True)[1] + except: + from tkinter.filedialog import asksaveasfilename + result = asksaveasfilename(**options) + return result +### End of MIT license + # note: customtkinter-5.2.0 def show_gui(): global using_gui_launcher using_gui_launcher = True - from tkinter.filedialog import askopenfilename, askdirectory - from tkinter.filedialog import asksaveasfilename # if args received, launch if len(sys.argv) != 1 and not args.showgui: import tkinter as tk root = tk.Tk() #we dont want the useless window to be visible, but we want it in taskbar root.attributes("-alpha", 0) - args.model_param = askopenfilename(title="Select ggml model .bin or .gguf file or .kcpps config") + args.model_param = zentk_askopenfilename(title="Select ggml model .bin or .gguf file or .kcpps config") root.withdraw() root.quit() if args.model_param and args.model_param!="" and (args.model_param.lower().endswith('.kcpps') or args.model_param.lower().endswith('.kcppt') or args.model_param.lower().endswith('.kcpps?download=true') or args.model_param.lower().endswith('.kcppt?download=true')): @@ -3770,16 +3874,16 @@ def show_gui(): initialDir = initialDir if os.path.isdir(initialDir) else None fnam = None if dialog_type==2: - fnam = askdirectory(title=text, mustexist=True, initialdir=initialDir) + fnam = zentk_askdirectory(title=text, mustexist=True, initialdir=initialDir) elif dialog_type==1: - fnam = asksaveasfilename(title=text, filetypes=filetypes, defaultextension=filetypes, initialdir=initialDir) + fnam = zentk_asksaveasfilename(title=text, filetypes=filetypes, defaultextension=filetypes, initialdir=initialDir) if not fnam: fnam = "" else: fnam = str(fnam).strip() fnam = f"{fnam}.jsondb" if ".jsondb" not in fnam.lower() else fnam else: - fnam = askopenfilename(title=text,filetypes=filetypes, initialdir=initialDir) + fnam = zentk_askopenfilename(title=text,filetypes=filetypes, initialdir=initialDir) if fnam: var.set(fnam) if onchoosefile: @@ -4181,7 +4285,7 @@ def show_gui(): def pickpremadetemplate(): initialDir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'kcpp_adapters') initialDir = initialDir if os.path.isdir(initialDir) else None - fnam = askopenfilename(title="Pick Premade ChatCompletions Adapter",filetypes=[("JSON Adapter", "*.json")], initialdir=initialDir) + fnam = zentk_askopenfilename(title="Pick Premade ChatCompletions Adapter",filetypes=[("JSON Adapter", "*.json")], initialdir=initialDir) if fnam: chatcompletionsadapter_var.set(fnam) ctk.CTkButton(model_tab, 64, text="Pick Premade", command=pickpremadetemplate).grid(row=25, column=0, padx=322, stick="nw") @@ -4311,7 +4415,7 @@ def show_gui(): file_type = [("KoboldCpp LaunchTemplate", "*.kcppt")] #remove blacklisted fields savdict = convert_args_to_template(savdict) - filename = asksaveasfilename(filetypes=file_type, defaultextension=".kcppt") + filename = zentk_asksaveasfilename(filetypes=file_type, defaultextension=".kcppt") if not filename: return filenamestr = str(filename).strip() @@ -4335,7 +4439,7 @@ def show_gui(): # launch def guilaunch(): if model_var.get() == "" and sd_model_var.get() == "" and whisper_model_var.get() == "" and tts_model_var.get() == "" and embeddings_model_var.get() == "" and nomodel.get()!=1: - tmp = askopenfilename(title="Select ggml model .bin or .gguf file") + tmp = zentk_askopenfilename(title="Select ggml model .bin or .gguf file") model_var.set(tmp) nonlocal nextstate nextstate = 1 @@ -4713,7 +4817,7 @@ def show_gui(): export_vars() savdict = json.loads(json.dumps(args.__dict__)) file_type = [("KoboldCpp Settings", "*.kcpps")] - filename = asksaveasfilename(filetypes=file_type, defaultextension=".kcpps") + filename = zentk_asksaveasfilename(filetypes=file_type, defaultextension=".kcpps") if not filename: return filenamestr = str(filename).strip() @@ -4727,7 +4831,7 @@ def show_gui(): def load_config_gui(): #this is used to populate the GUI with a config file, whereas load_config_cli simply overwrites cli args file_type = [("KoboldCpp Settings", "*.kcpps *.kcppt")] global runmode_untouched - filename = askopenfilename(filetypes=file_type, defaultextension=".kcppt", initialdir=None) + filename = zentk_askopenfilename(filetypes=file_type, defaultextension=".kcppt", initialdir=None) if not filename or filename=="": return runmode_untouched = False @@ -5371,8 +5475,7 @@ def analyze_gguf_model(args,filename): def analyze_gguf_model_wrapper(filename=""): if not filename or filename=="": try: - from tkinter.filedialog import askopenfilename - filename = askopenfilename(title="Select GGUF to analyze") + filename = zentk_askopenfilename(title="Select GGUF to analyze") except Exception as e: print(f"Cannot select file to analyze: {e}") if not filename or filename=="" or not os.path.exists(filename):