reslop 0.2.6 → 0.2.7

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 (42) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/lib/ansi.js +42 -2
  3. package/lib/api.js +1 -1
  4. package/lib/cli.js +21 -1
  5. package/lib/dashboard/model.js +28 -3
  6. package/lib/dashboard/npm.js +116 -19
  7. package/lib/files.js +13 -7
  8. package/lib/find.js +61 -0
  9. package/lib/git-deps.js +1 -0
  10. package/lib/git-worktree.js +22 -4
  11. package/lib/keys.js +26 -1
  12. package/lib/render/chrome.js +257 -79
  13. package/lib/render/dash-blocks.js +297 -46
  14. package/lib/render/dash-table.js +6 -1
  15. package/lib/render/files.js +23 -4
  16. package/lib/render/frame.js +4 -0
  17. package/lib/render/notes.js +13 -2
  18. package/lib/render/packages.js +93 -0
  19. package/lib/render/primitives.js +13 -6
  20. package/lib/render/repos.js +226 -0
  21. package/lib/render/search.js +25 -0
  22. package/lib/render/tiles.js +107 -12
  23. package/lib/review.js +5 -4
  24. package/lib/session/actions.js +136 -30
  25. package/lib/session/branches.js +3 -5
  26. package/lib/session/commit.js +31 -4
  27. package/lib/session/compose.js +19 -4
  28. package/lib/session/dashboard.js +160 -15
  29. package/lib/session/dispatch.js +38 -4
  30. package/lib/session/files-pane.js +17 -2
  31. package/lib/session/find.js +94 -0
  32. package/lib/session/lifecycle.js +27 -0
  33. package/lib/session/navigation.js +2 -0
  34. package/lib/session/packages.js +143 -0
  35. package/lib/session/pointer.js +14 -0
  36. package/lib/session/tasks.js +3 -3
  37. package/lib/session/view.js +15 -2
  38. package/lib/session/watch.js +5 -0
  39. package/lib/session/workspace.js +455 -0
  40. package/lib/session.js +14 -0
  41. package/lib/workspace.js +166 -0
  42. package/package.json +1 -1
