reslop 0.2.1 → 0.2.4

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.
Files changed (81) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +19 -4
  3. package/bin/reslop.js +4 -2
  4. package/lib/ansi.js +120 -4
  5. package/lib/api.js +2 -1
  6. package/lib/cli.js +21 -9
  7. package/lib/clipboard.js +6 -3
  8. package/lib/deps-diff.js +61 -115
  9. package/lib/deps.js +57 -54
  10. package/lib/detect.js +4 -2
  11. package/lib/diff/align.js +2 -1
  12. package/lib/diff/diff.js +6 -3
  13. package/lib/diff/display.js +2 -1
  14. package/lib/diff/inline.js +2 -1
  15. package/lib/diff/patch.js +2 -1
  16. package/lib/diff/unit.js +4 -2
  17. package/lib/editor.js +22 -4
  18. package/lib/files.js +38 -12
  19. package/lib/git-branches.js +150 -165
  20. package/lib/git-deps.js +20 -60
  21. package/lib/git-worktree.js +508 -0
  22. package/lib/git.js +23 -580
  23. package/lib/github.js +2 -1
  24. package/lib/gitlab.js +2 -1
  25. package/lib/highlight/bash.js +2 -1
  26. package/lib/highlight/css.js +2 -1
  27. package/lib/highlight/csv.js +2 -1
  28. package/lib/highlight/dot.js +2 -1
  29. package/lib/highlight/html.js +2 -1
  30. package/lib/highlight/json.js +2 -1
  31. package/lib/highlight/log.js +2 -1
  32. package/lib/highlight/txt.js +2 -1
  33. package/lib/highlight.js +2 -1
  34. package/lib/keys.js +12 -6
  35. package/lib/manifest.js +4 -2
  36. package/lib/npm-commands.js +592 -0
  37. package/lib/remote.js +20 -13
  38. package/lib/render/branches.js +25 -41
  39. package/lib/render/chrome.js +163 -128
  40. package/lib/render/commits.js +727 -65
  41. package/lib/render/diff.js +130 -47
  42. package/lib/render/files.js +4 -12
  43. package/lib/render/frame.js +41 -13
  44. package/lib/render/notes.js +4 -11
  45. package/lib/render/npm.js +216 -0
  46. package/lib/render/present.js +2 -1
  47. package/lib/render/primitives.js +34 -50
  48. package/lib/render/render.js +2 -1
  49. package/lib/render/unit.js +14 -11
  50. package/lib/report-model.js +129 -0
  51. package/lib/report-parse.js +89 -0
  52. package/lib/report-render.js +104 -0
  53. package/lib/report-run.js +166 -0
  54. package/lib/report-test.js +507 -0
  55. package/lib/report-tools.js +337 -0
  56. package/lib/review-merge.js +2 -1
  57. package/lib/review.js +6 -2
  58. package/lib/session/actions.js +77 -44
  59. package/lib/session/branches.js +13 -49
  60. package/lib/session/change-actions.js +2 -1
  61. package/lib/session/commit.js +113 -53
  62. package/lib/session/compose.js +83 -14
  63. package/lib/session/dispatch.js +42 -22
  64. package/lib/session/files-pane.js +16 -21
  65. package/lib/session/items.js +6 -3
  66. package/lib/session/lifecycle.js +12 -5
  67. package/lib/session/load.js +4 -2
  68. package/lib/session/navigation.js +2 -0
  69. package/lib/session/npm.js +388 -0
  70. package/lib/session/ops.js +15 -1
  71. package/lib/session/pointer.js +30 -3
  72. package/lib/session/review.js +2 -1
  73. package/lib/session/terminal.js +4 -2
  74. package/lib/session/update.js +2 -1
  75. package/lib/session/view.js +41 -4
  76. package/lib/session.js +44 -22
  77. package/lib/source.js +6 -3
  78. package/lib/update.js +5 -9
  79. package/lib/utilities.js +4 -2
  80. package/lib/wrap.js +3 -1
  81. package/package.json +6 -6
