Attempt 2 --> Promising

This commit is contained in:
bobbert committed 2026-05-26 18:08:00 +01:00
1 parent d7f33b7cef
commit a040efe56c
2 files changed
+1708

No files matched your search

+909
View File
@@ -0,0 +1,909 @@
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()