Files
2026-05-26 18:08:00 +01:00

909 lines
41 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 sys
try:
import fitz # PyMuPDF
from PIL import Image, ImageTk
PDF_AVAILABLE = True
except ImportError:
PDF_AVAILABLE = False
# Modern SaaS theme
ctk.set_appearance_mode("light")
ctk.set_default_color_theme("blue")
# Color palette (SaaS-inspired)
COLORS = {
"primary": "#6366f1", # Indigo
"primary_hover": "#4f46e5",
"accent": "#10b981", # Emerald
"danger": "#ef4444",
"surface": "#ffffff",
"surface_alt": "#f9fafb",
"border": "#e5e7eb",
"text": "#111827",
"text_muted": "#6b7280",
"bg": "#f3f4f6",
}
class CVBuilderApp(ctk.CTk):
def __init__(self):
super().__init__()
self.title("CV Forge — Professional LaTeX Resume Builder")
self.geometry("1400x900")
self.minsize(1100, 750)
self.configure(fg_color=COLORS["bg"])
# State
self.current_tab = "personal"
self.section_frames = {}
self.nav_buttons = {}
self.latex_text = ""
self.pdf_image_refs = []
self.temp_dir = None
self.exp_entries = []
self.proj_entries = []
self.lead_entries = []
self._build_header()
self._build_tab_bar()
self._build_content_area()
self._populate_sample_data()
# ------------------------------------------------------------------ #
# HEADER BAR
# ------------------------------------------------------------------ #
def _build_header(self):
header = ctk.CTkFrame(self, fg_color=COLORS["surface"], height=70, corner_radius=0)
header.grid(row=0, column=0, sticky="ew")
header.grid_columnconfigure(2, weight=1)
header.grid_propagate(False)
# Brand
brand = ctk.CTkFrame(header, fg_color="transparent")
brand.grid(row=0, column=0, padx=24, pady=0, sticky="w")
logo = ctk.CTkLabel(brand, text="CV", font=ctk.CTkFont(size=18, weight="bold"),
text_color="white", width=36, height=36, corner_radius=8,
fg_color=COLORS["primary"])
logo.pack(side="left", padx=(0, 12))
brand_text = ctk.CTkFrame(brand, fg_color="transparent")
brand_text.pack(side="left")
ctk.CTkLabel(brand_text, text="CV Forge", font=ctk.CTkFont(size=16, weight="bold"),
text_color=COLORS["text"]).pack(anchor="w")
ctk.CTkLabel(brand_text, text="Professional Resume Builder",
font=ctk.CTkFont(size=11), text_color=COLORS["text_muted"]).pack(anchor="w")
# Divider
sep = ctk.CTkFrame(header, width=1, fg_color=COLORS["border"])
sep.grid(row=0, column=1, sticky="ns", padx=24, pady=18)
# Status indicator
self.status_pill = ctk.CTkLabel(
header, text=" Ready ", font=ctk.CTkFont(size=11, weight="bold"),
text_color=COLORS["accent"], fg_color="#d1fae5",
corner_radius=12, height=24
)
self.status_pill.grid(row=0, column=2, sticky="w", padx=12)
# Actions (right side)
actions = ctk.CTkFrame(header, fg_color="transparent")
actions.grid(row=0, column=3, padx=24, sticky="e")
self.import_btn = ctk.CTkButton(
actions, text="Import .tex", command=self._import_latex,
fg_color="transparent", border_width=1, border_color=COLORS["border"],
text_color=COLORS["text"], hover_color=COLORS["surface_alt"],
height=36, corner_radius=8, font=ctk.CTkFont(size=12, weight="bold")
)
self.import_btn.pack(side="left", padx=6)
self.generate_btn = ctk.CTkButton(
actions, text="Generate", command=self._generate_latex,
fg_color=COLORS["primary"], hover_color=COLORS["primary_hover"],
text_color="white", height=36, corner_radius=8,
font=ctk.CTkFont(size=12, weight="bold")
)
self.generate_btn.pack(side="left", padx=6)
self.compile_btn = ctk.CTkButton(
actions, text="Compile PDF", command=self._compile_latex,
fg_color=COLORS["accent"], hover_color="#059669",
text_color="white", height=36, corner_radius=8,
font=ctk.CTkFont(size=12, weight="bold"), state="disabled"
)
self.compile_btn.pack(side="left", padx=(6, 0))
# ------------------------------------------------------------------ #
# HORIZONTAL TAB BAR
# ------------------------------------------------------------------ #
def _build_tab_bar(self):
tab_bar = ctk.CTkFrame(self, fg_color=COLORS["surface"], height=56, corner_radius=0)
tab_bar.grid(row=1, column=0, sticky="ew")
tab_bar.grid_propagate(False)
tabs_container = ctk.CTkFrame(tab_bar, fg_color="transparent")
tabs_container.pack(side="left", padx=24, pady=10)
tab_items = [
("personal", "Personal"),
("education", "Education"),
("coursework", "Coursework"),
("experience", "Experience"),
("projects", "Projects"),
("skills", "Skills"),
("leadership", "Leadership"),
("output", "Preview"),
]
for key, label in tab_items:
btn = ctk.CTkButton(
tabs_container, text=label,
command=lambda k=key: self._navigate_to(k),
fg_color="transparent", text_color=COLORS["text_muted"],
hover_color=COLORS["surface_alt"],
height=34, corner_radius=8,
font=ctk.CTkFont(size=13, weight="bold")
)
btn.pack(side="left", padx=2)
self.nav_buttons[key] = btn
# Separator at bottom of tab bar
sep = ctk.CTkFrame(tab_bar, height=1, fg_color=COLORS["border"])
sep.pack(side="bottom", fill="x")
# ------------------------------------------------------------------ #
# MAIN CONTENT AREA
# ------------------------------------------------------------------ #
def _build_content_area(self):
self.content = ctk.CTkFrame(self, fg_color=COLORS["bg"], corner_radius=0)
self.content.grid(row=2, column=0, sticky="nsew")
self.content.grid_columnconfigure(0, weight=1)
self.content.grid_rowconfigure(0, weight=1)
self.grid_rowconfigure(2, weight=1)
# Build all section frames
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()
self._navigate_to("personal")
# Reusable components
def _make_card(self, parent, title=None):
card = ctk.CTkFrame(parent, fg_color=COLORS["surface"], corner_radius=12, border_width=1, border_color=COLORS["border"])
if title:
ctk.CTkLabel(card, text=title, font=ctk.CTkFont(size=14, weight="bold"),
text_color=COLORS["text"]).grid(row=0, column=0, columnspan=2,
sticky="w", padx=20, pady=(16, 8))
return card
def _section_header(self, parent, title, subtitle):
wrap = ctk.CTkFrame(parent, fg_color="transparent")
wrap.grid(row=0, column=0, sticky="ew", pady=(0, 20))
ctk.CTkLabel(wrap, text=title, font=ctk.CTkFont(size=24, weight="bold"),
text_color=COLORS["text"]).pack(anchor="w")
ctk.CTkLabel(wrap, text=subtitle, font=ctk.CTkFont(size=13),
text_color=COLORS["text_muted"]).pack(anchor="w", pady=(4, 0))
return wrap
def _form_row(self, parent, label, widget, row):
ctk.CTkLabel(parent, text=label, font=ctk.CTkFont(size=12, weight="bold"),
text_color=COLORS["text_muted"]).grid(row=row, column=0, sticky="w",
padx=20, pady=8)
widget.grid(row=row, column=1, sticky="ew", padx=20, pady=8)
# ------------------------------------------------------------------ #
# PERSONAL
# ------------------------------------------------------------------ #
def _create_personal_section(self):
frame = ctk.CTkScrollableFrame(self.content, fg_color="transparent")
frame.grid_columnconfigure(0, weight=1)
self._section_header(frame, "Personal Information",
"Tell employers how to reach you.")
card = self._make_card(frame)
card.grid(row=1, column=0, sticky="ew")
card.grid_columnconfigure(1, weight=1)
# Name row
ctk.CTkLabel(card, text="Full Name", font=ctk.CTkFont(size=12, weight="bold"),
text_color=COLORS["text_muted"]).grid(row=0, column=0, sticky="w",
padx=20, pady=(20, 8))
name_row = ctk.CTkFrame(card, fg_color="transparent")
name_row.grid(row=0, column=1, sticky="ew", padx=20, pady=(20, 8))
name_row.grid_columnconfigure((0, 1), weight=1)
self.first_name = ctk.CTkEntry(name_row, height=38, placeholder_text="First name",
border_color=COLORS["border"])
self.first_name.grid(row=0, column=0, sticky="ew", padx=(0, 8))
self.last_name = ctk.CTkEntry(name_row, height=38, placeholder_text="Last name",
border_color=COLORS["border"])
self.last_name.grid(row=0, column=1, sticky="ew", padx=(8, 0))
# Contact fields
self.personal_vars = {}
fields = [
("Address", "address", "e.g. 123 Main St, New York, NY"),
("Phone", "phone", "e.g. 555-123-4567"),
("Email", "email", "you@example.com"),
("LinkedIn", "linkedin", "username"),
("GitHub", "github", "username"),
]
for i, (label, key, ph) in enumerate(fields, start=1):
entry = ctk.CTkEntry(card, height=38, placeholder_text=ph, border_color=COLORS["border"])
self._form_row(card, label, entry, i)
self.personal_vars[key] = entry
# Spacer
ctk.CTkFrame(card, height=10, fg_color="transparent").grid(row=len(fields)+1, column=0)
return frame
# ------------------------------------------------------------------ #
# EDUCATION
# ------------------------------------------------------------------ #
def _create_education_section(self):
frame = ctk.CTkScrollableFrame(self.content, fg_color="transparent")
frame.grid_columnconfigure(0, weight=1)
self._section_header(frame, "Education", "Your academic background.")
card = self._make_card(frame)
card.grid(row=1, column=0, sticky="ew")
card.grid_columnconfigure(1, weight=1)
self.edu_vars = {}
fields = [
("University", "university", "e.g. Stanford University"),
("Degree", "degree", "e.g. B.S. Computer Science"),
("Start Date", "start_date", "e.g. Sep. 2020"),
("End Date", "end_date", "e.g. May 2024"),
("Location", "location", "e.g. Stanford, CA"),
]
for i, (label, key, ph) in enumerate(fields):
entry = ctk.CTkEntry(card, height=38, placeholder_text=ph, border_color=COLORS["border"])
self._form_row(card, label, entry, i)
self.edu_vars[key] = entry
ctk.CTkFrame(card, height=10, fg_color="transparent").grid(row=len(fields), column=0)
return frame
# ------------------------------------------------------------------ #
# COURSEWORK
# ------------------------------------------------------------------ #
def _create_coursework_section(self):
frame = ctk.CTkScrollableFrame(self.content, fg_color="transparent")
frame.grid_columnconfigure(0, weight=1)
self._section_header(frame, "Relevant Coursework",
"Highlight academic courses that match the role.")
card = self._make_card(frame)
card.grid(row=1, column=0, sticky="ew")
card.grid_columnconfigure(0, weight=1)
self.coursework = ctk.CTkTextbox(card, height=140, corner_radius=8,
border_width=1, border_color=COLORS["border"],
font=("Segoe UI", 13))
self.coursework.grid(row=0, column=0, sticky="ew", padx=20, pady=20)
ctk.CTkLabel(card, text="Separate courses with commas. They'll be auto-formatted.",
font=ctk.CTkFont(size=12), text_color=COLORS["text_muted"]).grid(
row=1, column=0, sticky="w", padx=20, pady=(0, 16))
return frame
# ------------------------------------------------------------------ #
# EXPERIENCE
# ------------------------------------------------------------------ #
def _create_experience_section(self):
frame = ctk.CTkScrollableFrame(self.content, fg_color="transparent")
frame.grid_columnconfigure(0, weight=1)
self._section_header(frame, "Professional Experience",
"Your work history — most recent first.")
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)
ctk.CTkButton(
frame, text="+ Add Position", command=self._add_experience_entry,
fg_color=COLORS["primary"], hover_color=COLORS["primary_hover"],
text_color="white", height=38, corner_radius=8,
font=ctk.CTkFont(size=13, weight="bold")
).grid(row=2, column=0, pady=20, sticky="w")
return frame
def _add_experience_entry(self):
idx = len(self.exp_entries)
card = self._make_card(self.exp_container, title=f"Position {idx + 1}")
card.grid(row=idx, column=0, sticky="ew", pady=(0, 12))
card.grid_columnconfigure(1, weight=1)
vars_dict = {}
fields = [
("Company", "company", "Company name"),
("Title", "title", "e.g. Software Engineer"),
("Start Date", "start_date", "e.g. Jun. 2023"),
("End Date", "end_date", "e.g. Present"),
("Location", "location", "e.g. Remote"),
]
for i, (label, key, ph) in enumerate(fields, start=1):
entry = ctk.CTkEntry(card, height=36, placeholder_text=ph, border_color=COLORS["border"])
self._form_row(card, label, entry, i)
vars_dict[key] = entry
ctk.CTkLabel(card, text="Responsibilities (one per line)",
font=ctk.CTkFont(size=12, weight="bold"),
text_color=COLORS["text_muted"]).grid(
row=len(fields)+1, column=0, sticky="w", padx=20, pady=(12, 6))
resp = ctk.CTkTextbox(card, height=120, corner_radius=8,
border_width=1, border_color=COLORS["border"])
resp.grid(row=len(fields)+2, column=0, columnspan=2, sticky="ew", padx=20)
ctk.CTkButton(
card, text="Remove", command=lambda c=card: self._remove_entry(c, self.exp_entries),
fg_color="transparent", border_width=1, border_color=COLORS["danger"],
text_color=COLORS["danger"], hover_color="#fee2e2",
height=32, corner_radius=8, width=90
).grid(row=len(fields)+3, column=0, columnspan=2, pady=(12, 16), sticky="w", padx=20)
self.exp_entries.append((vars_dict, resp, card))
# ------------------------------------------------------------------ #
# PROJECTS
# ------------------------------------------------------------------ #
def _create_projects_section(self):
frame = ctk.CTkScrollableFrame(self.content, fg_color="transparent")
frame.grid_columnconfigure(0, weight=1)
self._section_header(frame, "Projects",
"Showcase your side projects and personal work.")
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)
ctk.CTkButton(
frame, text="+ Add Project", command=self._add_project_entry,
fg_color=COLORS["primary"], hover_color=COLORS["primary_hover"],
text_color="white", height=38, corner_radius=8,
font=ctk.CTkFont(size=13, weight="bold")
).grid(row=2, column=0, pady=20, sticky="w")
return frame
def _add_project_entry(self):
idx = len(self.proj_entries)
card = self._make_card(self.proj_container, title=f"Project {idx + 1}")
card.grid(row=idx, column=0, sticky="ew", pady=(0, 12))
card.grid_columnconfigure(1, weight=1)
vars_dict = {}
fields = [
("Project Name", "name", "e.g. Task Manager App"),
("Technologies", "technologies", "e.g. React, Node.js, PostgreSQL"),
("Date", "date", "e.g. Summer 2023"),
]
for i, (label, key, ph) in enumerate(fields, start=1):
entry = ctk.CTkEntry(card, height=36, placeholder_text=ph, border_color=COLORS["border"])
self._form_row(card, label, entry, i)
vars_dict[key] = entry
ctk.CTkLabel(card, text="Description (one per line)",
font=ctk.CTkFont(size=12, weight="bold"),
text_color=COLORS["text_muted"]).grid(
row=len(fields)+1, column=0, sticky="w", padx=20, pady=(12, 6))
desc = ctk.CTkTextbox(card, height=100, corner_radius=8,
border_width=1, border_color=COLORS["border"])
desc.grid(row=len(fields)+2, column=0, columnspan=2, sticky="ew", padx=20)
ctk.CTkButton(
card, text="Remove", command=lambda c=card: self._remove_entry(c, self.proj_entries),
fg_color="transparent", border_width=1, border_color=COLORS["danger"],
text_color=COLORS["danger"], hover_color="#fee2e2",
height=32, corner_radius=8, width=90
).grid(row=len(fields)+3, column=0, columnspan=2, pady=(12, 16), sticky="w", padx=20)
self.proj_entries.append((vars_dict, desc, card))
# ------------------------------------------------------------------ #
# SKILLS
# ------------------------------------------------------------------ #
def _create_skills_section(self):
frame = ctk.CTkScrollableFrame(self.content, fg_color="transparent")
frame.grid_columnconfigure(0, weight=1)
self._section_header(frame, "Technical Skills",
"Languages, tools, and technologies you know.")
card = self._make_card(frame)
card.grid(row=1, column=0, sticky="ew")
card.grid_columnconfigure(1, weight=1)
self.skills_vars = {}
fields = [
("Languages", "languages", "e.g. Python, JavaScript, Go"),
("Developer Tools", "tools", "e.g. Git, Docker, VS Code"),
("Frameworks", "technologies", "e.g. React, Django, TensorFlow"),
]
for i, (label, key, ph) in enumerate(fields):
entry = ctk.CTkEntry(card, height=38, placeholder_text=ph, border_color=COLORS["border"])
self._form_row(card, label, entry, i)
self.skills_vars[key] = entry
ctk.CTkFrame(card, height=10, fg_color="transparent").grid(row=len(fields), column=0)
return frame
# ------------------------------------------------------------------ #
# LEADERSHIP
# ------------------------------------------------------------------ #
def _create_leadership_section(self):
frame = ctk.CTkScrollableFrame(self.content, fg_color="transparent")
frame.grid_columnconfigure(0, weight=1)
self._section_header(frame, "Leadership & Activities",
"Volunteer work, clubs, and community involvement.")
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)
ctk.CTkButton(
frame, text="+ Add Activity", command=self._add_leadership_entry,
fg_color=COLORS["primary"], hover_color=COLORS["primary_hover"],
text_color="white", height=38, corner_radius=8,
font=ctk.CTkFont(size=13, weight="bold")
).grid(row=2, column=0, pady=20, sticky="w")
return frame
def _add_leadership_entry(self):
idx = len(self.lead_entries)
card = self._make_card(self.lead_container, title=f"Activity {idx + 1}")
card.grid(row=idx, column=0, sticky="ew", pady=(0, 12))
card.grid_columnconfigure(1, weight=1)
vars_dict = {}
fields = [
("Organization", "organization", "e.g. ACM Student Chapter"),
("Role", "role", "e.g. President"),
("Date", "date", "e.g. 2022 — 2023"),
("Location", "location", "e.g. Campus"),
]
for i, (label, key, ph) in enumerate(fields, start=1):
entry = ctk.CTkEntry(card, height=36, placeholder_text=ph, border_color=COLORS["border"])
self._form_row(card, label, entry, i)
vars_dict[key] = entry
ctk.CTkLabel(card, text="Achievements (one per line)",
font=ctk.CTkFont(size=12, weight="bold"),
text_color=COLORS["text_muted"]).grid(
row=len(fields)+1, column=0, sticky="w", padx=20, pady=(12, 6))
ach = ctk.CTkTextbox(card, height=100, corner_radius=8,
border_width=1, border_color=COLORS["border"])
ach.grid(row=len(fields)+2, column=0, columnspan=2, sticky="ew", padx=20)
ctk.CTkButton(
card, text="Remove", command=lambda c=card: self._remove_entry(c, self.lead_entries),
fg_color="transparent", border_width=1, border_color=COLORS["danger"],
text_color=COLORS["danger"], hover_color="#fee2e2",
height=32, corner_radius=8, width=90
).grid(row=len(fields)+3, column=0, columnspan=2, pady=(12, 16), sticky="w", padx=20)
self.lead_entries.append((vars_dict, ach, card))
# ------------------------------------------------------------------ #
# OUTPUT (Side-by-side editor + preview)
# ------------------------------------------------------------------ #
def _create_output_section(self):
frame = ctk.CTkFrame(self.content, fg_color="transparent")
frame.grid_columnconfigure(0, weight=1)
frame.grid_rowconfigure(1, weight=1)
self._section_header(frame, "Preview & Export",
"Your LaTeX source alongside the compiled PDF.")
# Two-column layout
split = ctk.CTkFrame(frame, fg_color="transparent")
split.grid(row=1, column=0, sticky="nsew")
split.grid_columnconfigure((0, 1), weight=1)
split.grid_rowconfigure(0, weight=1)
# LaTeX editor card
editor_card = self._make_card(split)
editor_card.grid(row=0, column=0, sticky="nsew", padx=(0, 8))
editor_card.grid_columnconfigure(0, weight=1)
editor_card.grid_rowconfigure(1, weight=1)
ed_head = ctk.CTkFrame(editor_card, fg_color="transparent")
ed_head.grid(row=0, column=0, sticky="ew", padx=20, pady=(16, 10))
ed_head.grid_columnconfigure(0, weight=1)
ctk.CTkLabel(ed_head, text="LaTeX Source", font=ctk.CTkFont(size=14, weight="bold"),
text_color=COLORS["text"]).grid(row=0, column=0, sticky="w")
ed_actions = ctk.CTkFrame(ed_head, fg_color="transparent")
ed_actions.grid(row=0, column=1, sticky="e")
ctk.CTkButton(ed_actions, text="Save", command=self._save_tex,
fg_color="transparent", border_width=1, border_color=COLORS["border"],
text_color=COLORS["text"], hover_color=COLORS["surface_alt"],
height=28, corner_radius=6, width=60,
font=ctk.CTkFont(size=11, weight="bold")).pack(side="left", padx=4)
ctk.CTkButton(ed_actions, text="Copy", command=self._copy_latex,
fg_color="transparent", border_width=1, border_color=COLORS["border"],
text_color=COLORS["text"], hover_color=COLORS["surface_alt"],
height=28, corner_radius=6, width=60,
font=ctk.CTkFont(size=11, weight="bold")).pack(side="left")
self.latex_editor = ctk.CTkTextbox(
editor_card, corner_radius=8, font=("Consolas", 11),
border_width=1, border_color=COLORS["border"], fg_color="#fafafa"
)
self.latex_editor.grid(row=1, column=0, sticky="nsew", padx=20, pady=(0, 20))
# Preview card
preview_card = self._make_card(split)
preview_card.grid(row=0, column=1, sticky="nsew", padx=(8, 0))
preview_card.grid_columnconfigure(0, weight=1)
preview_card.grid_rowconfigure(1, weight=1)
pv_head = ctk.CTkFrame(preview_card, fg_color="transparent")
pv_head.grid(row=0, column=0, sticky="ew", padx=20, pady=(16, 10))
ctk.CTkLabel(pv_head, text="PDF Preview", font=ctk.CTkFont(size=14, weight="bold"),
text_color=COLORS["text"]).pack(side="left")
self.pdf_viewer = ctk.CTkScrollableFrame(preview_card, corner_radius=8,
border_width=1, border_color=COLORS["border"],
fg_color=COLORS["surface_alt"])
self.pdf_viewer.grid(row=1, column=0, sticky="nsew", padx=20, pady=(0, 20))
self.placeholder = ctk.CTkLabel(
self.pdf_viewer,
text="Click 'Generate' then 'Compile PDF'\nto preview your resume here.",
font=ctk.CTkFont(size=13), text_color=COLORS["text_muted"],
justify="center"
)
self.placeholder.pack(expand=True, pady=60)
return frame
# ------------------------------------------------------------------ #
# SHARED UTILITIES
# ------------------------------------------------------------------ #
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, key):
if self.current_tab and self.current_tab in self.section_frames:
self.section_frames[self.current_tab].grid_remove()
self.current_tab = key
self.section_frames[key].grid(row=0, column=0, sticky="nsew")
for k, btn in self.nav_buttons.items():
if k == key:
btn.configure(fg_color=COLORS["primary"], text_color="white",
hover_color=COLORS["primary_hover"])
else:
btn.configure(fg_color="transparent", text_color=COLORS["text_muted"],
hover_color=COLORS["surface_alt"])
def _set_status(self, text, color=None):
color = color or COLORS["accent"]
self.status_pill.configure(text=f" {text} ", text_color=color)
# ------------------------------------------------------------------ #
# LATEX GENERATION
# ------------------------------------------------------------------ #
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")
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=0.75in}
\setlist[itemize]{leftmargin=*, nosep, topsep=2pt, itemsep=2pt}
\pagestyle{empty}
\definecolor{primary}{RGB}{99,102,241}
\definecolor{secondary}{RGB}{107,114,128}
\hypersetup{colorlinks=true, linkcolor=primary, urlcolor=primary}
\begin{document}
"""
first = self.first_name.get().strip()
last = self.last_name.get().strip()
latex += f"\n\\begin{{center}}\n\\textbf{{\\Huge \\textcolor{{primary}}{{{first} {last}}}}}\\\\[4pt]\n"
contacts = []
for key in ['address', 'phone', 'email', 'linkedin', 'github']:
v = self.personal_vars[key].get().strip()
if not v: continue
if key == 'phone': contacts.append(f"\\href{{tel:{v}}}{{{v}}}")
elif key == 'email': contacts.append(f"\\href{{mailto:{v}}}{{{v}}}")
elif key == 'linkedin': contacts.append(f"\\href{{https://linkedin.com/in/{v}}}{{linkedin.com/in/{v}}}")
elif key == 'github': contacts.append(f"\\href{{https://github.com/{v}}}{{github.com/{v}}}")
else: contacts.append(v)
latex += " \\textcolor{secondary}{$\\cdot$} ".join(contacts) + "\n\\end{center}\n\n\\vspace{4pt}\n\\hrule\n\\vspace{6pt}\n\n"
if self.edu_vars['university'].get().strip():
latex += "\\section*{\\textcolor{primary}{Education}}\n"
uni = self.edu_vars['university'].get().strip()
deg = self.edu_vars['degree'].get().strip()
s = self.edu_vars['start_date'].get().strip()
e = self.edu_vars['end_date'].get().strip()
loc = self.edu_vars['location'].get().strip()
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 += "\\\\\n"
latex += f"\\textit{{{deg}}}"
if loc: latex += f" \\textcolor{{secondary}}{{$\\cdot$}} {loc}"
latex += "\n\n"
cw = self.coursework.get("1.0", tk.END).strip()
if cw:
latex += "\\section*{\\textcolor{primary}{Relevant Coursework}}\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"
if any(v['company'].get().strip() for v, _, _ in self.exp_entries):
latex += "\\section*{\\textcolor{primary}{Experience}}\n"
for v, t, _ in self.exp_entries:
comp = v['company'].get().strip()
if not comp: continue
title = v['title'].get().strip()
s = v['start_date'].get().strip()
e = v['end_date'].get().strip()
loc = v['location'].get().strip()
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 += "\\\\\n"
latex += f"\\textit{{{title}}}"
if loc: latex += f" \\textcolor{{secondary}}{{$\\cdot$}} {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"
if any(v['name'].get().strip() for v, _, _ in self.proj_entries):
latex += "\\section*{\\textcolor{primary}{Projects}}\n"
for v, t, _ in self.proj_entries:
name = v['name'].get().strip()
if not name: continue
tech = v['technologies'].get().strip()
date = v['date'].get().strip()
if tech: latex += f"\\noindent\\textbf{{{name}}} \\textcolor{{secondary}}{{|}} \\textit{{{tech}}}"
else: latex += f"\\noindent\\textbf{{{name}}}"
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"
has_skill = any(self.skills_vars[k].get().strip() for k in ['languages', 'tools', 'technologies'])
if has_skill:
latex += "\\section*{\\textcolor{primary}{Technical Skills}}\n"
for key, label in [('languages','Languages'), ('tools','Developer Tools'), ('technologies','Frameworks')]:
v = self.skills_vars[key].get().strip()
if v: latex += f"\\noindent\\textbf{{{label}:}} {v}\\\\\n"
latex += "\n"
if any(v['organization'].get().strip() for v, _, _ in self.lead_entries):
latex += "\\section*{\\textcolor{primary}{Leadership \\& Activities}}\n"
for v, t, _ in self.lead_entries:
org = v['organization'].get().strip()
if not org: continue
role = v['role'].get().strip()
date = v['date'].get().strip()
loc = v['location'].get().strip()
latex += f"\\noindent\\textbf{{{org}}}"
if date: latex += f"\\hfill \\textcolor{{secondary}}{{{date}}}"
latex += "\\\\\n"
latex += f"\\textit{{{role}}}"
if loc: latex += f" \\textcolor{{secondary}}{{$\\cdot$}} {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
# ------------------------------------------------------------------ #
# IMPORT
# ------------------------------------------------------------------ #
def _import_latex(self):
path = filedialog.askopenfilename(title="Import LaTeX CV",
filetypes=[("LaTeX", "*.tex"), ("All", "*.*")])
if not path: return
try:
with open(path, "r", encoding="utf-8") as f:
content = f.read()
self._parse_and_populate(content)
self._set_status("Imported from file")
except Exception as e:
messagebox.showerror("Import Error", f"Could not parse file:\n{e}")
def _parse_and_populate(self, text):
name_m = re.search(r'\\textbf\{\\Huge \\textcolor\{primary\}\{(.*?)\}\}', text)
if name_m:
parts = name_m.group(1).split(None, 1)
self.first_name.delete(0, tk.END)
self.last_name.delete(0, tk.END)
self.first_name.insert(0, parts[0] if parts else "")
self.last_name.insert(0, parts[1] if len(parts) > 1 else "")
contact_m = re.search(r'\\end\{center\}(.*?)\\hrule', text, re.DOTALL)
if contact_m:
block = contact_m.group(1)
email = re.search(r'([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)', block)
if email:
self.personal_vars['email'].delete(0, tk.END)
self.personal_vars['email'].insert(0, email.group(1))
phone = re.search(r'(\d{3}[-.\s]?\d{3}[-.\s]?\d{4})', block)
if phone:
self.personal_vars['phone'].delete(0, tk.END)
self.personal_vars['phone'].insert(0, phone.group(1))
edu_m = re.search(r'\\section\*\{.*?Education\}.*?\\textbf\{(.*?)\}.*?\\textit\{(.*?)\}', text, re.DOTALL)
if edu_m:
self.edu_vars['university'].delete(0, tk.END)
self.edu_vars['university'].insert(0, edu_m.group(1))
self.edu_vars['degree'].delete(0, tk.END)
self.edu_vars['degree'].insert(0, edu_m.group(2))
cw_m = re.search(r'\\section\*\{.*?Coursework\}.*?\\begin\{itemize\}(.*?)\\end\{itemize\}', text, re.DOTALL)
if cw_m:
self.coursework.delete("1.0", tk.END)
clean = cw_m.group(1).replace('\\item', '').replace('\\quad $\\bullet$', ',').replace('\n', '').strip()
self.coursework.insert("1.0", clean)
# ------------------------------------------------------------------ #
# COMPILE & PDF
# ------------------------------------------------------------------ #
def _compile_latex(self):
if not shutil.which("pdflatex"):
messagebox.showerror("LaTeX Not Found",
"pdflatex is not on your PATH.\nInstall 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...", COLORS["primary"])
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("Compiled successfully")
self._render_pdf(pdf_path)
else:
self._set_status("Compilation failed", COLORS["danger"])
messagebox.showerror("Compile Error", "Check your LaTeX syntax.")
except Exception as e:
self._set_status("Error", COLORS["danger"])
messagebox.showerror("Error", str(e))
finally:
self.compile_btn.configure(state="normal")
def _render_pdf(self, pdf_path):
if not PDF_AVAILABLE:
ctk.CTkLabel(self.pdf_viewer,
text="Install PyMuPDF to enable PDF preview.",
text_color=COLORS["danger"]).pack(pady=30)
return
for w in self.pdf_viewer.winfo_children(): w.destroy()
self.pdf_image_refs.clear()
try:
doc = fitz.open(pdf_path)
for pg in range(len(doc)):
pix = doc.load_page(pg).get_pixmap(matrix=fitz.Matrix(1.8, 1.8))
img = Image.open(io.BytesIO(pix.tobytes("ppm")))
photo = ImageTk.PhotoImage(img)
self.pdf_image_refs.append(photo)
lbl = ctk.CTkLabel(self.pdf_viewer, image=photo, text="")
lbl.pack(pady=10)
shadow = ctk.CTkFrame(self.pdf_viewer, height=1, fg_color=COLORS["border"])
shadow.pack(fill="x", padx=20)
doc.close()
except Exception as e:
ctk.CTkLabel(self.pdf_viewer, text=f"Render error: {e}",
text_color=COLORS["danger"]).pack(pady=20)
# ------------------------------------------------------------------ #
# FILE OPERATIONS
# ------------------------------------------------------------------ #
def _save_tex(self):
path = filedialog.asksaveasfilename(defaultextension=".tex",
filetypes=[("LaTeX", "*.tex")],
initialfile="resume.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")
# ------------------------------------------------------------------ #
# SAMPLE DATA
# ------------------------------------------------------------------ #
def _populate_sample_data(self):
self.first_name.insert(0, "Jane")
self.last_name.insert(0, "Doe")
self.personal_vars['address'].insert(0, "San Francisco, CA")
self.personal_vars['phone'].insert(0, "555-123-4567")
self.personal_vars['email'].insert(0, "jane.doe@example.com")
self.personal_vars['linkedin'].insert(0, "janedoe")
self.personal_vars['github'].insert(0, "janedoe")
self.edu_vars['university'].insert(0, "Stanford University")
self.edu_vars['degree'].insert(0, "B.S. Computer Science, GPA 3.9")
self.edu_vars['start_date'].insert(0, "Sep. 2020")
self.edu_vars['end_date'].insert(0, "Jun. 2024")
self.edu_vars['location'].insert(0, "Stanford, CA")
self.coursework.insert("1.0",
"Data Structures, Algorithms, Distributed Systems, "
"Machine Learning, Operating Systems, Computer Networks, "
"Database Systems, Software Engineering")
def main():
app = CVBuilderApp()
app.mainloop()
if __name__ == "__main__":
main()