__________
https://linktr.ee/marcususa
Wednesday, July 8, 2026
Green, Blue, Amber "inspection" criteria
import customtkinter as ctk
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from pathlib import Path
import chess
import chess.pgn
import json
import os
# ----------------------------
# Main Window
# ----------------------------
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
app = ctk.CTk()
style = ttk.Style()
style.theme_use("alt")
print(style.theme_names())
reopen_btn = ctk.CTkLabel(
app,
text="🮟",
font=("Arial", 16, "bold"),
text_color = ("#515985")
)
reopen_btn.place(x=2, rely=1.0, y=0, anchor="sw")
app.configure(fg_color="#172134")
app.title("🩒 Chess Catalog 🩒")
app.geometry("800x700") #change default width later to be longer
# ----------------------------
# Configure Grid
# ----------------------------
app.grid_rowconfigure(1, weight=1)
app.grid_columnconfigure(1, weight=1)
# ----------------------------
# PGN Collection
# ----------------------------
imported_files = []
cataloged_files = []
sidebar_visible = True
# ----------------------------
# Import PGN
# ----------------------------
def show_import_workspace(filename):
global add_catalog_button
# Scan the entire PGN and collect every unique header tag
all_tags = set()
with open(filename, encoding="utf-8") as pgn_file:
while True:
game = chess.pgn.read_game(pgn_file)
if game is None:
break
all_tags.update(game.headers.keys())
# No games found
if not all_tags:
return
print("all_tags =", all_tags)
print("Number of unique tags =", len(all_tags))
game_data_tags = []
# Event
if "Event" in all_tags:
game_data_tags.append("Event")
# Round
if "Round" in all_tags:
game_data_tags.append("Round")
# Players (White and/or Black)
if "White" in all_tags or "Black" in all_tags:
game_data_tags.append("Players")
# Result
if "Result" in all_tags:
game_data_tags.append("Result")
# Title (WhiteTitle and/or BlackTitle)
if "WhiteTitle" in all_tags or "BlackTitle" in all_tags:
game_data_tags.append("Title")
other_data_tags = []
column1_groups = {
"Event",
"Site",
"Date",
"Round",
"Players",
"Result",
"Title",
}
# Clear the workspace
for widget in workspace.winfo_children():
widget.destroy()
# Title
title = ctk.CTkLabel(
workspace,
text=f"{Path(filename).name}",
font=("Arial", 24, "bold")
)
title.pack(
anchor="w",
padx=20,
pady=(15, 20)
)
# Button frame
button_frame = ctk.CTkFrame(workspace, fg_color="transparent")
button_frame.pack(anchor="w", padx=20, pady=5)
# Import, Add to Catalog, Catalog buttons
add_catalog_button = ctk.CTkButton(
button_frame,
text="Add to Catalog",
command=catalog_pgns
)
add_catalog_button.pack(side="left", padx=(10, 0))
ctk.CTkButton(
button_frame,
text="Import",
command=import_pgn
).pack(side="left", padx=(10, 0))
ctk.CTkButton(
button_frame,
text="Catalog",
command=show_catalog_workspace
).pack(side="left", padx=(10, 0))
# Main container
options_frame = ctk.CTkFrame(workspace,
fg_color="#283858"
)#game_data_panel
options_frame.pack(fill="x", padx=20, pady=10)
# Three columns
game_data_frame = ctk.CTkFrame(options_frame, #fg_color="#34426e" fg_color="#00ff00"
fg_color="#283858") #column color over game_data_panel_background
other_data_frame = ctk.CTkFrame(options_frame, fg_color="#283858") #same
depth_rating = ctk.CTkFrame(options_frame, fg_color="#283858") #same
# Pack the columns
for col in (game_data_frame, other_data_frame, depth_rating):
col.pack(side="left", anchor="n", padx=(10, 30), pady=10)
# ---------- Column 1 ----------
ctk.CTkLabel(
game_data_frame,
text="Game Data",
font=("Arial", 16, "bold")
).pack(anchor="w", pady=(0, 10))
game_data_vars = {}
for tag in game_data_tags:
game_data_vars[tag] = tk.BooleanVar(value=False)
ctk.CTkCheckBox(
game_data_frame,
text=tag,
variable=game_data_vars[tag],
fg_color="#68aa68",
hover_color="#347434",
border_color="#68aa68",
checkmark_color="#346834",
text_color="#aabbaa"
).pack(anchor="w", pady=4)
# ---------- Column 2 ----------
ctk.CTkLabel(
other_data_frame,
text="",
height=28
).pack(anchor="w", pady=(0, 10))
# Elo (WhiteElo and/or BlackElo)
if "WhiteElo" in all_tags or "BlackElo" in all_tags:
other_data_tags.append("Elo")
# FideId (WhiteFideId and/or BlackFideId)
if "WhiteFideId" in all_tags or "BlackFideId" in all_tags:
other_data_tags.append("FideId")
# EventDate
if "EventDate" in all_tags:
other_data_tags.append("EventDate")
# EventType
if "EventType" in all_tags:
other_data_tags.append("EventType")
other_data_vars = {}
print(other_data_tags)
print(len(other_data_tags))
print("other_data_tags =", other_data_tags)
print("Number of displayed tags =", len(other_data_tags))
# Remove tags that are already represented in Columns 1 and 2
known_tags = {
"Fen",
"White", "Black",
"Result",
"ECO",
"Opening",
"Variation",
"WhiteTitle", "BlackTitle",
"WhiteElo", "BlackElo",
"WhiteFideId", "BlackFideId",
"EventDate",
"EventType"
}
# Standard tags that belong in Column 2
recognized_other_tags = [
tag for tag in (
"WhiteFideId", "BlackFideId",
"EventDate",
"EventType"
)
if tag in all_tags
]
# Unknown or misspelled tags
unknown_tags = sorted(
tag for tag in all_tags
if tag not in known_tags
)
# Show recognized tags first
other_data_tags.extend(recognized_other_tags)
# Then show unknown tags
other_data_tags.extend(unknown_tags)
other_data_tags = [
tag for tag in other_data_tags
if not tag.startswith(("White", "Black"))
]
other_data_tags = [
tag for tag in other_data_tags
if tag not in column1_groups
]
for tag in other_data_tags:
other_data_vars[tag] = tk.BooleanVar(value=False)
# Amber if the tag is unknown
if tag in unknown_tags:
ctk.CTkCheckBox(
other_data_frame,
text=tag,
variable=other_data_vars[tag],
fg_color="#d98c00",
hover_color="#b36f00",
border_color="#d98c00",
checkmark_color="#8a5600",
text_color="#ffcc66"
).pack(anchor="w", pady=4)
# Blue if the tag is recognized
else:
ctk.CTkCheckBox(
other_data_frame,
text=tag,
variable=other_data_vars[tag],
fg_color="#3d6ca8",
hover_color="#2d5a90",
border_color="#3d6ca8",
checkmark_color="#214c7a",
text_color="#82b8ff"
).pack(anchor="w", pady=4)
# ---------- Column 3 ----------
ctk.CTkLabel(
depth_rating,
text="Depth Rating"
).pack(anchor="w", pady=(35, 0))
depth_menu = ctk.CTkOptionMenu(
depth_rating,
values=[
"15 Moves",
"20 Moves",
"25 Moves",
"30 Moves",
"All Moves"
]
)
depth_menu.pack(anchor="w")
depth_menu.set("15 Moves")
ctk.CTkLabel(
depth_rating,
text="Rating Filter"
).pack(anchor="w", pady=(20, 0))
rating_filter = ctk.CTkOptionMenu(
depth_rating,
values=[
"2100+",
"2400+",
"2600+",
"All"
]
)
rating_filter.pack(anchor="w")
rating_filter.set("2100+")
def import_pgn():
print("1 - import_pgn started")
filename = filedialog.askopenfilename(
title="Import PGN",
filetypes=[("PGN Files", "*.pgn")]
)
print("2 - file dialog returned")
if not filename:
return
print(f"3 - Selected: {filename}")
show_import_workspace(filename)
print("4 - show_import_workspace finished")
# Don't allow the same file twice
if filename in imported_files:
status.configure(
text=f"{Path(filename).name} is already imported."
)
return
imported_files.append(filename)
short_name = Path(filename).name
sidebar.insert(
pgn_node,
"end",
text=short_name
)
status.configure(
text=f"Imported: {short_name}"
)
status.configure(
text=f"Imported: {short_name}"
)
def import_fen():
print("Import FEN selected")
# ----------------------------
# Menu Bar
# ----------------------------
menu_bar = tk.Menu(app)
app.config(menu=menu_bar)
def catalog_pgns():
global add_catalog_button
if not imported_files:
status.configure(
text="No PGNs imported to catalog."
)
return
add_catalog_button.configure(text="Please Wait...")
workspace.update()
new_files = []
total_games = 0
if os.path.exists("personal_catalog.json"):
with open("personal_catalog.json", "r", encoding="utf-8") as f:
catalog = json.load(f)
cataloged_files = catalog.get("cataloged_files", [])
else:
catalog = {
"cataloged_files": [],
"A": {},
"B": {},
"C": {},
"D": {},
"E": {}
}
cataloged_files = []
for filename in imported_files:
if filename in cataloged_files:
continue
with open(filename, encoding="utf-8") as pgn:
while True:
game = chess.pgn.read_game(pgn)
if game is None:
break
total_games += 1
eco = game.headers.get("ECO", "").strip()
opening = game.headers.get("Opening", "").strip()
variation = game.headers.get("Variation", "").strip()
if eco and opening:
group = eco[0]
if eco not in catalog[group]:
catalog[group][eco] = {
"opening": opening,
"variation": variation,
"frequency": 0
}
catalog[group][eco]["frequency"] += 1
cataloged_files.append(filename)
new_files.append(filename)
if not new_files:
add_catalog_button.configure(text="Add to Catalog")
status.configure(
text="PGNs already cataloged. Import new games to catalog more."
)
return
print(catalog)
catalog["cataloged_files"] = cataloged_files
with open("personal_catalog.json", "w", encoding="utf-8") as f:
json.dump(catalog, f, indent=4, sort_keys=True)
print("JSON written.")
status.configure(
text=f"Cataloged {total_games} game(s) from {len(new_files)} PGN(s)."
)
def clear_catalog():
answer = messagebox.askyesno(
"Clear Catalog",
"Delete the entire opening catalog?"
)
if not answer:
return
if os.path.exists("personal_catalog.json"):
os.remove("personal_catalog.json")
cataloged_files.clear()
status.configure(
text="Catalog cleared."
)
show_catalog_workspace()
def show_catalog_workspace():
# Clear the workspace
for widget in workspace.winfo_children():
widget.destroy()
print("A")
# Title
title = ctk.CTkLabel(
workspace,
text="Opening Catalog",
font=("Arial", 24, "bold")
)
title.pack(anchor="w", padx=20, pady=(15, 10))
print("B")
# ----- Table Frame -----
table_frame = ctk.CTkFrame(workspace)
table_frame.pack(fill="both", anchor="w", expand=True, padx=(0, 0), pady=(0, 0))
columns = (
"Select",
"Games",
"ECO",
"Opening",
"Variation",
)
catalog_table = ttk.Treeview(
table_frame,
columns=columns,
show="headings"
)
# Headings
catalog_table.heading("Select", anchor="center", text=" ☐")
catalog_table.heading("Games", anchor="center", text=" Games")
catalog_table.heading("ECO", anchor="w", text=" ECO")
catalog_table.heading("Opening", anchor="w", text=" Opening")
catalog_table.heading("Variation", anchor="w", text=" Variation")
# Column widths
catalog_table.column("Select", width=45, anchor="center", stretch=False)
catalog_table.column("Games", width=70, anchor="center", stretch=False)
catalog_table.column("ECO", width=50, anchor="w", stretch=False)
catalog_table.column("Opening", width=150, anchor ="w", stretch=False)
catalog_table.column("Variation", width=200, anchor="w", stretch=True)
# Scrollbar
scrollbar = ttk.Scrollbar(
table_frame,
orient="vertical",
command=catalog_table.yview
)
catalog_table.configure(yscrollcommand=scrollbar.set)
catalog_table.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# -------------------------
# Load catalog from JSON
# -------------------------
if not os.path.exists("personal_catalog.json"):
return
with open("personal_catalog.json", "r", encoding="utf-8") as f:
catalog = json.load(f)
rows = []
total_games = 0
for group in catalog:
if group == "cataloged_files":
continue
for eco, entry in catalog[group].items():
opening = entry.get("opening", "")
variation = entry.get("variation", "")
games = entry.get("frequency", 0)
total_games += games
rows.append((
games,
eco,
opening,
variation
))
# Highest frequency first
rows.sort(key=lambda row: row[0], reverse=True)
# Insert into table
for games, eco, opening, variation in rows:
catalog_table.insert(
"",
"end",
values=(
"☐",
games,
eco,
opening,
variation
)
)
# Total games in catalog
catalog_table.heading(
"Select",
text=str(total_games)
)
reopen_btn.bind("", lambda event: toggle_sidebar())
def toggle_sidebar():
global sidebar_visible
if sidebar_visible:
left_frame.grid_remove()
sidebar_footer.grid_remove()
reopen_btn.configure(text="🮝")
else:
left_frame.grid()
sidebar_footer.grid()
reopen_btn.configure(text="🮟")
reopen_btn.place(x=2, rely=1.0, y=0, anchor="sw")
reopen_btn.lift()
sidebar_visible = not sidebar_visible
# ----------------------------
# File Menu
# ----------------------------
file_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="File",
menu=file_menu
)
file_menu.add_command(
label="Import PGN...",
command=import_pgn
)
file_menu.add_command(
label="Import FEN...",
command=import_fen
)
file_menu.add_command(label="Export PGN")
file_menu.add_separator()
file_menu.add_command(
label="Clear Catalog",
command=clear_catalog
)
file_menu.add_separator()
file_menu.add_command(
label="Exit",
command=app.quit
)
# ----------------------------
# Edit Menu
# ----------------------------
edit_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="Edit",
menu=edit_menu
)
# ----------------------------
# View Menu
# ----------------------------
view_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="View",
menu=view_menu
)
view_menu.add_command(
label="Show / Hide Sidebar",
command=toggle_sidebar
)
# ----------------------------
# Tools Menu
# ----------------------------
tools_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="Tools",
menu=tools_menu
)
tools_menu.add_command(
label="Catalog",
command=show_catalog_workspace
)
tools_menu.add_command(label="Sort")
tools_menu.add_command(label="Analyze")
# ----------------------------
# Help Menu
# ----------------------------
help_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="Help",
menu=help_menu
)
# ----------------------------
# Left Navigation
# ----------------------------
left_frame = ctk.CTkFrame(
app,
width=250,
fg_color="#283858" #sidebar right and bottom padding color
)
left_frame.grid(
row=1,
column=0,
sticky="ns"
)
left_frame.grid_propagate(False)
# ----------------------------
# Tree Style
# ----------------------------
style = ttk.Style()
style.theme_use("default")
# ---- Treeview body (for menu bar)----
style.configure(
"Treeview",
background="#172134",
fieldbackground="#172134", #fix - Not field background, it is the background for the whole main body
foreground="#FFFFFF",
relief="flat",
borderwidth=0,
rowheight=24
)
# ---- Treeview headers (for the catalog) ----
style.configure(
"Treeview.Heading",
relief="solid",
borderwidth=0,
padding="2",
background="#1f6aa5",
foreground="#eeeeee"
)
style.map(
"Treeview",
background=[("selected", "#346934")],
foreground=[("selected", "#dddddd")]
)
# ---- Disable ALL header interaction visuals ----
style.map(
"Treeview.Heading",
background=[("active", "#1f6aa5"), ("pressed", "#1f6aa5")],
foreground=[("active", "#eeeeee"), ("pressed", "#eeeeee")]
)
sidebar = ttk.Treeview(
left_frame,
style="Treeview",
show="tree"
)
sidebar.pack(
fill="both",
expand=True,
padx=(0, 4),
pady=(0, 4)
)
sidebar_footer = ctk.CTkFrame(
app,
height=28,
fg_color="#172134" #where the collapse area is in the footer
)
sidebar_footer.grid(
row=2,
column=0,
sticky="ew"
)
# Root Items
pgn_node = sidebar.insert(
"",
"end",
text="PGN Collection",
open=True
)
catalog_node = sidebar.insert(
"",
"end",
text="Catalog",
open=True
)
notes_node = sidebar.insert(
"",
"end",
text="Notes",
open=True
)
def save_notes():
global notes_box
print("Saving notes...")
try:
with open("notes.txt", "w") as f:
f.write(notes_box.get("1.0", "end-1c"))
except Exception as e:
print(e)
def on_tree_select(event):
global notes_box
item = sidebar.focus()
text = sidebar.item(item, "text")
if text != "Notes" and "notes_box" in globals():
save_notes()
# Clear the workspace
for widget in workspace.winfo_children():
widget.destroy()
if text == "Notes":
notes_box = ctk.CTkTextbox(workspace)
notes_box.pack(fill="both", expand=True, padx=10, pady=10)
try:
with open("notes.txt", "r") as f:
notes_box.insert("1.0", f.read())
except FileNotFoundError:
pass
elif text == "PGN Collection":
label = ctk.CTkLabel(
workspace,
text="PGN Collection",
font=("Arial", 24)
)
label.pack(expand=True)
elif text == "Catalog":
show_catalog_workspace()
sidebar.bind("<>", on_tree_select)
# ----------------------------
# Workspace
# ----------------------------
workspace = ctk.CTkFrame(
app, fg_color="#172134" #main_workspace_background
)
workspace.grid(
row=1,
column=1,
sticky="nsew",
padx=1,
pady=1
)
label = ctk.CTkLabel(
workspace,
text="Workspace",
font=("Arial", 24)
)
label.pack(
expand=True
)
# ----------------------------
# Status Bar
# ----------------------------
status = ctk.CTkLabel(
app,
text="Import PGN files ",
anchor="e",
height=26
)
status.grid(
row=2,
column=1,
sticky="ew",
padx=10
)
reopen_btn.lift()
app.mainloop()
Tuesday, July 7, 2026
Inspection vs. Import (Colorize things)
Just when you thought you had it, you didn't.
Column 1 good, 2 contentious area, and 3 waiting.
import customtkinter as ctk
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from pathlib import Path
import chess
import chess.pgn
import json
import os
# ----------------------------
# Main Window
# ----------------------------
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
app = ctk.CTk()
style = ttk.Style()
style.theme_use("alt")
print(style.theme_names())
reopen_btn = ctk.CTkLabel(
app,
text="🮟",
font=("Arial", 16, "bold"),
text_color = ("#515985")
)
reopen_btn.place(x=2, rely=1.0, y=0, anchor="sw")
app.configure(fg_color="#172134")
app.title("🩒 Chess Catalog 🩒")
app.geometry("800x700") #change default width later to be longer
# ----------------------------
# Configure Grid
# ----------------------------
app.grid_rowconfigure(1, weight=1)
app.grid_columnconfigure(1, weight=1)
# ----------------------------
# PGN Collection
# ----------------------------
imported_files = []
cataloged_files = []
sidebar_visible = True
# ----------------------------
# Import PGN
# ----------------------------
def show_import_workspace(filename):
global add_catalog_button
# Scan the entire PGN and collect every unique header tag
all_tags = set()
with open(filename, encoding="utf-8") as pgn_file:
while True:
game = chess.pgn.read_game(pgn_file)
if game is None:
break
all_tags.update(game.headers.keys())
# No games found
if not all_tags:
return
print("all_tags =", all_tags)
print("Number of unique tags =", len(all_tags))
game_data_tags = []
# Event
if "Event" in all_tags:
game_data_tags.append("Event")
# Round
if "Round" in all_tags:
game_data_tags.append("Round")
# Players (White and/or Black)
if "White" in all_tags or "Black" in all_tags:
game_data_tags.append("Players")
# Result
if "Result" in all_tags:
game_data_tags.append("Result")
# Title (WhiteTitle and/or BlackTitle)
if "WhiteTitle" in all_tags or "BlackTitle" in all_tags:
game_data_tags.append("Title")
other_data_tags = []
column1_groups = {
"Event",
"Site",
"Date",
"Round",
"Players",
"Result",
"Title",
}
# Clear the workspace
for widget in workspace.winfo_children():
widget.destroy()
# Title
title = ctk.CTkLabel(
workspace,
text=f"{Path(filename).name}",
font=("Arial", 24, "bold")
)
title.pack(
anchor="w",
padx=20,
pady=(15, 20)
)
# Button frame
button_frame = ctk.CTkFrame(workspace, fg_color="transparent")
button_frame.pack(anchor="w", padx=20, pady=5)
# Import, Add to Catalog, Catalog buttons
add_catalog_button = ctk.CTkButton(
button_frame,
text="Add to Catalog",
command=catalog_pgns
)
add_catalog_button.pack(side="left", padx=(10, 0))
ctk.CTkButton(
button_frame,
text="Import",
command=import_pgn
).pack(side="left", padx=(10, 0))
ctk.CTkButton(
button_frame,
text="Catalog",
command=show_catalog_workspace
).pack(side="left", padx=(10, 0))
# Main container
options_frame = ctk.CTkFrame(workspace,
fg_color="#283858"
)#game_data_panel
options_frame.pack(fill="x", padx=20, pady=10)
# Three columns
game_data_frame = ctk.CTkFrame(options_frame, #fg_color="#34426e" fg_color="#00ff00"
fg_color="#283858") #column color over game_data_panel_background
other_data_frame = ctk.CTkFrame(options_frame, fg_color="#283858") #same
depth_rating = ctk.CTkFrame(options_frame, fg_color="#283858") #same
# Pack the columns
for col in (game_data_frame, other_data_frame, depth_rating):
col.pack(side="left", anchor="n", padx=(10, 30), pady=10)
# ---------- Column 1 ----------
ctk.CTkLabel(
game_data_frame,
text="Game Data",
font=("Arial", 16, "bold")
).pack(anchor="w", pady=(0, 10))
game_data_vars = {}
for tag in game_data_tags:
game_data_vars[tag] = tk.BooleanVar(value=False)
ctk.CTkCheckBox(
game_data_frame,
text=tag,
variable=game_data_vars[tag],
fg_color="#4CAF50",
hover_color="#5ECF63",
border_color="#4CAF50",
checkmark_color="white",
text_color="#8EE68E"
).pack(anchor="w", pady=4)
# ---------- Column 2 ----------
ctk.CTkLabel(
other_data_frame,
text="",
font=("Arial", 16, "bold")
).pack(anchor="w", pady=(0, 10))
other_data_tags = []
# Elo (WhiteElo and/or BlackElo)
if "WhiteElo" in all_tags or "BlackElo" in all_tags:
other_data_tags.append("Elo")
# FideId (WhiteFideId and/or BlackFideId)
if "WhiteFideId" in all_tags or "BlackFideId" in all_tags:
other_data_tags.append("FideId")
# EventDate
if "EventDate" in all_tags:
other_data_tags.append("EventDate")
# EventType
if "EventType" in all_tags:
other_data_tags.append("EventType")
other_data_vars = {}
print(other_data_tags)
print(len(other_data_tags))
print("other_data_tags =", other_data_tags)
print("Number of displayed tags =", len(other_data_tags))
# Remove tags that are already represented in Columns 1 and 2
known_tags = {
"Fen",
"White", "Black",
"Result",
"ECO",
"Opening",
"Variation",
"WhiteTitle", "BlackTitle",
"WhiteElo", "BlackElo",
"WhiteFideId", "BlackFideId",
"EventDate",
"EventType"
}
# Standard tags that belong in Column 2
recognized_other_tags = [
tag for tag in (
"WhiteFideId", "BlackFideId",
"EventDate",
"EventType"
)
if tag in all_tags
]
# Unknown or misspelled tags
unknown_tags = sorted(
tag for tag in all_tags
if tag not in known_tags
)
# Show recognized tags first
other_data_tags.extend(recognized_other_tags)
# Then show unknown tags
other_data_tags.extend(unknown_tags)
other_data_tags = [
tag for tag in other_data_tags
if not tag.startswith(("White", "Black"))
]
other_data_tags = [
tag for tag in other_data_tags
if tag not in column1_groups
]
for tag in other_data_tags:
other_data_vars[tag] = tk.BooleanVar(value=False)
print("Creating checkbox:", tag)
ctk.CTkCheckBox(
other_data_frame,
text=tag,
variable=other_data_vars[tag]
).pack(anchor="w", pady=4)
# ---------- Column 3 ----------
ctk.CTkLabel(
depth_rating,
text="Depth Rating"
).pack(anchor="w", pady=(35, 0))
depth_menu = ctk.CTkOptionMenu(
depth_rating,
values=[
"15 Moves",
"20 Moves",
"25 Moves",
"30 Moves",
"All Moves"
]
)
depth_menu.pack(anchor="w")
depth_menu.set("15 Moves")
ctk.CTkLabel(
depth_rating,
text="Rating Filter"
).pack(anchor="w", pady=(20, 0))
rating_filter = ctk.CTkOptionMenu(
depth_rating,
values=[
"2100+",
"2400+",
"2600+",
"All"
]
)
rating_filter.pack(anchor="w")
rating_filter.set("2100+")
def import_pgn():
print("1 - import_pgn started")
filename = filedialog.askopenfilename(
title="Import PGN",
filetypes=[("PGN Files", "*.pgn")]
)
print("2 - file dialog returned")
if not filename:
return
print(f"3 - Selected: {filename}")
show_import_workspace(filename)
print("4 - show_import_workspace finished")
# Don't allow the same file twice
if filename in imported_files:
status.configure(
text=f"{Path(filename).name} is already imported."
)
return
imported_files.append(filename)
short_name = Path(filename).name
sidebar.insert(
pgn_node,
"end",
text=short_name
)
status.configure(
text=f"Imported: {short_name}"
)
status.configure(
text=f"Imported: {short_name}"
)
def import_fen():
print("Import FEN selected")
# ----------------------------
# Menu Bar
# ----------------------------
menu_bar = tk.Menu(app)
app.config(menu=menu_bar)
def catalog_pgns():
global add_catalog_button
if not imported_files:
status.configure(
text="No PGNs imported to catalog."
)
return
add_catalog_button.configure(text="Please Wait...")
workspace.update()
new_files = []
total_games = 0
if os.path.exists("personal_catalog.json"):
with open("personal_catalog.json", "r", encoding="utf-8") as f:
catalog = json.load(f)
cataloged_files = catalog.get("cataloged_files", [])
else:
catalog = {
"cataloged_files": [],
"A": {},
"B": {},
"C": {},
"D": {},
"E": {}
}
cataloged_files = []
for filename in imported_files:
if filename in cataloged_files:
continue
with open(filename, encoding="utf-8") as pgn:
while True:
game = chess.pgn.read_game(pgn)
if game is None:
break
total_games += 1
eco = game.headers.get("ECO", "").strip()
opening = game.headers.get("Opening", "").strip()
variation = game.headers.get("Variation", "").strip()
if eco and opening:
group = eco[0]
if eco not in catalog[group]:
catalog[group][eco] = {
"opening": opening,
"variation": variation,
"frequency": 0
}
catalog[group][eco]["frequency"] += 1
cataloged_files.append(filename)
new_files.append(filename)
if not new_files:
add_catalog_button.configure(text="Add to Catalog")
status.configure(
text="PGNs already cataloged. Import new games to catalog more."
)
return
print(catalog)
catalog["cataloged_files"] = cataloged_files
with open("personal_catalog.json", "w", encoding="utf-8") as f:
json.dump(catalog, f, indent=4, sort_keys=True)
print("JSON written.")
status.configure(
text=f"Cataloged {total_games} game(s) from {len(new_files)} PGN(s)."
)
status.configure(
text=f"Cataloged {total_games} game(s) from {len(new_files)} PGN(s)."
)
add_catalog_button.configure(text="Add to Catalog")
def clear_catalog():
answer = messagebox.askyesno(
"Clear Catalog",
"Delete the entire opening catalog?"
)
if not answer:
return
if os.path.exists("personal_catalog.json"):
os.remove("personal_catalog.json")
cataloged_files.clear()
status.configure(
text="Catalog cleared."
)
show_catalog_workspace()
def show_catalog_workspace():
# Clear the workspace
for widget in workspace.winfo_children():
widget.destroy()
print("A")
# Title
title = ctk.CTkLabel(
workspace,
text="Opening Catalog",
font=("Arial", 24, "bold")
)
title.pack(anchor="w", padx=20, pady=(15, 10))
print("B")
# ----- Table Frame -----
table_frame = ctk.CTkFrame(workspace)
table_frame.pack(fill="both", anchor="w", expand=True, padx=(0, 0), pady=(0, 0))
columns = (
"Select",
"Games",
"ECO",
"Opening",
"Variation",
)
catalog_table = ttk.Treeview(
table_frame,
columns=columns,
show="headings"
)
# Headings
catalog_table.heading("Select", anchor="center", text=" ☐")
catalog_table.heading("Games", anchor="center", text=" Games")
catalog_table.heading("ECO", anchor="w", text=" ECO")
catalog_table.heading("Opening", anchor="w", text=" Opening")
catalog_table.heading("Variation", anchor="w", text=" Variation")
# Column widths
catalog_table.column("Select", width=45, anchor="center", stretch=False)
catalog_table.column("Games", width=70, anchor="center", stretch=False)
catalog_table.column("ECO", width=50, anchor="w", stretch=False)
catalog_table.column("Opening", width=150, anchor ="w", stretch=False)
catalog_table.column("Variation", width=200, anchor="w", stretch=True)
# Scrollbar
scrollbar = ttk.Scrollbar(
table_frame,
orient="vertical",
command=catalog_table.yview
)
catalog_table.configure(yscrollcommand=scrollbar.set)
catalog_table.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# -------------------------
# Load catalog from JSON
# -------------------------
if not os.path.exists("personal_catalog.json"):
return
with open("personal_catalog.json", "r", encoding="utf-8") as f:
catalog = json.load(f)
rows = []
total_games = 0
for group in catalog:
if group == "cataloged_files":
continue
for eco, entry in catalog[group].items():
opening = entry.get("opening", "")
variation = entry.get("variation", "")
games = entry.get("frequency", 0)
total_games += games
rows.append((
games,
eco,
opening,
variation
))
# Highest frequency first
rows.sort(key=lambda row: row[0], reverse=True)
# Insert into table
for games, eco, opening, variation in rows:
catalog_table.insert(
"",
"end",
values=(
"☐",
games,
eco,
opening,
variation
)
)
# Total games in catalog
catalog_table.heading(
"Select",
text=str(total_games)
)
reopen_btn.bind("", lambda event: toggle_sidebar())
def toggle_sidebar():
global sidebar_visible
if sidebar_visible:
left_frame.grid_remove()
sidebar_footer.grid_remove()
reopen_btn.configure(text="🮝")
else:
left_frame.grid()
sidebar_footer.grid()
reopen_btn.configure(text="🮟")
reopen_btn.place(x=2, rely=1.0, y=0, anchor="sw")
reopen_btn.lift()
sidebar_visible = not sidebar_visible
# ----------------------------
# File Menu
# ----------------------------
file_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="File",
menu=file_menu
)
file_menu.add_command(
label="Import PGN...",
command=import_pgn
)
file_menu.add_command(
label="Import FEN...",
command=import_fen
)
file_menu.add_command(label="Export PGN")
file_menu.add_separator()
file_menu.add_command(
label="Clear Catalog",
command=clear_catalog
)
file_menu.add_separator()
file_menu.add_command(
label="Exit",
command=app.quit
)
# ----------------------------
# Edit Menu
# ----------------------------
edit_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="Edit",
menu=edit_menu
)
# ----------------------------
# View Menu
# ----------------------------
view_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="View",
menu=view_menu
)
view_menu.add_command(
label="Show / Hide Sidebar",
command=toggle_sidebar
)
# ----------------------------
# Tools Menu
# ----------------------------
tools_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="Tools",
menu=tools_menu
)
tools_menu.add_command(
label="Catalog",
command=show_catalog_workspace
)
tools_menu.add_command(label="Sort")
tools_menu.add_command(label="Analyze")
# ----------------------------
# Help Menu
# ----------------------------
help_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="Help",
menu=help_menu
)
# ----------------------------
# Left Navigation
# ----------------------------
left_frame = ctk.CTkFrame(
app,
width=250,
fg_color="#283858" #sidebar right and bottom padding color
)
left_frame.grid(
row=1,
column=0,
sticky="ns"
)
left_frame.grid_propagate(False)
# ----------------------------
# Tree Style
# ----------------------------
style = ttk.Style()
style.theme_use("default")
# ---- Treeview body (for menu bar)----
style.configure(
"Treeview",
background="#172134",
fieldbackground="#172134", #fix - Not field background, it is the background for the whole main body
foreground="#FFFFFF",
relief="flat",
borderwidth=0,
rowheight=24
)
# ---- Treeview headers (for the catalog) ----
style.configure(
"Treeview.Heading",
relief="solid",
borderwidth=0,
padding="2",
background="#1f6aa5",
foreground="#eeeeee"
)
style.map(
"Treeview",
background=[("selected", "#c5c5c5")],
foreground=[("selected", "#172134")]
)
# ---- Disable ALL header interaction visuals ----
style.map(
"Treeview.Heading",
background=[("active", "#1f6aa5"), ("pressed", "#1f6aa5")],
foreground=[("active", "#eeeeee"), ("pressed", "#eeeeee")]
)
sidebar = ttk.Treeview(
left_frame,
style="Treeview",
show="tree"
)
sidebar.pack(
fill="both",
expand=True,
padx=(0, 4),
pady=(0, 4)
)
sidebar_footer = ctk.CTkFrame(
app,
height=28,
fg_color="#172134" #where the collapse area is in the footer
)
sidebar_footer.grid(
row=2,
column=0,
sticky="ew"
)
# Root Items
pgn_node = sidebar.insert(
"",
"end",
text="PGN Collection",
open=True
)
catalog_node = sidebar.insert(
"",
"end",
text="Catalog",
open=True
)
notes_node = sidebar.insert(
"",
"end",
text="Notes",
open=True
)
def save_notes():
global notes_box
print("Saving notes...")
try:
with open("notes.txt", "w") as f:
f.write(notes_box.get("1.0", "end-1c"))
except Exception as e:
print(e)
def on_tree_select(event):
global notes_box
item = sidebar.focus()
text = sidebar.item(item, "text")
if text != "Notes" and "notes_box" in globals():
save_notes()
# Clear the workspace
for widget in workspace.winfo_children():
widget.destroy()
if text == "Notes":
notes_box = ctk.CTkTextbox(workspace)
notes_box.pack(fill="both", expand=True, padx=10, pady=10)
try:
with open("notes.txt", "r") as f:
notes_box.insert("1.0", f.read())
except FileNotFoundError:
pass
elif text == "PGN Collection":
label = ctk.CTkLabel(
workspace,
text="PGN Collection",
font=("Arial", 24)
)
label.pack(expand=True)
elif text == "Catalog":
show_catalog_workspace()
sidebar.bind("<>", on_tree_select)
# ----------------------------
# Workspace
# ----------------------------
workspace = ctk.CTkFrame(
app, fg_color="#172134" #main_workspace_background
)
workspace.grid(
row=1,
column=1,
sticky="nsew",
padx=1,
pady=1
)
label = ctk.CTkLabel(
workspace,
text="Workspace",
font=("Arial", 24)
)
label.pack(
expand=True
)
# ----------------------------
# Status Bar
# ----------------------------
status = ctk.CTkLabel(
app,
text="Import PGN files ",
anchor="e",
height=26
)
status.grid(
row=2,
column=1,
sticky="ew",
padx=10
)
reopen_btn.lift()
app.mainloop()
Monday, July 6, 2026
Before dividing into multiple .py files and with color scheme mostly done
import customtkinter as ctk
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from pathlib import Path
import chess
import chess.pgn
import json
import os
# ----------------------------
# Main Window
# ----------------------------
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
app = ctk.CTk()
style = ttk.Style()
style.theme_use("alt")
print(style.theme_names())
reopen_btn = ctk.CTkLabel(
app,
text="🮟",
font=("Arial", 16, "bold"),
text_color = ("#515985")
)
reopen_btn.place(x=2, rely=1.0, y=0, anchor="sw")
app.configure(fg_color="#172134")
app.title("🩒 Chess Catalog 🩒")
app.geometry("800x700") #change default width later to be longer
# ----------------------------
# Configure Grid
# ----------------------------
app.grid_rowconfigure(1, weight=1)
app.grid_columnconfigure(1, weight=1)
# ----------------------------
# PGN Collection
# ----------------------------
imported_files = []
cataloged_files = []
sidebar_visible = True
# ----------------------------
# Import PGN
# ----------------------------
def show_import_workspace(filename):
global add_catalog_button
with open(filename, encoding="utf-8") as pgn_file:
game = chess.pgn.read_game(pgn_file)
if game is None:
return
game_data_tags = ["Event", "Site", "Date", "Round", "White", "Black", "FEN", "Result"]
other_data_tags = []
headers = game.headers
# Clear the workspace
for widget in workspace.winfo_children():
widget.destroy()
# Title
title = ctk.CTkLabel(
workspace,
text=f"{Path(filename).name}",
font=("Arial", 24, "bold")
)
title.pack(
anchor="w",
padx=20,
pady=(15, 20)
)
# Button frame
button_frame = ctk.CTkFrame(workspace, fg_color="transparent")
button_frame.pack(anchor="w", padx=20, pady=5)
# Import, Add to Catalog, Catalog buttons
add_catalog_button = ctk.CTkButton(
button_frame,
text="Add to Catalog",
command=catalog_pgns
)
add_catalog_button.pack(side="left", padx=(10, 0))
ctk.CTkButton(
button_frame,
text="Import",
command=import_pgn
).pack(side="left", padx=(10, 0))
ctk.CTkButton(
button_frame,
text="Catalog",
command=show_catalog_workspace
).pack(side="left", padx=(10, 0))
# Main container
options_frame = ctk.CTkFrame(workspace,
fg_color="#283858"
)#game_data_panel
options_frame.pack(fill="x", padx=20, pady=10)
tags_list = list(headers.keys())
# Three columns
game_data_frame = ctk.CTkFrame(options_frame, #fg_color="#34426e" fg_color="#00ff00"
fg_color="#283858") #column color over game_data_panel_background
other_data_frame = ctk.CTkFrame(options_frame, fg_color="#283858") #same
depth_rating = ctk.CTkFrame(options_frame, fg_color="#283858") #same
# Pack the columns
for col in (game_data_frame, other_data_frame, depth_rating):
col.pack(side="left", anchor="n", padx=(10, 30), pady=10)
# ---------- Column 1 ----------
ctk.CTkLabel(
game_data_frame,
text="Game Data",
font=("Arial", 16, "bold")
).pack(anchor="w", pady=(0, 10))
game_data_vars = {}
for tag in game_data_tags:
game_data_vars[tag] = tk.BooleanVar(value=False)
ctk.CTkCheckBox(
game_data_frame,
text=tag,
variable=game_data_vars[tag]
).pack(anchor="w", pady=4)
# ---------- Column 2 ----------
ctk.CTkLabel(
other_data_frame,
text="",
font=("Arial", 16, "bold")
).pack(anchor="w", pady=(0, 10))
other_data_tags = [
"WhiteTitle",
"BlackTitle",
"WhiteElo",
"BlackElo",
"WhiteFideId",
"BlackFideId",
"EventDate",
"EventType",
]
other_data_vars = {}
for tag in other_data_tags:
other_data_vars[tag] = tk.BooleanVar(value=False)
ctk.CTkCheckBox(
other_data_frame,
text=tag,
variable=other_data_vars[tag]
).pack(anchor="w", pady=4)
# ---------- Column 3 ----------
ctk.CTkLabel(
depth_rating,
text="Depth Rating"
).pack(anchor="w", pady=(35, 0))
depth_menu = ctk.CTkOptionMenu(
depth_rating,
values=[
"15 Moves",
"20 Moves",
"25 Moves",
"30 Moves",
"All Moves"
]
)
depth_menu.pack(anchor="w")
depth_menu.set("15 Moves")
ctk.CTkLabel(
depth_rating,
text="Rating Filter"
).pack(anchor="w", pady=(20, 0))
rating_filter = ctk.CTkOptionMenu(
depth_rating,
values=[
"2100+",
"2400+",
"2600+",
"All"
]
)
rating_filter.pack(anchor="w")
rating_filter.set("2100+")
def import_pgn():
filename = filedialog.askopenfilename(
title="Import PGN",
filetypes=[("PGN Files", "*.pgn")]
)
if not filename:
return
show_import_workspace(filename)
# Don't allow the same file twice
if filename in imported_files:
status.configure(
text=f"{Path(filename).name} is already imported."
)
return
imported_files.append(filename)
short_name = Path(filename).name
sidebar.insert(
pgn_node,
"end",
text=short_name
)
status.configure(
text=f"Imported: {short_name}"
)
# ----------------------------
# Menu Bar
# ----------------------------
menu_bar = tk.Menu(app)
app.config(menu=menu_bar)
def catalog_pgns():
global add_catalog_button
if not imported_files:
status.configure(
text="No PGNs imported to catalog."
)
return
add_catalog_button.configure(text="Please Wait...")
workspace.update()
new_files = []
total_games = 0
if os.path.exists("personal_catalog.json"):
with open("personal_catalog.json", "r", encoding="utf-8") as f:
catalog = json.load(f)
cataloged_files = catalog.get("cataloged_files", [])
else:
catalog = {
"cataloged_files": [],
"A": {},
"B": {},
"C": {},
"D": {},
"E": {}
}
cataloged_files = []
for filename in imported_files:
if filename in cataloged_files:
continue
with open(filename, encoding="utf-8") as pgn:
while True:
game = chess.pgn.read_game(pgn)
if game is None:
break
total_games += 1
eco = game.headers.get("ECO", "").strip()
opening = game.headers.get("Opening", "").strip()
variation = game.headers.get("Variation", "").strip()
if eco and opening:
group = eco[0]
if eco not in catalog[group]:
catalog[group][eco] = {
"opening": opening,
"variation": variation,
"frequency": 0
}
catalog[group][eco]["frequency"] += 1
cataloged_files.append(filename)
new_files.append(filename)
if not new_files:
add_catalog_button.configure(text="Add to Catalog")
status.configure(
text="PGNs already cataloged. Import new games to catalog more."
)
return
print(catalog)
catalog["cataloged_files"] = cataloged_files
with open("personal_catalog.json", "w", encoding="utf-8") as f:
json.dump(catalog, f, indent=4, sort_keys=True)
print("JSON written.")
status.configure(
text=f"Cataloged {total_games} game(s) from {len(new_files)} PGN(s)."
)
status.configure(
text=f"Cataloged {total_games} game(s) from {len(new_files)} PGN(s)."
)
add_catalog_button.configure(text="Add to Catalog")
def clear_catalog():
answer = messagebox.askyesno(
"Clear Catalog",
"Delete the entire opening catalog?"
)
if not answer:
return
if os.path.exists("personal_catalog.json"):
os.remove("personal_catalog.json")
cataloged_files.clear()
status.configure(
text="Catalog cleared."
)
show_catalog_workspace()
def show_catalog_workspace():
# Clear the workspace
for widget in workspace.winfo_children():
widget.destroy()
# Title
title = ctk.CTkLabel(
workspace,
text="Opening Catalog",
font=("Arial", 24, "bold")
)
title.pack(anchor="w", padx=20, pady=(15, 10))
# ----- Table Frame -----
table_frame = ctk.CTkFrame(workspace)
table_frame.pack(fill="both", anchor="w", expand=True, padx=(0, 0), pady=(0, 0))
columns = (
"Select",
"Games",
"ECO",
"Opening",
"Variation",
)
catalog_table = ttk.Treeview(
table_frame,
columns=columns,
show="headings"
)
# Headings
catalog_table.heading("Select", anchor="center", text=" ☐")
catalog_table.heading("Games", anchor="center", text=" Games")
catalog_table.heading("ECO", anchor="w", text=" ECO")
catalog_table.heading("Opening", anchor="w", text=" Opening")
catalog_table.heading("Variation", anchor="w", text=" Variation")
# Column widths
catalog_table.column("Select", width=45, anchor="center", stretch=False)
catalog_table.column("Games", width=70, anchor="center", stretch=False)
catalog_table.column("ECO", width=50, anchor="w", stretch=False)
catalog_table.column("Opening", width=150, anchor ="w", stretch=False)
catalog_table.column("Variation", width=200, anchor="w", stretch=True)
# Scrollbar
scrollbar = ttk.Scrollbar(
table_frame,
orient="vertical",
command=catalog_table.yview
)
catalog_table.configure(yscrollcommand=scrollbar.set)
catalog_table.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# -------------------------
# Load catalog from JSON
# -------------------------
if not os.path.exists("personal_catalog.json"):
return
with open("personal_catalog.json", "r", encoding="utf-8") as f:
catalog = json.load(f)
rows = []
total_games = 0
for group in catalog:
if group == "cataloged_files":
continue
for eco, entry in catalog[group].items():
opening = entry.get("opening", "")
variation = entry.get("variation", "")
games = entry.get("frequency", 0)
total_games += games
rows.append((
games,
eco,
opening,
variation
))
# Highest frequency first
rows.sort(key=lambda row: row[0], reverse=True)
# Insert into table
for games, eco, opening, variation in rows:
catalog_table.insert(
"",
"end",
values=(
"☐",
games,
eco,
opening,
variation
)
)
# Total games in catalog
catalog_table.heading(
"Select",
text=str(total_games)
)
reopen_btn.bind("", lambda event: toggle_sidebar())
def toggle_sidebar():
global sidebar_visible
if sidebar_visible:
left_frame.grid_remove()
sidebar_footer.grid_remove()
reopen_btn.configure(text="🮝")
else:
left_frame.grid()
sidebar_footer.grid()
reopen_btn.configure(text="🮟")
reopen_btn.place(x=2, rely=1.0, y=0, anchor="sw")
reopen_btn.lift()
sidebar_visible = not sidebar_visible
# ----------------------------
# File Menu
# ----------------------------
file_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="File",
menu=file_menu
)
file_menu.add_command(
label="Import PGN...",
command=import_pgn
)
file_menu.add_command(label="Export PGN")
file_menu.add_separator()
file_menu.add_command(
label="Clear Catalog",
command=clear_catalog
)
file_menu.add_separator()
file_menu.add_command(
label="Exit",
command=app.quit
)
# ----------------------------
# Edit Menu
# ----------------------------
edit_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="Edit",
menu=edit_menu
)
# ----------------------------
# View Menu
# ----------------------------
view_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="View",
menu=view_menu
)
view_menu.add_command(
label="Show / Hide Sidebar",
command=toggle_sidebar
)
# ----------------------------
# Tools Menu
# ----------------------------
tools_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="Tools",
menu=tools_menu
)
tools_menu.add_command(
label="Catalog",
command=show_catalog_workspace
)
tools_menu.add_command(label="Sort")
tools_menu.add_command(label="Analyze")
# ----------------------------
# Help Menu
# ----------------------------
help_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="Help",
menu=help_menu
)
# ----------------------------
# Left Navigation
# ----------------------------
left_frame = ctk.CTkFrame(
app,
width=250,
fg_color="#283858" #sidebar right and bottom padding color
)
left_frame.grid(
row=1,
column=0,
sticky="ns"
)
left_frame.grid_propagate(False)
# ----------------------------
# Tree Style
# ----------------------------
style = ttk.Style()
style.theme_use("default")
# ---- Treeview body (for menu bar)----
style.configure(
"Treeview",
background="#172134",
fieldbackground="#172134", #fix - Not field background, it is the background for the whole main body
foreground="#FFFFFF",
relief="flat",
borderwidth=0,
rowheight=24
)
# ---- Treeview headers (for the catalog) ----
style.configure(
"Treeview.Heading",
relief="solid",
borderwidth=0,
padding="2",
background="#1f6aa5",
foreground="#eeeeee"
)
style.map(
"Treeview",
background=[("selected", "#c5c5c5")],
foreground=[("selected", "#172134")]
)
# ---- Disable ALL header interaction visuals ----
style.map(
"Treeview.Heading",
background=[("active", "#1f6aa5"), ("pressed", "#1f6aa5")],
foreground=[("active", "#eeeeee"), ("pressed", "#eeeeee")]
)
sidebar = ttk.Treeview(
left_frame,
style="Treeview",
show="tree"
)
sidebar.pack(
fill="both",
expand=True,
padx=(0, 4),
pady=(0, 4)
)
sidebar_footer = ctk.CTkFrame(
app,
height=28,
fg_color="#172134" #where the collapse area is in the footer
)
sidebar_footer.grid(
row=2,
column=0,
sticky="ew"
)
# Root Items
pgn_node = sidebar.insert(
"",
"end",
text="PGN Collection",
open=True
)
catalog_node = sidebar.insert(
"",
"end",
text="Catalog",
open=True
)
notes_node = sidebar.insert(
"",
"end",
text="Notes",
open=True
)
def save_notes():
global notes_box
print("Saving notes...")
try:
with open("notes.txt", "w") as f:
f.write(notes_box.get("1.0", "end-1c"))
except Exception as e:
print(e)
def on_tree_select(event):
global notes_box
item = sidebar.focus()
text = sidebar.item(item, "text")
if text != "Notes" and "notes_box" in globals():
save_notes()
# Clear the workspace
for widget in workspace.winfo_children():
widget.destroy()
if text == "Notes":
notes_box = ctk.CTkTextbox(workspace)
notes_box.pack(fill="both", expand=True, padx=10, pady=10)
try:
with open("notes.txt", "r") as f:
notes_box.insert("1.0", f.read())
except FileNotFoundError:
pass
elif text == "PGN Collection":
label = ctk.CTkLabel(
workspace,
text="PGN Collection",
font=("Arial", 24)
)
label.pack(expand=True)
elif text == "Catalog":
show_catalog_workspace()
sidebar.bind("<>", on_tree_select)
# ----------------------------
# Workspace
# ----------------------------
workspace = ctk.CTkFrame(
app, fg_color="#172134" #main_workspace_background
)
workspace.grid(
row=1,
column=1,
sticky="nsew",
padx=1,
pady=1
)
label = ctk.CTkLabel(
workspace,
text="Workspace",
font=("Arial", 24)
)
label.pack(
expand=True
)
# ----------------------------
# Status Bar
# ----------------------------
status = ctk.CTkLabel(
app,
text="Import PGN files ",
anchor="e",
height=26
)
status.grid(
row=2,
column=1,
sticky="ew",
padx=10
)
reopen_btn.lift()
app.mainloop()
Monday, June 29, 2026
Importing .pgn files using if, elif, else
class="separator" style="clear: both;"href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjaWfoxT25FzareJyLo-Chn9LiSWQMo_ZdZzxcZkU2GjDwoTc4RemK1nuwuPQ85RjMh_DrLfAmcNz5slYuMT1myDwgqVSBaZy_9XCQfVFsjz-9v4Nr681Y_O1K-OwMcJJUyZurY56fAeN2K6W23lfI0MHuiP8If2P8hue5-p83-Td31CJniRVAnDj0Neyy2/s805/Screenshot_2026-06-30_00-15-15ifelifelse.png" style="display: block; padding: 1em 0; text-align: center; "alt="" border="0" height="320" data-original-height="805" data-original-width="621" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjaWfoxT25FzareJyLo-Chn9LiSWQMo_ZdZzxcZkU2GjDwoTc4RemK1nuwuPQ85RjMh_DrLfAmcNz5slYuMT1myDwgqVSBaZy_9XCQfVFsjz-9v4Nr681Y_O1K-OwMcJJUyZurY56fAeN2K6W23lfI0MHuiP8If2P8hue5-p83-Td31CJniRVAnDj0Neyy2/s320/Screenshot_2026-06-30_00-15-15ifelifelse.png"div
6/30/26 (Document and date your shit.)
https://bsky.app/profile/marcususa.bsky.social
Saturday, June 27, 2026
First Shell Before Importing and Exporting
import customtkinter as ctk
import tkinter as tk
from tkinter import ttk
# ----------------------------
# Main Window
# ----------------------------
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
app = ctk.CTk()
app.title("Chess Study")
app.geometry("1200x700")
# ----------------------------
# Configure Grid
# ----------------------------
app.grid_rowconfigure(1, weight=1)
app.grid_columnconfigure(1, weight=1)
# ----------------------------
# Menu Bar
# ----------------------------
# ----------------------------
# Menu Bar
# ----------------------------
menu_bar = tk.Menu(app)
app.config(menu=menu_bar)
# ----------------------------
# File Menu
# ----------------------------
file_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="File",
menu=file_menu
)
file_menu.add_command(label="Import PGN")
file_menu.add_command(label="Export PGN")
file_menu.add_separator()
file_menu.add_command(
label="Exit",
command=app.quit
)
# ----------------------------
# Edit Menu
# ----------------------------
edit_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="Edit",
menu=edit_menu
)
# ----------------------------
# View Menu
# ----------------------------
view_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="View",
menu=view_menu
)
# ----------------------------
# Analysis Menu
# ----------------------------
analysis_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="Analysis",
menu=analysis_menu
)
# ----------------------------
# Tools Menu
# ----------------------------
tools_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="Tools",
menu=tools_menu
)
# ----------------------------
# Help Menu
# ----------------------------
help_menu = tk.Menu(menu_bar, tearoff=0)
menu_bar.add_cascade(
label="Help",
menu=help_menu
)
# ----------------------------
# Left Navigation
# ----------------------------
left_frame = ctk.CTkFrame(
app,
width=250
)
left_frame.grid(
row=1,
column=0,
sticky="ns"
)
left_frame.grid_propagate(False)
# ----------------------------
# Tree Style
# ----------------------------
style = ttk.Style()
style.theme_use("default")
style.configure(
"Treeview",
background="#111111",
fieldbackground="#111111",
foreground="#EEEEEE",
borderwidth=0,
rowheight=24
)
style.map(
"Treeview",
background=[("selected", "#1F6AA5")],
foreground=[("selected", "#FFFFFF")]
)
tree = ttk.Treeview(
left_frame,
style="Treeview",
show="tree"
)
tree.pack(
fill="both",
expand=True
)
# Root Items
collections = tree.insert(
"",
"end",
text="Collections",
open=True
)
analysis = tree.insert(
"",
"end",
text="Analysis",
open=True
)
music = tree.insert(
"",
"end",
text="Music Notes",
open=True
)
# Collections
worldcup = tree.insert(
collections,
"end",
text="World Cup"
)
olympiad = tree.insert(
collections,
"end",
text="Olympiad"
)
highschool = tree.insert(
collections,
"end",
text="High School"
)
# Example Children
tree.insert(
worldcup,
"end",
text="Round 1"
)
tree.insert(
worldcup,
"end",
text="Round 2"
)
tree.insert(
olympiad,
"end",
text="Open Section"
)
tree.insert(
olympiad,
"end",
text="Women's Section"
)
# ----------------------------
# Workspace
# ----------------------------
workspace = ctk.CTkFrame(app)
workspace.grid(
row=1,
column=1,
sticky="nsew",
padx=5,
pady=5
)
label = ctk.CTkLabel(
workspace,
text="Workspace",
font=("Arial", 24)
)
label.pack(
expand=True
)
# ----------------------------
# Status Bar
# ----------------------------
status = ctk.CTkLabel(
app,
text="Ready",
anchor="w",
height=24
)
status.grid(
row=2,
column=0,
columnspan=2,
sticky="ew"
)
app.mainloop()
Friday, June 26, 2026
Tracking down 11, 12, 14, and 13 Python "stuff"
python3.11-tk
Python interpreter
Yes, that is part of it. 3.11 was from 2025, but this is 2026. Linux Mint Cinnamon 22.3 is using some 12 version. I was trying to get "14" on the PyCharm I had, but due to a "sandbox" issue, I couldn't. I completely removed the flatpak Pycharm I was using which was determined as a sandbox for some reason, and then I put on tar installation and it worked. It automatically recognized the 12 version and I could use customtkinter in PyCharm.
Yes, now I remember the sequence much more clearly.
Here's what happened:
Originally (2025):
You had a Flatpak installation of PyCharm.
Because Flatpak runs applications in a sandbox, it couldn't properly see the Python installations on your system.
You were trying to use Python 3.14, but PyCharm either couldn't find it or couldn't use it correctly because of the sandbox.
The fix:
You removed the Flatpak version of PyCharm.
You installed the tarball version (through JetBrains Toolbox or directly).
That version was no longer sandboxed and could access the system Python.
On Linux Mint 22.3:
PyCharm automatically detected the system Python 3.12 interpreter.
You installed customtkinter into that interpreter (or its virtual environment).
Everything worked normally, and you decided you were happy using Python 3.12 instead of chasing 3.14.
Now you're on PeppermintOS Trixie, which means we need to determine whether you're back in the same situation.
I am now on PeppermintOS Trixie. I typed in Pycharm with Python 12 and it gave back nothing.
If typing something like:
pycharm
or searching for "PyCharm" returns nothing, there are a few possibilities.
First, let's determine whether PyCharm is installed at all.
In a terminal, try these commands one at a time:
which pycharm
which pycharm-community
which idea
Then check whether Python 3.12 exists:
python3 --version
python3.12 --version
Also see if the executable is somewhere on your system:
find ~ -iname "pycharm*" 2>/dev/null
or, if you installed it system-wide:
find /opt -iname "pycharm*" 2>/dev/null
Since you switched to PeppermintOS
Remember that changing distributions does not carry over installed applications unless you reused the same installation. If this is a fresh Peppermint installation, you'll likely need to reinstall:
PyCharm
your virtual environments
packages like python-chess and customtkinter
Your project files are only still there if they're on a partition or home directory that you kept.
Can you tell me exactly what you typed? When you wrote:
"I typed in Pycharm with Python 12 and it gave back nothing."
Did you mean:
you searched the application menu for "PyCharm"?
you typed pycharm in a terminal?
or you were searching inside PyCharm for a Python 3.12 interpreter?
Also, please post the output of:
python3 --version
which python3
which pycharm
That will tell us where things stand.
Thought for a few seconds
Yes. From what I can recall, the issue wasn't that Python 3.12 itself was missing—it was that after switching environments, PyCharm wasn't using a Python installation with the components you needed.
More specifically:
PyCharm ended up using /usr/bin/python.
Running your customtkinter test failed because it couldn't find tkinter (ModuleNotFoundError: No module named 'tkinter').
You preferred to run installation commands in the normal terminal, not the PyCharm terminal, and we discussed installing the missing python3.11-tk package there.
Now you've switched to PeppermintOS Trixie, so we may be dealing with a different issue.
When you said:
"I typed in Pycharm with Python 12 and it gave back nothing."
it reminds me of another problem we had recently: PyCharm couldn't see or select the Python interpreter you wanted.
Can you tell me exactly where you typed "Python 12"?
In PyCharm → Add Interpreter?
In the application launcher?
In a terminal?
Somewhere else?
That will help determine whether this is the same interpreter-detection problem or something new.
python3.11-tk
Python interpreter
Yes, that is part of it. 3.11 was from 2025, but this is 2026. Linux Mint Cinnamon 22.3 is using some 12 version. I was trying to get "14" on the PyCharm I had, but due to a "sandbox" issue, I couldn't. I completely removed the flatpak Pycharm I was using which was determined as a sandbox for some reason, and then I put on tar installation and it worked. It automatically recognized the 12 version and I could use customtkinter in PyCharm.
Yes, now I remember the sequence much more clearly.
Here's what happened:
Originally (2025):
You had a Flatpak installation of PyCharm.
Because Flatpak runs applications in a sandbox, it couldn't properly see the Python installations on your system.
You were trying to use Python 3.14, but PyCharm either couldn't find it or couldn't use it correctly because of the sandbox.
The fix:
You removed the Flatpak version of PyCharm.
You installed the tarball version (through JetBrains Toolbox or directly).
That version was no longer sandboxed and could access the system Python.
On Linux Mint 22.3:
PyCharm automatically detected the system Python 3.12 interpreter.
You installed customtkinter into that interpreter (or its virtual environment).
Everything worked normally, and you decided you were happy using Python 3.12 instead of chasing 3.14.
Now you're on PeppermintOS Trixie, which means we need to determine whether you're back in the same situation.
The first thing I'd like to know is how you installed PyCharm on Peppermint.
Was it:
Flatpak
JetBrains Toolbox
Downloaded tar.gz from JetBrains
APT package
Something else?
If it's Flatpak again, I'd immediately suspect the same sandbox problem.
Also, could you run these two commands?
which pycharm
and
python3 --version
If which pycharm returns nothing, that's okay—it tells us PyCharm isn't on your PATH, which helps narrow down how it was installed.
Python 3.13.5 (LOL, there's my 13?) Why did Linux Mint skip from 12 to 14?
😄 That is a good catch.
The answer is: Linux Mint didn't actually skip from 3.12 to 3.14.
Here's what likely happened:
Linux Mint 22.x is based on a version of Ubuntu whose default Python is 3.12.
At some point you installed Python 3.14 yourself (or from a PPA or source), which is why you were trying to get PyCharm to recognize it.
Now that you're on PeppermintOS Trixie (which is based on a newer Debian release), the default system Python is 3.13.5.
So your timeline is probably:
Linux Mint 22.3 → Python 3.12.x (system default)
You manually installed 3.14 and tried to use it in PyCharm.
Flatpak PyCharm couldn't see it because of sandboxing.
Tarball PyCharm worked with the system Python 3.12.
Now PeppermintOS Trixie ships with Python 3.13.5 as the default.
In other words, the operating systems didn't "jump" from 3.12 to 3.14—you introduced 3.14 yourself. The new operating system simply comes with 3.13.
You can verify what you have installed with:
python3 --version
python3.13 --version
python3.14 --version
ls /usr/bin/python3*
I'm actually pleased to see 3.13.5 on your Peppermint system. It means you probably don't need to chase a newer interpreter this time. We can just point PyCharm at Python 3.13 and install python3-tk, python3-venv, python3-pip, python-chess, and customtkinter in that environment. That should give you essentially the same smooth setup you ended up with on Linux Mint.
Thursday, June 25, 2026
MODIFICATION - Add comments, Import chess games, catalog them, list from first variation move and by frequency. / Chess and Music brainstorming
Also see hhttps://1a2b3c4d5e6f7g8h9i10j11k12l13m14n.blogspot.com/2026/06/modification-import-chess-games-catalog.html
and https://1a2b3c4d5e6f7g8h9i10j11k12l13m14n.blogspot.com/2026/06/import-chess-games-catalog-them-list.html
"""
chessopenings3cleanup.py
Interactive Opening Catalog + Continuation Analyzer
- Loads existing opening catalog (JSON)
- Processes ONE PGN at a time
- Preserves PGN comments { }
- Displays full interactive catalog
- Allows manual reordering
- Save with comments or without comments
Requires:
pip install python-chess
"""
import chess.pgn
import os
import json
import re
JSON_FILE = "personal_openings.json"
START_PLY = 0
MAX_PLIES = 30
# ----------------------------
# LOAD / SAVE
# ----------------------------
def load_data():
if os.path.exists(JSON_FILE):
with open(JSON_FILE, "r", encoding="utf-8") as f:
return json.load(f)
return {}
def save_data(data):
with open(JSON_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4)
def strip_comments(text):
return re.sub(r"\s*\{.*?\}", "", text)
def remove_comments_from_data(data):
clean_data = {}
for opening_key, variations in data.items():
clean_data[opening_key] = {}
for line, freq in variations.items():
clean_line = strip_comments(line)
if clean_line not in clean_data[opening_key]:
clean_data[opening_key][clean_line] = freq
else:
clean_data[opening_key][clean_line] += freq
return clean_data
# ----------------------------
# FORMAT MOVES
# ----------------------------
def format_continuation(san_moves, start_ply):
lines = []
i = 0
ply = start_ply
while i < len(san_moves) and ply < MAX_PLIES:
move_number = (ply // 2) + 1
white = san_moves[i]
i += 1
ply += 1
line = f"{move_number}. {white}"
if i < len(san_moves) and ply < MAX_PLIES:
black = san_moves[i]
i += 1
ply += 1
line += f" {black}"
lines.append(line)
return lines
# ----------------------------
# PROCESS PGN
# ----------------------------
def process_pgn(filename, data):
game_count = 0
with open(filename, "r", encoding="utf-8", errors="ignore") as pgn:
while True:
game = chess.pgn.read_game(pgn)
if game is None:
break
game_count += 1
eco = game.headers.get("ECO", "").strip()
opening = game.headers.get("Opening", "").strip()
variation = game.headers.get("Variation", "").strip()
white_elo = game.headers.get("WhiteElo", "")
black_elo = game.headers.get("BlackElo", "")
try:
white_elo = int(white_elo)
black_elo = int(black_elo)
except ValueError:
continue
if white_elo < 2100 and black_elo < 2100:
continue
if not opening:
opening = "Unknown Opening"
key = f"{eco} | {opening}"
if variation:
key += f" | {variation}"
board = game.board()
san_moves = []
node = game
ply_count = 0
while node.variations and ply_count < MAX_PLIES:
next_node = node.variation(0)
move_text = board.san(next_node.move)
comment = next_node.comment.strip()
if comment:
move_text += f" {{{comment}}}"
san_moves.append(move_text)
board.push(next_node.move)
node = next_node
ply_count += 1
catalog_lines = format_continuation(
san_moves,
START_PLY
)
catalog_str = " ".join(catalog_lines)
if key not in data:
data[key] = {}
if catalog_str not in data[key]:
data[key][catalog_str] = 1
else:
data[key][catalog_str] += 1
print("\nGames processed:", game_count)
# ----------------------------
# DISPLAY
# ----------------------------
def display(data):
print("\nOPENING CATALOG")
print("=" * 100)
items = sorted(
data.items(),
key=lambda item: sum(item[1].values()),
reverse=True
)
for i, (key, variations) in enumerate(items, start=1):
print(f"\n{i:3} {key}")
sorted_variations = sorted(
variations.items(),
key=lambda x: x[1],
reverse=True
)
for j, (cont, freq) in enumerate(
sorted_variations[:5],
start=1
):
print(f"\n [{j}] freq={freq}")
print(f" {cont}")
print("-" * 100)
# ----------------------------
# REORDER
# ----------------------------
def reorder(data):
items = list(data.items())
while True:
display(dict(items))
print("\nCommands:")
print(" X=Y move item X to position Y")
print(" s save WITH comments")
print(" n save WITHOUT comments")
print(" q quit")
cmd = input("\nCommand: ").strip().lower()
if cmd == "s":
save_data(dict(items))
print("\nSaved with comments.")
continue
if cmd == "n":
clean_data = remove_comments_from_data(
dict(items)
)
save_data(clean_data)
print("\nSaved without comments.")
continue
if cmd == "q":
return dict(items)
if "=" in cmd:
try:
a, b = cmd.split("=")
a = int(a) - 1
b = int(b) - 1
item = items.pop(a)
items.insert(b, item)
except:
print("Invalid command")
# ----------------------------
# MAIN
# ----------------------------
def main():
data = load_data()
print("\nChessOpenings3 Cleanup")
print("----------------------")
filename = input(
"\nPGN filename (WITHOUT .pgn): "
).strip()
if not filename:
return
filename += ".pgn"
if not os.path.exists(filename):
print("File not found:", filename)
return
process_pgn(filename, data)
data = reorder(data)
print("\nDone.")
if __name__ == "__main__":
main()
Subscribe to:
Posts (Atom)
-
**See 6/22 post for adjustments.** Code can be tested at https://www.programiz.com/python-programming/online-compiler/ (Remove the 3 lines ...
-
The code below starts the bidding process. It's possible now to add graphics, that might be next as well as continuing with picking up. ...

