389 lines
16 KiB
Python
389 lines
16 KiB
Python
"""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(
|
||
"<Configure>",
|
||
lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
||
canvas.bind(
|
||
"<Configure>",
|
||
lambda e: canvas.itemconfig(self._cards_window, width=e.width))
|
||
canvas.bind_all("<MouseWheel>", 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("<KeyRelease>", 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("<KeyRelease>", 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()
|