feat: Add fish-style path abbreviation to CurrentWorkingDir widget (#80)

* feat: Add fish-style path abbreviation to CurrentWorkingDir widget

- Added new toggle option for fish-style path abbreviation
- Keeps home directory as ~, first and last directories complete
- Abbreviates middle directories to first letter only
- Preserves dot for hidden directories (e.g. .config → .c)
- Maintains backward compatibility with segments feature
- Both options are mutually exclusive (fish-style takes precedence)

Example: ~/Documents/Projects/my-project → ~/D/P/my-project

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Modify cwd widget fish-style code to use handleEditorAction instead of renderEditor, misc lint fixes

---------

Co-authored-by: Jishuai <aqianlikuaizaifeng@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Matthew Breedlove <sirmalloc@gmail.com>
This commit is contained in:
MIAO Jishuai
2025-09-19 01:25:16 -04:00
committed by GitHub
co-authored by Jishuai Claude Matthew Breedlove
parent 0a5f0e58b9
commit d23af975b2
+91 -10
View File
@@ -3,6 +3,7 @@ import {
Text,
useInput
} from 'ink';
import * as os from 'node:os';
import React, { useState } from 'react';
import type { RenderContext } from '../types/RenderContext';
@@ -21,9 +22,12 @@ export class CurrentWorkingDirWidget implements Widget {
getDisplayName(): string { return 'Current Working Dir'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
const segments = item.metadata?.segments ? parseInt(item.metadata.segments, 10) : undefined;
const fishStyle = item.metadata?.fishStyle === 'true';
const modifiers: string[] = [];
if (segments && segments > 0) {
if (fishStyle) {
modifiers.push('fish-style');
} else if (segments && segments > 0) {
modifiers.push(`segments: ${segments}`);
}
@@ -33,36 +37,74 @@ export class CurrentWorkingDirWidget implements Widget {
};
}
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
if (action === 'toggle-fish-style') {
const currentFishStyle = item.metadata?.fishStyle === 'true';
const newFishStyle = !currentFishStyle;
// Toggle fish style and clear segments
if (newFishStyle) {
// When enabling fish-style, clear segments
const { segments, ...restMetadata } = item.metadata ?? {};
void segments;
return {
...item,
metadata: {
...restMetadata,
fishStyle: 'true'
}
};
} else {
// When disabling fish-style
const { fishStyle, ...restMetadata } = item.metadata ?? {};
void fishStyle;
return {
...item,
metadata: Object.keys(restMetadata).length > 0 ? restMetadata : undefined
};
}
}
return null;
}
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
const segments = item.metadata?.segments ? parseInt(item.metadata.segments, 10) : undefined;
const fishStyle = item.metadata?.fishStyle === 'true';
if (context.isPreview) {
const segments = item.metadata?.segments ? parseInt(item.metadata.segments, 10) : undefined;
let previewPath: string;
if (segments && segments > 0) {
if (fishStyle) {
previewPath = '~/D/P/my-project';
} else if (segments && segments > 0) {
if (segments === 1) {
previewPath = '.../project';
} else {
previewPath = '.../example/project';
}
} else {
previewPath = '/Users/example/project';
previewPath = '/Users/example/Documents/Projects/my-project';
}
return item.rawValue ? previewPath : `cwd: ${previewPath}`;
}
const cwd = context.data?.cwd;
if (!cwd) {
if (!cwd)
return null;
}
const segments = item.metadata?.segments ? parseInt(item.metadata.segments, 10) : undefined;
let displayPath = cwd;
if (segments && segments > 0) {
if (fishStyle) {
displayPath = this.abbreviatePath(cwd);
} else if (segments && segments > 0) {
// Support both POSIX ('/') and Windows ('\\') separators; preserve original separator in output
const useBackslash = cwd.includes('\\') && !cwd.includes('/');
const outSep = useBackslash ? '\\' : '/';
const pathParts = cwd.split(/[\\/]+/);
// Remove empty strings from splitting (e.g., leading slash or UNC leading separators)
const filteredParts = pathParts.filter(part => part !== '');
@@ -78,7 +120,8 @@ export class CurrentWorkingDirWidget implements Widget {
getCustomKeybinds(): CustomKeybind[] {
return [
{ key: 's', label: '(s)egments', action: 'edit-segments' }
{ key: 's', label: '(s)egments', action: 'edit-segments' },
{ key: 'f', label: '(f)ish style', action: 'toggle-fish-style' }
];
}
@@ -88,6 +131,44 @@ export class CurrentWorkingDirWidget implements Widget {
supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
private abbreviatePath(path: string): string {
const homeDir = os.homedir();
const useBackslash = path.includes('\\') && !path.includes('/');
const sep = useBackslash ? '\\' : '/';
// Replace home directory with ~
let normalizedPath = path;
if (path.startsWith(homeDir)) {
normalizedPath = '~' + path.slice(homeDir.length);
}
// Split path into parts
const parts = normalizedPath.split(/[\\/]+/).filter(part => part !== '');
// Keep first and last parts full, abbreviate middle parts
const abbreviated = parts.map((part, index) => {
if (index === 0 || index === parts.length - 1) {
return part; // Keep full
}
// Hidden directories keep the dot
if (part.startsWith('.') && part.length > 1) {
return '.' + (part[1] ?? '');
}
return part[0]; // Only first letter for others
});
// Rebuild path
if (normalizedPath.startsWith('~')) {
return abbreviated.join(sep);
} else if (normalizedPath.startsWith('/')) {
return sep + abbreviated.join(sep);
} else {
return abbreviated.join(sep);
}
}
}
const CurrentWorkingDirEditor: React.FC<WidgetEditorProps> = ({ widget, onComplete, onCancel, action }) => {