import { execSync } from 'child_process';
import {
Box,
Text,
useInput
} from 'ink';
import React, { useState } from 'react';
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
CustomKeybind,
Widget,
WidgetEditorDisplay,
WidgetEditorProps,
WidgetItem
} from '../types/Widget';
export class CustomCommandWidget implements Widget {
getDefaultColor(): string { return 'white'; }
getDescription(): string { return 'Executes a custom shell command and displays output'; }
getDisplayName(): string { return 'Custom Command'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
const cmd = item.commandPath ?? 'No command';
const truncatedCmd = cmd.length > 20 ? `${cmd.substring(0, 17)}...` : cmd;
const displayText = `${this.getDisplayName()} (${truncatedCmd})`;
// Build modifiers string
const modifiers: string[] = [];
if (item.maxWidth) {
modifiers.push(`max:${item.maxWidth}`);
}
if (item.timeout && item.timeout !== 1000) {
modifiers.push(`timeout:${item.timeout}ms`);
}
if (item.preserveColors) {
modifiers.push('preserve');
}
return {
displayText,
modifierText: modifiers.length > 0 ? `(${modifiers.join(', ')})` : undefined
};
}
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
if (action === 'toggle-preserve') {
return { ...item, preserveColors: !item.preserveColors };
}
return null;
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
if (context.isPreview) {
return item.commandPath ? `[cmd: ${item.commandPath.substring(0, 20)}${item.commandPath.length > 20 ? '...' : ''}]` : '[No command]';
} else if (item.commandPath && context.data) {
try {
const timeout = item.timeout ?? 1000;
const jsonInput = JSON.stringify(context.data);
let output = execSync(item.commandPath, {
encoding: 'utf8',
input: jsonInput,
timeout: timeout,
stdio: ['pipe', 'pipe', 'ignore'],
env: process.env
}).trim();
// Strip ANSI codes if preserveColors is false
if (!item.preserveColors) {
// Strip all ANSI escape sequences
output = output.replace(/\x1b\[[0-9;]*m/g, '');
}
if (item.maxWidth && output.length > item.maxWidth) {
output = output.substring(0, item.maxWidth - 3) + '...';
}
return output || null;
} catch (error) {
// Provide more specific error messages
if (error instanceof Error) {
const execError = error as Error & {
code?: string;
signal?: string;
status?: number;
};
if (execError.code === 'ENOENT') {
return '[Cmd not found]';
} else if (execError.code === 'ETIMEDOUT') {
return '[Timeout]';
} else if (execError.code === 'EACCES') {
return '[Permission denied]';
} else if (execError.signal) {
return `[Signal: ${execError.signal}]`;
} else if (execError.status !== undefined) {
return `[Exit: ${execError.status}]`;
}
}
return '[Error]';
}
}
return null;
}
getCustomKeybinds(): CustomKeybind[] {
return [
{ key: 'e', label: '(e)dit cmd', action: 'edit-command' },
{ key: 'w', label: '(w)idth', action: 'edit-width' },
{ key: 't', label: '(t)imeout', action: 'edit-timeout' },
{ key: 'p', label: '(p)reserve colors', action: 'toggle-preserve' }
];
}
renderEditor(props: WidgetEditorProps): React.ReactElement {
return ;
}
supportsRawValue(): boolean { return false; }
supportsColors(item: WidgetItem): boolean {
// Only supports colors if preserveColors is false
return !item.preserveColors;
}
}
interface EditorMode { type: 'command' | 'width' | 'timeout' | null }
const CustomCommandEditor: React.FC = ({ widget, onComplete, onCancel, action }) => {
const getMode = (): EditorMode['type'] => {
switch (action) {
case 'edit-command': return 'command';
case 'edit-width': return 'width';
case 'edit-timeout': return 'timeout';
default: return 'command';
}
};
const mode = getMode();
const [commandInput, setCommandInput] = useState(widget.commandPath ?? '');
const [commandCursorPos, setCommandCursorPos] = useState(commandInput.length);
const [widthInput, setWidthInput] = useState(widget.maxWidth?.toString() ?? '');
const [timeoutInput, setTimeoutInput] = useState(widget.timeout?.toString() ?? '1000');
useInput((input, key) => {
if (mode === 'command') {
if (key.return) {
onComplete({ ...widget, commandPath: commandInput });
} else if (key.escape) {
onCancel();
} else if (key.leftArrow) {
setCommandCursorPos(Math.max(0, commandCursorPos - 1));
} else if (key.rightArrow) {
setCommandCursorPos(Math.min(commandInput.length, commandCursorPos + 1));
} else if (key.backspace) {
if (commandCursorPos > 0) {
setCommandInput(commandInput.slice(0, commandCursorPos - 1) + commandInput.slice(commandCursorPos));
setCommandCursorPos(commandCursorPos - 1);
}
} else if (key.delete) {
if (commandCursorPos < commandInput.length) {
setCommandInput(commandInput.slice(0, commandCursorPos) + commandInput.slice(commandCursorPos + 1));
}
} else if (input) {
setCommandInput(commandInput.slice(0, commandCursorPos) + input + commandInput.slice(commandCursorPos));
setCommandCursorPos(commandCursorPos + input.length);
}
} else if (mode === 'width') {
if (key.return) {
const width = parseInt(widthInput, 10);
if (!isNaN(width) && width > 0) {
onComplete({ ...widget, maxWidth: width });
} else {
const { maxWidth, ...rest } = widget;
void maxWidth; // Intentionally unused
onComplete(rest);
}
} else if (key.escape) {
onCancel();
} else if (key.backspace) {
setWidthInput(widthInput.slice(0, -1));
} else if (input && /\d/.test(input)) {
setWidthInput(widthInput + input);
}
} else if (mode === 'timeout') {
if (key.return) {
const timeout = parseInt(timeoutInput, 10);
if (!isNaN(timeout) && timeout > 0) {
onComplete({ ...widget, timeout });
} else {
const { timeout, ...rest } = widget;
void timeout; // Intentionally unused
onComplete(rest);
}
} else if (key.escape) {
onCancel();
} else if (key.backspace) {
setTimeoutInput(timeoutInput.slice(0, -1));
} else if (input && /\d/.test(input)) {
setTimeoutInput(timeoutInput + input);
}
}
});
if (mode === 'command') {
return (
Enter command path:
{' '}
{commandInput.slice(0, commandCursorPos)}
{commandInput[commandCursorPos] ?? ' '}
{commandInput.slice(commandCursorPos + 1)}
←→ move cursor, Enter save, ESC cancel
);
} else if (mode === 'width') {
return (
Enter max width (blank for no limit):
{widthInput}
{' '}
Press Enter to save, ESC to cancel
);
} else if (mode === 'timeout') {
return (
Enter timeout in milliseconds (default 1000):
{timeoutInput}
{' '}
Press Enter to save, ESC to cancel
);
}
return Unknown editor mode;
};