@gjsify/readline 0.1.0

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 ADDED
@@ -0,0 +1,33 @@
1
+ # @gjsify/readline
2
+
3
+ GJS implementation of the Node.js `readline` module. Provides Interface, createInterface, question, prompt, and async iterator support.
4
+
5
+ Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @gjsify/readline
11
+ # or
12
+ yarn add @gjsify/readline
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```typescript
18
+ import { createInterface } from '@gjsify/readline';
19
+
20
+ const rl = createInterface({
21
+ input: process.stdin,
22
+ output: process.stdout,
23
+ });
24
+
25
+ rl.question('What is your name? ', (answer) => {
26
+ console.log(`Hello, ${answer}!`);
27
+ rl.close();
28
+ });
29
+ ```
30
+
31
+ ## License
32
+
33
+ MIT
@@ -0,0 +1,258 @@
1
+ import { EventEmitter } from "node:events";
2
+ class Interface extends EventEmitter {
3
+ terminal;
4
+ line = "";
5
+ cursor = 0;
6
+ _input;
7
+ _output;
8
+ _prompt;
9
+ _closed = false;
10
+ _paused = false;
11
+ history;
12
+ _historySize;
13
+ _crlfDelay;
14
+ _lineBuffer = "";
15
+ _questionCallback = null;
16
+ constructor(input, output) {
17
+ super();
18
+ let opts;
19
+ if (input && typeof input === "object" && !("read" in input && typeof input.read === "function")) {
20
+ opts = input;
21
+ } else {
22
+ opts = { input, output };
23
+ }
24
+ this._input = opts.input || null;
25
+ this._output = opts.output || null;
26
+ this._prompt = opts.prompt || "> ";
27
+ this.terminal = opts.terminal ?? this._output !== null;
28
+ this._historySize = opts.historySize ?? 30;
29
+ this.history = [];
30
+ this._crlfDelay = opts.crlfDelay ?? 100;
31
+ if (this._input) {
32
+ this._input.on("data", (chunk) => this._onData(chunk));
33
+ this._input.on("end", () => this._onEnd());
34
+ this._input.on("error", (err) => this.emit("error", err));
35
+ if ("setEncoding" in this._input && typeof this._input.setEncoding === "function") {
36
+ this._input.setEncoding("utf8");
37
+ }
38
+ }
39
+ }
40
+ _onData(chunk) {
41
+ if (this._closed || this._paused) return;
42
+ const str = typeof chunk === "string" ? chunk : chunk.toString("utf8");
43
+ this._lineBuffer += str;
44
+ const lineEnd = /\r\n|\r|\n/;
45
+ let m;
46
+ while ((m = lineEnd.exec(this._lineBuffer)) !== null) {
47
+ const line = this._lineBuffer.substring(0, m.index);
48
+ this._lineBuffer = this._lineBuffer.substring(m.index + m[0].length);
49
+ this._onLine(line);
50
+ }
51
+ }
52
+ _onLine(line) {
53
+ if (line.length > 0 && this._historySize > 0) {
54
+ if (this.history.length === 0 || this.history[0] !== line) {
55
+ this.history.unshift(line);
56
+ if (this.history.length > this._historySize) {
57
+ this.history.pop();
58
+ }
59
+ }
60
+ }
61
+ if (this._questionCallback) {
62
+ const cb = this._questionCallback;
63
+ this._questionCallback = null;
64
+ cb(line);
65
+ }
66
+ this.emit("line", line);
67
+ }
68
+ _onEnd() {
69
+ if (this._lineBuffer.length > 0) {
70
+ this._onLine(this._lineBuffer);
71
+ this._lineBuffer = "";
72
+ }
73
+ this.close();
74
+ }
75
+ /** Set the prompt string. */
76
+ setPrompt(prompt) {
77
+ this._prompt = prompt;
78
+ }
79
+ /** Get the current prompt string. */
80
+ getPrompt() {
81
+ return this._prompt;
82
+ }
83
+ /** Write the prompt to the output stream. */
84
+ prompt(preserveCursor) {
85
+ if (this._closed) {
86
+ throw new Error("readline was closed");
87
+ }
88
+ if (this._output) {
89
+ this._output.write(this._prompt);
90
+ }
91
+ }
92
+ question(query, optionsOrCallback, callback) {
93
+ if (this._closed) {
94
+ throw new Error("readline was closed");
95
+ }
96
+ const cb = typeof optionsOrCallback === "function" ? optionsOrCallback : callback;
97
+ this._questionCallback = cb;
98
+ if (this._output) {
99
+ this._output.write(query);
100
+ }
101
+ }
102
+ /** Write data to the output stream. */
103
+ write(data, key) {
104
+ if (this._closed) return;
105
+ if (data !== null && data !== void 0) {
106
+ if (this._output) {
107
+ this._output.write(data);
108
+ }
109
+ }
110
+ }
111
+ /** Close the interface. */
112
+ close() {
113
+ if (this._closed) return;
114
+ this._closed = true;
115
+ if (this._input) {
116
+ this._input.removeAllListeners("data");
117
+ this._input.removeAllListeners("end");
118
+ }
119
+ this.emit("close");
120
+ }
121
+ /** Pause the input stream. */
122
+ pause() {
123
+ if (this._closed) return this;
124
+ this._paused = true;
125
+ if (this._input && "pause" in this._input && typeof this._input.pause === "function") {
126
+ this._input.pause();
127
+ }
128
+ this.emit("pause");
129
+ return this;
130
+ }
131
+ /** Resume the input stream. */
132
+ resume() {
133
+ if (this._closed) return this;
134
+ this._paused = false;
135
+ if (this._input && "resume" in this._input && typeof this._input.resume === "function") {
136
+ this._input.resume();
137
+ }
138
+ this.emit("resume");
139
+ return this;
140
+ }
141
+ /** Get the current line content. */
142
+ getCursorPos() {
143
+ return { rows: 0, cols: this.cursor };
144
+ }
145
+ [Symbol.asyncIterator]() {
146
+ const lines = [];
147
+ let resolve = null;
148
+ let done = false;
149
+ this.on("line", (line) => {
150
+ if (resolve) {
151
+ const r = resolve;
152
+ resolve = null;
153
+ r({ value: line, done: false });
154
+ } else {
155
+ lines.push(line);
156
+ }
157
+ });
158
+ this.on("close", () => {
159
+ done = true;
160
+ if (resolve) {
161
+ const r = resolve;
162
+ resolve = null;
163
+ r({ value: void 0, done: true });
164
+ }
165
+ });
166
+ return {
167
+ next() {
168
+ if (lines.length > 0) {
169
+ return Promise.resolve({ value: lines.shift(), done: false });
170
+ }
171
+ if (done) {
172
+ return Promise.resolve({ value: void 0, done: true });
173
+ }
174
+ return new Promise((r) => {
175
+ resolve = r;
176
+ });
177
+ },
178
+ return() {
179
+ done = true;
180
+ return Promise.resolve({ value: void 0, done: true });
181
+ },
182
+ [Symbol.asyncIterator]() {
183
+ return this;
184
+ }
185
+ };
186
+ }
187
+ }
188
+ function createInterface(input, output, completer, terminal) {
189
+ if (typeof input === "object" && input !== null && !("read" in input && typeof input.read === "function")) {
190
+ return new Interface(input);
191
+ }
192
+ return new Interface({ input, output, completer, terminal });
193
+ }
194
+ function clearLine(stream, dir, callback) {
195
+ if (!stream || typeof stream.write !== "function") {
196
+ if (callback) callback();
197
+ return true;
198
+ }
199
+ const code = dir < 0 ? "\x1B[1K" : dir > 0 ? "\x1B[0K" : "\x1B[2K";
200
+ return stream.write(code, callback);
201
+ }
202
+ function clearScreenDown(stream, callback) {
203
+ if (!stream || typeof stream.write !== "function") {
204
+ if (callback) callback();
205
+ return true;
206
+ }
207
+ return stream.write("\x1B[0J", callback);
208
+ }
209
+ function cursorTo(stream, x, y, callback) {
210
+ if (!stream || typeof stream.write !== "function") {
211
+ if (typeof y === "function") y();
212
+ else if (callback) callback();
213
+ return true;
214
+ }
215
+ if (typeof y === "function") {
216
+ callback = y;
217
+ y = void 0;
218
+ }
219
+ const code = typeof y === "number" ? `\x1B[${y + 1};${x + 1}H` : `\x1B[${x + 1}G`;
220
+ return stream.write(code, callback);
221
+ }
222
+ function moveCursor(stream, dx, dy, callback) {
223
+ if (!stream || typeof stream.write !== "function") {
224
+ if (callback) callback();
225
+ return true;
226
+ }
227
+ let code = "";
228
+ if (dx > 0) code += `\x1B[${dx}C`;
229
+ else if (dx < 0) code += `\x1B[${-dx}D`;
230
+ if (dy > 0) code += `\x1B[${dy}B`;
231
+ else if (dy < 0) code += `\x1B[${-dy}A`;
232
+ if (code) {
233
+ return stream.write(code, callback);
234
+ }
235
+ if (callback) callback();
236
+ return true;
237
+ }
238
+ function emitKeypressEvents(_stream, _interface) {
239
+ }
240
+ var index_default = {
241
+ Interface,
242
+ createInterface,
243
+ clearLine,
244
+ clearScreenDown,
245
+ cursorTo,
246
+ moveCursor,
247
+ emitKeypressEvents
248
+ };
249
+ export {
250
+ Interface,
251
+ clearLine,
252
+ clearScreenDown,
253
+ createInterface,
254
+ cursorTo,
255
+ index_default as default,
256
+ emitKeypressEvents,
257
+ moveCursor
258
+ };
@@ -0,0 +1,26 @@
1
+ import { Interface as BaseInterface } from "./index.js";
2
+ class Interface extends BaseInterface {
3
+ /** Ask a question and return the answer as a Promise. */
4
+ question(query, options) {
5
+ return new Promise((resolve) => {
6
+ super.question(query, resolve);
7
+ });
8
+ }
9
+ }
10
+ function createInterface(input, output) {
11
+ if (typeof input === "object" && input !== null && !("read" in input && typeof input.read === "function")) {
12
+ const opts = input;
13
+ const rl = new Interface(opts);
14
+ return rl;
15
+ }
16
+ return new Interface({ input, output });
17
+ }
18
+ var promises_default = {
19
+ Interface,
20
+ createInterface
21
+ };
22
+ export {
23
+ Interface,
24
+ createInterface,
25
+ promises_default as default
26
+ };
@@ -0,0 +1,102 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type { Readable, Writable } from 'node:stream';
3
+ export interface InterfaceOptions {
4
+ input?: Readable;
5
+ output?: Writable;
6
+ prompt?: string;
7
+ terminal?: boolean;
8
+ historySize?: number;
9
+ completer?: (line: string, callback: (err: Error | null, result: [string[], string]) => void) => void;
10
+ crlfDelay?: number;
11
+ removeHistoryDuplicates?: boolean;
12
+ escapeCodeTimeout?: number;
13
+ tabSize?: number;
14
+ }
15
+ /**
16
+ * readline.Interface — reads lines from a Readable stream.
17
+ */
18
+ export declare class Interface extends EventEmitter {
19
+ terminal: boolean;
20
+ line: string;
21
+ cursor: number;
22
+ private _input;
23
+ private _output;
24
+ private _prompt;
25
+ private _closed;
26
+ private _paused;
27
+ history: string[];
28
+ private _historySize;
29
+ private _crlfDelay;
30
+ private _lineBuffer;
31
+ private _questionCallback;
32
+ constructor(input?: Readable | InterfaceOptions, output?: Writable);
33
+ private _onData;
34
+ private _onLine;
35
+ private _onEnd;
36
+ /** Set the prompt string. */
37
+ setPrompt(prompt: string): void;
38
+ /** Get the current prompt string. */
39
+ getPrompt(): string;
40
+ /** Write the prompt to the output stream. */
41
+ prompt(preserveCursor?: boolean): void;
42
+ /**
43
+ * Display the query and wait for user input.
44
+ */
45
+ question(query: string, callback: (answer: string) => void): void;
46
+ question(query: string, options: Record<string, unknown>, callback: (answer: string) => void): void;
47
+ /** Write data to the output stream. */
48
+ write(data: string | Buffer | null, key?: {
49
+ ctrl?: boolean;
50
+ meta?: boolean;
51
+ shift?: boolean;
52
+ name?: string;
53
+ }): void;
54
+ /** Close the interface. */
55
+ close(): void;
56
+ /** Pause the input stream. */
57
+ pause(): this;
58
+ /** Resume the input stream. */
59
+ resume(): this;
60
+ /** Get the current line content. */
61
+ getCursorPos(): {
62
+ rows: number;
63
+ cols: number;
64
+ };
65
+ [Symbol.asyncIterator](): AsyncIterableIterator<string>;
66
+ }
67
+ /**
68
+ * Create a readline Interface.
69
+ */
70
+ export declare function createInterface(input?: Readable | InterfaceOptions, output?: Writable, completer?: InterfaceOptions['completer'], terminal?: boolean): Interface;
71
+ /**
72
+ * Clear the current line of a TTY stream.
73
+ * dir: -1 = to the left, 1 = to the right, 0 = entire line
74
+ */
75
+ export declare function clearLine(stream: Writable, dir: number, callback?: () => void): boolean;
76
+ /**
77
+ * Clear from cursor to end of screen.
78
+ */
79
+ export declare function clearScreenDown(stream: Writable, callback?: () => void): boolean;
80
+ /**
81
+ * Move cursor to the specified position.
82
+ */
83
+ export declare function cursorTo(stream: Writable, x: number, y?: number | (() => void), callback?: () => void): boolean;
84
+ /**
85
+ * Move cursor relative to its current position.
86
+ */
87
+ export declare function moveCursor(stream: Writable, dx: number, dy: number, callback?: () => void): boolean;
88
+ /**
89
+ * Enable keypress events on a stream (no-op in GJS — full implementation
90
+ * requires raw terminal mode which depends on the input stream type).
91
+ */
92
+ export declare function emitKeypressEvents(_stream: Readable, _interface?: Interface): void;
93
+ declare const _default: {
94
+ Interface: typeof Interface;
95
+ createInterface: typeof createInterface;
96
+ clearLine: typeof clearLine;
97
+ clearScreenDown: typeof clearScreenDown;
98
+ cursorTo: typeof cursorTo;
99
+ moveCursor: typeof moveCursor;
100
+ emitKeypressEvents: typeof emitKeypressEvents;
101
+ };
102
+ export default _default;
@@ -0,0 +1,19 @@
1
+ import { Interface as BaseInterface } from './index.js';
2
+ import type { InterfaceOptions } from './index.js';
3
+ import type { Readable, Writable } from 'node:stream';
4
+ /**
5
+ * Promise-based readline Interface.
6
+ */
7
+ export declare class Interface extends BaseInterface {
8
+ /** Ask a question and return the answer as a Promise. */
9
+ question(query: string, options?: any): Promise<string>;
10
+ }
11
+ /**
12
+ * Create a promise-based readline Interface.
13
+ */
14
+ export declare function createInterface(input?: Readable | InterfaceOptions, output?: Writable): Interface;
15
+ declare const _default: {
16
+ Interface: typeof Interface;
17
+ createInterface: typeof createInterface;
18
+ };
19
+ export default _default;
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@gjsify/readline",
3
+ "version": "0.1.0",
4
+ "description": "Node.js readline module for Gjs",
5
+ "type": "module",
6
+ "module": "lib/esm/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/esm/index.js"
12
+ },
13
+ "./promises": {
14
+ "types": "./lib/types/promises.d.ts",
15
+ "default": "./lib/esm/promises.js"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "clear": "rm -rf lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs test.node.mjs || exit 0",
20
+ "check": "tsc --noEmit",
21
+ "build": "yarn build:gjsify && yarn build:types",
22
+ "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
23
+ "build:types": "tsc",
24
+ "build:test": "yarn build:test:gjs && yarn build:test:node",
25
+ "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
26
+ "build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
27
+ "test": "yarn build:gjsify && yarn build:test && yarn test:node && yarn test:gjs",
28
+ "test:gjs": "gjs -m test.gjs.mjs",
29
+ "test:node": "node test.node.mjs"
30
+ },
31
+ "keywords": [
32
+ "gjs",
33
+ "node",
34
+ "readline"
35
+ ],
36
+ "dependencies": {
37
+ "@gjsify/events": "^0.1.0"
38
+ },
39
+ "devDependencies": {
40
+ "@gjsify/cli": "^0.1.0",
41
+ "@gjsify/unit": "^0.1.0",
42
+ "@types/node": "^25.5.0",
43
+ "typescript": "^6.0.2"
44
+ }
45
+ }