@@ -0,0 +1,143 @@
1
+ 'use strict';
2
+
3
+ const utilities = require('../utilities.js');
4
+ const { IS_WIN, npmBin, oneLine, runProc } = utilities;
5
+
6
+ const INSTALL_MS = 120000;
7
+
8
+ class PackageController {
9
+ constructor(ui) {
10
+ this.ui = ui;
11
+ this.dropName = '';
12
+ this.addDev = false;
13
+ this.running = false;
14
+ this.job = null;
15
+ }
16
+
17
+ rows() {
18
+ const data = this.ui.dashboard.packagesView();
19
+ const modules = data.modules;
20
+ return modules && modules.packages ? modules.packages : [];
21
+ }
22
+
23
+ selected() {
24
+ return this.rows()[this.ui.nav.packagesCursor] ?? null;
25
+ }
26
+
27
+ canWrite() {
28
+ if (this.running) return false;
29
+ const ui = this.ui;
30
+ const caps = ui.capabilities;
31
+ if (!ui.readOnly && (!caps || caps.changes !== false)) return true;
32
+ ui.status = 'read only';
33
+ return false;
34
+ }
35
+
36
+ need() {
37
+ if (!this.canWrite()) return null;
38
+ const pkg = this.selected();
39
+ if (pkg) return pkg;
40
+ this.ui.status = 'not a package';
41
+ return null;
42
+ }
43
+
44
+ create() {
45
+ if (!this.canWrite()) return;
46
+ const pkg = this.selected();
47
+ if (pkg && pkg.transitive) {
48
+ this.ui.status = 'transitive';
49
+ return;
50
+ }
51
+ this.addDev = Boolean(pkg && pkg.dev);
52
+ this.ui.composer.openCompose('package', '');
53
+ }
54
+
55
+ finishAdd() {
56
+ const editor = this.ui.composer.editor;
57
+ if (!editor) return;
58
+ const name = editor.text.trim();
59
+ if (!name || /\s/.test(name) || name.startsWith('-')) {
60
+ this.ui.status = 'name';
61
+ return;
62
+ }
63
+ const args = ['install', name];
64
+ if (this.addDev) args.push('--save-dev');
65
+ this.ui.composer.closeCompose();
66
+ this.job = this.exec(args, 'npm i', `installed ${name}`);
67
+ }
68
+
69
+ askDrop() {
70
+ const pkg = this.need();
71
+ if (!pkg) return;
72
+ if (pkg.transitive) {
73
+ this.ui.status = 'transitive';
74
+ return;
75
+ }
76
+ this.dropName = pkg.name;
77
+ this.ui.mode = 'confirmDrop';
78
+ this.ui.status = '';
79
+ }
80
+
81
+ cancelDrop() {
82
+ this.dropName = '';
83
+ this.ui.mode = 'review';
84
+ this.ui.status = '';
85
+ }
86
+
87
+ confirmDrop() {
88
+ const name = this.dropName;
89
+ this.dropName = '';
90
+ this.ui.mode = 'review';
91
+ if (!name) return;
92
+ const done = `dropped ${name}`;
93
+ this.job = this.exec(['uninstall', name], 'npm uninstall', done);
94
+ }
95
+
96
+ updateWanted() {
97
+ const pkg = this.need();
98
+ if (!pkg) return;
99
+ const done = `updated ${pkg.name}`;
100
+ this.job = this.exec(['update', pkg.name], 'npm i', done);
101
+ }
102
+
103
+ updateLatest() {
104
+ const pkg = this.need();
105
+ if (!pkg) return;
106
+ const args = ['install', `${pkg.name}@latest`];
107
+ if (pkg.dev) args.push('--save-dev');
108
+ this.job = this.exec(args, 'npm i', `updated ${pkg.name}`);
109
+ }
110
+
111
+ async exec(args, busy, done) {
112
+ const ui = this.ui;
113
+ this.running = true;
114
+ ui.status = '';
115
+ ui.ops.setBusy(busy);
116
+ ui.ignoreWatch(INSTALL_MS);
117
+ ui.paint();
118
+ let result;
119
+ try {
120
+ const hook = ui.repo && ui.repo.runPackage;
121
+ const options = { cwd: ui.top, shell: IS_WIN };
122
+ result = hook
123
+ ? await hook(ui.top, args)
124
+ : await runProc(npmBin(), args, options);
125
+ } catch (error) {
126
+ result = { error, status: 1, stderr: '' };
127
+ }
128
+ this.running = false;
129
+ ui.ops.clearBusy();
130
+ const failed = !result || result.error || result.status;
131
+ if (failed) {
132
+ const message = result && result.error ? result.error.message : '';
133
+ const stderr = result ? result.stderr : '';
134
+ ui.status = oneLine(stderr, message || 'npm failed');
135
+ } else {
136
+ ui.status = done;
137
+ ui.dashboard.tasks.npm.request();
138
+ }
139
+ ui.paint();
140
+ }
141
+ }
142
+
143
+ module.exports = { PackageController };
@@ -103,6 +103,13 @@ const RELEASE_TARGETS = [
103
103
  hits: 'fileHits',
104
104
  onHit: (ui, hit) => ui.dashboard.open(hit.cursor),
105
105
  },
106
+ {
107
+ match: (ui) => ui.pane === 'repos',
108
+ hits: 'fileHits',
109
+ onHit: (ui, hit) => {
110
+ void ui.workspace.openAt(hit.cursor);
111
+ },
112
+ },
106
113
  {
107
114
  match: (ui) => ui.pane === 'files',
108
115
  hits: 'fileHits',
@@ -131,6 +138,13 @@ const RELEASE_TARGETS = [
131
138
  if (again) filesPane.onOpenFile(ui);
132
139
  },
133
140
  },
