Add Current Working Dir component

This commit is contained in:
Matthew Breedlove
2025-08-21 01:28:17 -04:00
parent f9a113a9d2
commit 3aea75403f
10 changed files with 177 additions and 24 deletions
+9
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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'>
+1 -1
View File
@@ -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
+8 -1
View File
@@ -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
View File
@@ -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;
}
}
}
+1
View File
@@ -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()],
+134
View File
@@ -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>;
};
+10 -10
View File
@@ -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>
);
+2 -1
View File
@@ -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';