diff --git a/.changeset/terminal-display-error.md b/.changeset/terminal-display-error.md new file mode 100644 index 00000000000..b28877d6da0 --- /dev/null +++ b/.changeset/terminal-display-error.md @@ -0,0 +1,6 @@ +--- +"@effect/platform": patch +"@effect/platform-node-shared": patch +--- + +Add `displayError` method to `Terminal` for writing to stderr. diff --git a/packages/cli/test/services/MockTerminal.ts b/packages/cli/test/services/MockTerminal.ts index d34e47f692b..7d1c10052a6 100644 --- a/packages/cli/test/services/MockTerminal.ts +++ b/packages/cli/test/services/MockTerminal.ts @@ -60,6 +60,8 @@ export const make = Effect.gen(function*() { const display: MockTerminal["display"] = (input) => Console.log(input) + const displayError: MockTerminal["displayError"] = (input) => Console.error(input) + const readInput: MockTerminal["readInput"] = Effect.succeed(queue) return MockTerminal.of({ @@ -67,6 +69,7 @@ export const make = Effect.gen(function*() { rows: Effect.succeed(24), isTTY: Effect.succeed(true), display, + displayError, readInput, readLine: Effect.succeed(""), inputKey, diff --git a/packages/platform-node-shared/src/internal/terminal.ts b/packages/platform-node-shared/src/internal/terminal.ts index e1db804c1e4..7465f86bb15 100644 --- a/packages/platform-node-shared/src/internal/terminal.ts +++ b/packages/platform-node-shared/src/internal/terminal.ts @@ -17,6 +17,7 @@ export const make = Effect.fnUntraced(function*( ) { const stdin = process.stdin const stdout = process.stdout + const stderr = process.stderr // Acquire readline interface with TTY setup/cleanup inside the scope const rlRef = yield* RcRef.make({ @@ -90,13 +91,31 @@ export const make = Effect.fnUntraced(function*( }) ) + const displayError = (prompt: string) => + Effect.uninterruptible( + Effect.async((resume) => { + stderr.write(prompt, (err) => + err + ? resume(Effect.fail( + new Error.BadArgument({ + module: "Terminal", + method: "displayError", + description: "Failed to write prompt to stderr", + cause: err + }) + )) + : resume(Effect.void)) + }) + ) + return Terminal.Terminal.of({ columns, rows, isTTY, readInput, readLine, - display + display, + displayError }) }) diff --git a/packages/platform-node-shared/test/Terminal.test.ts b/packages/platform-node-shared/test/Terminal.test.ts index 79bb7768ef8..d0f4982afcf 100644 --- a/packages/platform-node-shared/test/Terminal.test.ts +++ b/packages/platform-node-shared/test/Terminal.test.ts @@ -1,13 +1,36 @@ import * as NodeTerminal from "@effect/platform-node-shared/NodeTerminal" import * as Terminal from "@effect/platform/Terminal" import { describe, expect, it } from "@effect/vitest" +import * as Chunk from "effect/Chunk" import * as Effect from "effect/Effect" +import * as Ref from "effect/Ref" const runPromise = (self: Effect.Effect) => Effect.runPromise( Effect.provide(self, NodeTerminal.layer) ) +const makeTestTerminal = Effect.gen(function*() { + const displayOutput = yield* Ref.make(Chunk.empty()) + const displayErrorOutput = yield* Ref.make(Chunk.empty()) + + const terminal = Terminal.Terminal.of({ + columns: Effect.succeed(80), + rows: Effect.succeed(24), + isTTY: Effect.succeed(true), + readInput: Effect.die("not implemented"), + readLine: Effect.die("not implemented"), + display: (text) => Ref.update(displayOutput, Chunk.append(text)), + displayError: (text) => Ref.update(displayErrorOutput, Chunk.append(text)) + }) + + return { + terminal, + displayOutput, + displayErrorOutput + } +}) + describe("Terminal", () => { it("columns", () => runPromise(Effect.gen(function*() { @@ -29,4 +52,26 @@ describe("Terminal", () => { const isTTY = yield* terminal.isTTY expect(typeof isTTY).toEqual("boolean") }))) + + it("display", () => + Effect.gen(function*() { + const { displayOutput, terminal } = yield* makeTestTerminal + + yield* terminal.display("hello") + yield* terminal.display("world") + + const output = yield* Ref.get(displayOutput) + expect(Chunk.toArray(output)).toEqual(["hello", "world"]) + }).pipe(Effect.runPromise)) + + it("displayError", () => + Effect.gen(function*() { + const { displayErrorOutput, terminal } = yield* makeTestTerminal + + yield* terminal.displayError("error1") + yield* terminal.displayError("error2") + + const errors = yield* Ref.get(displayErrorOutput) + expect(Chunk.toArray(errors)).toEqual(["error1", "error2"]) + }).pipe(Effect.runPromise)) }) diff --git a/packages/platform/src/Terminal.ts b/packages/platform/src/Terminal.ts index 88977209e01..2f89e035fb8 100644 --- a/packages/platform/src/Terminal.ts +++ b/packages/platform/src/Terminal.ts @@ -42,6 +42,10 @@ export interface Terminal { * Displays text to the the default standard output. */ readonly display: (text: string) => Effect + /** + * Displays text to the the default standard error. + */ + readonly displayError: (text: string) => Effect } /**