reslop 0.2.4 → 0.2.6

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 (109) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/LICENSE +1 -1
  3. package/README.md +12 -13
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -12
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +48 -18
  9. package/lib/dashboard/git.js +128 -0
  10. package/lib/dashboard/model.js +332 -0
  11. package/lib/dashboard/npm.js +193 -0
  12. package/lib/dashboard/tree.js +278 -0
  13. package/lib/deps-diff.js +196 -404
  14. package/lib/deps.js +240 -399
  15. package/lib/detect.js +8 -18
  16. package/lib/diff/align.js +41 -113
  17. package/lib/diff/diff.js +12 -43
  18. package/lib/diff/display.js +25 -36
  19. package/lib/diff/edit.js +6 -20
  20. package/lib/diff/inline.js +138 -141
  21. package/lib/diff/items.js +33 -47
  22. package/lib/diff/parse.js +28 -56
  23. package/lib/diff/patch.js +12 -33
  24. package/lib/diff/unit.js +90 -206
  25. package/lib/editor.js +98 -55
  26. package/lib/files.js +86 -103
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +182 -344
  29. package/lib/git-reword-todo.js +5 -13
  30. package/lib/git-worktree.js +141 -212
  31. package/lib/git.js +21 -49
  32. package/lib/github.js +67 -181
  33. package/lib/gitlab.js +40 -90
  34. package/lib/highlight/bash.js +55 -177
  35. package/lib/highlight/core.js +157 -132
  36. package/lib/highlight/css.js +25 -58
  37. package/lib/highlight/csv.js +19 -37
  38. package/lib/highlight/dart.js +72 -213
  39. package/lib/highlight/dot.js +16 -26
  40. package/lib/highlight/html.js +42 -63
  41. package/lib/highlight/js.js +177 -506
  42. package/lib/highlight/json.js +17 -37
  43. package/lib/highlight/log.js +22 -35
  44. package/lib/highlight/txt.js +9 -22
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +62 -85
  47. package/lib/manifest.js +67 -130
  48. package/lib/npm-commands.js +119 -189
  49. package/lib/remote.js +141 -197
  50. package/lib/render/branches.js +66 -73
  51. package/lib/render/chrome.js +264 -323
  52. package/lib/render/commits.js +187 -500
  53. package/lib/render/dash-activity.js +212 -0
  54. package/lib/render/dash-blocks.js +245 -0
  55. package/lib/render/dash-table.js +290 -0
  56. package/lib/render/dashboard.js +62 -0
  57. package/lib/render/diff.js +123 -228
  58. package/lib/render/files.js +45 -99
  59. package/lib/render/frame.js +129 -183
  60. package/lib/render/notes.js +144 -160
  61. package/lib/render/npm.js +234 -112
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +131 -170
  64. package/lib/render/render.js +5 -14
  65. package/lib/render/report.js +43 -0
  66. package/lib/render/tiles.js +260 -0
  67. package/lib/render/unit.js +15 -28
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -38
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -78
  72. package/lib/report-test.js +256 -141
  73. package/lib/report-tools.js +96 -161
  74. package/lib/review-merge.js +114 -152
  75. package/lib/review.js +115 -189
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +89 -75
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +82 -63
  80. package/lib/session/branches.js +193 -247
  81. package/lib/session/change-actions.js +171 -214
  82. package/lib/session/commit.js +280 -342
  83. package/lib/session/compose.js +559 -674
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +164 -157
  86. package/lib/session/edit-click.js +57 -0
  87. package/lib/session/files-pane.js +85 -53
  88. package/lib/session/items.js +73 -104
  89. package/lib/session/lifecycle.js +403 -541
  90. package/lib/session/load.js +48 -68
  91. package/lib/session/navigation.js +96 -87
  92. package/lib/session/npm.js +409 -345
  93. package/lib/session/ops.js +196 -63
  94. package/lib/session/pointer.js +23 -15
  95. package/lib/session/progress.js +33 -21
  96. package/lib/session/review.js +33 -54
  97. package/lib/session/tasks.js +333 -0
  98. package/lib/session/templates.js +51 -59
  99. package/lib/session/terminal.js +85 -136
  100. package/lib/session/unit.js +3 -4
  101. package/lib/session/update.js +76 -97
  102. package/lib/session/view.js +52 -83
  103. package/lib/session/watch.js +208 -204
  104. package/lib/session.js +112 -237
  105. package/lib/update.js +45 -116
  106. package/lib/utilities.js +67 -89
  107. package/lib/wrap.js +33 -44
  108. package/package.json +1 -1
  109. package/lib/session/todos.js +0 -293
