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.
- package/CHANGELOG.md +11 -1
- package/lib/ansi.js +42 -2
- package/lib/api.js +1 -1
- package/lib/cli.js +21 -1
- package/lib/dashboard/model.js +28 -3
- package/lib/dashboard/npm.js +116 -19
- package/lib/files.js +13 -7
- package/lib/find.js +61 -0
- package/lib/git-deps.js +1 -0
- package/lib/git-worktree.js +22 -4
- package/lib/keys.js +26 -1
- package/lib/render/chrome.js +257 -79
- package/lib/render/dash-blocks.js +297 -46
- package/lib/render/dash-table.js +6 -1
- package/lib/render/files.js +23 -4
- package/lib/render/frame.js +4 -0
- package/lib/render/notes.js +13 -2
- package/lib/render/packages.js +93 -0
- package/lib/render/primitives.js +13 -6
- package/lib/render/repos.js +226 -0
- package/lib/render/search.js +25 -0
- package/lib/render/tiles.js +107 -12
- package/lib/review.js +5 -4
- package/lib/session/actions.js +136 -30
- package/lib/session/branches.js +3 -5
- package/lib/session/commit.js +31 -4
- package/lib/session/compose.js +19 -4
- package/lib/session/dashboard.js +160 -15
- package/lib/session/dispatch.js +38 -4
- package/lib/session/files-pane.js +17 -2
- package/lib/session/find.js +94 -0
- package/lib/session/lifecycle.js +27 -0
- package/lib/session/navigation.js +2 -0
- package/lib/session/packages.js +143 -0
- package/lib/session/pointer.js +14 -0
- package/lib/session/tasks.js +3 -3
- package/lib/session/view.js +15 -2
- package/lib/session/watch.js +5 -0
- package/lib/session/workspace.js +455 -0
- package/lib/session.js +14 -0
- package/lib/workspace.js +166 -0
- 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 };
|
package/lib/session/pointer.js
CHANGED
|
@@ -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',
|
package/lib/session/tasks.js
CHANGED
|
@@ -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 :
|
|
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(
|
|
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
|
}
|
package/lib/session/view.js
CHANGED
|
@@ -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) =>
|
|
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:
|
|
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
|
|
package/lib/session/watch.js
CHANGED
|
@@ -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 };
|