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()