Initial commit
						commit
						fdad031f12
					
				@ -0,0 +1,3 @@
 | 
			
		||||
/@types/
 | 
			
		||||
/lib/
 | 
			
		||||
/node_modules/
 | 
			
		||||
@ -0,0 +1,14 @@
 | 
			
		||||
{
 | 
			
		||||
  useTabs: true,
 | 
			
		||||
  tabWidth: 4,
 | 
			
		||||
  trailingComma: "es5",
 | 
			
		||||
  "overrides": [
 | 
			
		||||
    {
 | 
			
		||||
      "files": "*.yml",
 | 
			
		||||
      "options": {
 | 
			
		||||
        "tabWidth": 2,
 | 
			
		||||
        "useTabs": false
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,19 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "node-zenity",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "lockfileVersion": 1,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@types/node": {
 | 
			
		||||
      "version": "14.14.17",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.17.tgz",
 | 
			
		||||
      "integrity": "sha512-G0lD1/7qD60TJ/mZmhog76k7NcpLWkPVGgzkRy3CTlnFu4LUQh5v2Wa661z6vnXmD8EQrnALUyf0VRtrACYztw=="
 | 
			
		||||
    },
 | 
			
		||||
    "typescript": {
 | 
			
		||||
      "version": "4.1.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
 | 
			
		||||
      "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
 | 
			
		||||
      "dev": true
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,19 @@
 | 
			
		||||
{
 | 
			
		||||
	"name": "node-zenity",
 | 
			
		||||
	"description": "Node zenity bindings with typescript support",
 | 
			
		||||
	"version": "1.0.0",
 | 
			
		||||
	"author": "Kuba Orlik <kontakt@kuba-orlik.name>",
 | 
			
		||||
	"license": "MIT",
 | 
			
		||||
	"main": "./lib/index.js",
 | 
			
		||||
	"types": "./@types/index.d.ts",
 | 
			
		||||
	"scripts": {
 | 
			
		||||
		"build": "tsc",
 | 
			
		||||
		"watch": "npm run build -- --watch"
 | 
			
		||||
	},
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"@types/node": "^14.14.17"
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
		"typescript": "^4.1.3"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,183 @@
 | 
			
		||||
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<void> {
 | 
			
		||||
		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<DialogResponse> {
 | 
			
		||||
		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 });
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,22 @@
 | 
			
		||||
import Dialog from "./dialog";
 | 
			
		||||
 | 
			
		||||
export default class Entry {
 | 
			
		||||
	static async show(
 | 
			
		||||
		options?: Partial<{
 | 
			
		||||
			text: string;
 | 
			
		||||
			"entry-text": string;
 | 
			
		||||
			"hide-text": true;
 | 
			
		||||
			title: string;
 | 
			
		||||
			width: number;
 | 
			
		||||
			height: number;
 | 
			
		||||
		}>
 | 
			
		||||
	): Promise<string> {
 | 
			
		||||
		const dialog = new Dialog("entry", {
 | 
			
		||||
			...options,
 | 
			
		||||
			width: ((options?.width as unknown) as string)?.toString(),
 | 
			
		||||
			height: ((options?.height as unknown) as string)?.toString(),
 | 
			
		||||
		});
 | 
			
		||||
		await dialog.show();
 | 
			
		||||
		return dialog.output.split("\n")[0];
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,20 @@
 | 
			
		||||
import Dialog from "./dialog";
 | 
			
		||||
 | 
			
		||||
export default class FileSelection {
 | 
			
		||||
	static async getFile(
 | 
			
		||||
		options: Partial<{
 | 
			
		||||
			multiple?: true;
 | 
			
		||||
			directory?: true;
 | 
			
		||||
			save?: true;
 | 
			
		||||
			separator: string;
 | 
			
		||||
			"confirm-overwrite"?: true;
 | 
			
		||||
			"file-filter": string;
 | 
			
		||||
			title: string;
 | 
			
		||||
		}>
 | 
			
		||||
	): Promise<string[]> {
 | 
			
		||||
		const dialog = new Dialog("file-selection", options);
 | 
			
		||||
		await dialog.show();
 | 
			
		||||
		await dialog.getAnswer();
 | 
			
		||||
		return dialog.output.split("\n");
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,8 @@
 | 
			
		||||
export { default as FileSelection } from "./file-selection";
 | 
			
		||||
export { default as Info } from "./info";
 | 
			
		||||
export {
 | 
			
		||||
	default as List,
 | 
			
		||||
	responseIsCustomButton,
 | 
			
		||||
	ItemsSelectedResponse,
 | 
			
		||||
} from "./list";
 | 
			
		||||
export { default as Entry } from "./entry";
 | 
			
		||||
@ -0,0 +1,18 @@
 | 
			
		||||
import Dialog from "./dialog";
 | 
			
		||||
 | 
			
		||||
export default class Info {
 | 
			
		||||
	static async show(
 | 
			
		||||
		text: string,
 | 
			
		||||
		options?: Partial<{
 | 
			
		||||
			"icon-name": string;
 | 
			
		||||
			"no-wrap": true;
 | 
			
		||||
			"no-markup": true;
 | 
			
		||||
			ellipsize: true;
 | 
			
		||||
			title: string;
 | 
			
		||||
		}>
 | 
			
		||||
	): Promise<void> {
 | 
			
		||||
		const dialog = new Dialog("info", { ...options, text });
 | 
			
		||||
		await dialog.show();
 | 
			
		||||
		await dialog.getAnswer();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,103 @@
 | 
			
		||||
import Dialog, { DialogOptions } from "./dialog";
 | 
			
		||||
import sleep from "./sleep";
 | 
			
		||||
 | 
			
		||||
export type ListOptions = {
 | 
			
		||||
	mode: "checklist" | "radiolist" | "imagelist" | "list";
 | 
			
		||||
	columns: string[];
 | 
			
		||||
} & Partial<{
 | 
			
		||||
	title: string;
 | 
			
		||||
	text: string;
 | 
			
		||||
	rows: string[][];
 | 
			
		||||
	multiple: true;
 | 
			
		||||
	separator: string;
 | 
			
		||||
	editable: true;
 | 
			
		||||
	"print-column": number;
 | 
			
		||||
	extraButtons: string[];
 | 
			
		||||
	height: number;
 | 
			
		||||
	width: number;
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
type CustomButtonPressed = { _type: "custom_button"; button_pressed: string };
 | 
			
		||||
export type ItemsSelectedResponse = {
 | 
			
		||||
	_type: "items_selected";
 | 
			
		||||
	items_selected: string[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type ListResponse = CustomButtonPressed | ItemsSelectedResponse;
 | 
			
		||||
 | 
			
		||||
export function responseIsCustomButton(
 | 
			
		||||
	response: ListResponse
 | 
			
		||||
): response is CustomButtonPressed {
 | 
			
		||||
	return response._type === "custom_button";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default class List {
 | 
			
		||||
	private dialog: Dialog;
 | 
			
		||||
	constructor(private options: ListOptions) {}
 | 
			
		||||
 | 
			
		||||
	async show(): Promise<void> {
 | 
			
		||||
		const dialog = new Dialog(
 | 
			
		||||
			"list",
 | 
			
		||||
			List.prepareOptions(this.options),
 | 
			
		||||
			List.preparePositionalArguments(this.options)
 | 
			
		||||
		);
 | 
			
		||||
		this.dialog = dialog;
 | 
			
		||||
		await dialog.show();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async getAnswer(): Promise<ListResponse> {
 | 
			
		||||
		const response = await this.dialog.getAnswer();
 | 
			
		||||
		if (response.code === 1) {
 | 
			
		||||
			return {
 | 
			
		||||
				_type: "custom_button",
 | 
			
		||||
				button_pressed: response.output.split("\n")[0],
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
		return {
 | 
			
		||||
			_type: "items_selected",
 | 
			
		||||
			items_selected: response.output
 | 
			
		||||
				.split("\n")[0]
 | 
			
		||||
				.split(this.options.separator || "|"),
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static async showAndGetAnswer(options: ListOptions) {
 | 
			
		||||
		const list = new List(options);
 | 
			
		||||
		await list.show();
 | 
			
		||||
		return list.getAnswer();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static prepareOptions(options: ListOptions): DialogOptions {
 | 
			
		||||
		const ret: DialogOptions = {};
 | 
			
		||||
		if (options.mode !== "list") {
 | 
			
		||||
			ret[options.mode] = true;
 | 
			
		||||
		}
 | 
			
		||||
		ret.title = options.title;
 | 
			
		||||
		ret.text = options.text;
 | 
			
		||||
		if (options.columns) {
 | 
			
		||||
			ret.column = options.columns;
 | 
			
		||||
		}
 | 
			
		||||
		ret.multiple = options.multiple;
 | 
			
		||||
		ret.separator = options.separator;
 | 
			
		||||
		ret.editable = options.editable;
 | 
			
		||||
		ret["print-column"] = options["print-column"]?.toString();
 | 
			
		||||
		ret["extra-button"] = options.extraButtons;
 | 
			
		||||
		ret.height = options.height?.toString();
 | 
			
		||||
		ret.width = options.width?.toString();
 | 
			
		||||
		return ret;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static preparePositionalArguments(options: ListOptions): string[] {
 | 
			
		||||
		const ret: string[] = [];
 | 
			
		||||
		for (const row of options.rows || []) {
 | 
			
		||||
			for (const element of row) {
 | 
			
		||||
				ret.push(element);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return ret;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hide() {
 | 
			
		||||
		this.dialog.process.kill();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,14 @@
 | 
			
		||||
import { spawn } from "child_process";
 | 
			
		||||
 | 
			
		||||
export default function simpleSpawn(cmd: string, arg: string[]) {
 | 
			
		||||
	const process = spawn(cmd, arg);
 | 
			
		||||
	let output = "";
 | 
			
		||||
	let err = "";
 | 
			
		||||
	return new Promise((resolve, reject) => {
 | 
			
		||||
		process.on("close", (code) => {
 | 
			
		||||
			code === 0 ? resolve(output) : reject(err);
 | 
			
		||||
		});
 | 
			
		||||
		process.stdout.on("data", (data) => (output += data.toString("utf-8")));
 | 
			
		||||
		process.stderr.on("data", (data) => (err += data.toString("utf-8")));
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,8 @@
 | 
			
		||||
import { promisify } from "util";
 | 
			
		||||
 | 
			
		||||
const sleep = (timeout: number) =>
 | 
			
		||||
	promisify((time: number, cb: (...args: any[]) => void) =>
 | 
			
		||||
		setTimeout(cb, time)
 | 
			
		||||
	)(timeout);
 | 
			
		||||
 | 
			
		||||
export default sleep;
 | 
			
		||||
@ -0,0 +1,20 @@
 | 
			
		||||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "module": "commonjs",
 | 
			
		||||
    "moduleResolution": "node",
 | 
			
		||||
    "noImplicitAny": true,
 | 
			
		||||
    "noImplicitThis": true,
 | 
			
		||||
    "strictNullChecks": true,
 | 
			
		||||
    "target": "ES2020",
 | 
			
		||||
    "declaration": true,
 | 
			
		||||
    "esModuleInterop": true,
 | 
			
		||||
    "lib": ["es6", "esnext"],
 | 
			
		||||
    "outDir": "lib",
 | 
			
		||||
    "checkJs": true,
 | 
			
		||||
    "allowJs": true,
 | 
			
		||||
    "declarationDir": "@types",
 | 
			
		||||
    "resolveJsonModule": true,
 | 
			
		||||
    "sourceMap": true
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src/**/*"]
 | 
			
		||||
}
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue