@effect/platform-node-shared 0.44.0 → 0.46.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.
@@ -1,126 +1,101 @@
1
1
  import * as Error from "@effect/platform/Error"
2
2
  import * as Terminal from "@effect/platform/Terminal"
3
3
  import * as Effect from "effect/Effect"
4
+ import * as Exit from "effect/Exit"
4
5
  import * as Layer from "effect/Layer"
6
+ import * as Mailbox from "effect/Mailbox"
5
7
  import * as Option from "effect/Option"
8
+ import * as RcRef from "effect/RcRef"
6
9
  import * as readline from "node:readline"
7
10
 
8
- const defaultShouldQuit = (input: Terminal.UserInput): boolean =>
11
+ const defaultShouldQuit = (input: Terminal.UserInput) =>
9
12
  input.key.ctrl && (input.key.name === "c" || input.key.name === "d")
10
13
 
11
14
  /** @internal */
12
- export const make = (
15
+ export const make = Effect.fnUntraced(function*(
13
16
  shouldQuit: (input: Terminal.UserInput) => boolean = defaultShouldQuit
14
- ) =>
15
- Effect.gen(function*() {
16
- const input = yield* Effect.sync(() => globalThis.process.stdin)
17
- const output = yield* Effect.sync(() => globalThis.process.stdout)
17
+ ) {
18
+ const stdin = process.stdin
19
+ const stdout = process.stdout
18
20
 
19
- // Acquire a readline interface
20
- const acquireReadlineInterface = Effect.sync(() =>
21
- readline.createInterface({
22
- input,
23
- escapeCodeTimeout: 50
24
- })
21
+ // Acquire readline interface with TTY setup/cleanup inside the scope
22
+ const rlRef = yield* RcRef.make({
23
+ acquire: Effect.acquireRelease(
24
+ Effect.sync(() => {
25
+ const rl = readline.createInterface({ input: stdin, escapeCodeTimeout: 50 })
26
+ readline.emitKeypressEvents(stdin, rl)
27
+
28
+ if (stdin.isTTY) {
29
+ stdin.setRawMode(true)
30
+ }
31
+ return rl
32
+ }),
33
+ (rl) =>
34
+ Effect.sync(() => {
35
+ if (stdin.isTTY) {
36
+ stdin.setRawMode(false)
37
+ }
38
+ rl.close()
39
+ })
25
40
  )
41
+ })
26
42
 
27
- // Uses the readline interface to force `stdin` to emit keypress events
28
- const emitKeypressEvents = (rl: readline.Interface): readline.Interface => {
29
- readline.emitKeypressEvents(input, rl)
30
- if (input.isTTY) {
31
- input.setRawMode(true)
43
+ const columns = Effect.sync(() => stdout.columns ?? 0)
44
+
45
+ const readInput = Effect.gen(function*() {
46
+ yield* RcRef.get(rlRef)
47
+ const mailbox = yield* Mailbox.make<Terminal.UserInput>()
48
+ const handleKeypress = (s: string | undefined, k: readline.Key) => {
49
+ const userInput = {
50
+ input: Option.fromNullable(s),
51
+ key: { name: k.name ?? "", ctrl: !!k.ctrl, meta: !!k.meta, shift: !!k.shift }
52
+ }
53
+ if (shouldQuit(userInput)) {
54
+ mailbox.unsafeDone(Exit.void)
55
+ } else {
56
+ mailbox.unsafeOffer(userInput)
32
57
  }
33
- return rl
34
58
  }
59
+ yield* Effect.addFinalizer(() => Effect.sync(() => stdin.off("keypress", handleKeypress)))
60
+ stdin.on("keypress", handleKeypress)
61
+ return mailbox as Mailbox.ReadonlyMailbox<Terminal.UserInput>
62
+ })
35
63
 
36
- // Close the `readline` interface
37
- const releaseReadlineInterface = (rl: readline.Interface) =>
38
- Effect.sync(() => {
39
- if (input.isTTY) {
40
- input.setRawMode(false)
41
- }
42
- rl.close()
43
- })
44
-
45
- // Handle the `"keypress"` event emitted by `stdin` (forced by readline)
46
- const handleKeypressEvent = (input: typeof globalThis.process.stdin) =>
47
- Effect.async<Terminal.UserInput, Terminal.QuitException>((resume) => {
48
- const handleKeypress = (input: string | undefined, key: readline.Key) => {
49
- const userInput: Terminal.UserInput = {
50
- input: Option.fromNullable(input),
51
- key: {
52
- name: key.name || "",
53
- ctrl: key.ctrl || false,
54
- meta: key.meta || false,
55
- shift: key.shift || false
56
- }
57
- }
58
- if (shouldQuit(userInput)) {
59
- resume(Effect.fail(new Terminal.QuitException()))
60
- } else {
61
- resume(Effect.succeed(userInput))
62
- }
63
- }
64
- input.once("keypress", handleKeypress)
65
- return Effect.sync(() => {
66
- input.removeListener("keypress", handleKeypress)
67
- })
64
+ const readLine = RcRef.get(rlRef).pipe(
65
+ Effect.flatMap((readlineInterface) =>
66
+ Effect.async<string, Terminal.QuitException>((resume) => {
67
+ const onLine = (line: string) => resume(Effect.succeed(line))
68
+ readlineInterface.once("line", onLine)
69
+ return Effect.sync(() => readlineInterface.off("line", onLine))
68
70
  })
71
+ ),
72
+ Effect.scoped
73
+ )
69
74
 
70
- // Handle the `"line"` event emitted by the readline interface
71
- const handleLineEvent = (rl: readline.Interface) =>
72
- Effect.async<string, Terminal.QuitException, never>((resume) => {
73
- const handleLine = (line: string) => {
74
- resume(Effect.succeed(line))
75
- }
76
- rl.on("line", handleLine)
77
- return Effect.sync(() => {
78
- rl.removeListener("line", handleLine)
79
- })
75
+ const display = (prompt: string) =>
76
+ Effect.uninterruptible(
77
+ Effect.async<void, Error.PlatformError>((resume) => {
78
+ stdout.write(prompt, (err) =>
79
+ err
80
+ ? resume(Effect.fail(
81
+ new Error.BadArgument({
82
+ module: "Terminal",
83
+ method: "display",
84
+ description: "Failed to write prompt to stdout",
85
+ cause: err
86
+ })
87
+ ))
88
+ : resume(Effect.void))
80
89
  })
81
-
82
- const readInput = Effect.acquireUseRelease(
83
- acquireReadlineInterface.pipe(Effect.map(emitKeypressEvents)),
84
- () => handleKeypressEvent(input),
85
- releaseReadlineInterface
86
- )
87
-
88
- const readLine = Effect.acquireUseRelease(
89
- acquireReadlineInterface,
90
- (rl) => handleLineEvent(rl),
91
- releaseReadlineInterface
92
90
  )
93
91
 
94
- const display = (prompt: string): Effect.Effect<void, Error.PlatformError> =>
95
- Effect.uninterruptible(
96
- Effect.async((resume) => {
97
- output.write(prompt, (err) => {
98
- if (err) {
99
- resume(Effect.fail(
100
- new Error.BadArgument({
101
- module: "Terminal",
102
- method: "display",
103
- description: "Failed to write prompt to output",
104
- cause: err
105
- })
106
- ))
107
- }
108
- resume(Effect.void)
109
- })
110
- })
111
- )
112
-
113
- return Terminal.Terminal.of({
114
- // The columns property can be undefined if stdout was redirected
115
- columns: Effect.sync(() => output.columns || 0),
116
- readInput,
117
- readLine,
118
- display
119
- })
92
+ return Terminal.Terminal.of({
93
+ columns,
94
+ readInput,
95
+ readLine,
96
+ display
120
97
  })
98
+ })
121
99
 
122
100
  /** @internal */
123
- export const layer: Layer.Layer<Terminal.Terminal> = Layer.scoped(
124
- Terminal.Terminal,
125
- make(defaultShouldQuit)
126
- )
101
+ export const layer: Layer.Layer<Terminal.Terminal> = Layer.scoped(Terminal.Terminal, make(defaultShouldQuit))