mirror of
https://github.com/tiennm99/ccstatusline.git
synced 2026-09-03 02:19:38 +00:00
Add Current Working Dir component
This commit is contained in:
@@ -59,6 +59,15 @@ The project has dual runtime compatibility - works with both Bun and Node.js:
|
||||
- **claude-settings.ts**: Integration with Claude Code settings.json
|
||||
- **colors.ts**: Color definitions and ANSI code mapping
|
||||
|
||||
### Widgets (src/widgets/)
|
||||
Custom widgets implementing the StatusItemWidget interface:
|
||||
- Model, Version, OutputStyle - Claude Code metadata display
|
||||
- GitBranch, GitChanges - Git repository status
|
||||
- TokensInput, TokensOutput, TokensCached, TokensTotal - Token usage metrics
|
||||
- ContextLength, ContextPercentage, ContextPercentageUsable - Context window metrics
|
||||
- BlockTimer, SessionClock - Time tracking
|
||||
- CurrentWorkingDir, TerminalWidth - Environment info
|
||||
|
||||
## Key Implementation Details
|
||||
|
||||
- **Cross-platform stdin reading**: Detects Bun vs Node.js environment and uses appropriate stdin API
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"session_id":"05091387-3173-4179-afdd-fc3a3694b11f","transcript_path":"/Users/sirmalloc/.claude/projects/-Users-sirmalloc-Projects-Personal-ccstatusline/05091387-3173-4179-afdd-fc3a3694b11f.jsonl","cwd":"/Users/sirmalloc/Projects/Personal/ccstatusline","model":{"id":"claude-opus-4-1-20250805","display_name":"Opus 4.1"},"workspace":{"current_dir":"/Users/sirmalloc/Projects/Personal/ccstatusline","project_dir":"/Users/sirmalloc/Projects/Personal/ccstatusline"},"version":"1.0.86","output_style":{"name":"default"},"cost":{"total_cost_usd":5.474908800000001,"total_duration_ms":3466518,"total_api_duration_ms":219805,"total_lines_added":0,"total_lines_removed":0}}
|
||||
+1
-1
@@ -201,7 +201,7 @@ export const App: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box flexDirection='column' padding={1}>
|
||||
<Box flexDirection='column'>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>
|
||||
<Gradient name='retro'>
|
||||
|
||||
@@ -269,7 +269,7 @@ export const ItemsEditor: React.FC<ItemsEditorProps> = ({ widgets, onUpdate, onB
|
||||
const customKeybinds = widgetImpl.getCustomKeybinds();
|
||||
const matchedKeybind = customKeybinds.find(kb => kb.key === input);
|
||||
|
||||
if (matchedKeybind) {
|
||||
if (matchedKeybind && !key.ctrl) {
|
||||
// Check if widget handles the action directly
|
||||
if (widgetImpl.handleEditorAction) {
|
||||
// Let the widget handle the action directly
|
||||
|
||||
@@ -14,7 +14,14 @@ export const StatusJSONSchema = z.looseObject({
|
||||
project_dir: z.string().optional()
|
||||
}).optional(),
|
||||
version: z.string().optional(),
|
||||
output_style: z.object({ name: z.string().optional() }).optional()
|
||||
output_style: z.object({ name: z.string().optional() }).optional(),
|
||||
cost: z.object({
|
||||
total_cost_usd: z.number().optional(),
|
||||
total_duration_ms: z.number().optional(),
|
||||
total_api_duration_ms: z.number().optional(),
|
||||
total_lines_added: z.number().optional(),
|
||||
total_lines_removed: z.number().optional()
|
||||
}).optional()
|
||||
});
|
||||
|
||||
export type StatusJSON = z.infer<typeof StatusJSONSchema>;
|
||||
+10
-10
@@ -96,18 +96,18 @@ function renderPowerlineStatusLine(
|
||||
if (context.isPreview) {
|
||||
// In preview mode, account for box borders and padding (6 chars total)
|
||||
if (flexMode === 'full') {
|
||||
terminalWidth = detectedWidth - 8;
|
||||
terminalWidth = detectedWidth - 6;
|
||||
} else if (flexMode === 'full-minus-40') {
|
||||
terminalWidth = detectedWidth - 43;
|
||||
terminalWidth = detectedWidth - 40;
|
||||
} else if (flexMode === 'full-until-compact') {
|
||||
terminalWidth = detectedWidth - 8;
|
||||
terminalWidth = detectedWidth - 6;
|
||||
}
|
||||
} else {
|
||||
// In actual rendering mode
|
||||
if (flexMode === 'full') {
|
||||
terminalWidth = detectedWidth - 4;
|
||||
terminalWidth = detectedWidth - 6;
|
||||
} else if (flexMode === 'full-minus-40') {
|
||||
terminalWidth = detectedWidth - 41;
|
||||
terminalWidth = detectedWidth - 40;
|
||||
} else if (flexMode === 'full-until-compact') {
|
||||
const threshold = settings.compactThreshold;
|
||||
const contextPercentage = context.tokenMetrics
|
||||
@@ -116,7 +116,7 @@ function renderPowerlineStatusLine(
|
||||
if (contextPercentage >= threshold) {
|
||||
terminalWidth = detectedWidth - 40;
|
||||
} else {
|
||||
terminalWidth = detectedWidth - 4;
|
||||
terminalWidth = detectedWidth - 6;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -502,7 +502,7 @@ export function renderStatusLine(
|
||||
if (flexMode === 'full') {
|
||||
terminalWidth = detectedWidth - 6; // Subtract 6 for box borders and padding in preview
|
||||
} else if (flexMode === 'full-minus-40') {
|
||||
terminalWidth = detectedWidth - 43; // -40 for auto-compact + 3 for preview
|
||||
terminalWidth = detectedWidth - 40; // -40 for auto-compact + 3 for preview
|
||||
} else if (flexMode === 'full-until-compact') {
|
||||
// For preview, always show full width minus preview padding
|
||||
terminalWidth = detectedWidth - 6;
|
||||
@@ -511,10 +511,10 @@ export function renderStatusLine(
|
||||
// In actual rendering mode
|
||||
if (flexMode === 'full') {
|
||||
// Use full width minus 4 for terminal padding
|
||||
terminalWidth = detectedWidth - 4;
|
||||
terminalWidth = detectedWidth - 6;
|
||||
} else if (flexMode === 'full-minus-40') {
|
||||
// Always subtract 41 for auto-compact message
|
||||
terminalWidth = detectedWidth - 41;
|
||||
terminalWidth = detectedWidth - 40;
|
||||
} else if (flexMode === 'full-until-compact') {
|
||||
// Check context percentage to decide
|
||||
const threshold = settings.compactThreshold;
|
||||
@@ -526,7 +526,7 @@ export function renderStatusLine(
|
||||
terminalWidth = detectedWidth - 40;
|
||||
} else {
|
||||
// Context is low, use full width minus 4 for padding
|
||||
terminalWidth = detectedWidth - 4;
|
||||
terminalWidth = detectedWidth - 6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ const widgetRegistry = new Map<WidgetItemType, Widget>([
|
||||
['output-style', new widgets.OutputStyleWidget()],
|
||||
['git-branch', new widgets.GitBranchWidget()],
|
||||
['git-changes', new widgets.GitChangesWidget()],
|
||||
['current-working-dir', new widgets.CurrentWorkingDirWidget()],
|
||||
['tokens-input', new widgets.TokensInputWidget()],
|
||||
['tokens-output', new widgets.TokensOutputWidget()],
|
||||
['tokens-cached', new widgets.TokensCachedWidget()],
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
useInput
|
||||
} from 'ink';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import type { RenderContext } from '../types/RenderContext';
|
||||
import type { Settings } from '../types/Settings';
|
||||
import type {
|
||||
CustomKeybind,
|
||||
Widget,
|
||||
WidgetEditorDisplay,
|
||||
WidgetEditorProps,
|
||||
WidgetItem
|
||||
} from '../types/Widget';
|
||||
|
||||
export class CurrentWorkingDirWidget implements Widget {
|
||||
getDefaultColor(): string { return 'blue'; }
|
||||
getDescription(): string { return 'Shows the current working directory'; }
|
||||
getDisplayName(): string { return 'Current Working Dir'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
const segments = item.metadata?.segments ? parseInt(item.metadata.segments, 10) : undefined;
|
||||
const modifiers: string[] = [];
|
||||
|
||||
if (segments && segments > 0) {
|
||||
modifiers.push(`segments: ${segments}`);
|
||||
}
|
||||
|
||||
return {
|
||||
displayText: this.getDisplayName(),
|
||||
modifierText: modifiers.length > 0 ? `(${modifiers.join(', ')})` : undefined
|
||||
};
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
if (context.isPreview) {
|
||||
const segments = item.metadata?.segments ? parseInt(item.metadata.segments, 10) : undefined;
|
||||
if (segments && segments > 0) {
|
||||
if (segments === 1) {
|
||||
return 'cwd: .../project';
|
||||
} else {
|
||||
return 'cwd: .../example/project';
|
||||
}
|
||||
}
|
||||
return 'cwd: /Users/example/project';
|
||||
}
|
||||
|
||||
const cwd = context.data?.cwd;
|
||||
if (!cwd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const segments = item.metadata?.segments ? parseInt(item.metadata.segments, 10) : undefined;
|
||||
let displayPath = cwd;
|
||||
|
||||
if (segments && segments > 0) {
|
||||
const pathParts = cwd.split('/');
|
||||
// Remove empty strings from splitting (e.g., leading slash creates empty first element)
|
||||
const filteredParts = pathParts.filter(part => part !== '');
|
||||
|
||||
if (filteredParts.length > segments) {
|
||||
// Take the last N segments
|
||||
const selectedSegments = filteredParts.slice(-segments);
|
||||
displayPath = '.../' + selectedSegments.join('/');
|
||||
}
|
||||
}
|
||||
|
||||
return `cwd: ${displayPath}`;
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return [
|
||||
{ key: 's', label: '(s)egments', action: 'edit-segments' }
|
||||
];
|
||||
}
|
||||
|
||||
renderEditor(props: WidgetEditorProps): React.ReactElement {
|
||||
return <CurrentWorkingDirEditor {...props} />;
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return false; }
|
||||
supportsColors(item: WidgetItem): boolean { return true; }
|
||||
}
|
||||
|
||||
const CurrentWorkingDirEditor: React.FC<WidgetEditorProps> = ({ widget, onComplete, onCancel, action }) => {
|
||||
const [segmentsInput, setSegmentsInput] = useState(widget.metadata?.segments ?? '');
|
||||
|
||||
useInput((input, key) => {
|
||||
if (action === 'edit-segments') {
|
||||
if (key.return) {
|
||||
const segments = parseInt(segmentsInput, 10);
|
||||
if (!isNaN(segments) && segments > 0) {
|
||||
onComplete({
|
||||
...widget,
|
||||
metadata: {
|
||||
...widget.metadata,
|
||||
segments: segments.toString()
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Clear segments if blank or invalid
|
||||
const { segments, ...restMetadata } = widget.metadata ?? {};
|
||||
void segments; // Intentionally unused
|
||||
onComplete({
|
||||
...widget,
|
||||
metadata: Object.keys(restMetadata).length > 0 ? restMetadata : undefined
|
||||
});
|
||||
}
|
||||
} else if (key.escape) {
|
||||
onCancel();
|
||||
} else if (key.backspace) {
|
||||
setSegmentsInput(segmentsInput.slice(0, -1));
|
||||
} else if (input && /\d/.test(input) && !key.ctrl) {
|
||||
setSegmentsInput(segmentsInput + input);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (action === 'edit-segments') {
|
||||
return (
|
||||
<Box flexDirection='column'>
|
||||
<Box>
|
||||
<Text>Enter number of segments to display (blank for full path): </Text>
|
||||
<Text>{segmentsInput}</Text>
|
||||
<Text backgroundColor='gray' color='black'>{' '}</Text>
|
||||
</Box>
|
||||
<Text dimColor>Press Enter to save, ESC to cancel</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return <Text>Unknown editor mode</Text>;
|
||||
};
|
||||
@@ -216,22 +216,22 @@ const CustomCommandEditor: React.FC<WidgetEditorProps> = ({ widget, onComplete,
|
||||
} else if (mode === 'width') {
|
||||
return (
|
||||
<Box flexDirection='column'>
|
||||
<Text>
|
||||
Enter max width (blank for no limit):
|
||||
{' '}
|
||||
{widthInput}
|
||||
</Text>
|
||||
<Box>
|
||||
<Text>Enter max width (blank for no limit): </Text>
|
||||
<Text>{widthInput}</Text>
|
||||
<Text backgroundColor='gray' color='black'>{' '}</Text>
|
||||
</Box>
|
||||
<Text dimColor>Press Enter to save, ESC to cancel</Text>
|
||||
</Box>
|
||||
);
|
||||
} else if (mode === 'timeout') {
|
||||
return (
|
||||
<Box flexDirection='column'>
|
||||
<Text>
|
||||
Enter timeout in milliseconds (default 1000):
|
||||
{' '}
|
||||
{timeoutInput}
|
||||
</Text>
|
||||
<Box>
|
||||
<Text>Enter timeout in milliseconds (default 1000): </Text>
|
||||
<Text>{timeoutInput}</Text>
|
||||
<Text backgroundColor='gray' color='black'>{' '}</Text>
|
||||
</Box>
|
||||
<Text dimColor>Press Enter to save, ESC to cancel</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -14,4 +14,5 @@ export { TerminalWidthWidget } from './TerminalWidth';
|
||||
export { VersionWidget } from './Version';
|
||||
export { CustomTextWidget } from './CustomText';
|
||||
export { CustomCommandWidget } from './CustomCommand';
|
||||
export { BlockTimerWidget } from './BlockTimer';
|
||||
export { BlockTimerWidget } from './BlockTimer';
|
||||
export { CurrentWorkingDirWidget } from './CurrentWorkingDir';
|
||||
Reference in New Issue
Block a user