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