reslop 0.2.0 → 0.2.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.
@@ -39,6 +39,7 @@ const BUSY_PROGRESS = [
39
39
  'rebasing',
40
40
  'dropping',
41
41
  'committing',
42
+ 'rewording',
42
43
  'force pushing',
43
44
  ];
44
45
 
@@ -62,6 +63,8 @@ const INFO_STATUS = [
62
63
  'unified',
63
64
  'mixed',
64
65
  'side-by-side',
66
+ 'dark',
67
+ 'light',
65
68
  'saved',
66
69
  'reloaded',
67
70
  'not a diff block',
@@ -69,6 +72,7 @@ const INFO_STATUS = [
69
72
  'diff mode',
70
73
  'committed',
71
74
  'amended',
75
+ 'reworded',
72
76
  'fixup',
73
77
  'updated',
74
78
  'updating reslop',
@@ -91,6 +95,7 @@ const INFO_STATUS = [
91
95
  'checked out',
92
96
  'created',
93
97
  'empty branch name',
98
+ 'empty commit message',
94
99
  ];
95
100
  const INFO_PREFIX = ['checked out ', 'created ', 'rebased onto ', 'dropped '];
96
101
 
@@ -162,7 +167,22 @@ const headerTarget = (view) => {
162
167
  return { path: entry.name, origin, n, total };
163
168
  }
164
169
  if (pane === 'commits') {
170
+ const compose = view.compose;
165
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
+ }
166
186
  const cursor = view.commitCursor ?? 0;
167
187
  const entry = commits[cursor];
168
188
  const total = commits.length;
@@ -191,10 +211,29 @@ const headerTarget = (view) => {
191
211
  };
192
212
  };
193
213
 
214
+ const headerScreen = (view) => {
215
+ const pane = view.pane ?? 'diff';
216
+ if (pane === 'branches') return 'branches';
217
+ if (pane === 'commits') return 'commits';
218
+ if (pane === 'files') {
219
+ const files = view.files ?? [];
220
+ const entry = files[view.fileCursor ?? 0];
221
+ if (isTodosEntry(entry)) return TODO_FILE;
222
+ return '';
223
+ }
224
+ if (view.item && view.item.origin === 'todo') return TODO_FILE;
225
+ return '';
226
+ };
227
+
228
+ const headerLabel = (view, target) => headerScreen(view) || target.path;
229
+
230
+ const headerSep = (view) => (headerScreen(view) ? ': ' : '/');
231
+
194
232
  const headerPrefix = (view) => {
195
233
  const repo = view.repoName || '';
196
- const { path } = headerTarget(view);
197
- if (repo && path) return ` ${LOGO} reslop: ${repo}/`;
234
+ const target = headerTarget(view);
235
+ const label = headerLabel(view, target);
236
+ if (repo && label) return ` ${LOGO} reslop: ${repo}${headerSep(view)}`;
198
237
  if (repo) return ` ${LOGO} reslop: ${repo} `;
199
238
  return ` ${LOGO} reslop: `;
200
239
  };
@@ -205,13 +244,14 @@ const headerSuffix = ({ origin, n, total }) =>
205
244
  const headerText = (view) => {
206
245
  const prefix = headerPrefix(view);
207
246
  const target = headerTarget(view);
208
- return `${prefix}${target.path}${headerSuffix(target)}`;
247
+ const label = headerLabel(view, target);
248
+ return `${prefix}${label}${headerSuffix(target)}`;
209
249
  };
210
250
 
211
251
  const headerPieces = (view, width) => {
212
252
  const repo = view.repoName || '';
213
253
  const target = headerTarget(view);
214
- const path = target.path;
254
+ const path = headerLabel(view, target);
215
255
  const suffix = headerSuffix(target);
216
256
  const right = ' ';
217
257
  const chrome = ` ${LOGO} reslop: `;
@@ -224,7 +264,9 @@ const headerPieces = (view, width) => {
224
264
  if (repo) {
225
265
  pieces.push({ text: repo, role: 'repo' });
226
266
  if (path) {
227
- pieces.push({ text: '/', role: 'slash' });
267
+ const sep = headerSep(view);
268
+ const role = sep === '/' ? 'slash' : 'chrome';
269
+ pieces.push({ text: sep, role });
228
270
  } else {
229
271
  pieces.push({ text: ' ', role: 'chrome' });
230
272
  }
@@ -55,10 +55,72 @@ const paintCommitRow = (entry, width, color, selected, cols) => {
55
55
  return out + edge;
56
56
  };
57
57
 
58
+ const paintCommitEdit = (compose, width, color, entry, cols) => {
59
+ const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
60
+ if (entry && cols) {
61
+ const row = paintCommitRow(
62
+ { ...entry, subject: text },
63
+ width,
64
+ color,
65
+ true,
66
+ cols,
67
+ );
68
+ const inner = Math.max(1, width - COMMIT_EDGE - COMMIT_TAIL);
69
+ const prefix = ` ${FILE_MARK} `;
70
+ const lead = ' ';
71
+ const parts = padColumns(commitRowFields(entry), cols, COMMIT_COL_ALIGN);
72
+ const suffix = joinColumns(parts, cols);
73
+ const rest = Math.max(1, inner - visibleWidth(prefix + lead + suffix));
74
+ const clipped = truncateVisible(text, rest);
75
+ const prefixW = visibleWidth(prefix + lead);
76
+ const col = Math.min(compose.cursor ?? 0, visibleWidth(clipped));
77
+ return {
78
+ row,
79
+ cursor: { x: prefixW + col + 1 },
80
+ at: 'replace',
81
+ };
82
+ }
83
+ const inner = Math.max(1, width - COMMIT_EDGE);
84
+ const prefix = ` ${FILE_MARK} `;
85
+ const lead = ' ';
86
+ const prefixW = visibleWidth(prefix + lead);
87
+ const rest = Math.max(1, inner - prefixW);
88
+ const clipped = truncateVisible(text, rest);
89
+ const pad = Math.max(0, rest - visibleWidth(clipped));
90
+ const gap = ' '.repeat(pad);
91
+ const fgRgb = THEME.chromeFg;
92
+ const bgRgb = THEME.buttonBg;
93
+ const nameFg = THEME.buttonHotFg;
94
+ const edge = fill(COMMIT_EDGE, fgRgb, THEME.ctxBg, color);
95
+ let row = `${prefix}${lead}${clipped}${gap}${edge}`;
96
+ if (color) {
97
+ row = `${seq(fgRgb, bgRgb)}${EL}`;
98
+ row += paint(prefix, fgRgb, bgRgb, color);
99
+ row += paint(lead, nameFg, bgRgb, color);
100
+ row += paint(clipped, nameFg, bgRgb, color);
101
+ row += paint(gap, fgRgb, bgRgb, color);
102
+ row += edge;
103
+ }
104
+ const col = Math.min(compose.cursor ?? 0, visibleWidth(clipped));
105
+ return { row, cursor: { x: prefixW + col + 1 }, at: 'start' };
106
+ };
107
+
108
+ const commitEditRow = (compose, commits, cursor, width, color, cols) => {
109
+ if (!compose || compose.kind !== 'commit') return null;
110
+ if (compose.commitKind === 'amend' || compose.commitKind === 'reword') {
111
+ const entry = commits[cursor];
112
+ if (!entry) return null;
113
+ return paintCommitEdit(compose, width, color, entry, cols);
114
+ }
115
+ return paintCommitEdit(compose, width, color);
116
+ };
117
+
58
118
  const paintBodyCommits = (view, width, color, bodyH, headerLines) => {
59
119
  const commits = view.commits ?? [];
60
120
  const cols = measureColumns(commits, commitRowFields, COMMIT_COL_ALIGN);
61
121
  const cursor = view.commitCursor ?? 0;
122
+ const compose = view.compose;
123
+ const extra = commitEditRow(compose, commits, cursor, width, color, cols);
62
124
  const paintRow = (entry, rowWidth, rowColor, selected) =>
63
125
  paintCommitRow(entry, rowWidth, rowColor, selected, cols);
64
126
  const painted = paintCursorList(
@@ -69,7 +131,7 @@ const paintBodyCommits = (view, width, color, bodyH, headerLines) => {
69
131
  bodyH,
70
132
  headerLines,
71
133
  paintRow,
72
- null,
134
+ extra,
73
135
  view.listScroll,
74
136
  );
75
137
  return paneResult({
@@ -78,8 +78,8 @@ const STAGED_STYLES = {
78
78
  };
79
79
 
80
80
  const SOFT_WRAP = ['warn', 'note'];
81
- const DEL_CHAR_BG = ansi.bg(THEME.delCharBg);
82
- const ADD_CHAR_BG = ansi.bg(THEME.addCharBg);
81
+ const delCharBg = () => ansi.bg(THEME.delCharBg);
82
+ const addCharBg = () => ansi.bg(THEME.addCharBg);
83
83
 
84
84
  const paintSpans = (pieces, lineStyle, budget, color) => {
85
85
  let out = '';
@@ -298,8 +298,8 @@ const paintBodyDiff = (view, width, color) => {
298
298
  };
299
299
 
300
300
  module.exports = {
301
- DEL_CHAR_BG,
302
- ADD_CHAR_BG,
301
+ delCharBg,
302
+ addCharBg,
303
303
  paintDiffLine,
304
304
  codeInnerWidth,
305
305
  paintBodyDiff,
@@ -88,6 +88,7 @@ const mapBodyCursor = (cursor, shift, scroll, headerLines, shown) => {
88
88
  const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
89
89
  const sourceBody = paintedBody.body;
90
90
  const sourceOwners = paintedBody.todoOwners;
91
+ const sourceChecks = paintedBody.todoChecks ?? [];
91
92
  const splitBody = paintedBody.splitBody === true;
92
93
  const pane = view.pane ?? 'diff';
93
94
  const listPane = LIST_PANES.includes(pane);
@@ -109,6 +110,13 @@ const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
109
110
  for (let i = 0; i < sourceOwners.length; i++) {
110
111
  todoOwners[ownerAt++] = sourceOwners[i];
111
112
  }
113
+ const checkLead = lead && sourceChecks.length;
114
+ const todoChecks = new Array(sourceChecks.length + (checkLead ? 1 : 0));
115
+ let checkAt = 0;
116
+ if (checkLead) todoChecks[checkAt++] = false;
117
+ for (let i = 0; i < sourceChecks.length; i++) {
118
+ todoChecks[checkAt++] = sourceChecks[i];
119
+ }
112
120
  const scrollRaw = view.scroll ?? 0;
113
121
  const scrollMax = Math.max(0, body.length - bodyH);
114
122
  const scroll = listPane ? 0 : Math.min(scrollRaw, scrollMax);
@@ -120,7 +128,11 @@ const windowBody = (view, width, color, bodyH, headerLines, paintedBody) => {
120
128
  for (let i = 0; i < copied; i++) {
121
129
  const owner = todoOwners[scroll + i];
122
130
  if (owner === undefined || owner === null) continue;
123
- todoHits.push({ y: headerLines + i + 1, cursor: owner });
131
+ const y = headerLines + i + 1;
132
+ if (todoChecks[scroll + i]) {
133
+ todoHits.push({ y, cursor: owner, x0: 1, x1: 4, check: true });
134
+ }
135
+ todoHits.push({ y, cursor: owner });
124
136
  }
125
137
  const cursor = mapBodyCursor(
126
138
  paintedBody.cursor,
@@ -11,7 +11,7 @@ const { THEME, visibleWidth, truncateVisible, seq, EL, RESET, BOLD } = ansi;
11
11
  const { paintBar, paintCheckBar, windowRows } = primitives;
12
12
  const { LIST_PANES, CHECK_LEAD, paneResult } = primitives;
13
13
 
14
- const NOTE_COMPOSE = ['feedback', 'commit'];
14
+ const NOTE_COMPOSE = ['feedback'];
15
15
  const INPUT_MAX = 4;
16
16
  const NOTE_MAX = 4;
17
17
  const NOTE_PAD = 1;
@@ -57,6 +57,7 @@ const paintTodoList = (texts, focus, width, color, edit) => {
57
57
  const inner = todoInnerWidth(width);
58
58
  const rows = [];
59
59
  const owners = [];
60
+ const checks = [];
60
61
  let cursor = null;
61
62
  for (let i = 0; i < texts.length; i++) {
62
63
  const selected = i === focus;
@@ -65,14 +66,22 @@ const paintTodoList = (texts, focus, width, color, edit) => {
65
66
  const text = texts[i];
66
67
  const wrapped = wrapTodoItem(text, inner);
67
68
  const rowStart = rows.length;
68
- for (const line of wrapped) {
69
- rows.push(paintCheckBar(` ${line}`, width, fgRgb, bgRgb, color));
69
+ for (let line = 0; line < wrapped.length; line++) {
70
+ const painted = paintCheckBar(
71
+ ` ${wrapped[line]}`,
72
+ width,
73
+ fgRgb,
74
+ bgRgb,
75
+ color,
76
+ );
77
+ rows.push(painted);
70
78
  owners.push(i);
79
+ checks.push(line === 0);
71
80
  }
72
81
  if (!edit || !selected) continue;
73
82
  cursor = todoCursor(text, edit, inner, rowStart);
74
83
  }
75
- return { rows, owners, cursor };
84
+ return { rows, owners, checks, cursor };
76
85
  };
77
86
 
78
87
  const noteInnerWidth = (width) => Math.max(1, width - NOTE_PAD * 2);
@@ -184,13 +193,21 @@ const paintBodyTodo = (view, width, color) => {
184
193
  const caption = paintTodoCaption(width, color);
185
194
  const body = [caption, ...painted.rows];
186
195
  const todoOwners = [null, ...painted.owners];
196
+ const todoChecks = [false, ...painted.checks];
187
197
  let cursor = null;
188
198
  if (painted.cursor) {
189
199
  cursor = { x: painted.cursor.x, row: painted.cursor.row + 1 };
190
200
  }
191
201
  const fileHits = [];
192
202
  const splitBody = false;
193
- return paneResult({ body, fileHits, todoOwners, cursor, splitBody });
203
+ return paneResult({
204
+ body,
205
+ fileHits,
206
+ todoOwners,
207
+ todoChecks,
208
+ cursor,
209
+ splitBody,
210
+ });
194
211
  };
195
212
 
196
213
  module.exports = {
@@ -104,6 +104,13 @@ const listWindowStart = (focus, offset, listH, count) => {
104
104
  return start;
105
105
  };
106
106
 
107
+ const extraIdx = (extraRow, cursor, count) => {
108
+ if (!extraRow) return -1;
109
+ if (extraRow.at === 'start') return 0;
110
+ if (extraRow.at === 'replace') return extraRow.index ?? cursor;
111
+ return count;
112
+ };
113
+
107
114
  const paintCursorList = (
108
115
  items,
109
116
  cursor,
@@ -112,15 +119,16 @@ const paintCursorList = (
112
119
  bodyH,
113
120
  headerLines,
114
121
  paintRow,
115
- tail,
122
+ extraRow,
116
123
  offset = 0,
117
124
  ) => {
118
125
  const pad = bodyH >= FILE_LIST_PAD * 2 + 1 ? FILE_LIST_PAD : 0;
119
126
  const listH = bodyH - pad * 2;
120
- const extra = tail ? 1 : 0;
127
+ const inserting = extraRow && extraRow.at === 'start';
128
+ const extra = extraRow && extraRow.at !== 'replace' ? 1 : 0;
121
129
  const count = items.length + extra;
122
- const last = count - 1;
123
- const focus = extra ? last : cursor;
130
+ const extraAt = extraIdx(extraRow, cursor, items.length);
131
+ const focus = extraRow ? extraAt : cursor;
124
132
  const start = listWindowStart(focus, offset, listH, count);
125
133
  const blank = paintBodyFill(width, color, 'files');
126
134
  const body = [];
@@ -129,19 +137,20 @@ const paintCursorList = (
129
137
  if (pad) body.push(blank);
130
138
  for (let i = 0; i < listH; i++) {
131
139
  const idx = start + i;
132
- if (tail && idx === items.length) {
133
- body.push(tail.row);
134
- if (tail.cursor) {
135
- textCursor = { x: tail.cursor.x, row: body.length - 1 };
140
+ if (extraRow && idx === extraAt) {
141
+ body.push(extraRow.row);
142
+ if (extraRow.cursor) {
143
+ textCursor = { x: extraRow.cursor.x, row: body.length - 1 };
136
144
  }
137
145
  continue;
138
146
  }
139
- const entry = items[idx];
147
+ const itemIdx = inserting ? idx - 1 : idx;
148
+ const entry = items[itemIdx];
140
149
  if (!entry) break;
141
- const selected = extra ? false : idx === cursor;
150
+ const selected = extraRow ? false : itemIdx === cursor;
142
151
  body.push(paintRow(entry, width, color, selected));
143
152
  const y = headerLines + pad + i + 1;
144
- fileHits.push({ y, cursor: idx });
153
+ fileHits.push({ y, cursor: itemIdx });
145
154
  }
146
155
  if (pad) body.push(blank);
147
156
  return { body, fileHits, cursor: textCursor, offset: start };
@@ -171,10 +180,43 @@ const paintBracketName = (label, nameFg, wrapFg, bgRgb, color, nameBold) => {
171
180
  );
172
181
  };
173
182
 
183
+ const paintListEdit = (compose, width, color, edgeW) => {
184
+ const inner = Math.max(1, width - edgeW);
185
+ const prefix = ` ${FILE_MARK} `;
186
+ const prefixW = visibleWidth(prefix);
187
+ const rest = Math.max(1, inner - prefixW);
188
+ const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
189
+ const label = `[${text}]`;
190
+ const clipped = truncateVisible(label, rest);
191
+ const pad = Math.max(0, rest - visibleWidth(clipped));
192
+ const left = `${prefix}${clipped}${' '.repeat(pad)}`;
193
+ const fgRgb = THEME.chromeFg;
194
+ const bgRgb = THEME.buttonBg;
195
+ const edge = fill(edgeW, fgRgb, THEME.ctxBg, color);
196
+ let row = `${left}${edge}`;
197
+ if (color) {
198
+ const gap = ' '.repeat(pad);
199
+ row = `${seq(fgRgb, bgRgb)}${EL}`;
200
+ row += paint(prefix, fgRgb, bgRgb, color);
201
+ row += paintBracketName(
202
+ clipped,
203
+ THEME.buttonHotFg,
204
+ THEME.mutedFg,
205
+ bgRgb,
206
+ color,
207
+ );
208
+ row += paint(gap, fgRgb, bgRgb, color);
209
+ row += edge;
210
+ }
211
+ const col = Math.min((compose.cursor ?? 0) + 1, visibleWidth(clipped));
212
+ return { row, cursor: { x: prefixW + col + 1 } };
213
+ };
214
+
174
215
  const paneResult = (fields) => ({
175
216
  body: fields.body,
176
217
  fileHits: fields.fileHits,
177
218
  todoOwners: fields.todoOwners,
219
+ todoChecks: fields.todoChecks ?? [],
178
220
  cursor: fields.cursor ?? null,
179
221
  splitBody: fields.splitBody === true,
180
222
  listScroll: fields.listScroll ?? 0,
@@ -239,6 +281,7 @@ module.exports = {
239
281
  paintBodyFill,
240
282
  windowRows,
241
283
  paintCursorList,
284
+ paintListEdit,
242
285
  branchLabel,
243
286
  paintBracketName,
244
287
  paneResult,
@@ -9,7 +9,7 @@ const { renderFrame } = require('./frame.js');
9
9
  const { QUIT_PROMPT, PUSH_PROMPT } = chrome;
10
10
  const { updatePrompt, dropPrompt } = chrome;
11
11
  const { formatBusyStatus, headerText, layoutButtons } = chrome;
12
- const { DEL_CHAR_BG, ADD_CHAR_BG, paintDiffLine, codeInnerWidth } = diff;
12
+ const { delCharBg, addCharBg, paintDiffLine, codeInnerWidth } = diff;
13
13
  const { todoInnerWidth, todoBodyWidth, noteInnerWidth } = notes;
14
14
  const { presentCursor, presentRows } = present;
15
15
 
@@ -18,8 +18,8 @@ module.exports = {
18
18
  PUSH_PROMPT,
19
19
  updatePrompt,
20
20
  dropPrompt,
21
- DEL_CHAR_BG,
22
- ADD_CHAR_BG,
21
+ delCharBg,
22
+ addCharBg,
23
23
  paintDiffLine,
24
24
  headerText,
25
25
  layoutButtons,