"""Welcome / splash screen.""" import os import threading import time import tkinter as tk from tkinter import ttk, messagebox, filedialog from knoe import screen as ui _WELCOME_CARDS = [ { "mode": "min", "title": "Just a Database", "body": "One knoe-db container via containerd.\nNo Kubernetes needed. Perfect for\ndeveloping a Spring app locally.", "badge": "Homebrew + 1Password", "color": "#8E44AD", }, { "mode": "k3d", "title": "Local Cluster", "body": "k3s-in-Docker cluster on your Mac.\nFull CNPG database — add Supabase,\nArgoCD, Gitea or GitLab.", "badge": "Docker + Homebrew + 1Password", "color": "#4A90D9", }, { "mode": "k3s", "title": "Homelab", "body": "Multi-node k3s on real hardware.\nFull Kerberos auth stack, Garage S3\nand monitoring.", "badge": "k3sup + Homebrew + 1Password", "color": "#27AE60", }, { "mode": "gke", "title": "Production", "body": "Dual GKE clusters on Google Cloud.\nCNPG + GCS backups and Workload\nIdentity.", "badge": "gcloud + 1Password", "color": "#E67E22", }, ] _MODE_TO_CLUSTER_ENV = {"min": "min", "k3d": "dev", "k3s": "service", "gke": "prod"} _MODE_NEEDS = { "min": "You'll need: Homebrew + 1Password", "k3d": "You'll need: Docker, Homebrew + 1Password", "k3s": "You'll need: k3sup, Homebrew + 1Password", "gke": "You'll need: gcloud CLI + 1Password", } class WelcomeScreenMixin: """Welcome / splash screen.""" def _render_welcome_page(self): content_width = self.bg_canvas.winfo_width() or 975 right_margin = content_width - 48 # Letterhead ui.canvas_text(self, right_margin, 40, "knoe.dev", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne") ui.canvas_text(self, right_margin, 85, "infrastructure.auto()", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne") self._render_title("Welcome to Knoe.DB", y=145) intro = ( "Knoe.DB Installer sets up a production-grade PostgreSQL cluster with Kerberos " "authentication.\nPick the mode that matches your hardware — we'll walk you through every step." ) ui.canvas_text(self, 48, 200, intro, fill="#1d1d1f", font=("SF Pro Text", 13), width=800) # Mode selector cards card_w = 185 card_h = 200 card_gap = 18 card_x0 = 48 card_y0 = 265 self._welcome_card_rects = {} for i, card in enumerate(_WELCOME_CARDS): x0 = card_x0 + i * (card_w + card_gap) x1 = x0 + card_w y0 = card_y0 y1 = card_y0 + card_h bg = self.bg_canvas.create_rectangle( x0, y0, x1, y1, fill="white", outline="#CCCCCC", width=2, ) self._canvas_items.append(bg) bar = self.bg_canvas.create_rectangle( x0 + 1, y0 + 1, x1 - 1, y0 + 6, fill=card["color"], outline="", ) self._canvas_items.append(bar) mode_lbl = self.bg_canvas.create_text( x0 + 14, y0 + 18, text=card["mode"].upper(), fill=card["color"], font=("SF Pro Text", 9, "bold"), anchor="nw", ) self._canvas_items.append(mode_lbl) title_lbl = self.bg_canvas.create_text( x0 + 14, y0 + 36, text=card["title"], fill="#1d1d1f", font=("SF Pro Text", 13, "bold"), anchor="nw", ) self._canvas_items.append(title_lbl) body_lbl = self.bg_canvas.create_text( x0 + 14, y0 + 62, text=card["body"], fill="#555555", font=("SF Pro Text", 11), anchor="nw", width=card_w - 28, ) self._canvas_items.append(body_lbl) badge_lbl = self.bg_canvas.create_text( x0 + 14, y1 - 26, text=card["badge"], fill=card["color"], font=("SF Pro Text", 9), anchor="nw", width=card_w - 28, ) self._canvas_items.append(badge_lbl) self._welcome_card_rects[card["mode"]] = bg mode = card["mode"] for item_id in (bg, bar, mode_lbl, title_lbl, body_lbl, badge_lbl): self.bg_canvas.tag_bind( item_id, "", lambda e, m=mode: self._on_welcome_card_click(m), ) self.bg_canvas.tag_bind( item_id, "", lambda e, r=bg, m=mode: self.bg_canvas.itemconfig( r, outline=self._MODE_COLORS.get(m, "#4A90D9") if self.deployment_mode.get() != m else None ), ) self.bg_canvas.tag_bind( item_id, "", lambda e, r=bg, m=mode: self.bg_canvas.itemconfig( r, outline=self._MODE_COLORS.get(m, "#4A90D9") if self.deployment_mode.get() == m else "#CCCCCC" ), ) # "You'll need" status line status_y = card_y0 + card_h + 20 self._splash_status_item = self.bg_canvas.create_text( 48, status_y, text="← Select a mode above to continue", fill="#8B8B7A", font=("SF Pro Text", 12, "italic"), anchor="nw", ) self._canvas_items.append(self._splash_status_item) # Restore highlight if a mode was already chosen (e.g. loaded from config) current = self.deployment_mode.get() if current and current in self._welcome_card_rects: self._highlight_welcome_card(current) try: self.bg_canvas.itemconfig( self._splash_status_item, text=_MODE_NEEDS.get(current, ""), ) except Exception: pass self.update_footer() def _on_welcome_card_click(self, mode: str) -> None: self.deployment_mode.set(mode) os.environ["KNOE_MODE"] = mode try: self.cluster_env.set(_MODE_TO_CLUSTER_ENV.get(mode, "dev")) except Exception: pass self._highlight_welcome_card(mode) try: if self._splash_status_item is not None: self.bg_canvas.itemconfig( self._splash_status_item, text=_MODE_NEEDS.get(mode, ""), ) except Exception: pass self.update_footer() def _highlight_welcome_card(self, selected_mode: str) -> None: for m, rect_id in getattr(self, "_welcome_card_rects", {}).items(): if m == selected_mode: color = self._MODE_COLORS.get(m, "#4A90D9") self.bg_canvas.itemconfig(rect_id, outline=color, width=3) else: self.bg_canvas.itemconfig(rect_id, outline="#CCCCCC", width=2) def _start_welcome_dependency_scan(self): self.splash_scan_running = True self.splash_scan_done_at = None self.splash_scan_started_at = time.time() deps = list(self.dependencies) total = len(deps) results = {} def set_status(text: str): try: if self._splash_status_item is not None: # Replace existing text to avoid creating many items self.bg_canvas.itemconfig(self._splash_status_item, text=text) except Exception: pass def worker(): missing_names = [] for idx, dep in enumerate(deps, start=1): try: ok, location, version = self.get_dep_info(dep) except Exception: ok, location, version = False, None, None results[dep["id"]] = (ok, location, version) # Update status text progressively if ok: txt = f"[{idx}/{total}] {dep['name']}: Installed" else: txt = f"[{idx}/{total}] {dep['name']}: Not installed" missing_names.append(dep["name"]) self.safe_after(lambda s=txt: set_status(s)) # tiny sleep to keep UI responsive without being too fast try: time.sleep(0.02) except Exception: pass # Final message final_msg = ( "All dependencies installed." if not missing_names else ("Preparing to install ... " + ", ".join(missing_names)) ) def on_done(): set_status(final_msg) self.splash_scan_running = False self.splash_scan_done_at = time.time() # Re-evaluate footer to potentially show Next self.update_footer() # Important: schedule a follow-up refresh slightly after the debounce window # so the Next button becomes visible without requiring further UI events. self.safe_after(self.update_footer, delay=600) self.safe_after(on_done) t = threading.Thread(target=worker, daemon=True) t.start() # Failsafe refresh slightly after the 8s gating window in case no UI events fire. self.safe_after(self.update_footer, delay=9000) def _create_page_welcome(self): f = self._page_container() ttk.Label(f, text="Welcome to Knoe.DB", style="Title.TLabel").pack( anchor="w", padx=24, pady=(24, 6) ) msg = "Thanks for joining Knoe. We will prepare your system and install the software needed to build and run Knoe." ttk.Label( f, text=msg, style="Body.TLabel", wraplength=800, justify="left" ).pack(anchor="w", padx=24) # self._register_page('welcome', f)