@@ -0,0 +1,333 @@
1
+ 'use strict';
2
+
3
+ const files = require('../files.js');
4
+ const { isTasksEntry, TASKS_FILE, TASK_KINDS, taskKind } = files;
5
+ const review = require('../review.js');
6
+ const { addTask, setTaskText, setTaskDone, removeTask, checkLabel } = review;
7
+
8
+ const TASK_TYPE_SKIP = [
9
+ 'enter',
10
+ 'escape',
11
+ 'tab',
12
+ 'backspace',
13
+ 'delete',
14
+ 'left',
15
+ 'right',
16
+ 'up',
17
+ 'down',
18
+ 'home',
19
+ 'end',
20
+ 'pageUp',
21
+ 'pageDown',
22
+ 'j',
23
+ 'k',
24
+ 'n',
25
+ 'p',
26
+ 'q',
27
+ 'f',
28
+ 'd',
29
+ 't',
30
+ 'b',
31
+ 'r',
32
+ 'c',
33
+ ];
34
+
35
+ const isTaskTypeKey = (key) => {
36
+ if (!key) return false;
37
+ if (TASK_TYPE_SKIP.includes(key)) return false;
38
+ if (key.startsWith('ctrl-') || key.startsWith('alt-')) return false;
39
+ return key.charCodeAt(0) >= 32;
40
+ };
41
+
42
+ const clamp = (value, last) => Math.max(0, Math.min(value, last));
43
+
44
+ class TaskPage {
45
+ constructor(composer) {
46
+ this.composer = composer;
47
+ }
48
+
49
+ get ui() {
50
+ return this.composer.ui;
51
+ }
52
+
53
+ get nav() {
54
+ return this.composer.ui.nav;
55
+ }
56
+
57
+ isOpen() {
58
+ return this.nav.pane === 'tasks';
59
+ }
60
+
61
+ list(kind) {
62
+ const notes = this.ui.review.store;
63
+ if (!notes) return [];
64
+ const rows = [];
65
+ for (const task of notes.tasks) {
66
+ if (taskKind(task) === kind) rows.push(task);
67
+ }
68
+ return rows;
69
+ }
70
+
71
+ rows() {
72
+ const rows = [];
73
+ for (const kind of TASK_KINDS) {
74
+ const tasks = this.list(kind.id);
75
+ for (const task of tasks) {
76
+ rows.push({ kind: kind.id, task, draft: false });
77
+ }
78
+ rows.push({ kind: kind.id, task: null, draft: true });
79
+ }
80
+ return rows;
81
+ }
82
+
83
+ rowAt(index) {
84
+ return this.rows()[index] ?? null;
85
+ }
86
+
87
+ rowIndex(dest) {
88
+ const rows = this.rows();
89
+ if (!dest) return 0;
90
+ for (let i = 0; i < rows.length; i++) {
91
+ const row = rows[i];
92
+ if (dest.draft) {
93
+ if (row.draft && row.kind === dest.kind) return i;
94
+ continue;
95
+ }
96
+ if (row.task && dest.task && row.task.id === dest.task.id) return i;
97
+ }
98
+ return clamp(0, rows.length - 1);
99
+ }
100
+
101
+ editingId() {
102
+ const composer = this.composer;
103
+ return composer.composeKind === 'tasks' ? composer.composeTaskId : null;
104
+ }
105
+
106
+ isDraftCompose() {
107
+ const composer = this.composer;
108
+ return composer.composeKind === 'tasks' && composer.composeTaskId === null;
109
+ }
110
+
111
+ canToggle() {
112
+ if (!this.isOpen()) return false;
113
+ return this.ui.mode === 'review';
114
+ }
115
+
116
+ focused() {
117
+ const row = this.rowAt(this.clampedTaskFocus());
118
+ return row && row.task ? row.task : null;
119
+ }
120
+
121
+ focusedKind() {
122
+ const row = this.rowAt(this.clampedTaskFocus());
123
+ return row ? row.kind : 'backlog';
124
+ }
125
+
126
+ closeEditor() {
127
+ this.composer.commitCompose();
128
+ this.composer.closeCompose();
129
+ }
130
+
131
+ taskSections() {
132
+ if (!this.isOpen()) return [];
133
+ const editor = this.composer.editor;
134
+ const editingId = this.editingId();
135
+ const draft = this.isDraftCompose();
136
+ const focus = this.clampedTaskFocus();
137
+ let at = 0;
138
+ const sections = [];
139
+ for (const kind of TASK_KINDS) {
140
+ const tasks = this.list(kind.id);
141
+ const texts = [];
142
+ for (const task of tasks) {
143
+ const live = editingId === task.id && editor;
144
+ texts.push(checkLabel(live ? editor.text : task.text, task.done));
145
+ at += 1;
146
+ }
147
+ const drafting = draft && at === focus && editor;
148
+ texts.push(checkLabel(drafting ? editor.text : '', false));
149
+ at += 1;
150
+ sections.push({ title: kind.title, texts });
151
+ }
152
+ return sections;
153
+ }
154
+
155
+ taskTexts() {
156
+ const texts = [];
157
+ for (const section of this.taskSections()) texts.push(...section.texts);
158
+ return texts;
159
+ }
160
+
161
+ taskRowCount() {
162
+ if (!this.isOpen()) return 0;
163
+ return this.rows().length;
164
+ }
165
+
166
+ clampedTaskFocus() {
167
+ return clamp(this.nav.tasksFocus, this.taskRowCount() - 1);
168
+ }
169
+
170
+ moveTaskFocus(delta) {
171
+ const count = this.taskRowCount();
172
+ if (!count) return;
173
+ this.nav.tasksFocus = clamp(this.clampedTaskFocus() + delta, count - 1);
174
+ }
175
+
176
+ openTasksPage() {
177
+ const nav = this.nav;
178
+ nav.tasksOpen = true;
179
+ nav.pane = 'tasks';
180
+ nav.scroll = 0;
181
+ nav.clearSelection();
182
+ this.ui.syncReviewPath();
183
+ }
184
+
185
+ closeTasksPage() {
186
+ this.nav.tasksOpen = false;
187
+ this.ui.syncReviewPath();
188
+ this.ui.syncFileCursor();
189
+ this.ui.lifecycle.catchUpWatch();
190
+ }
191
+
192
+ startDraftCompose() {
193
+ this.openTasksPage();
194
+ this.nav.tasksFocus = this.list('backlog').length;
195
+ this.nav.scroll = 0;
196
+ this.composer.openCompose('tasks', '', null);
197
+ }
198
+
199
+ editFocusedTask() {
200
+ if (!this.isOpen()) return;
201
+ const row = this.rowAt(this.clampedTaskFocus());
202
+ if (!row || row.draft) {
203
+ this.composer.openCompose('tasks', '', null);
204
+ return;
205
+ }
206
+ this.composer.openCompose('tasks', row.task.text, row.task.id);
207
+ }
208
+
209
+ editNextTask() {
210
+ if (!this.isOpen()) return;
211
+ const next = this.clampedTaskFocus() + 1;
212
+ if (next < this.taskRowCount()) this.nav.tasksFocus = next;
213
+ this.editFocusedTask();
214
+ }
215
+
216
+ moveTaskCompose(delta, width, extend) {
217
+ if (!this.isOpen()) return;
218
+ const editor = this.composer.editor;
219
+ if (editor && width) {
220
+ const before = editor.cursor;
221
+ editor.moveLine(delta, width, extend);
222
+ if (editor.cursor !== before || extend) return;
223
+ }
224
+ const rows = this.rows();
225
+ const at = this.clampedTaskFocus();
226
+ const next = clamp(at + delta, rows.length - 1);
227
+ if (next === at) return;
228
+ const dest = rows[next];
229
+ this.composer.commitCompose();
230
+ this.ui.flushReview();
231
+ this.composer.closeCompose();
232
+ this.nav.tasksFocus = this.rowIndex(dest);
233
+ this.editFocusedTask();
234
+ }
235
+
236
+ typeIntoTask(key) {
237
+ if (!this.canToggle() || !isTaskTypeKey(key)) return false;
238
+ this.editFocusedTask();
239
+ this.composer.editor.insert(key);
240
+ return true;
241
+ }
242
+
243
+ removeFocusedTask() {
244
+ const nav = this.nav;
245
+ if (!this.isOpen()) return;
246
+ const row = this.rowAt(this.clampedTaskFocus());
247
+ if (!row || !row.task) return;
248
+ const kind = row.kind;
249
+ removeTask(this.ui.review.store, row.task.id);
250
+ const rows = this.rows();
251
+ let focus = Math.min(nav.tasksFocus, rows.length - 1);
252
+ const at = rows[focus];
253
+ const prev = rows[focus - 1];
254
+ if (at && at.draft && prev && prev.task && prev.kind === kind) {
255
+ focus -= 1;
256
+ }
257
+ nav.tasksFocus = Math.max(0, focus);
258
+ this.ui.flushReview(true);
259
+ }
260
+
261
+ toggleFocusedTask() {
262
+ if (!this.canToggle()) return false;
263
+ const task = this.focused();
264
+ if (!task) return false;
265
+ setTaskDone(this.ui.review.store, task.id, !task.done);
266
+ this.ui.flushReview(true);
267
+ return true;
268
+ }
269
+
270
+ onTaskHit(cursor) {
271
+ if (!this.isOpen()) return;
272
+ const row = this.rowAt(cursor);
273
+ if (!row) return;
274
+ const composer = this.composer;
275
+ if (composer.composeKind === 'tasks') {
276
+ const same =
277
+ cursor === this.clampedTaskFocus() &&
278
+ ((row.draft && composer.composeTaskId === null) ||
279
+ (row.task && row.task.id === composer.composeTaskId));
280
+ if (same) return;
281
+ this.closeEditor();
282
+ }
283
+ this.nav.tasksFocus = cursor;
284
+ this.editFocusedTask();
285
+ }
286
+
287
+ onTaskCheck(cursor) {
288
+ if (!this.isOpen()) return;
289
+ const row = this.rowAt(cursor);
290
+ if (!row || !row.task) return void this.onTaskHit(cursor);
291
+ const composer = this.composer;
292
+ if (
293
+ composer.composeKind === 'tasks' &&
294
+ composer.composeTaskId !== row.task.id
295
+ ) {
296
+ this.closeEditor();
297
+ }
298
+ setTaskDone(this.ui.review.store, row.task.id, !row.task.done);
299
+ this.nav.tasksFocus = cursor;
300
+ this.ui.flushReview(true);
301
+ }
302
+
303
+ onTasks() {
304
+ const { nav } = this;
305
+ if (nav.pane === 'files') {
306
+ const entry = this.ui.fileCursorEntry();
307
+ if (entry && !isTasksEntry(entry)) {
308
+ nav.index = entry.openIndex ?? entry.firstIndex;
309
+ }
310
+ }
311
+ this.startDraftCompose();
312
+ }
313
+
314
+ taskEditView() {
315
+ const { composeKind, editor } = this.composer;
316
+ if (composeKind !== 'tasks' || !editor) return null;
317
+ return { cursor: editor.cursor };
318
+ }
319
+
320
+ commitTask(text) {
321
+ const notes = this.ui.review.store;
322
+ const id = this.composer.composeTaskId;
323
+ const prev =
324
+ id === null ? null : notes.tasks.find((task) => task.id === id);
325
+ if (prev) return void setTaskText(notes, id, text);
326
+ if (!text.trim()) return;
327
+ const kind = this.focusedKind();
328
+ const todo = addTask(notes, TASKS_FILE, text, false, kind);
329
+ this.composer.composeTaskId = todo.id;
330
+ }
331
+ }
332
+
333
+ module.exports = { TaskPage };
@@ -3,62 +3,54 @@
3
3
  const review = require('../review.js');
