chore(settings): support more than 3 lines (#53)

* chore(settings): support more than 3 lines
* chore: add confirm dialog
* chore: update example script
* chore: rm config override
* Fix spacing issues, disallow deletion of last line, fix issue where deleting lines 2 and 3 would keep them in the list, cleanup onDelete and onAppend method locations
* Initialize default settings with two empty lines

---------

Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
This commit is contained in:
Jack AllenandMatthew Breedlove authored and GitHub committed 2025-08-28 09:30:26 -04:00
1 parent 9e97e003d3
commit d5b2da2a47
9 files changed
+225 -48

No files matched your search

+1
View File
@@ -1,5 +1,6 @@
# Source files
src/
scripts/
*.ts
*.tsx
+1
View File
@@ -19,6 +19,7 @@
},
"cSpell.words": [
"ccstatusline",
"Powerline",
"statusline",
"Worktree",
"worktrees"
+6
View File
@@ -7,6 +7,7 @@
"@eslint/js": "^9.33.0",
"@stylistic/eslint-plugin": "^5.2.3",
"@types/bun": "latest",
"@types/pluralize": "^0.0.33",
"@types/react": "^19.1.10",
"chalk": "^5.5.0",
"eslint": "^9.33.0",
@@ -18,6 +19,7 @@
"ink": "^6.2.0",
"ink-gradient": "^3.0.0",
"ink-select-input": "^6.2.0",
"pluralize": "^8.0.0",
"react": "^19.1.1",
"react-devtools-core": "^6.1.5",
"strip-ansi": "^7.1.0",
@@ -194,6 +196,8 @@
"@types/node": ["@types/node@24.2.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw=="],
"@types/pluralize": ["@types/pluralize@0.0.33", "", {}, "sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg=="],
"@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="],
"@types/tinycolor2": ["@types/tinycolor2@1.4.6", "", {}, "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw=="],
@@ -678,6 +682,8 @@
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
+4 -2
View File
@@ -12,9 +12,9 @@
],
"scripts": {
"start": "bun run src/ccstatusline.ts",
"statusline": "bun run src/ccstatusline.ts",
"build": "rm -rf dist/* && bun build src/ccstatusline.ts --target=node --outfile=dist/ccstatusline.js --target-version=14",
"postbuild": "bun run scripts/replace-version.ts",
"example": "cat scripts/payload.example.json | bun start",
"prepublishOnly": "bun run build",
"lint": "bun tsc --noEmit; eslint . --config eslint.config.js --max-warnings=999999 --fix",
"test": "bun vitest"
@@ -41,7 +41,9 @@
"typescript": "^5.9.2",
"typescript-eslint": "^8.39.1",
"vitest": "^3.2.4",
"zod": "^4.0.17"
"zod": "^4.0.17",
"pluralize": "^8.0.0",
"@types/pluralize": "^0.0.33"
},
"keywords": [
"claude",
+2
View File
@@ -0,0 +1,2 @@
settings.json
payload.json
+25
View File
@@ -0,0 +1,25 @@
{
"hook_event_name": "Status",
"session_id": "abc123...",
"transcript_path": "/path/to/transcript.json",
"cwd": "/current/working/directory",
"model": {
"id": "claude-opus-4-1",
"display_name": "Opus"
},
"workspace": {
"current_dir": "/current/working/directory",
"project_dir": "/original/project/directory"
},
"version": "1.0.80",
"output_style": {
"name": "default"
},
"cost": {
"total_cost_usd": 0.01234,
"total_duration_ms": 45000,
"total_api_duration_ms": 2300,
"total_lines_added": 156,
"total_lines_removed": 23
}
}
+8 -4
View File
@@ -72,10 +72,6 @@ export const App: React.FC = () => {
void loadSettings().then((loadedSettings) => {
// Set global chalk level based on settings (default to 256 colors for compatibility)
chalk.level = loadedSettings.colorLevel;
// Ensure lines array has 3 slots
while (loadedSettings.lines.length < 3) {
loadedSettings.lines.push([]);
}
setSettings(loadedSettings);
setOriginalSettings(JSON.parse(JSON.stringify(loadedSettings)) as Settings); // Deep copy
});
@@ -195,6 +191,10 @@ export const App: React.FC = () => {
setSettings({ ...settings, lines: newLines });
};
const updateLines = (newLines: WidgetItem[][]) => {
setSettings({ ...settings, lines: newLines });
};
const handleLineSelect = (lineIndex: number) => {
setSelectedLine(lineIndex);
setScreen('items');
@@ -258,6 +258,7 @@ export const App: React.FC = () => {
setMenuSelections({ ...menuSelections, lines: line });
handleLineSelect(line);
}}
onLinesUpdate={updateLines}
onBack={() => {
// Save that we came from 'lines' menu (index 0)
// Clear the line selection so it resets next time we enter
@@ -266,6 +267,7 @@ export const App: React.FC = () => {
}}
initialSelection={menuSelections.lines}
title='Select Line to Edit Items'
allowEditing={true}
/>
)}
{screen === 'items' && (
@@ -284,6 +286,7 @@ export const App: React.FC = () => {
{screen === 'colorLines' && (
<LineSelector
lines={settings.lines}
onLinesUpdate={updateLines}
onSelect={(line) => {
setMenuSelections({ ...menuSelections, lines: line });
setSelectedLine(line);
@@ -298,6 +301,7 @@ export const App: React.FC = () => {
title='Select Line to Edit Colors'
blockIfPowerlineActive={true}
settings={settings}
allowEditing={false}
/>
)}
{screen === 'colors' && (
+174 -38
View File
@@ -3,45 +3,114 @@ import {
Text,
useInput
} from 'ink';
import React, { useState } from 'react';
import pluralize from 'pluralize';
import React, {
useEffect,
useMemo,
useState
} from 'react';
import type { Settings } from '../../types/Settings';
import type { WidgetItem } from '../../types/Widget';
import { ConfirmDialog } from './ConfirmDialog';
interface LineSelectorProps {
lines: WidgetItem[][];
onSelect: (line: number) => void;
onBack: () => void;
onLinesUpdate: (lines: WidgetItem[][]) => void;
initialSelection?: number;
title?: string;
blockIfPowerlineActive?: boolean;
settings?: Settings;
allowEditing?: boolean;
}
const LineSelector: React.FC<LineSelectorProps> = ({ lines, onSelect, onBack, initialSelection = 0, title, blockIfPowerlineActive = false, settings }) => {
const LineSelector: React.FC<LineSelectorProps> = ({
lines,
onSelect,
onBack,
onLinesUpdate,
initialSelection = 0,
title,
blockIfPowerlineActive = false,
settings,
allowEditing = false
}) => {
const [selectedIndex, setSelectedIndex] = useState(initialSelection);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [localLines, setLocalLines] = useState(lines);
useEffect(() => {
setLocalLines(lines);
}, [lines]);
const selectedLine = useMemo(
() => localLines[selectedIndex],
[localLines, selectedIndex]
);
const appendLine = () => {
const newLines = [...localLines, []];
setLocalLines(newLines);
onLinesUpdate(newLines);
setSelectedIndex(newLines.length - 1);
};
const deleteLine = (lineIndex: number) => {
// Don't allow deleting the last remaining line
if (localLines.length <= 1) {
return;
}
const newLines = [...localLines];
newLines.splice(lineIndex, 1);
setLocalLines(newLines);
onLinesUpdate(newLines);
};
// Check if powerline theme is managing colors
const powerlineEnabled = settings ? settings.powerline.enabled : false;
const powerlineTheme = settings ? settings.powerline.theme : undefined;
const isThemeManaged = blockIfPowerlineActive && powerlineEnabled && powerlineTheme && powerlineTheme !== 'custom';
const isThemeManaged
= blockIfPowerlineActive
&& powerlineEnabled
&& powerlineTheme
&& powerlineTheme !== 'custom';
// Handle keyboard input
useInput((input, key) => {
if (showDeleteDialog) {
return;
}
// If theme-managed and blocking is enabled, any key goes back
if (isThemeManaged) {
onBack();
return;
}
switch (input) {
case 'a':
if (allowEditing) {
appendLine();
}
return;
case 'd':
if (allowEditing && localLines.length > 1) {
setShowDeleteDialog(true);
}
return;
}
if (key.escape) {
onBack();
} else if (key.upArrow) {
setSelectedIndex(Math.max(0, selectedIndex - 1));
} else if (key.downArrow) {
setSelectedIndex(Math.min(3, selectedIndex + 1)); // 0-2 for lines, 3 for back
setSelectedIndex(Math.min(localLines.length, selectedIndex + 1));
} else if (key.return) {
if (selectedIndex === 3) {
if (selectedIndex === localLines.length) {
onBack();
} else {
onSelect(selectedIndex);
@@ -57,17 +126,23 @@ const LineSelector: React.FC<LineSelectorProps> = ({ lines, onSelect, onBack, in
<Box marginTop={1}>
<Text color='yellow'>
⚠ Colors are currently managed by the Powerline theme:
{' ' + powerlineTheme.charAt(0).toUpperCase() + powerlineTheme.slice(1)}
{' '
+ powerlineTheme.charAt(0).toUpperCase()
+ powerlineTheme.slice(1)}
</Text>
</Box>
<Box marginTop={1}>
<Text dimColor>To customize colors, either:</Text>
</Box>
<Box marginLeft={2}>
<Text dimColor>• Change to 'Custom' theme in Powerline Configuration → Themes</Text>
<Text dimColor>
• Change to 'Custom' theme in Powerline Configuration → Themes
</Text>
</Box>
<Box marginLeft={2}>
<Text dimColor>• Disable Powerline mode in Powerline Configuration</Text>
<Text dimColor>
• Disable Powerline mode in Powerline Configuration
</Text>
</Box>
<Box marginTop={2}>
<Text>Press any key to go back...</Text>
@@ -76,42 +151,103 @@ const LineSelector: React.FC<LineSelectorProps> = ({ lines, onSelect, onBack, in
);
}
return (
<Box flexDirection='column'>
<Text bold>{title ?? 'Select Line to Edit'}</Text>
<Text dimColor>Choose which status line to configure (up to 3 lines supported)</Text>
<Text dimColor>Press ESC to go back</Text>
<Box marginTop={1} flexDirection='column'>
<Box>
<Text color={selectedIndex === 0 ? 'green' : undefined}>
{selectedIndex === 0 ? '▶ ' : ' '}
☰ Line 1
{lines[0] && lines[0].length > 0 ? ` (${lines[0].length} widgets)` : ' (empty)'}
</Text>
</Box>
<Box>
<Text color={selectedIndex === 1 ? 'green' : undefined}>
{selectedIndex === 1 ? '▶ ' : ' '}
☰ Line 2
{lines[1] && lines[1].length > 0 ? ` (${lines[1].length} widgets)` : ' (empty)'}
</Text>
</Box>
<Box>
<Text color={selectedIndex === 2 ? 'green' : undefined}>
{selectedIndex === 2 ? '▶ ' : ' '}
☰ Line 3
{lines[2] && lines[2].length > 0 ? ` (${lines[2].length} widgets)` : ' (empty)'}
if (showDeleteDialog && selectedLine) {
const suffix
= selectedLine.length > 0
? pluralize('widget', selectedLine.length, true)
: 'empty';
return (
<Box flexDirection='column'>
<Box flexDirection='column' gap={1}>
<Text bold>
<Text>
<Text>
☰ Line
{' '}
{selectedIndex + 1}
</Text>
{' '}
<Text dimColor>
(
{suffix}
)
</Text>
</Text>
</Text>
<Text bold>Are you sure you want to delete line?</Text>
</Box>
<Box marginTop={1}>
<Text color={selectedIndex === 3 ? 'green' : undefined}>
{selectedIndex === 3 ? '▶ ' : ' '}
← Back
</Text>
<ConfirmDialog
inline={true}
onConfirm={() => {
deleteLine(selectedIndex);
setSelectedIndex(Math.max(0, selectedIndex - 1));
setShowDeleteDialog(false);
}}
onCancel={() => {
setShowDeleteDialog(false);
}}
/>
</Box>
</Box>
</Box>
);
}
return (
<>
<Box flexDirection='column'>
<Text bold>{title ?? 'Select Line to Edit'}</Text>
<Text dimColor>
Choose which status line to configure
</Text>
<Text dimColor>
{allowEditing ? (
localLines.length > 1
? '(a) to append new line, (d) to delete line, ESC to go back'
: '(a) to append new line, ESC to go back'
) : 'ESC to go back'}
</Text>
<Box marginTop={1} flexDirection='column'>
{localLines.map((line, index) => {
const isSelected = selectedIndex === index;
const suffix = line.length
? pluralize('widget', line.length, true)
: 'empty';
return (
<Box key={index}>
<Text color={isSelected ? 'green' : undefined}>
<Text>{isSelected ? '▶ ' : ' '}</Text>
<Text>
<Text>
☰ Line
{' '}
{index + 1}
</Text>
{' '}
<Text dimColor={!isSelected}>
(
{suffix}
)
</Text>
</Text>
</Text>
</Box>
);
})}
<Box marginTop={1}>
<Text color={selectedIndex === localLines.length ? 'green' : undefined}>
{selectedIndex === localLines.length ? '▶ ' : ' '}
← Back
</Text>
</Box>
</Box>
</Box>
</>
);
};
+4 -4
View File
@@ -27,7 +27,6 @@ export const SettingsSchema = z.object({
version: z.number().default(CURRENT_VERSION),
lines: z.array(z.array(WidgetItemSchema))
.min(1)
.max(3)
.default([
[
{ id: '1', type: 'model', color: 'cyan' },
@@ -37,9 +36,10 @@ export const SettingsSchema = z.object({
{ id: '5', type: 'git-branch', color: 'magenta' },
{ id: '6', type: 'separator' },
{ id: '7', type: 'git-changes', color: 'yellow' }
]
])
.transform(lines => lines.slice(0, 3)), // Ensure max 3 lines
],
[],
[]
]), // Ensure max 3 lines
flexMode: FlexModeSchema.default('full-minus-40'),
compactThreshold: z.number().min(1).max(99).default(60),
colorLevel: ColorLevelSchema.default(2),