Skip to content
This repository has been archived by the owner on Jul 16, 2024. It is now read-only.

Add readLine to Terminal and make columns property an Effect #319

Merged
merged 3 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/healthy-games-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@effect/platform-node": patch
"@effect/platform": patch
---

add Terminal.readLine to read input line-by-line from the terminal
6 changes: 6 additions & 0 deletions .changeset/tasty-vans-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@effect/platform-node": patch
"@effect/platform": patch
---

make Terminal.columns an Effect to account for resizing the terminal
1 change: 0 additions & 1 deletion packages/platform-bun/examples/http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,5 @@ Effect.flatMap(
).pipe(
Effect.tap(Effect.log),
Effect.provide(TodoServiceLive),
Effect.tapErrorCause(Effect.logError),
runMain
)
1 change: 0 additions & 1 deletion packages/platform-bun/examples/http-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,5 @@ const EnvLive = Layer.merge(ServerLive, NodeContext.layer)
serve.pipe(
Effect.scoped,
Effect.provide(EnvLive),
Effect.tapErrorCause(Effect.logError),
runMain
)
1 change: 0 additions & 1 deletion packages/platform-bun/examples/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@ const ServerLive = Http.server.layer({ port: 3000 })
Http.server.serve(Effect.succeed(Http.response.text("Hello World"))).pipe(
Effect.scoped,
Effect.provide(ServerLive),
Effect.tapErrorCause(Effect.logError),
runMain
)
1 change: 0 additions & 1 deletion packages/platform-node/examples/http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,5 @@ Effect.flatMap(
).pipe(
Effect.tap(Effect.log),
Effect.provide(TodoServiceLive),
Effect.tapErrorCause(Effect.logError),
runMain
)
1 change: 0 additions & 1 deletion packages/platform-node/examples/http-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,5 @@ const HttpLive = Layer.scopedDiscard(serve).pipe(
)

Layer.launch(HttpLive).pipe(
Effect.tapErrorCause(Effect.logError),
runMain
)
1 change: 0 additions & 1 deletion packages/platform-node/examples/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@ const ServerLive = Http.server.layer(() => createServer(), { port: 3000 })
Http.server.serve(Effect.succeed(Http.response.text("Hello World"))).pipe(
Effect.scoped,
Effect.provide(ServerLive),
Effect.tapErrorCause(Effect.logError),
runMain
)
23 changes: 23 additions & 0 deletions packages/platform-node/examples/terminal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as Runtime from "@effect/platform-node/Runtime"
import * as Terminal from "@effect/platform-node/Terminal"
import { Console, Effect } from "effect"

const program = Effect.gen(function*(_) {
const terminal = yield* _(Terminal.Terminal)

const line1 = yield* _(terminal.readLine)
yield* _(Console.log(`First line: ${line1}`))

const line2 = yield* _(terminal.readLine)
yield* _(Console.log(`Second line: ${line2}`))

const line3 = yield* _(terminal.readLine)
yield* _(Console.log(`Third line: ${line3}`))
})

const MainLive = Terminal.layer

program.pipe(
Effect.provide(MainLive),
Runtime.runMain
)
88 changes: 57 additions & 31 deletions packages/platform-node/src/internal/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,40 +16,22 @@ export const make = (
const input = yield* _(Effect.sync(() => globalThis.process.stdin))
const output = yield* _(Effect.sync(() => globalThis.process.stdout))

// Acquire a `readline` interface and use it to force `stdin` to emit
// keypress events
const acquireReadlineInterface = Effect.sync(() => {
const rl = readline.createInterface({ input, escapeCodeTimeout: 50 })
// Acquire a readline interface
const acquireReadlineInterface = Effect.sync(() =>
readline.createInterface({
input,
escapeCodeTimeout: 50
})
)

// Uses the readline interface to force `stdin` to emit keypress events
const emitKeypressEvents = (rl: readline.Interface): readline.Interface => {
readline.emitKeypressEvents(input, rl)
if (input.isTTY) {
input.setRawMode(true)
}
return rl
})

// Handle the `"keypress"` event emitted by `stdin` forced by `readline`
const handleKeypressEvent = Effect.async<never, Terminal.QuitException, Terminal.UserInput>((resume) => {
const handleKeypress = (input: string | undefined, key: readline.Key) => {
const userInput: Terminal.UserInput = {
input: Option.fromNullable(input),
key: {
name: key.name || "",
ctrl: key.ctrl || false,
meta: key.meta || false,
shift: key.shift || false
}
}
if (shouldQuit(userInput)) {
resume(Effect.fail(new Terminal.QuitException()))
} else {
resume(Effect.succeed(userInput))
}
}
input.once("keypress", handleKeypress)
return Effect.sync(() => {
input.removeListener("keypress", handleKeypress)
})
})
}

// Close the `readline` interface
const releaseReadlineInterface = (rl: readline.Interface) =>
Expand All @@ -60,9 +42,52 @@ export const make = (
rl.close()
})

// Handle the `"keypress"` event emitted by `stdin` (forced by readline)
const handleKeypressEvent = (input: typeof globalThis.process.stdin) =>
Effect.async<never, Terminal.QuitException, Terminal.UserInput>((resume) => {
const handleKeypress = (input: string | undefined, key: readline.Key) => {
const userInput: Terminal.UserInput = {
input: Option.fromNullable(input),
key: {
name: key.name || "",
ctrl: key.ctrl || false,
meta: key.meta || false,
shift: key.shift || false
}
}
if (shouldQuit(userInput)) {
resume(Effect.fail(new Terminal.QuitException()))
} else {
resume(Effect.succeed(userInput))
}
}
input.once("keypress", handleKeypress)
return Effect.sync(() => {
input.removeListener("keypress", handleKeypress)
})
})

// Handle the `"line"` event emitted by the readline interface
const handleLineEvent = (rl: readline.Interface) =>
Effect.async<never, Terminal.QuitException, string>((resume) => {
const handleLine = (line: string) => {
resume(Effect.succeed(line))
}
rl.on("line", handleLine)
return Effect.sync(() => {
rl.removeListener("line", handleLine)
})
})

const readInput = Effect.acquireUseRelease(
acquireReadlineInterface.pipe(Effect.map(emitKeypressEvents)),
() => handleKeypressEvent(input),
releaseReadlineInterface
)

const readLine = Effect.acquireUseRelease(
acquireReadlineInterface,
() => handleKeypressEvent,
(rl) => handleLineEvent(rl),
releaseReadlineInterface
)

Expand All @@ -84,8 +109,9 @@ export const make = (

return Terminal.Terminal.of({
// The columns property can be undefined if stdout was redirected
columns: output.columns || 0,
columns: Effect.sync(() => output.columns || 0),
readInput,
readLine,
display
})
})
Expand Down
6 changes: 5 additions & 1 deletion packages/platform/src/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ export interface Terminal {
/**
* The number of columns available on the platform's terminal interface.
*/
readonly columns: number
readonly columns: Effect<never, never, number>
/**
* Reads a single input event from the default standard input.
*/
readonly readInput: Effect<never, QuitException, UserInput>
/**
* Reads a single line from the default standard input.
*/
readonly readLine: Effect<never, QuitException, string>
/**
* Displays text to the the default standard output.
*/
Expand Down