@bobfrankston/cmsgbox
Version:
Cross-platform dialog utility for Node.js using Python and tkinter
285 lines (250 loc) • 9.88 kB
text/typescript
import { spawn } from 'child_process';
/**
* Options for the message box dialog.
*/
export interface MessageBoxOptions {
/** The message text to display in the dialog. */
message: string;
/** The title of the dialog window. Defaults to "Message". */
title?: string;
/** Whether to show an input dialog instead of a message dialog. */
input?: boolean;
/** The default text to pre-fill in the input field (only used if input is true). */
defaultText?: string;
}
const dirname = import.meta.dirname;
/**
* Displays a message dialog with the specified message and title.
* @param message The message text to display.
* @param title The title of the dialog window.
*/
export async function showMessage(message: string, title?: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
const titleText = title || "Message";
// Create JSON parameters for the Python script
const params = JSON.stringify({
type: 'message',
message: message,
title: titleText
});
// Call external Python script with JSON parameters
const pythonArgs = [
'pyutils/msgbox.py',
'-json', params
];
const child = spawn('python3', pythonArgs, {
stdio: ['pipe', 'pipe', 'pipe'],
cwd: dirname
});
let output = '';
let errorOutput = '';
child.stdout.on('data', (data) => output += data.toString());
child.stderr.on('data', (data) => errorOutput += data.toString());
child.on('exit', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`Python dialog failed: ${errorOutput}`));
}
});
child.on('error', (err) => {
// Try python instead of python3
const fallbackChild = spawn('python', pythonArgs, {
stdio: ['pipe', 'pipe', 'pipe'],
cwd: dirname
});
fallbackChild.on('exit', (fallbackCode) => {
if (fallbackCode === 0) {
resolve();
} else {
reject(new Error(`Python dialog failed: ${errorOutput}`));
}
});
fallbackChild.on('error', reject);
});
});
}
/**
* Displays an input dialog and returns the user's input.
* @param message The prompt message for the input dialog.
* @param title The title of the dialog window.
* @param defaultText The default text to pre-fill in the input field.
* @returns The user's input string, or undefined if canceled.
*/
/**
* Displays an input dialog and returns the user's input.
* @param message The prompt message for the input dialog.
* @param title The title of the dialog window.
* @param defaultText The default text to pre-fill in the input field.
* @returns The user's input string, or undefined if canceled.
*/
export async function showInput(message?: string, title?: string, defaultText?: string): Promise<string | undefined> {
return new Promise<string | undefined>((resolve, reject) => {
const messageText = message || "Enter text:";
const titleText = title || "Input";
const defaultValue = defaultText || "";
// Create JSON parameters for the Python script
const params = JSON.stringify({
type: 'input',
message: messageText,
title: titleText,
defaultValue: defaultValue
});
// Call external Python script with JSON parameters
const pythonArgs = [
'pyutils/msgbox.py',
'-json', params
];
const child = spawn('python3', pythonArgs, {
stdio: ['pipe', 'pipe', 'pipe'],
cwd: dirname
});
let output = '';
let errorOutput = '';
child.stdout.on('data', (data) => output += data.toString());
child.stderr.on('data', (data) => errorOutput += data.toString());
child.on('exit', (code) => {
if (code === 0) {
const result = output.trim();
if (result === "Cancel") {
resolve(undefined);
} else {
resolve(result);
}
} else {
reject(new Error(`Python dialog failed: ${errorOutput}`));
}
});
child.on('error', (err) => {
// Try python instead of python3
const fallbackChild = spawn('python', pythonArgs, {
stdio: ['pipe', 'pipe', 'pipe'],
cwd: dirname
});
let fallbackOutput = '';
let fallbackError = '';
fallbackChild.stdout.on('data', (data) => fallbackOutput += data.toString());
fallbackChild.stderr.on('data', (data) => fallbackError += data.toString());
fallbackChild.on('exit', (fallbackCode) => {
if (fallbackCode === 0) {
const result = fallbackOutput.trim();
if (result === "Cancel") {
resolve(undefined);
} else {
resolve(result);
}
} else {
reject(new Error(`Python dialog failed: ${fallbackError}`));
}
});
fallbackChild.on('error', reject);
});
});
}
/**
* Unified function to display either a message or input dialog based on options.
* @param options The options for the dialog.
* @returns For input dialogs, returns the user's input string or undefined. For message dialogs, returns undefined.
*/
export async function messageBox(options: MessageBoxOptions): Promise<string | void> {
if (options.input) {
return await showInput(options.message, options.title, options.defaultText);
} else {
await showMessage(options.message, options.title);
}
}
/**
* Class for creating message box instances (alternative API).
*/
export class MessageBox {
/**
* Displays a message dialog.
* @param message The message text.
* @param title The dialog title.
*/
async show(message: string, title?: string): Promise<void> {
return showMessage(message, title);
}
/**
* Displays an input dialog.
* @param message The prompt message.
* @param title The dialog title.
* @param defaultText The default input text.
* @returns The user's input or undefined.
*/
async input(message?: string, title?: string, defaultText?: string): Promise<string | undefined> {
return showInput(message, title, defaultText);
}
}
export default { showMessage, showInput, messageBox, MessageBox };
// Command line interface
if (import.meta.main) {
function showUsageAndExit(msg?: string) {
if (msg)
console.error(`Error: ${msg}`);
console.error('Usage: node index.js [-m <message>] [-t <title>] [-i] [-d <default>]');
console.error(' -m, --message Set the message text (can be multiple words)');
console.error(' -t, --title Set the title of the message box');
console.error(' -i, --input Show input dialog instead of message');
console.error(' -d, --default Default text for input dialog');
process.exit(1);
}
async function main() {
const args = process.argv.slice(2);
if (args.length === 0)
showUsageAndExit();
let message: string | undefined = undefined;
let title = 'Message';
let isInput = false;
let defaultText = '';
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '-m' || arg === '--message') {
if (i + 1 < args.length) {
i++; // Move to the message
message = args[i] || '';
// Continue collecting message until next flag or end
while (i + 1 < args.length && !args[i + 1].startsWith('-')) {
i++;
message += ' ' + (args[i] || '');
}
} else {
console.error('Error: -m requires a message argument');
showUsageAndExit();
}
} else if (arg === '-t' || arg === '--title') {
if (i + 1 < args.length) {
title = args[++i];
} else {
showUsageAndExit('Error: -t requires a title argument');
}
} else if (arg === '-i' || arg === '--input') {
isInput = true;
} else if (arg === '-d' || arg === '--default') {
if (i + 1 < args.length) {
defaultText = args[++i];
} else {
console.error('Error: -d requires a default text argument');
showUsageAndExit();
}
} else if (arg.startsWith('-')) {
showUsageAndExit(`Error: Unrecognized option '${arg}'`);
} else {
// If no -m flag and this is the first non-flag argument, treat as message
if (message === undefined) {
message = arg || '';
} else {
showUsageAndExit(`Error: Unexpected argument '${arg}'`);
}
}
}
if (message === undefined || message === '')
showUsageAndExit('Error: Message is required');
const result = await messageBox({ message, title, input: isInput, defaultText });
if (isInput && result) {
console.log(result);
}
}
await main();
}