diffstalker 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dependency-cruiser.cjs +67 -0
- package/.githooks/pre-commit +2 -0
- package/.githooks/pre-push +15 -0
- package/README.md +43 -35
- package/bun.lock +60 -4
- package/dist/App.js +495 -131
- package/dist/KeyBindings.js +134 -10
- package/dist/MouseHandlers.js +67 -20
- package/dist/core/ExplorerStateManager.js +37 -75
- package/dist/core/GitStateManager.js +252 -46
- package/dist/git/diff.js +99 -18
- package/dist/git/status.js +111 -54
- package/dist/git/test-helpers.js +67 -0
- package/dist/index.js +54 -43
- package/dist/ipc/CommandClient.js +6 -7
- package/dist/state/UIState.js +22 -0
- package/dist/types/remote.js +5 -0
- package/dist/ui/PaneRenderers.js +45 -15
- package/dist/ui/modals/BranchPicker.js +157 -0
- package/dist/ui/modals/CommitActionConfirm.js +66 -0
- package/dist/ui/modals/FileFinder.js +45 -75
- package/dist/ui/modals/HotkeysModal.js +35 -3
- package/dist/ui/modals/SoftResetConfirm.js +68 -0
- package/dist/ui/modals/StashListModal.js +98 -0
- package/dist/ui/modals/ThemePicker.js +1 -2
- package/dist/ui/widgets/CommitPanel.js +113 -7
- package/dist/ui/widgets/CompareListView.js +44 -23
- package/dist/ui/widgets/DiffView.js +216 -170
- package/dist/ui/widgets/ExplorerView.js +50 -54
- package/dist/ui/widgets/FileList.js +62 -95
- package/dist/ui/widgets/FlatFileList.js +65 -0
- package/dist/ui/widgets/Footer.js +25 -15
- package/dist/ui/widgets/Header.js +51 -9
- package/dist/ui/widgets/fileRowFormatters.js +73 -0
- package/dist/utils/ansiTruncate.js +0 -1
- package/dist/utils/displayRows.js +101 -21
- package/dist/utils/flatFileList.js +67 -0
- package/dist/utils/layoutCalculations.js +5 -3
- package/eslint.metrics.js +0 -1
- package/metrics/v0.2.2.json +229 -0
- package/metrics/v0.2.3.json +243 -0
- package/package.json +10 -3
|
@@ -1,53 +1,5 @@
|
|
|
1
1
|
import { categorizeFiles } from '../../utils/fileCategories.js';
|
|
2
|
-
import {
|
|
3
|
-
function getStatusChar(status) {
|
|
4
|
-
switch (status) {
|
|
5
|
-
case 'modified':
|
|
6
|
-
return 'M';
|
|
7
|
-
case 'added':
|
|
8
|
-
return 'A';
|
|
9
|
-
case 'deleted':
|
|
10
|
-
return 'D';
|
|
11
|
-
case 'untracked':
|
|
12
|
-
return '?';
|
|
13
|
-
case 'renamed':
|
|
14
|
-
return 'R';
|
|
15
|
-
case 'copied':
|
|
16
|
-
return 'C';
|
|
17
|
-
default:
|
|
18
|
-
return ' ';
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
function getStatusColor(status) {
|
|
22
|
-
switch (status) {
|
|
23
|
-
case 'modified':
|
|
24
|
-
return 'yellow';
|
|
25
|
-
case 'added':
|
|
26
|
-
return 'green';
|
|
27
|
-
case 'deleted':
|
|
28
|
-
return 'red';
|
|
29
|
-
case 'untracked':
|
|
30
|
-
return 'gray';
|
|
31
|
-
case 'renamed':
|
|
32
|
-
return 'blue';
|
|
33
|
-
case 'copied':
|
|
34
|
-
return 'cyan';
|
|
35
|
-
default:
|
|
36
|
-
return 'white';
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function formatStats(insertions, deletions) {
|
|
40
|
-
if (insertions === undefined && deletions === undefined)
|
|
41
|
-
return '';
|
|
42
|
-
const parts = [];
|
|
43
|
-
if (insertions !== undefined && insertions > 0) {
|
|
44
|
-
parts.push(`{green-fg}+${insertions}{/green-fg}`);
|
|
45
|
-
}
|
|
46
|
-
if (deletions !== undefined && deletions > 0) {
|
|
47
|
-
parts.push(`{red-fg}-${deletions}{/red-fg}`);
|
|
48
|
-
}
|
|
49
|
-
return parts.length > 0 ? ' ' + parts.join(' ') : '';
|
|
50
|
-
}
|
|
2
|
+
import { getStatusChar, getStatusColor, formatStats, formatSelectionIndicator, formatFilePath, formatOriginalPath, } from './fileRowFormatters.js';
|
|
51
3
|
/**
|
|
52
4
|
* Build the list of row items for the file list.
|
|
53
5
|
*/
|
|
@@ -81,10 +33,52 @@ export function buildFileListRows(files) {
|
|
|
81
33
|
}
|
|
82
34
|
return rows;
|
|
83
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Format a single file row as blessed-compatible tagged string.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Format hunk count indicator for a file, e.g. "@2/3".
|
|
41
|
+
* Returns empty string if not applicable.
|
|
42
|
+
*/
|
|
43
|
+
function formatHunkIndicator(file, hunkCounts) {
|
|
44
|
+
if (!hunkCounts)
|
|
45
|
+
return '';
|
|
46
|
+
const stagedHunks = hunkCounts.staged.get(file.path) ?? 0;
|
|
47
|
+
const unstagedHunks = hunkCounts.unstaged.get(file.path) ?? 0;
|
|
48
|
+
const total = stagedHunks + unstagedHunks;
|
|
49
|
+
if (total === 0)
|
|
50
|
+
return '';
|
|
51
|
+
const thisCount = file.staged ? stagedHunks : unstagedHunks;
|
|
52
|
+
// Show just @total when all hunks are in this state, otherwise @n/total
|
|
53
|
+
if (thisCount === total)
|
|
54
|
+
return ` {cyan-fg}@${total}{/cyan-fg}`;
|
|
55
|
+
return ` {cyan-fg}@${thisCount}/${total}{/cyan-fg}`;
|
|
56
|
+
}
|
|
57
|
+
function formatFileRow(file, fileIndex, selectedIndex, isFocused, maxPathLength, hunkCounts) {
|
|
58
|
+
const isSelected = fileIndex === selectedIndex;
|
|
59
|
+
const statusChar = getStatusChar(file.status);
|
|
60
|
+
const statusColor = getStatusColor(file.status);
|
|
61
|
+
const actionButton = file.staged ? '[-]' : '[+]';
|
|
62
|
+
const buttonColor = file.staged ? 'red' : 'green';
|
|
63
|
+
// Calculate available space for path
|
|
64
|
+
const stats = formatStats(file.insertions, file.deletions);
|
|
65
|
+
const hunkIndicator = formatHunkIndicator(file, hunkCounts);
|
|
66
|
+
const statsLength = stats.replace(/\{[^}]+\}/g, '').length;
|
|
67
|
+
const hunkLength = hunkIndicator.replace(/\{[^}]+\}/g, '').length;
|
|
68
|
+
const availableForPath = maxPathLength - statsLength - hunkLength;
|
|
69
|
+
let line = formatSelectionIndicator(isSelected, isFocused);
|
|
70
|
+
line += `{${buttonColor}-fg}${actionButton}{/${buttonColor}-fg} `;
|
|
71
|
+
line += `{${statusColor}-fg}${statusChar}{/${statusColor}-fg} `;
|
|
72
|
+
line += formatFilePath(file.path, isSelected, isFocused, availableForPath);
|
|
73
|
+
line += formatOriginalPath(file.originalPath);
|
|
74
|
+
line += stats;
|
|
75
|
+
line += hunkIndicator;
|
|
76
|
+
return line;
|
|
77
|
+
}
|
|
84
78
|
/**
|
|
85
79
|
* Format the file list as blessed-compatible tagged string.
|
|
86
80
|
*/
|
|
87
|
-
export function formatFileList(files, selectedIndex, isFocused, width, scrollOffset = 0, maxHeight) {
|
|
81
|
+
export function formatFileList(files, selectedIndex, isFocused, width, scrollOffset = 0, maxHeight, hunkCounts) {
|
|
88
82
|
if (files.length === 0) {
|
|
89
83
|
return '{gray-fg} No changes{/gray-fg}';
|
|
90
84
|
}
|
|
@@ -95,53 +89,26 @@ export function formatFileList(files, selectedIndex, isFocused, width, scrollOff
|
|
|
95
89
|
? rows.slice(scrollOffset, scrollOffset + maxHeight)
|
|
96
90
|
: rows.slice(scrollOffset);
|
|
97
91
|
const lines = [];
|
|
92
|
+
let seenFirstHeader = false;
|
|
98
93
|
for (const row of visibleRows) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const isHighlighted = isSelected && isFocused;
|
|
109
|
-
const statusChar = getStatusChar(file.status);
|
|
110
|
-
const statusColor = getStatusColor(file.status);
|
|
111
|
-
const actionButton = file.staged ? '[-]' : '[+]';
|
|
112
|
-
const buttonColor = file.staged ? 'red' : 'green';
|
|
113
|
-
// Calculate available space for path
|
|
114
|
-
const stats = formatStats(file.insertions, file.deletions);
|
|
115
|
-
const statsLength = stats.replace(/\{[^}]+\}/g, '').length;
|
|
116
|
-
const availableForPath = maxPathLength - statsLength;
|
|
117
|
-
const displayPath = shortenPath(file.path, availableForPath);
|
|
118
|
-
// Build the line
|
|
119
|
-
let line = '';
|
|
120
|
-
// Selection indicator
|
|
121
|
-
if (isHighlighted) {
|
|
122
|
-
line += '{cyan-fg}{bold}\u25b8 {/bold}{/cyan-fg}';
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
line += ' ';
|
|
126
|
-
}
|
|
127
|
-
// Action button
|
|
128
|
-
line += `{${buttonColor}-fg}${actionButton}{/${buttonColor}-fg} `;
|
|
129
|
-
// Status character
|
|
130
|
-
line += `{${statusColor}-fg}${statusChar}{/${statusColor}-fg} `;
|
|
131
|
-
// File path (with highlighting)
|
|
132
|
-
if (isHighlighted) {
|
|
133
|
-
line += `{cyan-fg}{inverse}${displayPath}{/inverse}{/cyan-fg}`;
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
line += displayPath;
|
|
137
|
-
}
|
|
138
|
-
// Original path for renames
|
|
139
|
-
if (file.originalPath) {
|
|
140
|
-
line += ` {gray-fg}\u2190 ${shortenPath(file.originalPath, 30)}{/gray-fg}`;
|
|
94
|
+
switch (row.type) {
|
|
95
|
+
case 'header': {
|
|
96
|
+
let headerLine = `{bold}{${row.headerColor}-fg}${row.content}{/${row.headerColor}-fg}{/bold}`;
|
|
97
|
+
if (!seenFirstHeader) {
|
|
98
|
+
seenFirstHeader = true;
|
|
99
|
+
headerLine += ' {gray-fg}(h:flat){/gray-fg}';
|
|
100
|
+
}
|
|
101
|
+
lines.push(headerLine);
|
|
102
|
+
break;
|
|
141
103
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
104
|
+
case 'spacer':
|
|
105
|
+
lines.push('');
|
|
106
|
+
break;
|
|
107
|
+
case 'file':
|
|
108
|
+
if (row.file && row.fileIndex !== undefined) {
|
|
109
|
+
lines.push(formatFileRow(row.file, row.fileIndex, selectedIndex, isFocused, maxPathLength, hunkCounts ?? null));
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
145
112
|
}
|
|
146
113
|
}
|
|
147
114
|
return lines.join('\n');
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { getStatusChar, getStatusColor, formatStats, formatSelectionIndicator, formatFilePath, formatOriginalPath, } from './fileRowFormatters.js';
|
|
2
|
+
function getStagingButton(state) {
|
|
3
|
+
switch (state) {
|
|
4
|
+
case 'unstaged':
|
|
5
|
+
return { text: '[+]', color: 'green' };
|
|
6
|
+
case 'staged':
|
|
7
|
+
return { text: '[-]', color: 'red' };
|
|
8
|
+
case 'partial':
|
|
9
|
+
return { text: '[~]', color: 'yellow' };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function formatFlatHunkIndicator(entry) {
|
|
13
|
+
if (entry.totalHunks === 0)
|
|
14
|
+
return '';
|
|
15
|
+
// Always show staged/total in flat view
|
|
16
|
+
return ` {cyan-fg}@${entry.stagedHunks}/${entry.totalHunks}{/cyan-fg}`;
|
|
17
|
+
}
|
|
18
|
+
function formatFlatFileRow(entry, index, selectedIndex, isFocused, maxPathLength) {
|
|
19
|
+
const isSelected = index === selectedIndex;
|
|
20
|
+
const statusChar = getStatusChar(entry.status);
|
|
21
|
+
const statusColor = getStatusColor(entry.status);
|
|
22
|
+
const button = getStagingButton(entry.stagingState);
|
|
23
|
+
const stats = formatStats(entry.insertions, entry.deletions);
|
|
24
|
+
const hunkIndicator = formatFlatHunkIndicator(entry);
|
|
25
|
+
const statsLength = stats.replace(/\{[^}]+\}/g, '').length;
|
|
26
|
+
const hunkLength = hunkIndicator.replace(/\{[^}]+\}/g, '').length;
|
|
27
|
+
const availableForPath = maxPathLength - statsLength - hunkLength;
|
|
28
|
+
let line = formatSelectionIndicator(isSelected, isFocused);
|
|
29
|
+
line += `{${button.color}-fg}${button.text}{/${button.color}-fg} `;
|
|
30
|
+
line += `{${statusColor}-fg}${statusChar}{/${statusColor}-fg} `;
|
|
31
|
+
line += formatFilePath(entry.path, isSelected, isFocused, availableForPath);
|
|
32
|
+
line += formatOriginalPath(entry.originalPath);
|
|
33
|
+
line += stats;
|
|
34
|
+
line += hunkIndicator;
|
|
35
|
+
return line;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Format the flat file list as blessed-compatible tagged string.
|
|
39
|
+
* Row 0 is a header; files start at row 1.
|
|
40
|
+
*/
|
|
41
|
+
export function formatFlatFileList(flatFiles, selectedIndex, isFocused, width, scrollOffset = 0, maxHeight) {
|
|
42
|
+
if (flatFiles.length === 0) {
|
|
43
|
+
return '{gray-fg} No changes{/gray-fg}';
|
|
44
|
+
}
|
|
45
|
+
const maxPathLength = width - 12;
|
|
46
|
+
// Build all rows: header + file rows
|
|
47
|
+
const allRows = [];
|
|
48
|
+
allRows.push('{bold}{gray-fg}All files (h):{/gray-fg}{/bold}');
|
|
49
|
+
for (let i = 0; i < flatFiles.length; i++) {
|
|
50
|
+
allRows.push(formatFlatFileRow(flatFiles[i], i, selectedIndex, isFocused, maxPathLength));
|
|
51
|
+
}
|
|
52
|
+
// Apply scroll offset and max height
|
|
53
|
+
const visibleRows = maxHeight
|
|
54
|
+
? allRows.slice(scrollOffset, scrollOffset + maxHeight)
|
|
55
|
+
: allRows.slice(scrollOffset);
|
|
56
|
+
return visibleRows.join('\n');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Total rows in the flat file list (header + files).
|
|
60
|
+
*/
|
|
61
|
+
export function getFlatFileListTotalRows(flatFiles) {
|
|
62
|
+
if (flatFiles.length === 0)
|
|
63
|
+
return 0;
|
|
64
|
+
return flatFiles.length + 1; // +1 for header
|
|
65
|
+
}
|
|
@@ -4,26 +4,36 @@
|
|
|
4
4
|
function calculateVisibleLength(content) {
|
|
5
5
|
return content.replace(/\{[^}]+\}/g, '').length;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Format a toggle indicator: blue when on, gray when off.
|
|
9
|
+
*/
|
|
10
|
+
function toggleIndicator(label, enabled) {
|
|
11
|
+
return enabled ? `{blue-fg}[${label}]{/blue-fg}` : `{gray-fg}[${label}]{/gray-fg}`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Build the left-side indicators for the standard (non-hunk) footer.
|
|
15
|
+
*/
|
|
16
|
+
function buildStandardIndicators(mouseEnabled, autoTabEnabled, wrapMode, followEnabled, showOnlyChanges, activeTab) {
|
|
17
|
+
const parts = [];
|
|
18
|
+
parts.push(mouseEnabled ? '{yellow-fg}[scroll]{/yellow-fg}' : '{yellow-fg}m:[select]{/yellow-fg}');
|
|
19
|
+
parts.push(toggleIndicator('auto', autoTabEnabled));
|
|
20
|
+
parts.push(toggleIndicator('wrap', wrapMode));
|
|
21
|
+
parts.push(toggleIndicator('follow', followEnabled));
|
|
22
|
+
if (activeTab === 'explorer') {
|
|
23
|
+
parts.push(toggleIndicator('changes', showOnlyChanges));
|
|
24
|
+
}
|
|
25
|
+
return parts.join(' ');
|
|
26
|
+
}
|
|
7
27
|
/**
|
|
8
28
|
* Format footer content as blessed-compatible tagged string.
|
|
9
29
|
*/
|
|
10
|
-
export function formatFooter(activeTab, mouseEnabled, autoTabEnabled, wrapMode, followEnabled, showOnlyChanges, width) {
|
|
30
|
+
export function formatFooter(activeTab, mouseEnabled, autoTabEnabled, wrapMode, followEnabled, showOnlyChanges, width, currentPane) {
|
|
11
31
|
// Left side: indicators
|
|
12
32
|
let leftContent = '{gray-fg}?{/gray-fg} ';
|
|
13
|
-
leftContent += mouseEnabled
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
leftContent += autoTabEnabled ? '{blue-fg}[auto]{/blue-fg}' : '{gray-fg}[auto]{/gray-fg}';
|
|
18
|
-
leftContent += ' ';
|
|
19
|
-
leftContent += wrapMode ? '{blue-fg}[wrap]{/blue-fg}' : '{gray-fg}[wrap]{/gray-fg}';
|
|
20
|
-
leftContent += ' ';
|
|
21
|
-
leftContent += followEnabled ? '{blue-fg}[follow]{/blue-fg}' : '{gray-fg}[follow]{/gray-fg}';
|
|
22
|
-
if (activeTab === 'explorer') {
|
|
23
|
-
leftContent += ' ';
|
|
24
|
-
leftContent += showOnlyChanges
|
|
25
|
-
? '{blue-fg}[changes]{/blue-fg}'
|
|
26
|
-
: '{gray-fg}[changes]{/gray-fg}';
|
|
33
|
+
leftContent += buildStandardIndicators(mouseEnabled, autoTabEnabled, wrapMode, followEnabled, showOnlyChanges, activeTab);
|
|
34
|
+
// Show hunk key hints when diff pane is focused on diff tab
|
|
35
|
+
if (activeTab === 'diff' && currentPane === 'diff') {
|
|
36
|
+
leftContent += ' {gray-fg}n/N:hunk s:toggle{/gray-fg}';
|
|
27
37
|
}
|
|
28
38
|
// Right side: tabs
|
|
29
39
|
const tabs = [
|
|
@@ -22,10 +22,23 @@ function formatBranch(branch) {
|
|
|
22
22
|
}
|
|
23
23
|
return result;
|
|
24
24
|
}
|
|
25
|
+
function computeBranchVisibleLength(branch) {
|
|
26
|
+
let len = branch.current.length;
|
|
27
|
+
if (branch.tracking) {
|
|
28
|
+
len += 3 + branch.tracking.length;
|
|
29
|
+
}
|
|
30
|
+
if (branch.ahead > 0) {
|
|
31
|
+
len += 3 + String(branch.ahead).length;
|
|
32
|
+
}
|
|
33
|
+
if (branch.behind > 0) {
|
|
34
|
+
len += 3 + String(branch.behind).length;
|
|
35
|
+
}
|
|
36
|
+
return len;
|
|
37
|
+
}
|
|
25
38
|
/**
|
|
26
39
|
* Format header content as blessed-compatible tagged string.
|
|
27
40
|
*/
|
|
28
|
-
export function formatHeader(repoPath, branch, isLoading, error, width) {
|
|
41
|
+
export function formatHeader(repoPath, branch, isLoading, error, width, remoteState) {
|
|
29
42
|
if (!repoPath) {
|
|
30
43
|
return '{gray-fg}Waiting for target path...{/gray-fg}';
|
|
31
44
|
}
|
|
@@ -42,6 +55,39 @@ export function formatHeader(repoPath, branch, isLoading, error, width) {
|
|
|
42
55
|
else if (error) {
|
|
43
56
|
leftContent += ` {red-fg}(${error}){/red-fg}`;
|
|
44
57
|
}
|
|
58
|
+
// Remote operation status (shown after left content)
|
|
59
|
+
let remoteStatus = '';
|
|
60
|
+
let remoteStatusLen = 0;
|
|
61
|
+
if (remoteState) {
|
|
62
|
+
if (remoteState.inProgress && remoteState.operation) {
|
|
63
|
+
const labels = {
|
|
64
|
+
push: 'pushing...',
|
|
65
|
+
fetch: 'fetching...',
|
|
66
|
+
pull: 'rebasing...',
|
|
67
|
+
stash: 'stashing...',
|
|
68
|
+
stashPop: 'popping stash...',
|
|
69
|
+
branchSwitch: 'switching branch...',
|
|
70
|
+
branchCreate: 'creating branch...',
|
|
71
|
+
softReset: 'resetting...',
|
|
72
|
+
cherryPick: 'cherry-picking...',
|
|
73
|
+
revert: 'reverting...',
|
|
74
|
+
};
|
|
75
|
+
const label = labels[remoteState.operation] ?? '';
|
|
76
|
+
remoteStatus = ` {yellow-fg}${label}{/yellow-fg}`;
|
|
77
|
+
remoteStatusLen = 1 + label.length;
|
|
78
|
+
}
|
|
79
|
+
else if (remoteState.error) {
|
|
80
|
+
const brief = remoteState.error.length > 40
|
|
81
|
+
? remoteState.error.slice(0, 40) + '\u2026'
|
|
82
|
+
: remoteState.error;
|
|
83
|
+
remoteStatus = ` {red-fg}${brief}{/red-fg}`;
|
|
84
|
+
remoteStatusLen = 1 + brief.length;
|
|
85
|
+
}
|
|
86
|
+
else if (remoteState.lastResult) {
|
|
87
|
+
remoteStatus = ` {green-fg}${remoteState.lastResult}{/green-fg}`;
|
|
88
|
+
remoteStatusLen = 1 + remoteState.lastResult.length;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
45
91
|
// Build right side content (branch info)
|
|
46
92
|
const rightContent = branch ? formatBranch(branch) : '';
|
|
47
93
|
if (rightContent) {
|
|
@@ -55,14 +101,10 @@ export function formatHeader(repoPath, branch, isLoading, error, width) {
|
|
|
55
101
|
else if (error) {
|
|
56
102
|
leftLen += error.length + 3; // " (error)"
|
|
57
103
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
(branch.tracking ? 3 + branch.tracking.length : 0) +
|
|
61
|
-
(branch.ahead > 0 ? 3 + String(branch.ahead).length : 0) +
|
|
62
|
-
(branch.behind > 0 ? 3 + String(branch.behind).length : 0)
|
|
63
|
-
: 0;
|
|
104
|
+
leftLen += remoteStatusLen;
|
|
105
|
+
const rightLen = branch ? computeBranchVisibleLength(branch) : 0;
|
|
64
106
|
const padding = Math.max(1, width - leftLen - rightLen - 2);
|
|
65
|
-
return leftContent + ' '.repeat(padding) + rightContent;
|
|
107
|
+
return leftContent + remoteStatus + ' '.repeat(padding) + rightContent;
|
|
66
108
|
}
|
|
67
|
-
return leftContent;
|
|
109
|
+
return leftContent + remoteStatus;
|
|
68
110
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { shortenPath } from '../../utils/formatPath.js';
|
|
2
|
+
export function getStatusChar(status) {
|
|
3
|
+
switch (status) {
|
|
4
|
+
case 'modified':
|
|
5
|
+
return 'M';
|
|
6
|
+
case 'added':
|
|
7
|
+
return 'A';
|
|
8
|
+
case 'deleted':
|
|
9
|
+
return 'D';
|
|
10
|
+
case 'untracked':
|
|
11
|
+
return '?';
|
|
12
|
+
case 'renamed':
|
|
13
|
+
return 'R';
|
|
14
|
+
case 'copied':
|
|
15
|
+
return 'C';
|
|
16
|
+
default:
|
|
17
|
+
return ' ';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function getStatusColor(status) {
|
|
21
|
+
switch (status) {
|
|
22
|
+
case 'modified':
|
|
23
|
+
return 'yellow';
|
|
24
|
+
case 'added':
|
|
25
|
+
return 'green';
|
|
26
|
+
case 'deleted':
|
|
27
|
+
return 'red';
|
|
28
|
+
case 'untracked':
|
|
29
|
+
return 'gray';
|
|
30
|
+
case 'renamed':
|
|
31
|
+
return 'blue';
|
|
32
|
+
case 'copied':
|
|
33
|
+
return 'cyan';
|
|
34
|
+
default:
|
|
35
|
+
return 'white';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function formatStats(insertions, deletions) {
|
|
39
|
+
if (insertions === undefined && deletions === undefined)
|
|
40
|
+
return '';
|
|
41
|
+
const parts = [];
|
|
42
|
+
if (insertions !== undefined && insertions > 0) {
|
|
43
|
+
parts.push(`{green-fg}+${insertions}{/green-fg}`);
|
|
44
|
+
}
|
|
45
|
+
if (deletions !== undefined && deletions > 0) {
|
|
46
|
+
parts.push(`{red-fg}-${deletions}{/red-fg}`);
|
|
47
|
+
}
|
|
48
|
+
return parts.length > 0 ? ' ' + parts.join(' ') : '';
|
|
49
|
+
}
|
|
50
|
+
export function formatSelectionIndicator(isSelected, isFocused) {
|
|
51
|
+
if (isSelected && isFocused) {
|
|
52
|
+
return '{cyan-fg}{bold}\u25b8 {/bold}{/cyan-fg}';
|
|
53
|
+
}
|
|
54
|
+
else if (isSelected) {
|
|
55
|
+
return '{gray-fg}\u25b8 {/gray-fg}';
|
|
56
|
+
}
|
|
57
|
+
return ' ';
|
|
58
|
+
}
|
|
59
|
+
export function formatFilePath(path, isSelected, isFocused, maxLength) {
|
|
60
|
+
const displayPath = shortenPath(path, maxLength);
|
|
61
|
+
if (isSelected && isFocused) {
|
|
62
|
+
return `{cyan-fg}{inverse}${displayPath}{/inverse}{/cyan-fg}`;
|
|
63
|
+
}
|
|
64
|
+
else if (isSelected) {
|
|
65
|
+
return `{cyan-fg}${displayPath}{/cyan-fg}`;
|
|
66
|
+
}
|
|
67
|
+
return displayPath;
|
|
68
|
+
}
|
|
69
|
+
export function formatOriginalPath(originalPath) {
|
|
70
|
+
if (!originalPath)
|
|
71
|
+
return '';
|
|
72
|
+
return ` {gray-fg}\u2190 ${shortenPath(originalPath, 30)}{/gray-fg}`;
|
|
73
|
+
}
|