@nbtca/prompt 1.4.2 → 1.5.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/README.md +27 -58
- package/SECURITY.md +16 -45
- package/dist/app/app.js +167 -64
- package/dist/app/chrome.js +67 -50
- package/dist/app/fields/list-field.js +12 -25
- package/dist/app/fields/text-field.js +3 -8
- package/dist/app/frame.js +16 -23
- package/dist/app/keys.js +110 -2
- package/dist/app/views/docs-render.js +34 -26
- package/dist/app/views/docs.js +280 -68
- package/dist/app/views/events-render.js +21 -27
- package/dist/app/views/events.js +57 -33
- package/dist/app/views/home.js +67 -47
- package/dist/app/views/schedule-grid-cursor.js +9 -18
- package/dist/app/views/schedule-render.js +51 -74
- package/dist/app/views/schedule.js +246 -101
- package/dist/app/views/settings-render.js +8 -19
- package/dist/app/views/settings.js +92 -17
- package/dist/auth/cookie-transport.js +31 -32
- package/dist/auth/errors.js +3 -1
- package/dist/auth/nbt-auth.js +42 -25
- package/dist/auth/session-store.js +17 -9
- package/dist/cli.js +570 -0
- package/dist/config/data.js +9 -11
- package/dist/config/preferences.js +21 -7
- package/dist/core/calendar-day.js +37 -0
- package/dist/core/canvas.js +1 -0
- package/dist/core/capabilities.js +6 -3
- package/dist/core/components/confirm.js +9 -8
- package/dist/core/components/menu.js +64 -38
- package/dist/core/components/messages.js +12 -4
- package/dist/core/components/painter.js +3 -1
- package/dist/core/components/spinner.js +34 -7
- package/dist/core/components/text-input.js +24 -18
- package/dist/core/icons.js +2 -2
- package/dist/core/logo.js +23 -5
- package/dist/core/motion.js +25 -19
- package/dist/core/text.js +186 -69
- package/dist/core/theme.js +0 -28
- package/dist/core/transitions.js +2 -2
- package/dist/core/ui.js +15 -13
- package/dist/core/vim-keys.js +156 -19
- package/dist/features/about.js +23 -0
- package/dist/features/calendar-heatmap.js +16 -40
- package/dist/features/calendar-query.js +1 -2
- package/dist/features/calendar-store.js +27 -0
- package/dist/features/calendar.js +66 -190
- package/dist/features/docs-client.js +225 -0
- package/dist/features/docs.js +615 -298
- package/dist/features/links.js +44 -29
- package/dist/features/schedule-render.js +65 -101
- package/dist/features/schedule-store.js +51 -9
- package/dist/features/schedule-view.js +46 -213
- package/dist/features/status.js +117 -60
- package/dist/features/student-timetable.js +74 -97
- package/dist/features/theme.js +9 -5
- package/dist/features/timetable-sanitize.js +40 -0
- package/dist/features/update.js +12 -29
- package/dist/i18n/index.js +83 -19
- package/dist/i18n/locales/en.json +8 -3
- package/dist/i18n/locales/zh.json +8 -3
- package/dist/index.js +6 -474
- package/dist/logo/ca-dotmatrix.txt +16 -18
- package/dist/main.js +7 -48
- package/package.json +28 -18
- package/bin/nbtca-welcome.js +0 -2
- package/dist/core/components/screen.js +0 -18
- package/dist/core/menu.js +0 -68
- package/dist/features/schedule-query.js +0 -47
- package/dist/features/settings.js +0 -127
- package/dist/logo/ca-logo.png +0 -0
package/README.md
CHANGED
|
@@ -1,89 +1,58 @@
|
|
|
1
1
|
# NBTCA Prompt
|
|
2
2
|
|
|
3
|
-
Terminal
|
|
3
|
+
Terminal client for NBTCA events, documentation, status and personal timetables.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@nbtca/prompt)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
+
## Requirements
|
|
9
|
+
|
|
10
|
+
- Node.js 20.12 or newer
|
|
11
|
+
|
|
8
12
|
## Install
|
|
9
13
|
|
|
10
14
|
```bash
|
|
11
|
-
npm install
|
|
15
|
+
npm install --global @nbtca/prompt
|
|
16
|
+
nbtca
|
|
12
17
|
```
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
Run a command without installing:
|
|
15
20
|
|
|
16
21
|
```bash
|
|
17
|
-
npx @nbtca/prompt
|
|
22
|
+
npx @nbtca/prompt --help
|
|
18
23
|
```
|
|
19
24
|
|
|
20
|
-
##
|
|
25
|
+
## Commands
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
```bash
|
|
27
|
+
```text
|
|
28
|
+
nbtca events [--json]
|
|
29
|
+
nbtca docs
|
|
30
|
+
nbtca status
|
|
28
31
|
nbtca schedule login
|
|
29
32
|
nbtca schedule terms
|
|
30
33
|
nbtca schedule export --term=2026:3 --week-one=YYYY-MM-DD
|
|
31
34
|
nbtca schedule logout
|
|
32
35
|
```
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
Use `--plain` for stable output without color. `schedule export --one-shot`
|
|
38
|
+
avoids reading or saving a campus session.
|
|
39
|
+
|
|
40
|
+
Prompt never stores passwords. Persisted sessions contain only a masked account
|
|
41
|
+
hint and a CookieJar, protected with user-only permissions on POSIX systems.
|
|
42
|
+
Treat exported calendars and session files as private data. See [SECURITY.md](SECURITY.md).
|
|
43
|
+
|
|
44
|
+
## Development
|
|
36
45
|
|
|
37
46
|
```bash
|
|
38
|
-
|
|
47
|
+
npm ci
|
|
48
|
+
npm run check
|
|
39
49
|
```
|
|
40
50
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
CookieJar in the user's state directory:
|
|
44
|
-
|
|
45
|
-
- Unix/macOS default: `~/.local/state/nbtca/session.json`
|
|
46
|
-
- Windows default: `%LOCALAPPDATA%/nbtca/session.json`
|
|
47
|
-
- `$XDG_STATE_HOME` is honored when it is an absolute path
|
|
48
|
-
|
|
49
|
-
On POSIX systems the directory is `0700` and the file is `0600`. `npx`, a local
|
|
50
|
-
installation and a global installation therefore share the same state without
|
|
51
|
-
depending on npm's disposable package cache. The session is a bearer secret;
|
|
52
|
-
use `--one-shot` on shared machines and `schedule logout` when finished.
|
|
53
|
-
Saved sessions use a sliding seven-day local expiry and are cleared immediately
|
|
54
|
-
when the school reports that they have expired.
|
|
55
|
-
|
|
56
|
-
JWXT currently returns week numbers and period times but not the first calendar
|
|
57
|
-
date of a term. When no authoritative date map is available, Prompt asks for
|
|
58
|
-
the first teaching Monday or accepts `--week-one=YYYY-MM-DD`. Confirm it against
|
|
59
|
-
the official school calendar; Prompt will not guess. A slider or other browser
|
|
60
|
-
challenge is also never bypassed—the CLI stops with an actionable message.
|
|
61
|
-
Without an authoritative date map, the result is a base teaching-week schedule:
|
|
62
|
-
holidays, make-up classes and temporary changes still require school notices.
|
|
63
|
-
|
|
64
|
-
## Documentation
|
|
65
|
-
|
|
66
|
-
Project documentation has been moved to the GitHub Wiki.
|
|
67
|
-
|
|
68
|
-
- [Home](https://github.com/nbtca/Prompt/wiki)
|
|
69
|
-
- [Getting Started](https://github.com/nbtca/Prompt/wiki/Getting-Started)
|
|
70
|
-
- [Development Guide](https://github.com/nbtca/Prompt/wiki/Development-Guide)
|
|
71
|
-
- [Terminal UX](https://github.com/nbtca/Prompt/wiki/Terminal-UX)
|
|
72
|
-
- [Release Notes](https://github.com/nbtca/Prompt/wiki/Release-Notes)
|
|
73
|
-
- [Features](https://github.com/nbtca/Prompt/wiki/Features)
|
|
74
|
-
- [Terminal Compatibility](https://github.com/nbtca/Prompt/wiki/Terminal-Compatibility)
|
|
75
|
-
- [FAQ](https://github.com/nbtca/Prompt/wiki/FAQ)
|
|
76
|
-
|
|
77
|
-
## Requirements
|
|
51
|
+
`check` runs formatting, lint, full TypeScript validation, tests, build, package
|
|
52
|
+
consumer checks and dependency audit.
|
|
78
53
|
|
|
79
|
-
|
|
54
|
+
Project guides and release notes live in the [Wiki](https://github.com/nbtca/Prompt/wiki).
|
|
80
55
|
|
|
81
56
|
## License
|
|
82
57
|
|
|
83
58
|
MIT
|
|
84
|
-
|
|
85
|
-
## Links
|
|
86
|
-
|
|
87
|
-
- Website: https://nbtca.space
|
|
88
|
-
- GitHub: https://github.com/nbtca
|
|
89
|
-
- NPM: https://www.npmjs.com/package/@nbtca/prompt
|
package/SECURITY.md
CHANGED
|
@@ -1,47 +1,18 @@
|
|
|
1
1
|
# Security Policy
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Errors expose only stable local codes and stages. Response bodies, redirect
|
|
21
|
-
queries, cookies, encrypted passwords and underlying network error objects are
|
|
22
|
-
not included in user-facing output.
|
|
23
|
-
|
|
24
|
-
## Persisted session
|
|
25
|
-
|
|
26
|
-
The optional persisted CookieJar is a bearer secret. It is stored under the
|
|
27
|
-
user's state directory with a versioned schema, an atomic write, a `0700`
|
|
28
|
-
directory and a `0600` file on POSIX systems. The saved account hint is masked,
|
|
29
|
-
and Prompt never adds the full student id or password to the state schema. The
|
|
30
|
-
opaque school cookies must still be treated as bearer secrets. Sessions have a
|
|
31
|
-
sliding seven-day local expiry.
|
|
32
|
-
|
|
33
|
-
Use `nbtca schedule logout` to clear it. Use `--one-shot` on a shared or
|
|
34
|
-
untrusted computer so no session is read or written.
|
|
35
|
-
|
|
36
|
-
## Timetable and ICS privacy
|
|
37
|
-
|
|
38
|
-
Raw JWXT responses are processed in memory. Only timetable fields needed for
|
|
39
|
-
normalization are passed to `@nbtca/nbtcal`; the student profile object is
|
|
40
|
-
discarded. Generated ICS files can reveal a person's location and routine, so
|
|
41
|
-
they are created with mode `0600` where supported and should not be uploaded to
|
|
42
|
-
public or "secret-link" hosting.
|
|
43
|
-
|
|
44
|
-
## Reporting vulnerabilities
|
|
45
|
-
|
|
46
|
-
Use the repository's GitHub Security Advisory page. Do not include credentials,
|
|
47
|
-
cookies, raw school responses or a personal ICS in a public issue.
|
|
3
|
+
Prompt sends credentials only to the allowlisted campus WebVPN and CAS login
|
|
4
|
+
flow. Passwords stay in memory, are masked during entry and are never accepted
|
|
5
|
+
through flags, environment variables or files. Interactive browser challenges
|
|
6
|
+
fail closed.
|
|
7
|
+
|
|
8
|
+
An optional persisted CookieJar is a bearer secret. Prompt writes it atomically
|
|
9
|
+
to the user's state directory with `0700` directory and `0600` file permissions
|
|
10
|
+
on POSIX systems. Use `nbtca schedule logout` to remove it, or `--one-shot` to
|
|
11
|
+
avoid reading or writing a session.
|
|
12
|
+
|
|
13
|
+
Exported calendars may reveal a person's routine and location. Keep session and
|
|
14
|
+
ICS files private.
|
|
15
|
+
|
|
16
|
+
Report vulnerabilities through GitHub Security Advisories. Never include
|
|
17
|
+
credentials, cookies, raw campus responses or personal calendars in public
|
|
18
|
+
issues.
|
package/dist/app/app.js
CHANGED
|
@@ -1,24 +1,14 @@
|
|
|
1
1
|
import { ansi, ensureCursorRestored } from '../core/canvas.js';
|
|
2
|
-
import {
|
|
3
|
-
import { routeGlobalKey } from './keys.js';
|
|
4
|
-
import { renderHeader, renderFooter,
|
|
2
|
+
import { composeFrameLines, computeBodyRows, diffFrame } from './frame.js';
|
|
3
|
+
import { isPrintableKey, KeyStreamDecoder, routeGlobalKey } from './keys.js';
|
|
4
|
+
import { renderHeader, renderFooter, resolveChromeLayout } from './chrome.js';
|
|
5
5
|
import { homeView } from './views/home.js';
|
|
6
6
|
import { scheduleView } from './views/schedule.js';
|
|
7
7
|
import { docsView } from './views/docs.js';
|
|
8
8
|
import { eventsView } from './views/events.js';
|
|
9
9
|
import { settingsView } from './views/settings.js';
|
|
10
10
|
import { getAppTabs } from './tabs.js';
|
|
11
|
-
|
|
12
|
-
* Event-driven full-screen app loop. Owns the alt-screen + raw-mode lifecycle
|
|
13
|
-
* and composes every tab as a native `View` rendered in place. `ctx.runClassic`
|
|
14
|
-
* remains as a scoped escape hatch a view can call itself when a single action
|
|
15
|
-
* genuinely needs the real terminal (e.g. Docs handing off to glow/less to
|
|
16
|
-
* read a file) — the app loop no longer dispatches whole tabs through it.
|
|
17
|
-
*
|
|
18
|
-
* Resolves once the user quits (q / Ctrl+C / Esc from home). Terminal state
|
|
19
|
-
* (alt-screen, raw mode, cursor) is always restored before this resolves,
|
|
20
|
-
* on SIGINT, on process exit, and even if an unexpected error is thrown.
|
|
21
|
-
*/
|
|
11
|
+
import { SPINNER_FRAME_MS } from '../core/components/spinner.js';
|
|
22
12
|
export async function runApp() {
|
|
23
13
|
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
24
14
|
return;
|
|
@@ -26,8 +16,14 @@ export async function runApp() {
|
|
|
26
16
|
let scroll = 0;
|
|
27
17
|
let running = true;
|
|
28
18
|
let suspended = false;
|
|
19
|
+
let entered = false;
|
|
20
|
+
const lifecycle = new AbortController();
|
|
21
|
+
const keyDecoder = new KeyStreamDecoder();
|
|
22
|
+
let keyFlushTimer;
|
|
23
|
+
let clockTimer;
|
|
24
|
+
let busyTimer;
|
|
25
|
+
let painted;
|
|
29
26
|
const viewIds = getAppTabs().map((tab) => tab.id);
|
|
30
|
-
// Every tab is a native View rendered in place inside the alt-screen frame.
|
|
31
27
|
const nativeViews = {
|
|
32
28
|
home: homeView,
|
|
33
29
|
schedule: scheduleView,
|
|
@@ -35,16 +31,42 @@ export async function runApp() {
|
|
|
35
31
|
events: eventsView,
|
|
36
32
|
settings: settingsView,
|
|
37
33
|
};
|
|
34
|
+
const pendingLoads = new Map();
|
|
35
|
+
function loadView(id) {
|
|
36
|
+
const target = nativeViews[id];
|
|
37
|
+
if (!target?.load || pendingLoads.has(id))
|
|
38
|
+
return;
|
|
39
|
+
const pending = target
|
|
40
|
+
.load(ctx)
|
|
41
|
+
.catch(() => undefined)
|
|
42
|
+
.finally(() => pendingLoads.delete(id));
|
|
43
|
+
pendingLoads.set(id, pending);
|
|
44
|
+
}
|
|
38
45
|
function size() {
|
|
39
46
|
return { rows: process.stdout.rows || 24, cols: process.stdout.columns || 80 };
|
|
40
47
|
}
|
|
41
48
|
const ctx = {
|
|
42
|
-
|
|
43
|
-
get
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
signal: lifecycle.signal,
|
|
50
|
+
get size() {
|
|
51
|
+
return size();
|
|
52
|
+
},
|
|
53
|
+
get bodyRows() {
|
|
54
|
+
const { rows } = size();
|
|
55
|
+
const chrome = resolveChromeLayout(rows);
|
|
56
|
+
return computeBodyRows(rows, chrome.headerLines, chrome.footerLines);
|
|
57
|
+
},
|
|
58
|
+
rerender() {
|
|
59
|
+
render();
|
|
60
|
+
},
|
|
61
|
+
resetScroll() {
|
|
62
|
+
scroll = 0;
|
|
63
|
+
},
|
|
64
|
+
runClassic(fn) {
|
|
65
|
+
return runClassic(fn);
|
|
66
|
+
},
|
|
67
|
+
quit() {
|
|
68
|
+
quit();
|
|
69
|
+
},
|
|
48
70
|
};
|
|
49
71
|
function render() {
|
|
50
72
|
if (suspended || !running)
|
|
@@ -52,24 +74,38 @@ export async function runApp() {
|
|
|
52
74
|
const { rows, cols } = size();
|
|
53
75
|
const active = nativeViews[view];
|
|
54
76
|
const tabs = getAppTabs();
|
|
55
|
-
const
|
|
56
|
-
const
|
|
77
|
+
const chrome = resolveChromeLayout(rows);
|
|
78
|
+
const header = renderHeader(tabs, view, cols, chrome.headerLines);
|
|
79
|
+
const footer = renderFooter(view, cols, tabs.length, active?.footerHint?.(tabs.length, cols), chrome.footerLines);
|
|
57
80
|
const body = active?.render(ctx) ?? [];
|
|
58
|
-
|
|
81
|
+
const bodyScroll = active?.capturesInput?.() ? Number.MAX_SAFE_INTEGER : scroll;
|
|
82
|
+
const lines = composeFrameLines(header, body, footer, rows, cols, bodyScroll);
|
|
83
|
+
const patch = diffFrame(painted?.cols === cols ? painted.lines : undefined, lines);
|
|
84
|
+
painted = { cols, lines };
|
|
85
|
+
if (patch)
|
|
86
|
+
process.stdout.write(patch);
|
|
87
|
+
scheduleBusyTick(active?.isBusy?.() === true);
|
|
59
88
|
}
|
|
60
|
-
function
|
|
61
|
-
|
|
89
|
+
function scheduleBusyTick(busy) {
|
|
90
|
+
if (busy && running && busyTimer === undefined) {
|
|
91
|
+
busyTimer = setTimeout(() => {
|
|
92
|
+
busyTimer = undefined;
|
|
93
|
+
render();
|
|
94
|
+
}, SPINNER_FRAME_MS);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (busy || busyTimer === undefined)
|
|
98
|
+
return;
|
|
99
|
+
clearTimeout(busyTimer);
|
|
100
|
+
busyTimer = undefined;
|
|
101
|
+
}
|
|
102
|
+
function dispatchKey(key) {
|
|
62
103
|
if (key === '\x03') {
|
|
104
|
+
process.exitCode = 130;
|
|
63
105
|
quit();
|
|
64
106
|
return;
|
|
65
107
|
} // Ctrl-C always quits, even mid-capture.
|
|
66
108
|
const active = nativeViews[view];
|
|
67
|
-
// Esc always reaches global routing, even while a view "captures" input
|
|
68
|
-
// for a focused field (login/search text entry). Without this carve-out,
|
|
69
|
-
// a view whose own Esc-handling doesn't escape its captured mode would
|
|
70
|
-
// trap the user on that tab with no way out except Ctrl-C (quitting the
|
|
71
|
-
// whole app). Esc must never be swallowed silently — it's the universal
|
|
72
|
-
// way out of anything.
|
|
73
109
|
if (active?.capturesInput?.() && key !== '\x1b') {
|
|
74
110
|
active.handleKey?.(key, ctx);
|
|
75
111
|
render();
|
|
@@ -81,17 +117,13 @@ export async function runApp() {
|
|
|
81
117
|
return;
|
|
82
118
|
}
|
|
83
119
|
if (g.back) {
|
|
84
|
-
// Esc steps back one level within the view first (e.g. its week grid
|
|
85
|
-
// back to its own hub) — only once the view has nowhere left to step
|
|
86
|
-
// back to does Esc leave the tab for Home. Matches how k9s/lazygit
|
|
87
|
-
// treat Esc: back one level, not straight to the root.
|
|
88
120
|
if (active?.handleBack?.(ctx)) {
|
|
89
121
|
scroll = 0; // the new sub-view's content height has nothing to do with the old one's
|
|
90
122
|
render();
|
|
91
123
|
return;
|
|
92
124
|
}
|
|
93
125
|
view = 'home';
|
|
94
|
-
|
|
126
|
+
loadView('home');
|
|
95
127
|
render();
|
|
96
128
|
return;
|
|
97
129
|
}
|
|
@@ -100,9 +132,12 @@ export async function runApp() {
|
|
|
100
132
|
return;
|
|
101
133
|
}
|
|
102
134
|
if (g.scrollBy) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
135
|
+
if (active?.capturesPageKeys?.()) {
|
|
136
|
+
active.handleKey?.(key, ctx);
|
|
137
|
+
scroll = 0;
|
|
138
|
+
render();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
106
141
|
const page = Math.max(1, ctx.bodyRows - 2);
|
|
107
142
|
scroll = Math.max(0, scroll + g.scrollBy * page);
|
|
108
143
|
render();
|
|
@@ -111,35 +146,85 @@ export async function runApp() {
|
|
|
111
146
|
active?.handleKey?.(key, ctx);
|
|
112
147
|
render();
|
|
113
148
|
}
|
|
149
|
+
function dispatchKeys(keys) {
|
|
150
|
+
for (let index = 0; index < keys.length && running && !suspended; index += 1) {
|
|
151
|
+
let key = keys[index];
|
|
152
|
+
if (key === undefined)
|
|
153
|
+
continue;
|
|
154
|
+
if (nativeViews[view]?.capturesInput?.() && isPrintableKey(key)) {
|
|
155
|
+
while (index + 1 < keys.length) {
|
|
156
|
+
const next = keys[index + 1];
|
|
157
|
+
if (next === undefined || !isPrintableKey(next))
|
|
158
|
+
break;
|
|
159
|
+
key += next;
|
|
160
|
+
index += 1;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
dispatchKey(key);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function scheduleClock() {
|
|
167
|
+
if (!running)
|
|
168
|
+
return;
|
|
169
|
+
clockTimer = setTimeout(() => {
|
|
170
|
+
clockTimer = undefined;
|
|
171
|
+
render();
|
|
172
|
+
scheduleClock();
|
|
173
|
+
}, 60_000 - (Date.now() % 60_000));
|
|
174
|
+
}
|
|
175
|
+
function clearKeyFlush() {
|
|
176
|
+
if (keyFlushTimer === undefined)
|
|
177
|
+
return;
|
|
178
|
+
clearTimeout(keyFlushTimer);
|
|
179
|
+
keyFlushTimer = undefined;
|
|
180
|
+
}
|
|
181
|
+
function onKey(data) {
|
|
182
|
+
clearKeyFlush();
|
|
183
|
+
dispatchKeys(keyDecoder.write(data));
|
|
184
|
+
if (!running || suspended || !keyDecoder.hasPending)
|
|
185
|
+
return;
|
|
186
|
+
keyFlushTimer = setTimeout(() => {
|
|
187
|
+
keyFlushTimer = undefined;
|
|
188
|
+
dispatchKeys(keyDecoder.flush());
|
|
189
|
+
}, 20);
|
|
190
|
+
}
|
|
114
191
|
function enter() {
|
|
192
|
+
if (entered || lifecycle.signal.aborted)
|
|
193
|
+
return;
|
|
194
|
+
entered = true;
|
|
195
|
+
painted = undefined;
|
|
196
|
+
keyDecoder.reset();
|
|
115
197
|
ensureCursorRestored();
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
process.stdin.
|
|
119
|
-
|
|
120
|
-
|
|
198
|
+
try {
|
|
199
|
+
process.stdout.write(ansi.enterAlt + ansi.hideCursor);
|
|
200
|
+
if (process.stdin.isTTY)
|
|
201
|
+
process.stdin.setRawMode(true);
|
|
202
|
+
process.stdin.resume();
|
|
203
|
+
process.stdin.on('data', onKey);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
leave();
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
121
209
|
}
|
|
122
210
|
function leave() {
|
|
211
|
+
if (!entered)
|
|
212
|
+
return;
|
|
213
|
+
entered = false;
|
|
214
|
+
clearKeyFlush();
|
|
215
|
+
keyDecoder.reset();
|
|
123
216
|
process.stdin.removeListener('data', onKey);
|
|
124
217
|
if (process.stdin.isTTY)
|
|
125
218
|
process.stdin.setRawMode(false);
|
|
126
219
|
process.stdout.write(ansi.showCursor + ansi.leaveAlt);
|
|
127
|
-
// `enter()` calls `stdin.resume()`; a resumed stdin stream keeps the
|
|
128
|
-
// Node event loop alive by design even with no listeners attached. The
|
|
129
|
-
// classic bridge calls `enter()` again right after, so pausing here is
|
|
130
|
-
// always safe — either it's about to be resumed, or the app is quitting
|
|
131
|
-
// for good and this is what lets the process actually exit.
|
|
132
220
|
process.stdin.pause();
|
|
133
221
|
}
|
|
134
222
|
function switchTo(id) {
|
|
135
223
|
scroll = 0;
|
|
136
224
|
view = id;
|
|
137
|
-
|
|
225
|
+
loadView(id);
|
|
138
226
|
render();
|
|
139
227
|
}
|
|
140
|
-
// A classic surface (currently only Docs' glow/less pager) owns its own
|
|
141
|
-
// raw-mode + rendering, so the app must fully leave() the alt-screen
|
|
142
|
-
// before invoking it and re-enter() after it returns.
|
|
143
228
|
async function runClassic(fn) {
|
|
144
229
|
suspended = true;
|
|
145
230
|
leave();
|
|
@@ -147,11 +232,10 @@ export async function runApp() {
|
|
|
147
232
|
await fn();
|
|
148
233
|
}
|
|
149
234
|
catch (err) {
|
|
150
|
-
// Classic surfaces are expected to surface their own errors, but if
|
|
151
|
-
// one throws anyway, don't swallow it silently: leave() has already
|
|
152
|
-
// restored cooked mode, so writing to stderr here is visible.
|
|
153
235
|
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
154
236
|
}
|
|
237
|
+
if (!running)
|
|
238
|
+
return;
|
|
155
239
|
enter();
|
|
156
240
|
suspended = false;
|
|
157
241
|
render();
|
|
@@ -163,10 +247,18 @@ export async function runApp() {
|
|
|
163
247
|
leave();
|
|
164
248
|
}
|
|
165
249
|
function onSigint() {
|
|
250
|
+
process.exitCode = 130;
|
|
251
|
+
quit();
|
|
252
|
+
}
|
|
253
|
+
function onSigterm() {
|
|
254
|
+
process.exitCode = 143;
|
|
166
255
|
quit();
|
|
167
|
-
process.exit(0);
|
|
168
256
|
}
|
|
169
|
-
|
|
257
|
+
function onSighup() {
|
|
258
|
+
process.exitCode = 129;
|
|
259
|
+
quit();
|
|
260
|
+
}
|
|
261
|
+
let resolveRun;
|
|
170
262
|
const done = new Promise((resolve) => {
|
|
171
263
|
resolveRun = resolve;
|
|
172
264
|
});
|
|
@@ -174,6 +266,12 @@ export async function runApp() {
|
|
|
174
266
|
if (!running)
|
|
175
267
|
return;
|
|
176
268
|
running = false;
|
|
269
|
+
lifecycle.abort();
|
|
270
|
+
if (clockTimer !== undefined) {
|
|
271
|
+
clearTimeout(clockTimer);
|
|
272
|
+
clockTimer = undefined;
|
|
273
|
+
}
|
|
274
|
+
scheduleBusyTick(false);
|
|
177
275
|
try {
|
|
178
276
|
leave();
|
|
179
277
|
}
|
|
@@ -181,22 +279,27 @@ export async function runApp() {
|
|
|
181
279
|
process.stdout.removeListener('resize', onResize);
|
|
182
280
|
process.removeListener('exit', onExit);
|
|
183
281
|
process.removeListener('SIGINT', onSigint);
|
|
282
|
+
process.removeListener('SIGTERM', onSigterm);
|
|
283
|
+
process.removeListener('SIGHUP', onSighup);
|
|
184
284
|
resolveRun();
|
|
185
285
|
}
|
|
186
286
|
}
|
|
187
287
|
process.on('exit', onExit);
|
|
188
288
|
process.stdout.on('resize', onResize);
|
|
189
289
|
process.once('SIGINT', onSigint);
|
|
290
|
+
process.once('SIGTERM', onSigterm);
|
|
291
|
+
process.once('SIGHUP', onSighup);
|
|
190
292
|
try {
|
|
191
293
|
enter();
|
|
192
|
-
|
|
294
|
+
loadView('home');
|
|
193
295
|
render();
|
|
296
|
+
scheduleClock();
|
|
194
297
|
await done;
|
|
195
298
|
}
|
|
196
299
|
finally {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
300
|
+
quit();
|
|
301
|
+
await Promise.allSettled(Object.values(nativeViews).map(async (target) => {
|
|
302
|
+
await target.dispose?.();
|
|
303
|
+
}));
|
|
201
304
|
}
|
|
202
305
|
}
|