git-flex 1.0.0 → 1.1.1
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/package.json +3 -2
- package/src/git.js +20 -6
- package/src/svg.js +79 -46
- package/src/terminal.js +76 -69
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-flex",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Show off your coding stats in style. Beautiful terminal cards, shareable SVGs, and fun highlights from your git history.",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"chalk": "5.6.2",
|
|
44
|
-
"commander": "12.1.0"
|
|
44
|
+
"commander": "12.1.0",
|
|
45
|
+
"string-width": "7.2.0"
|
|
45
46
|
}
|
|
46
47
|
}
|
package/src/git.js
CHANGED
|
@@ -86,17 +86,31 @@ export function getDiffStatsAndLangs({ author, since, until } = {}) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
export function getAllAuthors({ since, until } = {}) {
|
|
89
|
-
|
|
89
|
+
// Get commits + lines in one pass using a marker to separate commits
|
|
90
|
+
const args = ['git', 'log', '--no-merges', '--numstat', '--format=COMMIT:%an'];
|
|
90
91
|
if (since) args.push(`--since=${since}`);
|
|
91
92
|
if (until) args.push(`--until=${until}`);
|
|
92
93
|
const raw = runArgs(args);
|
|
93
94
|
if (!raw) return [];
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
|
|
96
|
+
const authors = {};
|
|
97
|
+
let current = null;
|
|
98
|
+
|
|
99
|
+
for (const ln of raw.split('\n')) {
|
|
100
|
+
if (ln.startsWith('COMMIT:')) {
|
|
101
|
+
current = ln.slice(7);
|
|
102
|
+
if (!authors[current]) authors[current] = { commits: 0, added: 0, removed: 0 };
|
|
103
|
+
authors[current].commits++;
|
|
104
|
+
} else if (current && ln.includes('\t')) {
|
|
105
|
+
const [a, r] = ln.split('\t');
|
|
106
|
+
if (a === '-') continue;
|
|
107
|
+
authors[current].added += parseInt(a) || 0;
|
|
108
|
+
authors[current].removed += parseInt(r) || 0;
|
|
109
|
+
}
|
|
97
110
|
}
|
|
98
|
-
|
|
99
|
-
|
|
111
|
+
|
|
112
|
+
return Object.entries(authors)
|
|
113
|
+
.map(([name, s]) => ({ name, commits: s.commits, added: s.added, removed: s.removed, net: s.added - s.removed }))
|
|
100
114
|
.sort((a, b) => b.commits - a.commits);
|
|
101
115
|
}
|
|
102
116
|
|
package/src/svg.js
CHANGED
|
@@ -21,31 +21,70 @@ export function generateSVG(stats, streakData, repoName, periodLabel, theme = 'd
|
|
|
21
21
|
const rank = getRank(stats, streakData);
|
|
22
22
|
const level = getLevel(stats.commits);
|
|
23
23
|
const highlights = getHighlights(stats, streakData);
|
|
24
|
-
const W = 480
|
|
24
|
+
const W = 480;
|
|
25
|
+
const F = `font-family="'SF Mono','Fira Code',monospace"`;
|
|
25
26
|
|
|
26
|
-
//
|
|
27
|
+
// Dynamic Y layout — each section pushes cursor down
|
|
28
|
+
let y = 0;
|
|
29
|
+
|
|
30
|
+
// Header: 0-54
|
|
31
|
+
y = 54;
|
|
32
|
+
|
|
33
|
+
// User + rank + level: 55-116
|
|
34
|
+
y = 116;
|
|
35
|
+
|
|
36
|
+
// Stats grid row 1: values at y+26, y+44
|
|
37
|
+
const statsY = y + 10; // 126 — labels
|
|
38
|
+
const statsValY = statsY + 18; // 144 — values
|
|
39
|
+
|
|
40
|
+
// Stats grid row 2
|
|
41
|
+
const stats2Y = statsValY + 25; // 169 — labels
|
|
42
|
+
const stats2ValY = stats2Y + 18; // 187 — values
|
|
43
|
+
|
|
44
|
+
// Highlights
|
|
45
|
+
y = stats2ValY + 20; // 207
|
|
46
|
+
const hlY = y;
|
|
47
|
+
|
|
48
|
+
// Language section
|
|
49
|
+
y = hlY + highlights.length * 18 + 14;
|
|
50
|
+
const langTitleY = y;
|
|
51
|
+
const langBarY = y + 14;
|
|
52
|
+
const langLabelY = langBarY + 20;
|
|
53
|
+
|
|
54
|
+
// Sparkline section
|
|
55
|
+
y = langLabelY + 16;
|
|
56
|
+
const sparkTitleY = y;
|
|
57
|
+
const sparkBaseY = sparkTitleY + 50;
|
|
58
|
+
|
|
59
|
+
// Footer
|
|
60
|
+
const footerY = sparkBaseY + 20;
|
|
61
|
+
const H = footerY + 10;
|
|
62
|
+
|
|
63
|
+
// Build language bars
|
|
27
64
|
let langBars = '';
|
|
28
65
|
let langLabels = '';
|
|
29
66
|
let xOffset = 30;
|
|
30
67
|
const barW = W - 60;
|
|
31
|
-
|
|
68
|
+
const langs = stats.languages.slice(0, 6);
|
|
69
|
+
const langColors = ['#58a6ff', '#3fb950', '#d29922', '#bc8cff', '#f85149', '#79c0ff'];
|
|
70
|
+
for (let i = 0; i < langs.length; i++) {
|
|
71
|
+
const lang = langs[i];
|
|
32
72
|
const w = Math.max(2, (lang.pct / 100) * barW);
|
|
33
|
-
|
|
34
|
-
const color = colors[stats.languages.indexOf(lang) % colors.length];
|
|
35
|
-
langBars += `<rect x="${xOffset}" y="290" width="${w}" height="8" rx="2" fill="${color}"/>`;
|
|
73
|
+
langBars += `<rect x="${xOffset}" y="${langBarY}" width="${w}" height="8" rx="2" fill="${langColors[i % langColors.length]}"/>`;
|
|
36
74
|
if (lang.pct >= 8) {
|
|
37
|
-
langLabels += `<text x="${xOffset + w / 2}" y="
|
|
75
|
+
langLabels += `<text x="${xOffset + w / 2}" y="${langLabelY}" fill="${c.textDim}" font-size="10" text-anchor="middle" ${F}>${lang.name} ${lang.pct}%</text>`;
|
|
38
76
|
}
|
|
39
77
|
xOffset += w + 2;
|
|
40
78
|
}
|
|
41
79
|
|
|
42
|
-
//
|
|
43
|
-
const
|
|
80
|
+
// Build sparkline
|
|
81
|
+
const maxHour = Math.max(...stats.hourCounts, 1);
|
|
44
82
|
let sparkline = '';
|
|
83
|
+
const sparkH = 40;
|
|
45
84
|
for (let i = 0; i < 24; i++) {
|
|
46
|
-
const h = (stats.hourCounts[i] /
|
|
85
|
+
const h = (stats.hourCounts[i] / maxHour) * sparkH;
|
|
47
86
|
const x = 30 + i * ((W - 60) / 24);
|
|
48
|
-
sparkline += `<rect x="${x}" y="${
|
|
87
|
+
sparkline += `<rect x="${x}" y="${sparkBaseY - h}" width="${(W - 60) / 24 - 1}" height="${h}" rx="1" fill="${c.accent}" opacity="0.6"/>`;
|
|
49
88
|
}
|
|
50
89
|
|
|
51
90
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
|
@@ -67,72 +106,66 @@ export function generateSVG(stats, streakData, repoName, periodLabel, theme = 'd
|
|
|
67
106
|
<!-- Background -->
|
|
68
107
|
<rect width="${W}" height="${H}" rx="12" fill="url(#bg)" stroke="${c.border}" stroke-width="1"/>
|
|
69
108
|
|
|
70
|
-
<!-- Header
|
|
109
|
+
<!-- Header -->
|
|
71
110
|
<rect x="0" y="54" width="${W}" height="1" fill="${c.border}"/>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<text x="30" y="36" fill="${c.accent}" font-size="16" font-weight="bold" font-family="'SF Mono','Fira Code',monospace" filter="url(#glow)">FLEX</text>
|
|
75
|
-
<text x="78" y="36" fill="${c.textDim}" font-size="13" font-family="'SF Mono','Fira Code',monospace">${escXml(repoName)} \u2022 ${escXml(periodLabel)}</text>
|
|
111
|
+
<text x="30" y="36" fill="${c.accent}" font-size="16" font-weight="bold" ${F} filter="url(#glow)">FLEX</text>
|
|
112
|
+
<text x="78" y="36" fill="${c.textDim}" font-size="13" ${F}>${escXml(repoName)} \u2022 ${escXml(periodLabel)}</text>
|
|
76
113
|
|
|
77
114
|
<!-- User + Rank -->
|
|
78
|
-
<text x="30" y="82" fill="${c.yellow}" font-size="15" font-weight="bold"
|
|
79
|
-
<text x="${30 + stats.author.length * 9.5}" y="82" fill="${c.purple}" font-size="13" font-style="italic"
|
|
115
|
+
<text x="30" y="82" fill="${c.yellow}" font-size="15" font-weight="bold" ${F}>${escXml(stats.author)}</text>
|
|
116
|
+
<text x="${30 + stats.author.length * 9.5}" y="82" fill="${c.purple}" font-size="13" font-style="italic" ${F}> ${rank.icon} ${escXml(rank.title)}</text>
|
|
80
117
|
|
|
81
118
|
<!-- Level bar -->
|
|
82
|
-
<text x="30" y="105" fill="${c.textDim}" font-size="11"
|
|
119
|
+
<text x="30" y="105" fill="${c.textDim}" font-size="11" ${F}>LVL ${level.level} \u2022 ${escXml(level.name)}</text>
|
|
83
120
|
<rect x="160" y="96" width="120" height="8" rx="4" fill="${c.border}"/>
|
|
84
121
|
<rect x="160" y="96" width="${level.level * 12}" height="8" rx="4" fill="url(#accent)"/>
|
|
85
122
|
|
|
86
123
|
<!-- Separator -->
|
|
87
124
|
<rect x="30" y="116" width="${W - 60}" height="1" fill="${c.border}"/>
|
|
88
125
|
|
|
89
|
-
<!-- Stats
|
|
90
|
-
<text x="30" y="
|
|
91
|
-
<text x="30" y="
|
|
126
|
+
<!-- Stats row 1 -->
|
|
127
|
+
<text x="30" y="${statsY}" fill="${c.textDim}" font-size="10" ${F}>COMMITS</text>
|
|
128
|
+
<text x="30" y="${statsValY}" fill="${c.text}" font-size="18" font-weight="bold" ${F}>${formatNumber(stats.commits)}</text>
|
|
92
129
|
|
|
93
|
-
<text x="140" y="
|
|
94
|
-
<text x="140" y="
|
|
130
|
+
<text x="140" y="${statsY}" fill="${c.textDim}" font-size="10" ${F}>ADDED</text>
|
|
131
|
+
<text x="140" y="${statsValY}" fill="${c.green}" font-size="18" font-weight="bold" ${F}>+${formatNumber(stats.added)}</text>
|
|
95
132
|
|
|
96
|
-
<text x="250" y="
|
|
97
|
-
<text x="250" y="
|
|
133
|
+
<text x="250" y="${statsY}" fill="${c.textDim}" font-size="10" ${F}>REMOVED</text>
|
|
134
|
+
<text x="250" y="${statsValY}" fill="${c.red}" font-size="18" font-weight="bold" ${F}>-${formatNumber(stats.removed)}</text>
|
|
98
135
|
|
|
99
|
-
<text x="370" y="
|
|
100
|
-
<text x="370" y="
|
|
136
|
+
<text x="370" y="${statsY}" fill="${c.textDim}" font-size="10" ${F}>FILES</text>
|
|
137
|
+
<text x="370" y="${statsValY}" fill="${c.text}" font-size="18" font-weight="bold" ${F}>${formatNumber(stats.filesCount)}</text>
|
|
101
138
|
|
|
102
|
-
<!--
|
|
103
|
-
<text x="30" y="
|
|
104
|
-
<text x="30" y="
|
|
139
|
+
<!-- Stats row 2 -->
|
|
140
|
+
<text x="30" y="${stats2Y}" fill="${c.textDim}" font-size="10" ${F}>PEAK HOUR</text>
|
|
141
|
+
<text x="30" y="${stats2ValY}" fill="${c.text}" font-size="14" font-weight="bold" ${F}>${formatHour(stats.peakHour)}</text>
|
|
105
142
|
|
|
106
|
-
<text x="140" y="
|
|
107
|
-
<text x="140" y="
|
|
143
|
+
<text x="140" y="${stats2Y}" fill="${c.textDim}" font-size="10" ${F}>STREAK</text>
|
|
144
|
+
<text x="140" y="${stats2ValY}" fill="${c.yellow}" font-size="14" font-weight="bold" ${F}>${streakData.current}d \u{1F525}</text>
|
|
108
145
|
|
|
109
|
-
<text x="250" y="
|
|
110
|
-
<text x="250" y="
|
|
146
|
+
<text x="250" y="${stats2Y}" fill="${c.textDim}" font-size="10" ${F}>TOP STREAK</text>
|
|
147
|
+
<text x="250" y="${stats2ValY}" fill="${c.text}" font-size="14" font-weight="bold" ${F}>${streakData.longest}d</text>
|
|
111
148
|
|
|
112
|
-
<text x="370" y="
|
|
113
|
-
<text x="370" y="
|
|
149
|
+
<text x="370" y="${stats2Y}" fill="${c.textDim}" font-size="10" ${F}>NET LINES</text>
|
|
150
|
+
<text x="370" y="${stats2ValY}" fill="${stats.net >= 0 ? c.green : c.red}" font-size="14" font-weight="bold" ${F}>${stats.net >= 0 ? '+' : ''}${formatNumber(stats.net)}</text>
|
|
114
151
|
|
|
115
152
|
<!-- Highlights -->
|
|
116
|
-
${highlights.map((h, i) => `<text x="30" y="${
|
|
153
|
+
${highlights.map((h, i) => `<text x="30" y="${hlY + i * 18}" fill="${c.yellow}" font-size="11" font-style="italic" ${F}>> ${escXml(h)}</text>`).join('\n ')}
|
|
117
154
|
|
|
118
|
-
<!--
|
|
155
|
+
<!-- Languages -->
|
|
156
|
+
<text x="30" y="${langTitleY}" fill="${c.textDim}" font-size="10" ${F}>LANGUAGES</text>
|
|
119
157
|
${langBars}
|
|
120
158
|
${langLabels}
|
|
121
159
|
|
|
122
160
|
<!-- Activity sparkline -->
|
|
123
|
-
<text x="30" y="
|
|
161
|
+
<text x="30" y="${sparkTitleY}" fill="${c.textDim}" font-size="10" ${F}>ACTIVITY BY HOUR</text>
|
|
124
162
|
${sparkline}
|
|
125
163
|
|
|
126
164
|
<!-- Footer -->
|
|
127
|
-
<text x="${W - 30}" y="${
|
|
165
|
+
<text x="${W - 30}" y="${footerY}" fill="${c.textDim}" font-size="9" text-anchor="end" ${F}>generated by git-flex</text>
|
|
128
166
|
</svg>`;
|
|
129
167
|
}
|
|
130
168
|
|
|
131
169
|
function escXml(s) {
|
|
132
170
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
133
171
|
}
|
|
134
|
-
|
|
135
|
-
function truncPath(p, max) {
|
|
136
|
-
if (p.length <= max) return p;
|
|
137
|
-
return '...' + p.slice(-(max - 3));
|
|
138
|
-
}
|
package/src/terminal.js
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import stringWidth from 'string-width';
|
|
2
3
|
import { formatHour, formatNumber } from './stats.js';
|
|
3
4
|
import { getRank, getLevel } from './rank.js';
|
|
4
5
|
import { getHighlights } from './highlights.js';
|
|
5
6
|
|
|
6
7
|
const BOX = { tl: '\u256D', tr: '\u256E', bl: '\u2570', br: '\u256F', h: '\u2500', v: '\u2502' };
|
|
7
|
-
const W =
|
|
8
|
+
const W = 58;
|
|
8
9
|
|
|
9
|
-
function line(
|
|
10
|
-
const
|
|
11
|
-
const pad =
|
|
12
|
-
return `${
|
|
10
|
+
function line(content, width = W) {
|
|
11
|
+
const vis = stringWidth(content);
|
|
12
|
+
const pad = width - 2 - vis;
|
|
13
|
+
return `${chalk.cyan(BOX.v)}${content}${' '.repeat(Math.max(0, pad))}${chalk.cyan(BOX.v)}`;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
function
|
|
16
|
-
return `${BOX.v} ${
|
|
16
|
+
function hr(width = W) {
|
|
17
|
+
return chalk.dim(`${BOX.v}${BOX.h.repeat(width - 2)}${BOX.v}`);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function
|
|
20
|
-
return chalk.
|
|
20
|
+
function top(width = W) {
|
|
21
|
+
return chalk.cyan(`${BOX.tl}${BOX.h.repeat(width - 2)}${BOX.tr}`);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
function bot(width = W) {
|
|
25
|
+
return chalk.cyan(`${BOX.bl}${BOX.h.repeat(width - 2)}${BOX.br}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sep(width = W) {
|
|
29
|
+
return chalk.cyan(`${BOX.v}${BOX.h.repeat(width - 2)}${BOX.v}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function pad(str, len) {
|
|
33
|
+
const vis = stringWidth(str);
|
|
34
|
+
return str + ' '.repeat(Math.max(0, len - vis));
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
function langBar(pct) {
|
|
@@ -35,24 +44,21 @@ export function renderCard(stats, streakData, repoName, periodLabel) {
|
|
|
35
44
|
const level = getLevel(stats.commits);
|
|
36
45
|
const o = [];
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
o.push(chalk.cyan(`${BOX.tl}${BOX.h.repeat(W - 2)}${BOX.tr}`));
|
|
47
|
+
o.push(top());
|
|
40
48
|
|
|
41
49
|
// Header
|
|
42
50
|
const title = ` FLEX \u2022 ${repoName}`;
|
|
43
51
|
const period = periodLabel;
|
|
44
|
-
const headerPad = W - 2 - title
|
|
52
|
+
const headerPad = W - 2 - stringWidth(title) - stringWidth(period);
|
|
45
53
|
o.push(chalk.cyan(BOX.v) + chalk.bold.white(title) + ' '.repeat(Math.max(1, headerPad)) + chalk.dim(period) + chalk.cyan(BOX.v));
|
|
46
54
|
|
|
47
|
-
o.push(
|
|
55
|
+
o.push(sep());
|
|
48
56
|
|
|
49
57
|
// User + rank
|
|
50
|
-
|
|
51
|
-
o.push(line(chalk.cyan(BOX.v), userLine, chalk.cyan(BOX.v)));
|
|
58
|
+
o.push(line(` ${chalk.bold.yellow(stats.author)} ${rank.icon} ${chalk.italic.magenta(rank.title)}`));
|
|
52
59
|
|
|
53
60
|
// Level bar
|
|
54
|
-
|
|
55
|
-
o.push(line(chalk.cyan(BOX.v), lvlLine, chalk.cyan(BOX.v)));
|
|
61
|
+
o.push(line(` ${chalk.dim('LVL')} ${chalk.white(level.level)} ${chalk.cyan(level.bar)} ${chalk.dim(level.name)}`));
|
|
56
62
|
|
|
57
63
|
o.push(hr());
|
|
58
64
|
|
|
@@ -71,21 +77,21 @@ export function renderCard(stats, streakData, repoName, periodLabel) {
|
|
|
71
77
|
];
|
|
72
78
|
|
|
73
79
|
for (let i = 0; i < col1.length; i++) {
|
|
74
|
-
const left = ` ${
|
|
75
|
-
const right = `${
|
|
80
|
+
const left = ` ${pad(col1[i][0], 10)} ${pad(col1[i][1], 10)}`;
|
|
81
|
+
const right = `${pad(col2[i][0], 11)} ${col2[i][1]}`;
|
|
76
82
|
const full = left + ' ' + right;
|
|
77
|
-
o.push(line(
|
|
83
|
+
o.push(line(full));
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
// Languages
|
|
81
87
|
if (stats.languages.length) {
|
|
82
88
|
o.push(hr());
|
|
83
|
-
o.push(line(
|
|
89
|
+
o.push(line(` ${chalk.bold.white('Languages')}`));
|
|
84
90
|
for (const lang of stats.languages.slice(0, 5)) {
|
|
85
|
-
const lbl =
|
|
91
|
+
const lbl = pad(` ${chalk.white(lang.name)}`, 20);
|
|
86
92
|
const bar = langBar(lang.pct);
|
|
87
93
|
const pctStr = chalk.dim(`${lang.pct}%`);
|
|
88
|
-
o.push(line(
|
|
94
|
+
o.push(line(`${lbl} ${bar} ${pctStr}`));
|
|
89
95
|
}
|
|
90
96
|
}
|
|
91
97
|
|
|
@@ -94,13 +100,11 @@ export function renderCard(stats, streakData, repoName, periodLabel) {
|
|
|
94
100
|
if (highlights.length) {
|
|
95
101
|
o.push(hr());
|
|
96
102
|
for (const h of highlights) {
|
|
97
|
-
o.push(line(
|
|
103
|
+
o.push(line(` ${chalk.yellow('>')} ${chalk.italic.white(h)}`));
|
|
98
104
|
}
|
|
99
105
|
}
|
|
100
106
|
|
|
101
|
-
|
|
102
|
-
o.push(chalk.cyan(`${BOX.bl}${BOX.h.repeat(W - 2)}${BOX.br}`));
|
|
103
|
-
|
|
107
|
+
o.push(bot());
|
|
104
108
|
return o.join('\n');
|
|
105
109
|
}
|
|
106
110
|
|
|
@@ -108,71 +112,73 @@ export function renderComparison(stats1, stats2, streak1, streak2, repoName) {
|
|
|
108
112
|
const rank1 = getRank(stats1, streak1);
|
|
109
113
|
const rank2 = getRank(stats2, streak2);
|
|
110
114
|
const o = [];
|
|
111
|
-
const
|
|
115
|
+
const CW = 64;
|
|
112
116
|
|
|
113
|
-
o.push(
|
|
114
|
-
o.push(line(chalk.
|
|
115
|
-
o.push(
|
|
117
|
+
o.push(top(CW));
|
|
118
|
+
o.push(line(chalk.bold.white(` FLEX VS \u2022 ${repoName}`), CW));
|
|
119
|
+
o.push(sep(CW));
|
|
116
120
|
|
|
117
121
|
function vsLine(label, v1, v2) {
|
|
118
|
-
return line(chalk.
|
|
119
|
-
` ${padRight(chalk.dim(label), 12)} ${padRight(v1, 14)} ${chalk.dim('vs')} ${padRight(v2, 14)}`,
|
|
120
|
-
chalk.cyan(BOX.v));
|
|
122
|
+
return line(` ${pad(chalk.dim(label), 12)} ${pad(v1, 14)} ${chalk.dim('vs')} ${pad(v2, 14)}`, CW);
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
// Names
|
|
124
125
|
o.push(vsLine('', chalk.bold.yellow(stats1.author), chalk.bold.blue(stats2.author)));
|
|
125
126
|
o.push(vsLine('Rank', chalk.magenta(`${rank1.icon} ${rank1.title}`), chalk.magenta(`${rank2.icon} ${rank2.title}`)));
|
|
126
|
-
o.push(hr());
|
|
127
|
+
o.push(hr(CW));
|
|
127
128
|
o.push(vsLine('Commits', chalk.white(formatNumber(stats1.commits)), chalk.white(formatNumber(stats2.commits))));
|
|
128
129
|
o.push(vsLine('Added', chalk.green('+' + formatNumber(stats1.added)), chalk.green('+' + formatNumber(stats2.added))));
|
|
129
130
|
o.push(vsLine('Removed', chalk.red('-' + formatNumber(stats1.removed)), chalk.red('-' + formatNumber(stats2.removed))));
|
|
130
131
|
o.push(vsLine('Files', chalk.white(formatNumber(stats1.filesCount)), chalk.white(formatNumber(stats2.filesCount))));
|
|
131
132
|
o.push(vsLine('Streak', chalk.yellow(`${streak1.current}d`), chalk.yellow(`${streak2.current}d`)));
|
|
132
133
|
|
|
133
|
-
o.push(
|
|
134
|
+
o.push(bot(CW));
|
|
134
135
|
return o.join('\n');
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
export function renderTeam(authors, repoName, periodLabel) {
|
|
138
139
|
const o = [];
|
|
139
|
-
const
|
|
140
|
+
const TW = 76;
|
|
141
|
+
|
|
142
|
+
o.push(top(TW));
|
|
143
|
+
o.push(line(chalk.bold.white(` FLEX TEAM \u2022 ${repoName} \u2022 ${periodLabel}`), TW));
|
|
144
|
+
o.push(sep(TW));
|
|
140
145
|
|
|
141
|
-
|
|
142
|
-
o.push(line(chalk.
|
|
143
|
-
o.push(
|
|
146
|
+
// Header row
|
|
147
|
+
o.push(line(` ${pad('', 4)} ${pad(chalk.dim('Name'), 18)} ${pad(chalk.dim('Commits'), 8)} ${pad(chalk.dim('Added'), 9)} ${pad(chalk.dim('Removed'), 9)} ${pad(chalk.dim('Net'), 9)}`, TW));
|
|
148
|
+
o.push(hr(TW));
|
|
144
149
|
|
|
145
150
|
const medals = ['\u{1F947}', '\u{1F948}', '\u{1F949}'];
|
|
146
|
-
const maxCommits = authors[0]?.commits || 1;
|
|
147
151
|
|
|
148
152
|
for (let i = 0; i < Math.min(authors.length, 15); i++) {
|
|
149
153
|
const a = authors[i];
|
|
150
154
|
const medal = medals[i] || chalk.dim(`${i + 1}.`);
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
const name = pad(chalk.white(a.name), 18);
|
|
156
|
+
const commits = pad(chalk.bold.white(formatNumber(a.commits)), 8);
|
|
157
|
+
const added = pad(chalk.green('+' + formatNumber(a.added)), 9);
|
|
158
|
+
const removed = pad(chalk.red('-' + formatNumber(a.removed)), 9);
|
|
159
|
+
const net = a.net >= 0
|
|
160
|
+
? chalk.green('+' + formatNumber(a.net))
|
|
161
|
+
: chalk.red(formatNumber(a.net));
|
|
162
|
+
o.push(line(` ${pad(medal, 4)} ${name} ${commits} ${added} ${removed} ${pad(net, 9)}`, TW));
|
|
156
163
|
}
|
|
157
164
|
|
|
158
|
-
o.push(
|
|
165
|
+
o.push(bot(TW));
|
|
159
166
|
return o.join('\n');
|
|
160
167
|
}
|
|
161
168
|
|
|
162
169
|
export function renderStreak(streakData, stats, repoName) {
|
|
163
170
|
const o = [];
|
|
164
|
-
o.push(
|
|
165
|
-
o.push(line(chalk.
|
|
166
|
-
o.push(
|
|
171
|
+
o.push(top());
|
|
172
|
+
o.push(line(chalk.bold.white(` FLEX STREAK \u2022 ${repoName}`)));
|
|
173
|
+
o.push(sep());
|
|
167
174
|
|
|
168
|
-
o.push(line(
|
|
169
|
-
o.push(line(
|
|
170
|
-
o.push(line(
|
|
175
|
+
o.push(line(` ${chalk.dim('Author')} ${chalk.bold.yellow(stats.author)}`));
|
|
176
|
+
o.push(line(` ${chalk.dim('Current')} ${chalk.bold.yellow(`${streakData.current} days`)} ${streakData.current >= 7 ? '\u{1F525}' : ''}`));
|
|
177
|
+
o.push(line(` ${chalk.dim('Longest')} ${chalk.bold.white(`${streakData.longest} days`)} \u{1F3C6}`));
|
|
171
178
|
|
|
172
|
-
// Visual streak bar
|
|
173
179
|
const days = Math.min(streakData.current, 30);
|
|
174
180
|
const bar = chalk.yellow('\u2588'.repeat(days)) + chalk.dim('\u2591'.repeat(30 - days));
|
|
175
|
-
o.push(line(
|
|
181
|
+
o.push(line(` ${bar}`));
|
|
176
182
|
|
|
177
183
|
let msg = '';
|
|
178
184
|
if (streakData.current === 0) msg = 'Start a streak today!';
|
|
@@ -181,30 +187,31 @@ export function renderStreak(streakData, stats, repoName) {
|
|
|
181
187
|
else if (streakData.current >= 7) msg = 'One week strong!';
|
|
182
188
|
else if (streakData.current >= 3) msg = 'Building momentum...';
|
|
183
189
|
else msg = 'Keep it going!';
|
|
184
|
-
o.push(line(
|
|
190
|
+
o.push(line(` ${chalk.italic.dim(msg)}`));
|
|
185
191
|
|
|
186
|
-
o.push(
|
|
192
|
+
o.push(bot());
|
|
187
193
|
return o.join('\n');
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
export function renderLangs(stats, repoName, periodLabel) {
|
|
191
197
|
const o = [];
|
|
192
|
-
|
|
193
|
-
o.push(
|
|
194
|
-
o.push(chalk.
|
|
198
|
+
const LW = 62;
|
|
199
|
+
o.push(top(LW));
|
|
200
|
+
o.push(line(chalk.bold.white(` FLEX LANGS \u2022 ${repoName} \u2022 ${periodLabel}`), LW));
|
|
201
|
+
o.push(sep(LW));
|
|
195
202
|
|
|
196
203
|
if (!stats.languages.length) {
|
|
197
|
-
o.push(line(chalk.
|
|
204
|
+
o.push(line(chalk.dim(' No language data found'), LW));
|
|
198
205
|
} else {
|
|
199
206
|
for (const lang of stats.languages) {
|
|
200
|
-
const lbl =
|
|
207
|
+
const lbl = pad(` ${chalk.bold.white(lang.name)}`, 16);
|
|
201
208
|
const bar = langBar(lang.pct);
|
|
202
|
-
const pct =
|
|
209
|
+
const pct = pad(chalk.yellow(`${lang.pct}%`), 5);
|
|
203
210
|
const cnt = chalk.dim(`(${lang.count} files)`);
|
|
204
|
-
o.push(line(
|
|
211
|
+
o.push(line(`${lbl} ${bar} ${pct} ${cnt}`, LW));
|
|
205
212
|
}
|
|
206
213
|
}
|
|
207
214
|
|
|
208
|
-
o.push(
|
|
215
|
+
o.push(bot(LW));
|
|
209
216
|
return o.join('\n');
|
|
210
217
|
}
|