diff --git a/docker_setup/docker-compose.yml b/docker_setup/docker-compose.yml index 7cd2e653fe..a878411f3a 100644 --- a/docker_setup/docker-compose.yml +++ b/docker_setup/docker-compose.yml @@ -28,7 +28,10 @@ services: volumes: - ${DATA_DIR}:${DATA_DIR}:rw # To run UniqueBibleApp terminal mode: - # > docker run -v $HOME/UniqueBible:$HOME/UniqueBible -it uniquebible-ubterm + # > docker run --rm --name ubterm -v $HOME/UniqueBible:$HOME/UniqueBible --network uniquebible_default uniquebible-ubterm + # Shell only: + # > docker run -it --rm --name ubterm -v $HOME/UniqueBible:$HOME/UniqueBible --network uniquebible_default uniquebible-ubterm sh + # Remarks: Use `docker network ls` to check the network name. The default network name is [COMPOSE_PROJECT_NAME]_default ubterm: build: container_name: ubterm diff --git a/docker_setup/uniquebible/Dockerfile_ubterm b/docker_setup/uniquebible/Dockerfile_ubterm index 8f2b0548bd..50080b4df4 100644 --- a/docker_setup/uniquebible/Dockerfile_ubterm +++ b/docker_setup/uniquebible/Dockerfile_ubterm @@ -13,7 +13,7 @@ RUN \ RUN \ apk update && \ - apk add doas python3 py3-pip ffmpeg micro nano && \ + apk add doas python3 py3-pip ffmpeg micro nano w3m lynx && \ echo 'permit nopass :wheel' >> /etc/doas.conf USER ${USER} diff --git a/setup.py b/setup.py index fba6947302..326be21402 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.2.0", + version="0.2.3", 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, @@ -563,9 +563,11 @@ }, entry_points={ "console_scripts": [ - f"{package}={package}.uba:main", + f"{package}={package}.uba:main", # gui in a subprocess + f"ubgui={package}.uba:gui", # gui in a single process f"ub={package}.uba:stream", f"ubapi={package}.uba:api", + f"uba={package}.uba:api", # essentailly the same as ubapi, just quicker to type f"ubhttp={package}.uba:http", f"ubssh={package}.uba:ssh", f"ubtelnet={package}.uba:telnet", diff --git a/uniquebible/latest_changes.txt b/uniquebible/latest_changes.txt index aaa4855069..ffe21aad45 100755 --- a/uniquebible/latest_changes.txt +++ b/uniquebible/latest_changes.txt @@ -1,5 +1,13 @@ PIP package: +0.2.1-0.2.3 + +* added support of xonsh auto-completions + +* improved cli options + +* fixed vlc loop issue + 0.2.0 * fixed fresh installation diff --git a/uniquebible/requirements.txt b/uniquebible/requirements.txt index 4c329322ff..7f90291e80 100644 --- a/uniquebible/requirements.txt +++ b/uniquebible/requirements.txt @@ -8,4 +8,6 @@ opencc-python-reimplemented mistralai openai groq -packaging \ No newline at end of file +packaging +haversine +xonsh[full] \ No newline at end of file diff --git a/uniquebible/startup/nonGui.py b/uniquebible/startup/nonGui.py index 8dc9cccfee..16bc066de6 100755 --- a/uniquebible/startup/nonGui.py +++ b/uniquebible/startup/nonGui.py @@ -238,6 +238,8 @@ def getApiOutput(command: str): 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 uniquebible.util.PromptValidator import NumberValidator + from prompt_toolkit import prompt 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 @@ -246,6 +248,34 @@ def multiturn_api_output(apiCommandSuggestions=None): from prompt_toolkit.completion import WordCompleter, NestedCompleter, ThreadedCompleter, FuzzyCompleter import webbrowser + def simplePrompt(default="", numberOnly=False, inputIndicator=">>> "): + promptStyle = Style.from_dict({ + "": config.terminalCommandEntryColor2, + "indicator": config.terminalPromptIndicatorColor2, + }) + inputIndicator = [ + ("class:indicator", inputIndicator), + ] + if numberOnly: + userInput = prompt(inputIndicator, style=promptStyle, default=default, validator=NumberValidator()).strip() + else: + userInput = prompt(inputIndicator, style=promptStyle, default=default).strip() + return userInput + + def changeSettings(): + print("# Chaning web API endpoint ...") # config.web_api_endpoint + if configuration := simplePrompt(config.web_api_endpoint): + config.web_api_endpoint = configuration + ConfigUtil.save() + print("# Chaning web API timeout ...") # config.web_api_timeout + if configuration := simplePrompt(str(config.web_api_timeout), True): + config.web_api_timeout = int(configuration) + ConfigUtil.save() + print("# Chaning web API private key ...") # config.web_api_private + if configuration := simplePrompt(config.web_api_private): + config.web_api_private = configuration + ConfigUtil.save() + # startup set_title("Unique Bible App API-Client") print("Running Unique Bible App api-client ...") @@ -260,7 +290,11 @@ def multiturn_api_output(apiCommandSuggestions=None): # initiate main prompt session initiateMainPrompt() - command_completer = FuzzyCompleter(ThreadedCompleter(NestedCompleter.from_nested_dict(apiCommandSuggestions))) if apiCommandSuggestions is not None else None + if apiCommandSuggestions is None: + apiCommandSuggestions = {} + for i in (".quit", ".help", ".settings"): + apiCommandSuggestions[i] = None + command_completer = FuzzyCompleter(ThreadedCompleter(NestedCompleter.from_nested_dict(apiCommandSuggestions))) auto_suggestion=AutoSuggestFromHistory() toolbar = " [ctrl+q] .quit [escape+h] .help " style = Style.from_dict({ @@ -295,6 +329,9 @@ def multiturn_api_output(apiCommandSuggestions=None): if command: if command.lower() == ".quit": break + elif command.lower() == ".settings": + changeSettings() + continue elif command.lower() == ".help": webbrowser.open("https://github.com/eliranwong/UniqueBibleAPI") continue diff --git a/uniquebible/uba.py b/uniquebible/uba.py index e7cc42047c..cdcc2a92cb 100755 --- a/uniquebible/uba.py +++ b/uniquebible/uba.py @@ -152,6 +152,10 @@ def desktopFileContent(): else: subprocess.Popen([python, mainFile, initialCommand] if initialCommand else [python, mainFile]) +def gui(): + sys.argv.insert(1, "gui") + main() + def stream(): sys.argv.insert(1, "stream") main() diff --git a/uniquebible/util/ConfigUtil.py b/uniquebible/util/ConfigUtil.py index 0362150bdb..80c77aea37 100644 --- a/uniquebible/util/ConfigUtil.py +++ b/uniquebible/util/ConfigUtil.py @@ -167,7 +167,7 @@ def updateModules(module, isInstalled): # Check installed and latest versions on startup.""", True) - # UBA Web API + # UBA Web API (work with GUI) setConfig("uniquebible_api_endpoint", """ # UBA Web API server API endpoint""", "https://bible.gospelchurch.uk/html") @@ -178,6 +178,18 @@ def updateModules(module, isInstalled): # UBA Web API server API key to access private data""", "") + # UBA Web API (work with API client mode on console) + # 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 groq chat setting # config.llm_backend # config.addBibleQnA @@ -313,17 +325,6 @@ def updateModules(module, isInstalled): # When there is no bible reference found in the entry, after trying with the default command, the original command will be prefixed with the value of `config.secondDefaultCommand` and executed with it.""", "REGEXSEARCH:::") - # 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/TextCommandParser.py b/uniquebible/util/TextCommandParser.py index cbbf93d577..15498995e4 100644 --- a/uniquebible/util/TextCommandParser.py +++ b/uniquebible/util/TextCommandParser.py @@ -1751,13 +1751,13 @@ def getHideOutputSuffix(): model_config_path = f"""{model_path}.json""" if os.path.isfile(model_path): if shutil.which("cvlc"): - cmd = f'''"{shutil.which("piper")}" --model "{model_path}" --config "{model_config_path}" --output-raw | cvlc --play-and-exit --rate {config.vlcSpeed} --demux=rawaud --rawaud-channels=1 --rawaud-samplerate=22050 -{getHideOutputSuffix()}''' + cmd = f'''"{shutil.which("piper")}" --model "{model_path}" --config "{model_config_path}" --output-raw | cvlc --no-loop --play-and-exit --rate {config.vlcSpeed} --demux=rawaud --rawaud-channels=1 --rawaud-samplerate=22050 -{getHideOutputSuffix()}''' elif shutil.which("aplay"): cmd = f'''"{shutil.which("piper")}" --model "{model_path}" --config "{model_config_path}" --output-raw | aplay -r 22050 -f S16_LE -t raw -{getHideOutputSuffix()}''' else: print("[Downloading voice ...] ") if shutil.which("cvlc"): - cmd = f'''"{shutil.which("piper")}" --model {config.piperVoice} --download-dir "{model_dir}" --data-dir "{model_dir}" --output-raw | cvlc --play-and-exit --rate {config.vlcSpeed} --demux=rawaud --rawaud-channels=1 --rawaud-samplerate=22050 -{getHideOutputSuffix()}''' + cmd = f'''"{shutil.which("piper")}" --model {config.piperVoice} --download-dir "{model_dir}" --data-dir "{model_dir}" --output-raw | cvlc --no-loop --play-and-exit --rate {config.vlcSpeed} --demux=rawaud --rawaud-channels=1 --rawaud-samplerate=22050 -{getHideOutputSuffix()}''' elif shutil.which("aplay"): cmd = f'''"{shutil.which("piper")}" --model {config.piperVoice} --download-dir "{model_dir}" --data-dir "{model_dir}" --output-raw | aplay -r 22050 -f S16_LE -t raw -{getHideOutputSuffix()}''' pydoc.pipepager(text, cmd=cmd) diff --git a/uniquebible/util/VlcUtil.py b/uniquebible/util/VlcUtil.py index 3cd3ca1b32..95e13bace4 100644 --- a/uniquebible/util/VlcUtil.py +++ b/uniquebible/util/VlcUtil.py @@ -86,7 +86,7 @@ def playMediaFileVlcNoGui(filePath, vlcSpeed): command = f'''"{config.windowsVlc}" --intf dummy --play-and-exit --rate {vlcSpeed} "{filePath}"''' # vlc on other platforms elif VlcUtil.isPackageInstalled("cvlc"): - command = f'''cvlc --play-and-exit --rate {vlcSpeed} "{filePath}" &> /dev/null''' + command = f'''cvlc --no-loop --play-and-exit --rate {vlcSpeed} "{filePath}" &> /dev/null''' # use .communicate() to wait for the playback to be completed as .wait() or checking pid existence does not work subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() except: @@ -104,7 +104,7 @@ def playMediaFileVlcGui(filePath, vlcSpeed): command = f'''"{config.windowsVlc}" --play-and-exit --rate {vlcSpeed} "{filePath}"''' # vlc on other platforms elif VlcUtil.isPackageInstalled("vlc"): - command = f'''vlc --play-and-exit --rate {vlcSpeed} "{filePath}" &> /dev/null''' + command = f'''vlc --no-loop --play-and-exit --rate {vlcSpeed} "{filePath}" &> /dev/null''' # use .communicate() to wait for the playback to be completed as .wait() or checking pid existence does not work subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() except: diff --git a/uniquebible/xonsh/README.md b/uniquebible/xonsh/README.md new file mode 100644 index 0000000000..09b0ce8804 --- /dev/null +++ b/uniquebible/xonsh/README.md @@ -0,0 +1 @@ +To work with auto suggestions and completions on Xonsh, copy the content in completer.py to ~/.xonshrc \ No newline at end of file diff --git a/uniquebible/xonsh/completer.py b/uniquebible/xonsh/completer.py new file mode 100644 index 0000000000..6ba7e9e62d --- /dev/null +++ b/uniquebible/xonsh/completer.py @@ -0,0 +1,81 @@ +from xonsh.completers.tools import * + +# UniqueBible App command keywords + +bibleKeywords = ["text", "studytext", "_chapters", "_bibleinfo", "_vnsc", "_vndc", "readchapter", "readverse", "readword", "readlexeme", "compare", "comparechapter", "count", "search", "andsearch", "orsearch", "advancedsearch", "regexsearch", "bible", "chapter", "main", "study", "read", "readsync", "_verses", "_biblenote"] +lexiconKeywords = ["lexicon", "searchlexicon", "reverselexicon"] +commentaryKeywords = ["_commentarychapters", "_commentaryinfo", "commentary", "_commentaryverses", "_commentary"] +referenceKeywords = ["_book", "book", "searchbook", "searself.crossPlatformchbookchapter"] +thirdPartyDictKeywords = ["thirddictionary", "searchthirddictionary", "s3dict", "3dict"] + +# Get UniqueBible App local resources + +from uniquebible import config as ubaconfig +from uniquebible.util.ConfigUtil import ConfigUtil +ConfigUtil.setup() +ubaconfig.noQt=True +from uniquebible.util.CrossPlatform import CrossPlatform +crossPlatform = CrossPlatform() +crossPlatform.setupResourceLists() +resources = crossPlatform.resources + +@non_exclusive_completer +@contextual_completer +def ub_completer(context): + if context.command and context.command.args and context.command.args[0].value == "ub" and context.command.prefix: + check = " ".join([i.value for i in context.command.args[1:] if hasattr(i, "value")]) + context.command.prefix + if re.search(f"^({'|'.join(bibleKeywords)}):::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in resources.get("bibleListAbb", [])]) + elif re.search("^concordance:::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in resources.get("strongBibleListAbb", [])]) + elif re.search(f"^({'|'.join(lexiconKeywords)}):::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in resources.get("lexiconList", [])]) + elif re.search("^data:::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in resources.get("dataList", [])]) + elif re.search(f"^({'|'.join(commentaryKeywords)}):::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in resources.get("commentaryListAbb", [])]) + elif re.search("^dictionary:::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in resources.get("dictionaryListAbb", [])]) + elif re.search("^encyclopedia:::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in resources.get("encyclopediaListAbb", [])]) + elif re.search(f"^({'|'.join(referenceKeywords)}):::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in resources.get("referenceBookList", [])]) + elif re.search(f"^({'|'.join(thirdPartyDictKeywords)}):::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in resources.get("thirdPartyDictionaryList", [])]) + elif re.search("^searchtool:::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in resources.get("searchToolList", [])]) +completer add "ub_completer" ub_completer "start" + +# Get UniqueBible App API resources +import requests, re +url = f"https://bible.gospelchurch.uk/json?cmd=.resources" +response = requests.get(url) +response.encoding = "utf-8" +api_resources = response.json() + +@non_exclusive_completer +@contextual_completer +def ubapi_completer(context): + if context.command and context.command.args and context.command.args[0].value in ('ubapi', 'uba') and context.command.prefix: + check = " ".join([i.value for i in context.command.args[1:] if hasattr(i, "value")]) + context.command.prefix + if re.search(f"^({'|'.join(bibleKeywords)}):::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in api_resources.get("bibleListAbb", [])]) + elif re.search("^concordance:::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in api_resources.get("strongBibleListAbb", [])]) + elif re.search(f"^({'|'.join(lexiconKeywords)}):::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in api_resources.get("lexiconList", [])]) + elif re.search("^data:::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in api_resources.get("dataList", [])]) + elif re.search(f"^({'|'.join(commentaryKeywords)}):::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in api_resources.get("commentaryListAbb", [])]) + elif re.search("^dictionary:::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in api_resources.get("dictionaryListAbb", [])]) + elif re.search("^encyclopedia:::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in api_resources.get("encyclopediaListAbb", [])]) + elif re.search(f"^({'|'.join(referenceKeywords)}):::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in api_resources.get("referenceBookList", [])]) + elif re.search(f"^({'|'.join(thirdPartyDictKeywords)}):::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in api_resources.get("thirdPartyDictionaryList", [])]) + elif re.search("^searchtool:::", check, re.IGNORECASE): + return set([context.command.prefix+i for i in api_resources.get("searchToolList", [])]) +completer add "ubapi_completer" ubapi_completer "start"