git-watchtower 1.6.1 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/git-watchtower.js +49 -2
- package/package.json +6 -1
- package/sounds/README.md +34 -0
- package/src/casino/index.js +721 -0
- package/src/casino/sounds.js +245 -0
- package/src/cli/args.js +239 -0
- package/src/config/loader.js +329 -0
- package/src/config/schema.js +305 -0
- package/src/git/branch.js +428 -0
- package/src/git/commands.js +416 -0
- package/src/git/pr.js +111 -0
- package/src/git/remote.js +127 -0
- package/src/index.js +179 -0
- package/src/polling/engine.js +157 -0
- package/src/server/process.js +329 -0
- package/src/server/static.js +95 -0
- package/src/state/store.js +527 -0
- package/src/telemetry/analytics.js +142 -0
- package/src/telemetry/config.js +123 -0
- package/src/telemetry/index.js +93 -0
- package/src/ui/actions.js +425 -0
- package/src/ui/ansi.js +498 -0
- package/src/ui/keybindings.js +198 -0
- package/src/ui/renderer.js +1328 -0
- package/src/utils/async.js +219 -0
- package/src/utils/browser.js +40 -0
- package/src/utils/errors.js +490 -0
- package/src/utils/gitignore.js +174 -0
- package/src/utils/sound.js +33 -0
- package/src/utils/time.js +27 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracted keyboard action handlers (pure state reducers).
|
|
3
|
+
*
|
|
4
|
+
* Each function takes the current state object (and optional context)
|
|
5
|
+
* and returns an object of state updates, or null when no change is needed.
|
|
6
|
+
* Async side-effects (git operations, server calls) are intentionally
|
|
7
|
+
* excluded -- only synchronous state mutations live here.
|
|
8
|
+
*
|
|
9
|
+
* @module ui/actions
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
isPrintableChar,
|
|
14
|
+
isBackspaceKey,
|
|
15
|
+
isEscapeKey,
|
|
16
|
+
isEnterKey,
|
|
17
|
+
KEYS,
|
|
18
|
+
filterBranches,
|
|
19
|
+
} = require('./keybindings');
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Return the branch list that is currently visible (filtered or full).
|
|
27
|
+
* @param {object} state
|
|
28
|
+
* @returns {Array<{name: string}>}
|
|
29
|
+
*/
|
|
30
|
+
function getDisplayBranches(state) {
|
|
31
|
+
return state.filteredBranches !== null ? state.filteredBranches : state.branches;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Navigation
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Move the selection cursor up by one row.
|
|
40
|
+
* @param {object} state
|
|
41
|
+
* @returns {object|null} State updates, or null if already at the top.
|
|
42
|
+
*/
|
|
43
|
+
function moveUp(state) {
|
|
44
|
+
const displayBranches = getDisplayBranches(state);
|
|
45
|
+
if (state.selectedIndex > 0) {
|
|
46
|
+
const newIndex = state.selectedIndex - 1;
|
|
47
|
+
return {
|
|
48
|
+
selectedIndex: newIndex,
|
|
49
|
+
selectedBranchName: displayBranches[newIndex] ? displayBranches[newIndex].name : null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Move the selection cursor down by one row.
|
|
57
|
+
* @param {object} state
|
|
58
|
+
* @returns {object|null} State updates, or null if already at the bottom.
|
|
59
|
+
*/
|
|
60
|
+
function moveDown(state) {
|
|
61
|
+
const displayBranches = getDisplayBranches(state);
|
|
62
|
+
if (state.selectedIndex < displayBranches.length - 1) {
|
|
63
|
+
const newIndex = state.selectedIndex + 1;
|
|
64
|
+
return {
|
|
65
|
+
selectedIndex: newIndex,
|
|
66
|
+
selectedBranchName: displayBranches[newIndex] ? displayBranches[newIndex].name : null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Search mode
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Enter search (filter) mode, resetting the query and cursor.
|
|
78
|
+
* @param {object} state
|
|
79
|
+
* @returns {object} State updates.
|
|
80
|
+
*/
|
|
81
|
+
function enterSearchMode(state) {
|
|
82
|
+
return {
|
|
83
|
+
searchMode: true,
|
|
84
|
+
searchQuery: '',
|
|
85
|
+
selectedIndex: 0,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Process a single keypress while in search mode.
|
|
91
|
+
*
|
|
92
|
+
* - Escape cancels the search and clears the filter.
|
|
93
|
+
* - Enter confirms the current filter and exits search mode.
|
|
94
|
+
* - Backspace removes the last character from the query.
|
|
95
|
+
* - Printable characters are appended to the query.
|
|
96
|
+
*
|
|
97
|
+
* @param {object} state
|
|
98
|
+
* @param {string} key - The raw key string from stdin.
|
|
99
|
+
* @returns {object|null} State updates, or null if the key was not handled.
|
|
100
|
+
*/
|
|
101
|
+
function handleSearchInput(state, key) {
|
|
102
|
+
if (isEscapeKey(key) || isEnterKey(key)) {
|
|
103
|
+
const updates = { searchMode: false };
|
|
104
|
+
if (isEscapeKey(key)) {
|
|
105
|
+
updates.searchQuery = '';
|
|
106
|
+
updates.filteredBranches = null;
|
|
107
|
+
}
|
|
108
|
+
return updates;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (isBackspaceKey(key)) {
|
|
112
|
+
const newQuery = state.searchQuery.slice(0, -1);
|
|
113
|
+
const filtered = filterBranches(state.branches, newQuery);
|
|
114
|
+
let newIndex = state.selectedIndex;
|
|
115
|
+
if (filtered && newIndex >= filtered.length) {
|
|
116
|
+
newIndex = Math.max(0, filtered.length - 1);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
searchQuery: newQuery,
|
|
120
|
+
filteredBranches: filtered,
|
|
121
|
+
selectedIndex: newIndex,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isPrintableChar(key)) {
|
|
126
|
+
const newQuery = state.searchQuery + key;
|
|
127
|
+
const filtered = filterBranches(state.branches, newQuery);
|
|
128
|
+
let newIndex = state.selectedIndex;
|
|
129
|
+
if (filtered && newIndex >= filtered.length) {
|
|
130
|
+
newIndex = Math.max(0, filtered.length - 1);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
searchQuery: newQuery,
|
|
134
|
+
filteredBranches: filtered,
|
|
135
|
+
selectedIndex: newIndex,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return null; // Key not handled
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Modal toggles
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Toggle the diff/preview panel.
|
|
148
|
+
* When opening, the caller is responsible for loading preview data
|
|
149
|
+
* asynchronously after applying the returned state updates.
|
|
150
|
+
* @param {object} state
|
|
151
|
+
* @returns {object} State updates.
|
|
152
|
+
*/
|
|
153
|
+
function togglePreview(state) {
|
|
154
|
+
if (state.previewMode) {
|
|
155
|
+
return { previewMode: false, previewData: null };
|
|
156
|
+
}
|
|
157
|
+
// Opening preview requires async data loading -- return flag indicating need.
|
|
158
|
+
return { previewMode: true };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Toggle the commit history panel.
|
|
163
|
+
* @param {object} state
|
|
164
|
+
* @returns {object} State updates.
|
|
165
|
+
*/
|
|
166
|
+
function toggleHistory(state) {
|
|
167
|
+
return { historyMode: !state.historyMode };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Toggle the info/help panel.
|
|
172
|
+
* @param {object} state
|
|
173
|
+
* @returns {object} State updates.
|
|
174
|
+
*/
|
|
175
|
+
function toggleInfo(state) {
|
|
176
|
+
return { infoMode: !state.infoMode };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Toggle the log viewer panel.
|
|
181
|
+
* No-ops when running without a server (`state.noServer`).
|
|
182
|
+
* @param {object} state
|
|
183
|
+
* @returns {object|null} State updates, or null if no server is configured.
|
|
184
|
+
*/
|
|
185
|
+
function toggleLogView(state) {
|
|
186
|
+
if (state.noServer) return null;
|
|
187
|
+
if (state.logViewMode) {
|
|
188
|
+
return { logViewMode: false, logScrollOffset: 0 };
|
|
189
|
+
}
|
|
190
|
+
return { logViewMode: true, logScrollOffset: 0 };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Close the action confirmation modal, clearing its data and loading flag.
|
|
195
|
+
* @param {object} state
|
|
196
|
+
* @returns {object} State updates.
|
|
197
|
+
*/
|
|
198
|
+
function closeActionModal(state) {
|
|
199
|
+
return { actionMode: false, actionData: null, actionLoading: false };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Log view actions
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Switch the active tab inside the log viewer.
|
|
208
|
+
* @param {object} state
|
|
209
|
+
* @param {string} tab - The tab identifier (e.g. 'server' or 'activity').
|
|
210
|
+
* @returns {object} State updates.
|
|
211
|
+
*/
|
|
212
|
+
function switchLogTab(state, tab) {
|
|
213
|
+
return { logViewTab: tab, logScrollOffset: 0 };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Scroll the log viewer up or down by one line.
|
|
218
|
+
* @param {object} state
|
|
219
|
+
* @param {'up'|'down'} direction
|
|
220
|
+
* @returns {object} State updates.
|
|
221
|
+
*/
|
|
222
|
+
function scrollLog(state, direction) {
|
|
223
|
+
const logData = state.logViewTab === 'server' ? state.serverLogBuffer : state.activityLog;
|
|
224
|
+
const maxScroll = Math.max(0, logData.length - 10);
|
|
225
|
+
|
|
226
|
+
if (direction === 'up') {
|
|
227
|
+
return { logScrollOffset: Math.min(state.logScrollOffset + 1, maxScroll) };
|
|
228
|
+
} else {
|
|
229
|
+
return { logScrollOffset: Math.max(0, state.logScrollOffset - 1) };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Settings
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Toggle the notification sound on or off.
|
|
239
|
+
* @param {object} state
|
|
240
|
+
* @returns {object} State updates.
|
|
241
|
+
*/
|
|
242
|
+
function toggleSound(state) {
|
|
243
|
+
return { soundEnabled: !state.soundEnabled };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Set the number of visible branches to an exact value.
|
|
248
|
+
* @param {object} state
|
|
249
|
+
* @param {number} count
|
|
250
|
+
* @returns {object} State updates.
|
|
251
|
+
*/
|
|
252
|
+
function setVisibleBranchCount(state, count) {
|
|
253
|
+
return { visibleBranchCount: count };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Increase the visible branch count by one, up to a screen-imposed maximum.
|
|
258
|
+
* @param {object} state
|
|
259
|
+
* @param {number} maxForScreen - Maximum branches that fit on the current terminal.
|
|
260
|
+
* @returns {object|null} State updates, or null if already at max.
|
|
261
|
+
*/
|
|
262
|
+
function increaseVisibleBranches(state, maxForScreen) {
|
|
263
|
+
if (state.visibleBranchCount < maxForScreen) {
|
|
264
|
+
return { visibleBranchCount: state.visibleBranchCount + 1 };
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Decrease the visible branch count by one, with a minimum of 1.
|
|
271
|
+
* @param {object} state
|
|
272
|
+
* @returns {object|null} State updates, or null if already at minimum.
|
|
273
|
+
*/
|
|
274
|
+
function decreaseVisibleBranches(state) {
|
|
275
|
+
if (state.visibleBranchCount > 1) {
|
|
276
|
+
return { visibleBranchCount: state.visibleBranchCount - 1 };
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Cleanup confirm modal
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Open the cleanup confirmation modal.
|
|
287
|
+
* @param {object} state
|
|
288
|
+
* @param {string[]} goneBranches - Branch names to be cleaned up
|
|
289
|
+
* @returns {object} State updates.
|
|
290
|
+
*/
|
|
291
|
+
function openCleanupConfirm(state, goneBranches) {
|
|
292
|
+
return {
|
|
293
|
+
cleanupConfirmMode: true,
|
|
294
|
+
cleanupBranches: goneBranches,
|
|
295
|
+
cleanupSelectedIndex: 0,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Close the cleanup confirmation modal.
|
|
301
|
+
* @param {object} state
|
|
302
|
+
* @returns {object} State updates.
|
|
303
|
+
*/
|
|
304
|
+
function closeCleanupConfirm(state) {
|
|
305
|
+
return {
|
|
306
|
+
cleanupConfirmMode: false,
|
|
307
|
+
cleanupBranches: null,
|
|
308
|
+
cleanupSelectedIndex: 0,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// Dismiss flash / error
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Dismiss the current flash message.
|
|
318
|
+
* @param {object} state
|
|
319
|
+
* @returns {object|null} State updates, or null if there is no flash message.
|
|
320
|
+
*/
|
|
321
|
+
function dismissFlash(state) {
|
|
322
|
+
if (state.flashMessage) {
|
|
323
|
+
return { flashMessage: null };
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Dismiss the current error toast.
|
|
330
|
+
* @param {object} state
|
|
331
|
+
* @returns {object|null} State updates, or null if there is no error toast.
|
|
332
|
+
*/
|
|
333
|
+
function dismissErrorToast(state) {
|
|
334
|
+
if (state.errorToast) {
|
|
335
|
+
return { errorToast: null };
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// Escape handler (normal mode)
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Handle the Escape key in normal mode.
|
|
346
|
+
*
|
|
347
|
+
* If a search filter is active, clears it. Otherwise signals a quit by
|
|
348
|
+
* returning `{ _quit: true }`.
|
|
349
|
+
*
|
|
350
|
+
* @param {object} state
|
|
351
|
+
* @returns {object} State updates (may include `_quit: true`).
|
|
352
|
+
*/
|
|
353
|
+
function handleEscape(state) {
|
|
354
|
+
if (state.searchQuery || state.filteredBranches) {
|
|
355
|
+
return { searchQuery: '', filteredBranches: null };
|
|
356
|
+
}
|
|
357
|
+
// Quit signal
|
|
358
|
+
return { _quit: true };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// Selection query
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Return the branch object that is currently highlighted, or null if the
|
|
367
|
+
* selection is out of range or the list is empty.
|
|
368
|
+
* @param {object} state
|
|
369
|
+
* @returns {object|null} The selected branch, or null.
|
|
370
|
+
*/
|
|
371
|
+
function getSelectedBranch(state) {
|
|
372
|
+
const displayBranches = getDisplayBranches(state);
|
|
373
|
+
if (displayBranches.length > 0 && state.selectedIndex < displayBranches.length) {
|
|
374
|
+
return displayBranches[state.selectedIndex];
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
// Exports
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
module.exports = {
|
|
384
|
+
// helpers
|
|
385
|
+
getDisplayBranches,
|
|
386
|
+
|
|
387
|
+
// navigation
|
|
388
|
+
moveUp,
|
|
389
|
+
moveDown,
|
|
390
|
+
|
|
391
|
+
// search
|
|
392
|
+
enterSearchMode,
|
|
393
|
+
handleSearchInput,
|
|
394
|
+
|
|
395
|
+
// modal toggles
|
|
396
|
+
togglePreview,
|
|
397
|
+
toggleHistory,
|
|
398
|
+
toggleInfo,
|
|
399
|
+
toggleLogView,
|
|
400
|
+
closeActionModal,
|
|
401
|
+
|
|
402
|
+
// cleanup confirm
|
|
403
|
+
openCleanupConfirm,
|
|
404
|
+
closeCleanupConfirm,
|
|
405
|
+
|
|
406
|
+
// log view
|
|
407
|
+
switchLogTab,
|
|
408
|
+
scrollLog,
|
|
409
|
+
|
|
410
|
+
// settings
|
|
411
|
+
toggleSound,
|
|
412
|
+
setVisibleBranchCount,
|
|
413
|
+
increaseVisibleBranches,
|
|
414
|
+
decreaseVisibleBranches,
|
|
415
|
+
|
|
416
|
+
// dismiss
|
|
417
|
+
dismissFlash,
|
|
418
|
+
dismissErrorToast,
|
|
419
|
+
|
|
420
|
+
// escape
|
|
421
|
+
handleEscape,
|
|
422
|
+
|
|
423
|
+
// selection
|
|
424
|
+
getSelectedBranch,
|
|
425
|
+
};
|