1import * as Array from "effect/Array"
2import type * as Cause from "effect/Cause"
3import * as Context from "effect/Context"
4import * as Effect from "effect/Effect"
5import * as Layer from "effect/Layer"
6import * as Option from "effect/Option"
7import * as Queue from "effect/Queue"
8import type * as Scope from "effect/Scope"
9import * as Terminal from "effect/Terminal"
10
11// =============================================================================
12// Models
13// =============================================================================
14
15export 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
21export 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
33export const MockTerminal = Context.Service<Terminal.Terminal, MockTerminal>()(
34 Terminal.Terminal.key
35)
36
37// =============================================================================
38// Constructors
39// =============================================================================
40
41export 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
87export const layer: Layer.Layer<Terminal.Terminal> = Layer.effect(MockTerminal, make)
88
89// =============================================================================
90// Accessors
91// =============================================================================
92
93export const columns: Effect.Effect<number, never, Terminal.Terminal> = Effect.flatMap(
94 MockTerminal,
95 (terminal) => terminal.columns
96)
97
98export const displayLines: Effect.Effect<ReadonlyArray<string>, never, Terminal.Terminal> = Effect.flatMap(
99 MockTerminal,
100 (terminal) => terminal.displayLines
101)
102
103export 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
109export const readLine: Effect.Effect<string, Terminal.QuitError, Terminal.Terminal> = Effect.flatMap(
110 MockTerminal,
111 (terminal) => terminal.readLine
112)
113
114export 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
123export 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
133const shouldQuit = (input: Terminal.UserInput): boolean =>
134 input.key.ctrl && (input.key.name === "c" || input.key.name === "d")
135
136const 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}