@@ -4,7 +4,11 @@ const ansi = require('../ansi.js');
4
4
  const files = require('../files.js');
5
5
  const { THEME_NAMES, setTheme, themeName } = ansi;
6
6
  const { isTodosEntry, TODO_FILE } = files;
7
+ const npm = require('../render/npm.js');
8
+ const { logViewRows } = npm;
7
9
  const unit = require('./unit.js');
10
+ const actions = require('./actions.js');
11
+ const { LIST_PANES } = actions;
8
12
  const { setFileScope, openUnitFile, moveUnitBlock } = unit;
9
13
 
10
14
  const LAYOUTS = ['unified', 'mixed', 'side'];
@@ -28,7 +32,10 @@ const syncFileCursor = (ui) => {
28
32
  const showFiles = (ui) => {
29
33
  const nav = ui.nav;
30
34
  const held =
31
- nav.todoOpen === true || nav.pane === 'branches' || nav.pane === 'commits';
35
+ nav.todoOpen === true ||
36
+ nav.pane === 'branches' ||
37
+ nav.pane === 'commits' ||
38
+ nav.pane === 'npm';
32
39
  nav.pane = 'files';
33
40
  nav.clearSelection();
34
41
  followReviewPath(ui);
@@ -37,9 +44,6 @@ const showFiles = (ui) => {
37
44
  };
38
45
 
39
46
  const onEscape = (ui) => {
40
- if (ui.nav.pane === 'branches' || ui.nav.pane === 'commits') {
41
- return void showFiles(ui);
42
- }
43
47
  if (ui.nav.pane === 'files') return void ui.onQuit();
44
48
  showFiles(ui);
45
49
  };
@@ -65,7 +69,8 @@ const onOpenFile = (ui) => {
65
69
  if (ui.nav.pane === 'branches') {
66
70
  return void ui.gitBranches.onCheckoutBranch();
67
71
  }
68
- if (ui.nav.pane === 'commits') return;
72
+ if (ui.nav.pane === 'commits') return void ui.commits.onShowDiff();
73
+ if (ui.nav.pane === 'npm') return void ui.npm.run();
69
74
  if (ui.nav.pane !== 'files') {
70
75
  if (ui.nav.todoOpen) ui.composer.editFocusedTodo();
71
76
  return;
@@ -152,7 +157,9 @@ const scrollStep = (ui, fraction) => {
152
157
  const rows = size.height ?? 24;
153
158
  const fallback = Math.max(1, rows - 4);
154
159
  const bodyH = frame?.bodyH ?? fallback;
155
- const height = Math.max(1, bodyH);
160
+ const viewing = ui.nav.pane === 'npm' && ui.npm && ui.npm.viewing;
161
+ const viewRows = viewing ? logViewRows(bodyH) : bodyH;
162
+ const height = Math.max(1, viewRows);
156
163
  const span = Math.floor(height * Math.abs(fraction));
157
164
  const n = Math.max(1, span);
158
165
  if (fraction < 0) return -n;
@@ -167,6 +174,7 @@ const onScroll = (ui, delta) => {
167
174
  if (ui.nav.pane === 'commits') {
168
175
  return void ui.commits.onCommitMove(delta);
169
176
  }
177
+ if (ui.nav.pane === 'npm') return void ui.npm.move(delta);
170
178
  if (ui.nav.todoOpen && ui.composer.todoRowCount() > 1) {
171
179
  return void ui.composer.moveTodoFocus(delta);
172
180
  }
@@ -181,22 +189,9 @@ const onScroll = (ui, delta) => {
181
189
  };
182
190
 
183
191
  const onJump = (ui, toEnd) => {
184
- if (ui.nav.pane === 'files') {
185
- const list = ui.fileList();
186
- if (!list.length) {
187
- ui.status = 'nothing to review';
188
- return;
189
- }
190
- const target = toEnd ? list.length - 1 : 0;
191
- return void onFileMove(ui, target - ui.nav.fileCursor);
192
- }
193
- if (ui.nav.pane === 'branches') {
192
+ if (LIST_PANES.includes(ui.nav.pane)) {
194
193
  const delta = toEnd ? Number.MAX_SAFE_INTEGER : -Number.MAX_SAFE_INTEGER;
195
- return void ui.gitBranches.onBranchMove(delta);
196
- }
197
- if (ui.nav.pane === 'commits') {
198
- const delta = toEnd ? Number.MAX_SAFE_INTEGER : -Number.MAX_SAFE_INTEGER;
199
- return void ui.commits.onCommitMove(delta);
194
+ return void onScroll(ui, delta);
200
195
  }
201
196
  if (ui.nav.todoOpen && ui.composer.todoRowCount() > 0) {
202
197
  const last = ui.composer.todoRowCount() - 1;
@@ -1,11 +1,14 @@
1
1
  'use strict';
2
2
 
3
- const { stateView } = require('./accessors.js');
3
+ const accessors = require('./accessors.js');
4
+ const { stateView } = accessors;
4
5
 
5
6
  const files = require('../files.js');
6
7
  const { itemPath, TODO_FILE, isTodoItem } = files;
7
- const { feedbackKey } = require('../review.js');
8
- const { refreshIndexPatches } = require('../diff/diff.js');
8
+ const review = require('../review.js');
9
+ const { feedbackKey } = review;
10
+ const diff = require('../diff/diff.js');
11
+ const { refreshIndexPatches } = diff;
9
12
 
10
13
  const depChangeKey = (item) => {
11
14
  const change = item.dep && item.dep.change;
@@ -3,14 +3,18 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
- const { isAbortError } = require('./load.js');
7
- const { createDiskWatcher, POLL_MS, DEBOUNCE_MS } = require('./watch.js');
6
+ const sessionLoad = require('./load.js');
7
+ const { isAbortError } = sessionLoad;
8
+ const watch = require('./watch.js');
9
+ const { createDiskWatcher, POLL_MS, DEBOUNCE_MS } = watch;
8
10
  const files = require('../files.js');
9
11
  const { itemPath, relativeAge, TODO_FILE } = files;
10
- const { MANIFEST, LOCKFILE, depFileMeta } = require('../deps.js');
12
+ const deps = require('../deps.js');
13
+ const { MANIFEST, LOCKFILE, depFileMeta } = deps;
11
14
  const review = require('../review.js');
12
15
  const { hasNotes, loadReview } = review;
13
- const { mergeReview } = require('../review-merge.js');
16
+ const reviewMerge = require('../review-merge.js');
17
+ const { mergeReview } = reviewMerge;
14
18
 
15
19
  const loadOptions = (api, extra = {}) => {
16
20
  const { loadConfig, ui } = api;
@@ -485,7 +489,10 @@ const worktreeStamp = (api) => {
485
489
  };
486
490
 
487
491
  const watchesWorktree = (nav) =>
488
- nav.todoOpen === true || nav.pane === 'branches' || nav.pane === 'commits';
492
+ nav.todoOpen === true ||
493
+ nav.pane === 'branches' ||
494
+ nav.pane === 'commits' ||
495
+ nav.pane === 'npm';
489
496
 
490
497
  const currentFileStamp = (api) => {
491
498
  const { nav } = api;
@@ -1,8 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const { stateView } = require('./accessors.js');
3
+ const accessors = require('./accessors.js');
4
+ const { stateView } = accessors;
4
5
 
5
- const isAbortError = (error) => !!(error && error.code === 'ABORT');
6
+ const utilities = require('../utilities.js');
7
+ const { isAbort: isAbortError } = utilities;
6
8
 
7
9
  const createLoadCoordinator = () => {
8
10
  const state = {
@@ -9,6 +9,7 @@ const initialState = (options = {}) => ({
9
9
  fileCursor: 0,
10
10
  branchCursor: 0,
11
11
  commitCursor: 0,
12
+ npmCursor: 0,
12
13
  scroll: 0,
13
14
  listScroll: 0,
14
15
  reviewPath: null,
@@ -17,6 +18,7 @@ const initialState = (options = {}) => ({
17
18
  selection: null,
18
19
  mouseAnchor: null,
19
20
  pendingClick: null,
21
+ lastClick: null,
20
22
  todoOpen: false,
21
23
  todoFocus: 0,
22
24
  });
@@ -0,0 +1,388 @@
1
+ 'use strict';
2
+
3
+ const commands = require('../npm-commands.js');
4
+ const npm = require('../render/npm.js');
5
+ const { logViewRows } = npm;
6
+
7
+ const { listCommands, reduceOutput, writeLog, parseScriptLine } = commands;
8
+ const { staleLogFiles, removeStaleLogs, formatSize } = commands;
9
+ const { scriptLine, saveScript, removeScript, reorderScript } = commands;
10
+ const { startNpm } = commands;
11
+
12
+ const selected = (api) => {
13
+ const cursor = api.ui.nav.npmCursor;
14
+ return api.state.commands[cursor];
15
+ };
16
+
17
+ const clampCursor = (api) => {
18
+ const last = api.state.commands.length - 1;
19
+ const nav = api.ui.nav;
20
+ if (nav.npmCursor > last) nav.npmCursor = Math.max(0, last);
21
+ if (nav.npmCursor < 0) nav.npmCursor = 0;
22
+ };
23
+
24
+ const refresh = (api) => {
25
+ api.state.commands = listCommands(api.ui.top);
26
+ clampCursor(api);
27
+ };
28
+
29
+ const canEdit = (ui) => {
30
+ if (ui.capabilities && ui.capabilities.changes === false) return false;
31
+ if (ui.readOnly) return false;
32
+ return true;
33
+ };
34
+
35
+ const refreshLogs = (api) => {
36
+ try {
37
+ const found = staleLogFiles(api.ui.top);
38
+ api.state.logBytes = found.bytes;
39
+ } catch (error) {
40
+ api.state.logBytes = 0;
41
+ api.ui.status = error.message;
42
+ }
43
+ };
44
+
45
+ const open = (api) => {
46
+ if (api.ui.mode === 'compose') return;
47
+ refresh(api);
48
+ refreshLogs(api);
49
+ api.state.viewing = false;
50
+ api.state.output = '';
51
+ api.ui.nav.pane = 'npm';
52
+ api.ui.status = '';
53
+ api.ui.nav.clearSelection();
54
+ };
55
+
56
+ const scrollLog = (api, delta) => {
57
+ const lines = api.state.output.split('\n');
58
+ let count = lines.at(-1) === '' ? lines.length - 1 : lines.length;
59
+ if (api.state.running) count += 1;
60
+ const frame = api.ui.lastFrame;
61
+ const bodyH = frame && frame.bodyH ? frame.bodyH : 1;
62
+ const max = Math.max(0, count - logViewRows(bodyH));
63
+ let start = api.state.followEnd ? max : api.state.logScroll;
64
+ start = Math.min(max, Math.max(0, start + delta));
65
+ api.state.followEnd = start === max;
66
+ api.state.logScroll = start;
67
+ };
68
+
69
+ const move = (api, delta) => {
70
+ if (api.state.viewing) return void scrollLog(api, delta);
71
+ if (!api.state.commands.length) return;
72
+ const nav = api.ui.nav;
73
+ const last = api.state.commands.length - 1;
74
+ const next = Math.min(last, Math.max(0, nav.npmCursor + delta));
75
+ if (next === nav.npmCursor) return;
76
+ nav.npmCursor = next;
77
+ api.ui.status = '';
78
+ };
79
+
80
+ const stopRun = (api) => {
81
+ api.state.running = false;
82
+ api.state.child = null;
83
+ if (api.ui.progress) api.ui.progress.stop('npm');
84
+ };
85
+
86
+ const showOutput = (api, text) => {
87
+ if (!api.state.viewing || !api.state.running) return;
88
+ if (api.state.stopping) return;
89
+ api.state.output = text;
90
+ api.ui.paint();
91
+ };
92
+
93
+ const withNotice = (text, notice) => {
94
+ const body = `${text ?? ''}`;
95
+ if (body.endsWith(`${notice}\n`)) return body;
96
+ if (!body) return `${notice}\n`;
97
+ if (body.endsWith('\n')) return `${body}${notice}\n`;
98
+ return `${body}\n${notice}\n`;
99
+ };
100
+
101
+ const finishRun = (api, entry, result) => {
102
+ const stopped = api.state.stopping === true;
103
+ const reduced = stopped
104
+ ? withNotice(api.state.output, 'terminated')
105
+ : reduceOutput(result.text, api.ui.top, result.status);
106
+ api.state.output = reduced;
107
+ api.state.stopping = false;
108
+ stopRun(api);
109
+ try {
110
+ writeLog(api.ui.top, entry.name, reduced);
111
+ } catch (error) {
112
+ api.ui.status = error.message;
113
+ api.ui.paint();
114
+ return;
115
+ }
116
+ if (stopped && api.state.viewing) api.ui.status = 'terminated';
117
+ api.ui.paint();
118
+ };
119
+
120
+ const startRun = (api, entry) => {
121
+ api.state.output = '';
122
+ api.state.viewing = true;
123
+ api.state.followEnd = true;
124
+ api.state.logScroll = 0;
125
+ api.state.running = true;
126
+ api.state.stopping = false;
127
+ api.ui.status = '';
128
+ if (api.ui.progress) api.ui.progress.start('npm');
129
+ const runner = api.ui.repo && api.ui.repo.runNpmCommand;
130
+ const start = typeof runner === 'function' ? runner : startNpm;
131
+ api.state.child = start(
132
+ api.ui.top,
133
+ entry,
134
+ (text) => showOutput(api, reduceOutput(text, api.ui.top, '')),
135
+ (result) => finishRun(api, entry, result),
136
+ );
137
+ api.ui.paint();
138
+ };
139
+
140
+ const runSelected = (api) => {
141
+ if (api.state.viewing || api.state.child) return;
142
+ const entry = selected(api);
143
+ if (!entry) return;
144
+ startRun(api, entry);
145
+ };
146
+
147
+ const rerun = (api) => {
148
+ if (!api.state.viewing || api.state.running) return;
149
+ const entry = selected(api);
150
+ if (!entry) return;
151
+ startRun(api, entry);
152
+ };
153
+
154
+ const closeView = (api) => {
155
+ if (api.state.running) {
156
+ api.state.stopping = true;
157
+ if (api.state.child) api.state.child.kill();
158
+ if (!api.state.running) return;
159
+ api.state.output = withNotice(api.state.output, 'terminated');
160
+ stopRun(api);
161
+ api.ui.status = 'terminated';
162
+ api.ui.paint();
163
+ return;
164
+ }
165
+ api.state.viewing = false;
166
+ api.ui.status = '';
167
+ api.ui.paint();
168
+ };
169
+
170
+ const editSelected = (api) => {
171
+ if (api.state.viewing) return;
172
+ if (!canEdit(api.ui)) {
173
+ api.ui.status = 'read only';
174
+ return;
175
+ }
176
+ const entry = selected(api);
177
+ if (!entry || entry.kind !== 'script') {
178
+ api.ui.status = 'not a script';
179
+ return;
180
+ }
181
+ api.state.editName = entry.name;
182
+ api.ui.composer.openCompose('npm', scriptLine(entry));
183
+ };
184
+
185
+ const newCommand = (api) => {
186
+ if (api.state.viewing) return;
187
+ if (!canEdit(api.ui)) {
188
+ api.ui.status = 'read only';
189
+ return;
190
+ }
191
+ api.state.editName = '';
192
+ api.ui.composer.openCompose('npm', '');
193
+ };
194
+
195
+ const finishEdit = (api) => {
196
+ const editor = api.ui.composer.state.editor;
197
+ const text = editor ? editor.text : '';
198
+ const parsed = parseScriptLine(text);
199
+ if (!parsed) {
200
+ api.ui.status = 'name: command';
201
+ return;
202
+ }
203
+ try {
204
+ api.ui.ignoreWatch();
205
+ saveScript(api.ui.top, api.state.editName, parsed);
206
+ } catch (error) {
207
+ api.ui.status = error.message;
208
+ return;
209
+ }
210
+ api.ui.composer.closeCompose();
211
+ refresh(api);
212
+ const at = api.state.commands.findIndex(
213
+ (entry) => entry.name === parsed.name,
214
+ );
215
+ if (at >= 0) api.ui.nav.npmCursor = at;
216
+ api.ui.status = 'saved';
217
+ };
218
+
219
+ const askDrop = (api) => {
220
+ if (api.state.viewing) return;
221
+ if (!canEdit(api.ui)) {
222
+ api.ui.status = 'read only';
223
+ return;
224
+ }
225
+ const entry = selected(api);
226
+ if (!entry || entry.kind !== 'script') {
227
+ api.ui.status = 'not a script';
228
+ return;
229
+ }
230
+ api.state.dropKind = 'script';
231
+ api.state.dropName = entry.name;
232
+ api.ui.mode = 'confirmDrop';
233
+ api.ui.status = '';
234
+ };
235
+
236
+ const askLogs = (api) => {
237
+ if (api.state.viewing) return;
238
+ if (!canEdit(api.ui)) {
239
+ api.ui.status = 'read only';
240
+ return;
241
+ }
242
+ refreshLogs(api);
243
+ if (!api.state.logBytes) return;
244
+ api.state.dropKind = 'logs';
245
+ const size = formatSize(api.state.logBytes);
246
+ api.state.dropName = `logs older than 5 days (${size})`;
247
+ api.ui.mode = 'confirmDrop';
248
+ api.ui.status = '';
249
+ };
250
+
251
+ const confirmDrop = (api) => {
252
+ const kind = api.state.dropKind;
253
+ const name = api.state.dropName;
254
+ api.state.dropName = '';
255
+ api.state.dropKind = '';
256
+ api.ui.mode = 'review';
257
+ if (kind === 'logs') {
258
+ try {
259
+ removeStaleLogs(api.ui.top);
260
+ } catch (error) {
261
+ api.ui.status = error.message;
262
+ refreshLogs(api);
263
+ return;
264
+ }
265
+ refreshLogs(api);
266
+ api.ui.status = 'dropped logs';
267
+ return;
268
+ }
269
+ if (!name) return;
270
+ try {
271
+ api.ui.ignoreWatch();
272
+ removeScript(api.ui.top, name);
273
+ } catch (error) {
274
+ api.ui.status = error.message;
275
+ return;
276
+ }
277
+ refresh(api);
278
+ api.ui.status = `dropped ${name}`;
279
+ };
280
+
281
+ const cancelDrop = (api) => {
282
+ api.state.dropName = '';
283
+ api.state.dropKind = '';
284
+ api.ui.mode = 'review';
285
+ api.ui.status = '';
286
+ };
287
+
288
+ const reorder = (api, delta) => {
289
+ if (api.state.viewing) return;
290
+ if (!canEdit(api.ui)) {
291
+ api.ui.status = 'read only';
292
+ return;
293
+ }
294
+ const entry = selected(api);
295
+ if (!entry || entry.kind !== 'script') {
296
+ api.ui.status = 'not a script';
297
+ return;
298
+ }
299
+ try {
300
+ api.ui.ignoreWatch();
301
+ if (!reorderScript(api.ui.top, entry.name, delta)) return;
302
+ } catch (error) {
303
+ api.ui.status = error.message;
304
+ return;
305
+ }
306
+ refresh(api);
307
+ const at = api.state.commands.findIndex((item) => item.name === entry.name);
308
+ if (at >= 0) api.ui.nav.npmCursor = at;
309
+ api.ui.status = '';
310
+ };
311
+
312
+ const createNpmController = (ui) => {
313
+ const state = {
314
+ commands: [],
315
+ viewing: false,
316
+ output: '',
317
+ followEnd: true,
318
+ logScroll: 0,
319
+ editName: '',
320
+ dropName: '',
321
+ dropKind: '',
322
+ logBytes: 0,
323
+ running: false,
324
+ stopping: false,
325
+ child: null,
326
+ };
327
+ const api = { ui, state };
328
+ return {
329
+ state,
330
+ get commands() {
331
+ return state.commands;
332
+ },
333
+ get viewing() {
334
+ return state.viewing;
335
+ },
336
+ get output() {
337
+ return state.output;
338
+ },
339
+ get dropName() {
340
+ return state.dropName;
341
+ },
342
+ get logLabel() {
343
+ if (!state.logBytes) return '';
344
+ return `old logs ${formatSize(state.logBytes)}`;
345
+ },
346
+ get followEnd() {
347
+ return state.followEnd;
348
+ },
349
+ get logScroll() {
350
+ return state.logScroll;
351
+ },
352
+ get editName() {
353
+ return state.editName;
354
+ },
355
+ get running() {
356
+ return state.running;
357
+ },
358
+ reset() {
359
+ if (state.child) state.child.kill();
360
+ stopRun(api);
361
+ state.commands = [];
362
+ state.viewing = false;
363
+ state.output = '';
364
+ state.followEnd = true;
365
+ state.logScroll = 0;
366
+ state.editName = '';
367
+ state.dropName = '';
368
+ state.dropKind = '';
369
+ state.logBytes = 0;
370
+ state.stopping = false;
371
+ },
372
+ open: () => open(api),
373
+ move: (delta) => move(api, delta),
374
+ run: () => runSelected(api),
375
+ rerun: () => rerun(api),
376
+ closeView: () => closeView(api),
377
+ edit: () => editSelected(api),
378
+ create: () => newCommand(api),
379
+ finishEdit: () => finishEdit(api),
380
+ askDrop: () => askDrop(api),
381
+ askLogs: () => askLogs(api),
382
+ confirmDrop: () => confirmDrop(api),
383
+ cancelDrop: () => cancelDrop(api),
384
+ reorder: (delta) => reorder(api, delta),
385
+ };
386
+ };
387
+
388
+ module.exports = { createNpmController };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { stateView } = require('./accessors.js');
3
+ const accessors = require('./accessors.js');
4
+ const { stateView } = accessors;
4
5
 
5
6
  const ASYNC_WITHOUT_UI = ['pull', 'push'];
6
7
 
@@ -81,6 +82,18 @@ const createOpsRunner = (ui) => {
81
82
  finishBusy(done, after);
82
83
  };
83
84
 
85
+ const runWrite = (name, args, kind, done, after) => {
86
+ const picked = pickRepoWrite(ui.repo, name, ui.uiOpen);
87
+ const write = (top) => picked.write(top, ...args);
88
+ if (picked.async) {
89
+ const refresh = () => after({ doneStatus: done });
90
+ return void runBusy(kind, done, write, refresh);
91
+ }
92
+ if (!runRepo(done, write)) return;
93
+ after({});
94
+ if (!ui.status) ui.status = done;
95
+ };
96
+
84
97
  return stateView(
85
98
  state,
86
99
  {
@@ -88,6 +101,7 @@ const createOpsRunner = (ui) => {
88
101
  setBusy,
89
102
  clearBusy,
90
103
  runRepo,
104
+ runWrite,
91
105
  runBusy,
92
106
  },
93
107
  ['gitBusy', 'busy'],
@@ -1,8 +1,11 @@
1
1
  'use strict';
2
2
 
3
- const { extractText, isRange } = require('../select.js');
4
- const { hitAction } = require('../keys.js');
5
- const { copyText } = require('../clipboard.js');
3
+ const select = require('../select.js');
4
+ const { extractText, isRange } = select;
5
+ const keys = require('../keys.js');
6
+ const { hitAction } = keys;
7
+ const clipboard = require('../clipboard.js');
8
+ const { copyText } = clipboard;
6
9
 
7
10
  const mouseCell = (ui, event) => {
8
11
  const size = ui.lastSize ?? ui.getSize();
@@ -45,6 +48,20 @@ const clickButton = (ui, cell, clickId) => {
45
48
  return true;
46
49
  };
47
50
 
51
+ const DOUBLE_CLICK_MS = 500;
52
+
53
+ const armDoubleClick = (nav, pane, cursor) => {
54
+ const now = Date.now();
55
+ const prev = nav.lastClick;
56
+ const again =
57
+ prev !== null &&
58
+ prev.pane === pane &&
59
+ prev.cursor === cursor &&
60
+ now - prev.at <= DOUBLE_CLICK_MS;
61
+ nav.lastClick = { pane, cursor, at: now };
62
+ return again;
63
+ };
64
+
48
65
  const clickHit = (ui, hits, cell, onHit) => {
49
66
  const nav = ui.nav;
50
67
  if (!hits || isRange(nav.selection)) return false;
@@ -99,9 +116,19 @@ const RELEASE_TARGETS = [
99
116
  match: (ui) => ui.pane === 'commits',
100
117
  hits: 'fileHits',
101
118
  onHit: (ui, hit) => {
119
+ if (ui.mode === 'compose') return;
102
120
  ui.nav.commitCursor = hit.cursor;
103
121
  },
104
122
  },
123
+ {
124
+ match: (ui) => ui.pane === 'npm',
125
+ hits: 'fileHits',
126
+ onHit: (ui, hit) => {
127
+ const again = armDoubleClick(ui.nav, 'npm', hit.cursor);
128
+ ui.nav.npmCursor = hit.cursor;
129
+ if (again && ui.mode === 'review') ui.npm.run();
130
+ },
131
+ },
105
132
  {
106
133
  match: (ui) => ui.pane === 'diff',
107
134
  hits: 'todoHits',
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { stateView } = require('./accessors.js');
3
+ const accessors = require('./accessors.js');
4
+ const { stateView } = accessors;
4
5
 
5
6
  const review = require('../review.js');
6
7
  const { createStore, applyImportedNotes, setStatus } = review;
@@ -1,7 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const { presentRows, presentCursor } = require('../render/render.js');
4
- const { listen, watchResize } = require('../utilities.js');
3
+ const render = require('../render/render.js');
4
+ const { presentRows, presentCursor } = render;
5
+ const utilities = require('../utilities.js');
6
+ const { listen, watchResize } = utilities;
5
7
 
6
8
  const ENTER_TERM = '\x1b[?1049h\x1b[?25l\x1b[?7l\x1b[?1002h\x1b[?1006h';
7
9
  const LEAVE_TERM =
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { stateView } = require('./accessors.js');
3
+ const accessors = require('./accessors.js');
4
+ const { stateView } = accessors;
4
5
 
5
6
  const update = require('../update.js');
6
7
  const { checkUpdate, installUpdate, markSkipped } = update;