mirror of
https://github.com/tiennm99/ccstatusline.git
synced 2026-10-13 00:14:20 +00:00
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:
1 parent
9e97e003d3
commit
d5b2da2a47
9 files changed
+225
-48
No files matched your search
@@ -1,5 +1,6 @@
|
||||
# Source files
|
||||
src/
|
||||
scripts/
|
||||
*.ts
|
||||
*.tsx
|
||||
|
||||
|
||||
Vendored
+1
@@ -19,6 +19,7 @@
|
||||
},
|
||||
"cSpell.words": [
|
||||
"ccstatusline",
|
||||
"Powerline",
|
||||
"statusline",
|
||||
"Worktree",
|
||||
"worktrees"
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
settings.json
|
||||
payload.json
|
||||
@@ -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
@@ -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' && (
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user