mirror of
https://github.com/mudabbir-ahmad/Latex-CV-Builder.git
synced 2026-10-07 18:10:20 +00:00
Add CVBuilder application with UI and LaTeX generation functionality
This commit is contained in:
1 parent
1906b070c0
commit
d7f33b7cef
2 files changed
+625
-1
No files matched your search
+618
@@ -0,0 +1,618 @@
|
||||
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()
|
||||
+7
-1
@@ -2,4 +2,10 @@
|
||||
name = "latex-cv-builder"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
"customtkinter>=5.2.2",
|
||||
"fitz>=0.0.1.dev2",
|
||||
"frontend>=0.0.3",
|
||||
"pillow>=12.2.0",
|
||||
"pymupdf>=1.27.2.3",
|
||||
]
|
||||
Reference in new issue
Block a user