"""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("", ok) root.bind("", 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()