Module betterletter.main
This is the actual script.
Expand source code
"""This is the actual script."""
import sys
import os
import uuid
import re
from time import sleep
from datetime import datetime, timedelta
import typer
import yaml
import frontmatter
import markdown
from pathlib import Path
from fpdf import FPDF, HTMLMixin
from prompt_toolkit import PromptSession
# from prompt_toolkit import prompt
from prompt_toolkit.completion import WordCompleter
from rich import print
from rich import box
from rich.panel import Panel
from rich.console import Console
from rich.markdown import Markdown
from rich.prompt import Confirm
from rich.table import Table
from simple_term_menu import TerminalMenu
from rich.status import Status
from rich.traceback import install
install() # rich tracebacks
# install(show_locals=True) # rich tracebacks
COLOR = "bright_black"
APP_NAME = "betterletter"
VON = Path("betterletter_from.md")
TO = Path("betterletter_to.md")
BODY = Path("betterletter_body.md")
PDF_FILE = Path("betterletter_POC.pdf")
EDITORS = ["vi", "gedit", "codium", "nano", "kate", "vim", "nvim", "atom"]
HOTKEYS = [
"[1] ",
"[2] ",
"[3] ",
"[4] ",
"[5] ",
"[6] ",
"[7] ",
"[8] ",
"[9] ",
"[0] ",
"[a] ",
"[s] ",
"[d] ",
"[f] ",
"[g] ",
"[y] ",
"[x] ",
"[c] ",
"[v] ",
"[w] ",
"[e] ",
"[r] ",
"[t] ",
"[z] ",
"[u] ",
"[i] ",
"[o] ",
"[p] ",
"[b] ",
"[n] ",
"[m] ",
]
app_dir = typer.get_app_dir(APP_NAME)
config_path: Path = Path(app_dir) / "addressees.yml"
if not config_path.is_file():
typer.echo("Config file doesn't exist.")
# sleep(5)
addressees = {}
if not os.path.isdir(app_dir):
os.mkdir(app_dir)
with open(config_path, "w") as f:
yaml.dump(addressees, f)
app = typer.Typer() # initiate typer app
class Editor:
"""Configuration Class."""
# Generating Completions
completer_list = []
if os.path.isfile(VON):
with open(VON) as f:
von_frontmatter = frontmatter.load(f)
temp_dict = von_frontmatter.to_dict()
del temp_dict["content"]
del temp_dict["document"]
for key in temp_dict:
words = temp_dict[key].split()
if words != []:
completer_list.extend(words)
if os.path.isfile(TO):
with open(TO) as f:
von_frontmatter = frontmatter.load(f)
temp_dict = von_frontmatter.to_dict()
del temp_dict["content"]
del temp_dict["document"]
del temp_dict["uuid"]
for key in temp_dict:
words = temp_dict[key].split()
if words != []:
completer_list.extend(words)
if os.path.isfile(BODY):
with open(BODY) as f:
von_frontmatter = frontmatter.load(f)
temp_dict = von_frontmatter.to_dict()
del temp_dict["document"]
for line in temp_dict:
words = temp_dict[line].split()
if words != []:
completer_list.extend(words)
# removing duplicate words
completer_list = list(dict.fromkeys(completer_list))
test_completer = WordCompleter(sorted(completer_list))
favourite = "vi"
cons = Console()
sess = PromptSession(
message="❯ ", wrap_lines=True, vi_mode=True, completer=test_completer
)
last = {}
if os.path.isfile("betterletter_make.yml"):
with open("betterletter_make.yml") as f:
last = yaml.load(f, Loader=yaml.FullLoader)
adressees = {}
with open(config_path) as f:
addressees = yaml.load(f, Loader=yaml.FullLoader)
ed = Editor()
@app.callback(invoke_without_command=True)
def callback(ctx: typer.Context):
"""This is the callback function invoked when the app is run without any commands.
By default this just shows the --help. Here however the otion to run the app without any commands has been enabled.
"""
ed.cons.rule(title="better letter", align="right", style=COLOR)
sys.stdout.write("\x1b]2;" + "Betterletter" + "\x07")
if ctx.invoked_subcommand is None: # invoked without commands.
ed.cons.print(ctx.get_help())
@app.command()
def sender():
"""Work with the sender information"""
current_dir = os.getcwd()
# try:
# dirs_from_conf = conf["dirs"]
# if current_dir not in dirs_from_conf:
# cons.print(f'{current_dir} is not in congfiguration. Would you like to add it?')
# except KeyError:
# cons.print(f'Setting up {current_dir} as a working directory.')
# dirs_from_conf = [current_dir]
# with open(config_path, 'w') as f: # safe
# yaml.dump(conf, f)
if os.path.isfile(VON):
with open(VON) as f:
von_frontmatter = frontmatter.load(f)
else:
von_frontmatter = frontmatter.Post(
"""# Sender Information
Please confirm the following data
- Title - Herr, Frau, Professor Dr who or whatever or just leave it blank.
- First name
- Last name
- Address 1 - Usually street and number.
- Address 2 - Additional information. Often just blank.
- Address 3 - Post code, wich is the [5 digit number if you are in
Germany](https://en.wikipedia.org/wiki/List_of_postal_codes_in_Germany) and city.
Sometimes prepended with a country code, i.e. `DE` for Germany.
- Country - in English for international delivery sometimes the country code is found here: `DE Germany`
"""
)
von_frontmatter["document"] = "Sender Information"
with open(VON, "w") as f:
f.writelines(frontmatter.dumps(von_frontmatter))
ed.cons.print(Markdown(von_frontmatter.content))
from_fields = [
"title",
"first_name",
"last_name",
"address_1",
"address_2",
"post_code",
"city",
"country",
"phone",
"email",
"web_site",
]
ed.cons.print()
for von_item in from_fields:
try:
ed.cons.rule(
title=f'[green]{von_frontmatter[von_item]} [{COLOR}]❮ {von_item.replace("_", " ").capitalize()}',
style=COLOR,
align="left",
characters="❮",
)
except KeyError as identfier:
von_frontmatter[von_item] = ed.sess.prompt(
default=von_item.replace("_", " ").capitalize()
)
continue_on_one = 1
while continue_on_one == 1:
continue_on_one = menu(["[1] OK", "[2] Edit Field"])
if continue_on_one == 1:
choices = []
items = []
i = 0
for item in von_frontmatter.keys():
if item in from_fields:
items.append(item)
choices.append(
f'{HOTKEYS[i]}{item.replace("_", " ").capitalize()}: {von_frontmatter[item]}'
)
i += 1
try:
choice = menu(prompt="Field?", choices=choices)
von_frontmatter[items[choice]] = ed.sess.prompt(
default=von_frontmatter[items[choice]]
)
ed.cons.rule(
title=f'[green]{von_frontmatter[items[choice]]} [{COLOR}]❮ {items[choice].replace("_", " ").capitalize()}',
style=COLOR,
align="left",
characters="❮",
)
except TypeError:
pass
else:
ed.cons.print()
ed.cons.rule(
title=f"[{COLOR}]Sender Information checked and recorded in {str(VON)} ",
style=COLOR,
align="right",
)
with open(VON, "w") as f:
f.writelines(frontmatter.dumps(von_frontmatter))
@app.command()
def address(
address_book: bool = typer.Option(
False,
"--book",
"-b",
help="Address Book.",
),
):
"""Addressee information."""
current_dir = os.getcwd()
myuuid = str(uuid.uuid4())
# print(myuuid)
if address_book:
for a_uuid in ed.addressees:
add = ed.addressees[a_uuid]
panel = Panel(
renderable=f'[bold]{add["last_name"].upper()} {add["first_name"]}[/] {add["title"]}'.strip(),
title=f'{add["address_1"]} [bold]{add["city"].strip()}',
title_align="left",
subtitle=f'{add["address_2"]} {add["city"]}'.strip(),
subtitle_align="right",
expand=False,
style=COLOR,
border_style=COLOR,
# width: Optional[int]=None,
# height: Optional[int]=None,
# padding: PaddingDimensions=(0, 1),
# highlight: bool=False
)
print(panel)
choices = []
i = 0
for ab_uuid in ed.addressees:
add = ed.addressees[ab_uuid]
if i < len(HOTKEYS):
choices.append(
f'{HOTKEYS[i]}{add["last_name"].upper()} {add["first_name"]}, {add["city"]}|{ab_uuid}'
)
i += 1
else:
choices.append(
f'{add["last_name"].upper()} {add["first_name"]}, {add["city"]}|{ab_uuid}'
)
try:
choice = menu(choices=choices, prompt="Select Addressee")
myuuid = choices[choice].split("|")[-1]
if os.path.isfile(TO):
os.remove(TO)
except TypeError:
pass
from_fields = [
"title",
"first_name",
"last_name",
"address_1",
"address_2",
"post_code",
"city",
"country",
]
if os.path.isfile(TO):
with open(TO) as f:
von_frontmatter = frontmatter.load(f)
try:
myuuid = von_frontmatter["uuid"]
except KeyError:
von_frontmatter["uuid"] = myuuid
else:
# chose adressee or new
von_frontmatter = frontmatter.Post(
"""# Addressee Information
Please confirm the following data
- Title - Herr, Frau, Professor Dr who or whatever or just leave it blank.
- First name
- Last name
- Address 1 - Usually street and number.
- Address 2 - Additional information. Often just blank.
- Post Code - Post code, wich is the [5 digit number if you are in
Germany](https://en.wikipedia.org/wiki/List_of_postal_codes_in_Germany).
- City - City name.
- Country - in English for international delivery sometimes the country code is found here: `DE Germany`
"""
)
von_frontmatter["document"] = "Addressee Information"
von_frontmatter["uuid"] = myuuid
if myuuid in ed.addressees: # if in address book
add = ed.addressees[myuuid]
for von_item in from_fields: # populate from address book
try:
von_frontmatter[von_item] = ed.addressees[myuuid][von_item]
except KeyError:
pass
with open(TO, "w") as f:
f.writelines(frontmatter.dumps(von_frontmatter))
ed.cons.print(Markdown(von_frontmatter.content))
ed.cons.print()
for von_item in from_fields:
try:
ed.cons.rule(
title=f'[green]{von_frontmatter[von_item]} [{COLOR}]❮ {von_item.replace("_", " ").capitalize()}',
style=COLOR,
align="left",
characters="❮",
)
except KeyError as identfier:
von_frontmatter[von_item] = ed.sess.prompt(
default=von_item.replace("_", " ").capitalize()
)
continue_on_one = 1
while continue_on_one == 1:
continue_on_one = menu(["[1] OK", "[2] Edit Field"])
if continue_on_one == 1:
choices = []
items = []
i = 0
for item in von_frontmatter.keys():
if item in from_fields:
items.append(item)
choices.append(
f'{HOTKEYS[i]}{item.replace("_", " ").capitalize()}: {von_frontmatter[item]}'
)
i += 1
try:
choice = menu(prompt="Change field?", choices=choices)
von_frontmatter[items[choice]] = ed.sess.prompt(
default=von_frontmatter[items[choice]]
)
ed.cons.rule(
title=f'[green]{von_frontmatter[items[choice]]} ❮ [{COLOR}]{items[choice].replace("_", " ").capitalize()}',
style=COLOR,
align="left",
characters="❮",
)
except TypeError:
pass
else:
ed.cons.print()
ed.cons.rule(
title=f"[{COLOR}]Addressee Information checked and recorded in {str(TO)} ",
style=COLOR,
align="right",
)
with open(TO, "w") as f:
f.writelines(frontmatter.dumps(von_frontmatter))
if ed.addressees:
myuuid = von_frontmatter["uuid"]
else:
ed.addressees = {}
temp_dict = von_frontmatter.to_dict()
del temp_dict["uuid"]
del temp_dict["content"]
del temp_dict["document"]
ed.addressees.update({myuuid: temp_dict})
with open(config_path, "w") as f:
yaml.dump(ed.addressees, f)
@app.command()
def write():
"""Write the letter."""
ed.cons.print(f"[{COLOR}]Checking Sender Information.")
if not os.path.isfile(VON):
sender()
ed.cons.print(f"[{COLOR}]Checking Addressee Information.")
if not os.path.isfile(TO):
address()
ed.cons.print(f"[bold]What is your favourite text editor right now?")
ed.favourite = ed.sess.prompt(
default=ed.favourite, completer=WordCompleter(EDITORS)
)
ed.sess = PromptSession(
message="❯ ", wrap_lines=True, vi_mode=True, completer=ed.test_completer
)
current_dir = os.getcwd()
if os.path.isfile(BODY):
with open(BODY) as f:
von_frontmatter = frontmatter.load(f)
else:
von_frontmatter = frontmatter.Post(
"""Letter about line will be printed in bold type.
This is the body of the letter.
> Replace with your own text.
"""
)
von_frontmatter["document"] = "Body Text"
von_frontmatter["about"] = "About line of the letter."
with open(BODY, "w") as f:
f.writelines(frontmatter.dumps(von_frontmatter))
# cons.print(Markdown(von_frontmatter.content))
from_fields = ["about"]
ed.cons.print()
for von_item in from_fields:
try:
ed.cons.rule(
title=f"[{COLOR}]❯ [bold green]{von_frontmatter[von_item]}",
style=COLOR,
align="left",
characters="❮",
)
except KeyError as identfier:
von_frontmatter[von_item] = ed.sess.prompt(
default=von_item.replace("_", " ").capitalize()
)
continue_on_one = 1
ed.cons.print(Markdown(von_frontmatter.content))
while continue_on_one == 1:
continue_on_one = menu(["[1] OK", "[2] Edit Field", "[3] Edit Letter Body"])
if continue_on_one == 1:
choices = []
items = []
for item in von_frontmatter.keys():
items.append(item)
choices.append(
f'{item.replace("_", " ").capitalize()}: {von_frontmatter[item]}'
)
choice = menu(prompt="Change field?", choices=choices)
von_frontmatter[items[choice]] = ed.sess.prompt(
default=von_frontmatter[items[choice]]
)
ed.cons.rule(
title=f"[{COLOR}]❯ [green]{von_frontmatter[items[choice]]}",
style=COLOR,
align="left",
characters="❮",
)
elif continue_on_one == 2:
if ed.favourite in ["vi", "vim", "nvim"]:
choices = [
"Deutsch|de_de",
"English (US)|en_us",
"English (GB)|en_gb",
"Deutsch (AU)|de_au",
"None",
]
choice = menu(prompt="Spellang for checking?", choices=choices)
if choice == 4:
spelllang = ""
else:
spelllang = (
f'-c ":set spell spelllang={choices[choice].split("|")[-1]}" '
)
else:
spelllang = ""
mtime = os.path.getmtime(BODY)
os.system(f"{ed.favourite} {spelllang}{BODY}")
if mtime == os.path.getmtime(BODY):
ed.cons.print("\n[bold]No changes.")
else:
with open(BODY) as f:
von_frontmatter = frontmatter.load(f)
ed.cons.print(Markdown(von_frontmatter.content))
continue_on_one = 1
else:
ed.cons.print()
with open(BODY, "w") as f:
f.writelines(frontmatter.dumps(von_frontmatter))
ed.cons.rule(
title=f"[{COLOR}]Letter body text checked and recorded in {str(BODY)} ",
style=COLOR,
align="right",
)
if ed.last:
ed.cons.print(
Panel(
renderable=str(ed.last),
style=COLOR,
title="configuration",
title_align="left",
subtitle="betterletter make --help",
subtitle_align="right",
border_style=COLOR,
)
)
@app.command()
def make(
image_file: str = typer.Option(
None,
"--image",
"-i",
help="A logo raster graphic file placed in the top left corner. Width: 20mm",
),
body_font: str = typer.Option(
None,
"--bodyfont",
help="Letter body font.",
),
bold_font: str = typer.Option(
None,
"--boldfont",
help="Bold font used in the about line.",
),
logo_line1: str = typer.Option(
None,
"--logo1",
"-1",
help="Logo Line 1",
),
logo_line1_font: str = typer.Option(
None,
"--logo1font",
help="Logo Line 1 Font",
),
logo_line2: str = typer.Option(
None,
"--logo2",
"-2",
help="Logo Line 2",
),
logo_line2_font: str = typer.Option(
None,
"--logo2font",
help="Logo Line 2 Font",
),
logo_link: str = typer.Option(
"https://betterletter.harmlos.info",
"--link",
"-l",
help="Logo Link.",
),
):
"""Generate letter as PDF Document.
Provide your own artwork:
Place imgage file in working directory, use the `--image` option.
Provide your own fonts:
Place TTF Font files in working directory, use the following options to reference."""
current_dir = os.getcwd()
write()
ed.cons.rule(
title=f"[{COLOR}]betterletter make",
style=COLOR,
align="left",
)
if ed.last != {}:
if not image_file:
try:
image_file = ed.last["image_file"]
except KeyError:
pass
if not body_font:
try:
body_font = ed.last["body_font"]
except KeyError:
pass
if not bold_font:
try:
bold_font = ed.last["bold_font"]
except KeyError:
pass
if not logo_line1:
try:
logo_line1 = ed.last["logo_line1"]
except KeyError:
pass
if not logo_line1_font:
try:
logo_line1_font = ed.last["logo_line1_font"]
except KeyError:
pass
if not logo_line2:
try:
logo_line2 = ed.last["logo_line2"]
except KeyError:
pass
if not logo_line2_font:
try:
logo_line2_font = ed.last["logo_line2_font"]
except KeyError:
pass
if not logo_link:
try:
logo_link = ed.last["logo_link"]
except KeyError:
pass
class PDF(FPDF, HTMLMixin):
def header(self):
pdf.set_left_margin(0)
pdf.set_right_margin(0)
# Rendering logo:
if image_file:
self.image(image_file, 20, 8, 20)
# Setting font: helvetica bold 15
if body_font:
body = "body"
pdf.add_font(body, fname=body_font, uni=True)
else:
body = "helvetica"
self.set_font(body, "", 15)
if not body_font and not logo_line1_font:
self.set_font("Helvetica", "B", 15)
if logo_line1_font:
self.add_font("logo1", fname=logo_line1_font, uni=True)
self.set_font("logo1", size=15)
self.ln(5)
# Moving cursor to the right:
self.cell(w=40)
# Printing title:
if logo_line1:
if image_file:
self.cell(txt=logo_line1)
else:
self.set_x(x=0)
self.cell(w=210, txt=logo_line1, align="C")
self.ln(6)
self.cell(w=40)
if logo_line2_font:
self.add_font("logo2", fname=logo_line2_font, uni=True)
self.set_font("logo2", size=6)
else:
self.set_font(body, "", size=6)
if logo_line2:
if image_file:
self.cell(txt=logo_line2, link=logo_link)
else:
self.set_x(x=0)
self.cell(w=210, txt=logo_line2, link=logo_link, align="C")
self.ln(12)
# Text Margins
self.set_left_margin(24)
self.set_right_margin(24)
if self.page_no() == 1:
with open(VON) as f:
von_frontmatter = frontmatter.load(f)
# cons.print(von_frontmatter["first_name"])
self.set_font(body, "", 10)
self.set_xy(x=120, y=51)
self.cell(w=1, txt=von_frontmatter["title"])
self.set_x(x=120)
self.cell(
w=1,
txt=f'{von_frontmatter["first_name"]} {von_frontmatter["last_name"]}',
)
self.ln()
self.set_x(x=120)
self.cell(w=1, txt=von_frontmatter["address_1"])
self.ln()
self.set_x(x=120)
self.cell(w=1, txt=von_frontmatter["address_2"])
self.ln()
self.set_x(x=120)
self.cell(
w=1,
txt=von_frontmatter["post_code"] + " " + von_frontmatter["city"],
)
self.ln()
self.set_x(x=120)
self.cell(w=1, txt=von_frontmatter["country"])
self.ln()
self.set_x(x=120)
self.cell(w=1, txt="Tel " + von_frontmatter["phone"])
self.ln()
self.set_x(x=120)
self.cell(
w=1,
txt=von_frontmatter["email"],
link="mailto:" + von_frontmatter["email"],
)
self.ln()
self.set_x(x=120)
self.cell(
w=1,
txt=von_frontmatter["web_site"],
link="https://" + von_frontmatter["web_site"],
)
self.ln()
with open(TO) as f:
an_frontmatter = frontmatter.load(f)
self.set_font(body, "", 12)
self.set_y(y=51)
self.cell(w=1, txt=an_frontmatter["title"])
self.ln()
self.cell(
w=1,
txt=f'{an_frontmatter["first_name"]} {an_frontmatter["last_name"]}',
)
self.ln()
# pdf.set_x(x=10)
self.cell(w=1, txt=an_frontmatter["address_1"])
self.ln()
# pdf.set_x(x=10)
self.cell(w=1, txt=an_frontmatter["address_2"])
self.ln()
# pdf.set_x(x=10)
self.cell(
w=1, txt=an_frontmatter["post_code"] + " " + an_frontmatter["city"]
)
self.ln()
self.ln()
self.cell(w=1, txt=an_frontmatter["country"])
self.ln()
self.set_font("helvetica", size=12)
text = von_frontmatter["city"]
text = text.strip() + ", " + datetime.now().now().strftime("%d.%m.%Y")
self.ln()
self.set_x(x=120)
self.cell(txt=text)
def footer(self):
# Position cursor at 1.5 cm from bottom:
self.set_y(-15)
# Setting font: helvetica italic 8
self.set_font("helvetica", "I", 8)
# Printing page number:
self.cell(
0,
10,
f"a betterletter - p {self.page_no()}/{{nb}}",
0,
0,
"C",
link="https://betterletter.harmlos.info",
)
# Add folding marks
self.line(x1=0, y1=87, x2=6, y2=87)
self.line(x1=0, y1=148.5, x2=9, y2=148.5)
self.line(x1=0, y1=192, x2=6, y2=192)
with ed.cons.status(str(PDF_FILE), spinner="pong"):
pdf = PDF()
# count pages - see footer()
pdf.alias_nb_pages()
# add first page - auto page break is enabled by default
pdf.add_page()
# set font
with open(BODY) as f:
body_frontmatter = frontmatter.load(f)
if body_font:
pdf.set_font("body", "", 12)
else:
pdf.set_font("helvetica", "B", 12)
if bold_font:
pdf.add_font("bold", fname=bold_font, uni=True)
pdf.set_font("bold", "", 12)
line_height = pdf.font_size * 1.2
pdf.set_y(y=91)
pdf.write(txt=body_frontmatter["about"])
if body_font:
pdf.set_font("body", "", 10)
else:
pdf.set_font("helvetica", "", 10)
pdf.ln()
html_body = markdown.markdown(
body_frontmatter.content, extensions=["toc", "extra"]
)
pdf.write_html(text=html_body)
with open(VON) as f:
von_frontmatter = frontmatter.load(f)
pdf.ln()
pdf.ln()
pdf.ln()
pdf.write(txt="______________________________")
pdf.ln(7.5)
if body_font:
pdf.set_font("body", "", 8)
else:
pdf.set_font("helvetica", "", 8)
pdf.write(
txt=f'{von_frontmatter["title"]} {von_frontmatter["first_name"]} {von_frontmatter["last_name"]}'
)
pdf.output(PDF_FILE)
ed.cons.print(f"[green]{str(PDF_FILE)} [{COLOR}]ready in {current_dir}")
if image_file:
ed.last.update({"image_file": image_file})
if body_font:
ed.last.update({"body_font": body_font})
if bold_font:
ed.last.update({"bold_font": bold_font})
if logo_line1:
ed.last.update({"logo_line1": logo_line1})
if logo_line1_font:
ed.last.update({"logo_line1_font": logo_line1_font})
if logo_line2:
ed.last.update({"logo_line2": logo_line2})
if logo_line2_font:
ed.last.update({"logo_line2_font": logo_line2_font})
if logo_link:
ed.last.update({"logo_link": logo_link})
if ed.last != {}:
with open("betterletter_make.yml", "w") as f:
yaml.dump(ed.last, f)
def menu(choices, prompt=""):
"""Ask for user interaction."""
main_menu_title = prompt
main_menu_items = choices
main_menu_cursor = "👉 "
main_menu_cursor_style = ("fg_red", "bold")
main_menu = TerminalMenu(
menu_entries=main_menu_items,
title=main_menu_title,
menu_cursor=main_menu_cursor,
menu_cursor_style=main_menu_cursor_style,
cycle_cursor=True,
clear_screen=False,
show_search_hint=False,
accept_keys=("enter", "alt-d", "ctrl-i", " "),
)
return main_menu.show()
if __name__ == "__main__":
app()
Functions
def address(address_book: bool = <typer.models.OptionInfo object>)
-
Addressee information.
Expand source code
@app.command() def address( address_book: bool = typer.Option( False, "--book", "-b", help="Address Book.", ), ): """Addressee information.""" current_dir = os.getcwd() myuuid = str(uuid.uuid4()) # print(myuuid) if address_book: for a_uuid in ed.addressees: add = ed.addressees[a_uuid] panel = Panel( renderable=f'[bold]{add["last_name"].upper()} {add["first_name"]}[/] {add["title"]}'.strip(), title=f'{add["address_1"]} [bold]{add["city"].strip()}', title_align="left", subtitle=f'{add["address_2"]} {add["city"]}'.strip(), subtitle_align="right", expand=False, style=COLOR, border_style=COLOR, # width: Optional[int]=None, # height: Optional[int]=None, # padding: PaddingDimensions=(0, 1), # highlight: bool=False ) print(panel) choices = [] i = 0 for ab_uuid in ed.addressees: add = ed.addressees[ab_uuid] if i < len(HOTKEYS): choices.append( f'{HOTKEYS[i]}{add["last_name"].upper()} {add["first_name"]}, {add["city"]}|{ab_uuid}' ) i += 1 else: choices.append( f'{add["last_name"].upper()} {add["first_name"]}, {add["city"]}|{ab_uuid}' ) try: choice = menu(choices=choices, prompt="Select Addressee") myuuid = choices[choice].split("|")[-1] if os.path.isfile(TO): os.remove(TO) except TypeError: pass from_fields = [ "title", "first_name", "last_name", "address_1", "address_2", "post_code", "city", "country", ] if os.path.isfile(TO): with open(TO) as f: von_frontmatter = frontmatter.load(f) try: myuuid = von_frontmatter["uuid"] except KeyError: von_frontmatter["uuid"] = myuuid else: # chose adressee or new von_frontmatter = frontmatter.Post( """# Addressee Information Please confirm the following data - Title - Herr, Frau, Professor Dr who or whatever or just leave it blank. - First name - Last name - Address 1 - Usually street and number. - Address 2 - Additional information. Often just blank. - Post Code - Post code, wich is the [5 digit number if you are in Germany](https://en.wikipedia.org/wiki/List_of_postal_codes_in_Germany). - City - City name. - Country - in English for international delivery sometimes the country code is found here: `DE Germany` """ ) von_frontmatter["document"] = "Addressee Information" von_frontmatter["uuid"] = myuuid if myuuid in ed.addressees: # if in address book add = ed.addressees[myuuid] for von_item in from_fields: # populate from address book try: von_frontmatter[von_item] = ed.addressees[myuuid][von_item] except KeyError: pass with open(TO, "w") as f: f.writelines(frontmatter.dumps(von_frontmatter)) ed.cons.print(Markdown(von_frontmatter.content)) ed.cons.print() for von_item in from_fields: try: ed.cons.rule( title=f'[green]{von_frontmatter[von_item]} [{COLOR}]❮ {von_item.replace("_", " ").capitalize()}', style=COLOR, align="left", characters="❮", ) except KeyError as identfier: von_frontmatter[von_item] = ed.sess.prompt( default=von_item.replace("_", " ").capitalize() ) continue_on_one = 1 while continue_on_one == 1: continue_on_one = menu(["[1] OK", "[2] Edit Field"]) if continue_on_one == 1: choices = [] items = [] i = 0 for item in von_frontmatter.keys(): if item in from_fields: items.append(item) choices.append( f'{HOTKEYS[i]}{item.replace("_", " ").capitalize()}: {von_frontmatter[item]}' ) i += 1 try: choice = menu(prompt="Change field?", choices=choices) von_frontmatter[items[choice]] = ed.sess.prompt( default=von_frontmatter[items[choice]] ) ed.cons.rule( title=f'[green]{von_frontmatter[items[choice]]} ❮ [{COLOR}]{items[choice].replace("_", " ").capitalize()}', style=COLOR, align="left", characters="❮", ) except TypeError: pass else: ed.cons.print() ed.cons.rule( title=f"[{COLOR}]Addressee Information checked and recorded in {str(TO)} ", style=COLOR, align="right", ) with open(TO, "w") as f: f.writelines(frontmatter.dumps(von_frontmatter)) if ed.addressees: myuuid = von_frontmatter["uuid"] else: ed.addressees = {} temp_dict = von_frontmatter.to_dict() del temp_dict["uuid"] del temp_dict["content"] del temp_dict["document"] ed.addressees.update({myuuid: temp_dict}) with open(config_path, "w") as f: yaml.dump(ed.addressees, f)
def callback(ctx: typer.models.Context)
-
This is the callback function invoked when the app is run without any commands.
By default this just shows the –help. Here however the otion to run the app without any commands has been enabled.
Expand source code
@app.callback(invoke_without_command=True) def callback(ctx: typer.Context): """This is the callback function invoked when the app is run without any commands. By default this just shows the --help. Here however the otion to run the app without any commands has been enabled. """ ed.cons.rule(title="better letter", align="right", style=COLOR) sys.stdout.write("\x1b]2;" + "Betterletter" + "\x07") if ctx.invoked_subcommand is None: # invoked without commands. ed.cons.print(ctx.get_help())
def make(image_file: str = <typer.models.OptionInfo object>, body_font: str = <typer.models.OptionInfo object>, bold_font: str = <typer.models.OptionInfo object>, logo_line1: str = <typer.models.OptionInfo object>, logo_line1_font: str = <typer.models.OptionInfo object>, logo_line2: str = <typer.models.OptionInfo object>, logo_line2_font: str = <typer.models.OptionInfo object>, logo_link: str = <typer.models.OptionInfo object>)
-
Generate letter as PDF Document.
Provide your own artwork: Place imgage file in working directory, use the
--image
option.Provide your own fonts: Place TTF Font files in working directory, use the following options to reference.
Expand source code
@app.command() def make( image_file: str = typer.Option( None, "--image", "-i", help="A logo raster graphic file placed in the top left corner. Width: 20mm", ), body_font: str = typer.Option( None, "--bodyfont", help="Letter body font.", ), bold_font: str = typer.Option( None, "--boldfont", help="Bold font used in the about line.", ), logo_line1: str = typer.Option( None, "--logo1", "-1", help="Logo Line 1", ), logo_line1_font: str = typer.Option( None, "--logo1font", help="Logo Line 1 Font", ), logo_line2: str = typer.Option( None, "--logo2", "-2", help="Logo Line 2", ), logo_line2_font: str = typer.Option( None, "--logo2font", help="Logo Line 2 Font", ), logo_link: str = typer.Option( "https://betterletter.harmlos.info", "--link", "-l", help="Logo Link.", ), ): """Generate letter as PDF Document. Provide your own artwork: Place imgage file in working directory, use the `--image` option. Provide your own fonts: Place TTF Font files in working directory, use the following options to reference.""" current_dir = os.getcwd() write() ed.cons.rule( title=f"[{COLOR}]betterletter make", style=COLOR, align="left", ) if ed.last != {}: if not image_file: try: image_file = ed.last["image_file"] except KeyError: pass if not body_font: try: body_font = ed.last["body_font"] except KeyError: pass if not bold_font: try: bold_font = ed.last["bold_font"] except KeyError: pass if not logo_line1: try: logo_line1 = ed.last["logo_line1"] except KeyError: pass if not logo_line1_font: try: logo_line1_font = ed.last["logo_line1_font"] except KeyError: pass if not logo_line2: try: logo_line2 = ed.last["logo_line2"] except KeyError: pass if not logo_line2_font: try: logo_line2_font = ed.last["logo_line2_font"] except KeyError: pass if not logo_link: try: logo_link = ed.last["logo_link"] except KeyError: pass class PDF(FPDF, HTMLMixin): def header(self): pdf.set_left_margin(0) pdf.set_right_margin(0) # Rendering logo: if image_file: self.image(image_file, 20, 8, 20) # Setting font: helvetica bold 15 if body_font: body = "body" pdf.add_font(body, fname=body_font, uni=True) else: body = "helvetica" self.set_font(body, "", 15) if not body_font and not logo_line1_font: self.set_font("Helvetica", "B", 15) if logo_line1_font: self.add_font("logo1", fname=logo_line1_font, uni=True) self.set_font("logo1", size=15) self.ln(5) # Moving cursor to the right: self.cell(w=40) # Printing title: if logo_line1: if image_file: self.cell(txt=logo_line1) else: self.set_x(x=0) self.cell(w=210, txt=logo_line1, align="C") self.ln(6) self.cell(w=40) if logo_line2_font: self.add_font("logo2", fname=logo_line2_font, uni=True) self.set_font("logo2", size=6) else: self.set_font(body, "", size=6) if logo_line2: if image_file: self.cell(txt=logo_line2, link=logo_link) else: self.set_x(x=0) self.cell(w=210, txt=logo_line2, link=logo_link, align="C") self.ln(12) # Text Margins self.set_left_margin(24) self.set_right_margin(24) if self.page_no() == 1: with open(VON) as f: von_frontmatter = frontmatter.load(f) # cons.print(von_frontmatter["first_name"]) self.set_font(body, "", 10) self.set_xy(x=120, y=51) self.cell(w=1, txt=von_frontmatter["title"]) self.set_x(x=120) self.cell( w=1, txt=f'{von_frontmatter["first_name"]} {von_frontmatter["last_name"]}', ) self.ln() self.set_x(x=120) self.cell(w=1, txt=von_frontmatter["address_1"]) self.ln() self.set_x(x=120) self.cell(w=1, txt=von_frontmatter["address_2"]) self.ln() self.set_x(x=120) self.cell( w=1, txt=von_frontmatter["post_code"] + " " + von_frontmatter["city"], ) self.ln() self.set_x(x=120) self.cell(w=1, txt=von_frontmatter["country"]) self.ln() self.set_x(x=120) self.cell(w=1, txt="Tel " + von_frontmatter["phone"]) self.ln() self.set_x(x=120) self.cell( w=1, txt=von_frontmatter["email"], link="mailto:" + von_frontmatter["email"], ) self.ln() self.set_x(x=120) self.cell( w=1, txt=von_frontmatter["web_site"], link="https://" + von_frontmatter["web_site"], ) self.ln() with open(TO) as f: an_frontmatter = frontmatter.load(f) self.set_font(body, "", 12) self.set_y(y=51) self.cell(w=1, txt=an_frontmatter["title"]) self.ln() self.cell( w=1, txt=f'{an_frontmatter["first_name"]} {an_frontmatter["last_name"]}', ) self.ln() # pdf.set_x(x=10) self.cell(w=1, txt=an_frontmatter["address_1"]) self.ln() # pdf.set_x(x=10) self.cell(w=1, txt=an_frontmatter["address_2"]) self.ln() # pdf.set_x(x=10) self.cell( w=1, txt=an_frontmatter["post_code"] + " " + an_frontmatter["city"] ) self.ln() self.ln() self.cell(w=1, txt=an_frontmatter["country"]) self.ln() self.set_font("helvetica", size=12) text = von_frontmatter["city"] text = text.strip() + ", " + datetime.now().now().strftime("%d.%m.%Y") self.ln() self.set_x(x=120) self.cell(txt=text) def footer(self): # Position cursor at 1.5 cm from bottom: self.set_y(-15) # Setting font: helvetica italic 8 self.set_font("helvetica", "I", 8) # Printing page number: self.cell( 0, 10, f"a betterletter - p {self.page_no()}/{{nb}}", 0, 0, "C", link="https://betterletter.harmlos.info", ) # Add folding marks self.line(x1=0, y1=87, x2=6, y2=87) self.line(x1=0, y1=148.5, x2=9, y2=148.5) self.line(x1=0, y1=192, x2=6, y2=192) with ed.cons.status(str(PDF_FILE), spinner="pong"): pdf = PDF() # count pages - see footer() pdf.alias_nb_pages() # add first page - auto page break is enabled by default pdf.add_page() # set font with open(BODY) as f: body_frontmatter = frontmatter.load(f) if body_font: pdf.set_font("body", "", 12) else: pdf.set_font("helvetica", "B", 12) if bold_font: pdf.add_font("bold", fname=bold_font, uni=True) pdf.set_font("bold", "", 12) line_height = pdf.font_size * 1.2 pdf.set_y(y=91) pdf.write(txt=body_frontmatter["about"]) if body_font: pdf.set_font("body", "", 10) else: pdf.set_font("helvetica", "", 10) pdf.ln() html_body = markdown.markdown( body_frontmatter.content, extensions=["toc", "extra"] ) pdf.write_html(text=html_body) with open(VON) as f: von_frontmatter = frontmatter.load(f) pdf.ln() pdf.ln() pdf.ln() pdf.write(txt="______________________________") pdf.ln(7.5) if body_font: pdf.set_font("body", "", 8) else: pdf.set_font("helvetica", "", 8) pdf.write( txt=f'{von_frontmatter["title"]} {von_frontmatter["first_name"]} {von_frontmatter["last_name"]}' ) pdf.output(PDF_FILE) ed.cons.print(f"[green]{str(PDF_FILE)} [{COLOR}]ready in {current_dir}") if image_file: ed.last.update({"image_file": image_file}) if body_font: ed.last.update({"body_font": body_font}) if bold_font: ed.last.update({"bold_font": bold_font}) if logo_line1: ed.last.update({"logo_line1": logo_line1}) if logo_line1_font: ed.last.update({"logo_line1_font": logo_line1_font}) if logo_line2: ed.last.update({"logo_line2": logo_line2}) if logo_line2_font: ed.last.update({"logo_line2_font": logo_line2_font}) if logo_link: ed.last.update({"logo_link": logo_link}) if ed.last != {}: with open("betterletter_make.yml", "w") as f: yaml.dump(ed.last, f)
-
Ask for user interaction.
Expand source code
def menu(choices, prompt=""): """Ask for user interaction.""" main_menu_title = prompt main_menu_items = choices main_menu_cursor = "👉 " main_menu_cursor_style = ("fg_red", "bold") main_menu = TerminalMenu( menu_entries=main_menu_items, title=main_menu_title, menu_cursor=main_menu_cursor, menu_cursor_style=main_menu_cursor_style, cycle_cursor=True, clear_screen=False, show_search_hint=False, accept_keys=("enter", "alt-d", "ctrl-i", " "), ) return main_menu.show()
def sender()
-
Work with the sender information
Expand source code
@app.command() def sender(): """Work with the sender information""" current_dir = os.getcwd() # try: # dirs_from_conf = conf["dirs"] # if current_dir not in dirs_from_conf: # cons.print(f'{current_dir} is not in congfiguration. Would you like to add it?') # except KeyError: # cons.print(f'Setting up {current_dir} as a working directory.') # dirs_from_conf = [current_dir] # with open(config_path, 'w') as f: # safe # yaml.dump(conf, f) if os.path.isfile(VON): with open(VON) as f: von_frontmatter = frontmatter.load(f) else: von_frontmatter = frontmatter.Post( """# Sender Information Please confirm the following data - Title - Herr, Frau, Professor Dr who or whatever or just leave it blank. - First name - Last name - Address 1 - Usually street and number. - Address 2 - Additional information. Often just blank. - Address 3 - Post code, wich is the [5 digit number if you are in Germany](https://en.wikipedia.org/wiki/List_of_postal_codes_in_Germany) and city. Sometimes prepended with a country code, i.e. `DE` for Germany. - Country - in English for international delivery sometimes the country code is found here: `DE Germany` """ ) von_frontmatter["document"] = "Sender Information" with open(VON, "w") as f: f.writelines(frontmatter.dumps(von_frontmatter)) ed.cons.print(Markdown(von_frontmatter.content)) from_fields = [ "title", "first_name", "last_name", "address_1", "address_2", "post_code", "city", "country", "phone", "email", "web_site", ] ed.cons.print() for von_item in from_fields: try: ed.cons.rule( title=f'[green]{von_frontmatter[von_item]} [{COLOR}]❮ {von_item.replace("_", " ").capitalize()}', style=COLOR, align="left", characters="❮", ) except KeyError as identfier: von_frontmatter[von_item] = ed.sess.prompt( default=von_item.replace("_", " ").capitalize() ) continue_on_one = 1 while continue_on_one == 1: continue_on_one = menu(["[1] OK", "[2] Edit Field"]) if continue_on_one == 1: choices = [] items = [] i = 0 for item in von_frontmatter.keys(): if item in from_fields: items.append(item) choices.append( f'{HOTKEYS[i]}{item.replace("_", " ").capitalize()}: {von_frontmatter[item]}' ) i += 1 try: choice = menu(prompt="Field?", choices=choices) von_frontmatter[items[choice]] = ed.sess.prompt( default=von_frontmatter[items[choice]] ) ed.cons.rule( title=f'[green]{von_frontmatter[items[choice]]} [{COLOR}]❮ {items[choice].replace("_", " ").capitalize()}', style=COLOR, align="left", characters="❮", ) except TypeError: pass else: ed.cons.print() ed.cons.rule( title=f"[{COLOR}]Sender Information checked and recorded in {str(VON)} ", style=COLOR, align="right", ) with open(VON, "w") as f: f.writelines(frontmatter.dumps(von_frontmatter))
def write()
-
Write the letter.
Expand source code
@app.command() def write(): """Write the letter.""" ed.cons.print(f"[{COLOR}]Checking Sender Information.") if not os.path.isfile(VON): sender() ed.cons.print(f"[{COLOR}]Checking Addressee Information.") if not os.path.isfile(TO): address() ed.cons.print(f"[bold]What is your favourite text editor right now?") ed.favourite = ed.sess.prompt( default=ed.favourite, completer=WordCompleter(EDITORS) ) ed.sess = PromptSession( message="❯ ", wrap_lines=True, vi_mode=True, completer=ed.test_completer ) current_dir = os.getcwd() if os.path.isfile(BODY): with open(BODY) as f: von_frontmatter = frontmatter.load(f) else: von_frontmatter = frontmatter.Post( """Letter about line will be printed in bold type. This is the body of the letter. > Replace with your own text. """ ) von_frontmatter["document"] = "Body Text" von_frontmatter["about"] = "About line of the letter." with open(BODY, "w") as f: f.writelines(frontmatter.dumps(von_frontmatter)) # cons.print(Markdown(von_frontmatter.content)) from_fields = ["about"] ed.cons.print() for von_item in from_fields: try: ed.cons.rule( title=f"[{COLOR}]❯ [bold green]{von_frontmatter[von_item]}", style=COLOR, align="left", characters="❮", ) except KeyError as identfier: von_frontmatter[von_item] = ed.sess.prompt( default=von_item.replace("_", " ").capitalize() ) continue_on_one = 1 ed.cons.print(Markdown(von_frontmatter.content)) while continue_on_one == 1: continue_on_one = menu(["[1] OK", "[2] Edit Field", "[3] Edit Letter Body"]) if continue_on_one == 1: choices = [] items = [] for item in von_frontmatter.keys(): items.append(item) choices.append( f'{item.replace("_", " ").capitalize()}: {von_frontmatter[item]}' ) choice = menu(prompt="Change field?", choices=choices) von_frontmatter[items[choice]] = ed.sess.prompt( default=von_frontmatter[items[choice]] ) ed.cons.rule( title=f"[{COLOR}]❯ [green]{von_frontmatter[items[choice]]}", style=COLOR, align="left", characters="❮", ) elif continue_on_one == 2: if ed.favourite in ["vi", "vim", "nvim"]: choices = [ "Deutsch|de_de", "English (US)|en_us", "English (GB)|en_gb", "Deutsch (AU)|de_au", "None", ] choice = menu(prompt="Spellang for checking?", choices=choices) if choice == 4: spelllang = "" else: spelllang = ( f'-c ":set spell spelllang={choices[choice].split("|")[-1]}" ' ) else: spelllang = "" mtime = os.path.getmtime(BODY) os.system(f"{ed.favourite} {spelllang}{BODY}") if mtime == os.path.getmtime(BODY): ed.cons.print("\n[bold]No changes.") else: with open(BODY) as f: von_frontmatter = frontmatter.load(f) ed.cons.print(Markdown(von_frontmatter.content)) continue_on_one = 1 else: ed.cons.print() with open(BODY, "w") as f: f.writelines(frontmatter.dumps(von_frontmatter)) ed.cons.rule( title=f"[{COLOR}]Letter body text checked and recorded in {str(BODY)} ", style=COLOR, align="right", ) if ed.last: ed.cons.print( Panel( renderable=str(ed.last), style=COLOR, title="configuration", title_align="left", subtitle="betterletter make --help", subtitle_align="right", border_style=COLOR, ) )
Classes
class Editor
-
Configuration Class.
Expand source code
class Editor: """Configuration Class.""" # Generating Completions completer_list = [] if os.path.isfile(VON): with open(VON) as f: von_frontmatter = frontmatter.load(f) temp_dict = von_frontmatter.to_dict() del temp_dict["content"] del temp_dict["document"] for key in temp_dict: words = temp_dict[key].split() if words != []: completer_list.extend(words) if os.path.isfile(TO): with open(TO) as f: von_frontmatter = frontmatter.load(f) temp_dict = von_frontmatter.to_dict() del temp_dict["content"] del temp_dict["document"] del temp_dict["uuid"] for key in temp_dict: words = temp_dict[key].split() if words != []: completer_list.extend(words) if os.path.isfile(BODY): with open(BODY) as f: von_frontmatter = frontmatter.load(f) temp_dict = von_frontmatter.to_dict() del temp_dict["document"] for line in temp_dict: words = temp_dict[line].split() if words != []: completer_list.extend(words) # removing duplicate words completer_list = list(dict.fromkeys(completer_list)) test_completer = WordCompleter(sorted(completer_list)) favourite = "vi" cons = Console() sess = PromptSession( message="❯ ", wrap_lines=True, vi_mode=True, completer=test_completer ) last = {} if os.path.isfile("betterletter_make.yml"): with open("betterletter_make.yml") as f: last = yaml.load(f, Loader=yaml.FullLoader) adressees = {} with open(config_path) as f: addressees = yaml.load(f, Loader=yaml.FullLoader)
Class variables
var addressees
var adressees
var completer_list
var cons
var f
var favourite
var key
var last
var line
var sess
var temp_dict
var test_completer
var von_frontmatter
var words