Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions src/widgets/GitBranch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export class GitBranchWidget implements Widget {
const modifiers: string[] = [];
if (isLink)
modifiers.push('repo link');
if (item.metadata?.fishStyle === 'true')
modifiers.push('fish-style');
const maxWidthText = getMaxWidthModifier(item);
if (maxWidthText)
modifiers.push(maxWidthText);
Expand All @@ -95,16 +97,27 @@ export class GitBranchWidget implements Widget {
if (action === TOGGLE_LINK_ACTION) {
return toggleLink(item);
}
if (action === 'toggle-fish-style') {
const enabled = item.metadata?.fishStyle === 'true';
const { fishStyle, ...restMetadata } = item.metadata ?? {};
const nextMetadata = enabled ? restMetadata : { ...restMetadata, fishStyle: 'true' };
return {
...item,
metadata: Object.keys(nextMetadata).length > 0 ? nextMetadata : undefined
};
}
return null;
}

render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
const hideNoGit = isHidden(item, NO_GIT_HIDEABLE_STATE.key);
const isLink = isLinkEnabled(item);
const fishStyle = item.metadata?.fishStyle === 'true';
const prefix = formatSymbolPrefix(item, DEFAULT_SYMBOL);

if (context.isPreview) {
const text = item.rawValue ? 'main' : `${prefix}main`;
const preview = this.abbreviateBranch('main', fishStyle);
const text = item.rawValue ? preview : `${prefix}${preview}`;
return isLink ? renderOsc8Link('https://github.com/owner/repo/tree/main', text) : text;
}

Expand All @@ -117,7 +130,8 @@ export class GitBranchWidget implements Widget {
return hideNoGit ? null : `${prefix}no git`;
}

const displayText = applyMaxWidth(item.rawValue ? branch : `${prefix}${branch}`, item.maxWidth);
const displayBranch = this.abbreviateBranch(branch, fishStyle);
const displayText = applyMaxWidth(item.rawValue ? displayBranch : `${prefix}${displayBranch}`, item.maxWidth);

if (isLink) {
const origin = getRemoteInfo('origin', context);
Expand All @@ -136,9 +150,23 @@ export class GitBranchWidget implements Widget {
return runGit('symbolic-ref --short HEAD', context);
}

// Fish-style branch abbreviation: every segment but the last collapses to
// its first character, so 'main' -> 'm' and 'fix/np/story' -> 'f/p/story'.
private abbreviateBranch(branch: string, fishStyle: boolean): string {
if (!fishStyle) {
return branch;
}
const parts = branch.split('/');
if (parts.length === 1) {
return branch[0] ?? '';
}
return parts.map((part, index) => (index === parts.length - 1 ? part : part[0] ?? '')).join('/');
}

getCustomKeybinds(): CustomKeybind[] {
return [
{ key: 'l', label: '(l)ink to repo', action: TOGGLE_LINK_ACTION },
{ key: 'f', label: '(f)ish style', action: 'toggle-fish-style' },
getMaxWidthKeybind(),
getSymbolKeybind()
];
Expand Down
53 changes: 47 additions & 6 deletions src/widgets/GitWorktree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
runGit
} from '../utils/git';

import { makeModifierText } from './shared/editor-display';
import {
NO_GIT_HIDEABLE_STATE,
isHidden
Expand All @@ -30,31 +31,68 @@ export class GitWorktreeWidget implements Widget {
getDisplayName(): string { return 'Git Worktree'; }
getCategory(): string { return 'Git'; }
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
const modifiers: string[] = [];
if (item.metadata?.fishStyle === 'true')
modifiers.push('fish-style');
return {
displayText: this.getDisplayName(),
modifierText: makeModifierText(modifiers)
};
}

getHideableStates(): HideableState[] {
return [NO_GIT_HIDEABLE_STATE];
}

handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
if (action === 'toggle-fish-style') {
const enabled = item.metadata?.fishStyle === 'true';
const { fishStyle, ...restMetadata } = item.metadata ?? {};
const nextMetadata = enabled ? restMetadata : { ...restMetadata, fishStyle: 'true' };
return {
...item,
metadata: Object.keys(nextMetadata).length > 0 ? nextMetadata : undefined
};
}
return null;
}

render(item: WidgetItem, context: RenderContext): string | null {
const hideNoGit = isHidden(item, NO_GIT_HIDEABLE_STATE.key);
const fishStyle = item.metadata?.fishStyle === 'true';
const prefix = formatSymbolPrefix(item, DEFAULT_SYMBOL);

if (context.isPreview)
return item.rawValue ? 'main' : `${prefix}main`;
if (context.isPreview) {
const preview = this.abbreviateWorktree('main', fishStyle);
return item.rawValue ? preview : `${prefix}${preview}`;
}

if (!isInsideGitWorkTree(context)) {
return hideNoGit ? null : `${prefix}no git`;
}

const worktree = this.getGitWorktree(context);
if (worktree)
return item.rawValue ? worktree : `${prefix}${worktree}`;
if (worktree) {
const displayWorktree = this.abbreviateWorktree(worktree, fishStyle);
return item.rawValue ? displayWorktree : `${prefix}${displayWorktree}`;
}

return hideNoGit ? null : `${prefix}no git`;
}

// Same fish-style rule as Git Branch: every segment but the last collapses
// to its first character, so 'main' -> 'm' and 'dir/wt' -> 'd/wt'.
private abbreviateWorktree(worktree: string, fishStyle: boolean): string {
if (!fishStyle) {
return worktree;
}
const parts = worktree.split('/');
if (parts.length === 1) {
return worktree[0] ?? '';
}
return parts.map((part, index) => (index === parts.length - 1 ? part : part[0] ?? '')).join('/');
}

private getGitWorktree(context: RenderContext): string | null {
const worktreeDir = runGit('rev-parse --git-dir', context);
if (!worktreeDir)
Expand Down Expand Up @@ -85,7 +123,10 @@ export class GitWorktreeWidget implements Widget {
}

getCustomKeybinds(): CustomKeybind[] {
return [getSymbolKeybind()];
return [
{ key: 'f', label: '(f)ish style', action: 'toggle-fish-style' },
getSymbolKeybind()
];
}

renderEditor(props: WidgetEditorProps) {
Expand Down
24 changes: 24 additions & 0 deletions src/widgets/__tests__/GitBranch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,28 @@ describe('GitBranchWidget', () => {
expect(toggled?.metadata).toEqual({ hide: 'no-git' });
});
});

describe('fish style', () => {
it.each([
{ name: 'collapses a plain branch to its first letter', branch: 'main', expected: 'βŽ‡ m' },
{ name: 'keeps the last segment full', branch: 'feature/worktree', expected: 'βŽ‡ f/worktree' }
])('$name', ({ branch, expected }) => {
mockExecFileSync.mockReturnValueOnce('true\n');
mockExecFileSync.mockReturnValueOnce(branch);

expect(render({ metadata: { fishStyle: 'true' } })).toBe(expected);
});

it('shows the fish-style modifier and keybind in the editor', () => {
const widget = new GitBranchWidget();

expect(widget.getEditorDisplay({ id: 'git-branch', type: 'git-branch', metadata: { fishStyle: 'true' } }).modifierText)
.toBe('(fish-style)');
expect(widget.getCustomKeybinds()).toContainEqual({
key: 'f',
label: '(f)ish style',
action: 'toggle-fish-style'
});
});
});
});
31 changes: 30 additions & 1 deletion src/widgets/__tests__/GitWorktree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,23 @@ function render(options: {
cwd?: string;
hideNoGit?: boolean;
isPreview?: boolean;
metadata?: Record<string, string>;
rawValue?: boolean;
} = {}) {
const widget = new GitWorktreeWidget();
const context: RenderContext = {
isPreview: options.isPreview,
data: options.cwd ? { cwd: options.cwd } : undefined
};
const metadata = {
...options.metadata,
...(options.hideNoGit ? { hide: 'no-git' } : {})
};
const item: WidgetItem = {
id: 'git-worktree',
type: 'git-worktree',
rawValue: options.rawValue,
metadata: options.hideNoGit ? { hide: 'no-git' } : undefined
metadata: Object.keys(metadata).length > 0 ? metadata : undefined
};

return widget.render(item, context);
Expand Down Expand Up @@ -142,4 +147,28 @@ describe('GitWorktreeWidget', () => {

expect(render()).toBe('π– ° some-worktree');
});

describe('fish style', () => {
it.each([
{ name: 'collapses the main worktree to its first letter', gitDir: '.git', expected: 'π– ° m' },
{ name: 'keeps the last segment full', gitDir: '/some/path/.git/worktrees/some-dir/some-worktree', expected: 'π– ° s/some-worktree' }
])('$name', ({ gitDir, expected }) => {
mockExecFileSync.mockReturnValueOnce('true\n');
mockExecFileSync.mockReturnValueOnce(gitDir);

expect(render({ metadata: { fishStyle: 'true' } })).toBe(expected);
});

it('shows the fish-style modifier and keybind in the editor', () => {
const widget = new GitWorktreeWidget();

expect(widget.getEditorDisplay({ id: 'git-worktree', type: 'git-worktree', metadata: { fishStyle: 'true' } }).modifierText)
.toBe('(fish-style)');
expect(widget.getCustomKeybinds()).toContainEqual({
key: 'f',
label: '(f)ish style',
action: 'toggle-fish-style'
});
});
});
});