diff --git a/src/claude-settings.ts b/src/claude-settings.ts index 96f4dc4..dce119f 100644 --- a/src/claude-settings.ts +++ b/src/claude-settings.ts @@ -43,7 +43,7 @@ export async function saveClaudeSettings(settings: ClaudeSettings): Promise { const settings = await loadClaudeSettings(); - return settings.statusLine?.command === 'npx ccstatusline'; + return settings.statusLine?.command === 'npx -y ccstatusline'; } export async function installStatusLine(): Promise { @@ -52,7 +52,7 @@ export async function installStatusLine(): Promise { // Update settings with our status line (confirmation already handled in TUI) settings.statusLine = { type: 'command', - command: 'npx ccstatusline', + command: 'npx -y ccstatusline', padding: 1 }; diff --git a/src/tui.tsx b/src/tui.tsx index 5a6995e..e611e64 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -14,7 +14,7 @@ const StatusLinePreview: React.FC = ({ items, terminalWi const width = 80; // Status line is always 80 chars max const elements: string[] = []; let hasFlexSeparator = false; - + items.forEach(item => { switch (item.type) { case 'model': @@ -58,13 +58,13 @@ const StatusLinePreview: React.FC = ({ items, terminalWi break; } }); - + // Build the status line with flex separator support let statusLine = ''; if (hasFlexSeparator) { const parts: string[][] = [[]]; let currentPart = 0; - + for (let i = 0; i < items.length; i++) { const item = items[i]; if (item && item.type === 'flex-separator') { @@ -77,20 +77,20 @@ const StatusLinePreview: React.FC = ({ items, terminalWi } } } - + // Calculate total length of all non-flex content const partLengths = parts.map(part => { const joined = part.join(''); return joined.replace(/\x1b\[[0-9;]*m/g, '').length; }); const totalContentLength = partLengths.reduce((sum, len) => sum + len, 0); - + // Calculate space to distribute among flex separators const flexCount = parts.length - 1; // Number of flex separators const totalSpace = Math.max(0, width - totalContentLength); const spacePerFlex = flexCount > 0 ? Math.floor(totalSpace / flexCount) : 0; const extraSpace = flexCount > 0 ? totalSpace % flexCount : 0; - + // Build the status line with distributed spacing statusLine = ''; for (let i = 0; i < parts.length; i++) { @@ -107,13 +107,13 @@ const StatusLinePreview: React.FC = ({ items, terminalWi } else { statusLine = elements.filter(e => e !== 'FLEX').join(''); } - + // Build the Claude Code input box - account for ink's padding const boxWidth = Math.min(terminalWidth - 4, process.stdout.columns - 4 || 76); const topLine = chalk.dim('╭' + '─'.repeat(Math.max(0, boxWidth - 2)) + '╮'); const middleLine = chalk.dim('│') + ' > ' + ' '.repeat(Math.max(0, boxWidth - 5)) + chalk.dim('│'); const bottomLine = chalk.dim('╰' + '─'.repeat(Math.max(0, boxWidth - 2)) + '╯'); - + return ( {topLine} @@ -135,14 +135,14 @@ const ConfirmDialog: React.FC = ({ message, onConfirm, onCan { label: '✅ Yes', value: 'yes' }, { label: '❌ No', value: 'no' }, ]; - + return ( {message} - item.value === 'yes' ? onConfirm() : onCancel()} + item.value === 'yes' ? onConfirm() : onCancel()} /> @@ -161,7 +161,7 @@ const MainMenu: React.FC = ({ onSelect, isClaudeInstalled, hasCha { label: '🎨 Configure Colors', value: 'colors' }, { label: isClaudeInstalled ? '🗑️ Uninstall from Claude Code' : '📦 Install to Claude Code', value: 'install' }, ]; - + if (hasChanges) { items.push( { label: '💾 Save & Exit', value: 'save' }, @@ -170,7 +170,7 @@ const MainMenu: React.FC = ({ onSelect, isClaudeInstalled, hasCha } else { items.push({ label: '🚪 Exit', value: 'exit' }); } - + return ( Main Menu @@ -190,7 +190,7 @@ interface ItemsEditorProps { const ItemsEditor: React.FC = ({ items, onUpdate, onBack }) => { const [selectedIndex, setSelectedIndex] = useState(0); const [moveMode, setMoveMode] = useState(false); - + useInput((input, key) => { if (moveMode) { // In move mode, use up/down to move the selected item @@ -224,7 +224,7 @@ const ItemsEditor: React.FC = ({ items, onUpdate, onBack }) => setSelectedIndex(Math.min(items.length - 1, selectedIndex + 1)); } else if (key.leftArrow && items.length > 0) { // Toggle item type backwards - const types: StatusItemType[] = ['model', 'git-branch', 'separator', 'flex-separator', + const types: StatusItemType[] = ['model', 'git-branch', 'separator', 'flex-separator', 'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage']; const currentItem = items[selectedIndex]; if (currentItem) { @@ -286,7 +286,7 @@ const ItemsEditor: React.FC = ({ items, onUpdate, onBack }) => } } }); - + const getItemDisplay = (item: StatusItem) => { switch (item.type) { case 'model': @@ -311,7 +311,7 @@ const ItemsEditor: React.FC = ({ items, onUpdate, onBack }) => return chalk.cyan('Context %'); } }; - + return ( Edit Status Line Items {moveMode && [MOVE MODE]} @@ -345,18 +345,18 @@ interface ColorMenuProps { } const ColorMenu: React.FC = ({ items, onUpdate, onBack }) => { - const colorableItems = items.filter(item => + const colorableItems = items.filter(item => ['model', 'git-branch', 'tokens-input', 'tokens-output', 'tokens-cached', 'tokens-total', 'context-length', 'context-percentage'].includes(item.type) ); const [selectedIndex, setSelectedIndex] = useState(0); - + // Handle ESC key useInput((input, key) => { if (key.escape) { onBack(); } }); - + if (colorableItems.length === 0) { return ( @@ -372,7 +372,7 @@ const ColorMenu: React.FC = ({ items, onUpdate, onBack }) => { ); } - + const getItemLabel = (item: StatusItem) => { switch (item.type) { case 'model': return 'Model'; @@ -386,7 +386,7 @@ const ColorMenu: React.FC = ({ items, onUpdate, onBack }) => { default: return item.type; } }; - + // Create menu items with colored labels const menuItems = colorableItems.map((item, index) => { const color = item.color || 'white'; @@ -397,7 +397,7 @@ const ColorMenu: React.FC = ({ items, onUpdate, onBack }) => { }; }); menuItems.push({ label: '← Back', value: 'back' }); - + const handleSelect = (selected: { value: string }) => { if (selected.value === 'back') { onBack(); @@ -414,7 +414,7 @@ const ColorMenu: React.FC = ({ items, onUpdate, onBack }) => { onUpdate(newItems); } }; - + const handleHighlight = (item: { value: string }) => { if (item.value !== 'back') { const itemIndex = colorableItems.findIndex(i => i.id === item.value); @@ -423,19 +423,19 @@ const ColorMenu: React.FC = ({ items, onUpdate, onBack }) => { } } }; - + // Color list for cycling const colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'gray', 'redBright', 'greenBright', 'yellowBright', 'blueBright', 'magentaBright', 'cyanBright', 'whiteBright']; - + // Get current color for selected item (if a valid colorable item is selected) const selectedItem = selectedIndex < colorableItems.length ? colorableItems[selectedIndex] : null; const currentColor = selectedItem ? (selectedItem.color || 'white') : 'white'; const colorIndex = colors.indexOf(currentColor); const colorNumber = colorIndex === -1 ? 8 : colorIndex + 1; // Default to white (8) if not found const colorDisplay = (chalk as any)[currentColor] ? (chalk as any)[currentColor](currentColor) : chalk.white(currentColor); - + return ( Configure Colors @@ -446,8 +446,8 @@ const ColorMenu: React.FC = ({ items, onUpdate, onBack }) => { )} - { const [confirmDialog, setConfirmDialog] = useState<{ message: string; action: () => Promise } | null>(null); const [isClaudeInstalled, setIsClaudeInstalled] = useState(false); const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80); - + useEffect(() => { loadSettings().then(loadedSettings => { setSettings(loadedSettings); setOriginalSettings(JSON.parse(JSON.stringify(loadedSettings))); // Deep copy }); isInstalled().then(setIsClaudeInstalled); - + const handleResize = () => { setTerminalWidth(process.stdout.columns || 80); }; - + process.stdout.on('resize', handleResize); return () => { process.stdout.off('resize', handleResize); }; }, []); - + // Check for changes whenever settings update useEffect(() => { if (settings && originalSettings) { @@ -491,17 +491,17 @@ const App: React.FC = () => { setHasChanges(hasAnyChanges); } }, [settings, originalSettings]); - + useInput((input, key) => { if (key.ctrl && input === 'c') { exit(); } }); - + if (!settings) { return Loading settings...; } - + const handleInstallUninstall = async () => { if (isClaudeInstalled) { // Uninstall @@ -519,15 +519,15 @@ const App: React.FC = () => { // Always ask for consent before modifying Claude settings const existing = await getExistingStatusLine(); let message: string; - - if (existing && existing !== 'npx ccstatusline') { - message = `This will modify ~/.claude/settings.json\n\nA status line is already configured: "${existing}"\nReplace it with ccstatusline?`; - } else if (existing === 'npx ccstatusline') { + + if (existing && existing !== 'npx -y ccstatusline') { + message = `This will modify ~/.claude/settings.json\n\nA status line is already configured: "${existing}"\nReplace it with npx -y ccstatusline?`; + } else if (existing === 'npx -y ccstatusline') { message = 'ccstatusline is already installed in ~/.claude/settings.json\nReinstall it?'; } else { message = 'This will modify ~/.claude/settings.json to add ccstatusline.\nContinue?'; } - + setConfirmDialog({ message, action: async () => { @@ -540,7 +540,7 @@ const App: React.FC = () => { setScreen('confirm'); } }; - + const handleMainMenuSelect = async (value: string) => { switch (value) { case 'items': @@ -563,36 +563,36 @@ const App: React.FC = () => { break; } }; - + const updateItems = (items: StatusItem[]) => { setSettings({ ...settings, items }); }; - + return ( 🎨 CCStatusline Configuration - + Preview: - + {screen === 'main' && } {screen === 'items' && ( - setScreen('main')} + onBack={() => setScreen('main')} /> )} {screen === 'colors' && ( - setScreen('main')} + onBack={() => setScreen('main')} /> )} {screen === 'confirm' && confirmDialog && (