mirror of
https://github.com/mudabbir-ahmad/Latex-CV-Builder.git
synced 2026-10-07 18:10:20 +00:00
Attempt 2 --> Promising
This commit is contained in:
1 parent
d7f33b7cef
commit
a040efe56c
2 files changed
+1708
No files matched your search
+799
@@ -0,0 +1,799 @@
|
||||
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("system") # Automatically matches system light/dark mode
|
||||
ctk.set_default_color_theme("blue")
|
||||
|
||||
# Design System Color Palette (Light, Dark)
|
||||
COLOR_BG_MAIN = ("#f8fafc", "#0f172a") # Slate 50, Slate 900
|
||||
COLOR_BG_SIDEBAR = ("#f1f5f9", "#1e293b") # Slate 100, Slate 800
|
||||
COLOR_BG_CARD = ("#ffffff", "#0f172a") # White, Slate 900
|
||||
COLOR_BORDER = ("#cbd5e1", "#334155") # Slate 300, Slate 700
|
||||
COLOR_TEXT_PRIMARY = ("#0f172a", "#f8fafc") # Slate 900, Slate 50
|
||||
COLOR_TEXT_MUTED = ("#64748b", "#94a3b8") # Slate 500, Slate 400
|
||||
|
||||
|
||||
class CVBuilderApp(ctk.CTk):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title("Professional CV Builder (LaTeX Core)")
|
||||
self.geometry("1400x900")
|
||||
self.minsize(1200, 800)
|
||||
self.configure(fg_color=COLOR_BG_MAIN)
|
||||
|
||||
# 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):
|
||||
"""Clean navigation sidebar with modern hierarchy"""
|
||||
self.sidebar = ctk.CTkFrame(self, width=260, corner_radius=0, fg_color=COLOR_BG_SIDEBAR)
|
||||
self.sidebar.grid(row=0, column=0, sticky="nsew")
|
||||
self.sidebar.grid_rowconfigure(9, weight=1) # Space out navigation from action buttons
|
||||
|
||||
# Branding
|
||||
brand_frame = ctk.CTkFrame(self.sidebar, fg_color="transparent")
|
||||
brand_frame.grid(row=0, column=0, padx=20, pady=(30, 20), sticky="ew")
|
||||
|
||||
ctk.CTkLabel(
|
||||
brand_frame,
|
||||
text="LaTeX CV Builder",
|
||||
font=ctk.CTkFont(family="Inter", size=20, weight="bold"),
|
||||
text_color=COLOR_TEXT_PRIMARY
|
||||
).pack(anchor="w")
|
||||
|
||||
ctk.CTkLabel(
|
||||
brand_frame,
|
||||
text="High-Fidelity Document Engine",
|
||||
font=ctk.CTkFont(family="Inter", size=11, weight="normal"),
|
||||
text_color=COLOR_TEXT_MUTED
|
||||
).pack(anchor="w", pady=(2, 0))
|
||||
|
||||
# Navigation Menu Items
|
||||
nav_items = [
|
||||
("personal", "Personal Information"),
|
||||
("education", "Academic Education"),
|
||||
("coursework", "Relevant Coursework"),
|
||||
("experience", "Work Experience"),
|
||||
("projects", "Technical Projects"),
|
||||
("skills", "Core Skills Matrix"),
|
||||
("leadership", "Leadership & Activities"),
|
||||
("output", "LaTeX & Live Preview")
|
||||
]
|
||||
|
||||
self.nav_buttons = {}
|
||||
for idx, (key, label) in enumerate(nav_items, start=1):
|
||||
btn = ctk.CTkButton(
|
||||
self.sidebar, text=label, command=lambda k=key: self._navigate_to(k),
|
||||
fg_color="transparent", text_color=COLOR_TEXT_PRIMARY,
|
||||
hover_color=("white", "gray10"), height=38, corner_radius=6, anchor="w",
|
||||
font=ctk.CTkFont(family="Inter", size=13, weight="normal")
|
||||
)
|
||||
btn.grid(row=idx, column=0, padx=12, pady=3, sticky="ew")
|
||||
self.nav_buttons[key] = btn
|
||||
|
||||
# Actions block at bottom of sidebar
|
||||
actions_frame = ctk.CTkFrame(self.sidebar, fg_color="transparent")
|
||||
actions_frame.grid(row=9, column=0, padx=12, pady=(15, 5), sticky="ew")
|
||||
actions_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.import_btn = ctk.CTkButton(
|
||||
actions_frame, text="Import LaTeX File", command=self._import_latex,
|
||||
fg_color=("#64748b", "#475569"), hover_color=("#475569", "#334155"),
|
||||
text_color="white", height=36, corner_radius=6,
|
||||
font=ctk.CTkFont(family="Inter", size=12, weight="bold")
|
||||
)
|
||||
self.import_btn.grid(row=0, column=0, pady=4, sticky="ew")
|
||||
|
||||
self.generate_btn = ctk.CTkButton(
|
||||
actions_frame, text="Generate LaTeX Source", command=self._generate_latex,
|
||||
fg_color=("#2563eb", "#3b82f6"), hover_color=("#1d4ed8", "#2563eb"),
|
||||
text_color="white", height=38, corner_radius=6,
|
||||
font=ctk.CTkFont(family="Inter", size=12, weight="bold")
|
||||
)
|
||||
self.generate_btn.grid(row=1, column=0, pady=4, sticky="ew")
|
||||
|
||||
self.compile_btn = ctk.CTkButton(
|
||||
actions_frame, text="Compile Live PDF", command=self._compile_latex,
|
||||
fg_color=("#16a34a", "#22c55e"), hover_color=("#15803d", "#16a34a"),
|
||||
text_color="white", height=38, corner_radius=6,
|
||||
font=ctk.CTkFont(family="Inter", size=12, weight="bold"), state="disabled"
|
||||
)
|
||||
self.compile_btn.grid(row=2, column=0, pady=(4, 15), sticky="ew")
|
||||
|
||||
# Status Bar Indicator
|
||||
self.status_label = ctk.CTkLabel(
|
||||
self.sidebar, text="Ready", text_color=COLOR_TEXT_MUTED,
|
||||
font=ctk.CTkFont(family="Inter", size=11, weight="normal")
|
||||
)
|
||||
self.status_label.grid(row=10, column=0, padx=15, pady=(0, 20), sticky="w")
|
||||
|
||||
def _build_main_area(self):
|
||||
"""Content area panel containing form card views"""
|
||||
self.main_container = ctk.CTkFrame(self, corner_radius=0, fg_color="transparent")
|
||||
self.main_container.grid(row=0, column=1, sticky="nsew", padx=30, pady=30)
|
||||
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, subtitle=None):
|
||||
header = ctk.CTkFrame(parent, fg_color="transparent")
|
||||
header.grid(row=0, column=0, sticky="ew", pady=(0, 20))
|
||||
ctk.CTkLabel(
|
||||
header, text=title, font=ctk.CTkFont(family="Inter", size=24, weight="bold"),
|
||||
text_color=COLOR_TEXT_PRIMARY
|
||||
).pack(anchor="w")
|
||||
if subtitle:
|
||||
ctk.CTkLabel(
|
||||
header, text=subtitle, font=ctk.CTkFont(family="Inter", size=12, weight="normal"),
|
||||
text_color=COLOR_TEXT_MUTED
|
||||
).pack(anchor="w", pady=(2, 0))
|
||||
return header
|
||||
|
||||
def _create_form_field(self, parent, label_text, row, col, columnspan=1, height=36, is_textbox=False):
|
||||
"""Helper function to keep form structures consistent, elegant, and perfectly structured"""
|
||||
field_container = ctk.CTkFrame(parent, fg_color="transparent")
|
||||
field_container.grid(row=row, column=col, columnspan=columnspan, sticky="ew", padx=10, pady=10)
|
||||
field_container.grid_columnconfigure(0, weight=1)
|
||||
|
||||
ctk.CTkLabel(
|
||||
field_container, text=label_text,
|
||||
font=ctk.CTkFont(family="Inter", size=12, weight="bold"),
|
||||
text_color=COLOR_TEXT_PRIMARY
|
||||
).grid(row=0, column=0, sticky="w", pady=(0, 5))
|
||||
|
||||
if is_textbox:
|
||||
field = ctk.CTkTextbox(
|
||||
field_container, height=height, corner_radius=6,
|
||||
border_width=1, border_color=COLOR_BORDER, fg_color=COLOR_BG_MAIN
|
||||
)
|
||||
else:
|
||||
field = ctk.CTkEntry(
|
||||
field_container, height=height, corner_radius=6,
|
||||
border_width=1, border_color=COLOR_BORDER, fg_color=COLOR_BG_MAIN
|
||||
)
|
||||
field.grid(row=1, column=0, sticky="ew")
|
||||
return field
|
||||
|
||||
def _create_personal_section(self):
|
||||
frame = ctk.CTkScrollableFrame(self.main_container, corner_radius=12, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
self._create_section_header(frame, "Personal Information",
|
||||
"Define your key contact details and online portfolios.")
|
||||
|
||||
card = ctk.CTkFrame(frame, corner_radius=10, fg_color=COLOR_BG_CARD, border_width=1, border_color=COLOR_BORDER)
|
||||
card.grid(row=1, column=0, sticky="ew", padx=5, pady=5)
|
||||
card.grid_columnconfigure((0, 1), weight=1)
|
||||
|
||||
self.first_name = self._create_form_field(card, "First Name", 0, 0)
|
||||
self.last_name = self._create_form_field(card, "Last Name", 0, 1)
|
||||
|
||||
self.personal_vars = {
|
||||
'address': self._create_form_field(card, "Mailing / Primary Address", 1, 0, columnspan=2),
|
||||
'phone': self._create_form_field(card, "Phone Number", 2, 0),
|
||||
'email': self._create_form_field(card, "Email Address", 2, 1),
|
||||
'linkedin': self._create_form_field(card, "LinkedIn Username", 3, 0),
|
||||
'github': self._create_form_field(card, "GitHub Username", 3, 1)
|
||||
}
|
||||
return frame
|
||||
|
||||
def _create_education_section(self):
|
||||
frame = ctk.CTkScrollableFrame(self.main_container, corner_radius=12, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
self._create_section_header(frame, "Education",
|
||||
"Enter your higher education details, locations, and degree pathways.")
|
||||
|
||||
card = ctk.CTkFrame(frame, corner_radius=10, fg_color=COLOR_BG_CARD, border_width=1, border_color=COLOR_BORDER)
|
||||
card.grid(row=1, column=0, sticky="ew", padx=5, pady=5)
|
||||
card.grid_columnconfigure((0, 1), weight=1)
|
||||
|
||||
self.edu_vars = {
|
||||
'university': self._create_form_field(card, "University / College Name", 0, 0, columnspan=2),
|
||||
'degree': self._create_form_field(card, "Degree Major / Qualification", 1, 0, columnspan=2),
|
||||
'start_date': self._create_form_field(card, "Start Date (e.g. Sep. 2017)", 2, 0),
|
||||
'end_date': self._create_form_field(card, "Graduation Date (e.g. May 2021)", 2, 1),
|
||||
'location': self._create_form_field(card, "Institution Location (City, State)", 3, 0, columnspan=2)
|
||||
}
|
||||
return frame
|
||||
|
||||
def _create_coursework_section(self):
|
||||
frame = ctk.CTkScrollableFrame(self.main_container, corner_radius=12, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
self._create_section_header(frame, "Relevant Coursework",
|
||||
"Highlight relevant classes, certificates, and specialized course nodes.")
|
||||
|
||||
card = ctk.CTkFrame(frame, corner_radius=10, fg_color=COLOR_BG_CARD, border_width=1, border_color=COLOR_BORDER)
|
||||
card.grid(row=1, column=0, sticky="ew", padx=5, pady=5)
|
||||
card.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.coursework = self._create_form_field(card, "Enter Courses (Separated strictly by commas)", 0, 0,
|
||||
is_textbox=True, height=180)
|
||||
return frame
|
||||
|
||||
def _create_experience_section(self):
|
||||
frame = ctk.CTkScrollableFrame(self.main_container, corner_radius=12, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
self._create_section_header(frame, "Professional Experience",
|
||||
"Outline your historical career track, positions, and achievements.")
|
||||
|
||||
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 Card", command=self._add_experience_entry,
|
||||
fg_color=("#2563eb", "#3b82f6"), hover_color=("#1d4ed8", "#2563eb"),
|
||||
height=36, corner_radius=6, font=ctk.CTkFont(family="Inter", size=12, weight="bold")
|
||||
).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, fg_color=COLOR_BG_CARD, border_width=1,
|
||||
border_color=COLOR_BORDER)
|
||||
card.grid(row=idx, column=0, sticky="ew", pady=10, padx=5)
|
||||
card.grid_columnconfigure((0, 1), weight=1)
|
||||
|
||||
ctk.CTkLabel(
|
||||
card, text=f"Work Experience Block #{idx + 1}",
|
||||
font=ctk.CTkFont(family="Inter", size=14, weight="bold"),
|
||||
text_color=COLOR_TEXT_PRIMARY
|
||||
).grid(row=0, column=0, columnspan=2, padx=15, pady=(15, 5), sticky="w")
|
||||
|
||||
vars_dict = {
|
||||
'company': self._create_form_field(card, "Company / Employer Name", 1, 0),
|
||||
'title': self._create_form_field(card, "Job Title / Designation", 1, 1),
|
||||
'start_date': self._create_form_field(card, "Start Date (e.g., Jun. 2019)", 2, 0),
|
||||
'end_date': self._create_form_field(card, "End Date (or 'Present')", 2, 1),
|
||||
'location': self._create_form_field(card, "Work Location", 3, 0, columnspan=2)
|
||||
}
|
||||
|
||||
resp_text = self._create_form_field(card, "Job Responsibilities / Impacts (One point per line)", 4, 0,
|
||||
columnspan=2, is_textbox=True, height=120)
|
||||
|
||||
ctk.CTkButton(
|
||||
card, text="Delete Position", command=lambda c=card: self._remove_entry(c, self.exp_entries),
|
||||
fg_color=("#ef4444", "#dc2626"), hover_color=("#dc2626", "#b91c1c"),
|
||||
width=120, height=32, corner_radius=6, font=ctk.CTkFont(family="Inter", size=12, weight="bold")
|
||||
).grid(row=5, column=1, pady=(0, 15), padx=15, sticky="e")
|
||||
|
||||
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="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
self._create_section_header(frame, "Technical Projects",
|
||||
"Detail non-trivial software engineering or personal dev 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 Node", command=self._add_project_entry,
|
||||
fg_color=("#2563eb", "#3b82f6"), hover_color=("#1d4ed8", "#2563eb"),
|
||||
height=36, corner_radius=6, font=ctk.CTkFont(family="Inter", size=12, weight="bold")
|
||||
).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, fg_color=COLOR_BG_CARD, border_width=1,
|
||||
border_color=COLOR_BORDER)
|
||||
card.grid(row=idx, column=0, sticky="ew", pady=10, padx=5)
|
||||
card.grid_columnconfigure((0, 1), weight=1)
|
||||
|
||||
ctk.CTkLabel(
|
||||
card, text=f"Project Workspace #{idx + 1}",
|
||||
font=ctk.CTkFont(family="Inter", size=14, weight="bold"),
|
||||
text_color=COLOR_TEXT_PRIMARY
|
||||
).grid(row=0, column=0, columnspan=2, padx=15, pady=(15, 5), sticky="w")
|
||||
|
||||
vars_dict = {
|
||||
'name': self._create_form_field(card, "Project Name", 1, 0, columnspan=2),
|
||||
'technologies': self._create_form_field(card, "Core Tech Stack Used (e.g. React, C++, Docker)", 2, 0),
|
||||
'date': self._create_form_field(card, "Timeline Date (e.g., Fall 2021)", 2, 1)
|
||||
}
|
||||
|
||||
desc_text = self._create_form_field(card, "Project Deliverables / Features (One bullet point per line)", 3, 0,
|
||||
columnspan=2, is_textbox=True, height=120)
|
||||
|
||||
ctk.CTkButton(
|
||||
card, text="Delete Project", command=lambda c=card: self._remove_entry(c, self.proj_entries),
|
||||
fg_color=("#ef4444", "#dc2626"), hover_color=("#dc2626", "#b91c1c"),
|
||||
width=120, height=32, corner_radius=6, font=ctk.CTkFont(family="Inter", size=12, weight="bold")
|
||||
).grid(row=4, column=1, pady=(0, 15), padx=15, sticky="e")
|
||||
|
||||
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="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
self._create_section_header(frame, "Core Skills Matrix",
|
||||
"Index your technical qualifications so they are easily scanable.")
|
||||
|
||||
card = ctk.CTkFrame(frame, corner_radius=10, fg_color=COLOR_BG_CARD, border_width=1, border_color=COLOR_BORDER)
|
||||
card.grid(row=1, column=0, sticky="ew", padx=5, pady=5)
|
||||
card.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.skills_vars = {
|
||||
'languages': self._create_form_field(card, "Coding / Query Languages (e.g., Python, SQL, C++)", 0, 0,
|
||||
is_textbox=True, height=70),
|
||||
'tools': self._create_form_field(card, "Developer Platforms & DevOps (e.g., Git, Docker, AWS)", 1, 0,
|
||||
is_textbox=True, height=70),
|
||||
'technologies': self._create_form_field(card, "Libraries / Architecture (e.g., React.js, PyTorch, Node.js)",
|
||||
2, 0, is_textbox=True, height=70)
|
||||
}
|
||||
return frame
|
||||
|
||||
def _create_leadership_section(self):
|
||||
frame = ctk.CTkScrollableFrame(self.main_container, corner_radius=12, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
self._create_section_header(frame, "Leadership & Extracurriculars",
|
||||
"List university clubs, hackathons organized, or non-profit volunteer work.")
|
||||
|
||||
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 Block", command=self._add_leadership_entry,
|
||||
fg_color=("#2563eb", "#3b82f6"), hover_color=("#1d4ed8", "#2563eb"),
|
||||
height=36, corner_radius=6, font=ctk.CTkFont(family="Inter", size=12, weight="bold")
|
||||
).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, fg_color=COLOR_BG_CARD, border_width=1,
|
||||
border_color=COLOR_BORDER)
|
||||
card.grid(row=idx, column=0, sticky="ew", pady=10, padx=5)
|
||||
card.grid_columnconfigure((0, 1), weight=1)
|
||||
|
||||
ctk.CTkLabel(
|
||||
card, text=f"Activity Workspace #{idx + 1}",
|
||||
font=ctk.CTkFont(family="Inter", size=14, weight="bold"),
|
||||
text_color=COLOR_TEXT_PRIMARY
|
||||
).grid(row=0, column=0, columnspan=2, padx=15, pady=(15, 5), sticky="w")
|
||||
|
||||
vars_dict = {
|
||||
'organization': self._create_form_field(card, "Organization / Project Initiative", 1, 0),
|
||||
'role': self._create_form_field(card, "Your Role / Position", 1, 1),
|
||||
'date': self._create_form_field(card, "Timeline Date", 2, 0),
|
||||
'location': self._create_form_field(card, "Location", 2, 1)
|
||||
}
|
||||
|
||||
achieve_text = self._create_form_field(card, "Core Accomplishments / Impacts (One point per line)", 3, 0,
|
||||
columnspan=2, is_textbox=True, height=120)
|
||||
|
||||
ctk.CTkButton(
|
||||
card, text="Delete Activity", command=lambda c=card: self._remove_entry(c, self.lead_entries),
|
||||
fg_color=("#ef4444", "#dc2626"), hover_color=("#dc2626", "#b91c1c"),
|
||||
width=120, height=32, corner_radius=6, font=ctk.CTkFont(family="Inter", size=12, weight="bold")
|
||||
).grid(row=4, column=1, pady=(0, 15), padx=15, sticky="e")
|
||||
|
||||
self.lead_entries.append((vars_dict, achieve_text, card))
|
||||
|
||||
def _create_output_section(self):
|
||||
"""High-fidelity dual column workspace design (IDE source layout and live PDF rendering view)"""
|
||||
frame = ctk.CTkFrame(self.main_container, corner_radius=12, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
frame.grid_rowconfigure(1, weight=1)
|
||||
self._create_section_header(frame, "Compilation Workspace",
|
||||
"Review raw macro-injected LaTeX outputs and track compiled system states.")
|
||||
|
||||
# Paned Window layout splits IDE view cleanly
|
||||
paned = tk.PanedWindow(frame, orient=tk.HORIZONTAL, bg="#1e1e24", sashrelief="flat", sashwidth=6)
|
||||
paned.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
|
||||
|
||||
# Code block terminal style editor (Left)
|
||||
code_frame = ctk.CTkFrame(paned, corner_radius=8, fg_color=COLOR_BG_SIDEBAR)
|
||||
ctk.CTkLabel(
|
||||
code_frame, text="LaTeX Source Engine Output",
|
||||
font=ctk.CTkFont(family="Inter", size=13, weight="bold"), text_color=("#2563eb", "#3b82f6")
|
||||
).pack(padx=15, pady=(12, 6), anchor="w")
|
||||
|
||||
self.latex_editor = ctk.CTkTextbox(
|
||||
code_frame, corner_radius=6, font=("Consolas", 12),
|
||||
fg_color=COLOR_BG_MAIN, border_width=1, border_color=COLOR_BORDER
|
||||
)
|
||||
self.latex_editor.pack(fill="both", expand=True, padx=12, pady=(0, 12))
|
||||
paned.add(code_frame, width=500)
|
||||
|
||||
# Live vector asset viewport (Right)
|
||||
viewer_frame = ctk.CTkFrame(paned, corner_radius=8, fg_color=COLOR_BG_SIDEBAR)
|
||||
ctk.CTkLabel(
|
||||
viewer_frame, text="Live Compiled PDF Display",
|
||||
font=ctk.CTkFont(family="Inter", size=13, weight="bold"), text_color=("#16a34a", "#22c55e")
|
||||
).pack(padx=15, pady=(12, 6), anchor="w")
|
||||
|
||||
self.pdf_canvas_frame = ctk.CTkScrollableFrame(viewer_frame, corner_radius=6, fg_color=COLOR_BG_MAIN)
|
||||
self.pdf_canvas_frame.pack(fill="both", expand=True, padx=12, pady=(0, 12))
|
||||
|
||||
self.placeholder_label = ctk.CTkLabel(
|
||||
self.pdf_canvas_frame, text="Generate source and run 'Compile PDF' execution pipeline.",
|
||||
font=ctk.CTkFont(family="Inter", size=13, weight="normal"), text_color=COLOR_TEXT_MUTED
|
||||
)
|
||||
self.placeholder_label.pack(expand=True, pady=100)
|
||||
paned.add(viewer_frame)
|
||||
|
||||
# File export actions panel
|
||||
btn_frame = ctk.CTkFrame(frame, fg_color="transparent", height=45)
|
||||
btn_frame.grid(row=2, column=0, sticky="ew", pady=(15, 0))
|
||||
|
||||
ctk.CTkButton(
|
||||
btn_frame, text="Export Source (.tex)", command=self._save_tex,
|
||||
fg_color=("#64748b", "#475569"), hover_color=("#475569", "#334155"),
|
||||
width=160, height=36, corner_radius=6, font=ctk.CTkFont(family="Inter", size=12, weight="bold")
|
||||
).pack(side="left", padx=5)
|
||||
|
||||
ctk.CTkButton(
|
||||
btn_frame, text="Copy to Clipboard", command=self._copy_latex,
|
||||
fg_color=("#64748b", "#475569"), hover_color=("#475569", "#334155"),
|
||||
width=160, height=36, corner_radius=6, font=ctk.CTkFont(family="Inter", size=12, weight="bold")
|
||||
).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():
|
||||
if key == section_key:
|
||||
btn.configure(fg_color=("#2563eb", "#3b82f6"), text_color="white", hover_color=("#1d4ed8", "#2563eb"))
|
||||
else:
|
||||
btn.configure(fg_color="transparent", text_color=COLOR_TEXT_PRIMARY, hover_color=("white", "gray10"))
|
||||
|
||||
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 runtime compilation target tree verified.")
|
||||
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
|
||||
if self.exp_entries:
|
||||
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
|
||||
if self.proj_entries:
|
||||
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
|
||||
has_skills = False
|
||||
skills_latex = "\\section*{\\textcolor{primary}{Technical Skills}}\n\\addvspace{5pt}\n"
|
||||
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:
|
||||
skills_latex += f"\\noindent\\textbf{{{label}:}} {txt}\\\\\n"
|
||||
has_skills = True
|
||||
if has_skills:
|
||||
latex += skills_latex + "\n"
|
||||
|
||||
# Leadership
|
||||
if self.lead_entries:
|
||||
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 Target",
|
||||
filetypes=[("LaTeX", "*.tex"), ("All Profiles", "*.*")])
|
||||
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("Input profile mapping synchronized successfully.")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Import Error", f"Failed to parse LaTeX structure:\n{e}")
|
||||
|
||||
def _parse_and_populate(self, latex_text):
|
||||
"""Standard regex parser to map elements cleanly back to CTK states"""
|
||||
# Personal Name Mapping
|
||||
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 Node Mapping
|
||||
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(
|
||||
'}', '')
|
||||
|
||||
phone_pattern = r'(\d{3}-\d{3}-\d{4})'
|
||||
self.personal_vars['phone'].delete(0, tk.END)
|
||||
self.personal_vars['phone'].insert(0, re.search(phone_pattern, text).group(1) if re.search(phone_pattern,
|
||||
text) else "")
|
||||
|
||||
email_pattern = r'([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)'
|
||||
self.personal_vars['email'].delete(0, tk.END)
|
||||
self.personal_vars['email'].insert(0, re.search(email_pattern, text).group(1) if re.search(email_pattern,
|
||||
text) else "")
|
||||
|
||||
# Academic Parsing
|
||||
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 parsing
|
||||
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("Asset import matching done. Review structural fields.")
|
||||
|
||||
def _compile_latex(self):
|
||||
if not shutil.which("pdflatex"):
|
||||
messagebox.showerror("Toolchain Missing",
|
||||
"pdflatex binaries were not found in standard system PATH environments. 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 with pdflatex...")
|
||||
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("Compilation Error", "LaTeX subprocess run failed. Review code markup.")
|
||||
except Exception as e:
|
||||
self._set_status("Pipeline Error")
|
||||
messagebox.showerror("Runtime Trace", str(e))
|
||||
finally:
|
||||
self.compile_btn.configure(state="normal")
|
||||
|
||||
def _render_pdf(self, pdf_path):
|
||||
"""Direct vector rendering pipeline showing beautiful visual proofs on high-DPI scaling"""
|
||||
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)
|
||||
|
||||
# Render page inside frame with beautiful spacing
|
||||
page_label = ctk.CTkLabel(self.pdf_canvas_frame, image=photo, text="")
|
||||
page_label.pack(pady=10, padx=10)
|
||||
doc.close()
|
||||
except Exception as e:
|
||||
ctk.CTkLabel(self.pdf_canvas_frame, text=f"Render pipeline failed: {e}", text_color="#ef4444").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 high quality engineering data fields for clear demonstration"""
|
||||
self.first_name.insert(0, "Alex")
|
||||
self.last_name.insert(0, "Morgan")
|
||||
self.personal_vars['address'].insert(0, "789 Tech Drive, San Francisco, CA 94107")
|
||||
self.personal_vars['phone'].insert(0, "415-555-0199")
|
||||
self.personal_vars['email'].insert(0, "alex.morgan@domain.com")
|
||||
self.personal_vars['linkedin'].insert(0, "alexmorgan")
|
||||
self.personal_vars['github'].insert(0, "alexmorgan-dev")
|
||||
|
||||
self.edu_vars['university'].insert(0, "University of California, Berkeley")
|
||||
self.edu_vars['degree'].insert(0, "Bachelor of Science in Computer Science and Engineering")
|
||||
self.edu_vars['start_date'].insert(0, "Aug. 2018")
|
||||
self.edu_vars['end_date'].insert(0, "May 2022")
|
||||
self.edu_vars['location'].insert(0, "Berkeley, CA")
|
||||
|
||||
self.coursework.insert("1.0",
|
||||
"Data Structures, Efficient Algorithms, Distributed Systems, Software Engineering, Database Implementations, Computer Security, Artificial Intelligence")
|
||||
|
||||
|
||||
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()
|
||||
+909
@@ -0,0 +1,909 @@
|
||||
import customtkinter as ctk
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox
|
||||
import subprocess
|
||||
import tempfile
|
||||
import os
|
||||
import shutil
|
||||
import io
|
||||
import re
|
||||
import sys
|
||||
|
||||
try:
|
||||
import fitz # PyMuPDF
|
||||
from PIL import Image, ImageTk
|
||||
PDF_AVAILABLE = True
|
||||
except ImportError:
|
||||
PDF_AVAILABLE = False
|
||||
|
||||
# Modern SaaS theme
|
||||
ctk.set_appearance_mode("light")
|
||||
ctk.set_default_color_theme("blue")
|
||||
|
||||
# Color palette (SaaS-inspired)
|
||||
COLORS = {
|
||||
"primary": "#6366f1", # Indigo
|
||||
"primary_hover": "#4f46e5",
|
||||
"accent": "#10b981", # Emerald
|
||||
"danger": "#ef4444",
|
||||
"surface": "#ffffff",
|
||||
"surface_alt": "#f9fafb",
|
||||
"border": "#e5e7eb",
|
||||
"text": "#111827",
|
||||
"text_muted": "#6b7280",
|
||||
"bg": "#f3f4f6",
|
||||
}
|
||||
|
||||
class CVBuilderApp(ctk.CTk):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title("CV Forge — Professional LaTeX Resume Builder")
|
||||
self.geometry("1400x900")
|
||||
self.minsize(1100, 750)
|
||||
self.configure(fg_color=COLORS["bg"])
|
||||
|
||||
# State
|
||||
self.current_tab = "personal"
|
||||
self.section_frames = {}
|
||||
self.nav_buttons = {}
|
||||
self.latex_text = ""
|
||||
self.pdf_image_refs = []
|
||||
self.temp_dir = None
|
||||
self.exp_entries = []
|
||||
self.proj_entries = []
|
||||
self.lead_entries = []
|
||||
|
||||
self._build_header()
|
||||
self._build_tab_bar()
|
||||
self._build_content_area()
|
||||
self._populate_sample_data()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# HEADER BAR
|
||||
# ------------------------------------------------------------------ #
|
||||
def _build_header(self):
|
||||
header = ctk.CTkFrame(self, fg_color=COLORS["surface"], height=70, corner_radius=0)
|
||||
header.grid(row=0, column=0, sticky="ew")
|
||||
header.grid_columnconfigure(2, weight=1)
|
||||
header.grid_propagate(False)
|
||||
|
||||
# Brand
|
||||
brand = ctk.CTkFrame(header, fg_color="transparent")
|
||||
brand.grid(row=0, column=0, padx=24, pady=0, sticky="w")
|
||||
|
||||
logo = ctk.CTkLabel(brand, text="CV", font=ctk.CTkFont(size=18, weight="bold"),
|
||||
text_color="white", width=36, height=36, corner_radius=8,
|
||||
fg_color=COLORS["primary"])
|
||||
logo.pack(side="left", padx=(0, 12))
|
||||
|
||||
brand_text = ctk.CTkFrame(brand, fg_color="transparent")
|
||||
brand_text.pack(side="left")
|
||||
ctk.CTkLabel(brand_text, text="CV Forge", font=ctk.CTkFont(size=16, weight="bold"),
|
||||
text_color=COLORS["text"]).pack(anchor="w")
|
||||
ctk.CTkLabel(brand_text, text="Professional Resume Builder",
|
||||
font=ctk.CTkFont(size=11), text_color=COLORS["text_muted"]).pack(anchor="w")
|
||||
|
||||
# Divider
|
||||
sep = ctk.CTkFrame(header, width=1, fg_color=COLORS["border"])
|
||||
sep.grid(row=0, column=1, sticky="ns", padx=24, pady=18)
|
||||
|
||||
# Status indicator
|
||||
self.status_pill = ctk.CTkLabel(
|
||||
header, text=" Ready ", font=ctk.CTkFont(size=11, weight="bold"),
|
||||
text_color=COLORS["accent"], fg_color="#d1fae5",
|
||||
corner_radius=12, height=24
|
||||
)
|
||||
self.status_pill.grid(row=0, column=2, sticky="w", padx=12)
|
||||
|
||||
# Actions (right side)
|
||||
actions = ctk.CTkFrame(header, fg_color="transparent")
|
||||
actions.grid(row=0, column=3, padx=24, sticky="e")
|
||||
|
||||
self.import_btn = ctk.CTkButton(
|
||||
actions, text="Import .tex", command=self._import_latex,
|
||||
fg_color="transparent", border_width=1, border_color=COLORS["border"],
|
||||
text_color=COLORS["text"], hover_color=COLORS["surface_alt"],
|
||||
height=36, corner_radius=8, font=ctk.CTkFont(size=12, weight="bold")
|
||||
)
|
||||
self.import_btn.pack(side="left", padx=6)
|
||||
|
||||
self.generate_btn = ctk.CTkButton(
|
||||
actions, text="Generate", command=self._generate_latex,
|
||||
fg_color=COLORS["primary"], hover_color=COLORS["primary_hover"],
|
||||
text_color="white", height=36, corner_radius=8,
|
||||
font=ctk.CTkFont(size=12, weight="bold")
|
||||
)
|
||||
self.generate_btn.pack(side="left", padx=6)
|
||||
|
||||
self.compile_btn = ctk.CTkButton(
|
||||
actions, text="Compile PDF", command=self._compile_latex,
|
||||
fg_color=COLORS["accent"], hover_color="#059669",
|
||||
text_color="white", height=36, corner_radius=8,
|
||||
font=ctk.CTkFont(size=12, weight="bold"), state="disabled"
|
||||
)
|
||||
self.compile_btn.pack(side="left", padx=(6, 0))
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# HORIZONTAL TAB BAR
|
||||
# ------------------------------------------------------------------ #
|
||||
def _build_tab_bar(self):
|
||||
tab_bar = ctk.CTkFrame(self, fg_color=COLORS["surface"], height=56, corner_radius=0)
|
||||
tab_bar.grid(row=1, column=0, sticky="ew")
|
||||
tab_bar.grid_propagate(False)
|
||||
|
||||
tabs_container = ctk.CTkFrame(tab_bar, fg_color="transparent")
|
||||
tabs_container.pack(side="left", padx=24, pady=10)
|
||||
|
||||
tab_items = [
|
||||
("personal", "Personal"),
|
||||
("education", "Education"),
|
||||
("coursework", "Coursework"),
|
||||
("experience", "Experience"),
|
||||
("projects", "Projects"),
|
||||
("skills", "Skills"),
|
||||
("leadership", "Leadership"),
|
||||
("output", "Preview"),
|
||||
]
|
||||
|
||||
for key, label in tab_items:
|
||||
btn = ctk.CTkButton(
|
||||
tabs_container, text=label,
|
||||
command=lambda k=key: self._navigate_to(k),
|
||||
fg_color="transparent", text_color=COLORS["text_muted"],
|
||||
hover_color=COLORS["surface_alt"],
|
||||
height=34, corner_radius=8,
|
||||
font=ctk.CTkFont(size=13, weight="bold")
|
||||
)
|
||||
btn.pack(side="left", padx=2)
|
||||
self.nav_buttons[key] = btn
|
||||
|
||||
# Separator at bottom of tab bar
|
||||
sep = ctk.CTkFrame(tab_bar, height=1, fg_color=COLORS["border"])
|
||||
sep.pack(side="bottom", fill="x")
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# MAIN CONTENT AREA
|
||||
# ------------------------------------------------------------------ #
|
||||
def _build_content_area(self):
|
||||
self.content = ctk.CTkFrame(self, fg_color=COLORS["bg"], corner_radius=0)
|
||||
self.content.grid(row=2, column=0, sticky="nsew")
|
||||
self.content.grid_columnconfigure(0, weight=1)
|
||||
self.content.grid_rowconfigure(0, weight=1)
|
||||
self.grid_rowconfigure(2, weight=1)
|
||||
|
||||
# Build all section frames
|
||||
self.section_frames["personal"] = self._create_personal_section()
|
||||
self.section_frames["education"] = self._create_education_section()
|
||||
self.section_frames["coursework"] = self._create_coursework_section()
|
||||
self.section_frames["experience"] = self._create_experience_section()
|
||||
self.section_frames["projects"] = self._create_projects_section()
|
||||
self.section_frames["skills"] = self._create_skills_section()
|
||||
self.section_frames["leadership"] = self._create_leadership_section()
|
||||
self.section_frames["output"] = self._create_output_section()
|
||||
|
||||
self._navigate_to("personal")
|
||||
|
||||
# Reusable components
|
||||
def _make_card(self, parent, title=None):
|
||||
card = ctk.CTkFrame(parent, fg_color=COLORS["surface"], corner_radius=12, border_width=1, border_color=COLORS["border"])
|
||||
if title:
|
||||
ctk.CTkLabel(card, text=title, font=ctk.CTkFont(size=14, weight="bold"),
|
||||
text_color=COLORS["text"]).grid(row=0, column=0, columnspan=2,
|
||||
sticky="w", padx=20, pady=(16, 8))
|
||||
return card
|
||||
|
||||
def _section_header(self, parent, title, subtitle):
|
||||
wrap = ctk.CTkFrame(parent, fg_color="transparent")
|
||||
wrap.grid(row=0, column=0, sticky="ew", pady=(0, 20))
|
||||
ctk.CTkLabel(wrap, text=title, font=ctk.CTkFont(size=24, weight="bold"),
|
||||
text_color=COLORS["text"]).pack(anchor="w")
|
||||
ctk.CTkLabel(wrap, text=subtitle, font=ctk.CTkFont(size=13),
|
||||
text_color=COLORS["text_muted"]).pack(anchor="w", pady=(4, 0))
|
||||
return wrap
|
||||
|
||||
def _form_row(self, parent, label, widget, row):
|
||||
ctk.CTkLabel(parent, text=label, font=ctk.CTkFont(size=12, weight="bold"),
|
||||
text_color=COLORS["text_muted"]).grid(row=row, column=0, sticky="w",
|
||||
padx=20, pady=8)
|
||||
widget.grid(row=row, column=1, sticky="ew", padx=20, pady=8)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# PERSONAL
|
||||
# ------------------------------------------------------------------ #
|
||||
def _create_personal_section(self):
|
||||
frame = ctk.CTkScrollableFrame(self.content, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self._section_header(frame, "Personal Information",
|
||||
"Tell employers how to reach you.")
|
||||
|
||||
card = self._make_card(frame)
|
||||
card.grid(row=1, column=0, sticky="ew")
|
||||
card.grid_columnconfigure(1, weight=1)
|
||||
|
||||
# Name row
|
||||
ctk.CTkLabel(card, text="Full Name", font=ctk.CTkFont(size=12, weight="bold"),
|
||||
text_color=COLORS["text_muted"]).grid(row=0, column=0, sticky="w",
|
||||
padx=20, pady=(20, 8))
|
||||
|
||||
name_row = ctk.CTkFrame(card, fg_color="transparent")
|
||||
name_row.grid(row=0, column=1, sticky="ew", padx=20, pady=(20, 8))
|
||||
name_row.grid_columnconfigure((0, 1), weight=1)
|
||||
|
||||
self.first_name = ctk.CTkEntry(name_row, height=38, placeholder_text="First name",
|
||||
border_color=COLORS["border"])
|
||||
self.first_name.grid(row=0, column=0, sticky="ew", padx=(0, 8))
|
||||
self.last_name = ctk.CTkEntry(name_row, height=38, placeholder_text="Last name",
|
||||
border_color=COLORS["border"])
|
||||
self.last_name.grid(row=0, column=1, sticky="ew", padx=(8, 0))
|
||||
|
||||
# Contact fields
|
||||
self.personal_vars = {}
|
||||
fields = [
|
||||
("Address", "address", "e.g. 123 Main St, New York, NY"),
|
||||
("Phone", "phone", "e.g. 555-123-4567"),
|
||||
("Email", "email", "you@example.com"),
|
||||
("LinkedIn", "linkedin", "username"),
|
||||
("GitHub", "github", "username"),
|
||||
]
|
||||
for i, (label, key, ph) in enumerate(fields, start=1):
|
||||
entry = ctk.CTkEntry(card, height=38, placeholder_text=ph, border_color=COLORS["border"])
|
||||
self._form_row(card, label, entry, i)
|
||||
self.personal_vars[key] = entry
|
||||
|
||||
# Spacer
|
||||
ctk.CTkFrame(card, height=10, fg_color="transparent").grid(row=len(fields)+1, column=0)
|
||||
|
||||
return frame
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# EDUCATION
|
||||
# ------------------------------------------------------------------ #
|
||||
def _create_education_section(self):
|
||||
frame = ctk.CTkScrollableFrame(self.content, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
self._section_header(frame, "Education", "Your academic background.")
|
||||
|
||||
card = self._make_card(frame)
|
||||
card.grid(row=1, column=0, sticky="ew")
|
||||
card.grid_columnconfigure(1, weight=1)
|
||||
|
||||
self.edu_vars = {}
|
||||
fields = [
|
||||
("University", "university", "e.g. Stanford University"),
|
||||
("Degree", "degree", "e.g. B.S. Computer Science"),
|
||||
("Start Date", "start_date", "e.g. Sep. 2020"),
|
||||
("End Date", "end_date", "e.g. May 2024"),
|
||||
("Location", "location", "e.g. Stanford, CA"),
|
||||
]
|
||||
for i, (label, key, ph) in enumerate(fields):
|
||||
entry = ctk.CTkEntry(card, height=38, placeholder_text=ph, border_color=COLORS["border"])
|
||||
self._form_row(card, label, entry, i)
|
||||
self.edu_vars[key] = entry
|
||||
|
||||
ctk.CTkFrame(card, height=10, fg_color="transparent").grid(row=len(fields), column=0)
|
||||
return frame
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# COURSEWORK
|
||||
# ------------------------------------------------------------------ #
|
||||
def _create_coursework_section(self):
|
||||
frame = ctk.CTkScrollableFrame(self.content, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
self._section_header(frame, "Relevant Coursework",
|
||||
"Highlight academic courses that match the role.")
|
||||
|
||||
card = self._make_card(frame)
|
||||
card.grid(row=1, column=0, sticky="ew")
|
||||
card.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.coursework = ctk.CTkTextbox(card, height=140, corner_radius=8,
|
||||
border_width=1, border_color=COLORS["border"],
|
||||
font=("Segoe UI", 13))
|
||||
self.coursework.grid(row=0, column=0, sticky="ew", padx=20, pady=20)
|
||||
|
||||
ctk.CTkLabel(card, text="Separate courses with commas. They'll be auto-formatted.",
|
||||
font=ctk.CTkFont(size=12), text_color=COLORS["text_muted"]).grid(
|
||||
row=1, column=0, sticky="w", padx=20, pady=(0, 16))
|
||||
return frame
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# EXPERIENCE
|
||||
# ------------------------------------------------------------------ #
|
||||
def _create_experience_section(self):
|
||||
frame = ctk.CTkScrollableFrame(self.content, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
self._section_header(frame, "Professional Experience",
|
||||
"Your work history — most recent first.")
|
||||
|
||||
self.exp_container = ctk.CTkFrame(frame, fg_color="transparent")
|
||||
self.exp_container.grid(row=1, column=0, sticky="ew")
|
||||
self.exp_container.grid_columnconfigure(0, weight=1)
|
||||
|
||||
ctk.CTkButton(
|
||||
frame, text="+ Add Position", command=self._add_experience_entry,
|
||||
fg_color=COLORS["primary"], hover_color=COLORS["primary_hover"],
|
||||
text_color="white", height=38, corner_radius=8,
|
||||
font=ctk.CTkFont(size=13, weight="bold")
|
||||
).grid(row=2, column=0, pady=20, sticky="w")
|
||||
return frame
|
||||
|
||||
def _add_experience_entry(self):
|
||||
idx = len(self.exp_entries)
|
||||
card = self._make_card(self.exp_container, title=f"Position {idx + 1}")
|
||||
card.grid(row=idx, column=0, sticky="ew", pady=(0, 12))
|
||||
card.grid_columnconfigure(1, weight=1)
|
||||
|
||||
vars_dict = {}
|
||||
fields = [
|
||||
("Company", "company", "Company name"),
|
||||
("Title", "title", "e.g. Software Engineer"),
|
||||
("Start Date", "start_date", "e.g. Jun. 2023"),
|
||||
("End Date", "end_date", "e.g. Present"),
|
||||
("Location", "location", "e.g. Remote"),
|
||||
]
|
||||
for i, (label, key, ph) in enumerate(fields, start=1):
|
||||
entry = ctk.CTkEntry(card, height=36, placeholder_text=ph, border_color=COLORS["border"])
|
||||
self._form_row(card, label, entry, i)
|
||||
vars_dict[key] = entry
|
||||
|
||||
ctk.CTkLabel(card, text="Responsibilities (one per line)",
|
||||
font=ctk.CTkFont(size=12, weight="bold"),
|
||||
text_color=COLORS["text_muted"]).grid(
|
||||
row=len(fields)+1, column=0, sticky="w", padx=20, pady=(12, 6))
|
||||
resp = ctk.CTkTextbox(card, height=120, corner_radius=8,
|
||||
border_width=1, border_color=COLORS["border"])
|
||||
resp.grid(row=len(fields)+2, column=0, columnspan=2, sticky="ew", padx=20)
|
||||
|
||||
ctk.CTkButton(
|
||||
card, text="Remove", command=lambda c=card: self._remove_entry(c, self.exp_entries),
|
||||
fg_color="transparent", border_width=1, border_color=COLORS["danger"],
|
||||
text_color=COLORS["danger"], hover_color="#fee2e2",
|
||||
height=32, corner_radius=8, width=90
|
||||
).grid(row=len(fields)+3, column=0, columnspan=2, pady=(12, 16), sticky="w", padx=20)
|
||||
|
||||
self.exp_entries.append((vars_dict, resp, card))
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# PROJECTS
|
||||
# ------------------------------------------------------------------ #
|
||||
def _create_projects_section(self):
|
||||
frame = ctk.CTkScrollableFrame(self.content, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
self._section_header(frame, "Projects",
|
||||
"Showcase your side projects and personal work.")
|
||||
|
||||
self.proj_container = ctk.CTkFrame(frame, fg_color="transparent")
|
||||
self.proj_container.grid(row=1, column=0, sticky="ew")
|
||||
self.proj_container.grid_columnconfigure(0, weight=1)
|
||||
|
||||
ctk.CTkButton(
|
||||
frame, text="+ Add Project", command=self._add_project_entry,
|
||||
fg_color=COLORS["primary"], hover_color=COLORS["primary_hover"],
|
||||
text_color="white", height=38, corner_radius=8,
|
||||
font=ctk.CTkFont(size=13, weight="bold")
|
||||
).grid(row=2, column=0, pady=20, sticky="w")
|
||||
return frame
|
||||
|
||||
def _add_project_entry(self):
|
||||
idx = len(self.proj_entries)
|
||||
card = self._make_card(self.proj_container, title=f"Project {idx + 1}")
|
||||
card.grid(row=idx, column=0, sticky="ew", pady=(0, 12))
|
||||
card.grid_columnconfigure(1, weight=1)
|
||||
|
||||
vars_dict = {}
|
||||
fields = [
|
||||
("Project Name", "name", "e.g. Task Manager App"),
|
||||
("Technologies", "technologies", "e.g. React, Node.js, PostgreSQL"),
|
||||
("Date", "date", "e.g. Summer 2023"),
|
||||
]
|
||||
for i, (label, key, ph) in enumerate(fields, start=1):
|
||||
entry = ctk.CTkEntry(card, height=36, placeholder_text=ph, border_color=COLORS["border"])
|
||||
self._form_row(card, label, entry, i)
|
||||
vars_dict[key] = entry
|
||||
|
||||
ctk.CTkLabel(card, text="Description (one per line)",
|
||||
font=ctk.CTkFont(size=12, weight="bold"),
|
||||
text_color=COLORS["text_muted"]).grid(
|
||||
row=len(fields)+1, column=0, sticky="w", padx=20, pady=(12, 6))
|
||||
desc = ctk.CTkTextbox(card, height=100, corner_radius=8,
|
||||
border_width=1, border_color=COLORS["border"])
|
||||
desc.grid(row=len(fields)+2, column=0, columnspan=2, sticky="ew", padx=20)
|
||||
|
||||
ctk.CTkButton(
|
||||
card, text="Remove", command=lambda c=card: self._remove_entry(c, self.proj_entries),
|
||||
fg_color="transparent", border_width=1, border_color=COLORS["danger"],
|
||||
text_color=COLORS["danger"], hover_color="#fee2e2",
|
||||
height=32, corner_radius=8, width=90
|
||||
).grid(row=len(fields)+3, column=0, columnspan=2, pady=(12, 16), sticky="w", padx=20)
|
||||
|
||||
self.proj_entries.append((vars_dict, desc, card))
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# SKILLS
|
||||
# ------------------------------------------------------------------ #
|
||||
def _create_skills_section(self):
|
||||
frame = ctk.CTkScrollableFrame(self.content, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
self._section_header(frame, "Technical Skills",
|
||||
"Languages, tools, and technologies you know.")
|
||||
|
||||
card = self._make_card(frame)
|
||||
card.grid(row=1, column=0, sticky="ew")
|
||||
card.grid_columnconfigure(1, weight=1)
|
||||
|
||||
self.skills_vars = {}
|
||||
fields = [
|
||||
("Languages", "languages", "e.g. Python, JavaScript, Go"),
|
||||
("Developer Tools", "tools", "e.g. Git, Docker, VS Code"),
|
||||
("Frameworks", "technologies", "e.g. React, Django, TensorFlow"),
|
||||
]
|
||||
for i, (label, key, ph) in enumerate(fields):
|
||||
entry = ctk.CTkEntry(card, height=38, placeholder_text=ph, border_color=COLORS["border"])
|
||||
self._form_row(card, label, entry, i)
|
||||
self.skills_vars[key] = entry
|
||||
|
||||
ctk.CTkFrame(card, height=10, fg_color="transparent").grid(row=len(fields), column=0)
|
||||
return frame
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# LEADERSHIP
|
||||
# ------------------------------------------------------------------ #
|
||||
def _create_leadership_section(self):
|
||||
frame = ctk.CTkScrollableFrame(self.content, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
self._section_header(frame, "Leadership & Activities",
|
||||
"Volunteer work, clubs, and community involvement.")
|
||||
|
||||
self.lead_container = ctk.CTkFrame(frame, fg_color="transparent")
|
||||
self.lead_container.grid(row=1, column=0, sticky="ew")
|
||||
self.lead_container.grid_columnconfigure(0, weight=1)
|
||||
|
||||
ctk.CTkButton(
|
||||
frame, text="+ Add Activity", command=self._add_leadership_entry,
|
||||
fg_color=COLORS["primary"], hover_color=COLORS["primary_hover"],
|
||||
text_color="white", height=38, corner_radius=8,
|
||||
font=ctk.CTkFont(size=13, weight="bold")
|
||||
).grid(row=2, column=0, pady=20, sticky="w")
|
||||
return frame
|
||||
|
||||
def _add_leadership_entry(self):
|
||||
idx = len(self.lead_entries)
|
||||
card = self._make_card(self.lead_container, title=f"Activity {idx + 1}")
|
||||
card.grid(row=idx, column=0, sticky="ew", pady=(0, 12))
|
||||
card.grid_columnconfigure(1, weight=1)
|
||||
|
||||
vars_dict = {}
|
||||
fields = [
|
||||
("Organization", "organization", "e.g. ACM Student Chapter"),
|
||||
("Role", "role", "e.g. President"),
|
||||
("Date", "date", "e.g. 2022 — 2023"),
|
||||
("Location", "location", "e.g. Campus"),
|
||||
]
|
||||
for i, (label, key, ph) in enumerate(fields, start=1):
|
||||
entry = ctk.CTkEntry(card, height=36, placeholder_text=ph, border_color=COLORS["border"])
|
||||
self._form_row(card, label, entry, i)
|
||||
vars_dict[key] = entry
|
||||
|
||||
ctk.CTkLabel(card, text="Achievements (one per line)",
|
||||
font=ctk.CTkFont(size=12, weight="bold"),
|
||||
text_color=COLORS["text_muted"]).grid(
|
||||
row=len(fields)+1, column=0, sticky="w", padx=20, pady=(12, 6))
|
||||
ach = ctk.CTkTextbox(card, height=100, corner_radius=8,
|
||||
border_width=1, border_color=COLORS["border"])
|
||||
ach.grid(row=len(fields)+2, column=0, columnspan=2, sticky="ew", padx=20)
|
||||
|
||||
ctk.CTkButton(
|
||||
card, text="Remove", command=lambda c=card: self._remove_entry(c, self.lead_entries),
|
||||
fg_color="transparent", border_width=1, border_color=COLORS["danger"],
|
||||
text_color=COLORS["danger"], hover_color="#fee2e2",
|
||||
height=32, corner_radius=8, width=90
|
||||
).grid(row=len(fields)+3, column=0, columnspan=2, pady=(12, 16), sticky="w", padx=20)
|
||||
|
||||
self.lead_entries.append((vars_dict, ach, card))
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# OUTPUT (Side-by-side editor + preview)
|
||||
# ------------------------------------------------------------------ #
|
||||
def _create_output_section(self):
|
||||
frame = ctk.CTkFrame(self.content, fg_color="transparent")
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
frame.grid_rowconfigure(1, weight=1)
|
||||
|
||||
self._section_header(frame, "Preview & Export",
|
||||
"Your LaTeX source alongside the compiled PDF.")
|
||||
|
||||
# Two-column layout
|
||||
split = ctk.CTkFrame(frame, fg_color="transparent")
|
||||
split.grid(row=1, column=0, sticky="nsew")
|
||||
split.grid_columnconfigure((0, 1), weight=1)
|
||||
split.grid_rowconfigure(0, weight=1)
|
||||
|
||||
# LaTeX editor card
|
||||
editor_card = self._make_card(split)
|
||||
editor_card.grid(row=0, column=0, sticky="nsew", padx=(0, 8))
|
||||
editor_card.grid_columnconfigure(0, weight=1)
|
||||
editor_card.grid_rowconfigure(1, weight=1)
|
||||
|
||||
ed_head = ctk.CTkFrame(editor_card, fg_color="transparent")
|
||||
ed_head.grid(row=0, column=0, sticky="ew", padx=20, pady=(16, 10))
|
||||
ed_head.grid_columnconfigure(0, weight=1)
|
||||
ctk.CTkLabel(ed_head, text="LaTeX Source", font=ctk.CTkFont(size=14, weight="bold"),
|
||||
text_color=COLORS["text"]).grid(row=0, column=0, sticky="w")
|
||||
|
||||
ed_actions = ctk.CTkFrame(ed_head, fg_color="transparent")
|
||||
ed_actions.grid(row=0, column=1, sticky="e")
|
||||
ctk.CTkButton(ed_actions, text="Save", command=self._save_tex,
|
||||
fg_color="transparent", border_width=1, border_color=COLORS["border"],
|
||||
text_color=COLORS["text"], hover_color=COLORS["surface_alt"],
|
||||
height=28, corner_radius=6, width=60,
|
||||
font=ctk.CTkFont(size=11, weight="bold")).pack(side="left", padx=4)
|
||||
ctk.CTkButton(ed_actions, text="Copy", command=self._copy_latex,
|
||||
fg_color="transparent", border_width=1, border_color=COLORS["border"],
|
||||
text_color=COLORS["text"], hover_color=COLORS["surface_alt"],
|
||||
height=28, corner_radius=6, width=60,
|
||||
font=ctk.CTkFont(size=11, weight="bold")).pack(side="left")
|
||||
|
||||
self.latex_editor = ctk.CTkTextbox(
|
||||
editor_card, corner_radius=8, font=("Consolas", 11),
|
||||
border_width=1, border_color=COLORS["border"], fg_color="#fafafa"
|
||||
)
|
||||
self.latex_editor.grid(row=1, column=0, sticky="nsew", padx=20, pady=(0, 20))
|
||||
|
||||
# Preview card
|
||||
preview_card = self._make_card(split)
|
||||
preview_card.grid(row=0, column=1, sticky="nsew", padx=(8, 0))
|
||||
preview_card.grid_columnconfigure(0, weight=1)
|
||||
preview_card.grid_rowconfigure(1, weight=1)
|
||||
|
||||
pv_head = ctk.CTkFrame(preview_card, fg_color="transparent")
|
||||
pv_head.grid(row=0, column=0, sticky="ew", padx=20, pady=(16, 10))
|
||||
ctk.CTkLabel(pv_head, text="PDF Preview", font=ctk.CTkFont(size=14, weight="bold"),
|
||||
text_color=COLORS["text"]).pack(side="left")
|
||||
|
||||
self.pdf_viewer = ctk.CTkScrollableFrame(preview_card, corner_radius=8,
|
||||
border_width=1, border_color=COLORS["border"],
|
||||
fg_color=COLORS["surface_alt"])
|
||||
self.pdf_viewer.grid(row=1, column=0, sticky="nsew", padx=20, pady=(0, 20))
|
||||
|
||||
self.placeholder = ctk.CTkLabel(
|
||||
self.pdf_viewer,
|
||||
text="Click 'Generate' then 'Compile PDF'\nto preview your resume here.",
|
||||
font=ctk.CTkFont(size=13), text_color=COLORS["text_muted"],
|
||||
justify="center"
|
||||
)
|
||||
self.placeholder.pack(expand=True, pady=60)
|
||||
|
||||
return frame
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# SHARED UTILITIES
|
||||
# ------------------------------------------------------------------ #
|
||||
def _remove_entry(self, card, entry_list):
|
||||
for i, (_, _, c) in enumerate(entry_list):
|
||||
if c == card:
|
||||
card.destroy()
|
||||
entry_list.pop(i)
|
||||
break
|
||||
|
||||
def _navigate_to(self, key):
|
||||
if self.current_tab and self.current_tab in self.section_frames:
|
||||
self.section_frames[self.current_tab].grid_remove()
|
||||
|
||||
self.current_tab = key
|
||||
self.section_frames[key].grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
for k, btn in self.nav_buttons.items():
|
||||
if k == key:
|
||||
btn.configure(fg_color=COLORS["primary"], text_color="white",
|
||||
hover_color=COLORS["primary_hover"])
|
||||
else:
|
||||
btn.configure(fg_color="transparent", text_color=COLORS["text_muted"],
|
||||
hover_color=COLORS["surface_alt"])
|
||||
|
||||
def _set_status(self, text, color=None):
|
||||
color = color or COLORS["accent"]
|
||||
self.status_pill.configure(text=f" {text} ", text_color=color)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# LATEX GENERATION
|
||||
# ------------------------------------------------------------------ #
|
||||
def _generate_latex(self):
|
||||
self.latex_text = self._build_latex_cv()
|
||||
self.latex_editor.delete("1.0", tk.END)
|
||||
self.latex_editor.insert("1.0", self.latex_text)
|
||||
self.compile_btn.configure(state="normal")
|
||||
self._set_status("LaTeX generated")
|
||||
self._navigate_to("output")
|
||||
|
||||
def _build_latex_cv(self):
|
||||
latex = r"""\documentclass[11pt,a4paper]{article}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{geometry}
|
||||
\usepackage{enumitem}
|
||||
\usepackage{hyperref}
|
||||
\usepackage{parskip}
|
||||
\usepackage{xcolor}
|
||||
|
||||
\geometry{margin=0.75in}
|
||||
\setlist[itemize]{leftmargin=*, nosep, topsep=2pt, itemsep=2pt}
|
||||
\pagestyle{empty}
|
||||
|
||||
\definecolor{primary}{RGB}{99,102,241}
|
||||
\definecolor{secondary}{RGB}{107,114,128}
|
||||
|
||||
\hypersetup{colorlinks=true, linkcolor=primary, urlcolor=primary}
|
||||
|
||||
\begin{document}
|
||||
"""
|
||||
first = self.first_name.get().strip()
|
||||
last = self.last_name.get().strip()
|
||||
latex += f"\n\\begin{{center}}\n\\textbf{{\\Huge \\textcolor{{primary}}{{{first} {last}}}}}\\\\[4pt]\n"
|
||||
|
||||
contacts = []
|
||||
for key in ['address', 'phone', 'email', 'linkedin', 'github']:
|
||||
v = self.personal_vars[key].get().strip()
|
||||
if not v: continue
|
||||
if key == 'phone': contacts.append(f"\\href{{tel:{v}}}{{{v}}}")
|
||||
elif key == 'email': contacts.append(f"\\href{{mailto:{v}}}{{{v}}}")
|
||||
elif key == 'linkedin': contacts.append(f"\\href{{https://linkedin.com/in/{v}}}{{linkedin.com/in/{v}}}")
|
||||
elif key == 'github': contacts.append(f"\\href{{https://github.com/{v}}}{{github.com/{v}}}")
|
||||
else: contacts.append(v)
|
||||
latex += " \\textcolor{secondary}{$\\cdot$} ".join(contacts) + "\n\\end{center}\n\n\\vspace{4pt}\n\\hrule\n\\vspace{6pt}\n\n"
|
||||
|
||||
if self.edu_vars['university'].get().strip():
|
||||
latex += "\\section*{\\textcolor{primary}{Education}}\n"
|
||||
uni = self.edu_vars['university'].get().strip()
|
||||
deg = self.edu_vars['degree'].get().strip()
|
||||
s = self.edu_vars['start_date'].get().strip()
|
||||
e = self.edu_vars['end_date'].get().strip()
|
||||
loc = self.edu_vars['location'].get().strip()
|
||||
date_str = f"{s} -- {e}" if s and e else ""
|
||||
latex += f"\\noindent\\textbf{{{uni}}}"
|
||||
if date_str: latex += f"\\hfill \\textcolor{{secondary}}{{{date_str}}}"
|
||||
latex += "\\\\\n"
|
||||
latex += f"\\textit{{{deg}}}"
|
||||
if loc: latex += f" \\textcolor{{secondary}}{{$\\cdot$}} {loc}"
|
||||
latex += "\n\n"
|
||||
|
||||
cw = self.coursework.get("1.0", tk.END).strip()
|
||||
if cw:
|
||||
latex += "\\section*{\\textcolor{primary}{Relevant Coursework}}\n"
|
||||
courses = [c.strip() for c in cw.split(',') if c.strip()]
|
||||
latex += "\\begin{itemize}\n"
|
||||
for i in range(0, len(courses), 4):
|
||||
latex += " \\item " + " \\quad $\\bullet$ ".join(courses[i:i+4]) + "\n"
|
||||
latex += "\\end{itemize}\n\n"
|
||||
|
||||
if any(v['company'].get().strip() for v, _, _ in self.exp_entries):
|
||||
latex += "\\section*{\\textcolor{primary}{Experience}}\n"
|
||||
for v, t, _ in self.exp_entries:
|
||||
comp = v['company'].get().strip()
|
||||
if not comp: continue
|
||||
title = v['title'].get().strip()
|
||||
s = v['start_date'].get().strip()
|
||||
e = v['end_date'].get().strip()
|
||||
loc = v['location'].get().strip()
|
||||
date_str = f"{s} -- {e}" if s and e else ""
|
||||
latex += f"\\noindent\\textbf{{{comp}}}"
|
||||
if date_str: latex += f"\\hfill \\textcolor{{secondary}}{{{date_str}}}"
|
||||
latex += "\\\\\n"
|
||||
latex += f"\\textit{{{title}}}"
|
||||
if loc: latex += f" \\textcolor{{secondary}}{{$\\cdot$}} {loc}"
|
||||
latex += "\n"
|
||||
resp = t.get("1.0", tk.END).strip()
|
||||
if resp:
|
||||
latex += "\\begin{itemize}\n"
|
||||
for r in resp.split('\n'):
|
||||
if r.strip(): latex += f" \\item {r.strip()}\n"
|
||||
latex += "\\end{itemize}\n"
|
||||
latex += "\n"
|
||||
|
||||
if any(v['name'].get().strip() for v, _, _ in self.proj_entries):
|
||||
latex += "\\section*{\\textcolor{primary}{Projects}}\n"
|
||||
for v, t, _ in self.proj_entries:
|
||||
name = v['name'].get().strip()
|
||||
if not name: continue
|
||||
tech = v['technologies'].get().strip()
|
||||
date = v['date'].get().strip()
|
||||
if tech: latex += f"\\noindent\\textbf{{{name}}} \\textcolor{{secondary}}{{|}} \\textit{{{tech}}}"
|
||||
else: latex += f"\\noindent\\textbf{{{name}}}"
|
||||
if date: latex += f"\\hfill \\textcolor{{secondary}}{{{date}}}"
|
||||
latex += "\n"
|
||||
desc = t.get("1.0", tk.END).strip()
|
||||
if desc:
|
||||
latex += "\\begin{itemize}\n"
|
||||
for d in desc.split('\n'):
|
||||
if d.strip(): latex += f" \\item {d.strip()}\n"
|
||||
latex += "\\end{itemize}\n"
|
||||
latex += "\n"
|
||||
|
||||
has_skill = any(self.skills_vars[k].get().strip() for k in ['languages', 'tools', 'technologies'])
|
||||
if has_skill:
|
||||
latex += "\\section*{\\textcolor{primary}{Technical Skills}}\n"
|
||||
for key, label in [('languages','Languages'), ('tools','Developer Tools'), ('technologies','Frameworks')]:
|
||||
v = self.skills_vars[key].get().strip()
|
||||
if v: latex += f"\\noindent\\textbf{{{label}:}} {v}\\\\\n"
|
||||
latex += "\n"
|
||||
|
||||
if any(v['organization'].get().strip() for v, _, _ in self.lead_entries):
|
||||
latex += "\\section*{\\textcolor{primary}{Leadership \\& Activities}}\n"
|
||||
for v, t, _ in self.lead_entries:
|
||||
org = v['organization'].get().strip()
|
||||
if not org: continue
|
||||
role = v['role'].get().strip()
|
||||
date = v['date'].get().strip()
|
||||
loc = v['location'].get().strip()
|
||||
latex += f"\\noindent\\textbf{{{org}}}"
|
||||
if date: latex += f"\\hfill \\textcolor{{secondary}}{{{date}}}"
|
||||
latex += "\\\\\n"
|
||||
latex += f"\\textit{{{role}}}"
|
||||
if loc: latex += f" \\textcolor{{secondary}}{{$\\cdot$}} {loc}"
|
||||
latex += "\n"
|
||||
ach = t.get("1.0", tk.END).strip()
|
||||
if ach:
|
||||
latex += "\\begin{itemize}\n"
|
||||
for a in ach.split('\n'):
|
||||
if a.strip(): latex += f" \\item {a.strip()}\n"
|
||||
latex += "\\end{itemize}\n"
|
||||
latex += "\n"
|
||||
|
||||
latex += "\\end{document}"
|
||||
return latex
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# IMPORT
|
||||
# ------------------------------------------------------------------ #
|
||||
def _import_latex(self):
|
||||
path = filedialog.askopenfilename(title="Import LaTeX CV",
|
||||
filetypes=[("LaTeX", "*.tex"), ("All", "*.*")])
|
||||
if not path: return
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
self._parse_and_populate(content)
|
||||
self._set_status("Imported from file")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Import Error", f"Could not parse file:\n{e}")
|
||||
|
||||
def _parse_and_populate(self, text):
|
||||
name_m = re.search(r'\\textbf\{\\Huge \\textcolor\{primary\}\{(.*?)\}\}', text)
|
||||
if name_m:
|
||||
parts = name_m.group(1).split(None, 1)
|
||||
self.first_name.delete(0, tk.END)
|
||||
self.last_name.delete(0, tk.END)
|
||||
self.first_name.insert(0, parts[0] if parts else "")
|
||||
self.last_name.insert(0, parts[1] if len(parts) > 1 else "")
|
||||
|
||||
contact_m = re.search(r'\\end\{center\}(.*?)\\hrule', text, re.DOTALL)
|
||||
if contact_m:
|
||||
block = contact_m.group(1)
|
||||
email = re.search(r'([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)', block)
|
||||
if email:
|
||||
self.personal_vars['email'].delete(0, tk.END)
|
||||
self.personal_vars['email'].insert(0, email.group(1))
|
||||
phone = re.search(r'(\d{3}[-.\s]?\d{3}[-.\s]?\d{4})', block)
|
||||
if phone:
|
||||
self.personal_vars['phone'].delete(0, tk.END)
|
||||
self.personal_vars['phone'].insert(0, phone.group(1))
|
||||
|
||||
edu_m = re.search(r'\\section\*\{.*?Education\}.*?\\textbf\{(.*?)\}.*?\\textit\{(.*?)\}', text, re.DOTALL)
|
||||
if edu_m:
|
||||
self.edu_vars['university'].delete(0, tk.END)
|
||||
self.edu_vars['university'].insert(0, edu_m.group(1))
|
||||
self.edu_vars['degree'].delete(0, tk.END)
|
||||
self.edu_vars['degree'].insert(0, edu_m.group(2))
|
||||
|
||||
cw_m = re.search(r'\\section\*\{.*?Coursework\}.*?\\begin\{itemize\}(.*?)\\end\{itemize\}', text, re.DOTALL)
|
||||
if cw_m:
|
||||
self.coursework.delete("1.0", tk.END)
|
||||
clean = cw_m.group(1).replace('\\item', '').replace('\\quad $\\bullet$', ',').replace('\n', '').strip()
|
||||
self.coursework.insert("1.0", clean)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# COMPILE & PDF
|
||||
# ------------------------------------------------------------------ #
|
||||
def _compile_latex(self):
|
||||
if not shutil.which("pdflatex"):
|
||||
messagebox.showerror("LaTeX Not Found",
|
||||
"pdflatex is not on your PATH.\nInstall TeX Live or MiKTeX.")
|
||||
return
|
||||
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
tex_path = os.path.join(self.temp_dir, "cv.tex")
|
||||
try:
|
||||
with open(tex_path, "w", encoding="utf-8") as f:
|
||||
f.write(self.latex_text)
|
||||
self._set_status("Compiling...", COLORS["primary"])
|
||||
self.compile_btn.configure(state="disabled")
|
||||
self.update()
|
||||
|
||||
result = subprocess.run(
|
||||
["pdflatex", "-interaction=nonstopmode", "cv.tex"],
|
||||
cwd=self.temp_dir, capture_output=True, text=True
|
||||
)
|
||||
pdf_path = os.path.join(self.temp_dir, "cv.pdf")
|
||||
if result.returncode == 0 and os.path.exists(pdf_path):
|
||||
self._set_status("Compiled successfully")
|
||||
self._render_pdf(pdf_path)
|
||||
else:
|
||||
self._set_status("Compilation failed", COLORS["danger"])
|
||||
messagebox.showerror("Compile Error", "Check your LaTeX syntax.")
|
||||
except Exception as e:
|
||||
self._set_status("Error", COLORS["danger"])
|
||||
messagebox.showerror("Error", str(e))
|
||||
finally:
|
||||
self.compile_btn.configure(state="normal")
|
||||
|
||||
def _render_pdf(self, pdf_path):
|
||||
if not PDF_AVAILABLE:
|
||||
ctk.CTkLabel(self.pdf_viewer,
|
||||
text="Install PyMuPDF to enable PDF preview.",
|
||||
text_color=COLORS["danger"]).pack(pady=30)
|
||||
return
|
||||
for w in self.pdf_viewer.winfo_children(): w.destroy()
|
||||
self.pdf_image_refs.clear()
|
||||
try:
|
||||
doc = fitz.open(pdf_path)
|
||||
for pg in range(len(doc)):
|
||||
pix = doc.load_page(pg).get_pixmap(matrix=fitz.Matrix(1.8, 1.8))
|
||||
img = Image.open(io.BytesIO(pix.tobytes("ppm")))
|
||||
photo = ImageTk.PhotoImage(img)
|
||||
self.pdf_image_refs.append(photo)
|
||||
lbl = ctk.CTkLabel(self.pdf_viewer, image=photo, text="")
|
||||
lbl.pack(pady=10)
|
||||
shadow = ctk.CTkFrame(self.pdf_viewer, height=1, fg_color=COLORS["border"])
|
||||
shadow.pack(fill="x", padx=20)
|
||||
doc.close()
|
||||
except Exception as e:
|
||||
ctk.CTkLabel(self.pdf_viewer, text=f"Render error: {e}",
|
||||
text_color=COLORS["danger"]).pack(pady=20)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# FILE OPERATIONS
|
||||
# ------------------------------------------------------------------ #
|
||||
def _save_tex(self):
|
||||
path = filedialog.asksaveasfilename(defaultextension=".tex",
|
||||
filetypes=[("LaTeX", "*.tex")],
|
||||
initialfile="resume.tex")
|
||||
if path:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(self.latex_text)
|
||||
self._set_status(f"Saved: {os.path.basename(path)}")
|
||||
|
||||
def _copy_latex(self):
|
||||
self.clipboard_clear()
|
||||
self.clipboard_append(self.latex_text)
|
||||
self._set_status("Copied to clipboard")
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# SAMPLE DATA
|
||||
# ------------------------------------------------------------------ #
|
||||
def _populate_sample_data(self):
|
||||
self.first_name.insert(0, "Jane")
|
||||
self.last_name.insert(0, "Doe")
|
||||
self.personal_vars['address'].insert(0, "San Francisco, CA")
|
||||
self.personal_vars['phone'].insert(0, "555-123-4567")
|
||||
self.personal_vars['email'].insert(0, "jane.doe@example.com")
|
||||
self.personal_vars['linkedin'].insert(0, "janedoe")
|
||||
self.personal_vars['github'].insert(0, "janedoe")
|
||||
|
||||
self.edu_vars['university'].insert(0, "Stanford University")
|
||||
self.edu_vars['degree'].insert(0, "B.S. Computer Science, GPA 3.9")
|
||||
self.edu_vars['start_date'].insert(0, "Sep. 2020")
|
||||
self.edu_vars['end_date'].insert(0, "Jun. 2024")
|
||||
self.edu_vars['location'].insert(0, "Stanford, CA")
|
||||
|
||||
self.coursework.insert("1.0",
|
||||
"Data Structures, Algorithms, Distributed Systems, "
|
||||
"Machine Learning, Operating Systems, Computer Networks, "
|
||||
"Database Systems, Software Engineering")
|
||||
|
||||
|
||||
def main():
|
||||
app = CVBuilderApp()
|
||||
app.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user