import { ChildProcessWithoutNullStreams, spawn } from "child_process"; import { EventEmitter } from "events"; import { promisify } from "util"; import simpleSpawn from "./simple-spawn"; import sleep from "./sleep"; export type DialogOptions = Record< string, true | string | undefined | string[] >; // array value means use this flag this many times let last_position: { x: number; y: number } | null = null; async function get_geometry() { const result = (await simpleSpawn("bash", [ "-c", 'wmctrl -l -x node-zenity -G | grep node-zenity | awk \'{print $3 " " $4 " " $5 " " $6}\'', ])) as string; const [x, y, width, height] = result .split("\n")[0] .split(" ") .map((s) => parseInt(s)); return { x, y, width, height }; } async function set_geometry({ x, y, width, height, }: { x: number; y: number; width?: number; height?: number; }) { if (width === undefined) { width = -1; } if (height === undefined) { height = -1; } await simpleSpawn("wmctrl", [ "-x", "-a", "node-zenity", "-e", `0,${Math.round(x)},${Math.round(y)},${Math.round(width)},${Math.round( height )}`, ]); } type DialogResponse = { code: number; output: string; error: string; }; export default class Dialog extends EventEmitter { shown = false; process: ChildProcessWithoutNullStreams; output: string = ""; error: string = ""; constructor( public type_option: string, public args: DialogOptions, public positional_args: string[] = [] ) { super(); } private prepareArguments(): string[] { const ret = [`--${this.type_option}`]; for (const arg in this.args) { const value = this.args[arg]; if (typeof value === "boolean") { ret.push(`--${arg}`); } else if (typeof value === "string") { ret.push(`--${arg}=${value}`); } else if (Array.isArray(value)) { value.forEach((element) => ret.push(`--${arg}=${element}`)); } else if (value === undefined) { //void } else { console.log("unknown value:", value); throw new Error("uknown arg value type"); } } return ret; } async show(): Promise { const args: string[] = [ "--class=node-zenity", ...this.prepareArguments(), ...this.positional_args, ]; console.log("spawning process!", args.join(" ")); this.process = spawn("zenity", args); let position_listener_interval = setInterval(async () => { const { x, y, width, height } = await get_geometry(); console.log( { x, y, width, height }, { x: Math.round(x + width / 2), y: Math.round(y + height / 2) } ); last_position = { x: Math.round(x + width / 2), y: Math.round(y + height / 2), }; }, 2000); let spawned = false; return new Promise((resolve, reject) => { this.process.on("spawn", async () => { spawned = true; resolve(); let tries = 0; let succeded = false; while (!succeded && tries < 15) { try { tries++; await sleep(50); await simpleSpawn("wmctrl", [ "-x", "-r", "node-zenity", "-b", "add,above", ]); // to pin to top await simpleSpawn("wmctrl", [ "-x", "-a", "node-zenity", ]); // to activate it if (last_position) { const { width, height } = await get_geometry(); await set_geometry({ x: last_position.x - width / 2 - 11, y: last_position.y - height / 2 - 82, }); // to put it where it last was } succeded = true; console.log("WMCTRL SUCCEEDED!"); } catch (e) { console.error(e); continue; } } }); this.process.stdout.on("data", (chunk) => { this.output += chunk; }); this.process.stderr.on("data", (chunk) => { this.error += chunk; }); this.process.on("close", (code) => { clearInterval(position_listener_interval); console.log("process closed. output:", this.output); if (code !== 0) { if (!spawned) { reject({ code, message: this.error }); } else { this.emit("error", code); } } else { this.emit("done"); } }); }); } getAnswer(): Promise { return new Promise((resolve, reject) => { this.on("done", () => resolve({ code: 0, output: this.output, error: this.error }) ); this.on("error", (code) => { resolve({ code, output: this.output, error: this.error }); }); }); } }