Files

618 lines
31 KiB
Python

import customtkinter as ctk
import tkinter as tk
from tkinter import filedialog, messagebox
import subprocess
import tempfile
import os
import shutil
import io
import re
import fitz # PyMuPDF
from PIL import Image, ImageTk
import sys
# Professional theme configuration
ctk.set_appearance_mode("light")
ctk.set_default_color_theme("blue")
class CVBuilderApp(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Professional CV Builder")
self.geometry("1350x900")
self.minsize(1200, 800)
# Layout configuration
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
# State management
self.current_section = None
self.section_frames = {}
self.latex_text = ""
self.pdf_image_refs = []
self.temp_dir = None
self._build_ui()
self._load_sample_data()
def _build_ui(self):
self._build_sidebar()
self._build_main_area()
self._navigate_to("personal")
def _build_sidebar(self):
"""Professional navigation sidebar"""
self.sidebar = ctk.CTkFrame(self, width=240, corner_radius=0, fg_color=("white", "#2b2b2b"))
self.sidebar.grid(row=0, column=0, sticky="nsew")
self.sidebar.grid_rowconfigure(1, weight=1)
# App branding
ctk.CTkLabel(self.sidebar, text="CV BUILDER", font=ctk.CTkFont(size=18, weight="bold")).grid(
row=0, column=0, padx=20, pady=(25, 5), sticky="w"
)
ctk.CTkLabel(self.sidebar, text="LaTeX Professional", font=ctk.CTkFont(size=11), text_color="gray").grid(
row=0, column=0, padx=20, pady=(0, 20), sticky="w"
)
# Navigation
nav_items = [
("personal", "Personal Information"),
("education", "Education"),
("coursework", "Relevant Coursework"),
("experience", "Professional Experience"),
("projects", "Projects"),
("skills", "Technical Skills"),
("leadership", "Leadership & Activities"),
("output", "LaTeX & PDF Preview")
]
self.nav_buttons = {}
for idx, (key, label) in enumerate(nav_items):
btn = ctk.CTkButton(
self.sidebar, text=label, command=lambda k=key: self._navigate_to(k),
fg_color="transparent", text_color=("black", "white"),
hover_color=("gray90", "gray10"), height=38, corner_radius=8, anchor="w"
)
btn.grid(row=idx + 1, column=0, padx=12, pady=4, sticky="ew")
self.nav_buttons[key] = btn
# Action buttons
self.import_btn = ctk.CTkButton(
self.sidebar, text="Import LaTeX", command=self._import_latex,
fg_color="#666666", hover_color="#4d4d4d", height=38, corner_radius=8
)
self.import_btn.grid(row=8, column=0, padx=12, pady=(15, 5), sticky="ew")
self.generate_btn = ctk.CTkButton(
self.sidebar, text="Generate LaTeX", command=self._generate_latex,
fg_color="#0066cc", hover_color="#0052a3", height=40, corner_radius=8,
font=ctk.CTkFont(size=13, weight="bold")
)
self.generate_btn.grid(row=9, column=0, padx=12, pady=5, sticky="ew")
self.compile_btn = ctk.CTkButton(
self.sidebar, text="Compile PDF", command=self._compile_latex,
fg_color="#0066cc", hover_color="#0052a3", height=40, corner_radius=8,
font=ctk.CTkFont(size=13, weight="bold"), state="disabled"
)
self.compile_btn.grid(row=10, column=0, padx=12, pady=(5, 20), sticky="ew")
# Status bar
self.status_label = ctk.CTkLabel(self.sidebar, text="", text_color="#0066cc", font=ctk.CTkFont(size=11))
self.status_label.grid(row=11, column=0, padx=12, pady=(0, 15), sticky="w")
def _build_main_area(self):
"""Main content container"""
self.main_container = ctk.CTkFrame(self, corner_radius=0, fg_color=("white", "#2b2b2b"))
self.main_container.grid(row=0, column=1, sticky="nsew", padx=20, pady=20)
self.main_container.grid_columnconfigure(0, weight=1)
self.main_container.grid_rowconfigure(0, weight=1)
self.section_frames["personal"] = self._create_personal_section()
self.section_frames["education"] = self._create_education_section()
self.section_frames["coursework"] = self._create_coursework_section()
self.section_frames["experience"] = self._create_experience_section()
self.section_frames["projects"] = self._create_projects_section()
self.section_frames["skills"] = self._create_skills_section()
self.section_frames["leadership"] = self._create_leadership_section()
self.section_frames["output"] = self._create_output_section()
def _create_section_header(self, parent, title):
header = ctk.CTkFrame(parent, fg_color="transparent", height=45)
header.grid(row=0, column=0, sticky="ew", pady=(0, 15))
ctk.CTkLabel(header, text=title, font=ctk.CTkFont(size=22, weight="bold")).pack(anchor="w", pady=10)
return header
def _create_personal_section(self):
frame = ctk.CTkScrollableFrame(self.main_container, corner_radius=12, fg_color=("white", "#2b2b2b"))
frame.grid_columnconfigure(0, weight=1)
self._create_section_header(frame, "Personal Information")
name_frame = ctk.CTkFrame(frame, fg_color="transparent")
name_frame.grid(row=1, column=0, sticky="ew", pady=10)
name_frame.grid_columnconfigure((0, 1), weight=1)
ctk.CTkLabel(name_frame, text="First Name").grid(row=0, column=0, sticky="w", padx=(0, 10))
self.first_name = ctk.CTkEntry(name_frame, height=38)
self.first_name.grid(row=1, column=0, sticky="ew", padx=(0, 10))
ctk.CTkLabel(name_frame, text="Last Name").grid(row=0, column=1, sticky="w", padx=(10, 0))
self.last_name = ctk.CTkEntry(name_frame, height=38)
self.last_name.grid(row=1, column=1, sticky="ew", padx=(10, 0))
contact_frame = ctk.CTkFrame(frame, fg_color="transparent")
contact_frame.grid(row=2, column=0, sticky="ew", pady=10)
contact_frame.grid_columnconfigure(1, weight=1)
self.personal_vars = {}
contacts = [("Address", "address"), ("Phone", "phone"), ("Email", "email"), ("LinkedIn", "linkedin"), ("GitHub", "github")]
for i, (label, key) in enumerate(contacts):
ctk.CTkLabel(contact_frame, text=label, width=100).grid(row=i, column=0, sticky="w", pady=8)
self.personal_vars[key] = ctk.CTkEntry(contact_frame, height=38)
self.personal_vars[key].grid(row=i, column=1, sticky="ew", padx=15, pady=8)
return frame
def _create_education_section(self):
frame = ctk.CTkScrollableFrame(self.main_container, corner_radius=12, fg_color=("white", "#2b2b2b"))
frame.grid_columnconfigure(0, weight=1)
self._create_section_header(frame, "Education")
edu_card = ctk.CTkFrame(frame, corner_radius=10)
edu_card.grid(row=1, column=0, sticky="ew", pady=10)
edu_card.grid_columnconfigure(1, weight=1)
self.edu_vars = {}
fields = [("University", "university"), ("Degree", "degree"), ("Start Date", "start_date"), ("End Date", "end_date"), ("Location", "location")]
for i, (label, key) in enumerate(fields):
ctk.CTkLabel(edu_card, text=label).grid(row=i, column=0, sticky="w", padx=15, pady=10)
self.edu_vars[key] = ctk.CTkEntry(edu_card, height=38)
self.edu_vars[key].grid(row=i, column=1, sticky="ew", padx=15, pady=10)
return frame
def _create_coursework_section(self):
frame = ctk.CTkScrollableFrame(self.main_container, corner_radius=12, fg_color=("white", "#2b2b2b"))
frame.grid_columnconfigure(0, weight=1)
self._create_section_header(frame, "Relevant Coursework")
self.coursework = ctk.CTkTextbox(frame, height=150, corner_radius=10)
self.coursework.grid(row=1, column=0, sticky="ew", pady=10)
ctk.CTkLabel(frame, text="Enter courses separated by commas", text_color="gray").grid(row=2, column=0, sticky="w", pady=(0, 10))
return frame
def _create_experience_section(self):
frame = ctk.CTkScrollableFrame(self.main_container, corner_radius=12, fg_color=("white", "#2b2b2b"))
frame.grid_columnconfigure(0, weight=1)
self._create_section_header(frame, "Professional Experience")
self.exp_container = ctk.CTkFrame(frame, fg_color="transparent")
self.exp_container.grid(row=1, column=0, sticky="ew")
self.exp_container.grid_columnconfigure(0, weight=1)
self.exp_entries = []
ctk.CTkButton(frame, text="Add Position", command=self._add_experience_entry, fg_color="#0066cc", hover_color="#0052a3", height=36).grid(row=2, column=0, pady=15, sticky="w")
return frame
def _add_experience_entry(self):
idx = len(self.exp_entries)
card = ctk.CTkFrame(self.exp_container, corner_radius=10)
card.grid(row=idx, column=0, sticky="ew", pady=10)
card.grid_columnconfigure(1, weight=1)
ctk.CTkLabel(card, text=f"Position {idx + 1}", font=ctk.CTkFont(weight="bold")).grid(row=0, column=0, columnspan=2, padx=15, pady=(15, 5), sticky="w")
vars_dict = {}
fields = [("Company", "company"), ("Title", "title"), ("Start Date", "start_date"), ("End Date", "end_date"), ("Location", "location")]
for i, (label, key) in enumerate(fields, start=1):
ctk.CTkLabel(card, text=label).grid(row=i, column=0, sticky="w", padx=15, pady=5)
vars_dict[key] = ctk.CTkEntry(card, height=38)
vars_dict[key].grid(row=i, column=1, sticky="ew", padx=15, pady=5)
ctk.CTkLabel(card, text="Responsibilities (one per line)").grid(row=len(fields) + 1, column=0, columnspan=2, sticky="w", padx=15, pady=(15, 5))
resp_text = ctk.CTkTextbox(card, height=100, corner_radius=10)
resp_text.grid(row=len(fields) + 2, column=0, columnspan=2, sticky="ew", padx=15, pady=(0, 15))
ctk.CTkButton(card, text="Remove", command=lambda c=card: self._remove_entry(c, self.exp_entries), fg_color="#cc3333", hover_color="#a62929", width=100, height=32).grid(row=len(fields) + 3, column=0, columnspan=2, pady=(0, 15))
self.exp_entries.append((vars_dict, resp_text, card))
def _create_projects_section(self):
frame = ctk.CTkScrollableFrame(self.main_container, corner_radius=12, fg_color=("white", "#2b2b2b"))
frame.grid_columnconfigure(0, weight=1)
self._create_section_header(frame, "Projects")
self.proj_container = ctk.CTkFrame(frame, fg_color="transparent")
self.proj_container.grid(row=1, column=0, sticky="ew")
self.proj_container.grid_columnconfigure(0, weight=1)
self.proj_entries = []
ctk.CTkButton(frame, text="Add Project", command=self._add_project_entry, fg_color="#0066cc", hover_color="#0052a3", height=36).grid(row=2, column=0, pady=15, sticky="w")
return frame
def _add_project_entry(self):
idx = len(self.proj_entries)
card = ctk.CTkFrame(self.proj_container, corner_radius=10)
card.grid(row=idx, column=0, sticky="ew", pady=10)
card.grid_columnconfigure(1, weight=1)
ctk.CTkLabel(card, text=f"Project {idx + 1}", font=ctk.CTkFont(weight="bold")).grid(row=0, column=0, columnspan=2, padx=15, pady=(15, 5), sticky="w")
vars_dict = {}
fields = [("Project Name", "name"), ("Technologies", "technologies"), ("Date", "date")]
for i, (label, key) in enumerate(fields, start=1):
ctk.CTkLabel(card, text=label).grid(row=i, column=0, sticky="w", padx=15, pady=5)
vars_dict[key] = ctk.CTkEntry(card, height=38)
vars_dict[key].grid(row=i, column=1, sticky="ew", padx=15, pady=5)
ctk.CTkLabel(card, text="Description (one per line)").grid(row=len(fields) + 1, column=0, columnspan=2, sticky="w", padx=15, pady=(15, 5))
desc_text = ctk.CTkTextbox(card, height=100, corner_radius=10)
desc_text.grid(row=len(fields) + 2, column=0, columnspan=2, sticky="ew", padx=15, pady=(0, 15))
ctk.CTkButton(card, text="Remove", command=lambda c=card: self._remove_entry(c, self.proj_entries), fg_color="#cc3333", hover_color="#a62929", width=100, height=32).grid(row=len(fields) + 3, column=0, columnspan=2, pady=(0, 15))
self.proj_entries.append((vars_dict, desc_text, card))
def _create_skills_section(self):
frame = ctk.CTkScrollableFrame(self.main_container, corner_radius=12, fg_color=("white", "#2b2b2b"))
frame.grid_columnconfigure(0, weight=1)
self._create_section_header(frame, "Technical Skills")
skills_card = ctk.CTkFrame(frame, corner_radius=10)
skills_card.grid(row=1, column=0, sticky="ew", pady=10)
skills_card.grid_columnconfigure(1, weight=1)
self.skills_vars = {}
fields = [("Languages", "languages"), ("Developer Tools", "tools"), ("Technologies/Frameworks", "technologies")]
for i, (label, key) in enumerate(fields):
ctk.CTkLabel(skills_card, text=label).grid(row=i, column=0, sticky="nw", padx=15, pady=15)
self.skills_vars[key] = ctk.CTkTextbox(skills_card, height=60, corner_radius=10)
self.skills_vars[key].grid(row=i, column=1, sticky="ew", padx=15, pady=15)
return frame
def _create_leadership_section(self):
frame = ctk.CTkScrollableFrame(self.main_container, corner_radius=12, fg_color=("white", "#2b2b2b"))
frame.grid_columnconfigure(0, weight=1)
self._create_section_header(frame, "Leadership & Activities")
self.lead_container = ctk.CTkFrame(frame, fg_color="transparent")
self.lead_container.grid(row=1, column=0, sticky="ew")
self.lead_container.grid_columnconfigure(0, weight=1)
self.lead_entries = []
ctk.CTkButton(frame, text="Add Activity", command=self._add_leadership_entry, fg_color="#0066cc", hover_color="#0052a3", height=36).grid(row=2, column=0, pady=15, sticky="w")
return frame
def _add_leadership_entry(self):
idx = len(self.lead_entries)
card = ctk.CTkFrame(self.lead_container, corner_radius=10)
card.grid(row=idx, column=0, sticky="ew", pady=10)
card.grid_columnconfigure(1, weight=1)
ctk.CTkLabel(card, text=f"Activity {idx + 1}", font=ctk.CTkFont(weight="bold")).grid(row=0, column=0, columnspan=2, padx=15, pady=(15, 5), sticky="w")
vars_dict = {}
fields = [("Organization", "organization"), ("Role", "role"), ("Date", "date"), ("Location", "location")]
for i, (label, key) in enumerate(fields, start=1):
ctk.CTkLabel(card, text=label).grid(row=i, column=0, sticky="w", padx=15, pady=5)
vars_dict[key] = ctk.CTkEntry(card, height=38)
vars_dict[key].grid(row=i, column=1, sticky="ew", padx=15, pady=5)
ctk.CTkLabel(card, text="Achievements (one per line)").grid(row=len(fields) + 1, column=0, columnspan=2, sticky="w", padx=15, pady=(15, 5))
achieve_text = ctk.CTkTextbox(card, height=100, corner_radius=10)
achieve_text.grid(row=len(fields) + 2, column=0, columnspan=2, sticky="ew", padx=15, pady=(0, 15))
ctk.CTkButton(card, text="Remove", command=lambda c=card: self._remove_entry(c, self.lead_entries), fg_color="#cc3333", hover_color="#a62929", width=100, height=32).grid(row=len(fields) + 3, column=0, columnspan=2, pady=(0, 15))
self.lead_entries.append((vars_dict, achieve_text, card))
def _create_output_section(self):
frame = ctk.CTkFrame(self.main_container, corner_radius=12, fg_color=("white", "#2b2b2b"))
frame.grid_columnconfigure(0, weight=1)
frame.grid_rowconfigure(1, weight=1)
self._create_section_header(frame, "LaTeX Output & PDF Preview")
paned = tk.PanedWindow(frame, orient=tk.HORIZONTAL, bg="white", sashrelief="flat", sashwidth=8)
paned.grid(row=1, column=0, sticky="nsew", padx=15, pady=(0, 15))
code_frame = ctk.CTkFrame(paned, corner_radius=8, fg_color=("white", "#2b2b2b"))
ctk.CTkLabel(code_frame, text="LaTeX Source", font=ctk.CTkFont(weight="bold")).pack(padx=10, pady=(10, 5), anchor="w")
self.latex_editor = ctk.CTkTextbox(code_frame, corner_radius=8, font=("Consolas", 11))
self.latex_editor.pack(fill="both", expand=True, padx=10, pady=(0, 10))
paned.add(code_frame, width=450)
viewer_frame = ctk.CTkFrame(paned, corner_radius=8, fg_color=("white", "#2b2b2b"))
ctk.CTkLabel(viewer_frame, text="PDF Preview", font=ctk.CTkFont(weight="bold")).pack(padx=10, pady=(10, 5), anchor="w")
self.pdf_canvas_frame = ctk.CTkScrollableFrame(viewer_frame, corner_radius=8, fg_color=("white", "#2b2b2b"))
self.pdf_canvas_frame.pack(fill="both", expand=True, padx=10, pady=(0, 10))
self.placeholder_label = ctk.CTkLabel(self.pdf_canvas_frame, text="Compile PDF to see preview", font=ctk.CTkFont(size=14), text_color="gray")
self.placeholder_label.pack(expand=True)
paned.add(viewer_frame)
btn_frame = ctk.CTkFrame(frame, fg_color="transparent", height=45)
btn_frame.grid(row=2, column=0, sticky="ew", padx=15, pady=(0, 15))
ctk.CTkButton(btn_frame, text="Save .tex", command=self._save_tex, fg_color="#0066cc", hover_color="#0052a3", width=120).pack(side="left", padx=5)
ctk.CTkButton(btn_frame, text="Copy LaTeX", command=self._copy_latex, fg_color="#666666", hover_color="#4d4d4d", width=120).pack(side="left", padx=5)
return frame
def _remove_entry(self, card, entry_list):
for i, (_, _, c) in enumerate(entry_list):
if c == card:
card.destroy()
entry_list.pop(i)
break
def _navigate_to(self, section_key):
if self.current_section:
self.current_section.grid_remove()
self.current_section = self.section_frames[section_key]
self.current_section.grid(row=0, column=0, sticky="nsew")
for key, btn in self.nav_buttons.items():
btn.configure(fg_color=("#e6f0ff", "#1a3355") if key == section_key else "transparent")
def _generate_latex(self):
self.latex_text = self._build_latex_cv()
self.latex_editor.delete("1.0", tk.END)
self.latex_editor.insert("1.0", self.latex_text)
self.compile_btn.configure(state="normal")
self._set_status("LaTeX generated successfully")
self._navigate_to("output")
def _build_latex_cv(self):
latex = r"""\documentclass[11pt,a4paper]{article}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{geometry}
\usepackage{enumitem}
\usepackage{hyperref}
\usepackage{parskip}
\usepackage{xcolor}
\geometry{margin=1in}
\setlist[itemize]{leftmargin=*, nosep, topsep=3pt, itemsep=3pt}
\pagestyle{empty}
\definecolor{primary}{RGB}{0,85,170}
\definecolor{secondary}{RGB}{80,80,80}
\hypersetup{colorlinks=true, linkcolor=primary, urlcolor=primary}
\begin{document}
"""
# Header
first = self.first_name.get().strip()
last = self.last_name.get().strip()
latex += f"\\begin{{center}}\n\\textbf{{\\LARGE \\textcolor{{primary}}{{{first} {last}}}}}\\\\\n"
contacts = []
if self.personal_vars['address'].get().strip(): contacts.append(self.personal_vars['address'].get().strip())
if self.personal_vars['phone'].get().strip(): p = self.personal_vars['phone'].get().strip(); contacts.append(f"\\href{{tel:{p}}}{{{p}}}")
if self.personal_vars['email'].get().strip(): e = self.personal_vars['email'].get().strip(); contacts.append(f"\\href{{mailto:{e}}}{{{e}}}")
if self.personal_vars['linkedin'].get().strip(): l = self.personal_vars['linkedin'].get().strip(); contacts.append(f"\\href{{https://linkedin.com/in/{l}}}{{linkedin.com/in/{l}}}")
if self.personal_vars['github'].get().strip(): g = self.personal_vars['github'].get().strip(); contacts.append(f"\\href{{https://github.com/{g}}}{{github.com/{g}}}")
latex += " \\textcolor{secondary}{|} ".join(contacts) + "\n\\end{center}\n\n\\hrule\n\n"
# Education
if self.edu_vars['university'].get().strip():
latex += "\\section*{\\textcolor{primary}{Education}}\n\\addvspace{5pt}\n"
uni, deg, s, e, loc = [v.get().strip() for v in self.edu_vars.values()]
date_str = f"{s} -- {e}" if s and e else ""
latex += f"\\noindent\\textbf{{{uni}}}"
if date_str: latex += f"\\hfill \\textcolor{{secondary}}{{{date_str}}}"
latex += f"\\\\\n\\textit{{{deg}}}"
if loc: latex += f" \\textcolor{{secondary}}{{|}} {loc}"
latex += "\n\n"
# Coursework
cw = self.coursework.get("1.0", tk.END).strip()
if cw:
latex += "\\section*{\\textcolor{primary}{Relevant Coursework}}\n\\addvspace{5pt}\n"
courses = [c.strip() for c in cw.split(',') if c.strip()]
latex += "\\begin{itemize}\n"
for i in range(0, len(courses), 4):
latex += " \\item " + " \\quad $\\bullet$ ".join(courses[i:i+4]) + "\n"
latex += "\\end{itemize}\n\n"
# Experience
latex += "\\section*{\\textcolor{primary}{Experience}}\n\\addvspace{5pt}\n"
for v, t, _ in self.exp_entries:
comp = v['company'].get().strip()
if not comp: continue
title, s, e, loc = [v[k].get().strip() for k in ['title', 'start_date', 'end_date', 'location']]
date_str = f"{s} -- {e}" if s and e else ""
latex += f"\\noindent\\textbf{{{comp}}}"
if date_str: latex += f"\\hfill \\textcolor{{secondary}}{{{date_str}}}"
latex += f"\\\\\n\\textit{{{title}}}"
if loc: latex += f" \\textcolor{{secondary}}{{|}} {loc}"
latex += "\n"
resp = t.get("1.0", tk.END).strip()
if resp:
latex += "\\begin{itemize}\n"
for r in resp.split('\n'):
if r.strip(): latex += f" \\item {r.strip()}\n"
latex += "\\end{itemize}\n"
latex += "\n"
# Projects
latex += "\\section*{\\textcolor{primary}{Projects}}\n\\addvspace{5pt}\n"
for v, t, _ in self.proj_entries:
name = v['name'].get().strip()
if not name: continue
tech, date = [v[k].get().strip() for k in ['technologies', 'date']]
tech_str = f" \\textcolor{{secondary}}{{|}} {tech}" if tech else ""
latex += f"\\noindent\\textbf{{{name}}}{tech_str}"
if date: latex += f"\\hfill \\textcolor{{secondary}}{{{date}}}"
latex += "\n"
desc = t.get("1.0", tk.END).strip()
if desc:
latex += "\\begin{itemize}\n"
for d in desc.split('\n'):
if d.strip(): latex += f" \\item {d.strip()}\n"
latex += "\\end{itemize}\n"
latex += "\n"
# Skills
latex += "\\section*{\\textcolor{primary}{Technical Skills}}\n\\addvspace{5pt}\n"
has_skills = False
for key, label in [('languages', 'Languages'), ('tools', 'Developer Tools'), ('technologies', 'Technologies/Frameworks')]:
txt = self.skills_vars[key].get("1.0", tk.END).strip()
if txt:
latex += f"\\noindent\\textbf{{{label}:}} {txt}\\\\\n"
has_skills = True
if has_skills: latex += "\n"
# Leadership
latex += "\\section*{\\textcolor{primary}{Leadership / Extracurricular}}\n\\addvspace{5pt}\n"
for v, t, _ in self.lead_entries:
org = v['organization'].get().strip()
if not org: continue
role, date, loc = [v[k].get().strip() for k in ['role', 'date', 'location']]
latex += f"\\noindent\\textbf{{{org}}}"
if date: latex += f"\\hfill \\textcolor{{secondary}}{{{date}}}"
latex += f"\\\\\n\\textit{{{role}}}"
if loc: latex += f" \\textcolor{{secondary}}{{|}} {loc}"
latex += "\n"
ach = t.get("1.0", tk.END).strip()
if ach:
latex += "\\begin{itemize}\n"
for a in ach.split('\n'):
if a.strip(): latex += f" \\item {a.strip()}\n"
latex += "\\end{itemize}\n"
latex += "\n"
latex += "\\end{document}"
return latex
def _import_latex(self):
file_path = filedialog.askopenfilename(title="Import LaTeX CV", filetypes=[("LaTeX", "*.tex"), ("All", "*.*")])
if not file_path: return
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
self._parse_and_populate(content)
self._set_status("Imported successfully")
except Exception as e:
messagebox.showerror("Import Error", f"Failed to parse LaTeX file:\n{e}")
def _parse_and_populate(self, latex_text):
"""Basic regex parser to populate UI from LaTeX"""
# Personal
name_match = re.search(r'\\textbf\{\\LARGE \\textcolor\{primary\}\{(.*?)\}\}', latex_text)
if name_match:
names = name_match.group(1).split()
self.first_name.delete(0, tk.END); self.first_name.insert(0, names[0] if names else "")
self.last_name.delete(0, tk.END); self.last_name.insert(0, " ".join(names[1:]) if len(names)>1 else "")
contact_line = re.search(r'\\end\{center\}\s*\n(.*?)\\hrule', latex_text, re.DOTALL)
if contact_line:
text = contact_line.group(1).replace('\\textcolor{secondary}{|}', '|').replace('\\href{tel:', '').replace('}', '')
# Simple extraction (improves with consistent formatting)
self.personal_vars['phone'].delete(0, tk.END); self.personal_vars['phone'].insert(0, re.search(r'(\d{3}-\d{3}-\d{4})', text).group(1) if re.search(r'\d{3}-\d{3}-\d{4}', text) else "")
self.personal_vars['email'].delete(0, tk.END); self.personal_vars['email'].insert(0, re.search(r'([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)', text).group(1) if re.search(r'[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+', text) else "")
# Education
edu_match = re.search(r'\\section\*\{\\textcolor\{primary\}\{Education\}\}.*?\\textbf\{(.*?)\}.*?\\textit\{(.*?)\}', latex_text, re.DOTALL)
if edu_match:
self.edu_vars['university'].delete(0, tk.END); self.edu_vars['university'].insert(0, edu_match.group(1))
self.edu_vars['degree'].delete(0, tk.END); self.edu_vars['degree'].insert(0, edu_match.group(2))
# Coursework
cw_match = re.search(r'\\section\*\{\\textcolor\{primary\}\{Relevant Coursework\}\}.*?\\begin\{itemize\}(.*?)\\end\{itemize\}', latex_text, re.DOTALL)
if cw_match:
self.coursework.delete("1.0", tk.END)
self.coursework.insert("1.0", cw_match.group(1).replace('\\item', '').replace('\\quad $\\bullet$', ',').replace('\n', '').strip())
self._set_status("Imported. Review fields for accuracy.")
def _compile_latex(self):
if not shutil.which("pdflatex"):
messagebox.showerror("LaTeX Missing", "pdflatex not found. Install TeX Live or MiKTeX.")
return
self.temp_dir = tempfile.mkdtemp()
tex_path = os.path.join(self.temp_dir, "cv.tex")
try:
with open(tex_path, "w", encoding="utf-8") as f:
f.write(self.latex_text)
self._set_status("Compiling...")
self.compile_btn.configure(state="disabled")
self.update()
result = subprocess.run(["pdflatex", "-interaction=nonstopmode", "cv.tex"], cwd=self.temp_dir, capture_output=True, text=True)
pdf_path = os.path.join(self.temp_dir, "cv.pdf")
if result.returncode == 0 and os.path.exists(pdf_path):
self._set_status("Compilation successful")
self._render_pdf(pdf_path)
else:
self._set_status("Compilation failed")
messagebox.showerror("Error", "LaTeX compilation failed.")
except Exception as e:
self._set_status("Error")
messagebox.showerror("Exception", str(e))
finally:
self.compile_btn.configure(state="normal")
def _render_pdf(self, pdf_path):
for widget in self.pdf_canvas_frame.winfo_children():
widget.destroy()
self.pdf_image_refs.clear()
self.placeholder_label.pack_forget()
try:
doc = fitz.open(pdf_path)
for page_num in range(len(doc)):
page = doc.load_page(page_num)
pix = page.get_pixmap(matrix=fitz.Matrix(2.0, 2.0))
img = Image.open(io.BytesIO(pix.tobytes("ppm")))
photo = ImageTk.PhotoImage(img)
self.pdf_image_refs.append(photo)
ctk.CTkLabel(self.pdf_canvas_frame, image=photo, text="").pack(pady=10)
doc.close()
except Exception as e:
ctk.CTkLabel(self.pdf_canvas_frame, text=f"Render error: {e}", text_color="red").pack(pady=20)
def _save_tex(self):
path = filedialog.asksaveasfilename(defaultextension=".tex", filetypes=[("LaTeX", "*.tex")], initialfile="cv.tex")
if path:
with open(path, "w", encoding="utf-8") as f:
f.write(self.latex_text)
self._set_status(f"Saved: {os.path.basename(path)}")
def _copy_latex(self):
self.clipboard_clear()
self.clipboard_append(self.latex_text)
self._set_status("Copied to clipboard")
def _set_status(self, msg):
self.status_label.configure(text=msg)
def _load_sample_data(self):
"""Pre-fill for demonstration"""
self.first_name.insert(0, "First")
self.last_name.insert(0, "Last")
self.personal_vars['address'].insert(0, "123 Street Name, Town, State 12345")
self.personal_vars['phone'].insert(0, "123-456-7890")
self.personal_vars['email'].insert(0, "email@gmail.com")
self.personal_vars['linkedin'].insert(0, "username")
self.personal_vars['github'].insert(0, "username")
self.edu_vars['university'].insert(0, "State University")
self.edu_vars['degree'].insert(0, "Bachelor of Science in Computer Science")
self.edu_vars['start_date'].insert(0, "Sep. 2017")
self.edu_vars['end_date'].insert(0, "May 2021")
self.edu_vars['location'].insert(0, "City, State")
self.coursework.insert("1.0", "Data Structures, Algorithms Analysis, Artificial Intelligence, Systems Programming, Software Methodology, Database Management, Internet Technology, Computer Architecture")
def main():
try:
app = CVBuilderApp()
app.mainloop()
except Exception as e:
messagebox.showerror("Startup Error", f"Failed to launch:\n{e}")
sys.exit(1)
if __name__ == "__main__":
main()