foxygit / RPA-Remisser Log in
commits tags

/rpa/prompt_dialog.py · 3.88 KB

raw
"""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()