Initial commit

master
Kuba Orlik 3 years ago
commit fdad031f12

3
.gitignore vendored

@ -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
}
}
]
}

19
package-lock.json generated

@ -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…
Cancel
Save