diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..79a16af --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 \ No newline at end of file diff --git a/src/main.py b/src/main.py index f57bbdc..6816bc0 100644 --- a/src/main.py +++ b/src/main.py @@ -1,23 +1,27 @@ import os from time import time -from utils import * - +from utils import BASE_DIR, style_names, styles, logger +from wizard import TextQuestion, SelectQuestion, Wizard, inq_ask from textual.app import App from textual.widgets import Header, Footer, Label, LoadingIndicator from textual.validation import Length -from textual import log +from PIL import Image from textual.events import Key from click_extra import extra_command, option, ExtraContext, Parameter +# from textual import log VERSION = "1.1.1" -from wizard import * - BASIC_INFO_QUESTIONS = [ - TextQuestion("name", "Your project's name", [Length(1, failure_description="Your project's name cannot be blank")], "super-octo-project" ), + TextQuestion( + "name", + "Your project's name", + [Length(1, failure_description="Your project's name cannot be blank")], + "super-octo-project"), SelectQuestion("style", "Logo Style", style_names, "first_letter_underlined") ] + class OctoLogoApp(App): BINDINGS = [ ("ctrl+q", "quit", "Quit"), @@ -38,7 +42,6 @@ async def on_key(self, event: Key): elif event.key == "v" and self.finished: self.result.show() - def on_wizard_finished(self, message: Wizard.Finished): # Get the wizard answers and the wizard's id self.answers.update(message.answers) @@ -53,7 +56,7 @@ def on_wizard_finished(self, message: Wizard.Finished): style_wizard.questions = styles[self.answers['style']].module.questions style_wizard.title = "Style Settings" self.mount(style_wizard) - # When the style-specific wizard is finished, create the image and save it + # When the style-specific wizard is finished, create the image and save it elif finished_wizard_id == "style_wizard": style = styles[self.answers['style']].module self.result = style.get_image(self.answers) @@ -64,10 +67,12 @@ def on_wizard_finished(self, message: Wizard.Finished): # Final message def final_message(self): self.loading_wid.add_class("hidden") - self.mount(Label(f"Logo saved to [bold]{self.save_to}[/bold].\n[blue blink]-> Press v to view the result[/blue blink]\n[red]Press enter to quit[/red]")) + self.mount(Label( + f"Logo saved to [bold]{self.save_to}[/bold].\n" + f"[blue blink]-> Press v to view the result[/blue blink]\n" + f"[red]Press enter to quit[/red]")) self.result.save(self.save_to) self.finished = True - def compose(self): self.app.title = f"Octo Logo v{VERSION}" @@ -84,10 +89,11 @@ def compose(self): def disable_ansi(ctx: ExtraContext, param: Parameter, val: bool): ctx.color = not val - + # We must return the value for the main function no_ansi parameter not to be None return val + @extra_command(params=[]) @option("-t", "--no-tui", is_flag=True, help="Dont use the Textual Terminal User Interface") def main(no_tui: bool): @@ -108,10 +114,10 @@ def main(no_tui: bool): style = styles[answers['style']].module result = style.get_image(answers) save_to = f'output/{answers["name"]}_{int(time())}.png' - + result.save(save_to) logger.success(f"Image saved to : {save_to}") - + if __name__ == "__main__": main() diff --git a/src/styles/all_underlined.py b/src/styles/all_underlined.py index dd85425..bdb55d7 100644 --- a/src/styles/all_underlined.py +++ b/src/styles/all_underlined.py @@ -4,5 +4,6 @@ active = True questions = underline_core.questions + def get_image(answers): - return underline_core.get_image(answers, "all") \ No newline at end of file + return underline_core.get_image(answers, "all") diff --git a/src/styles/first_letter_underlined.py b/src/styles/first_letter_underlined.py index 2263850..a9591cc 100644 --- a/src/styles/first_letter_underlined.py +++ b/src/styles/first_letter_underlined.py @@ -4,5 +4,6 @@ active = True questions = underline_core.questions + def get_image(answers): - return underline_core.get_image(answers, "first_letter") \ No newline at end of file + return underline_core.get_image(answers, "first_letter") diff --git a/src/utils.py b/src/utils.py index 2802943..3171fd9 100644 --- a/src/utils.py +++ b/src/utils.py @@ -7,7 +7,9 @@ from sys import stdout logger.remove() -logger.add(stdout, format="[ {time:HH:mm:ss} ]" +logger.add( + stdout, + format="[ {time:HH:mm:ss} ]" " - {level} -> " "{message}") @@ -32,6 +34,7 @@ def remove_ext(filename): """ return filename.split(".")[0] + class Style(): display_name: str module: Any @@ -40,18 +43,20 @@ def __init__(self, display_name: str, module: Any) -> None: self.display_name = display_name self.module = module - BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) FONTS_DIR = os.path.join(BASE_DIR, "fonts") COLORS_DIR = os.path.join(BASE_DIR, "colors") + # Get all the fonts in the fonts directory def get_font_list() -> list[str]: return os.listdir(FONTS_DIR) + font_list = get_font_list() + # Get all the color schemes in the colors directory # keep only files with the .toml extension # and remove the extension @@ -68,8 +73,11 @@ def get_color_schemes() -> dict[str, dict[str, str]]: return colors + color_schemes = get_color_schemes() -color_scheme_names: dict[str, str] = [(color_schemes[color_scheme]['name'], color_scheme) for color_scheme in color_schemes] +color_scheme_names: dict[str, str] = [ + (color_schemes[color_scheme]['name'], color_scheme) for color_scheme in color_schemes] + def get_styles() -> dict[str, Style]: """ @@ -96,4 +104,4 @@ def get_styles() -> dict[str, Style]: styles = get_styles() -style_names: dict[str, str] = [(styles[style].display_name, style) for style in styles] \ No newline at end of file +style_names: dict[str, str] = [(styles[style].display_name, style) for style in styles] diff --git a/src/wizard.py b/src/wizard.py index ec7c1ca..d39b91d 100644 --- a/src/wizard.py +++ b/src/wizard.py @@ -1,5 +1,4 @@ from utils import logger -from textual import log from typing import Any from textual.validation import Validator from textual.widgets import Input, Select, Static, Button, Label @@ -8,6 +7,8 @@ from textual.message import Message from textual import on import inquirer as inq +# from textual import log + class QuestionBase(): name: str @@ -18,6 +19,7 @@ def __init__(self, name: str, label: str) -> None: self.type = type self.label = label + class TextQuestion(QuestionBase): validators: list[Validator] | None placeholder: str @@ -27,8 +29,8 @@ class TextQuestion(QuestionBase): def __init__( self, name: str, - label: str, - validators: list[Validator]| None = None, + label: str, + validators: list[Validator] | None = None, placeholder: str = "", default_value: str = "" ) -> None: @@ -43,14 +45,15 @@ def as_widget(self): _input.value = self.default_value return _input + class SelectQuestion(QuestionBase): options: list type: str = "select" default_value: Any | None = None def __init__( - self, - name: str, + self, + name: str, label: str, options: list, default_value: str | None = None @@ -60,16 +63,24 @@ def __init__( self.default_value = default_value def as_widget(self): - _select = Select(classes="full-width", id=self.name, options=self.options, allow_blank=False, value=self.default_value) + _select = Select( + classes="full-width", + id=self.name, + options=self.options, + allow_blank=False, + value=self.default_value) + _select.border_title = self.label return _select + class BackNextButtons(Static): def compose(self): yield Button("Back", variant="warning", id="back") yield Button("Next", variant="success", id="next") + class Wizard(Static): question_index = reactive(-1) answers: dict = dict() @@ -82,6 +93,7 @@ class Wizard(Static): class Finished(Message): answers = dict wizard_id: str + def __init__(self, answers, wizard_id): self.answers = answers self.wizard_id = wizard_id @@ -95,7 +107,7 @@ def on_back(self): @on(Button.Pressed, "#next") async def on_next(self): - # If the selected question is an input then fire the submit event so that + # If the selected question is an input then fire the submit event so that # validation is made and the next question is shown. # Else, just go to the next question since a select cannot be invalid if isinstance(self.selected_question, Input): @@ -112,43 +124,43 @@ def handle_validation_result(self, validation_result: ValidationResult): self.input_message.add_class("hidden") else: - # If the validation comports an error then disable the next button, + # If the validation comports an error then disable the next button, # Show and set the content of the error message and set the input's color to red self.query_one("#next").disabled = True self.input_message.remove_class("hidden") self.input_message.renderable = validation_result.failure_descriptions[0] self.selected_question.add_class("invalid") - def on_input_changed(self, message: Input.Changed): # When an input is changed, save its new value into the self.answers dict self.answers[message.input.id] = message.value # Show error messages if any self.handle_validation_result(message.validation_result) - + def on_input_submitted(self, message: Input.Submitted): - # Handle the validation result to show + # Handle the validation result to show # a message if there are any errors self.handle_validation_result(message.validation_result) # When the input is submitted, if it is valid then go to the next question if (message.validation_result.is_valid): self.question_index += 1 - - + def on_select_changed(self, message: Select.Changed): # When a select is changed update the value in the self.answers dict self.answers[message.select.id] = message.value def compose(self): # Render directly every input - # They are all hidden by default + # They are all hidden by default for i, question in enumerate(self.questions): wid = question.as_widget() - # For every select, the value in the answers dict will not be updated if the user just keeps the default value - # and click next without changing the value, the on_select_changed function will not be called and the + # For every select, the value in the answers dict + # will not be updated if the user just keeps the default value + # and click next without changing the value, + # the on_select_changed function will not be called and the # answers dict will not contain the key corresponding to the select which can result in a KeyError if isinstance(wid, Select): self.answers[wid.id] = question.default_value @@ -165,19 +177,18 @@ def compose(self): self.input_message.styles.color = "tomato" self.input_message.styles.max_width = "100%" yield self.input_message - + # ---------------------------- yield BackNextButtons() - def on_mount(self): # Trigger the watch_question_index function to make the first input appear self.question_index = 0 - + def watch_question_index(self): - + # Remove the selected class from the previous shown input if any - if not self.selected_question is None: + if self.selected_question is not None: self.selected_question.add_class("hidden") # If the question index has been incremented but it is now out of bound then @@ -185,11 +196,10 @@ def watch_question_index(self): if self.question_index == len(self.questions): self.post_message(self.Finished(self.answers, self.id)) return - + # Put the question index in the border title self.border_title = f"{self.title} [{self.question_index + 1}/{len(self.questions)}]" - # Show the input corresponding to the new value of self.question_index self.selected_question = self.query_one(f"#{self.questions[self.question_index].name}") self.selected_question.remove_class("hidden") @@ -199,21 +209,27 @@ def watch_question_index(self): # the "Back" button since there arent any questions before self.query_one("#back").disabled = self.question_index == 0 + def validate_text_question(question: SelectQuestion | TextQuestion, value: str) -> bool: for validator in question.validators: validation = validator.validate(value) if not validation.is_valid: logger.error(validation.failure_descriptions[0]) return False - + return True + def inq_ask(questions: list[SelectQuestion | TextQuestion]) -> dict[str, Any]: answers = dict() for question in questions: if isinstance(question, SelectQuestion): - answers[question.name] = inq.list_input(question.label, choices=question.options, default=question.default_value) + answers[question.name] = inq.list_input( + question.label, + choices=question.options, + default=question.default_value) + elif isinstance(question, TextQuestion): while True: temp = inq.text(question.label, default=question.default_value) @@ -223,4 +239,4 @@ def inq_ask(questions: list[SelectQuestion | TextQuestion]) -> dict[str, Any]: answers[question.name] = temp break - return answers \ No newline at end of file + return answers