reslop 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.
@@ -10,21 +10,33 @@ const { ACTION_IDS, BRANCH_ACTIONS, COMMIT_ACTIONS, actionLetter } = keys;
10
10
  const { buttonWord } = keys;
11
11
  const { FILES_HIDDEN, FILES_GIT_DISABLED, BRANCHES_DISABLED } = keys;
12
12
  const { COMMITS_DISABLED, TODO_DISABLED, DIFF_DISABLED, UNIT_DISABLED } = keys;
13
+ const { NPM_DISABLED } = keys;
13
14
  const { diffGitDisabled, branchGitDisabled, commitGitDisabled } = keys;
14
15
  const { CONFIRM, confirmPrefix, promptFromChoices } = keys;
15
16
  const { THEME, paint, visibleWidth } = ansi;
16
17
  const { truncateVisible, fg, RESET, EL, seq } = ansi;
17
- const { fill, branchLabel, paintBracketName } = primitives;
18
+ const { fill, branchLabel } = primitives;
18
19
 
19
20
  const HEADER_ROLE_FG = {
20
21
  repo: THEME.headerRepoFg,
21
22
  dir: THEME.headerDirFg,
22
23
  slash: THEME.headerSlashFg,
23
24
  file: THEME.headerFileFg,
24
- meta: THEME.headerMetaFg,
25
25
  };
26
26
 
