commits
tags
"""Interaktiv dialog för prompt-steget (rpa/flow_engine.py).
Körs som en egen process (tkinter vill ha huvudtråden – samma mönster som
rpa/desktop_capture.py). Läser en spec som JSON på stdin, visar en ruta och
skriver resultatet som JSON på stdout.
Spec:
{"mode": "confirm" | "input" | "fields",
"message": "...", # färdig-interpolerad text
"fields": ["namn", ...], # mode=fields
"values": {"namn": "värde", ...}} # aktuella värden (mode=fields/input)
Resultat:
confirm -> {"ok": true|false}
input -> {"ok": true, "value": "..."} | {"ok": false}
fields -> {"ok": true, "values": {...}} | {"ok": false}
python -m rpa.prompt_dialog (läser stdin – för manuellt test:
echo {"mode":"confirm","message":"Hej"} | python -m rpa.prompt_dialog)
"""
from __future__ import annotations
import json
import sys
def _run(spec: dict) -> dict:
try:
import tkinter as tk
from tkinter import ttk
except Exception as exc: # noqa: BLE001 # pragma: no cover
return {"ok": False, "error": f"tkinter saknas: {exc}"}
try: # DPI-medveten (som desktop_capture)
import ctypes
ctypes.windll.shcore.SetProcessDpiAwareness(2)
except Exception: # noqa: BLE001
pass
mode = spec.get("mode", "confirm")
message = str(spec.get("message", ""))
field_names = list(spec.get("fields", []) or [])
values = dict(spec.get("values", {}) or {})
state: dict = {"ok": False}
root = tk.Tk()
root.title("RPA – bekräfta")
root.attributes("-topmost", True)
root.resizable(False, False)
frm = ttk.Frame(root, padding=16)
frm.grid(sticky="nsew")
ttk.Label(frm, text=message, wraplength=460, justify="left").grid(
row=0, column=0, columnspan=2, sticky="w", pady=(0, 12)
)
entries: dict[str, tk.Entry] = {}
single: tk.Entry | None = None
row = 1
if mode == "input":
single = ttk.Entry(frm, width=48)
single.insert(0, str(spec.get("default", "")))
single.grid(row=row, column=0, columnspan=2, sticky="ew", pady=(0, 12))
single.focus_set()
row += 1
elif mode == "fields":
for name in field_names:
ttk.Label(frm, text=name).grid(row=row, column=0, sticky="w", padx=(0, 8), pady=2)
e = ttk.Entry(frm, width=42)
e.insert(0, str(values.get(name, "")))
e.grid(row=row, column=1, sticky="ew", pady=2)
entries[name] = e
row += 1
ttk.Label(frm, text="").grid(row=row, column=0, pady=6)
row += 1
def ok(_e=None):
state["ok"] = True
if mode == "input" and single is not None:
state["value"] = single.get()
if mode == "fields":
state["values"] = {n: e.get() for n, e in entries.items()}
root.destroy()
def cancel(_e=None):
state["ok"] = False
root.destroy()
btns = ttk.Frame(frm)
btns.grid(row=row, column=0, columnspan=2, sticky="e", pady=(8, 0))
ttk.Button(btns, text="Avbryt", command=cancel).grid(row=0, column=0, padx=(0, 8))
ok_btn = ttk.Button(btns, text="OK", command=ok)
ok_btn.grid(row=0, column=1)
if mode != "input":
ok_btn.focus_set()
frm.columnconfigure(1, weight=1)
root.bind("<Return>", ok)
root.bind("<Escape>", cancel)
root.protocol("WM_DELETE_WINDOW", cancel)
root.after(80, lambda: (root.lift(), root.focus_force()))
root.eval("tk::PlaceWindow . center")
root.mainloop()
return state
def main() -> None:
raw = sys.stdin.read()
try:
spec = json.loads(raw) if raw.strip() else {"mode": "confirm", "message": raw}
except json.JSONDecodeError:
spec = {"mode": "confirm", "message": raw}
result = _run(spec)
print(json.dumps(result, ensure_ascii=False))
sys.exit(0 if result.get("ok") else 1)
if __name__ == "__main__":
main()