Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

terminal ui with textual #5

Merged
merged 1 commit into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion colors/adi1090x.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
name = "Adi1090x"
background = "#131b20"
text = "#283039"
accent = "#a8bf3e"
accent = "#a8bf3e"
1 change: 1 addition & 0 deletions colors/cherry.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
name = "Cherry"
background = "#ffdcdc"
text = "#fda8ac"
accent = "#ef5e81"
1 change: 1 addition & 0 deletions colors/midnight_abyss.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
name = "Midnight Abyss"
background = "#000000"
text = "#272c38bd"
accent = "#30b962"
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pillow==9.3.0
inquirer==3.1.3
toml==0.10.2
importlib==1.0.4
textual==0.38.1
35 changes: 35 additions & 0 deletions src/app.tcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* Wtf ????? Terminal CSS ??? >:0 */

Wizard {
padding: 1 4;
width: auto;
margin: 0 2;
border: solid $primary-background-lighten-3;
}

BackNextButtons {
margin-top: 1;
layout: horizontal;
width: auto;
}

Screen {
align: center middle;
}

.full-width {
width: 100%;
}

Input, Select {
border: round skyblue;
background: transparent;
}

.invalid {
border: round tomato;
}

.hidden {
display: none;
}
117 changes: 73 additions & 44 deletions src/main.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,88 @@
import inquirer as inq
from importlib import import_module
import os
from time import time
from utils import *

# Look for DEBUG in the environment variables
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
from textual.app import App
from textual.widgets import Header, Footer, Label, LoadingIndicator
from textual.validation import Length
from textual import log
from textual.events import Key

def main():
VERSION = "1.0.0"

from wizard import *

# Load the styles in the styles directory
styles = dict()
for style in os.listdir(os.path.join(BASE_DIR, "src", "styles")):
# Only keep .py files
if not style.endswith(".py"):
continue

# Try to import the script
# If it fails, ignore it
if DEBUG:
module = import_module(f"styles.{remove_ext(style)}")
else:
try:
module = import_module(f"styles.{remove_ext(style)}")
except:
print(f"Error while importing {style}, ignoring...")
continue

# Only keep files with the active attribute set to True
# This allows to ignore some scripts that may be in the styles directory
if not module.active:
continue

styles[module.name] = module

