"""Pop-out editor for the Get Variables Semi-Auto sequence. A sequence is an ordered list of steps that the host renders to PowerShell at upload time. Each step is one of: wait {"kind": "wait", "ms": int} send_keys {"kind": "send_keys", "keys": str} run {"kind": "run", "command": str} check {"kind": "check", "command": str, "operator": "equals"|"contains"|"regex"|"numeric_gt"|"numeric_lt"|"exists", "expected": str, # for `exists`, this is the path "value": str} # value the variable is set to on match The 1-based index of each ``check`` step becomes its toggle_count outcome, so check steps are read top-to-bottom by the device's Scroll Lock decoder. """ import tkinter as tk from tkinter import ttk, messagebox _BG = "#2D2D3D" _CARD_BG = "#252535" _ENTRY_BG = "#1E1E2E" _FG = "#FFFFFF" _DIM_FG = "#AAAAAA" _BTN_BG = "#3D3D5C" _SAVE_BG = "#0077CC" _DEL_FG = "#FF7777" STEP_KINDS = [ ("Wait", "wait"), ("Send Keys", "send_keys"), ("Run Command", "run"), ("Check + Signal", "check"), ] CHECK_OPERATORS = [ ("Output equals", "equals"), ("Output contains", "contains"), ("Output matches regex", "regex"), ("Output > number", "numeric_gt"), ("Output < number", "numeric_lt"), ("Path exists", "exists"), ] def _kind_label(kind: str) -> str: for label, k in STEP_KINDS: if k == kind: return label return kind def _op_label(op: str) -> str: for label, k in CHECK_OPERATORS: if k == op: return label return op class SequenceEditorDialog(tk.Toplevel): """Modal dialog that edits a sequence in-place via on_save callback.""" def __init__(self, parent, sequence: list, on_save): super().__init__(parent) self.title("Get Variables — Sequence Editor") self.geometry("720x600") self.minsize(560, 420) self.configure(bg=_BG) self.transient(parent.winfo_toplevel()) self._on_save = on_save # Per-step copy so Cancel discards edits (steps are flat dicts, no nested mutables) self._steps = [dict(s) for s in (sequence or [])] try: import ctypes self.update_idletasks() hwnd = ctypes.windll.user32.GetParent(self.winfo_id()) ctypes.windll.dwmapi.DwmSetWindowAttribute( hwnd, 20, ctypes.byref(ctypes.c_int(1)), ctypes.sizeof(ctypes.c_int)) except Exception: pass self._build_ui() self._refresh() self.protocol("WM_DELETE_WINDOW", self.destroy) def _build_ui(self): hdr = tk.Frame(self, bg=_BG) hdr.pack(fill="x", padx=14, pady=(12, 6)) tk.Label(hdr, text="Sequence", bg=_BG, fg="#0AACFF", font=("Segoe UI", 12, "bold")).pack(side="left") tk.Label(hdr, text="Each Check step's index (1, 2, 3 …) becomes its toggle " "count. First match wins — no match = Fail output.", bg=_BG, fg=_DIM_FG, font=("Segoe UI", 8), justify="left", wraplength=460).pack(side="left", padx=(12, 0)) bar = tk.Frame(self, bg=_BG) bar.pack(fill="x", padx=14, pady=(0, 6)) tk.Label(bar, text="Add step:", bg=_BG, fg=_FG, font=("Segoe UI", 9, "bold")).pack(side="left") for label, kind in STEP_KINDS: tk.Button(bar, text=f"+ {label}", bg=_BTN_BG, fg=_FG, relief="flat", font=("Segoe UI", 9), padx=8, command=lambda k=kind: self._add_step(k)).pack( side="left", padx=(6, 0)) outer = tk.Frame(self, bg=_BG) outer.pack(fill="both", expand=True, padx=14, pady=4) canvas = tk.Canvas(outer, bg=_BG, highlightthickness=0) scrollbar = tk.Scrollbar(outer, orient="vertical", command=canvas.yview) canvas.configure(yscrollcommand=scrollbar.set) scrollbar.pack(side="right", fill="y") canvas.pack(side="left", fill="both", expand=True) self._canvas = canvas self._cards_frame = tk.Frame(canvas, bg=_BG) self._cards_window = canvas.create_window((0, 0), window=self._cards_frame, anchor="nw") self._cards_frame.bind( "", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) canvas.bind( "", lambda e: canvas.itemconfig(self._cards_window, width=e.width)) canvas.bind_all("", lambda e: canvas.yview_scroll( int(-1 * (e.delta / 120)), "units")) btns = tk.Frame(self, bg=_BG) btns.pack(fill="x", padx=14, pady=(4, 12)) tk.Button(btns, text="Cancel", bg=_BTN_BG, fg=_FG, relief="flat", padx=12, font=("Segoe UI", 9), command=self.destroy).pack(side="right", padx=(6, 0)) tk.Button(btns, text="Save", bg=_SAVE_BG, fg=_FG, relief="flat", padx=14, font=("Segoe UI", 9, "bold"), command=self._save).pack(side="right") def _refresh(self): for w in self._cards_frame.winfo_children(): w.destroy() check_idx = 0 for i, step in enumerate(self._steps): if step.get("kind") == "check": check_idx += 1 self._draw_card(i, step, check_idx if step.get("kind") == "check" else None) if not self._steps: tk.Label(self._cards_frame, text="No steps yet — click an + Add button above to begin.", bg=_BG, fg=_DIM_FG, font=("Segoe UI", 9, "italic")).pack( pady=30) def _draw_card(self, idx: int, step: dict, check_n): card = tk.Frame(self._cards_frame, bg=_CARD_BG, bd=0) card.pack(fill="x", pady=4, padx=2) head = tk.Frame(card, bg=_CARD_BG) head.pack(fill="x", padx=8, pady=(6, 2)) kind = step.get("kind", "") title = f"{idx + 1}. {_kind_label(kind)}" if check_n is not None: title += f" [toggle = {check_n}]" tk.Label(head, text=title, bg=_CARD_BG, fg="#0AACFF", font=("Segoe UI", 10, "bold")).pack(side="left") tk.Button(head, text="×", bg=_CARD_BG, fg=_DEL_FG, relief="flat", font=("Segoe UI", 10, "bold"), width=2, command=lambda i=idx: self._delete(i)).pack(side="right") tk.Button(head, text="↓", bg=_CARD_BG, fg=_FG, relief="flat", font=("Segoe UI", 10), width=2, command=lambda i=idx: self._move(i, +1)).pack(side="right") tk.Button(head, text="↑", bg=_CARD_BG, fg=_FG, relief="flat", font=("Segoe UI", 10), width=2, command=lambda i=idx: self._move(i, -1)).pack(side="right") body = tk.Frame(card, bg=_CARD_BG) body.pack(fill="x", padx=10, pady=(0, 8)) if kind == "wait": self._draw_wait_fields(body, step) elif kind == "send_keys": self._draw_send_keys_fields(body, step) elif kind == "run": self._draw_run_fields(body, step) elif kind == "check": self._draw_check_fields(body, step) else: tk.Label(body, text=f"Unknown step kind: {kind}", bg=_CARD_BG, fg=_DEL_FG, font=("Segoe UI", 9, "italic")).pack(anchor="w") def _draw_wait_fields(self, parent, step): row = tk.Frame(parent, bg=_CARD_BG) row.pack(fill="x") tk.Label(row, text="Wait (ms):", bg=_CARD_BG, fg=_FG, font=("Segoe UI", 9), width=12, anchor="w").pack(side="left") v = tk.StringVar(value=str(step.get("ms", 500))) e = tk.Entry(row, textvariable=v, bg=_ENTRY_BG, fg=_FG, insertbackground=_FG, font=("Consolas", 10), relief="flat", width=10) e.pack(side="left", padx=4, ipady=2) def save(*_): try: step["ms"] = max(0, int(v.get())) except ValueError: pass v.trace_add("write", save) def _draw_send_keys_fields(self, parent, step): row = tk.Frame(parent, bg=_CARD_BG) row.pack(fill="x") tk.Label(row, text="Keys:", bg=_CARD_BG, fg=_FG, font=("Segoe UI", 9), width=12, anchor="w").pack(side="left") v = tk.StringVar(value=str(step.get("keys", ""))) tk.Entry(row, textvariable=v, bg=_ENTRY_BG, fg=_FG, insertbackground=_FG, font=("Consolas", 10), relief="flat").pack(side="left", fill="x", expand=True, padx=4, ipady=2) tk.Label(parent, text="WScript.Shell SendKeys syntax — e.g. {ENTER}, ^c, +{TAB 3}", bg=_CARD_BG, fg=_DIM_FG, font=("Segoe UI", 8)).pack( anchor="w", pady=(2, 0)) def save(*_): step["keys"] = v.get() v.trace_add("write", save) def _draw_run_fields(self, parent, step): tk.Label(parent, text="PowerShell:", bg=_CARD_BG, fg=_FG, font=("Segoe UI", 9)).pack(anchor="w") text = tk.Text(parent, height=3, bg=_ENTRY_BG, fg=_FG, insertbackground=_FG, font=("Consolas", 10), relief="flat", wrap="none") text.insert("1.0", step.get("command", "")) text.pack(fill="x", pady=2) def save(*_): step["command"] = text.get("1.0", "end-1c") text.bind("", save) def _draw_check_fields(self, parent, step): op_row = tk.Frame(parent, bg=_CARD_BG) op_row.pack(fill="x") tk.Label(op_row, text="Operator:", bg=_CARD_BG, fg=_FG, font=("Segoe UI", 9), width=12, anchor="w").pack(side="left") op_display = [d for d, _ in CHECK_OPERATORS] op_value = [v for _, v in CHECK_OPERATORS] cur = step.get("operator", "equals") idx = op_value.index(cur) if cur in op_value else 0 op_var = tk.StringVar(value=op_display[idx]) op_box = ttk.Combobox(op_row, textvariable=op_var, values=op_display, state="readonly") op_box.pack(side="left", fill="x", expand=True, padx=4) # Operator-specific fields; rebuilt on operator change spec = tk.Frame(parent, bg=_CARD_BG) spec.pack(fill="x", pady=(4, 0)) val_row = tk.Frame(parent, bg=_CARD_BG) val_row.pack(fill="x", pady=(6, 0)) tk.Label(val_row, text="On match, set var to:", bg=_CARD_BG, fg=_FG, font=("Segoe UI", 9), width=20, anchor="w").pack(side="left") val_var = tk.StringVar(value=str(step.get("value", ""))) tk.Entry(val_row, textvariable=val_var, bg=_ENTRY_BG, fg=_FG, insertbackground=_FG, font=("Consolas", 10), relief="flat").pack(side="left", fill="x", expand=True, padx=4, ipady=2) def save_value(*_): step["value"] = val_var.get() val_var.trace_add("write", save_value) def render_spec(): for w in spec.winfo_children(): w.destroy() current_op = step.get("operator", "equals") if current_op == "exists": tk.Label(spec, text="Path:", bg=_CARD_BG, fg=_FG, font=("Segoe UI", 9), width=12, anchor="w").pack(side="left") pv = tk.StringVar(value=step.get("expected", "")) tk.Entry(spec, textvariable=pv, bg=_ENTRY_BG, fg=_FG, insertbackground=_FG, font=("Consolas", 10), relief="flat").pack(side="left", fill="x", expand=True, padx=4, ipady=2) def save_path(*_): step["expected"] = pv.get() pv.trace_add("write", save_path) else: tk.Label(spec, text="Command:", bg=_CARD_BG, fg=_FG, font=("Segoe UI", 9)).pack(anchor="w") cmd_text = tk.Text(spec, height=2, bg=_ENTRY_BG, fg=_FG, insertbackground=_FG, font=("Consolas", 10), relief="flat", wrap="none") cmd_text.insert("1.0", step.get("command", "")) cmd_text.pack(fill="x", pady=2) def save_cmd(*_): step["command"] = cmd_text.get("1.0", "end-1c") cmd_text.bind("", save_cmd) exp_row = tk.Frame(spec, bg=_CARD_BG) exp_row.pack(fill="x", pady=(2, 0)) exp_label = "Number:" if current_op in ( "numeric_gt", "numeric_lt") else ( "Pattern:" if current_op == "regex" else "Expected:") tk.Label(exp_row, text=exp_label, bg=_CARD_BG, fg=_FG, font=("Segoe UI", 9), width=12, anchor="w").pack(side="left") ev = tk.StringVar(value=step.get("expected", "")) tk.Entry(exp_row, textvariable=ev, bg=_ENTRY_BG, fg=_FG, insertbackground=_FG, font=("Consolas", 10), relief="flat").pack(side="left", fill="x", expand=True, padx=4, ipady=2) def save_exp(*_): step["expected"] = ev.get() ev.trace_add("write", save_exp) def on_op_change(*_): disp = op_var.get() i = op_display.index(disp) if disp in op_display else 0 new_op = op_value[i] if new_op != step.get("operator"): step["operator"] = new_op render_spec() op_var.trace_add("write", on_op_change) render_spec() def _add_step(self, kind: str): step = {"kind": kind} if kind == "wait": step["ms"] = 500 elif kind == "send_keys": step["keys"] = "" elif kind == "run": step["command"] = "" elif kind == "check": step.update({ "operator": "equals", "command": "", "expected": "", "value": "", }) self._steps.append(step) self._refresh() def _delete(self, i: int): if 0 <= i < len(self._steps): self._steps.pop(i) self._refresh() def _move(self, i: int, delta: int): j = i + delta if 0 <= j < len(self._steps): self._steps[i], self._steps[j] = self._steps[j], self._steps[i] self._refresh() def _save(self): # An empty value would silently set the variable to "", which is rarely intended check_count = 0 for i, step in enumerate(self._steps): if step.get("kind") == "check": check_count += 1 if not (step.get("value") or "").strip(): if not messagebox.askyesno( "Empty value", f"Check step {check_count} has no value to set the " f"variable to. Save anyway? (Variable will be set " f"to an empty string on match.)", parent=self, ): return break try: self._on_save(self._steps) except Exception as e: messagebox.showerror("Sequence Editor", str(e), parent=self) return self.destroy()