diff --git a/cv-fancy.py b/cv-fancy.py new file mode 100644 index 0000000..21ebf35 --- /dev/null +++ b/cv-fancy.py @@ -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() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 140f6b1..5e28908 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", +]