diff --git a/setup.py b/setup.py index 1bc3a962b..3b9fd3ff7 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ # https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/ setup( name=package, - version="0.1.57", + version="0.1.59", python_requires=">=3.8, <3.13", description=f"UniqueBible App is a cross-platform & offline bible application, integrated with high-quality resources and unique features. Developers: Eliran Wong and Oliver Tseng", long_description=long_description, @@ -564,6 +564,12 @@ entry_points={ "console_scripts": [ f"{package}={package}.uba:main", + f"ub={package}.uba:stream", + f"ubapi={package}.uba:api", + f"ubhttp={package}.uba:http", + f"ubssh={package}.uba:ssh", + f"ubtelnet={package}.uba:telnet", + f"ubterm={package}.uba:term", ], }, keywords="bible scripture na28 bsha hebrew greek ai marvelbible biblebento uba uniquebible", diff --git a/uniquebible/main.py b/uniquebible/main.py index 9d102a7fd..fcf712912 100755 --- a/uniquebible/main.py +++ b/uniquebible/main.py @@ -28,7 +28,7 @@ os.environ[key] = value # check runmode and initial command -config.noQt = True if config.runMode in ("stream", "terminal", "ssh-server", "telnet-server", "http-server", "api-server", "execute-macro") or os.path.isdir("/data/data/com.termux/files/home") else False +config.noQt = True if config.runMode in ("stream", "terminal", "ssh-server", "telnet-server", "http-server", "api-server", "api-client", "execute-macro") or os.path.isdir("/data/data/com.termux/files/home") else False config.cli = True if config.runMode == "cli" else False config.enableCli = True if config.runMode in ("cli", "gui", "docker") else False config.enableApiServer = True if config.runMode == "api-server" else False @@ -112,6 +112,8 @@ from uniquebible.startup.nonGui import * if config.runMode == "stream": run_stream_mode() + elif config.runMode == "api-client": + run_api_client_mode() elif config.runMode == "terminal": run_terminal_mode() elif config.runMode == "ssh-server": diff --git a/uniquebible/startup/nonGui.py b/uniquebible/startup/nonGui.py index e8a026e9e..6220774bb 100755 --- a/uniquebible/startup/nonGui.py +++ b/uniquebible/startup/nonGui.py @@ -100,7 +100,7 @@ def checkCommand(command): def run_terminal_mode(): from uniquebible.util.LocalCliHandler import LocalCliHandler from uniquebible.util.prompt_shared_key_bindings import prompt_shared_key_bindings - from uniquebible.util.uba_command_prompt_key_bindings import uba_command_prompt_key_bindings + from uniquebible.util.uba_command_prompt_key_bindings import api_command_prompt_key_bindings from prompt_toolkit.key_binding import merge_key_bindings from prompt_toolkit.shortcuts import set_title, clear_title from prompt_toolkit.auto_suggest import AutoSuggestFromHistory @@ -224,19 +224,133 @@ def run_terminal_mode(): clear_title() sys.exit(0) +# api-client mode + +def run_api_client_mode(): + + def getApiOutput(command: str): + private = f"private={config.web_api_private}&" if config.web_api_private else "" + url = f"""{config.web_api_endpoint}?{private}cmd={command}""" + response = requests.get(url, timeout=config.web_api_timeout) + response.encoding = "utf-8" + print(response.text.strip()) + + def multiturn_api_output(apiCommandSuggestions=None): + from uniquebible.util.prompt_shared_key_bindings import prompt_shared_key_bindings + from uniquebible.util.uba_command_prompt_key_bindings import api_command_prompt_key_bindings + from prompt_toolkit.key_binding import merge_key_bindings + from prompt_toolkit.shortcuts import set_title, clear_title + from prompt_toolkit.auto_suggest import AutoSuggestFromHistory + from prompt_toolkit.styles import Style + from prompt_toolkit.filters import Condition + from prompt_toolkit.completion import WordCompleter, NestedCompleter, ThreadedCompleter, FuzzyCompleter + import webbrowser + + # startup + set_title("Unique Bible App API-Client") + print("Running Unique Bible App api-client ...") + print("Enter an Unique Bible App command:") + print("For API documentation, visit https://github.com/eliranwong/UniqueBibleAPI") + + # make key bindings available in config to allow futher customisation via plugins + config.key_bindings = merge_key_bindings([ + prompt_shared_key_bindings, + api_command_prompt_key_bindings, + ]) + + # initiate main prompt session + initiateMainPrompt() + command_completer = FuzzyCompleter(ThreadedCompleter(NestedCompleter.from_nested_dict(apiCommandSuggestions))) if apiCommandSuggestions is not None else None + auto_suggestion=AutoSuggestFromHistory() + toolbar = " [ctrl+q] .quit [escape+h] .help " + style = Style.from_dict({ + # User input (default text). + "": config.terminalCommandEntryColor1, + # Prompt. + "indicator": config.terminalPromptIndicatorColor1, + }) + promptIndicator = ">>> " + promptIndicator = [ + ("class:indicator", promptIndicator), + ] + + command = "" + while True: + # User command input + command = config.main_prompt_session.prompt( + promptIndicator, + style=style, + completer=command_completer, + complete_in_thread=None, + auto_suggest=auto_suggestion, + bottom_toolbar=toolbar, + #default=default, + key_bindings=config.key_bindings, + # enable system prompt without auto-completion + # use escape+! + enable_system_prompt=True, + swap_light_and_dark_colors=Condition(lambda: not config.terminalResourceLinkColor.startswith("ansibright")), + #rprompt="Enter an UBA command", + ).strip() + if command: + if command.lower() == ".quit": + break + elif command.lower() == ".help": + webbrowser.open("https://github.com/eliranwong/UniqueBibleAPI") + continue + command = checkCommand(command) + # remove spaces before and after ":::" + command = re.sub("[ ]*?:::[ ]+?([^ ])", r":::\1", command) + # remove "_" before ":::" + command = re.sub("_:::", ":::", command) + # format chapter no. and verse no + command = re.sub("([0-9]+?)_([0-9]+?)_([0-9])", r"\1.\2.\3", command) + command = re.sub("_([0-9]+?)_([0-9]+?,)", r" \1:\2", command) + command = re.sub("_([0-9]+?)_([0-9]+?)$", r" \1:\2", command) + # change full width characters + command = re.sub(":::", r":::", command) + + getApiOutput(command) + + clear_title() + + try: + stdin_text = sys.stdin.read() if not sys.stdin.isatty() else "" + command = " ".join(sys.argv[2:]).strip() + if stdin_text: + command = f"{command} {stdin_text}" + + if command: + # stream output directly + getApiOutput(command) + else: + # interactive mode + private = f"private={config.web_api_private}&" if config.web_api_private else "" + r = requests.get(f"{config.web_api_endpoint}?{private}cmd=.suggestions", timeout=config.web_api_timeout) + r.encoding = "utf-8" + apiCommandSuggestions = r.json() + + multiturn_api_output(apiCommandSuggestions=apiCommandSuggestions) + + except: + #import traceback + #print(traceback.format_exc()) + print(f"Failed to connect '{config.web_api_endpoint}' at the moment!") + # raw mode def run_stream_mode(): from uniquebible.util.LocalCliHandler import LocalCliHandler - input_text = sys.stdin.read() if not sys.stdin.isatty() else "" + stdin_text = sys.stdin.read() if not sys.stdin.isatty() else "" # Set initial command - command = config.initial_command if config.initial_command else " ".join(sys.argv[2:]).strip() - if input_text: - command = f"{command} {input_text}" + command = " ".join(sys.argv[2:]).strip() + if stdin_text: + command = f"{command} {stdin_text}" if command.strip(): config.mainWindow = LocalCliHandler() output_text = config.mainWindow.getContent(command, False) + output_text = re.sub("\n[-]+?$", "", output_text) else: output_text = "Command not given!" print(output_text, file=sys.stdout) diff --git a/uniquebible/uba.py b/uniquebible/uba.py index 39e5af2ba..72f88a705 100755 --- a/uniquebible/uba.py +++ b/uniquebible/uba.py @@ -16,7 +16,7 @@ def main(): # check running mode and initial command runMode = sys.argv[1] if len(sys.argv) > 1 else "" - enableCli = True if runMode.lower() in ("stream", "cli", "cli.py", "gui", "terminal", "docker", "telnet-server", "http-server", "execute-macro", "api-server") else False + enableCli = True if runMode.lower() in ("stream", "cli", "cli.py", "gui", "terminal", "docker", "telnet-server", "http-server", "execute-macro", "api-server", "api-client") else False initialCommand = input("Enter command: ").strip() if runMode == "-i" else " ".join(sys.argv[1:]).strip() initialCommand = initialCommand.strip() @@ -197,5 +197,30 @@ def desktopFileContent(): else: subprocess.Popen([python, mainFile, initialCommand] if initialCommand else [python, mainFile]) +def stream(): + sys.argv.insert(1, "stream") + main() + +def api(): + # web api-client, not api-server mode + sys.argv.insert(1, "api-client") + main() + +def http(): + sys.argv.insert(1, "http-server") + main() + +def ssh(): + sys.argv.insert(1, "ssh-server") + main() + +def telnet(): + sys.argv.insert(1, "telnet-server") + main() + +def term(): + sys.argv.insert(1, "terminal") + main() + if __name__ == '__main__': main() \ No newline at end of file diff --git a/uniquebible/util/ConfigUtil.py b/uniquebible/util/ConfigUtil.py index 3c69552b8..0bf1cfd29 100644 --- a/uniquebible/util/ConfigUtil.py +++ b/uniquebible/util/ConfigUtil.py @@ -63,15 +63,8 @@ def setup(noQt=None, cli=None, enableCli=None, enableApiServer=None, enableHttpS config.ubaUserDir = ubaUserDir if os.path.isdir(ubaUserDir) else os.path.dirname(os.path.dirname(os.path.realpath(__file__))) # check running mode - config.runMode = sys.argv[1] if len(sys.argv) > 1 else "" - if " " in config.runMode: - import re - config.runMode, config.initial_command = config.runMode.split(" ", 1) - else: - config.initial_command = "" - config.runMode = config.runMode.lower() - - if config.runMode and not config.runMode in ("stream", "setup-only", "cli", "gui", "terminal", "docker", "telnet-server", "http-server", "execute-macro", "api-server"): + config.runMode = sys.argv[1].lower() if len(sys.argv) > 1 else "" + if config.runMode and not config.runMode in ("stream", "setup-only", "cli", "gui", "terminal", "docker", "telnet-server", "http-server", "execute-macro", "api-server", "api-client"): config.runMode = "" # Temporary configurations @@ -175,6 +168,17 @@ def getCurrentVenvDir(): # Use PySide6 as Qt Library, even config.qtLibrary is set to a value other than 'pyside6'.""", False) + # Start of api-client mode setting + setConfig("web_api_endpoint", """ + # UniqueBible App web API endpoint.""", + "https://bible.gospelchurch.uk/plain") + setConfig("web_api_timeout", """ + # UniqueBible App web API timeout.""", + 10) + setConfig("web_api_private", """ + # UniqueBible App web API private key.""", + "") + # Start of terminal mode setting setConfig("terminalWrapWords", """ # Wrap words in terminal mode.""", diff --git a/uniquebible/util/RemoteHttpHandler.py b/uniquebible/util/RemoteHttpHandler.py index ad549db81..afd4eb714 100644 --- a/uniquebible/util/RemoteHttpHandler.py +++ b/uniquebible/util/RemoteHttpHandler.py @@ -390,6 +390,7 @@ def do_GET(self): # convert to plain text for plain endpoint if api in ("json", "plain"): content = TextUtil.htmlToPlainText(content).strip() + content = re.sub("\n[-]+?$", "", content) if api == "json": output = {} if not ":::" in self.command: diff --git a/uniquebible/util/uba_command_prompt_key_bindings.py b/uniquebible/util/uba_command_prompt_key_bindings.py index 58c3751c1..801c02826 100644 --- a/uniquebible/util/uba_command_prompt_key_bindings.py +++ b/uniquebible/util/uba_command_prompt_key_bindings.py @@ -2,6 +2,20 @@ from prompt_toolkit.key_binding import KeyBindings #from prompt_toolkit.application import run_in_terminal +api_command_prompt_key_bindings = KeyBindings() + +# Escape+H launch help menu +@api_command_prompt_key_bindings.add("escape", "h") +def _(event): + event.app.current_buffer.text = ".help" + event.app.current_buffer.validate_and_handle() + +# Ctrl+Q quit UBA +@api_command_prompt_key_bindings.add("c-q") +def _(event): + event.app.current_buffer.text = ".quit" + event.app.current_buffer.validate_and_handle() + uba_command_prompt_key_bindings = KeyBindings() # add key bindings from Ctrl+B to Ctrl+Y, skipping Ctrl+A, C, D, E, H, J, M, N, O, P, T, V, X