questions = [
inq.Text("name", message="Project's name"),
inq.List("style", message="Select a style", choices=list(styles.keys()))
class OctoLogoApp(App):
BINDINGS = [
("ctrl+q", "quit", "Quit"),
("ctrl+t", "toggle_dark", "Toggle Dark Mode")
]
answers = dict()

CSS_PATH = os.path.join(BASE_DIR, "src", "app.tcss")
TITLE = "Octo Logo Wizard"
finished: bool = False
save_to: str | None = None
result: Image.Image | None = None
loading_wid: LoadingIndicator = LoadingIndicator(classes="hidden")

async def on_key(self, event: Key):
if event.key == "enter" and self.finished:
await self.action_quit()
elif event.key == "v" and self.finished:
self.result.show()


answers = inq.prompt(questions)
def on_wizard_finished(self, message: Wizard.Finished):
# Get the wizard answers and the wizard's id
self.answers.update(message.answers)
finished_wizard_id = message.wizard_id

# Force the user to enter a name
if not len(answers["name"]) > 0:
print("Error : You must enter a name")
quit()
# remove the wizard
self.query_one(f"#{finished_wizard_id}").remove()

selected_style = styles[answers["style"]]
# When the basic info wizard is finished, mount the style-specific wizard
if finished_wizard_id == "basic_info_wizard":
style_wizard = Wizard(id="style_wizard")
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
elif finished_wizard_id == "style_wizard":
style = styles[self.answers['style']].module
self.result = style.get_image(self.answers)
self.save_to = f'output/{self.answers["name"]}_{int(time())}.png'
self.loading_wid.remove_class("hidden")
self.set_timer(2, self.final_message)

image = selected_style.get_image(answers["name"])
# 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.result.save(self.save_to)
self.finished = True


def compose(self):
self.app.title = f"Octo Logo v{VERSION}"

yield Header(show_clock=True)
yield Footer()

basic_info_wizard = Wizard(id="basic_info_wizard")
basic_info_wizard.questions = [
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")
]
basic_info_wizard.title = "Basic Information"
yield basic_info_wizard
yield self.loading_wid

def main():

# Save result or show if debug is enabled
save_to = f'output/{answers["name"]}_{int(time())}.png'
image.show() if DEBUG else image.save(save_to)
print(f"Logo saved to {save_to}")
app = OctoLogoApp()
app.run()
quit(0)

if __name__ == "__main__":
main()
7 changes: 4 additions & 3 deletions src/styles/all_underlined.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from . import underline_core

name = "All text underlined"
display_name = "All text underlined"
active = True
questions = underline_core.questions

def get_image(name):
return underline_core.get_image(name, "all")
def get_image(answers):
return underline_core.get_image(answers, "all")
7 changes: 4 additions & 3 deletions src/styles/first_letter_underlined.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from . import underline_core

name = "First letter underlined"
display_name = "First letter underlined"
active = True
questions = underline_core.questions

def get_image(name):
return underline_core.get_image(name, "first_letter")
def get_image(answers):
return underline_core.get_image(answers, "first_letter")
81 changes: 30 additions & 51 deletions src/styles/underline_core.py
Original file line number Diff line number Diff line change
@@ -1,105 +1,85 @@
import toml
from wizard import *
from textual.validation import *

from PIL import Image, ImageDraw, ImageFont, ImageColor
import inquirer as inq

import sys
sys.path.append("..")

from utils import *

questions = [
SelectQuestion("font", "Select a font", [(font, font) for font in font_list], "Iosevka-Nerd-Font-Complete.ttf"),
SelectQuestion("color", "Select a color scheme", color_scheme_names, "adi1090x"),
TextQuestion("padding_x", "Padding x (px)", [Number()], "200", "200"),
TextQuestion("padding_y", "Padding y (px)", [Number()], "20", "20"),
TextQuestion("gap", "Gap between text and bar (px)", [Number()], "20", "20"),
TextQuestion("bar_size", "Bar weight (px)", [Number()], "20", "20"),
TextQuestion("additionnal_bar_width", "Additionnal bar width (px)", [Number()], "20", "20"),
]

active = False

def get_image(name, type):
def get_image(answers, type):
if not type in ['all', 'first_letter']:
raise ValueError("Invalid type")

questions = [
inq.List("font", message="Select a font", choices=font_list),
inq.List("color", message="Select a color scheme", choices=color_list, default="adi1090x"),
inq.Text("padding_x", message="Padding x (px)", default=200),
inq.Text("padding_y", message="Padding y (px)", default=20),
inq.Text(
"gap", message="Gap between the first letter and the bar (px)", default=20
),
inq.Text("bar_size", message="Bar size (px)", default=20),
inq.Text(
"additionnal_bar_width", message="Addionnal bar width (px)", default=5
),
]

answers = inq.prompt(questions)

# Convert the answers to integers
try:
padding_x = int(answers["padding_x"])
padding_y = int(answers["padding_y"])
gap = int(answers["gap"])
bar_size = int(answers["bar_size"])
additionnal_bar_width = int(answers["additionnal_bar_width"])
except ValueError:
print("px values must be integer")
exit(1)

# Load the selected font
font_size = 500
font = ImageFont.truetype(os.path.join(FONTS_DIR, answers["font"]), font_size)

# Load the selected color scheme
color_scheme_file = os.path.join(COLORS_DIR, f'{answers["color"]}.toml')
color_scheme = toml.load(color_scheme_file)

background = ImageColor.getrgb(color_scheme["background"])
text = ImageColor.getrgb(color_scheme["text"])
accent = ImageColor.getrgb(color_scheme["accent"])
background = ImageColor.getrgb(color_schemes[answers['color']]["background"])
text = ImageColor.getrgb(color_schemes[answers['color']]["text"])
accent = ImageColor.getrgb(color_schemes[answers['color']]["accent"])

# Get the width and height of the texts
text_width, text_height = get_text_size(name, font)
text_width, text_height = get_text_size(answers['name'], font)
font_height = get_font_height(font)

# Get the correct image width and height
image_width = 2 * padding_x + text_width
image_height = 2 * padding_y + font_height
image_width = 2 * int(answers['padding_x']) + text_width
image_height = 2 * int(answers['padding_y']) + font_height

# Create the image
image = Image.new("RGB", (image_width, image_height), background)
draw = ImageDraw.Draw(image)

# Get the anchor position and type
anchor_type = "lm"
anchor_x = padding_x
anchor_y = image_height / 2 - (gap + bar_size) / 2
anchor_x = int(answers['padding_x'])
anchor_y = image_height / 2 - (int(answers['gap']) + int(answers['bar_size'])) / 2

anchor_pos = (anchor_x, anchor_y)

# Get the bbox of the first letter

first_letter_bbox = draw.textbbox(
anchor_pos, name[0], font=font, anchor=anchor_type
anchor_pos, answers['name'][0], font=font, anchor=anchor_type
)

# Get the underline position
underline_start_x = first_letter_bbox[0] - additionnal_bar_width
underline_start_y = first_letter_bbox[3] + gap
underline_start_x = first_letter_bbox[0] - int(answers['additionnal_bar_width'])
underline_start_y = first_letter_bbox[3] + int(answers['gap'])

# The end of the underline depends on the type
# If the type is 'all', the underline will go from the start of the first letter to the end of the text
# If the type is 'first_letter', the underline will go from the start of the first letter to the end of the first letter
underline_end_x = additionnal_bar_width + (first_letter_bbox[2] if type == 'first_letter' else padding_x + text_width)
underline_end_y = underline_start_y + bar_size
underline_end_x = int(answers['additionnal_bar_width']) + (first_letter_bbox[2] if type == 'first_letter' else int(answers['padding_x']) + text_width)
underline_end_y = underline_start_y + int(answers['bar_size'])

underline_start = (underline_start_x, underline_start_y)
underline_end = (underline_end_x, underline_end_y)

underline_pos = [underline_start, underline_end]

# Underline the first letter
draw.rectangle(underline_pos, fill=accent, width=bar_size)
draw.rectangle(underline_pos, fill=accent, width=answers['bar_size'])


# Draw the text
draw.text(
anchor_pos,
name,
answers['name'],
font=font,
fill=text,
anchor=anchor_type,
Expand All @@ -108,11 +88,10 @@ def get_image(name, type):
# Redraw the first letter
draw.text(
anchor_pos,
name[0],
answers['name'][0],
font=font,
fill=accent,
anchor=anchor_type,
)


return image
Loading