mirror of
https://github.com/mudabbir-ahmad/Latex-CV-Builder.git
synced 2026-10-07 18:10:20 +00:00
1605 lines
74 KiB
Python
1605 lines
74 KiB
Python
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
|
|
|
|
# Comprehensive Month & Season List for High-Fidelity Academic Resumes
|
|
MONTHS_LIST = [
|
|
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
|
"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November",
|
|
"December",
|
|
"Spring", "Summer", "Fall", "Winter"
|
|
]
|
|
YEARS_LIST = [str(y) for y in range(2030, 2010, -1)] + ["Present"]
|
|
|
|
SUGGESTED_LOCATIONS = [
|
|
"Berkeley, CA", "San Francisco, CA", "New York, NY", "Seattle, WA",
|
|
"Austin, TX", "Boston, MA", "Chicago, IL", "Los Angeles, CA",
|
|
"London, UK", "Toronto, ON", "Vancouver, BC", "Paris, France",
|
|
"Berlin, Germany", "Tokyo, Japan", "Sydney, Australia", "Singapore", "City, State", "University Name"
|
|
]
|
|
|
|
|
|
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)
|
|
|
|
# State management
|
|
self.nav_orientation = "horizontal" # Starts in modern horizontal tab layout
|
|
self.current_appearance_mode = "System Mode"
|
|
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 escape_latex(self, text):
|
|
"""Escapes raw text entries to ensure special characters won't crash pdflatex compilation"""
|
|
if not text:
|
|
return ""
|
|
# Handle backslash replacement first to avoid recursive escaping
|
|
text = text.replace('\\', '\\textbackslash{}')
|
|
# Escape characters that have special meaning inside LaTeX
|
|
text = text.replace('&', '\\&')
|
|
text = text.replace('%', '\\%')
|
|
text = text.replace('$', '\\$')
|
|
text = text.replace('_', '\\_')
|
|
text = text.replace('{', '\\{')
|
|
text = text.replace('}', '\\}')
|
|
text = text.replace('#', '\\#')
|
|
text = text.replace('~', '\\textasciitilde{}')
|
|
text = text.replace('^', '\\textasciicircum{}')
|
|
return text
|
|
|
|
def _build_ui(self):
|
|
"""Clears and builds the interface dynamically based on chosen orientation"""
|
|
if hasattr(self, "sidebar") and self.sidebar:
|
|
self.sidebar.grid_forget()
|
|
if hasattr(self, "top_nav") and self.top_nav:
|
|
self.top_nav.grid_forget()
|
|
if hasattr(self, "main_container") and self.main_container:
|
|
self.main_container.grid_forget()
|
|
|
|
if self.nav_orientation == "vertical":
|
|
self._build_vertical_sidebar()
|
|
self._build_main_area(column=1, row=0)
|
|
else:
|
|
self._build_horizontal_top_nav()
|
|
self._build_main_area(column=0, row=1)
|
|
|
|
self._navigate_to("personal")
|
|
|
|
def _build_horizontal_top_nav(self):
|
|
"""Builds a horizontal navigation bar with all options side-by-side"""
|
|
self.top_nav = ctk.CTkFrame(self, height=75, corner_radius=0, fg_color=COLOR_BG_SIDEBAR)
|
|
self.top_nav.grid(row=0, column=0, columnspan=2, sticky="ew")
|
|
|
|
# Branding (Left section)
|
|
brand_frame = ctk.CTkFrame(self.top_nav, fg_color="transparent")
|
|
brand_frame.pack(side="left", padx=20, pady=5)
|
|
|
|
ctk.CTkLabel(
|
|
brand_frame, text="LaTeX CV Builder",
|
|
font=ctk.CTkFont(family="Inter", size=15, weight="bold"),
|
|
text_color=COLOR_TEXT_PRIMARY
|
|
).pack(anchor="w")
|
|
|
|
selectors_container = ctk.CTkFrame(brand_frame, fg_color="transparent")
|
|
selectors_container.pack(anchor="w", pady=(2, 0))
|
|
|
|
self.orientation_selector = ctk.CTkComboBox(
|
|
selectors_container, values=["Horizontal Layout", "Vertical Layout"],
|
|
command=self._toggle_orientation_layout, height=24, width=130,
|
|
font=ctk.CTkFont(family="Inter", size=10)
|
|
)
|
|
self.orientation_selector.pack(side="left", padx=(0, 5))
|
|
self.orientation_selector.set("Horizontal Layout")
|
|
|
|
self.appearance_selector = ctk.CTkComboBox(
|
|
selectors_container, values=["System Mode", "Light Mode", "Dark Mode"],
|
|
command=self._update_appearance_mode, height=24, width=110,
|
|
font=ctk.CTkFont(family="Inter", size=10)
|
|
)
|
|
self.appearance_selector.pack(side="left")
|
|
self.appearance_selector.set(self.current_appearance_mode)
|
|
|
|
# Actions (Right section)
|
|
actions_frame = ctk.CTkFrame(self.top_nav, fg_color="transparent")
|
|
actions_frame.pack(side="right", padx=15, pady=10)
|
|
|
|
self.import_btn = ctk.CTkButton(
|
|
actions_frame, text="Import", command=self._import_latex,
|
|
fg_color=("#64748b", "#475569"), hover_color=("#475569", "#334155"),
|
|
text_color="white", width=80, height=34, corner_radius=6,
|
|
font=ctk.CTkFont(family="Inter", size=11, weight="bold")
|
|
)
|
|
self.import_btn.pack(side="left", padx=4)
|
|
|
|
self.generate_btn = ctk.CTkButton(
|
|
actions_frame, text="Generate", command=self._generate_latex,
|
|
fg_color=("#2563eb", "#3b82f6"), hover_color=("#1d4ed8", "#2563eb"),
|
|
text_color="white", width=90, height=34, corner_radius=6,
|
|
font=ctk.CTkFont(family="Inter", size=11, weight="bold")
|
|
)
|
|
self.generate_btn.pack(side="left", padx=4)
|
|
|
|
self.compile_btn = ctk.CTkButton(
|
|
actions_frame, text="Compile", command=self._compile_latex,
|
|
fg_color=("#16a34a", "#22c55e"), hover_color=("#15803d", "#16a34a"),
|
|
text_color="white", width=90, height=34, corner_radius=6,
|
|
font=ctk.CTkFont(family="Inter", size=11, weight="bold"), state="disabled"
|
|
)
|
|
self.compile_btn.pack(side="left", padx=4)
|
|
|
|
# Tab Selectors (Center section)
|
|
tabs_frame = ctk.CTkFrame(self.top_nav, fg_color="transparent")
|
|
tabs_frame.pack(side="left", fill="x", expand=True, padx=20)
|
|
|
|
nav_items = [
|
|
("personal", "Personal Info"),
|
|
("education", "Education"),
|
|
("coursework", "Coursework"),
|
|
("experience", "Experience"),
|
|
("projects", "Projects"),
|
|
("skills", "Skills"),
|
|
("leadership", "Leadership"),
|
|
("visual_editor", "Word Visual Editor"),
|
|
("output", "LaTeX & Live Preview")
|
|
]
|
|
|
|
self.nav_buttons = {}
|
|
for key, label in nav_items:
|
|
btn = ctk.CTkButton(
|
|
tabs_frame, text=label, command=lambda k=key: self._navigate_to(k),
|
|
fg_color="transparent", text_color=COLOR_TEXT_PRIMARY,
|
|
hover_color=("white", "gray10"), height=34, corner_radius=6,
|
|
font=ctk.CTkFont(family="Inter", size=12, weight="normal")
|
|
)
|
|
btn.pack(side="left", padx=2)
|
|
self.nav_buttons[key] = btn
|
|
|
|
def _build_vertical_sidebar(self):
|
|
"""Builds a classic side-aligned vertical sidebar layout"""
|
|
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(12, weight=1)
|
|
|
|
# Branding
|
|
brand_frame = ctk.CTkFrame(self.sidebar, fg_color="transparent")
|
|
brand_frame.grid(row=0, column=0, padx=20, pady=(25, 10), sticky="ew")
|
|
|
|
ctk.CTkLabel(
|
|
brand_frame, text="LaTeX CV Builder",
|
|
font=ctk.CTkFont(family="Inter", size=18, weight="bold"),
|
|
text_color=COLOR_TEXT_PRIMARY
|
|
).pack(anchor="w")
|
|
|
|
ctk.CTkLabel(
|
|
brand_frame, text="ATS & AI-Optimized Layouts",
|
|
font=ctk.CTkFont(family="Inter", size=11, weight="normal"),
|
|
text_color=COLOR_TEXT_MUTED
|
|
).pack(anchor="w", pady=(2, 0))
|
|
|
|
# Orientation Selector
|
|
self.orientation_selector = ctk.CTkComboBox(
|
|
self.sidebar, values=["Horizontal Layout", "Vertical Layout"],
|
|
command=self._toggle_orientation_layout, height=30,
|
|
font=ctk.CTkFont(family="Inter", size=12)
|
|
)
|
|
self.orientation_selector.grid(row=1, column=0, padx=12, pady=(0, 5), sticky="ew")
|
|
self.orientation_selector.set("Vertical Layout")
|
|
|
|
# Appearance Mode Selector
|
|
self.appearance_selector = ctk.CTkComboBox(
|
|
self.sidebar, values=["System Mode", "Light Mode", "Dark Mode"],
|
|
command=self._update_appearance_mode, height=30,
|
|
font=ctk.CTkFont(family="Inter", size=12)
|
|
)
|
|
self.appearance_selector.grid(row=2, column=0, padx=12, pady=(0, 15), sticky="ew")
|
|
self.appearance_selector.set(self.current_appearance_mode)
|
|
|
|
# Navigation
|
|
nav_items = [
|
|
("personal", "Personal Information"),
|
|
("education", "Academic Education"),
|
|
("coursework", "Relevant Coursework"),
|
|
("experience", "Work Experience"),
|
|
("projects", "Technical Projects"),
|
|
("skills", "Core Skills Matrix"),
|
|
("leadership", "Leadership & Activities"),
|
|
("visual_editor", "Word Visual Editor"),
|
|
("output", "LaTeX & Live Preview")
|
|
]
|
|
|
|
self.nav_buttons = {}
|
|
for idx, (key, label) in enumerate(nav_items, start=3):
|
|
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=36, corner_radius=6, anchor="w",
|
|
font=ctk.CTkFont(family="Inter", size=13, weight="normal")
|
|
)
|
|
btn.grid(row=idx, column=0, padx=12, pady=2, sticky="ew")
|
|
self.nav_buttons[key] = btn
|
|
|
|
# Actions
|
|
actions_frame = ctk.CTkFrame(self.sidebar, fg_color="transparent")
|
|
actions_frame.grid(row=12, 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 Label
|
|
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=13, column=0, padx=15, pady=(0, 20), sticky="w")
|
|
|
|
def _update_appearance_mode(self, selected_option):
|
|
"""Updates the visual appearance theme dynamically and syncs text formatting boxes"""
|
|
self.current_appearance_mode = selected_option
|
|
|
|
if "Light" in selected_option:
|
|
ctk.set_appearance_mode("light")
|
|
bg_color = COLOR_BG_CARD[0]
|
|
fg_color = COLOR_TEXT_PRIMARY[0]
|
|
caret_color = "black"
|
|
elif "Dark" in selected_option:
|
|
ctk.set_appearance_mode("dark")
|
|
bg_color = COLOR_BG_CARD[1]
|
|
fg_color = COLOR_TEXT_PRIMARY[1]
|
|
caret_color = "white"
|
|
else:
|
|
ctk.set_appearance_mode("system")
|
|
# Determine system theme
|
|
current_mode = ctk.get_appearance_mode()
|
|
bg_color = COLOR_BG_CARD[1] if current_mode == "Dark" else COLOR_BG_CARD[0]
|
|
fg_color = COLOR_TEXT_PRIMARY[1] if current_mode == "Dark" else COLOR_TEXT_PRIMARY[0]
|
|
caret_color = "white" if current_mode == "Dark" else "black"
|
|
|
|
if hasattr(self, "visual_text") and self.visual_text:
|
|
self.visual_text.configure(bg=bg_color, fg=fg_color, insertbackground=caret_color)
|
|
|
|
if hasattr(self, "appearance_selector") and self.appearance_selector:
|
|
self.appearance_selector.set(selected_option)
|
|
|
|
def _toggle_orientation_layout(self, selected_option):
|
|
"""Transitions between horizontal top ribbon tabs or vertical side tabs layout"""
|
|
new_mode = "horizontal" if "Horizontal" in selected_option else "vertical"
|
|
if new_mode != self.nav_orientation:
|
|
self.nav_orientation = new_mode
|
|
self._build_ui()
|
|
|
|
def _build_main_area(self, column=1, row=0):
|
|
"""Instantiates all views inside a unified structural container"""
|
|
self.main_container = ctk.CTkFrame(self, corner_radius=0, fg_color="transparent")
|
|
self.main_container.grid(row=row, column=column, sticky="nsew", padx=30, pady=30)
|
|
self.main_container.grid_columnconfigure(0, weight=1)
|
|
self.main_container.grid_rowconfigure(0, weight=1)
|
|
|
|
# Setup correct responsive layout weights
|
|
if self.nav_orientation == "horizontal":
|
|
self.grid_rowconfigure(0, weight=0)
|
|
self.grid_rowconfigure(1, weight=1)
|
|
self.grid_columnconfigure(0, weight=1)
|
|
self.grid_columnconfigure(1, weight=0)
|
|
else:
|
|
self.grid_rowconfigure(0, weight=1)
|
|
self.grid_rowconfigure(1, weight=0)
|
|
self.grid_columnconfigure(0, weight=0)
|
|
self.grid_columnconfigure(1, weight=1)
|
|
|
|
# Instantiate all sections
|
|
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["visual_editor"] = self._create_visual_editor_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,
|
|
is_combobox=False, values=None):
|
|
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
|
|
)
|
|
elif is_combobox:
|
|
field = ctk.CTkComboBox(
|
|
field_container, height=height, corner_radius=6,
|
|
border_width=1, border_color=COLOR_BORDER, fg_color=COLOR_BG_MAIN,
|
|
values=values or []
|
|
)
|
|
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_date_selectors(self, parent, row, col, columnspan=1):
|
|
"""Structured side-by-side dropdown selectors for Month/Year"""
|
|
container = ctk.CTkFrame(parent, fg_color="transparent")
|
|
container.grid(row=row, column=col, columnspan=columnspan, sticky="ew", padx=10, pady=10)
|
|
container.grid_columnconfigure((0, 1, 2, 3), weight=1)
|
|
|
|
# Start Date
|
|
ctk.CTkLabel(container, text="Start Month", font=ctk.CTkFont(family="Inter", size=11, weight="bold"),
|
|
text_color=COLOR_TEXT_MUTED).grid(row=0, column=0, sticky="w")
|
|
start_month = ctk.CTkComboBox(container, height=32, corner_radius=6, values=MONTHS_LIST, width=90)
|
|
start_month.grid(row=1, column=0, padx=(0, 5), sticky="ew")
|
|
|
|
ctk.CTkLabel(container, text="Start Year", font=ctk.CTkFont(family="Inter", size=11, weight="bold"),
|
|
text_color=COLOR_TEXT_MUTED).grid(row=0, column=1, sticky="w")
|
|
start_year = ctk.CTkComboBox(container, height=32, corner_radius=6, values=YEARS_LIST[:-1], width=90)
|
|
start_year.grid(row=1, column=1, padx=(0, 10), sticky="ew")
|
|
|
|
# End Date
|
|
ctk.CTkLabel(container, text="End Month", font=ctk.CTkFont(family="Inter", size=11, weight="bold"),
|
|
text_color=COLOR_TEXT_MUTED).grid(row=0, column=2, sticky="w")
|
|
end_month = ctk.CTkComboBox(container, height=32, corner_radius=6, values=["Present"] + MONTHS_LIST, width=90)
|
|
end_month.grid(row=1, column=2, padx=(0, 5), sticky="ew")
|
|
|
|
ctk.CTkLabel(container, text="End Year", font=ctk.CTkFont(family="Inter", size=11, weight="bold"),
|
|
text_color=COLOR_TEXT_MUTED).grid(row=0, column=3, sticky="w")
|
|
end_year = ctk.CTkComboBox(container, height=32, corner_radius=6, values=YEARS_LIST, width=90)
|
|
end_year.grid(row=1, column=3, sticky="ew")
|
|
|
|
# Set clean defaults
|
|
start_month.set("Jan")
|
|
start_year.set("2018")
|
|
end_month.set("May")
|
|
end_year.set("2022")
|
|
|
|
def handle_present_select(val):
|
|
if val == "Present" or end_year.get() == "Present":
|
|
if val == "Present":
|
|
end_year.set("Present")
|
|
end_month.set("Present")
|
|
|
|
end_month.configure(command=handle_present_select)
|
|
end_year.configure(command=handle_present_select)
|
|
|
|
return {
|
|
"start_month": start_month,
|
|
"start_year": start_year,
|
|
"end_month": end_month,
|
|
"end_year": end_year
|
|
}
|
|
|
|
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_university = self._create_form_field(card, "University / College Name", 0, 0, columnspan=2)
|
|
self.edu_degree = self._create_form_field(card, "Degree Major / Qualification", 1, 0, columnspan=2)
|
|
self.edu_dates = self._create_date_selectors(card, 2, 0, columnspan=2)
|
|
self.edu_location = self._create_form_field(card, "Institution Location (Select or Type)", 3, 0, columnspan=2,
|
|
is_combobox=True, values=SUGGESTED_LOCATIONS)
|
|
self.edu_location.set("Berkeley, CA")
|
|
|
|
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")
|
|
|
|
company = self._create_form_field(card, "Company / Employer Name", 1, 0)
|
|
title = self._create_form_field(card, "Job Title / Designation", 1, 1)
|
|
dates = self._create_date_selectors(card, 2, 0, columnspan=2)
|
|
location = self._create_form_field(card, "Work Location (Select or Type)", 3, 0, columnspan=2, is_combobox=True,
|
|
values=SUGGESTED_LOCATIONS)
|
|
location.set("San Francisco, CA")
|
|
|
|
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(({
|
|
'company': company,
|
|
'title': title,
|
|
'dates': dates,
|
|
'location': location
|
|
}, 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")
|
|
|
|
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)
|
|
|
|
ctk.CTkLabel(card, text="Execution Date", font=ctk.CTkFont(family="Inter", size=11, weight="bold"),
|
|
text_color=COLOR_TEXT_MUTED).grid(row=2, column=1, sticky="w", padx=10, pady=(10, 0))
|
|
proj_date_container = ctk.CTkFrame(card, fg_color="transparent")
|
|
proj_date_container.grid(row=3, column=1, sticky="ew", padx=10, pady=(0, 10))
|
|
proj_date_container.grid_columnconfigure((0, 1), weight=1)
|
|
|
|
proj_month = ctk.CTkComboBox(proj_date_container, height=32, corner_radius=6, values=MONTHS_LIST)
|
|
proj_month.grid(row=0, column=0, padx=(0, 5), sticky="ew")
|
|
proj_month.set("Jan")
|
|
|
|
proj_year = ctk.CTkComboBox(proj_date_container, height=32, corner_radius=6, values=YEARS_LIST[:-1])
|
|
proj_year.grid(row=0, column=1, sticky="ew")
|
|
proj_year.set("2021")
|
|
|
|
desc_text = self._create_form_field(card, "Project Deliverables / Features (One bullet point per line)", 4, 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=5, column=1, pady=(0, 15), padx=15, sticky="e")
|
|
|
|
self.proj_entries.append(({
|
|
'name': name,
|
|
'technologies': technologies,
|
|
'proj_month': proj_month,
|
|
'proj_year': proj_year
|
|
}, 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")
|
|
|
|
organization = self._create_form_field(card, "Organization / Project Initiative", 1, 0)
|
|
role = self._create_form_field(card, "Your Role / Position", 1, 1)
|
|
dates = self._create_date_selectors(card, 2, 0, columnspan=2)
|
|
location = self._create_form_field(card, "Location", 3, 0, columnspan=2, is_combobox=True,
|
|
values=SUGGESTED_LOCATIONS)
|
|
location.set("Berkeley, CA")
|
|
|
|
achieve_text = self._create_form_field(card, "Core Accomplishments / Impacts (One point per line)", 4, 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=5, column=1, pady=(0, 15), padx=15, sticky="e")
|
|
|
|
self.lead_entries.append(({
|
|
'organization': organization,
|
|
'role': role,
|
|
'dates': dates,
|
|
'location': location
|
|
}, achieve_text, card))
|
|
|
|
def _create_visual_editor_section(self):
|
|
"""Builds a Microsoft Word style rich WYSIWYG visual editor workspace"""
|
|
frame = ctk.CTkFrame(self.main_container, fg_color="transparent")
|
|
frame.grid_columnconfigure(0, weight=1)
|
|
frame.grid_rowconfigure(2, weight=1)
|
|
|
|
self._create_section_header(
|
|
frame, "Microsoft Word WYSIWYG Editor",
|
|
"Type, format, and structure with classic word-processor controls. Changes compile directly to clean LaTeX."
|
|
)
|
|
|
|
# Ribbon Style Toolbar
|
|
toolbar = ctk.CTkFrame(frame, corner_radius=8, border_width=1, border_color=COLOR_BORDER,
|
|
fg_color=COLOR_BG_SIDEBAR)
|
|
toolbar.grid(row=1, column=0, sticky="ew", padx=5, pady=(0, 10))
|
|
|
|
# Alignment / Style actions
|
|
btn_bold = ctk.CTkButton(
|
|
toolbar, text="B", width=36, height=28, corner_radius=4,
|
|
font=ctk.CTkFont(family="Inter", size=13, weight="bold"),
|
|
command=lambda: self.toggle_tag("bold")
|
|
)
|
|
btn_bold.pack(side="left", padx=4, pady=6)
|
|
|
|
btn_italic = ctk.CTkButton(
|
|
toolbar, text="I", width=36, height=28, corner_radius=4,
|
|
font=ctk.CTkFont(family="Inter", size=13, weight="normal"),
|
|
command=lambda: self.toggle_tag("italic")
|
|
)
|
|
btn_italic.pack(side="left", padx=4, pady=6)
|
|
|
|
btn_underline = ctk.CTkButton(
|
|
toolbar, text="U", width=36, height=28, corner_radius=4,
|
|
font=ctk.CTkFont(family="Inter", size=13, weight="normal"),
|
|
command=lambda: self.toggle_tag("underline")
|
|
)
|
|
btn_underline.pack(side="left", padx=4, pady=6)
|
|
|
|
# Style Ribbon Divider
|
|
divider1 = ctk.CTkFrame(toolbar, width=1, height=20, fg_color=COLOR_BORDER)
|
|
divider1.pack(side="left", padx=10, pady=6)
|
|
|
|
btn_h1 = ctk.CTkButton(
|
|
toolbar, text="Section Heading", width=120, height=28, corner_radius=4,
|
|
fg_color=("#cbd5e1", "#334155"), text_color=COLOR_TEXT_PRIMARY,
|
|
font=ctk.CTkFont(family="Inter", size=11, weight="bold"),
|
|
command=lambda: self.toggle_tag("h1")
|
|
)
|
|
btn_h1.pack(side="left", padx=4, pady=6)
|
|
|
|
btn_h2 = ctk.CTkButton(
|
|
toolbar, text="Subsection", width=95, height=28, corner_radius=4,
|
|
fg_color=("#cbd5e1", "#334155"), text_color=COLOR_TEXT_PRIMARY,
|
|
font=ctk.CTkFont(family="Inter", size=11, weight="bold"),
|
|
command=lambda: self.toggle_tag("h2")
|
|
)
|
|
btn_h2.pack(side="left", padx=4, pady=6)
|
|
|
|
divider2 = ctk.CTkFrame(toolbar, width=1, height=20, fg_color=COLOR_BORDER)
|
|
divider2.pack(side="left", padx=10, pady=6)
|
|
|
|
btn_bullet = ctk.CTkButton(
|
|
toolbar, text="• Bullet Point", width=100, height=28, corner_radius=4,
|
|
fg_color=("#cbd5e1", "#334155"), text_color=COLOR_TEXT_PRIMARY,
|
|
font=ctk.CTkFont(family="Inter", size=11),
|
|
command=lambda: self.toggle_tag("bullet")
|
|
)
|
|
btn_bullet.pack(side="left", padx=4, pady=6)
|
|
|
|
# Clear formats
|
|
btn_clear = ctk.CTkButton(
|
|
toolbar, text="Clear Formatting", width=110, height=28, corner_radius=4,
|
|
fg_color=("#fee2e2", "#7f1d1d"), hover_color=("#fecaca", "#991b1b"),
|
|
text_color=("#dc2626", "#fca5a5"),
|
|
font=ctk.CTkFont(family="Inter", size=11, weight="bold"),
|
|
command=self.clear_visual_formatting
|
|
)
|
|
btn_clear.pack(side="right", padx=10, pady=6)
|
|
|
|
# Editor Frame Wrapper
|
|
editor_wrapper = ctk.CTkFrame(frame, corner_radius=10, border_width=1, border_color=COLOR_BORDER,
|
|
fg_color=COLOR_BG_CARD)
|
|
editor_wrapper.grid(row=2, column=0, sticky="nsew", padx=5, pady=5)
|
|
editor_wrapper.grid_rowconfigure(0, weight=1)
|
|
editor_wrapper.grid_columnconfigure(0, weight=1)
|
|
|
|
scrollbar_y = ctk.CTkScrollbar(editor_wrapper)
|
|
scrollbar_y.grid(row=0, column=1, sticky="ns")
|
|
|
|
# Resolve correct starting colors for manual Tk Text integration
|
|
current_mode = ctk.get_appearance_mode()
|
|
bg_init = COLOR_BG_CARD[1] if current_mode == "Dark" else COLOR_BG_CARD[0]
|
|
fg_init = COLOR_TEXT_PRIMARY[1] if current_mode == "Dark" else COLOR_TEXT_PRIMARY[0]
|
|
caret_init = "white" if current_mode == "Dark" else "black"
|
|
|
|
self.visual_text = tk.Text(
|
|
editor_wrapper, wrap="word", undo=True, font=("Inter", 12),
|
|
bg=bg_init, fg=fg_init, insertbackground=caret_init,
|
|
relief="flat", bd=0, padx=25, pady=25,
|
|
yscrollcommand=scrollbar_y.set
|
|
)
|
|
self.visual_text.grid(row=0, column=0, sticky="nsew")
|
|
scrollbar_y.configure(command=self.visual_text.yview)
|
|
|
|
# Config standard WYSIWYG live rendering tags
|
|
self.visual_text.tag_config("bold", font=("Inter", 12, "bold"))
|
|
self.visual_text.tag_config("italic", font=("Inter", 12, "italic"))
|
|
self.visual_text.tag_config("underline", underline=True)
|
|
self.visual_text.tag_config("h1", font=("Inter", 18, "bold"), foreground="#2563eb")
|
|
self.visual_text.tag_config("h2", font=("Inter", 14, "bold"), foreground="#4b5563")
|
|
self.visual_text.tag_config("bullet", font=("Inter", 12), lmargin1=20, lmargin2=35)
|
|
|
|
self._load_visual_sample_data()
|
|
|
|
return frame
|
|
|
|
def toggle_tag(self, tag_name):
|
|
"""Applies or removes formatting blocks/inlines across active selections"""
|
|
try:
|
|
start = self.visual_text.index("sel.first")
|
|
end = self.visual_text.index("sel.last")
|
|
except tk.TclError:
|
|
return # Empty selection trigger block
|
|
|
|
# Inline vs block style tags check
|
|
ranges = self.visual_text.tag_ranges(tag_name)
|
|
has_tag = False
|
|
for i in range(0, len(ranges), 2):
|
|
if self.visual_text.compare(start, ">=", ranges[i]) and self.visual_text.compare(end, "<=", ranges[i + 1]):
|
|
has_tag = True
|
|
break
|
|
|
|
if has_tag:
|
|
self.visual_text.tag_remove(tag_name, start, end)
|
|
else:
|
|
self.visual_text.tag_add(tag_name, start, end)
|
|
|
|
def clear_visual_formatting(self):
|
|
"""Restores plain-text status across selected range block"""
|
|
try:
|
|
start = self.visual_text.index("sel.first")
|
|
end = self.visual_text.index("sel.last")
|
|
except tk.TclError:
|
|
return
|
|
for tag in ["bold", "italic", "underline", "h1", "h2", "bullet"]:
|
|
self.visual_text.tag_remove(tag, start, end)
|
|
|
|
def _load_visual_sample_data(self):
|
|
"""Hydrates the Word Visual Editor with styled CV presets matching template"""
|
|
self.visual_text.delete("1.0", "end")
|
|
|
|
sample_blocks = [
|
|
("FIRST LAST\n", ["bold", "h1"]),
|
|
("123 Street Name, Town, State 12345 | 123-456-7890 | email@gmail.com\n\n", ["italic"]),
|
|
("Education\n", ["h1"]),
|
|
("State University - Sep 2017 to May 2021\n", ["bold"]),
|
|
("Bachelor of Science in Computer Science - GPA: 3.8/4.0\n\n", []),
|
|
("Relevant Coursework\n", ["h1"]),
|
|
("• Data Structures, Algorithms Analysis, Artificial Intelligence\n", ["bullet"]),
|
|
("• Systems Programming, Software Methodology, Computer Architecture\n\n", ["bullet"]),
|
|
("Experience\n", ["h1"]),
|
|
("Electronics Company - May 2020 to Aug 2020\n", ["bold"]),
|
|
("Software Engineer Intern\n", ["italic"]),
|
|
("• Developed automated daily unit testing pipelines reducing debug loops.\n", ["bullet"]),
|
|
("• Built continuous integration automation systems using Python and PowerShell.\n\n", ["bullet"]),
|
|
("Technical Skills\n", ["h1"]),
|
|
("• Languages: Python, Java, C, HTML/CSS, JavaScript, SQL\n", ["bullet"]),
|
|
("• Tools: VS Code, Eclipse, Google Cloud Platform, Android Studio\n", ["bullet"])
|
|
]
|
|
|
|
for text, tags in sample_blocks:
|
|
start_pos = self.visual_text.index("insert")
|
|
self.visual_text.insert("insert", text)
|
|
end_pos = self.visual_text.index("insert -1c")
|
|
for tag in tags:
|
|
self.visual_text.tag_add(tag, start_pos, end_pos)
|
|
|
|
def _create_output_section(self):
|
|
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 = 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, (data, _, 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):
|
|
"""Triggers targeted compilations based on focused visual or form tabs"""
|
|
is_visual_editor_active = (self.current_section == self.section_frames["visual_editor"])
|
|
|
|
if is_visual_editor_active:
|
|
self.latex_text = self._parse_visual_to_latex()
|
|
else:
|
|
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 _parse_visual_to_latex(self):
|
|
"""Converts formatted editor text cleanly into fully custom LaTeX CV layout"""
|
|
latex = r"""\documentclass[letterpaper,11pt]{article}
|
|
\usepackage{latexsym}
|
|
\usepackage[empty]{fullpage}
|
|
\usepackage{titlesec}
|
|
\usepackage{marvosym}
|
|
\usepackage[usenames,dvipsnames]{color}
|
|
\usepackage{verbatim}
|
|
\usepackage{enumitem}
|
|
\usepackage[hidelinks]{hyperref}
|
|
\usepackage{fancyhdr}
|
|
\usepackage[english]{babel}
|
|
\usepackage{tabularx}
|
|
\input{glyphtounicode}
|
|
|
|
\pagestyle{fancy}
|
|
\fancyhf{}
|
|
\fancyfoot{}
|
|
\renewcommand{\headrulewidth}{0pt}
|
|
\renewcommand{\footrulewidth}{0pt}
|
|
|
|
\addtolength{\oddsidemargin}{-0.5in}
|
|
\addtolength{\evensidemargin}{-0.5in}
|
|
\addtolength{\textwidth}{1.0in}
|
|
\addtolength{\topmargin}{-0.5in}
|
|
\addtolength{\textheight}{1.0in}
|
|
|
|
\urlstyle{same}
|
|
\raggedbottom
|
|
\raggedright
|
|
\setlength{\tabcolsep}{0in}
|
|
|
|
\titleformat{\section}{
|
|
\vspace{-4pt}\scshape\raggedright\large
|
|
}{}{0em}{}[\color{black}\titrule \vspace{-5pt}]
|
|
|
|
\pdfgentounicode=1
|
|
|
|
\newcommand{\resumeItem}[1]{
|
|
\item\small{{#1 \vspace{-2pt}}}
|
|
}
|
|
|
|
\begin{document}
|
|
"""
|
|
lines = self.visual_text.get("1.0", "end-1c").split("\n")
|
|
in_list = False
|
|
is_first_header = True
|
|
|
|
for idx, line in enumerate(lines):
|
|
if not line.strip():
|
|
continue
|
|
|
|
line_start = f"{idx + 1}.0"
|
|
tags = self.visual_text.tag_names(line_start)
|
|
|
|
if "h1" in tags:
|
|
if in_list:
|
|
latex += "\\end{itemize}\n\n"
|
|
in_list = False
|
|
|
|
# Treat the absolute first h1 tag as Centered Name Header
|
|
if is_first_header:
|
|
latex += f"\\begin{{center}}\n \\textbf{{\\Huge \\scshape {self.escape_latex(line.strip())}}} \\\\\n"
|
|
is_first_header = False
|
|
else:
|
|
latex += f"\\section{{{self.escape_latex(line.strip())}}}\n"
|
|
|
|
elif "h2" in tags:
|
|
if in_list:
|
|
latex += "\\end{itemize}\n\n"
|
|
in_list = False
|
|
latex += f"\\subsection*{{{self.escape_latex(line.strip())}}}\n"
|
|
|
|
elif line.strip().startswith("•") or "bullet" in tags:
|
|
if not in_list:
|
|
latex += "\\begin{itemize}[leftmargin=0.15in, label={}]\n"
|
|
in_list = True
|
|
cleaned_item = line.replace("•", "").strip()
|
|
formatted_item = self._format_line_text(idx + 1, cleaned_item)
|
|
latex += f" \\resumeItem{{{formatted_item}}}\n"
|
|
|
|
else:
|
|
if in_list:
|
|
latex += "\\end{itemize}\n\n"
|
|
in_list = False
|
|
|
|
formatted_line = self._format_line_text(idx + 1, line)
|
|
if not is_first_header and idx == 1: # Address/Contact subtitle line centered
|
|
latex += f" \\small {formatted_line} \\\\\n\\end{{center}}\n\n"
|
|
else:
|
|
latex += f"{formatted_line} \\\\\n"
|
|
|
|
if in_list:
|
|
latex += "\\end{itemize}\n"
|
|
|
|
latex += "\\end{document}"
|
|
return latex
|
|
|
|
def _format_line_text(self, line_num, text_str):
|
|
"""Iterates char tags to produce clean nested LaTeX formatting tags with robust escaping"""
|
|
formatted = ""
|
|
active_tags = set()
|
|
|
|
for char_idx, char in enumerate(text_str):
|
|
char_pos = f"{line_num}.{char_idx}"
|
|
tags_at_char = set(self.visual_text.tag_names(char_pos))
|
|
tags_at_char = {t for t in tags_at_char if t in ("bold", "italic", "underline")}
|
|
|
|
# Orderly tag closing
|
|
if "bold" in active_tags and "bold" not in tags_at_char:
|
|
formatted += "}"
|
|
active_tags.remove("bold")
|
|
if "italic" in active_tags and "italic" not in tags_at_char:
|
|
formatted += "}"
|
|
active_tags.remove("italic")
|
|
if "underline" in active_tags and "underline" not in tags_at_char:
|
|
formatted += "}"
|
|
active_tags.remove("underline")
|
|
|
|
# Orderly tag opening
|
|
if "underline" in tags_at_char and "underline" not in active_tags:
|
|
formatted += "\\underline{"
|
|
active_tags.add("underline")
|
|
if "italic" in tags_at_char and "italic" not in active_tags:
|
|
formatted += "\\textit{"
|
|
active_tags.add("italic")
|
|
if "bold" in tags_at_char and "bold" not in active_tags:
|
|
formatted += "\\textbf{"
|
|
active_tags.add("bold")
|
|
|
|
# Escaping LaTeX syntax
|
|
if char == "&":
|
|
formatted += "\\&"
|
|
elif char == "%":
|
|
formatted += "\\%"
|
|
elif char == "$":
|
|
formatted += "\\$"
|
|
elif char == "_":
|
|
formatted += "\\_"
|
|
elif char == "#":
|
|
formatted += "\\#"
|
|
elif char == "{":
|
|
formatted += "\\{"
|
|
elif char == "}":
|
|
formatted += "\\}"
|
|
else:
|
|
formatted += char
|
|
|
|
for tag in list(active_tags):
|
|
formatted += "}"
|
|
|
|
return formatted
|
|
|
|
def _build_latex_cv(self):
|
|
"""Generates traditional high-fidelity structure matching Template CV exactly, escaping inputs"""
|
|
|
|
def get_date_range_str(d):
|
|
s_m, s_y, e_m, e_y = d['start_month'].get(), d['start_year'].get(), d['end_month'].get(), d[
|
|
'end_year'].get()
|
|
if e_m == "Present" or e_y == "Present":
|
|
return f"{s_m} {s_y} -- Present"
|
|
return f"{s_m} {s_y} -- {e_m} {e_y}"
|
|
|
|
latex = r"""\documentclass[letterpaper,11pt]{article}
|
|
\usepackage{latexsym}
|
|
\usepackage[empty]{fullpage}
|
|
\usepackage{titlesec}
|
|
\usepackage{marvosym}
|
|
\usepackage[usenames,dvipsnames]{color}
|
|
\usepackage{verbatim}
|
|
\usepackage{enumitem}
|
|
\usepackage[hidelinks]{hyperref}
|
|
\usepackage{fancyhdr}
|
|
\usepackage[english]{babel}
|
|
\usepackage{tabularx}
|
|
\input{glyphtounicode}
|
|
|
|
\pagestyle{fancy}
|
|
\fancyhf{}
|
|
\fancyfoot{}
|
|
\renewcommand{\headrulewidth}{0pt}
|
|
\renewcommand{\footrulewidth}{0pt}
|
|
|
|
\addtolength{\oddsidemargin}{-0.5in}
|
|
\addtolength{\evensidemargin}{-0.5in}
|
|
\addtolength{\textwidth}{1.0in}
|
|
\addtolength{\topmargin}{-0.5in}
|
|
\addtolength{\textheight}{1.0in}
|
|
|
|
\urlstyle{same}
|
|
\raggedbottom
|
|
\raggedright
|
|
\setlength{\tabcolsep}{0in}
|
|
|
|
% Sections formatting - Left-aligned with an elegant horizontal divider rule
|
|
\titleformat{\section}{
|
|
\vspace{-4pt}\scshape\raggedright\large
|
|
}{}{0em}{}[\color{black}\titrule \vspace{-5pt}]
|
|
|
|
\pdfgentounicode=1
|
|
|
|
\newcommand{\resumeItem}[1]{
|
|
\item\small{{#1 \vspace{-2pt}}}
|
|
}
|
|
|
|
\newcommand{\resumeSubheading}[4]{
|
|
\vspace{-1pt}\item
|
|
\begin{tabular*}{0.97\textwidth}[t]{l@{\extracolsep{\fill}}r}
|
|
\textbf{#1} & #2 \\
|
|
\textit{#3} & \textit{#4} \\
|
|
\end{tabular*}\vspace{-5pt}
|
|
}
|
|
|
|
\newcommand{\resumeProjectHeading}[2]{
|
|
\vspace{-1pt}\item
|
|
\begin{tabular*}{0.97\textwidth}{l@{\extracolsep{\fill}}r}
|
|
\textbf{#1} & #2 \\
|
|
\end{tabular*}\vspace{-5pt}
|
|
}
|
|
|
|
\renewcommand\labelitemii{$\vcenter{\hbox{\tiny$\bullet$}}$}
|
|
|
|
\newcommand{\resumeSubHeadingListStart}{\begin{itemize}[leftmargin=0.15in, label={}]}
|
|
\newcommand{\resumeSubHeadingListEnd}{\end{itemize}}
|
|
\newcommand{\resumeItemListStart}{\begin{itemize}}
|
|
\newcommand{\resumeItemListEnd}{\end{itemize}\vspace{-5pt}}
|
|
|
|
\begin{document}
|
|
|
|
"""
|
|
# Center aligned Name Header
|
|
first = self.escape_latex(self.first_name.get().strip().upper())
|
|
last = self.escape_latex(self.last_name.get().strip().upper())
|
|
latex += f"\\begin{{center}}\n"
|
|
latex += f" \\textbf{{\\Huge \\scshape {first} {last}}} \\\\\n"
|
|
|
|
address = self.escape_latex(self.personal_vars['address'].get().strip())
|
|
if address:
|
|
latex += f" \\small {address} \\\\\n"
|
|
|
|
contacts = []
|
|
if self.personal_vars['phone'].get().strip():
|
|
p = self.escape_latex(self.personal_vars['phone'].get().strip())
|
|
contacts.append(f"\\small {p}")
|
|
if self.personal_vars['email'].get().strip():
|
|
e = self.personal_vars['email'].get().strip()
|
|
# href paths should not contain escaped structures
|
|
contacts.append(f"\\href{{mailto:{e}}}{{{self.escape_latex(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/{self.escape_latex(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/{self.escape_latex(g)}}}")
|
|
|
|
latex += " \\ $|$ \\ ".join(contacts) + "\n\\end{center}\n\n"
|
|
|
|
# Education Section
|
|
if self.edu_university.get().strip():
|
|
latex += "\\section{Education}\n"
|
|
latex += "\\resumeSubHeadingListStart\n"
|
|
uni = self.escape_latex(self.edu_university.get().strip())
|
|
deg = self.escape_latex(self.edu_degree.get().strip())
|
|
loc = self.escape_latex(self.edu_location.get().strip())
|
|
date_str = self.escape_latex(get_date_range_str(self.edu_dates))
|
|
latex += f" \\resumeSubheading\n" \
|
|
f" {{{uni}}}{{{date_str}}}\n" \
|
|
f" {{{deg}}}{{{loc}}}\n"
|
|
latex += "\\resumeSubHeadingListEnd\n\n"
|
|
|
|
# Relevant Coursework Section (Configured with high-fidelity side-by-side spacing)
|
|
cw = self.coursework.get("1.0", tk.END).strip()
|
|
if cw:
|
|
latex += "\\section{Relevant Coursework}\n"
|
|
courses = [self.escape_latex(c.strip()) for c in cw.split(',') if c.strip()]
|
|
latex += "\\begin{itemize}[leftmargin=0.15in, label={}]\n"
|
|
latex += " \\item\\small{\n"
|
|
# Format clean multi-column layouts securely inside standard tables to prevent overflow
|
|
latex += " \\begin{tabular*}{0.97\\textwidth}[t]{l@{\\extracolsep{\\fill}}l@{\\extracolsep{\\fill}}l@{\\extracolsep{\\fill}}l}\n"
|
|
for i in range(0, len(courses), 4):
|
|
chunk = courses[i:i + 4]
|
|
while len(chunk) < 4:
|
|
chunk.append("")
|
|
row_str = " & ".join([f"$\\bullet$ {c}" if c else "" for c in chunk])
|
|
latex += f" {row_str} \\\\\n"
|
|
latex += " \\end{tabular*}\n"
|
|
latex += " }\n\\end{itemize}\n\n"
|
|
|
|
# Professional Experience Section
|
|
if self.exp_entries:
|
|
latex += "\\section{Experience}\n"
|
|
latex += "\\resumeSubHeadingListStart\n"
|
|
for v, t, _ in self.exp_entries:
|
|
comp = self.escape_latex(v['company'].get().strip())
|
|
if not comp: continue
|
|
title = self.escape_latex(v['title'].get().strip())
|
|
loc = self.escape_latex(v['location'].get().strip())
|
|
date_str = self.escape_latex(get_date_range_str(v['dates']))
|
|
latex += f" \\resumeSubheading\n" \
|
|
f" {{{comp}}}{{{date_str}}}\n" \
|
|
f" {{{title}}}{{{loc}}}\n"
|
|
|
|
resp = t.get("1.0", tk.END).strip()
|
|
if resp:
|
|
latex += " \\resumeItemListStart\n"
|
|
for r in resp.split('\n'):
|
|
if r.strip():
|
|
latex += f" \\resumeItem{{{self.escape_latex(r.strip())}}}\n"
|
|
latex += " \\resumeItemListEnd\n"
|
|
latex += "\\resumeSubHeadingListEnd\n\n"
|
|
|
|
# Technical Projects Section
|
|
if self.proj_entries:
|
|
latex += "\\section{Projects}\n"
|
|
latex += "\\resumeSubHeadingListStart\n"
|
|
for v, t, _ in self.proj_entries:
|
|
name = self.escape_latex(v['name'].get().strip())
|
|
if not name: continue
|
|
tech = self.escape_latex(v['technologies'].get().strip())
|
|
p_m = self.escape_latex(v['proj_month'].get())
|
|
p_y = self.escape_latex(v['proj_year'].get())
|
|
date_str = f"{p_m} {p_y}"
|
|
|
|
tech_str = f" \\ $|$ \\ \\textit{{{tech}}}" if tech else ""
|
|
latex += f" \\resumeProjectHeading\n" \
|
|
f" {{\\textbf{{{name}}}{tech_str}}}{{{date_str}}}\n"
|
|
|
|
desc = t.get("1.0", tk.END).strip()
|
|
if desc:
|
|
latex += " \\resumeItemListStart\n"
|
|
for d in desc.split('\n'):
|
|
if d.strip():
|
|
latex += f" \\resumeItem{{{self.escape_latex(d.strip())}}}\n"
|
|
latex += " \\resumeItemListEnd\n"
|
|
latex += "\\resumeSubHeadingListEnd\n\n"
|
|
|
|
# Technical Skills Matrix
|
|
has_skills = False
|
|
skills_latex = "\\section{Technical Skills}\n" \
|
|
"\\begin{itemize}[leftmargin=0.15in, label={}]\n" \
|
|
" \\small{\\item{\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" \\textbf{{{label}:}} {{{self.escape_latex(txt)}}} \\\\\n"
|
|
has_skills = True
|
|
skills_latex += " }}\n\\end{itemize}\n"
|
|
if has_skills:
|
|
latex += skills_latex + "\n"
|
|
|
|
# Leadership & Extracurricular Activities
|
|
if self.lead_entries:
|
|
latex += "\\section{Leadership / Extracurricular}\n"
|
|
latex += "\\resumeSubHeadingListStart\n"
|
|
for v, t, _ in self.lead_entries:
|
|
org = self.escape_latex(v['organization'].get().strip())
|
|
if not org: continue
|
|
role = self.escape_latex(v['role'].get().strip())
|
|
loc = self.escape_latex(v['location'].get().strip())
|
|
date_str = self.escape_latex(get_date_range_str(v['dates']))
|
|
|
|
latex += f" \\resumeSubheading\n" \
|
|
f" {{{org}}}{{{date_str}}}\n" \
|
|
f" {{{role}}}{{{loc}}}\n"
|
|
|
|
ach = t.get("1.0", tk.END).strip()
|
|
if ach:
|
|
latex += " \\resumeItemListStart\n"
|
|
for a in ach.split('\n'):
|
|
if a.strip():
|
|
latex += f" \\resumeItem{{{self.escape_latex(a.strip())}}}\n"
|
|
latex += " \\resumeItemListEnd\n"
|
|
latex += "\\resumeSubHeadingListEnd\n\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):
|
|
# Personal Name Mapping
|
|
name_match = re.search(r'\\textbf\{\\Huge \\scshape (.*?)\}', 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 "")
|
|
|
|
edu_match = re.search(r'\\resumeSubheading\s*\{(.*?)\}\s*\{(.*?)\}\s*\{(.*?)\}\s*\{(.*?)\}', latex_text)
|
|
if edu_match:
|
|
self.edu_university.delete(0, tk.END)
|
|
self.edu_university.insert(0, edu_match.group(1))
|
|
self.edu_degree.delete(0, tk.END)
|
|
self.edu_degree.insert(0, edu_match.group(3))
|
|
self.edu_location.set(edu_match.group(4))
|
|
|
|
cw_match = re.search(r'\\section\{Relevant Coursework\}.*?\\item\\small\{(.*?)\}', latex_text, re.DOTALL)
|
|
if cw_match:
|
|
self.coursework.delete("1.0", tk.END)
|
|
cleaned_cw = cw_match.group(1).replace('\\item', '').replace('\\quad $\\bullet$', ',').replace('$\\bullet$',
|
|
'').replace(
|
|
'\\\\', '').replace('\n', '').strip()
|
|
self.coursework.insert("1.0", cleaned_cw)
|
|
|
|
self._set_status("Asset import matching done. Review 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")
|
|
# Show useful debug log window to trace markup structural failures
|
|
log_window = tk.Toplevel(self)
|
|
log_window.title("Compilation Logs / Diagnostic Window")
|
|
log_window.geometry("700x500")
|
|
log_text = ctk.CTkTextbox(log_window, wrap="word")
|
|
log_text.pack(fill="both", expand=True, padx=10, pady=10)
|
|
log_text.insert("1.0", result.stdout + "\n" + result.stderr)
|
|
messagebox.showerror("Compilation Error",
|
|
"LaTeX subprocess run failed. Review Diagnostics logs opened on screen.")
|
|
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):
|
|
"""Populates form controls with the exact high-fidelity template values out of the box"""
|
|
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_university.insert(0, "State University")
|
|
self.edu_degree.insert(0, "Bachelor of Science in Computer Science")
|
|
self.edu_dates['start_month'].set("Sep")
|
|
self.edu_dates['start_year'].set("2017")
|
|
self.edu_dates['end_month'].set("May")
|
|
self.edu_dates['end_year'].set("2021")
|
|
self.edu_location.set("City, State")
|
|
|
|
self.coursework.insert("1.0",
|
|
"Data Structures, Algorithms Analysis, Artificial Intelligence, Systems Programming, Software Methodology, Database Management, Internet Technology, Computer Architecture")
|
|
|
|
# Programmatically populate Experience Card Nodes matching template CV
|
|
for _, _, card in self.exp_entries:
|
|
card.destroy()
|
|
self.exp_entries.clear()
|
|
|
|
# Job 1
|
|
self._add_experience_entry()
|
|
job1 = self.exp_entries[0]
|
|
job1[0]['company'].insert(0, "Electronics Company")
|
|
job1[0]['title'].insert(0, "Software Engineer Intern")
|
|
job1[0]['dates']['start_month'].set("May")
|
|
job1[0]['dates']['start_year'].set("2020")
|
|
job1[0]['dates']['end_month'].set("Aug")
|
|
job1[0]['dates']['end_year'].set("2020")
|
|
job1[0]['location'].set("City, State")
|
|
job1[1].insert("1.0",
|
|
"Developed a service to automatically perform a set of unit tests daily on a product in development in order to decrease time needed for team members to identify and fix bugs/issues.\n"
|
|
"Incorporated scripts using Python and PowerShell to aggregate XML test results into an organized format and to load the latest build code onto the hardware, so that daily testing can be performed.\n"
|
|
"Utilized Jenkins to provide a continuous integration service in order to automate the entire process of loading the latest build code and test files, running the tests, and generating a report of the results once per day.\n"
|
|
"Explored ways to visualize and send a daily report of test results to team members using HTML, Javascript, and CSS."
|
|
)
|
|
|
|
# Job 2
|
|
self._add_experience_entry()
|
|
job2 = self.exp_entries[1]
|
|
job2[0]['company'].insert(0, "Startup, Inc")
|
|
job2[0]['title'].insert(0, "Front End Developer Intern")
|
|
job2[0]['dates']['start_month'].set("May")
|
|
job2[0]['dates']['start_year'].set("2019")
|
|
job2[0]['dates']['end_month'].set("Aug")
|
|
job2[0]['dates']['end_year'].set("2019")
|
|
job2[0]['location'].set("City, State")
|
|
job2[1].insert("1.0",
|
|
"Assisted in development of the front end of a mobile application for iOS/Android using Dart and the Flutter framework.\n"
|
|
"Worked with Google Firebase to manage user inputted data across multiple platforms including web and mobile apps.\n"
|
|
"Collaborated with team members using version control systems such as Git to organize modifications and assign tasks.\n"
|
|
"Utilized Android Studio as a development environment in order to visualize the application in both iOS and Android."
|
|
)
|
|
|
|
# Clear and load 3 projects from template CV
|
|
for _, _, card in self.proj_entries:
|
|
card.destroy()
|
|
self.proj_entries.clear()
|
|
|
|
# Project 1
|
|
self._add_project_entry()
|
|
p1 = self.proj_entries[0]
|
|
p1[0]['name'].insert(0, "Gym Reservation Bot")
|
|
p1[0]['technologies'].insert(0, "Python, Selenium, Google Cloud Console")
|
|
p1[0]['proj_month'].set("Jan")
|
|
p1[0]['proj_year'].set("2021")
|
|
p1[1].insert("1.0",
|
|
"Developed an automatic bot using Python and Google Cloud Console to register myself for a timeslot at my school gym.\n"
|
|
"Implemented Selenium to create an instance of Chrome in order to interact with the correct elements of the web page.\n"
|
|
"Created a Linux virtual machine to run on Google Cloud so that the program is able to run everyday from the cloud.\n"
|
|
"Used Cron to schedule the program to execute automatically at 11 AM every morning so a reservation is made for me."
|
|
)
|
|
|
|
# Project 2
|
|
self._add_project_entry()
|
|
p2 = self.proj_entries[1]
|
|
p2[0]['name'].insert(0, "Ticket Price Calculator App")
|
|
p2[0]['technologies'].insert(0, "Java, Android Studio")
|
|
p2[0]['proj_month'].set("Nov")
|
|
p2[0]['proj_year'].set("2020")
|
|
p2[1].insert("1.0",
|
|
"Created an Android application using Java and Android Studio to calculate ticket prices for trips to museums in NYC.\n"
|
|
"Processed user inputted information in the back-end of the app to return a subtotal price based on the tickets selected.\n"
|
|
"Utilized the layout editor to create a UI for the application in order to allow different scenes to interact with each other."
|
|
)
|
|
|
|
# Project 3
|
|
self._add_project_entry()
|
|
p3 = self.proj_entries[2]
|
|
p3[0]['name'].insert(0, "Transaction Management GUI")
|
|
p3[0]['technologies'].insert(0, "Java, Eclipse, JavaFX")
|
|
p3[0]['proj_month'].set("Oct")
|
|
p3[0]['proj_year'].set("2020")
|
|
p3[1].insert("1.0",
|
|
"Designed a sample banking transaction system using Java to simulate the common functions of using a bank account.\n"
|
|
"Used JavaFX to create a GUI that supports actions such as creating an account, deposit, withdraw, list all accounts, etc.\n"
|
|
"Implemented object-oriented programming practices such as inheritance to create different account types and databases."
|
|
)
|
|
|
|
# Technical Skills
|
|
self.skills_vars['languages'].insert("1.0", "Python, Java, C, HTML/CSS, JavaScript, SQL")
|
|
self.skills_vars['tools'].insert("1.0", "VS Code, Eclipse, Google Cloud Platform, Android Studio")
|
|
self.skills_vars['technologies'].insert("1.0", "Linux, Jenkins, GitHub, JUnit, WordPress")
|
|
|
|
# Leadership / Extracurricular Node
|
|
for _, _, card in self.lead_entries:
|
|
card.destroy()
|
|
self.lead_entries.clear()
|
|
|
|
self._add_leadership_entry()
|
|
l1 = self.lead_entries[0]
|
|
l1[0]['organization'].insert(0, "Fraternity")
|
|
l1[0]['role'].insert(0, "President")
|
|
l1[0]['dates']['start_month'].set("Spring")
|
|
l1[0]['dates']['start_year'].set("2020")
|
|
l1[0]['dates']['end_month'].set("Present")
|
|
l1[0]['dates']['end_year'].set("Present")
|
|
l1[0]['location'].set("University Name")
|
|
l1[1].insert("1.0",
|
|
"Achieved a 4 star fraternity ranking by the Office of Fraternity and Sorority Affairs (highest possible ranking).\n"
|
|
"Managed executive board of 5 members and ran weekly meetings to oversee progress in essential parts of the chapter.\n"
|
|
"Led chapter of 30+ members to work towards goals that improve and promote community service, academics, and unity."
|
|
)
|
|
|
|
|
|
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() |