Support emoji input for custom text

This commit is contained in:
Matthew Breedlove committed 2025-08-31 02:47:02 -04:00
1 parent dade396de1
commit 17b2576cca
1 file changed
+93 -15
+93 -15
View File
@@ -49,43 +49,121 @@ const CustomTextEditor: React.FC<WidgetEditorProps> = ({ widget, onComplete, onC
const [text, setText] = useState(widget.customText ?? '');
const [cursorPos, setCursorPos] = useState(text.length);
// Helper to get grapheme segments if Intl.Segmenter is available
const getGraphemes = (str: string): string[] => {
if ('Segmenter' in Intl) {
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
return Array.from(segmenter.segment(str), seg => seg.segment);
}
// Fallback to simple character array (won't handle complex emojis perfectly)
return Array.from(str);
};
// Convert between grapheme index and string index
const graphemeToStringIndex = (str: string, graphemeIndex: number): number => {
const graphemes = getGraphemes(str);
let stringIndex = 0;
for (let i = 0; i < Math.min(graphemeIndex, graphemes.length); i++) {
const grapheme = graphemes[i];
if (grapheme) {
stringIndex += grapheme.length;
}
}
return stringIndex;
};
const stringToGraphemeIndex = (str: string, stringIndex: number): number => {
const graphemes = getGraphemes(str);
let currentStringIndex = 0;
for (let i = 0; i < graphemes.length; i++) {
if (currentStringIndex >= stringIndex)
return i;
const grapheme = graphemes[i];
if (grapheme) {
currentStringIndex += grapheme.length;
}
}
return graphemes.length;
};
useInput((input, key) => {
if (key.return) {
onComplete({ ...widget, customText: text });
} else if (key.escape) {
onCancel();
} else if (key.leftArrow) {
setCursorPos(Math.max(0, cursorPos - 1));
const currentGraphemeIndex = stringToGraphemeIndex(text, cursorPos);
if (currentGraphemeIndex > 0) {
const newStringIndex = graphemeToStringIndex(text, currentGraphemeIndex - 1);
setCursorPos(newStringIndex);
}
} else if (key.rightArrow) {
setCursorPos(Math.min(text.length, cursorPos + 1));
const currentGraphemeIndex = stringToGraphemeIndex(text, cursorPos);
const graphemeCount = getGraphemes(text).length;
if (currentGraphemeIndex < graphemeCount) {
const newStringIndex = graphemeToStringIndex(text, currentGraphemeIndex + 1);
setCursorPos(newStringIndex);
}
} else if (key.ctrl && input === 'ArrowLeft') {
setCursorPos(0);
} else if (key.ctrl && input === 'ArrowRight') {
setCursorPos(text.length);
} else if (key.backspace) {
if (cursorPos > 0) {
setText(text.slice(0, cursorPos - 1) + text.slice(cursorPos));
setCursorPos(cursorPos - 1);
const currentGraphemeIndex = stringToGraphemeIndex(text, cursorPos);
if (currentGraphemeIndex > 0) {
const deleteFromIndex = graphemeToStringIndex(text, currentGraphemeIndex - 1);
const deleteToIndex = graphemeToStringIndex(text, currentGraphemeIndex);
setText(text.slice(0, deleteFromIndex) + text.slice(deleteToIndex));
setCursorPos(deleteFromIndex);
}
}
} else if (key.delete) {
if (cursorPos < text.length) {
setText(text.slice(0, cursorPos) + text.slice(cursorPos + 1));
const currentGraphemeIndex = stringToGraphemeIndex(text, cursorPos);
const graphemeCount = getGraphemes(text).length;
if (currentGraphemeIndex < graphemeCount) {
const deleteFromIndex = graphemeToStringIndex(text, currentGraphemeIndex);
const deleteToIndex = graphemeToStringIndex(text, currentGraphemeIndex + 1);
setText(text.slice(0, deleteFromIndex) + text.slice(deleteToIndex));
}
}
} else if (input && input.length === 1) {
setText(text.slice(0, cursorPos) + input + text.slice(cursorPos));
setCursorPos(cursorPos + 1);
} else if (input && !key.ctrl && !key.meta) {
// Insert the input at cursor position
const newText = text.slice(0, cursorPos) + input + text.slice(cursorPos);
setText(newText);
// Move cursor by the actual string length of the input
// This handles multi-byte characters including emojis with modifiers
setCursorPos(cursorPos + input.length);
}
});
// Get the grapheme at cursor position for display
const graphemes = getGraphemes(text);
const currentGraphemeIndex = stringToGraphemeIndex(text, cursorPos);
// Build display with ANSI codes for cursor highlighting
let display = 'Enter custom text: ';
for (let i = 0; i < graphemes.length; i++) {
const grapheme = graphemes[i];
if (grapheme) {
if (i === currentGraphemeIndex) {
// Use inverse video for cursor position
display += `\x1b[7m${grapheme}\x1b[0m`;
} else {
display += grapheme;
}
}
}
if (currentGraphemeIndex >= graphemes.length) {
// Cursor at end
display += '\x1b[7m \x1b[0m';
}
return (
<Box flexDirection='column'>
<Text>
Enter custom text:
{' '}
{text.slice(0, cursorPos)}
<Text backgroundColor='gray' color='black'>{text[cursorPos] ?? ' '}</Text>
{text.slice(cursorPos + 1)}
</Text>
<Text>{display}</Text>
<Text dimColor>←→ move cursor, Ctrl+←→ jump to start/end, Enter save, ESC cancel</Text>
</Box>
);