Skip to content

Instantly share code, notes, and snippets.

@Chadzilla-arilla
Created July 29, 2026 23:30
Show Gist options
  • Select an option

  • Save Chadzilla-arilla/6a2f1918df43d1d8d405a38560d2f736 to your computer and use it in GitHub Desktop.

Select an option

Save Chadzilla-arilla/6a2f1918df43d1d8d405a38560d2f736 to your computer and use it in GitHub Desktop.
aio app launcher
#!/usr/bin/env python
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog
import sqlite3, os, sys, subprocess, platform, zipfile, shutil
from datetime import datetime
import tkinter.font as tkFont
import csv
class CustomAppLunch:
def __init__(self, master):
self.master = master
master.title("App Lunch 5")
master.geometry("1100x600")
master.minsize(800,600)
master.resizable(True, True)
self.unsaved = False
self.undo_stack = []
# Data structures for app items and layout configuration.
self.app_items = {}
self.layout_config = {}
self.next_iid = 1
# Set the default database path (same base name as script with .db extension)
self.db_path = self.get_default_db_path()
new_db = not os.path.exists(self.db_path)
# Open the SQLite connection and create tables.
self.conn = sqlite3.connect(self.db_path)
self.conn.row_factory = sqlite3.Row
self.create_tables()
# If no DB exists, allow user to import a CSV.
if new_db:
csv_file = filedialog.askopenfilename(title="No database found. Select CSV file to import",
filetypes=[("CSV files", "*.csv")])
if csv_file:
self.import_csv_to_db(csv_file)
# Now load data from the database.
self.load_db()
# Configure grid layout for master.
master.grid_columnconfigure(0, weight=0, minsize=320) # Left: Tree view & controls
master.grid_columnconfigure(1, weight=0, minsize=50) # Middle: Control buttons
master.grid_columnconfigure(2, weight=1, minsize=400) # Right: App Info
master.grid_rowconfigure(0, weight=1)
master.grid_rowconfigure(1, weight=0) # Log row
master.grid_rowconfigure(2, weight=0) # Lunch/Backup row
self.left_frame = tk.Frame(master, bg="#f0f0f0", width=320, relief=tk.FLAT, borderwidth=0)
self.left_frame.grid(row=0, column=0, sticky="nsew", padx=(5,5), pady=5)
self.tree_frame = tk.Frame(self.left_frame, bg="#f0f0f0")
style = ttk.Style()
style.configure("Treeview", background="white", fieldbackground="white", borderwidth=0)
style.configure("Treeview.Heading", background="#e0e0e0", foreground="black", relief=tk.FLAT)
style.map('Treeview', background=[('selected', '#0078d4')], foreground=[('selected', 'white')])
# ===============================
# Left Frame: Tree View and Add/Remove/Edit Buttons
# ===============================
self.left_frame = tk.Frame(master, bg="lightgray", width=320)
self.left_frame.grid(row=0, column=0, sticky="nsew", padx=(10,5), pady=10)
self.left_frame.grid_propagate(False)
self.tree_frame = tk.Frame(self.left_frame, bg="lightgray")
self.tree_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
# Treeview with three columns: primary tree column and "version" and "Lunches"
self.tree = ttk.Treeview(self.tree_frame, columns=("version", "Lunches"), show="tree headings")
self.tree.heading("#0", text="App", anchor="w")
self.tree.heading("version", text="Version", anchor="e")
self.tree.heading("Lunches", text="Lunches", anchor="e")
self.tree.column("#0", anchor="w")
self.tree.column("version", anchor="e", width=80)
self.tree.column("Lunches", anchor="e", width=80)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.tree_scrollbar = tk.Scrollbar(self.tree_frame, orient="vertical", command=self.tree.yview)
self.tree_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.tree.config(yscrollcommand=self.tree_scrollbar.set)
self.tree.bind("<<TreeviewSelect>>", self.on_app_selected)
button_font = tkFont.Font(family="Segoe UI", size=10)
self.left_control_frame = tk.Frame(self.left_frame, bg="#f0f0f0")
self.left_control_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(5,0))
self.add_button = tk.Button(self.left_control_frame, text="Add", command=self.add_entry, font=button_font, width=6, height=1, bg="#0078d4", fg="white", relief=tk.FLAT, cursor="hand2")
self.add_button.pack(side=tk.LEFT, padx=2)
self.remove_button = tk.Button(self.left_control_frame, text="Remove", command=self.remove_entry, font=button_font, width=8, height=1, bg="#d83b01", fg="white", relief=tk.FLAT, cursor="hand2")
self.remove_button.pack(side=tk.LEFT, padx=2)
self.edit_button = tk.Button(self.left_control_frame, text="Edit", command=self.edit_entry, font=button_font, width=6, height=1, bg="#107c10", fg="white", relief=tk.FLAT, cursor="hand2")
self.edit_button.pack(side=tk.LEFT, padx=2)
self.undo_button = tk.Button(self.left_control_frame, text="Undo", command=self.undo_move, font=button_font, width=6, height=1, bg="#5c5c5c", fg="white", relief=tk.FLAT, cursor="hand2")
self.undo_button.pack(side=tk.RIGHT, padx=2)
# ===============================
# Middle Frame: Control Buttons (vertical layout)
# ===============================
button_font = tkFont.Font(family="Segoe UI", size=12, weight="bold")
self.mid_button_frame = tk.Frame(master, bg="#f8f8f8")
self.mid_button_frame.grid(row=0, column=1, sticky="ns", padx=(0,5), pady=10)
self.skip_up_button = tk.Button(self.mid_button_frame, text="⇈", command=self.move_item_skip_up, font=button_font, width=3, height=1, bg="#e0e0e0", relief=tk.FLAT, cursor="hand2")
self.skip_up_button.grid(row=0, column=0, pady=1, padx=1, sticky="ew")
self.up_button = tk.Button(self.mid_button_frame, text="↑", command=self.move_item_up, font=button_font, width=3, height=1, bg="#e0e0e0", relief=tk.FLAT, cursor="hand2")
self.up_button.grid(row=1, column=0, pady=1, padx=1, sticky="ew")
self.down_button = tk.Button(self.mid_button_frame, text="↓", command=self.move_item_down, font=button_font, width=3, height=1, bg="#e0e0e0", relief=tk.FLAT, cursor="hand2")
self.down_button.grid(row=2, column=0, pady=1, padx=1, sticky="ew")
self.skip_down_button = tk.Button(self.mid_button_frame, text="⇊", command=self.move_item_skip_down, font=button_font, width=3, height=1, bg="#e0e0e0", relief=tk.FLAT, cursor="hand2")
self.skip_down_button.grid(row=3, column=0, pady=1, padx=1, sticky="ew")
self.indent_button = tk.Button(self.mid_button_frame, text="→", command=self.indent_item, font=button_font, width=3, height=1, bg="#e0e0e0", relief=tk.FLAT, cursor="hand2")
self.indent_button.grid(row=4, column=0, pady=1, padx=1, sticky="ew")
self.outdent_button = tk.Button(self.mid_button_frame, text="←", command=self.outdent_item, font=button_font, width=3, height=1, bg="#e0e0e0", relief=tk.FLAT, cursor="hand2")
self.outdent_button.grid(row=5, column=0, pady=1, padx=1, sticky="ew")
# ===============================
# Right Frame: App Info
# ===============================
self.right_frame = tk.LabelFrame(master, text="App Info", font=("Segoe UI", 11, "bold"), bg="#ffffff", relief=tk.FLAT, borderwidth=1)
self.right_frame.grid(row=0, column=2, sticky="nsew", padx=(5,10), pady=10)
self.right_frame.grid_columnconfigure(1, weight=1)
self.field_names = ["App", "Path", "Filename", "Version"]
self.field_vars = {}
for i, field in enumerate(self.field_names):
lbl = tk.Label(self.right_frame, text=field + ":", bg="#ffffff", font=("Segoe UI", 10))
lbl.grid(row=i, column=0, sticky="w", padx=10, pady=3)
var = tk.StringVar()
ent = tk.Entry(self.right_frame, textvariable=var, state="readonly", width=40, readonlybackground="#f8f8f8", relief=tk.FLAT, borderwidth=1, highlightthickness=1, highlightcolor="#0078d4")
ent.grid(row=i, column=1, sticky="ew", padx=5, pady=2)
self.field_vars[field] = var
desc_label = tk.Label(self.right_frame, text="Description:", bg="#ffffff", font=("Segoe UI", 10))
desc_label.grid(row=len(self.field_names), column=0, sticky="nw", padx=10, pady=3)
desc_frame = tk.Frame(self.right_frame)
desc_frame.grid(row=len(self.field_names), column=1, columnspan=2, sticky="nsew", padx=5, pady=2)
self.desc_text = tk.Text(desc_frame, height=6, width=36, wrap="word", relief=tk.FLAT, borderwidth=1, highlightthickness=1, highlightcolor="#0078d4")
self.desc_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
desc_scroll = tk.Scrollbar(desc_frame, command=self.desc_text.yview)
desc_scroll.pack(side=tk.RIGHT, fill=tk.Y)
self.desc_text.config(yscrollcommand=desc_scroll.set)
self.right_frame.grid_rowconfigure(len(self.field_names), weight=1)
vh_label = tk.Label(self.right_frame, text="Version History:", bg="#ffffff", font=("Segoe UI", 10))
vh_label.grid(row=len(self.field_names)+1, column=0, sticky="w", padx=10, pady=3)
self.version_combobox = ttk.Combobox(self.right_frame, state="readonly")
self.version_combobox.grid(row=len(self.field_names)+1, column=1, sticky="w", padx=5, pady=2)
self.version_combobox.bind("<<ComboboxSelected>>", self.on_version_selected)
self.Lunch_count_label = tk.Label(self.right_frame, text="", bg="#ffffff", font=("Segoe UI", 9), fg="#0078d4")
self.Lunch_count_label.grid(row=len(self.field_names)+1, column=2, sticky="w", padx=5, pady=2)
self.add_version_button = tk.Button(self.right_frame, text="Add New Version", command=self.add_new_version, bg="#107c10", fg="white", relief=tk.FLAT, cursor="hand2", font=("Segoe UI", 9))
self.add_version_button.grid(row=len(self.field_names)+2, column=0, columnspan=2, sticky="w", padx=5, pady=2)
# Rating UI
'''
rating_label = tk.Label(self.right_frame, text="Rating:", bg="#ffffff", font=("Segoe UI", 10))
rating_label.grid(row=len(self.field_names)+3, column=0, sticky="w", padx=10, pady=3)
rating_frame = tk.Frame(self.right_frame, bg="#ffffff")
rating_frame.grid(row=len(self.field_names)+3, column=1, sticky="w", padx=5, pady=2)
self.rating_var = tk.IntVar(value=0)
self.rating_label = tk.Label(rating_frame, text="☆☆☆☆☆", bg="#ffffff", font=("Segoe UI", 14), fg="#ffa500")
self.rating_label.pack(side=tk.LEFT, padx=(0, 5))
self.rating_spinbox = tk.Spinbox(rating_frame, from_=0, to=5, textvariable=self.rating_var, width=5,
command=self.on_rating_changed, font=("Segoe UI", 9))
self.rating_spinbox.pack(side=tk.LEFT)
self.rating_spinbox.bind("<Return>", lambda e: self.on_rating_changed())
self.rating_spinbox.bind("<FocusOut>", lambda e: self.on_rating_changed())
'''
# ===============================
# Log Frame
# ===============================
self.log_frame = tk.Frame(master)
self.log_frame.grid(row=1, column=0, columnspan=3, sticky="nsew", padx=10, pady=(0,10))
self.log_text = tk.Text(self.log_frame, height=5, state="disabled", relief=tk.FLAT, borderwidth=1, bg="#f8f8f8", font=("Consolas", 9))
self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.log_scrollbar = tk.Scrollbar(self.log_frame, command=self.log_text.yview)
self.log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.log_text.config(yscrollcommand=self.log_scrollbar.set)
# ===============================
# Lunch/Backup Frame
# ===============================
self.Lunch_frame = tk.Frame(master)
self.Lunch_frame.grid(row=2, column=0, columnspan=3, sticky="e", padx=(0,10), pady=(0,10))
self.save_button = tk.Button(self.Lunch_frame, text="Save", command=self.manual_save, bg="#107c10", fg="white", relief=tk.FLAT, cursor="hand2", font=("Segoe UI", 10, "bold"))
self.save_button.pack(side=tk.LEFT, padx=3)
self.backup_button = tk.Button(self.Lunch_frame, text="Backup", command=self.backup_files, bg="#5c5c5c", fg="white", relief=tk.FLAT, cursor="hand2", font=("Segoe UI", 10))
self.backup_button.pack(side=tk.LEFT, padx=3)
self.Lunch_button = tk.Button(self.Lunch_frame, text="Launch →", command=self.Lunch_app, bg="#0078d4", fg="white", relief=tk.FLAT, cursor="hand2", font=("Segoe UI", 10, "bold"))
self.Lunch_button.pack(side=tk.LEFT, padx=3)
self.build_tree()
self.master.protocol("WM_DELETE_WINDOW", self.on_closing)
def get_default_db_path(self):
try:
script_file = __file__
except NameError:
script_file = sys.argv[0]
script_file = os.path.abspath(script_file)
base, _ = os.path.splitext(script_file)
return base + ".db"
def create_tables(self):
c = self.conn.cursor()
c.execute("""CREATE TABLE IF NOT EXISTS app_items (
iid INTEGER PRIMARY KEY AUTOINCREMENT,
App TEXT,
Path TEXT,
Filename TEXT,
Version TEXT,
Description TEXT,
Parent TEXT,
LunchCount INTEGER,
Version_float REAL,
Rating INTEGER DEFAULT 0
)""")
c.execute("""CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT
)""")
self.conn.commit()
# Add Rating column to existing databases if it doesn't exist
try:
c.execute("SELECT Rating FROM app_items LIMIT 1")
except sqlite3.OperationalError:
c.execute("ALTER TABLE app_items ADD COLUMN Rating INTEGER DEFAULT 0")
self.conn.commit()
def import_csv_to_db(self, csv_file):
c = self.conn.cursor()
with open(csv_file, newline='', encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
if row.get("iid", "") == "__CONFIG__":
config_str = row.get("Description", "")
for setting in config_str.split(";"):
if "=" in setting:
key, value = setting.split("=", 1)
c.execute("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", (key.strip(), value.strip()))
else:
try:
lunch = int(row.get("LunchCount", 0))
except:
lunch = 0
try:
vf = float(row.get("Version", "0"))
except:
vf = 0.0
try:
rating = int(row.get("Rating", 0))
if rating < 0 or rating > 5:
rating = 0
except:
rating = 0
c.execute("""INSERT INTO app_items (iid, App, Path, Filename, Version, Description, Parent, LunchCount, Version_float, Rating)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(row.get("iid"), row.get("App"), row.get("Path"), row.get("Filename"),
row.get("Version"), row.get("Description"), row.get("Parent", ""), lunch, vf, rating))
self.conn.commit()
def load_db(self):
c = self.conn.cursor()
c.execute("SELECT * FROM app_items")
rows = c.fetchall()
self.app_items = {}
self.next_iid = 1
for row in rows:
item = dict(row)
self.app_items[str(item["iid"])] = item
try:
num = int(item["iid"])
if num >= self.next_iid:
self.next_iid = num + 1
except:
pass
c.execute("SELECT key, value FROM config")
rows = c.fetchall()
self.layout_config = {row["key"]: row["value"] for row in rows}
self.conn.commit()
def save_db(self):
c = self.conn.cursor()
c.execute("DELETE FROM app_items")
for item in self.app_items.values():
c.execute("""INSERT INTO app_items (iid, App, Path, Filename, Version, Description, Parent, LunchCount, Version_float, Rating)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(item["iid"], item["App"], item["Path"], item["Filename"], item["Version"],
item["Description"], item["Parent"], item["LunchCount"], item.get("Version_float", 0.0), item.get("Rating", 0)))
for key, value in self.layout_config.items():
c.execute("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", (key, value))
self.conn.commit()
self.log_message(f"Database saved to {self.db_path}.")
def build_tree(self):
for item in self.tree.get_children():
self.tree.delete(item)
def insert_item_and_children(iid):
item = self.app_items[iid]
parent = item["Parent"] if item["Parent"] else ""
display_text = f"{item['App']} ({item['Version']})" if item["Version"] else item["App"]
self.tree.insert(parent, "end", iid, text=display_text, values=(item["Version"], item.get("LunchCount", 0)))
for child_iid in [cid for cid, citem in self.app_items.items() if str(citem["Parent"]) == iid]:
insert_item_and_children(child_iid)
for iid, item in self.app_items.items():
if item["Parent"] == "":
insert_item_and_children(iid)
top_items = self.tree.get_children("")
if top_items:
self.tree.selection_set(top_items[0])
self.tree.focus(top_items[0])
self.display_app_info(top_items[0])
def restore_layout_config(self):
# In this grid version, we simulate layout config restoration.
default_vertical = (0, 400)
default_top = (320, 0)
default_bottom = (0, 100)
try:
vs_str = self.layout_config.get("vertical_sash", f"{default_vertical[0]},{default_vertical[1]}")
ts_str = self.layout_config.get("top_sash", f"{default_top[0]},{default_top[1]}")
bs_str = self.layout_config.get("bottom_sash", f"{default_bottom[0]},{default_bottom[1]}")
vs = tuple(int(x) for x in vs_str.split(","))
ts = tuple(int(x) for x in ts_str.split(","))
bs = tuple(int(x) for x in bs_str.split(","))
except Exception as e:
vs, ts, bs = default_vertical, default_top, default_bottom
# Since we are using grid, there's no actual sash to move.
print("Layout restored from DB config (simulated):", vs, ts, bs)
def reset_layout(self):
default_vertical = (0, 400)
default_top = (320, 0)
default_bottom = (0, 100)
self.layout_config["vertical_sash"] = f"{default_vertical[0]},{default_vertical[1]}"
self.layout_config["top_sash"] = f"{default_top[0]},{default_top[1]}"
self.layout_config["bottom_sash"] = f"{default_bottom[0]},{default_bottom[1]}"
self.save_db()
print("Layout reset to defaults.")
def on_app_selected(self, event):
selection = self.tree.selection()
if selection:
iid = selection[0]
self.display_app_info(iid)
def display_app_info(self, iid):
if iid not in self.app_items:
return
selected_item = self.app_items[iid]
group = [item for item in self.app_items.values() if item["App"] == selected_item["App"] and item["Parent"] == selected_item["Parent"]]
if not group:
return
selected_row = max(group, key=lambda r: r.get("Version_float", 0.0))
for field in self.field_names:
self.field_vars[field].set(selected_row.get(field, ""))
self.desc_text.delete("1.0", tk.END)
self.desc_text.insert(tk.END, selected_row.get("Description", ""))
versions = [item["Version"] for item in group]
unique_versions = sorted(list(set(versions)), key=lambda v: float(v) if v.replace('.', '', 1).isdigit() else 0, reverse=True)
self.version_combobox['values'] = unique_versions
self.version_combobox.set(selected_row["Version"])
if len(unique_versions) <= 1:
self.version_combobox.configure(state="disabled")
else:
self.version_combobox.configure(state="readonly")
Lunch_count = selected_row.get("LunchCount", 0)
if Lunch_count > 0:
self.Lunch_count_label.config(text=f"Lunches: {Lunch_count}")
else:
self.Lunch_count_label.config(text="")
# Update rating display
'''
rating = selected_row.get("Rating", 0)
self.rating_var.set(rating)
self.update_rating_stars(rating)
'''
display_text = f"{selected_row['App']} ({selected_row['Version']})" if selected_row.get("Version") else selected_row["App"]
self.tree.item(iid, text=display_text, values=(selected_row["Version"], selected_row.get("LunchCount", 0)))
def on_version_selected(self, event):
selection = self.tree.selection()
if selection:
current_iid = selection[0]
current_item = self.app_items[current_iid]
group = [(iid, item) for iid, item in self.app_items.items() if item["App"] == current_item["App"] and item["Parent"] == current_item["Parent"]]
selected_version = self.version_combobox.get()
for iid, item in group:
if item.get("Version") == selected_version:
for field in self.field_names:
self.field_vars[field].set(item.get(field, ""))
self.desc_text.delete("1.0", tk.END)
self.desc_text.insert(tk.END, item.get("Description", ""))
Lunch_count = item.get("LunchCount", 0)
if Lunch_count > 0:
self.Lunch_count_label.config(text=f"Lunches: {Lunch_count}")
else:
self.Lunch_count_label.config(text="")
# Update rating display
'''
rating = item.get("Rating", 0)
self.rating_var.set(rating)
self.update_rating_stars(rating)
'''
break
'''
def on_rating_changed(self):
selection = self.tree.selection()
if not selection:
return
iid = selection[0]
try:
new_rating = int(self.rating_var.get())
if new_rating < 0:
new_rating = 0
elif new_rating > 5:
new_rating = 5
self.rating_var.set(new_rating)
except ValueError:
new_rating = 0
self.rating_var.set(0)
# Update the rating for the selected item
if iid in self.app_items:
old_rating = self.app_items[iid].get("Rating", 0)
self.app_items[iid]["Rating"] = new_rating
self.update_rating_stars(new_rating)
# Auto-save the rating change
self.save_db()
self.unsaved = False
#self.save_button.config(bg="#107c10")
if old_rating != new_rating:
self.log_message(f"Rating updated to {new_rating} stars for '{self.app_items[iid]['App']}'")
def update_rating_stars(self, rating):
"""Update the star display based on rating value"""
filled = "★" * rating
empty = "☆" * (5 - rating)
self.rating_label.config(text=filled + empty)
'''
def record_move(self, iid):
parent = self.tree.parent(iid)
siblings = list(self.tree.get_children(parent))
try:
old_index = siblings.index(iid)
except ValueError:
old_index = 0
self.undo_stack.append((iid, parent, old_index))
if len(self.undo_stack) > 10:
self.undo_stack.pop(0)
def undo_move(self):
if not self.undo_stack:
messagebox.showinfo("Undo", "No moves to undo.")
return
iid, old_parent, old_index = self.undo_stack.pop()
if not self.tree.exists(iid):
messagebox.showerror("Undo Error", "The item to undo no longer exists.")
return
self.tree.move(iid, old_parent, old_index)
self.tree.selection_set(iid)
self.mark_unsaved()
self.log_message(f"Undid move of item '{self.app_items[iid]['App']}' to its previous position.")
def mark_unsaved(self):
self.unsaved = True
# self.save_button.config(bg="#ff8c00")
def manual_save(self):
self.save_db()
self.unsaved = False
#self.save_button.config(bg="#107c10")
self.log_message("Changes saved.")
def log_message(self, msg):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
full_msg = f"{timestamp}: {msg}\n"
self.log_text.config(state="normal")
self.log_text.insert(tk.END, full_msg)
self.log_text.config(state="disabled")
self.log_text.see(tk.END)
def add_entry(self):
file_path = filedialog.askopenfilename(title="Select a file to add")
if not file_path:
return
file_name = os.path.basename(file_path)
file_dir = os.path.dirname(file_path)
exists = any(item for item in self.app_items.values() if item["App"] == file_name and item["Parent"] == "")
if exists:
if messagebox.askyesno("App Exists", f"App '{file_name}' already exists.\nDo you want to add a new version?"):
self.add_new_version()
return
version = simpledialog.askstring("Version", f"Enter version number for '{file_name}':")
if version is None:
return
new_iid = str(self.next_iid)
self.next_iid += 1
new_row = {
"iid": new_iid,
"App": file_name,
"Path": file_dir,
"Filename": file_name,
"Version": version,
"Description": "",
"Parent": "",
"LunchCount": 0,
# "Rating": 0
}
try:
new_row["Version_float"] = float(version)
except ValueError:
new_row["Version_float"] = 0.0
self.app_items[new_iid] = new_row
self.tree.insert("", "end", new_iid, text=f"{new_row['App']} ({new_row['Version']})",
values=(new_row["Version"], new_row.get("LunchCount", 0)))
self.tree.selection_set(new_iid)
self.tree.focus(new_iid)
self.log_message(f"App '{file_name}' added with version {version}.")
self.mark_unsaved()
def add_new_version(self):
selection = self.tree.selection()
if not selection:
messagebox.showwarning("Add New Version", "No app selected.")
return
current_iid = selection[0]
current_item = self.app_items[current_iid]
file_path = filedialog.askopenfilename(title=f"Select file for new version of '{current_item['App']}'")
if not file_path:
return
file_name = os.path.basename(file_path)
file_dir = os.path.dirname(file_path)
version = simpledialog.askstring("New Version", f"Enter version number for the new version of '{current_item['App']}':")
if version is None or version.strip() == "":
messagebox.showwarning("New Version", "Version number cannot be empty.")
return
group = [item for item in self.app_items.values() if item["App"] == current_item["App"] and item["Parent"] == current_item["Parent"]]
if any(item for item in group if item.get("Version") == version):
messagebox.showerror("New Version", f"Version {version} for '{current_item['App']}' already exists.")
return
new_iid = str(self.next_iid)
self.next_iid += 1
new_row = {
"iid": new_iid,
"App": current_item["App"],
"Path": file_dir,
"Filename": file_name,
"Version": version,
"Description": "",
"Parent": current_item["Parent"],
"LunchCount": 0,
# "Rating": 0
}
try:
new_row["Version_float"] = float(version)
except ValueError:
new_row["Version_float"] = 0.0
self.app_items[new_iid] = new_row
parent = new_row["Parent"]
self.tree.insert(parent, "end", new_iid, text=f"{new_row['App']} ({new_row['Version']})",
values=(new_row["Version"], new_row.get("LunchCount", 0)))
self.log_message(f"New version {version} added for app '{current_item['App']}'.")
self.mark_unsaved()
def remove_entry(self):
selection = self.tree.selection()
if not selection:
messagebox.showwarning("Remove", "No app selected to remove.")
return
iid = selection[0]
app_name = self.app_items[iid]["App"]
children = self.tree.get_children(iid)
if children:
confirm = messagebox.askyesno("Confirm Remove",
f"'{app_name}' contains nested items. Do you want to remove it and promote its children?")
if not confirm:
return
parent = self.tree.parent(iid)
for child in children:
self.tree.move(child, parent, "end")
self.app_items[child]["Parent"] = parent
self.log_message(f"Promoted child '{self.app_items[child]['App']}' to parent level.")
else:
confirm = messagebox.askyesno("Confirm Remove", f"Are you sure you want to remove '{app_name}'?")
if not confirm:
return
self.tree.delete(iid)
if iid in self.app_items:
del self.app_items[iid]
for field in self.field_names:
self.field_vars[field].set("")
self.desc_text.delete("1.0", tk.END)
self.version_combobox.set("")
self.Lunch_count_label.config(text="")
self.log_message(f"App '{app_name}' removed from list.")
self.mark_unsaved()
def edit_entry(self):
selection = self.tree.selection()
if not selection:
messagebox.showwarning("Edit", "No app selected to edit.")
return
iid = selection[0]
item = self.app_items[iid]
edit_win = tk.Toplevel(self.master)
edit_win.title(f"Edit '{item['App']}'")
labels = ["App", "Path", "Filename", "Version", "Description"]
entries = {}
for i, field in enumerate(labels):
tk.Label(edit_win, text=field + ":").grid(row=i, column=0, sticky="w", padx=5, pady=2)
if field != "Description":
var = tk.StringVar(value=item.get(field, ""))
ent = tk.Entry(edit_win, textvariable=var, width=40)
ent.grid(row=i, column=1, padx=5, pady=2)
entries[field] = var
else:
txt = tk.Text(edit_win, height=6, width=36, wrap="word")
txt.grid(row=i, column=1, padx=5, pady=2)
txt.insert(tk.END, item.get("Description", ""))
entries[field] = txt
def save_changes():
for field in ["App", "Path", "Filename", "Version"]:
item[field] = entries[field].get()
try:
item["Version_float"] = float(item["Version"])
except ValueError:
item["Version_float"] = 0.0
item["Description"] = entries["Description"].get("1.0", tk.END).strip()
self.app_items[iid] = item
self.tree.item(iid, text=f"{item['App']} ({item['Version']})")
self.log_message(f"App '{item['App']}' updated. Version set to {item['Version']}.")
edit_win.destroy()
messagebox.showinfo("Saved", "Changes have been saved.")
self.mark_unsaved()
save_button = tk.Button(edit_win, text="Save", command=save_changes)
save_button.grid(row=len(labels), column=0, columnspan=2, pady=5)
def Lunch_app(self):
selection = self.tree.selection()
if not selection:
messagebox.showwarning("Lunch", "No app selected to Lunch.")
return
iid = selection[0]
item = self.app_items[iid]
path = item.get("Path", "")
filename = item.get("Filename", "")
full_path = os.path.join(path, filename)
if not os.path.exists(full_path):
messagebox.showerror("Lunch Error", f"File not found: {full_path}")
return
try:
if platform.system() == 'Windows':
os.startfile(full_path)
elif platform.system() == 'Darwin':
subprocess.call(['open', full_path])
else:
subprocess.call(['xdg-open', full_path])
self.log_message(f"Lunched file: {full_path}")
item["LunchCount"] = item.get("LunchCount", 0) + 1
self.display_app_info(iid)
# Autosave after launching
self.save_db()
self.unsaved = False
# self.save_button.config(bg="#107c10")
except Exception as e:
messagebox.showerror("Lunch Error", f"Failed to lunch file: {e}")
def backup_files(self):
backup_zip = "appLunchBackups.zip"
unique_files = set()
for item in self.app_items.values():
file_path = os.path.join(item.get("Path", ""), item.get("Filename", ""))
if os.path.exists(file_path):
unique_files.add(file_path)
try:
with zipfile.ZipFile(backup_zip, "w", zipfile.ZIP_DEFLATED) as zipf:
for file in unique_files:
arcname = os.path.basename(file)
zipf.write(file, arcname=arcname)
self.log_message(f"Backup created successfully to {backup_zip}.")
messagebox.showinfo("Backup", f"Backup created successfully to {backup_zip}.")
except Exception as e:
messagebox.showerror("Backup Error", f"Failed to create backup: {e}")
self.log_message(f"Backup failed: {e}")
def move_item_up(self):
selection = self.tree.selection()
if not selection:
return
iid = selection[0]
parent = self.tree.parent(iid)
siblings = list(self.tree.get_children(parent))
index = siblings.index(iid)
if index > 0:
self.record_move(iid)
self.tree.move(iid, parent, index-1)
self.tree.selection_set(iid)
self.mark_unsaved()
def move_item_down(self):
selection = self.tree.selection()
if not selection:
return
iid = selection[0]
parent = self.tree.parent(iid)
siblings = list(self.tree.get_children(parent))
index = siblings.index(iid)
if index < len(siblings) - 1:
self.record_move(iid)
self.tree.move(iid, parent, index+1)
self.tree.selection_set(iid)
self.mark_unsaved()
def move_item_skip_up(self):
selection = self.tree.selection()
if not selection:
return
iid = selection[0]
parent = self.tree.parent(iid)
self.record_move(iid)
self.tree.move(iid, parent, 0)
self.tree.selection_set(iid)
self.mark_unsaved()
def move_item_skip_down(self):
selection = self.tree.selection()
if not selection:
return
iid = selection[0]
parent = self.tree.parent(iid)
siblings = list(self.tree.get_children(parent))
self.record_move(iid)
self.tree.move(iid, parent, len(siblings)-1)
self.tree.selection_set(iid)
self.mark_unsaved()
def indent_item(self):
selection = self.tree.selection()
if not selection:
return
iid = selection[0]
parent = self.tree.parent(iid)
siblings = list(self.tree.get_children(parent))
index = siblings.index(iid)
if index > 0:
new_parent = siblings[index-1]
self.record_move(iid)
self.tree.move(iid, new_parent, "end")
self.app_items[iid]["Parent"] = new_parent
self.tree.item(new_parent, open=True)
self.tree.selection_set(iid)
self.log_message(f"Indented item '{self.app_items[iid]['App']}' under '{self.app_items[new_parent]['App']}'")
self.mark_unsaved()
def outdent_item(self):
selection = self.tree.selection()
if not selection:
return
iid = selection[0]
parent = self.tree.parent(iid)
if parent:
grandparent = self.tree.parent(parent)
siblings = list(self.tree.get_children(grandparent))
index_parent = siblings.index(parent)
self.record_move(iid)
self.tree.move(iid, grandparent, index_parent+1)
self.app_items[iid]["Parent"] = grandparent
self.tree.selection_set(iid)
self.log_message(f"Outdented item '{self.app_items[iid]['App']}' to level of its former parent")
self.mark_unsaved()
def on_treeview_button_press(self, event):
pass
def on_treeview_motion(self, event):
pass
def on_treeview_button_release(self, event):
pass
def record_move(self, iid):
parent = self.tree.parent(iid)
siblings = list(self.tree.get_children(parent))
try:
old_index = siblings.index(iid)
except ValueError:
old_index = 0
self.undo_stack.append((iid, parent, old_index))
if len(self.undo_stack) > 10:
self.undo_stack.pop(0)
def mark_unsaved(self):
self.unsaved = True
# self.save_button.config(fg="blue")
def manual_save(self):
self.save_db()
self.unsaved = False
# self.save_button.config(fg="green")
self.log_message("Changes saved.")
def get_default_db_path(self):
try:
script_file = __file__
except NameError:
script_file = sys.argv[0]
script_file = os.path.abspath(script_file)
base, _ = os.path.splitext(script_file)
return base + ".db"
def save_db(self):
c = self.conn.cursor()
c.execute("DELETE FROM app_items")
for item in self.app_items.values():
c.execute("""INSERT INTO app_items (iid, App, Path, Filename, Version, Description, Parent, LunchCount, Version_float, Rating)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(item["iid"], item["App"], item["Path"], item["Filename"], item["Version"],
item["Description"], item["Parent"], item["LunchCount"], item.get("Version_float", 0.0), item.get("Rating", 0)))
for key, value in self.layout_config.items():
c.execute("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", (key, value))
self.conn.commit()
self.log_message(f"Database saved to {self.db_path}.")
def load_db(self):
c = self.conn.cursor()
c.execute("SELECT * FROM app_items")
rows = c.fetchall()
self.app_items = {}
self.next_iid = 1
for row in rows:
item = dict(row)
self.app_items[str(item["iid"])] = item
try:
num = int(item["iid"])
if num >= self.next_iid:
self.next_iid = num + 1
except:
pass
c.execute("SELECT key, value FROM config")
rows = c.fetchall()
self.layout_config = {row["key"]: row["value"] for row in rows}
self.conn.commit()
def create_tables(self):
c = self.conn.cursor()
c.execute("""CREATE TABLE IF NOT EXISTS app_items (
iid INTEGER PRIMARY KEY AUTOINCREMENT,
App TEXT,
Path TEXT,
Filename TEXT,
Version TEXT,
Description TEXT,
Parent TEXT,
LunchCount INTEGER,
Version_float REAL,
Rating INTEGER DEFAULT 0
)""")
c.execute("""CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT
)""")
self.conn.commit()
# Add Rating column to existing databases if it doesn't exist
try:
c.execute("SELECT Rating FROM app_items LIMIT 1")
except sqlite3.OperationalError:
c.execute("ALTER TABLE app_items ADD COLUMN Rating INTEGER DEFAULT 0")
self.conn.commit()
def import_csv_to_db(self, csv_file):
c = self.conn.cursor()
with open(csv_file, newline='', encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
if row.get("iid", "") == "__CONFIG__":
config_str = row.get("Description", "")
for setting in config_str.split(";"):
if "=" in setting:
key, value = setting.split("=", 1)
c.execute("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", (key.strip(), value.strip()))
else:
try:
lunch = int(row.get("LunchCount", 0))
except:
lunch = 0
try:
vf = float(row.get("Version", "0"))
except:
vf = 0.0
try:
rating = int(row.get("Rating", 0))
if rating < 0 or rating > 5:
rating = 0
except:
rating = 0
c.execute("""INSERT INTO app_items (iid, App, Path, Filename, Version, Description, Parent, LunchCount, Version_float, Rating)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(row.get("iid"), row.get("App"), row.get("Path"), row.get("Filename"),
row.get("Version"), row.get("Description"), row.get("Parent", ""), lunch, vf, rating))
self.conn.commit()
def on_closing(self):
# For demonstration, we simulate layout config defaults.
default_vertical = (0, 400)
default_top = (320, 0)
default_bottom = (0, 100)
self.layout_config["vertical_sash"] = f"{default_vertical[0]},{default_vertical[1]}"
self.layout_config["top_sash"] = f"{default_top[0]},{default_top[1]}"
self.layout_config["bottom_sash"] = f"{default_bottom[0]},{default_bottom[1]}"
self.save_db()
self.conn.close()
self.master.destroy()
if __name__ == "__main__":
root = tk.Tk()
# Check if DB exists; if not, later the app will prompt for CSV import.
app = CustomAppLunch(root)
root.mainloop()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment