| 1 | import * as Array from "effect/Array" |
| 2 | import type * as Cause from "effect/Cause" |
| 3 | import * as Context from "effect/Context" |
| 4 | import * as Effect from "effect/Effect" |
| 5 | import * as Layer from "effect/Layer" |
| 6 | import * as Option from "effect/Option" |
| 7 | import * as Queue from "effect/Queue" |
| 8 | import type * as Scope from "effect/Scope" |
| 9 | import * as Terminal from "effect/Terminal" |
| 10 | |
| 11 | // ============================================================================= |
| 12 | // Models |
| 13 | // ============================================================================= |
| 14 | |
| 15 | export interface MockTerminal extends Terminal.Terminal { |
| 16 | readonly displayLines: Effect.Effect<ReadonlyArray<string>> |
| 17 | readonly inputText: (text: string) => Effect.Effect<void> |
| 18 | readonly inputKey: (key: string, modifiers?: Partial<MockTerminal.Modifiers>) => Effect.Effect<void> |
| 19 | } |
| 20 | |
| 21 | export declare namespace MockTerminal { |
| 22 | export interface Modifiers { |
| 23 | readonly ctrl: boolean |
| 24 | readonly meta: boolean |
| 25 | readonly shift: boolean |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | // ============================================================================= |
| 30 | // Service |
| 31 | // ============================================================================= |
| 32 | |
| 33 | export const MockTerminal = Context.Service<Terminal.Terminal, MockTerminal>()( |
| 34 | Terminal.Terminal.key |
| 35 | ) |
| 36 | |
| 37 | // ============================================================================= |
| 38 | // Constructors |
| 39 | // ============================================================================= |
| 40 | |
| 41 | export const make = Effect.gen(function*() { |
| 42 | const output: Array<string> = [] |
| 43 | const queue = yield* Effect.acquireRelease( |
| 44 | Queue.make<Terminal.UserInput, Cause.Done>(), |
| 45 | (queue) => Queue.shutdown(queue) |
| 46 | ) |
| 47 | |
| 48 | const inputText: MockTerminal["inputText"] = (text: string) => { |
| 49 | const inputs = Array.map(text.split(""), (key) => toUserInput(key)) |
| 50 | return Queue.offerAll(queue, inputs).pipe(Effect.asVoid) |
| 51 | } |
| 52 | |
| 53 | const inputKey: MockTerminal["inputKey"] = ( |
| 54 | key: string, |
| 55 | modifiers?: Partial<MockTerminal.Modifiers> |
| 56 | ) => { |
| 57 | const input = toUserInput(key, modifiers) |
| 58 | return shouldQuit(input) ? Queue.end(queue) : Queue.offer(queue, input).pipe(Effect.asVoid) |
| 59 | } |
| 60 | |
| 61 | const display: MockTerminal["display"] = (input) => |
| 62 | Effect.sync(() => { |
| 63 | output.push(input) |
| 64 | }) |
| 65 | |
| 66 | const readInput: MockTerminal["readInput"] = Effect.succeed(Queue.asDequeue(queue)) |
| 67 | |
| 68 | const terminal = Terminal.make({ |
| 69 | columns: Effect.succeed(80), |
| 70 | rows: Effect.succeed(24), |
| 71 | display, |
| 72 | readInput, |
| 73 | readLine: Effect.succeed("") |
| 74 | }) |
| 75 | |
| 76 | return Object.assign(terminal, { |
| 77 | displayLines: Effect.sync(() => output.slice()), |
| 78 | inputKey, |
| 79 | inputText |
| 80 | }) |
| 81 | }) |
| 82 | |
| 83 | // ============================================================================= |
| 84 | // Layer |
| 85 | // ============================================================================= |
| 86 | |
| 87 | export const layer: Layer.Layer<Terminal.Terminal> = Layer.effect(MockTerminal, make) |
| 88 | |
| 89 | // ============================================================================= |
| 90 | // Accessors |
| 91 | // ============================================================================= |
| 92 | |
| 93 | export const columns: Effect.Effect<number, never, Terminal.Terminal> = Effect.flatMap( |
| 94 | MockTerminal, |
| 95 | (terminal) => terminal.columns |
| 96 | ) |
| 97 | |
| 98 | export const displayLines: Effect.Effect<ReadonlyArray<string>, never, Terminal.Terminal> = Effect.flatMap( |
| 99 | MockTerminal, |
| 100 | (terminal) => terminal.displayLines |
| 101 | ) |
| 102 | |
| 103 | export const readInput: Effect.Effect< |
| 104 | Queue.Dequeue<Terminal.UserInput, Cause.Done>, |
| 105 | never, |
| 106 | Terminal.Terminal | Scope.Scope |
| 107 | > = Effect.flatMap(MockTerminal, (terminal) => terminal.readInput) |
| 108 | |
| 109 | export const readLine: Effect.Effect<string, Terminal.QuitError, Terminal.Terminal> = Effect.flatMap( |
| 110 | MockTerminal, |
| 111 | (terminal) => terminal.readLine |
| 112 | ) |
| 113 | |
| 114 | export const inputKey = ( |
| 115 | key: string, |
| 116 | modifiers?: Partial<MockTerminal.Modifiers> |
| 117 | ): Effect.Effect<void, never, Terminal.Terminal> => |
| 118 | Effect.flatMap( |
| 119 | MockTerminal, |
| 120 | (terminal) => terminal.inputKey(key, modifiers) |
| 121 | ) |
| 122 | |
| 123 | export const inputText = (text: string): Effect.Effect<void, never, Terminal.Terminal> => |
| 124 | Effect.flatMap( |
| 125 | MockTerminal, |
| 126 | (terminal) => terminal.inputText(text) |
| 127 | ) |
| 128 | |
| 129 | // ============================================================================= |
| 130 | // Utilities |
| 131 | // ============================================================================= |
| 132 | |
| 133 | const shouldQuit = (input: Terminal.UserInput): boolean => |
| 134 | input.key.ctrl && (input.key.name === "c" || input.key.name === "d") |
| 135 | |
| 136 | const toUserInput = ( |
| 137 | key: string, |
| 138 | modifiers: Partial<MockTerminal.Modifiers> = {} |
| 139 | ): Terminal.UserInput => { |
| 140 | const { ctrl = false, meta = false, shift = false } = modifiers |
| 141 | return { |
| 142 | input: Option.some(key), |
| 143 | key: { name: key, ctrl, meta, shift } |
| 144 | } |
| 145 | } |