4
4
  const { rankedTemplates, prefixTemplates, TEMPLATE_SHOW } = review;
5
5
 
6
- const shownTemplates = (api) => {
7
- const notes = api.review.store;
8
- if (!notes) return [];
9
- const { state } = api;
10
- if (state.composeKind && state.composeKind !== 'feedback') return [];
11
- const ranked = rankedTemplates(notes.templates);
12
- const editing = state.composeKind && state.editor;
13
- const typed = editing ? state.editor.text : '';
14
- const exact = ranked.some((entry) => entry.text === typed);
15
- if (exact) return [];
16
- const matched = prefixTemplates(ranked, typed);
17
- return matched.slice(0, TEMPLATE_SHOW);
18
- };
19
-
20
- const clampedTemplateIndex = (api, shown) => {
21
- if (!shown.length) return -1;
22
- if (api.state.templateIndex >= shown.length) return shown.length - 1;
23
- return api.state.templateIndex;
24
- };
25
-
26
- const clearTemplatePick = (api) => {
27
- api.state.templateIndex = -1;
28
- api.state.templateFocus = false;
29
- };
30
-
31
- const focusTemplate = (api, index, apply) => {
32
- const shown = shownTemplates(api);
33
- if (!shown.length) return;
34
- let i = index;
35
- if (i < 0) i = shown.length - 1;
36
- if (i >= shown.length) i = 0;
37
- api.state.templateIndex = i;
38
- api.state.templateFocus = true;
39
- if (apply) api.state.editor.replace(shown[i].text);
40
- };
41
-
42
- const selectTemplate = (api, index) => {
43
- const shown = shownTemplates(api);
44
- if (!shown.length) return null;
45
- focusTemplate(api, index, true);
46
- return api.save();
47
- };
48
-
49
- const applyTemplate = (api) => {
50
- if (api.state.composeKind !== 'feedback') return null;
51
- const index = api.state.templateFocus ? api.state.templateIndex : 0;
52
- return selectTemplate(api, index);
53
- };
54
-
55
- const createTemplatePick = (api) => ({
56
- shownTemplates: () => shownTemplates(api),
57
- clampedTemplateIndex: (shown) => clampedTemplateIndex(api, shown),
58
- clearTemplatePick: () => clearTemplatePick(api),
59
- focusTemplate: (index, apply) => focusTemplate(api, index, apply),
60
- applyTemplate: () => applyTemplate(api),
61
- selectTemplate: (index) => selectTemplate(api, index),
62
- });
63
-
64
- module.exports = { createTemplatePick };
6
+ class TemplatePick {
7
+ constructor(composer) {
8
+ this.composer = composer;
9
+ }
10
+
11
+ shownTemplates() {
12
+ const notes = this.composer.ui.review.store;
13
+ if (!notes) return [];
14
+ const { composeKind, editor } = this.composer;
15
+ if (composeKind && composeKind !== 'feedback') return [];
16
+ const ranked = rankedTemplates(notes.templates);
17
+ const typed = composeKind && editor ? editor.text : '';
18
+ if (ranked.some((entry) => entry.text === typed)) return [];
19
+ return prefixTemplates(ranked, typed).slice(0, TEMPLATE_SHOW);
20
+ }
21
+
22
+ clampedTemplateIndex(shown) {
23
+ if (!shown.length) return -1;
24
+ return Math.min(this.composer.templateIndex, shown.length - 1);
25
+ }
26
+
27
+ clearTemplatePick() {
28
+ this.composer.templateIndex = -1;
29
+ this.composer.templateFocus = false;
30
+ }
31
+
32
+ focusTemplate(index, apply) {
33
+ const shown = this.shownTemplates();
34
+ if (!shown.length) return;
35
+ let i = index;
36
+ if (i < 0) i = shown.length - 1;
37
+ if (i >= shown.length) i = 0;
38
+ this.composer.templateIndex = i;
39
+ this.composer.templateFocus = true;
40
+ if (apply) this.composer.editor.replace(shown[i].text);
41
+ }
42
+
43
+ selectTemplate(index) {
44
+ if (!this.shownTemplates().length) return null;
45
+ this.focusTemplate(index, true);
46
+ return this.composer.saveCompose();
47
+ }
48
+
49
+ applyTemplate() {
50
+ const { composeKind, templateFocus, templateIndex } = this.composer;
51
+ if (composeKind !== 'feedback') return null;
52
+ return this.selectTemplate(templateFocus ? templateIndex : 0);
53
+ }
54
+ }
55
+
56
+ module.exports = { TemplatePick };
@@ -10,156 +10,105 @@ const LEAVE_TERM =
10
10
  '\x1b[?1006l\x1b[?1002l\x1b[?7h\x1b[0 q\x1b[?12l\x1b[?25h\x1b[?1049l';
