Initial public release
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user