141
+ {
142
+ match: (ui) => ui.pane === 'packages',
143
+ hits: 'fileHits',
144
+ onHit: (ui, hit) => {
145
+ ui.nav.packagesCursor = hit.cursor;
146
+ },
147
+ },
134
148
  {
135
149
  match: (ui) => ui.pane === 'npm',
136
150
  hits: 'fileHits',
@@ -33,7 +33,7 @@ const TASK_TYPE_SKIP = [
33
33
  ];
34
34
 
35
35
  const isTaskTypeKey = (key) => {
36
- if (!key) return false;
36
+ if (!key || key.length !== 1 || key === ' ') return false;
37
37
  if (TASK_TYPE_SKIP.includes(key)) return false;
38
38
  if (key.startsWith('ctrl-') || key.startsWith('alt-')) return false;
39
39
  return key.charCodeAt(0) >= 32;
@@ -120,7 +120,7 @@ class TaskPage {
120
120
 
121
121
  focusedKind() {
122
122
  const row = this.rowAt(this.clampedTaskFocus());
123
- return row ? row.kind : 'backlog';
123
+ return row ? row.kind : TASK_KINDS[0].id;
124
124
  }
125
125
 
126
126
  closeEditor() {
@@ -191,7 +191,7 @@ class TaskPage {
191
191
 
192
192
  startDraftCompose() {
193
193
  this.openTasksPage();
194
- this.nav.tasksFocus = this.list('backlog').length;
194
+ this.nav.tasksFocus = this.list(TASK_KINDS[0].id).length;
195
195
  this.nav.scroll = 0;
196
196
  this.composer.openCompose('tasks', '', null);
197
197
  }
@@ -87,6 +87,7 @@ const counts = (ui, notes = noteCounts(ui.review.store)) => {
87
87
  commit: 0,
88
88
  pr: 0,
89
89
  tasks: 0,
90
+ tasksDone: 0,
90
91
  feedback: 0,
91
92
  code: 0,
92
93
  };
@@ -96,11 +97,15 @@ const counts = (ui, notes = noteCounts(ui.review.store)) => {
96
97
  }
97
98
  next.feedback = notes.feedback;
98
99
  next.tasks = notes.tasks;
100
+ next.tasksDone = notes.tasksDone;
99
101
  next.code = notes.code;
100
102
  return next;
101
103
  };
102
104
 
103
- const viewStatus = (ui) => ui.status || ui.busy;
105
+ const viewStatus = (ui) => {
106
+ if (ui.status || ui.busy) return ui.status || ui.busy;
107
+ return '';
108
+ };
104
109
 
105
110
  const unitBodyLines = (ui) => {
106
111
  if (ui.nav.pane !== 'unit') return [];
@@ -160,6 +165,7 @@ const view = (ui) => {
160
165
  fileScope: nav.fileScope,
161
166
  files: fileList(ui),
162
167
  fileCursor: nav.fileCursor,
168
+ find: nav.find,
163
169
  reviewPath: nav.reviewPath,
164
170
  unitLine: nav.unitLine,
165
171
  unitLines: unitBodyLines(ui),
@@ -198,8 +204,15 @@ const view = (ui) => {
198
204
  templateIndex: composer.templates.clampedTemplateIndex(templates),
199
205
  updateFrom: updater.from,
200
206
  updateTo: updater.to,
201
- dropName: gitBranches.dropName || commits.dropName || npm.dropName,
207
+ dropName:
208
+ ui.packages.dropName ||
209
+ gitBranches.dropName ||
210
+ commits.dropName ||
211
+ npm.dropName,
202
212
  dashboard: nav.pane === 'dashboard' ? ui.dashboard.view() : null,
213
+ packages: nav.pane === 'packages' ? ui.dashboard.packagesView() : null,
214
+ packagesCursor: nav.packagesCursor,
215
+ repos: nav.pane === 'repos' ? ui.workspace.view() : null,
203
216
  };
204
217
  };
205
218
 
@@ -136,6 +136,7 @@ class DiskWatcher {
136
136
  this.onChange = options.onChange;
137
137
  this.onReview = options.onReview;
138
138
  this.onPaths = options.onPaths;
139
+ this.matchReview = options.matchReview ?? null;
139
140
  this.recursive = options.recursive ?? canWatchRecursive();
140
141
  this.watchers = [];
141
142
  this.watched = new Set();
@@ -227,6 +228,10 @@ class DiskWatcher {
227
228
  this.schedule('review', this.onReview);
228
229
  if (!wanted) return;
229
230
  }
231
+ if (rel && this.matchReview && this.matchReview(rel)) {
232
+ this.pending.add(rel);
233
+ return void this.schedule('paths', () => this.flushPaths());
234
+ }
230
235
  if (!name) return void this.scheduleUnnamed();
231
236
  if (!wanted && ignoredRel(rel)) return;
232
237
  if (isOwnDirEvent(dir, name)) return;
@@ -0,0 +1,455 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+
5
+ const tree = require('../dashboard/tree.js');
6
+ const { FileIndex, HEAT_MS } = tree;
7
+ const watch = require('./watch.js');
8
+ const { UNKNOWN_PATH } = watch;
9
+ const workspace = require('../workspace.js');
10
+ const { emptyDiff, countCommits, readDiffStat, taskStats, pickRun } = workspace;
11
+ const npmInfo = require('../dashboard/npm.js');
12
+ const { readNpmSummary } = npmInfo;
13
+ const gitDeps = require('../git-deps.js');
14
+ const { collectReportAsync } = gitDeps;
15
+
16
+ const TICK_ID = 'repos';
17
+ const HEAT_TIMER = 'repos-heat';
18
+
19
+ const freshBag = () => ({
20
+ files: [],
21
+ git: false,
22
+ runs: false,
23
+ tasks: false,
24
+ npm: false,
25
+ activity: false,
26
+ });
27
+
28
+ const fullBag = () => ({
29
+ files: [UNKNOWN_PATH],
30
+ git: true,
31
+ runs: true,
32
+ tasks: true,
33
+ npm: true,
34
+ activity: false,
35
+ });
36
+
37
+ const mergeBag = (left, right) => {
38
+ const base = left ?? freshBag();
39
+ return {
40
+ files: [...base.files, ...right.files],
41
+ git: base.git || right.git,
42
+ runs: base.runs || right.runs,
43
+ tasks: base.tasks || right.tasks,
44
+ npm: base.npm || right.npm,
45
+ activity: base.activity || right.activity,
46
+ };
47
+ };
48
+
49
+ const blankRow = (repo) => ({
50
+ name: repo.name,
51
+ dir: repo.dir,
52
+ index: new FileIndex(repo.dir),
53
+ bytes: 0,
54
+ exts: [],
55
+ tasks: 0,
56
+ tasksDone: 0,
57
+ commits: 0,
58
+ diff: emptyDiff(),
59
+ run: null,
60
+ activityAt: 0,
61
+ filesReady: false,
62
+ commitsReady: false,
63
+ diffReady: false,
64
+ tasksReady: false,
65
+ runsReady: false,
66
+ npm: null,
67
+ npmReady: false,
68
+ pending: null,
69
+ running: false,
70
+ });
71
+
72
+ const readFiles = async (row, bag) => {
73
+ if (!bag.files.length) return;
74
+ const unknown = bag.files.includes(UNKNOWN_PATH);
75
+ if (unknown || !row.index.ready) await row.index.rescan();
76
+ else await row.index.touch(bag.files);
77
+ if (!row.index.ready) return;
78
+ const summary = row.index.summary();
79
+ row.bytes = summary.total.bytes;
80
+ row.exts = summary.exts.slice(0, 3);
81
+ row.filesReady = true;
82
+ };
83
+
84
+ const readCommits = async (row) => {
85
+ row.commits = await countCommits(row.dir);
86
+ row.commitsReady = true;
87
+ };
88
+
89
+ const readDiff = async (row) => {
90
+ row.diff = await readDiffStat(row.dir);
91
+ row.diffReady = true;
92
+ };
93
+
94
+ const readTasks = (row) => {
95
+ const stats = taskStats(row.dir);
96
+ row.tasksDone = stats.done;
97
+ row.tasks = stats.total;
98
+ row.tasksReady = true;
99
+ };
100
+
101
+ const readRuns = (row) => {
102
+ row.run = pickRun(row.dir);
103
+ row.runsReady = true;
104
+ };
105
+
106
+ const reportCount = async (dir, kind) => {
107
+ const report = await collectReportAsync(dir, kind);
108
+ return report ? report.size : 0;
109
+ };
110
+
111
+ const readNpm = async (row) => {
112
+ const summary = await readNpmSummary(row.dir, row.npm);
113
+ const [audit, outdated] = await Promise.all([
114
+ reportCount(row.dir, 'audit'),
115
+ reportCount(row.dir, 'outdated'),
116
+ ]);
117
+ row.npm = { ...summary, audit, outdated };
118
+ row.npmReady = true;
119
+ };
120
+
121
+ const toTile = (row) => ({
122
+ id: row.name,
123
+ name: row.name,
124
+ bytes: row.bytes,
125
+ exts: row.exts,
126
+ tasks: row.tasks,
127
+ tasksDone: row.tasksDone,
128
+ commits: row.commits,
129
+ diff: row.diff,
130
+ run: row.run,
131
+ activityAt: row.activityAt,
132
+ filesReady: row.filesReady,
133
+ commitsReady: row.commitsReady,
134
+ diffReady: row.diffReady,
135
+ tasksReady: row.tasksReady,
136
+ runsReady: row.runsReady,
137
+ npm: row.npm,
138
+ npmReady: row.npmReady,
139
+ });
140
+
141
+ const rowBusy = (row) =>
142
+ !row.filesReady ||
143
+ !row.commitsReady ||
144
+ !row.diffReady ||
145
+ !row.tasksReady ||
146
+ !row.runsReady ||
147
+ !row.npmReady;
148
+
149
+ const classifyRest = (rest) => {
150
+ const bag = freshBag();
151
+ if (!rest) {
152
+ bag.files.push(UNKNOWN_PATH);
153
+ bag.git = true;
154
+ bag.runs = true;
155
+ bag.tasks = true;
156
+ bag.npm = true;
157
+ bag.activity = true;
158
+ return bag;
159
+ }
160
+ if (
161
+ rest === 'package.json' ||
162
+ rest === 'package-lock.json' ||
163
+ rest === 'node_modules/.package-lock.json'
164
+ ) {
165
+ bag.npm = true;
166
+ }
167
+ if (rest === '.git' || rest.startsWith('.git/')) {
168
+ bag.git = true;
169
+ return bag;
170
+ }
171
+ if (rest === '.log' || rest.startsWith('.log/')) {
172
+ bag.runs = true;
173
+ return bag;
174
+ }
175
+ if (rest === '.review' || rest.startsWith('.review/')) {
176
+ bag.tasks = true;
177
+ return bag;
178
+ }
179
+ bag.files.push(rest);
180
+ bag.activity = true;
181
+ return bag;
182
+ };
183
+
184
+ class Workspace {
185
+ constructor(ui) {
186
+ this.ui = ui;
187
+ this.epoch = 0;
188
+ this.reset();
189
+ }
190
+
191
+ reset() {
192
+ this.stop();
193
+ this.rows = [];
194
+ this.byName = new Map();
195
+ this.cursor = 0;
196
+ this.screen = {
197
+ now: 0,
198
+ frame: 0,
199
+ cursor: 0,
200
+ cols: 1,
201
+ tiles: [],
202
+ };
203
+ }
204
+
205
+ seed() {
206
+ const info = this.ui.workspaceInfo;
207
+ const repos = info ? info.repos : [];
208
+ this.rows = repos.map(blankRow);
209
+ this.byName = new Map(this.rows.map((row) => [row.name, row]));
210
+ if (this.cursor >= this.rows.length) this.cursor = 0;
211
+ }
212
+
213
+ stale(epoch) {
214
+ return epoch !== this.epoch || !this.active;
215
+ }
216
+
217
+ publish(epoch) {
218
+ if (this.stale(epoch)) return;
219
+ this.afterDraw();
220
+ if (this.ui.uiOpen && this.ui.nav.pane === 'repos') this.ui.paint();
221
+ }
222
+
223
+ async settle(epoch, read) {
224
+ await read();
225
+ this.publish(epoch);
226
+ }
227
+
228
+ async apply(row, bag, epoch) {
229
+ if (bag.activity) row.activityAt = Date.now();
230
+ const jobs = [];
231
+ if (bag.files.length) {
232
+ jobs.push(this.settle(epoch, () => readFiles(row, bag)));
233
+ }
234
+ if (bag.git) {
235
+ jobs.push(this.settle(epoch, () => readCommits(row)));
236
+ jobs.push(this.settle(epoch, () => readDiff(row)));
237
+ }
238
+ if (bag.tasks) jobs.push(this.settle(epoch, () => readTasks(row)));
239
+ if (bag.runs) jobs.push(this.settle(epoch, () => readRuns(row)));
240
+ if (bag.npm) jobs.push(this.settle(epoch, () => readNpm(row)));
241
+ await Promise.all(jobs);
242
+ }
243
+
244
+ async pump(row) {
245
+ row.running = true;
246
+ while (row.pending && this.active) {
247
+ const bag = row.pending;
248
+ const epoch = this.epoch;
249
+ row.pending = null;
250
+ try {
251
+ await this.apply(row, bag, epoch);
252
+ } catch {
253
+ // keep the previous snapshot until the next event
254
+ }
255
+ if (this.stale(epoch)) break;
256
+ this.afterDraw();
257
+ if (this.ui.uiOpen && this.ui.nav.pane === 'repos') this.ui.paint();
258
+ }
259
+ row.running = false;
260
+ if (row.pending && this.active) {
261
+ void this.pump(row);
262
+ return;
263
+ }
264
+ this.afterDraw();
265
+ if (this.ui.uiOpen && this.ui.nav.pane === 'repos') this.ui.paint();
266
+ }
267
+
268
+ kick(row, bag) {
269
+ if (!row || !this.active) return;
270
+ if (bag.activity) row.activityAt = Date.now();
271
+ row.pending = mergeBag(row.pending, bag);
272
+ if (row.running) return;
273
+ void this.pump(row);
274
+ }
275
+
276
+ requestAll() {
277
+ for (const row of this.rows) this.kick(row, fullBag());
278
+ }
279
+
280
+ notePaths(paths) {
281
+ if (!this.active) return;
282
+ for (const rel of paths) {
283
+ if (rel === UNKNOWN_PATH) return void this.requestAll();
284
+ }
285
+ const found = new Map();
286
+ for (const rel of paths) {
287
+ const slash = rel.indexOf('/');
288
+ const name = slash < 0 ? rel : rel.slice(0, slash);
289
+ const rest = slash < 0 ? '' : rel.slice(slash + 1);
290
+ const row = this.byName.get(name);
291
+ if (!row) continue;
292
+ found.set(name, mergeBag(found.get(name), classifyRest(rest)));
293
+ }
294
+ for (const [name, bag] of found) this.kick(this.byName.get(name), bag);
295
+ if (found.size) {
296
+ this.afterDraw();
297
+ if (this.ui.uiOpen) this.ui.paint();
298
+ }
299
+ }
300
+
301
+ start() {
302
+ if (this.active) return;
303
+ this.active = true;
304
+ if (!this.rows.length) this.seed();
305
+ this.requestAll();
306
+ }
307
+
308
+ stop() {
309
+ this.epoch += 1;
310
+ this.active = false;
311
+ this.cancelTimers();
312
+ }
313
+
314
+ cancelTimers() {
315
+ const { progress, term } = this.ui;
316
+ if (!progress || !term) return;
317
+ progress.stop(TICK_ID);
318
+ term.cancel(HEAT_TIMER);
319
+ }
320
+
321
+ hotUntil() {
322
+ let until = 0;
323
+ for (const row of this.rows) {
324
+ until = Math.max(until, row.activityAt + HEAT_MS);
325
+ }
326
+ return until;
327
+ }
328
+
329
+ afterDraw() {
330
+ if (!this.active) return;
331
+ const visible = this.ui.uiOpen && this.ui.nav.pane === 'repos';
332
+ if (!visible) return void this.cancelTimers();
333
+ const now = Date.now();
334
+ const until = this.hotUntil();
335
+ const hot = now < until;
336
+ const running = this.rows.some((row) => row.run?.status === 'running');
337
+ const loading = this.rows.some((row) => rowBusy(row));
338
+ const { progress, term } = this.ui;
339
+ if (running || hot || loading) progress.start(TICK_ID);
340
+ else progress.stop(TICK_ID);
341
+ if (!hot) return;
342
+ term.later(
343
+ HEAT_TIMER,
344
+ () => {
345
+ if (this.ui.nav.pane === 'repos') this.ui.paint();
346
+ },
347
+ until - now + 20,
348
+ );
349
+ }
350
+
351
+ view() {
352
+ const screen = this.screen;
353
+ screen.now = Date.now();
354
+ screen.frame = this.ui.progressFrame;
355
+ screen.cursor = this.cursor;
356
+ screen.tiles = this.rows.map(toTile);
357
+ return screen;
358
+ }
359
+
360
+ move(dx, dy) {
361
+ const count = this.rows.length;
362
+ if (!count) return;
363
+ const cols = Math.max(1, this.screen.cols || 1);
364
+ const next = this.cursor + dx + dy * cols;
365
+ if (next < 0 || next >= count) return;
366
+ this.cursor = next;
367
+ if (this.ui.uiOpen) this.ui.paint();
368
+ }
369
+
370
+ enter(name) {
371
+ const row = this.byName.get(name);
372
+ const ui = this.ui;
373
+ if (!row || ui.mode !== 'review') return Promise.resolve();
374
+ ui.flushReview();
375
+ this.stop();
376
+ ui.lifecycle.stopWatch();
377
+ ui.dashboard.stop();
378
+ ui.loader.reset();
379
+ ui.cwd = row.dir;
380
+ ui.paths = [];
381
+ ui.repoName = row.name;
382
+ ui.branch = '';
383
+ ui.rev = null;
384
+ ui.revShort = '';
385
+ ui.change = null;
386
+ ui.dashboardHome = true;
387
+ ui.collection.reset();
388
+ ui.review.reset();
389
+ ui.nav.pane = 'dashboard';
390
+ ui.nav.tasksOpen = false;
391
+ ui.nav.scroll = 0;
392
+ ui.nav.clearSelection();
393
+ ui.status = '';
394
+ try {
395
+ ui.ensureRepo();
396
+ } catch (error) {
397
+ ui.status = error instanceof Error ? error.message : `${error}`;
398
+ return this.show();
399
+ }
400
+ const pending = ui.openLoad();
401
+ if (ui.uiOpen) {
402
+ ui.lifecycle.startWatch();
403
+ ui.dashboard.start();
404
+ ui.paint();
405
+ }
406
+ return pending ?? Promise.resolve();
407
+ }
408
+
409
+ openSelected() {
410
+ const row = this.rows[this.cursor];
411
+ if (!row) return Promise.resolve();
412
+ return this.enter(row.name);
413
+ }
414
+
415
+ openAt(id) {
416
+ const index = this.rows.findIndex((row) => row.name === id);
417
+ if (index < 0) return Promise.resolve();
418
+ this.cursor = index;
419
+ return this.openSelected();
420
+ }
421
+
422
+ show() {
423
+ const ui = this.ui;
424
+ if (!ui.workspaceRoot) return Promise.resolve();
425
+ ui.flushReview();
426
+ ui.dashboard.stop();
427
+ ui.lifecycle.stopWatch();
428
+ ui.loader.reset();
429
+ ui.cwd = ui.workspaceRoot;
430
+ ui.top = ui.workspaceRoot;
431
+ ui.paths = [];
432
+ ui.repoName = path.basename(ui.workspaceRoot);
433
+ ui.branch = '';
434
+ ui.rev = null;
435
+ ui.revShort = '';
436
+ ui.change = null;
437
+ ui.dashboardHome = false;
438
+ ui.collection.reset();
439
+ ui.review.reset();
440
+ ui.nav.pane = 'repos';
441
+ ui.nav.tasksOpen = false;
442
+ ui.nav.scroll = 0;
443
+ ui.nav.clearSelection();
444
+ ui.status = '';
445
+ ui.loader.didLoad = true;
446
+ this.start();
447
+ if (ui.uiOpen) {
448
+ ui.lifecycle.startWatch();
449
+ ui.paint();
450
+ }
451
+ return Promise.resolve();
452
+ }
453
+ }
454
+
455
+ module.exports = { Workspace, classifyRest };