27
- const BUSY_BAR = ['▰▰▱▱▱▱', '▱▰▰▱▱▱', '▱▱▰▰▱▱', '▱▱▱▰▰▱', '▱▱▱▱▰▰', '▰▱▱▱▱▰'];
27
+ const WAVE = '·•●•·';
28
+
29
+ const busyBars = (wave) => {
30
+ const n = wave.length;
31
+ const frames = new Array(n);
32
+ for (let frame = 0; frame < n; frame++) {
33
+ const start = (n - frame) % n;
34
+ frames[frame] = wave.slice(start) + wave.slice(0, start);
35
+ }
36
+ return frames;
37
+ };
38
+
39
+ const BUSY_BAR = busyBars(WAVE);
28
40
  const BUSY_PROGRESS = [
29
41
  'pulling',
30
42
  'pushing',
@@ -34,6 +46,7 @@ const BUSY_PROGRESS = [
34
46
  'npm i',
35
47
  'npm uninstall',
36
48
  'npm audit fix',
49
+ 'running',
37
50
  'checking out',
38
51
  'creating branch',
39
52
  'rebasing',
@@ -105,21 +118,12 @@ const dropPrompt = (name) => ` drop ${name}? y/n`;
105
118
  const PUSH_REJECT_PREFIX = CONFIRM.confirmPush.prefix;
106
119
  const PUSH_PROMPT = `${PUSH_REJECT_PREFIX} force push`;
107
120
 
121
+ const BUTTON_LEAD = ' ';
108
122
  const BUTTON_GAP = ' ';
123
+ const BUTTON_EDGE = 1;
124
+ const buttonGap = (index) => (index ? BUTTON_GAP : BUTTON_LEAD);
109
125
  const LOGO = '👁️';
110
126
 
111
- const ORIGIN_LABEL = {
112
- commit: (view) => view.revShort || 'commit',
113
- pr: (view) => view.sourceLabel || 'pr',
114
- todo: () => 'todo',
115
- };
116
-
117
- const originLabel = (origin, view) => {
118
- const named = ORIGIN_LABEL[origin];
119
- if (named) return named(view);
120
- return origin ?? 'unknown';
121
- };
122
-
123
127
  const pathSegments = (rel) => {
124
128
  const segs = [];
125
129
  if (!rel) return segs;
@@ -137,84 +141,14 @@ const pathSegments = (rel) => {
137
141
  return segs;
138
142
  };
139
143
 
140
- const headerTarget = (view) => {
141
- const pane = view.pane ?? 'diff';
142
- if (pane === 'files') {
143
- const files = view.files ?? [];
144
- const cursor = view.fileCursor ?? 0;
145
- const entry = files[cursor];
146
- const total = files.length;
147
- const n = total ? cursor + 1 : 0;
148
- if (!entry) return { path: '', origin: '', n, total };
149
- if (isTodosEntry(entry)) {
150
- return { path: TODO_FILE, origin: 'todo', n, total };
151
- }
152
- return { path: entry.path, origin: entry.status, n, total };
153
- }
154
- if (pane === 'branches') {
155
- const compose = view.compose;
156
- const branches = view.branches ?? [];
157
- if (compose && compose.kind === 'branch') {
158
- const total = branches.length + 1;
159
- return { path: compose.text || '', origin: 'new', n: total, total };
160
- }
161
- const cursor = view.branchCursor ?? 0;
162
- const entry = branches[cursor];
163
- const total = branches.length;
164
- const n = total ? cursor + 1 : 0;
165
- if (!entry) return { path: '', origin: 'branch', n, total };
166
- const origin = entry.current ? 'current' : 'branch';
167
- return { path: entry.name, origin, n, total };
168
- }
169
- if (pane === 'commits') {
170
- const compose = view.compose;
171
- const commits = view.commits ?? [];
172
- if (compose && compose.kind === 'commit') {
173
- const path = `${compose.text || ''}`.replaceAll('\n', ' ');
174
- if (compose.commitKind === 'amend' || compose.commitKind === 'reword') {
175
- const cursor = view.commitCursor ?? 0;
176
- const entry = commits[cursor];
177
- const total = commits.length;
178
- const n = total ? cursor + 1 : 0;
179
- if (!entry) return { path, origin: 'commit', n, total };
180
- const origin = entry.head ? 'HEAD' : 'commit';
181
- return { path, origin, n, total };
182
- }
183
- const total = commits.length + 1;
184
- return { path, origin: 'new', n: 1, total };
185
- }
186
- const cursor = view.commitCursor ?? 0;
187
- const entry = commits[cursor];
188
- const total = commits.length;
189
- const n = total ? cursor + 1 : 0;
190
- if (!entry) return { path: '', origin: 'commit', n, total };
191
- const origin = entry.head ? 'HEAD' : 'commit';
192
- return { path: entry.subject ?? '', origin, n, total };
193
- }
194
- if (pane === 'unit') {
195
- const path = view.reviewPath ?? '';
196
- const lines = view.unitLines ?? [];
197
- const top = Math.max(0, (view.scroll ?? 0) - 1);
198
- const n = lines.length ? Math.min(lines.length, top + 1) : 0;
199
- const origin = view.item ? originLabel(view.item.origin, view) : 'unit';
200
- return { path, origin, n, total: lines.length };
201
- }
202
- if (!view.item) return { path: '', origin: '', n: 0, total: view.total ?? 0 };
203
- if (view.item.origin === 'todo') {
204
- return { path: TODO_FILE, origin: 'todo', n: 1, total: 1 };
205
- }
206
- return {
207
- path: itemPath(view.item),
208
- origin: originLabel(view.item.origin, view),
209
- n: view.index + 1,
210
- total: view.total,
211
- };
212
- };
213
-
214
144
  const headerScreen = (view) => {
215
145
  const pane = view.pane ?? 'diff';
216
146
  if (pane === 'branches') return 'branches';
217
- if (pane === 'commits') return 'commits';
147
+ if (pane === 'commits') {
148
+ const mode = view.commitView === 'full' ? 'full' : 'brief';
149
+ return `commits ${mode}`;
150
+ }
151
+ if (pane === 'npm') return 'npm';
218
152
  if (pane === 'files') {
219
153
  const files = view.files ?? [];
220
154
  const entry = files[view.fileCursor ?? 0];
@@ -225,38 +159,41 @@ const headerScreen = (view) => {
225
159
  return '';
226
160
  };
227
161
 
228
- const headerLabel = (view, target) => headerScreen(view) || target.path;
162
+ const headerTarget = (view) => {
163
+ const pane = view.pane ?? 'diff';
164
+ if (pane === 'files') {
165
+ const entry = (view.files ?? [])[view.fileCursor ?? 0];
166
+ if (!entry || isTodosEntry(entry)) return '';
167
+ return entry.path;
168
+ }
169
+ if (pane === 'unit') return view.reviewPath ?? '';
170
+ if (!view.item || view.item.origin === 'todo') return '';
171
+ return itemPath(view.item);
172
+ };
173
+
174
+ const headerLabel = (view) => headerScreen(view) || headerTarget(view);
229
175
 
230
176
  const headerSep = (view) => (headerScreen(view) ? ': ' : '/');
231
177
 
232
- const headerPrefix = (view) => {
178
+ const headerPrefix = (view, label) => {
233
179
  const repo = view.repoName || '';
234
- const target = headerTarget(view);
235
- const label = headerLabel(view, target);
236
180
  if (repo && label) return ` ${LOGO} reslop: ${repo}${headerSep(view)}`;
237
181
  if (repo) return ` ${LOGO} reslop: ${repo} `;
238
182
  return ` ${LOGO} reslop: `;
239
183
  };
240
184
 
241
- const headerSuffix = ({ origin, n, total }) =>
242
- origin ? ` ${origin} ${n}/${total}` : ` ${n}/${total}`;
243
-
244
185
  const headerText = (view) => {
245
- const prefix = headerPrefix(view);
246
- const target = headerTarget(view);
247
- const label = headerLabel(view, target);
248
- return `${prefix}${label}${headerSuffix(target)}`;
186
+ const label = headerLabel(view);
187
+ return `${headerPrefix(view, label)}${label}`;
249
188
  };
250
189
 
251
190
  const headerPieces = (view, width) => {
252
191
  const repo = view.repoName || '';
253
- const target = headerTarget(view);
254
- const path = headerLabel(view, target);
255
- const suffix = headerSuffix(target);
192
+ const path = headerLabel(view);
256
193
  const right = ' ';
257
194
  const chrome = ` ${LOGO} reslop: `;
258
- const prefixW = visibleWidth(headerPrefix(view));
259
- const used = prefixW + visibleWidth(suffix) + visibleWidth(right);
195
+ const prefixW = visibleWidth(headerPrefix(view, path));
196
+ const used = prefixW + visibleWidth(right);
260
197
  const room = Math.max(0, width - used);
261
198
  const clipped = truncateVisible(path, room);
262
199
  const pad = Math.max(0, room - visibleWidth(clipped));
@@ -277,7 +214,6 @@ const headerPieces = (view, width) => {
277
214
  if (pad) {
278
215
  pieces.push({ text: ' '.repeat(pad), role: 'chrome' });
279
216
  }
280
- pieces.push({ text: suffix, role: 'meta' });
281
217
  pieces.push({ text: right, role: 'chrome' });
282
218
  return pieces;
283
219
  };
@@ -340,6 +276,7 @@ const layoutButtons = (width, compact, hidden, extra, disabled) => {
340
276
  for (const add of extraIds) {
341
277
  if (!ids.includes(add)) ids.push(add);
342
278
  }
279
+ continue;
343
280
  }
344
281
  if (hiddenIds.includes(id)) continue;
345
282
  ids.push(id);
@@ -348,13 +285,15 @@ const layoutButtons = (width, compact, hidden, extra, disabled) => {
348
285
  const hits = [];
349
286
  const parts = [];
350
287
  let x = 0;
288
+ const limit = Math.max(0, width - BUTTON_EDGE);
351
289
  for (const id of ids) {
352
290
  const letter = actionLetter(id);
353
291
  const word = buttonWord(id);
354
292
  const label = short ? letter : word;
355
- const piece = `${BUTTON_GAP}${label}`;
293
+ const gap = buttonGap(parts.length);
294
+ const piece = `${gap}${label}`;
356
295
  const w = visibleWidth(piece);
357
- if (x + w > width && parts.length) break;
296
+ if (x + w > limit && parts.length) break;
358
297
  const off = disabledIds.includes(id);
359
298
  if (!off) hits.push({ id, x0: x, x1: x + w });
360
299
  parts.push({ action: { id }, piece, letter, label, disabled: off });
@@ -373,8 +312,10 @@ const paintButtons = (layout, width, color) => {
373
312
  let used = 0;
374
313
  const fgRgb = THEME.buttonFg;
375
314
  const bgRgb = THEME.buttonBg;
376
- for (const part of layout.parts) {
377
- out += paint(BUTTON_GAP, fgRgb, bgRgb, color);
315
+ for (let i = 0; i < layout.parts.length; i++) {
316
+ const part = layout.parts[i];
317
+ const gap = buttonGap(i);
318
+ out += paint(gap, fgRgb, bgRgb, color);
378
319
  if (part.disabled) out += paint(part.label, fgRgb, bgRgb, color);
379
320
  else out += paintMarkedWord(part.label, part.letter, color, BUTTON_MARK);
380
321
  used += visibleWidth(part.piece);
@@ -512,14 +453,10 @@ const paintChoicePrompt = (choices, width, color, prefix = '') => {
512
453
  return out + fill(pad, THEME.mutedFg, bgRgb, color);
513
454
  };
514
455
 
515
- const STATUS_MARK = /(\[[^\]]+\]|\+[0-9]+(?:\/[0-9]+)?|-[0-9]+(?:\/[0-9]+)?)/g;
456
+ const STATUS_MARK = /(\+[0-9]+(?:\/[0-9]+)?|-[0-9]+(?:\/[0-9]+)?)/g;
516
457
 
517
458
  const paintStatusMark = (token, color) => {
518
459
  const bgRgb = THEME.chromeBg;
519
- const muted = THEME.mutedFg;
520
- if (token.startsWith('[')) {
521
- return paintBracketName(token, THEME.buttonHotFg, muted, bgRgb, color);
522
- }
523
460
  if (token.startsWith('+')) {
524
461
  return paint(token, THEME.addLineFg, bgRgb, color);
525
462
  }
@@ -547,6 +484,68 @@ const paintStatusLeft = (text, color) => {
547
484
  return out;
548
485
  };
549
486
 
487
+ const paintBranchHead = (text, branch, color) => {
488
+ const name = branchLabel(branch);
489
+ if (!name || !text.startsWith(' ')) return null;
490
+ const head = ` ${name}`;
491
+ const shown = text.startsWith(head) ? name : text.slice(1);
492
+ if (!shown || !name.startsWith(shown)) return null;
493
+ const rest = text.slice(1 + shown.length);
494
+ const bgRgb = THEME.chromeBg;
495
+ return (
496
+ paint(' ', THEME.mutedFg, bgRgb, color) +
497
+ paint(shown, THEME.buttonHotFg, bgRgb, color, true) +
498
+ paintStatusLeft(rest, color)
499
+ );
500
+ };
501
+
502
+ const SAVE_HINT = 'Press **Ctrl-S** or **Enter** at EOF to save';
503
+ const HINT_HOT_FG = 0xff;
504
+
505
+ const hintParts = (source) => {
506
+ const bits = source.split('**');
507
+ const parts = [];
508
+ for (let i = 0; i < bits.length; i++) {
509
+ if (!bits[i]) continue;
510
+ parts.push({ text: bits[i], hot: i % 2 === 1 });
511
+ }
512
+ return parts;
513
+ };
514
+
515
+ const saveHintText = () => SAVE_HINT.replaceAll('**', '');
516
+
517
+ const commitSaveHint = (view) => {
518
+ if (view.mode !== 'compose' || view.commitView !== 'full') return false;
519
+ const compose = view.compose;
520
+ return Boolean(compose && compose.kind === 'commit');
521
+ };
522
+
523
+ const paintSaveHintBar = (width, color) => {
524
+ const bgRgb = THEME.buttonBg;
525
+ const plain = `${BUTTON_LEAD}${saveHintText()}`;
526
+ if (!color) {
527
+ const pad = Math.max(0, width - visibleWidth(plain));
528
+ return plain + ' '.repeat(pad);
529
+ }
530
+ let out = paint(BUTTON_LEAD, THEME.buttonFg, bgRgb, color);
531
+ for (const part of hintParts(SAVE_HINT)) {
532
+ const tone = part.hot ? HINT_HOT_FG : THEME.buttonFg;
533
+ out += paint(part.text, tone, bgRgb, color, part.hot);
534
+ }
535
+ const pad = Math.max(0, width - visibleWidth(plain));
536
+ return out + fill(pad, THEME.buttonFg, bgRgb, color);
537
+ };
538
+
539
+ const paintFooter = (view, layout, width, color) => {
540
+ if (commitSaveHint(view)) return paintSaveHintBar(width, color);
541
+ return paintButtons(layout, width, color);
542
+ };
543
+
544
+ const footerHits = (view, layout) => {
545
+ if (commitSaveHint(view)) return [];
546
+ return layout.hits;
547
+ };
548
+
550
549
  const paintStatusLine = (view, status, width, color) => {
551
550
  const confirm = CONFIRM[view.mode];
552
551
  if (confirm) {
@@ -560,8 +559,13 @@ const paintStatusLine = (view, status, width, color) => {
560
559
  return layout.clipped + leftGap + layout.msg + rightGap + layout.right;
561
560
  }
562
561
  const bgRgb = THEME.chromeBg;
562
+ const worktree = countsKind(view.sourceKind, view.revShort) === 'worktree';
563
+ const head = worktree
564
+ ? paintBranchHead(layout.clipped, view.branch, color)
565
+ : null;
566
+ const left = head ?? paintStatusLeft(layout.clipped, color);
563
567
  return (
564
- paintStatusLeft(layout.clipped, color) +
568
+ left +
565
569
  paint(leftGap, THEME.mutedFg, bgRgb, color) +
566
570
  paint(layout.msg, layout.msgFg, bgRgb, color) +
567
571
  paint(rightGap, THEME.mutedFg, bgRgb, color) +
@@ -583,10 +587,10 @@ const statusBranchHit = (view, width, y) => {
583
587
  if (!branchLabel(branch)) return null;
584
588
  if (countsKind(view.sourceKind, view.revShort) !== 'worktree') return null;
585
589
  const layout = statusLayout(view, view.status ?? '', width);
586
- const open = layout.clipped.indexOf('[');
587
- const close = layout.clipped.indexOf(']');
588
- if (open < 0 || close <= open) return null;
589
- return { id: 'branch', y, x0: open, x1: close + 1 };
590
+ const name = branchLabel(branch);
591
+ const at = layout.clipped.indexOf(name);
592
+ if (at < 0) return null;
593
+ return { id: 'branch', y, x0: at, x1: at + visibleWidth(name) };
590
594
  };
591
595
 
592
596
  const selectedBranch = (view) => (view.branches ?? [])[view.branchCursor ?? 0];
@@ -599,6 +603,7 @@ const hiddenActions = (view) => {
599
603
  if (pane === 'unit') return UNIT_DISABLED;
600
604
  if (pane === 'branches') return BRANCHES_DISABLED;
601
605
  if (pane === 'commits') return COMMITS_DISABLED;
606
+ if (pane === 'npm') return NPM_DISABLED;
602
607
  if (view.item && view.item.origin === 'todo') return TODO_DISABLED;
603
608
  return DIFF_DISABLED;
604
609
  };
@@ -619,6 +624,10 @@ const dimFooterActions = (view) => {
619
624
  if (!entry) return commitGitDisabled({ canCommit: staged });
620
625
  return commitGitDisabled({ ...entry, canCommit: staged });
621
626
  }
627
+ if (pane === 'npm' && view.npmView) {
628
+ if (view.npmRunning) return ['npmRerun'];
629
+ return [];
630
+ }
622
631
  return [];
623
632
  };
624
633
 
@@ -626,10 +635,14 @@ const extraFooterActions = (view) => {
626
635
  const pane = view.pane ?? 'diff';
627
636
  if (pane === 'files') {
628
637
  const scope = view.fileScope === 'file' ? 'diff' : 'file';
629
- return [scope, 'commit', 'pull', 'push'];
638
+ return ['npm', scope, 'commit', 'pull', 'push'];
630
639
  }
631
640
  if (pane === 'branches') return Object.keys(BRANCH_ACTIONS);
632
641
  if (pane === 'commits') return Object.keys(COMMIT_ACTIONS);
642
+ if (pane === 'npm') {
643
+ if (view.npmView) return ['npmStop', 'npmRerun'];
644
+ return ['npmEdit', 'npmNew'];
645
+ }
633
646
  return [];
634
647
  };
635
648
 
@@ -643,6 +656,8 @@ module.exports = {
643
656
  paintHeader,
644
657
  layoutButtons,
645
658
  paintButtons,
659
+ paintFooter,
660
+ footerHits,
646
661
  paintStatusLine,
647
662
  statusChoiceHits,
648
663
  statusBranchHit,