diff --git a/jarviscli/CmdInterpreter.py b/jarviscli/CmdInterpreter.py index 4af26354b..e67840788 100644 --- a/jarviscli/CmdInterpreter.py +++ b/jarviscli/CmdInterpreter.py @@ -4,18 +4,15 @@ import traceback from cmd import Cmd from functools import partial - from colorama import Fore - from packages.memory.memory import Memory from PluginManager import PluginManager from utilities import schedule from utilities.animations import SpinnerThread -from utilities.GeneralUtilities import get_parent_directory +from utilities.GeneralUtilities import get_parent_directory, theme_option from utilities.notification import notify from utilities.voice import create_voice - class JarvisAPI(object): """ Jarvis interface for plugins. @@ -42,7 +39,7 @@ def say(self, text, color="", speak=True): e.g. Fore.BLUE :param speak: False-, if text shouldn't be spoken even if speech is enabled """ - print(color + text + Fore.RESET, flush=True) + print(color + text + theme_option('reset_text'), flush=True) if speak: self._jarvis.speak(text) @@ -53,7 +50,7 @@ def input(self, prompt="", color=""): """ # we can't use input because for some reason input() and color codes do not work on # windows cmd - sys.stdout.write(color + prompt + Fore.RESET) + sys.stdout.write(color + prompt + theme_option('reset_text')) sys.stdout.flush() text = sys.stdin.readline() # return without newline @@ -137,7 +134,7 @@ def cancel(self, schedule_id): self._jarvis.scheduler.cancel(schedule_id) spinner.stop() - self.say('Cancellation successful', Fore.GREEN) + self.say('Cancellation successful', theme_option('positive_text')) # Voice wrapper def enable_voice(self): @@ -233,11 +230,13 @@ def spinner_start(self, message="Starting "): self.spinner = SpinnerThread(message, 0.15) self.spinner.start() - def spinner_stop(self, message="Task executed successfully! ", color=Fore.GREEN): + def spinner_stop(self, message="Task executed successfully! ", color=""): """ Function for stopping the spinner when prompted from a plugin and displaying the message after completing the task """ + color = theme_option('positive_text') + self.spinner.stop() self.say(message, color) self.spinner_running = False @@ -275,8 +274,8 @@ def incorrect_option(self): A function to notify the user that an incorrect option has been entered and prompting him to enter a correct one """ - self.say("Oops! Looks like you entered an incorrect option", Fore.RED) - self.say("Look at the options once again:", Fore.GREEN) + self.say("Oops! Looks like you entered an incorrect option", theme_option('negative_text')) + self.say("Look at the options once again:", theme_option('info')) def catch_all_exceptions(do, pass_self=True): @@ -290,12 +289,12 @@ def try_do(self, s): if self._api.is_spinner_running(): self.spinner_stop("It seems some error has occured") print( - Fore.RED + theme_option('negative_text') + "Some error occurred, please open an issue on github!") print("Here is error:") print('') traceback.print_exc() - print(Fore.RESET) + print(theme_option('reset_text')) return try_do @@ -315,6 +314,7 @@ def __init__( This constructor contains a dictionary with Jarvis Actions (what Jarvis can do). In alphabetically order. """ + Cmd.__init__(self) command = " ".join(sys.argv[1:]).strip() self.first_reaction = first_reaction @@ -346,8 +346,8 @@ def __init__( self.speech = create_voice( self, gtts_status, rate=self.speech_rate) except Exception as e: - self.say("Voice not supported", Fore.RED) - self.say(str(e), Fore.RED) + self.say("Voice not supported", theme_option('negative_text')) + self.say(str(e), theme_option('info')) self.fixed_responses = {"what time is it": "clock", "where am i": "pinpoint", @@ -364,20 +364,21 @@ def __init__( if self.first_reaction: self._api.say(self.first_reaction_text) + self.first_reaction = False # DEBUG, MIGHT BE USELESS def _init_plugin_info(self): plugin_status_formatter = { "disabled": len(self._plugin_manager.get_disabled()), "enabled": self._plugin_manager.get_number_plugins_loaded(), - "red": Fore.RED, - "blue": Fore.BLUE, - "reset": Fore.RESET + "info": theme_option('info'), + "text": theme_option('default_text'), + "reset": theme_option('reset_text') } - plugin_status = "{red}{enabled} {blue}plugins loaded" + plugin_status = "{info}{enabled} {text}plugins loaded" if plugin_status_formatter['disabled'] > 0: - plugin_status += " {red}{disabled} {blue}plugins disabled. More information: {red}status\n" - plugin_status += Fore.RESET + plugin_status += " {info}{disabled} {text}plugins disabled. More information: {info}status\n" + plugin_status += theme_option('reset_text') self.first_reaction_text += plugin_status.format( **plugin_status_formatter) @@ -428,7 +429,7 @@ def close(self): if self._api.is_spinner_running(): self._api.spinner_stop('Some error has occured') - self.say("Goodbye, see you later!", Fore.RED) + self.say("Goodbye, see you later!", theme_option('greeting')) self.scheduler.stop_all() sys.exit() @@ -438,7 +439,7 @@ def execute_once(self, command): def error(self): """Jarvis let you know if an error has occurred.""" - self.say("I could not identify your command...", Fore.RED) + self.say("I could not identify your command...", theme_option('negative_text')) def interrupt_handler(self, signal, frame): """Closes Jarvis on SIGINT signal. (Ctrl-C)""" @@ -469,7 +470,7 @@ def do_help(self, arg): headerString = "These are valid commands for Jarvis" formatString = "Format: command ([aliases for command])" self.say(headerString) - self.say(formatString, Fore.BLUE) + self.say(formatString, theme_option('info')) pluginDict = self._plugin_manager.get_plugins() uniquePlugins = {} for key in pluginDict.keys(): diff --git a/jarviscli/Jarvis.py b/jarviscli/Jarvis.py index dba472878..41ef147f6 100644 --- a/jarviscli/Jarvis.py +++ b/jarviscli/Jarvis.py @@ -6,13 +6,11 @@ import re import sys import tempfile -from utilities.GeneralUtilities import print_say +from utilities.GeneralUtilities import print_say, theme_option from CmdInterpreter import CmdInterpreter # register hist path HISTORY_FILENAME = tempfile.TemporaryFile('w+t') - - PROMPT_CHAR = '~>' """ @@ -29,32 +27,38 @@ the actual location of our laptops. """ - class Jarvis(CmdInterpreter, object): - # variable used at Breakpoint #1. - # allows Jarvis say "Hi", only at the first interaction. + """ + variable used at Breakpoint #1. + allows Jarvis say "Hi", only at the first interaction. + specified colors are immediately overwritten, but is + kept to ensure nothing accidentally breaks + """ first_reaction_text = "" - first_reaction_text += Fore.BLUE + \ - 'Jarvis\' sound is by default disabled.' + Fore.RESET - first_reaction_text += "\n" - first_reaction_text += Fore.BLUE + 'In order to let Jarvis talk out loud type: ' - first_reaction_text += Fore.RESET + Fore.RED + 'enable sound' + Fore.RESET - first_reaction_text += "\n" - first_reaction_text += Fore.BLUE + \ - "Type 'help' for a list of available actions." + Fore.RESET - first_reaction_text += "\n" - prompt = ( - Fore.RED - + "{} Hi, what can I do for you?\n".format(PROMPT_CHAR) - + Fore.RESET) + prompt = () # Used to store user specific data - def __init__(self, first_reaction_text=first_reaction_text, prompt=prompt, first_reaction=True, directories=["jarviscli/plugins", "custom"]): + directories = self._rel_path_fix(directories) + # Reset first_reaction_text on init in order to change theme + first_reaction_text += theme_option('default_text') + \ + 'Jarvis\' sound is by default disabled.' + theme_option('reset_text') + first_reaction_text += "\n" + first_reaction_text += theme_option('default_text') + 'In order to let Jarvis talk out loud type: ' + first_reaction_text += theme_option('reset_text') + theme_option('info') + 'enable sound' + theme_option('reset_text') + first_reaction_text += "\n" + first_reaction_text += theme_option('default_text') + \ + "Type " + theme_option('info') + " 'help' " + "for a list of available actions." + theme_option('reset_text') + first_reaction_text += "\n" + prompt = ( + theme_option('greeting') + + "{} Hi, what can I do for you?\n".format(PROMPT_CHAR) + + theme_option('reset_text')) + if sys.platform == 'win32': self.use_rawinput = False self.regex_dot = re.compile('\\.(?!\\w)') @@ -79,7 +83,7 @@ def _rel_path_fix(self, dirs): def default(self, data): """Jarvis let's you know if an error has occurred.""" - print_say("I could not identify your command...", self, Fore.RED) + print_say("I could not identify your command...", self, theme_option('negative_text')) def precmd(self, line): """Hook that executes before every command.""" @@ -104,12 +108,13 @@ def precmd(self, line): def postcmd(self, stop, line): """Hook that executes after every command.""" - if self.first_reaction: - self.prompt = ( - Fore.RED + # if self.first_reaction: + self.prompt = ( + theme_option('greeting') + "{} What can I do for you?\n".format(PROMPT_CHAR) - + Fore.RESET) - self.first_reaction = False + + theme_option('reset_text')) + # self.first_reaction = False + if self.enable_voice: self.speech.text_to_speech("What can I do for you?\n") @@ -128,7 +133,7 @@ def parse_input(self, data): # input sanitisation to not mess up urls / numbers data = self.regex_dot.sub("", data) - + # Check if Jarvis has a fixed response to this data if data in self.fixed_responses: output = self.fixed_responses[data] diff --git a/jarviscli/data/user_theme.json b/jarviscli/data/user_theme.json new file mode 100644 index 000000000..96e767746 --- /dev/null +++ b/jarviscli/data/user_theme.json @@ -0,0 +1 @@ +{"current": {"greeting": "\u001b[32m", "default_text": "\u001b[37m", "info": "\u001b[31m", "negative_text": "\u001b[31m", "positive_text": "\u001b[32m", "reset_text": "\u001b[37m"}, "default": {"greeting": "\u001b[31m", "default_text": "\u001b[34m", "info": "\u001b[31m", "negative_text": "\u001b[31m", "positive_text": "\u001b[32m", "reset_text": "\u001b[39m"}, "blurple": {"greeting": "\u001b[35m", "default_text": "\u001b[36m", "info": "\u001b[34m", "negative_text": "\u001b[31m", "positive_text": "\u001b[32m", "reset_text": "\u001b[39m"}, "colorless": {"greeting": "\u001b[39m", "default_text": "\u001b[39m", "info": "\u001b[39m", "negative_text": "\u001b[39m", "positive_text": "\u001b[39m", "reset_text": "\u001b[39m"}, "lightmode": {"greeting": "\u001b[31m", "default_text": "\u001b[30m", "info": "\u001b[34m", "negative_text": "\u001b[31m", "positive_text": "\u001b[32m", "reset_text": "\u001b[39m"}, "darkmode": {"greeting": "\u001b[37m", "default_text": "\u001b[39m", "info": "\u001b[32m", "negative_text": "\u001b[31m", "positive_text": "\u001b[32m", "reset_text": "\u001b[39m"}, "evil": {"greeting": "\u001b[33m", "default_text": "\u001b[31m", "info": "\u001b[33m", "negative_text": "\u001b[31m", "positive_text": "\u001b[32m", "reset_text": "\u001b[31m"}, "agent-smith": {"greeting": "\u001b[32m", "default_text": "\u001b[37m", "info": "\u001b[31m", "negative_text": "\u001b[31m", "positive_text": "\u001b[32m", "reset_text": "\u001b[37m"}} \ No newline at end of file diff --git a/jarviscli/plugins/change_color.py b/jarviscli/plugins/change_color.py new file mode 100644 index 000000000..523c0a2dc --- /dev/null +++ b/jarviscli/plugins/change_color.py @@ -0,0 +1,141 @@ +import json +import requests +from plugin import plugin, alias +from colorama import Fore + +USER_THEME_FILEPATH = "jarviscli/data/user_theme.json" +NUM_OPTIONS = 6 # Specifies the number of different color options + + +@alias("switch themes") +@alias("change themes") +@alias("switch theme") +@alias("change theme") +@alias("switch colours") +@alias("change colours") +@alias("switch colour") +@alias("change colour") +@alias("switch colors") +@alias("change colors") +@alias("switch color") +@plugin("change color") + +def change_color(jarvis, s): + """The user will first input whether or not they want to use a preset theme, + and the plugin will provide a selection of themes. If not, the user is then + prompted to create their own custom theme, which persists.""" + + f = open(USER_THEME_FILEPATH) + data = json.load(f) + theme = data['current'] + options = list(theme.keys()) + + color_dict = { + "black": Fore.BLACK, + "red": Fore.RED, + "green": Fore.GREEN, + "yellow": Fore.YELLOW, + "blue": Fore.BLUE, + "magenta": Fore.MAGENTA, + "cyan": Fore.CYAN, + "white": Fore.WHITE, + "reset": Fore.RESET, + } + + use_preset = jarvis.input( + "Use a preset theme? (y/n): ", + color=theme['greeting'] + ) + + # @TODO YES to preset theme + if (use_preset.lower() == 'y'): + presets = list(data.keys()) + jarvis.say( + theme['default_text'] + + "\nSupported themes are:\n" + + theme['info'] + + ', '.join(presets[1:]) + ) + choice = jarvis.input( + "So...what'll it be?\n", + color=theme['default_text'] + ) + + if (choice in presets): + data['current'] = data[choice] + with open("custom/user_theme.json", 'w') as new_json: + json.dump(data, new_json) + + jarvis.say("Preset selected!\n", + color=theme['positive_text']) + else: + jarvis.say("Invalid option. Please try again", + color=theme['negative_text']) + + + # NO to preset theme + elif (use_preset.lower() == 'n'): + use_custom = jarvis.input( + "Customize the current theme? (y/n): ", + color=theme['greeting'] + ) + + # YES to customizing theme + if (use_custom.lower() == 'y'): + supported_colors = "" + for key in color_dict.keys(): + supported_colors += color_dict[key] + key.upper() + ", " + + jarvis.say( + theme['default_text'] + + "\nSupported colors are:\n" + + supported_colors[:-2] + ) + jarvis.say( + theme['default_text'] + + "If you wish to skip an option, type " + + theme['info'] + "'skip'.\n" + ) + + it = 0 + while (it < NUM_OPTIONS): + cur_option = options[it] + new_color = jarvis.input( + "New color for " + + cur_option + + ": ", + color=theme[cur_option]) + + if (new_color.lower() in color_dict): + theme[cur_option] = color_dict[new_color.lower()] + it = it + 1 + + elif (new_color.lower() == "skip"): + it = it + 1 + + else: + jarvis.say("Invalid option. Please try again", + color=theme['negative_text']) + + data['current'] = theme + + with open("custom/user_theme.json", 'w') as new_json: + json.dump(data, new_json) + + jarvis.say("All done! Enjoy your new theme!\n", + color=theme['positive_text']) + + # NO to customizing theme (END) + elif (use_custom.lower() == 'n'): + jarvis.say("\n...I guess there's no pleasing you\n", + color=theme['negative_text']) + + # INVALID command (END) + else: + jarvis.say("Invalid input. Please try again.\n", + color=theme['negative_text']) + + # INVALID command (END) + else: + jarvis.say("Invalid input. Please try again.\n", + color=theme['negative_text']) diff --git a/jarviscli/utilities/GeneralUtilities.py b/jarviscli/utilities/GeneralUtilities.py index f3d69733e..a8fd6fbbc 100644 --- a/jarviscli/utilities/GeneralUtilities.py +++ b/jarviscli/utilities/GeneralUtilities.py @@ -2,11 +2,13 @@ import distutils.spawn import os from platform import win32_ver +import json import sys import warnings from colorama import Fore +USER_THEME_FILEPATH = "jarviscli/data/user_theme.json" MACOS = 'darwin' WIN = 'win32' IS_MACOS = sys.platform == MACOS @@ -15,6 +17,24 @@ if IS_WIN: WIN_VER = win32_ver()[0] +def get_user_theme(filepath=USER_THEME_FILEPATH): + """ + Returns a dictionary corresponding to the current user theme. + :param filepath: the filepath to the JSON theme file + """ + # Load user-theme from JSON + f = open(filepath) + data = json.load(f) + return data['current'] + +def theme_option(option): + """ + Returns a color corresponding to specified theme option. + :param option: a string specifying the theme option + (greeting, default_text, info, negative_text, positive_text) + """ + theme = get_user_theme() + return theme[option] def print_say(text, self, color=""): """ @@ -32,7 +52,7 @@ def print_say(text, self, color=""): removed in the future. Please use \ ``JarvisAPI.say(text, color=\"\", speak=True))`` instead.", DeprecationWarning) - print(color + text + Fore.RESET) + print(color + text + theme_option('reset_text')) if self.enable_voice: self.speech.text_to_speech(text) @@ -40,7 +60,7 @@ def print_say(text, self, color=""): # Functions for printing user output # TODO decide which ones use print_say instead of print def critical(string): - print(Fore.RED + string + Fore.RESET) + print(theme_option('negative_text') + string + theme_option('reset_text')) def error(string): @@ -48,7 +68,7 @@ def error(string): def important(string): - print(Fore.YELLOW + string + Fore.RESET) + print(theme_option('greeting') + string + theme_option('reset_text')) def warning(string): @@ -56,7 +76,7 @@ def warning(string): def info(string): - print(Fore.BLUE + string + Fore.RESET) + print(theme_option('info') + string + theme_option('reset_text')) def unsupported(platform, silent=False): @@ -66,7 +86,7 @@ def wrapped(*args, **kwargs): if not silent: print( '{}Command is unsupported for platform `{}`{}'.format( - Fore.RED, sys.platform, Fore.RESET)) + theme_option('negative_text'), sys.platform, theme_option('reset_text'))) else: func(*args, **kwargs)