11
11
 
12
12
  const sameCursor = (a, b) => {
13
- if (!a && !b) return true;
14
- if (!a || !b) return false;
13
+ if (!a || !b) return !a && !b;
15
14
  return a.x === b.x && a.y === b.y;
16
15
  };
17
16
 
18
- const hasTimer = (api, name) => api.timers.has(name);
19
-
20
- const stopTimer = (api, name) => {
21
- const id = api.timers.get(name);
22
- if (id === undefined) return;
23
- clearInterval(id);
24
- api.timers.delete(name);
25
- };
17
+ const sameSize = (a, b) =>
18
+ a !== null && a.width === b.width && a.height === b.height;
19
+
20
+ class Terminal {
21
+ constructor({ stdin = null, stdout, proc = process }) {
22
+ this.stdin = stdin;
23
+ this.stdout = stdout;
24
+ this.proc = proc;
25
+ this.timers = new Map();
26
+ this.disposers = [];
27
+ this.open = false;
28
+ this.disposed = false;
29
+ this.resetCache();
30
+ }
26
31
 
27
- const startTimer = (api, name, fn, ms) => {
28
- if (api.state.disposed) return;
29
- if (api.timers.has(name)) return;
30
- const id = setInterval(fn, ms);
31
- if (id && typeof id.unref === 'function') id.unref();
32
- api.timers.set(name, id);
33
- };
32
+ resetCache() {
33
+ this.carry = '';
34
+ this.lastFrame = null;
35
+ this.lastCursor = null;
36
+ this.lastSize = null;
37
+ }
34
38
 
35
- const restartTimer = (api, name, fn, ms) => {
36
- stopTimer(api, name);
37
- startTimer(api, name, fn, ms);
38
- };
39
+ hasTimer(name) {
40
+ return this.timers.has(name);
41
+ }
39
42
 
40
- const clearTimers = (api) => {
41
- for (const name of [...api.timers.keys()]) stopTimer(api, name);
42
- };
43
+ cancel(name) {
44
+ const id = this.timers.get(name);
45
+ if (id === undefined) return;
46
+ clearTimeout(id);
47
+ this.timers.delete(name);
48
+ }
43
49
 
44
- const stopListening = (api) => {
45
- while (api.disposers.length) {
46
- const stop = api.disposers.pop();
47
- stop();
50
+ later(name, fn, ms) {
51
+ if (this.disposed || this.timers.has(name)) return;
52
+ const id = setTimeout(() => {
53
+ this.timers.delete(name);
54
+ fn();
55
+ }, ms);
56
+ id.unref();
57
+ this.timers.set(name, id);
48
58
  }
49
- };
50
59
 
51
- const enter = (api) => {
52
- if (api.state.disposed) return;
53
- const { stdin, stdout, state } = api;
54
- if (stdin && stdin.setRawMode) stdin.setRawMode(true);
55
- if (stdin && typeof stdin.resume === 'function') stdin.resume();
56
- stdout.write(ENTER_TERM);
57
- state.open = true;
58
- };
60
+ clearTimers() {
61
+ for (const id of this.timers.values()) clearTimeout(id);
62
+ this.timers.clear();
63
+ }
59
64
 
60
- const leave = (api) => {
61
- if (!api.state.open) return;
62
- api.stdout.write(LEAVE_TERM);
63
- if (api.stdin && api.stdin.setRawMode) api.stdin.setRawMode(false);
64
- api.state.open = false;
65
- };
65
+ enter() {
66
+ if (this.disposed) return;
67
+ const stdin = this.stdin;
68
+ if (stdin && stdin.setRawMode) stdin.setRawMode(true);
69
+ if (stdin && stdin.resume) stdin.resume();
70
+ this.stdout.write(ENTER_TERM);
71
+ this.open = true;
72
+ }
66
73
 
67
- const resetCache = (api) => {
68
- api.state.carry = '';
69
- api.state.lastFrame = null;
70
- api.state.lastPresentedCursor = null;
71
- api.state.lastSize = null;
72
- };
74
+ leave() {
75
+ if (!this.open) return;
76
+ this.stdout.write(LEAVE_TERM);
77
+ if (this.stdin && this.stdin.setRawMode) this.stdin.setRawMode(false);
78
+ this.open = false;
79
+ }
73
80
 
74
- const close = (api) => {
75
- stopListening(api);
76
- clearTimers(api);
77
- leave(api);
78
- api.state.disposed = true;
79
- };
81
+ close() {
82
+ this.stopListening();
83
+ this.clearTimers();
84
+ this.leave();
85
+ this.disposed = true;
86
+ }
80
87
 
81
- const startListening = (api, handlers) => {
82
- if (api.state.disposed) return;
83
- const onData = (chunk) => handlers.onData(chunk);
84
- const onResize = () => handlers.onResize();
85
- api.disposers.push(listen(api.stdin, 'data', onData));
86
- api.disposers.push(watchResize(api.stdout, api.proc, onResize));
87
- };
88
+ startListening({ onData, onResize }) {
89
+ if (this.disposed) return;
90
+ this.disposers.push(listen(this.stdin, 'data', onData));
91
+ this.disposers.push(watchResize(this.stdout, this.proc, onResize));
92
+ }
88
93
 
89
- const paint = (api, frame, size, cursor) => {
90
- if (api.state.disposed) return;
91
- const lastSize = api.state.lastSize;
92
- const resized =
93
- !lastSize ||
94
- lastSize.width !== size.width ||
95
- lastSize.height !== size.height;
96
- if (
97
- !resized &&
98
- api.state.lastFrame &&
99
- api.state.lastFrame.text === frame.text
100
- ) {
101
- if (sameCursor(api.state.lastPresentedCursor, cursor)) return;
102
- api.state.lastPresentedCursor = cursor;
103
- api.stdout.write(presentCursor(cursor));
104
- return;
94
+ stopListening() {
95
+ while (this.disposers.length) this.disposers.pop()();
105
96
  }
106
- api.state.lastSize = size;
107
- api.state.lastFrame = frame;
108
- api.state.lastPresentedCursor = cursor;
109
- api.stdout.write(presentRows(frame.rows, { clear: resized, cursor }));
110
- };
111
97
 
112
- const createTerminal = (options) => {
113
- const state = {
114
- open: false,
115
- disposed: false,
116
- carry: '',
117
- lastFrame: null,
118
- lastPresentedCursor: null,
119
- lastSize: null,
120
- };
121
- const api = {
122
- stdin: options.stdin ?? null,
123
- stdout: options.stdout,
124
- proc: options.proc ?? process,
125
- timers: new Map(),
126
- disposers: [],
127
- state,
128
- };
129
- return {
130
- get lastFrame() {
131
- return state.lastFrame;
132
- },
133
- get lastSize() {
134
- return state.lastSize;
135
- },
136
- get carry() {
137
- return state.carry;
138
- },
139
- set carry(value) {
140
- state.carry = value;
141
- },
142
- get disposed() {
143
- return state.disposed;
144
- },
145
- enter: () => enter(api),
146
- leave: () => leave(api),
147
- close: () => close(api),
148
- paint: (frame, size, cursor) => paint(api, frame, size, cursor),
149
- startListening: (handlers) => startListening(api, handlers),
150
- stopListening: () => stopListening(api),
151
- startTimer: (name, fn, ms) => startTimer(api, name, fn, ms),
152
- stopTimer: (name) => stopTimer(api, name),
153
- restartTimer: (name, fn, ms) => restartTimer(api, name, fn, ms),
154
- clearTimers: () => clearTimers(api),
155
- hasTimer: (name) => hasTimer(api, name),
156
- resetCache: () => resetCache(api),
157
- };
158
- };
98
+ paint(frame, size, cursor) {
99
+ if (this.disposed) return;
100
+ const resized = !sameSize(this.lastSize, size);
101
+ const last = this.lastFrame;
102
+ this.lastFrame = frame;
103
+ if (!resized && last && last.text === frame.text) {
104
+ if (sameCursor(this.lastCursor, cursor)) return;
105
+ this.lastCursor = cursor;
106
+ return void this.stdout.write(presentCursor(cursor));
107
+ }
108
+ this.lastSize = size;
109
+ this.lastCursor = cursor;
110
+ this.stdout.write(presentRows(frame.rows, { clear: resized, cursor }));
111
+ }
112
+ }
159
113
 
160
- module.exports = {
161
- ENTER_TERM,
162
- LEAVE_TERM,
163
- sameCursor,
164
- createTerminal,
165
- };
114
+ module.exports = { ENTER_TERM, LEAVE_TERM, Terminal };