@akiojin/claude-worktree
Version:
Interactive Git worktree manager for Claude Code with graphical branch selection
153 lines • 5.48 kB
JavaScript
import chalk from 'chalk';
import stringWidth from 'string-width';
import { getChangedFilesCount } from '../git.js';
export async function createBranchTable(branches, worktrees) {
// Create worktree lookup map (excluding main repository)
const worktreeMap = new Map(worktrees
.filter(w => w.path !== process.cwd()) // Exclude main repository
.map(w => [w.branch, w]));
const choices = [];
// Set fixed width for branch name column
const branchNameColumnWidth = 32;
// Add header row
const headerRow = [
padEndUnicode(chalk.bold.cyan('ブランチ名'), branchNameColumnWidth),
padEndUnicode(chalk.bold.cyan('タイプ'), 14),
padEndUnicode(chalk.bold.cyan('Worktree'), 10),
padEndUnicode(chalk.bold.cyan('ステータス'), 12),
chalk.bold.cyan('変更')
].join(' ┃ ');
choices.push({
name: headerRow,
value: '__header__',
disabled: true
});
// Add separator row
const separatorRow = [
'─'.repeat(branchNameColumnWidth),
'─'.repeat(14),
'─'.repeat(10),
'─'.repeat(12),
'─'.repeat(4)
].join('─┼─');
choices.push({
name: chalk.gray(separatorRow),
value: '__separator__',
disabled: true
});
// Filter out "origin" branch and sort: current first, then by type
const filteredBranches = branches.filter(b => b.name !== 'origin');
const sortedBranches = [...filteredBranches].sort((a, b) => {
if (a.isCurrent && !b.isCurrent)
return -1;
if (!a.isCurrent && b.isCurrent)
return 1;
if (a.branchType === 'main' && b.branchType !== 'main')
return -1;
if (a.branchType !== 'main' && b.branchType === 'main')
return 1;
return a.name.localeCompare(b.name);
});
for (const branch of sortedBranches) {
const worktree = worktreeMap.get(branch.name);
const hasWorktree = !!worktree;
// Format branch name with indicators
let branchDisplay = branch.name;
if (branch.isCurrent) {
branchDisplay = `◉ ${branch.name}`;
}
// Format type with colors and icons
const typeIcon = getBranchTypeIcon(branch.branchType);
const typeText = `${typeIcon} ${branch.branchType}`;
// Format worktree status
const worktreeStatus = hasWorktree ? chalk.green('●') : chalk.gray('○');
// Format status with colors
let statusText = '';
if (branch.isCurrent) {
statusText = chalk.bgGreen.black(' CURRENT ');
}
else if (branch.type === 'remote') {
statusText = chalk.bgBlue.white(' REMOTE ');
}
else {
statusText = chalk.bgGray.white(' LOCAL ');
}
// Get changes count if worktree exists
let changesText = '';
if (hasWorktree && worktree) {
// Check if worktree is accessible
if (worktree.isAccessible === false) {
changesText = chalk.red('✗ Invalid');
}
else {
try {
const changedFiles = await getChangedFilesCount(worktree.path);
if (changedFiles > 0) {
changesText = chalk.yellow(`✎ ${changedFiles}`);
}
else {
changesText = chalk.gray('─');
}
}
catch {
changesText = chalk.gray('─');
}
}
}
else {
changesText = chalk.gray('─');
}
// Create table-like display string with truncated branch names
const displayName = [
padEndUnicode(truncateString(branchDisplay, branchNameColumnWidth), branchNameColumnWidth),
padEndUnicode(typeText, 14),
padEndUnicode(worktreeStatus, 10),
padEndUnicode(statusText, 12),
changesText // No padding for the last column
].join(' ┃ ');
choices.push({
name: displayName,
value: branch.name,
description: hasWorktree ? `Worktree: ${worktree.path}` : 'No worktree'
});
}
return choices;
}
function getBranchTypeIcon(branchType) {
switch (branchType) {
case 'main':
return '⚡';
case 'develop':
return '🔧';
case 'feature':
return '✨';
case 'hotfix':
return '🔥';
case 'release':
return '🚀';
default:
return '📌';
}
}
function padEndUnicode(str, targetLength, padString = ' ') {
const strWidth = stringWidth(str);
if (strWidth >= targetLength)
return str;
const padWidth = targetLength - strWidth;
return str + padString.repeat(Math.max(0, padWidth));
}
function truncateString(str, maxWidth) {
const strWidth = stringWidth(str);
if (strWidth <= maxWidth)
return str;
// Try to truncate while preserving meaning
let truncated = str;
const ellipsis = '...';
const ellipsisWidth = stringWidth(ellipsis);
const targetWidth = maxWidth - ellipsisWidth;
while (stringWidth(truncated) > targetWidth && truncated.length > 0) {
truncated = truncated.slice(0, -1);
}
return truncated + ellipsis;
}
//# sourceMappingURL=table.js.map