From 075159f5dc2c6e8959f1d1ec3ac15b1faa001653 Mon Sep 17 00:00:00 2001 From: Matthew Breedlove Date: Fri, 8 Aug 2025 23:25:16 -0400 Subject: [PATCH] Add custom text widgets --- src/ccstatusline.ts | 6 ++++ src/config.ts | 3 +- src/tui.tsx | 68 ++++++++++++++++++++++++++++++++++++++------- 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/src/ccstatusline.ts b/src/ccstatusline.ts index 23338bd..c18036c 100644 --- a/src/ccstatusline.ts +++ b/src/ccstatusline.ts @@ -470,6 +470,12 @@ function renderSingleLine(items: StatusItem[], settings: any, data: StatusJSON, elements.push({ content: 'FLEX', type: 'flex-separator' }); hasFlexSeparator = true; break; + + case 'custom-text': + const customTextColor = (chalk as any)[item.color || 'white'] || chalk.white; + const customText = item.customText || ''; + elements.push({ content: customTextColor(customText), type: 'custom-text' }); + break; } } diff --git a/src/config.ts b/src/config.ts index d45f4a0..49e1532 100644 --- a/src/config.ts +++ b/src/config.ts @@ -9,7 +9,7 @@ const writeFile = fs.promises?.writeFile || promisify(fs.writeFile); const mkdir = fs.promises?.mkdir || promisify(fs.mkdir); export type StatusItemType = 'model' | 'git-branch' | 'git-changes' | 'separator' | 'flex-separator' | - 'tokens-input' | 'tokens-output' | 'tokens-cached' | 'tokens-total' | 'context-length' | 'context-percentage' | 'terminal-width' | 'session-clock' | 'version'; + 'tokens-input' | 'tokens-output' | 'tokens-cached' | 'tokens-total' | 'context-length' | 'context-percentage' | 'terminal-width' | 'session-clock' | 'version' | 'custom-text'; export interface StatusItem { id: string; @@ -17,6 +17,7 @@ export interface StatusItem { color?: string; character?: string; // For separator and flex-separator types rawValue?: boolean; // Show value without label prefix + customText?: string; // For custom-text type } export type FlexMode = 'full' | 'full-minus-40' | 'full-until-compact'; diff --git a/src/tui.tsx b/src/tui.tsx index 81b39f4..3ad8a07 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -158,6 +158,11 @@ const renderSingleLine = (items: StatusItem[], terminalWidth: number, widthDetec elements.push('FLEX'); hasFlexSeparator = true; break; + case 'custom-text': + const customTextColor = (chalk as any)[item.color || 'white'] || chalk.white; + const customText = item.customText || ''; + elements.push(customTextColor(customText)); + break; } }); @@ -392,10 +397,33 @@ interface ItemsEditorProps { const ItemsEditor: React.FC = ({ items, onUpdate, onBack, lineNumber }) => { const [selectedIndex, setSelectedIndex] = useState(0); const [moveMode, setMoveMode] = useState(false); + const [editingText, setEditingText] = useState(false); + const [textInput, setTextInput] = useState(''); const separatorChars = ['|', '-', ',', ' ']; useInput((input, key) => { - if (moveMode) { + if (editingText) { + // In text editing mode + if (key.return) { + // Save the custom text + const currentItem = items[selectedIndex]; + if (currentItem) { + const newItems = [...items]; + newItems[selectedIndex] = { ...currentItem, customText: textInput }; + onUpdate(newItems); + } + setEditingText(false); + setTextInput(''); + } else if (key.escape) { + // Cancel editing + setEditingText(false); + setTextInput(''); + } else if (key.backspace || key.delete) { + setTextInput(textInput.slice(0, -1)); + } else if (input && input.length === 1) { + setTextInput(textInput + input); + } + } else if (moveMode) { // In move mode, use up/down to move the selected item if (key.upArrow && selectedIndex > 0) { const newItems = [...items]; @@ -429,7 +457,7 @@ const ItemsEditor: React.FC = ({ items, onUpdate, onBack, line // Toggle item type backwards const types: StatusItemType[] = ['model', 'git-branch', 'git-changes', 'separator', 'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage', - 'session-clock', 'terminal-width', 'version', 'flex-separator']; + 'session-clock', 'terminal-width', 'version', 'flex-separator', 'custom-text']; const currentItem = items[selectedIndex]; if (currentItem) { const currentType = currentItem.type; @@ -446,7 +474,7 @@ const ItemsEditor: React.FC = ({ items, onUpdate, onBack, line // Toggle item type forwards const types: StatusItemType[] = ['model', 'git-branch', 'git-changes', 'separator', 'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage', - 'session-clock', 'terminal-width', 'version', 'flex-separator']; + 'session-clock', 'terminal-width', 'version', 'flex-separator', 'custom-text']; const currentItem = items[selectedIndex]; if (currentItem) { const currentType = currentItem.type; @@ -472,15 +500,15 @@ const ItemsEditor: React.FC = ({ items, onUpdate, onBack, line onUpdate(newItems); setSelectedIndex(newItems.length - 1); // Move selection to new item } else if (input === 'i') { - // Insert item after selected + // Insert item before selected const newItem: StatusItem = { id: Date.now().toString(), type: 'separator', }; const newItems = [...items]; - newItems.splice(selectedIndex + 1, 0, newItem); + newItems.splice(selectedIndex, 0, newItem); onUpdate(newItems); - setSelectedIndex(selectedIndex + 1); // Selection already moves to new item + // Keep selection on the new item (which is now at selectedIndex) } else if (input === 'd' && items.length > 0) { // Delete selected item const newItems = items.filter((_, i) => i !== selectedIndex); @@ -506,11 +534,18 @@ const ItemsEditor: React.FC = ({ items, onUpdate, onBack, line } else if (input === 'r' && items.length > 0) { // Toggle raw value for non-separator items const currentItem = items[selectedIndex]; - if (currentItem && currentItem.type !== 'separator' && currentItem.type !== 'flex-separator') { + if (currentItem && currentItem.type !== 'separator' && currentItem.type !== 'flex-separator' && currentItem.type !== 'custom-text') { const newItems = [...items]; newItems[selectedIndex] = { ...currentItem, rawValue: !currentItem.rawValue }; onUpdate(newItems); } + } else if (input === 'e' && items.length > 0) { + // Edit custom text + const currentItem = items[selectedIndex]; + if (currentItem && currentItem.type === 'custom-text') { + setTextInput(currentItem.customText || ''); + setEditingText(true); + } } else if (key.escape) { onBack(); } @@ -550,6 +585,9 @@ const ItemsEditor: React.FC = ({ items, onUpdate, onBack, line return chalk.dim('Terminal Width'); case 'version': return chalk.green('Version'); + case 'custom-text': + const text = item.customText || 'Empty'; + return chalk.white(`Custom Text (${text})`); } }; @@ -560,12 +598,16 @@ const ItemsEditor: React.FC = ({ items, onUpdate, onBack, line const currentItem = items[selectedIndex]; const isSeparator = currentItem?.type === 'separator'; const isFlexSeparator = currentItem?.type === 'flex-separator'; - const canToggleRaw = currentItem && !isSeparator && !isFlexSeparator; + const isCustomText = currentItem?.type === 'custom-text'; + const canToggleRaw = currentItem && !isSeparator && !isFlexSeparator && !isCustomText; let helpText = '↑↓ select, ←→ change type'; if (isSeparator) { helpText += ', Space edit separator'; } + if (isCustomText) { + helpText += ', (e)dit text'; + } helpText += ', Enter to move, (a)dd, (i)nsert, (d)elete, (c)lear line'; if (canToggleRaw) { helpText += ', (r)aw value'; @@ -575,7 +617,12 @@ const ItemsEditor: React.FC = ({ items, onUpdate, onBack, line return ( Edit Line {lineNumber} {moveMode && [MOVE MODE]} - {moveMode ? ( + {editingText ? ( + + Enter custom text: {textInput} + Press Enter to save, ESC to cancel + + ) : moveMode ? ( ↑↓ to move item, ESC or Enter to exit move mode ) : ( {helpText} @@ -613,7 +660,7 @@ interface ColorMenuProps { const ColorMenu: React.FC = ({ items, onUpdate, onBack }) => { const colorableItems = items.filter(item => - ['model', 'git-branch', 'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage', 'session-clock', 'version'].includes(item.type) + ['model', 'git-branch', 'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage', 'session-clock', 'version', 'custom-text'].includes(item.type) ); const [selectedIndex, setSelectedIndex] = useState(0); @@ -653,6 +700,7 @@ const ColorMenu: React.FC = ({ items, onUpdate, onBack }) => { case 'context-percentage': return 'Context Percentage'; case 'session-clock': return 'Session Clock'; case 'version': return 'Version'; + case 'custom-text': return `Custom Text (${item.customText || 'Empty'})`; default: return item.type; } };