@tui-sandbox/library 9.0.0 → 9.1.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/browser/assets/{index-_30KjjEK.js → index-pw1tOGNt.js} +7 -7
  3. package/dist/browser/index.html +1 -1
  4. package/dist/src/browser/neovim-client.d.ts +5 -0
  5. package/dist/src/browser/neovim-client.js +17 -6
  6. package/dist/src/client/index.d.ts +2 -1
  7. package/dist/src/client/index.js +2 -1
  8. package/dist/src/client/{terminal-client.d.ts → neovim-terminal-client.d.ts} +1 -1
  9. package/dist/src/client/{terminal-client.js → neovim-terminal-client.js} +3 -3
  10. package/dist/src/client/terminal-terminal-client.d.ts +14 -0
  11. package/dist/src/client/terminal-terminal-client.js +72 -0
  12. package/dist/src/server/connection/trpc.d.ts +6 -14
  13. package/dist/src/server/cypress-support/contents.js +23 -1
  14. package/dist/src/server/dirtree/index.test.js +2 -0
  15. package/dist/src/server/neovim/NeovimApplication.d.ts +1 -1
  16. package/dist/src/server/neovim/environment/createTempDir.test.js +6 -5
  17. package/dist/src/server/neovim/index.js +6 -1
  18. package/dist/src/server/server.d.ts +46 -6
  19. package/dist/src/server/server.js +24 -0
  20. package/dist/src/server/terminal/TerminalTestApplication.d.ts +22 -0
  21. package/dist/src/server/terminal/TerminalTestApplication.js +60 -0
  22. package/dist/src/server/terminal/index.d.ts +12 -0
  23. package/dist/src/server/terminal/index.js +40 -0
  24. package/dist/src/server/types.d.ts +5 -0
  25. package/dist/src/server/utilities/TerminalApplication.d.ts +1 -0
  26. package/dist/src/server/utilities/TerminalApplication.js +10 -5
  27. package/dist/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +9 -9
  29. package/src/browser/neovim-client.ts +26 -6
  30. package/src/client/index.ts +2 -1
  31. package/src/client/{terminal-client.ts → neovim-terminal-client.ts} +3 -3
  32. package/src/client/terminal-terminal-client.ts +86 -0
  33. package/src/server/cypress-support/contents.ts +23 -1
  34. package/src/server/dirtree/index.test.ts +2 -0
  35. package/src/server/neovim/NeovimApplication.ts +3 -3
  36. package/src/server/neovim/environment/createTempDir.test.ts +6 -5
  37. package/src/server/neovim/index.ts +6 -1
  38. package/src/server/server.ts +34 -0
  39. package/src/server/terminal/TerminalTestApplication.ts +98 -0
  40. package/src/server/terminal/index.ts +62 -0
  41. package/src/server/types.ts +13 -0
  42. package/src/server/utilities/TerminalApplication.ts +10 -8
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <title>tui-sandbox integration tests</title>
6
- <script type="module" crossorigin src="/assets/index-_30KjjEK.js"></script>
6
+ <script type="module" crossorigin src="/assets/index-pw1tOGNt.js"></script>
7
7
  <link rel="stylesheet" crossorigin href="/assets/index-D6fBrqAi.css">
8
8
  </head>
9
9
  <body>
@@ -1,4 +1,5 @@
1
1
  import type { BlockingCommandClientInput, ExCommandClientInput, LuaCodeClientInput } from "../server/server.js";
2
+ import type { StartTerminalGenericArguments } from "../server/terminal/TerminalTestApplication.js";
2
3
  import type { BlockingShellCommandOutput, RunExCommandOutput, RunLuaCodeOutput, StartNeovimGenericArguments, TestDirectory } from "../server/types.js";
3
4
  export type GenericNeovimBrowserApi = {
4
5
  runBlockingShellCommand(input: BlockingCommandClientInput): Promise<BlockingShellCommandOutput>;
@@ -9,5 +10,9 @@ export type GenericNeovimBrowserApi = {
9
10
  declare global {
10
11
  interface Window {
11
12
  startNeovim(startArguments?: StartNeovimGenericArguments): Promise<GenericNeovimBrowserApi>;
13
+ startTerminalApplication(args: StartTerminalGenericArguments): Promise<GenericTerminalBrowserApi>;
12
14
  }
13
15
  }
16
+ export type GenericTerminalBrowserApi = {
17
+ dir: TestDirectory;
18
+ };
@@ -1,27 +1,38 @@
1
- import { TerminalClient } from "../client/index.js";
1
+ import { TerminalClient as NeovimTerminalClient } from "../client/index.js";
2
+ import { TerminalTerminalClient } from "../client/terminal-terminal-client.js";
3
+ import { Lazy } from "../server/utilities/Lazy.js";
2
4
  const app = document.querySelector("#app");
3
5
  if (!app) {
4
6
  throw new Error("No app element found");
5
7
  }
6
- const client = new TerminalClient(app);
8
+ // limitation: right now only one client can be used in the same test
9
+ const neovimClient = new Lazy(() => new NeovimTerminalClient(app));
10
+ const terminalClient = new Lazy(() => new TerminalTerminalClient(app));
7
11
  /** Entrypoint for the test runner (cypress) */
8
12
  window.startNeovim = async function (startArgs) {
9
- const testDirectory = await client.startNeovim({
13
+ const neovim = neovimClient.get();
14
+ const testDirectory = await neovim.startNeovim({
10
15
  additionalEnvironmentVariables: startArgs?.additionalEnvironmentVariables,
11
16
  filename: startArgs?.filename ?? "initial-file.txt",
12
17
  startupScriptModifications: startArgs?.startupScriptModifications ?? [],
13
18
  });
14
19
  const neovimBrowserApi = {
15
20
  runBlockingShellCommand(input) {
16
- return client.runBlockingShellCommand(input);
21
+ return neovim.runBlockingShellCommand(input);
17
22
  },
18
23
  runLuaCode(input) {
19
- return client.runLuaCode(input);
24
+ return neovim.runLuaCode(input);
20
25
  },
21
26
  runExCommand(input) {
22
- return client.runExCommand(input);
27
+ return neovim.runExCommand(input);
23
28
  },
24
29
  dir: testDirectory,
25
30
  };
26
31
  return neovimBrowserApi;
27
32
  };
33
+ /** Entrypoint for the test runner (cypress) */
34
+ window.startTerminalApplication = async function (args) {
35
+ const terminal = terminalClient.get();
36
+ const testDirectory = await terminal.startTerminalApplication(args);
37
+ return { dir: testDirectory };
38
+ };
@@ -1,2 +1,3 @@
1
1
  export { rgbify } from "./color-utilities.js";
2
- export { TerminalClient } from "./terminal-client.js";
2
+ export { NeovimTerminalClient as TerminalClient } from "./neovim-terminal-client.js";
3
+ export { TerminalTerminalClient } from "./terminal-terminal-client.js";
@@ -1,3 +1,4 @@
1
1
  // This is the public client api. Semantic versioning will be applied to this.
2
2
  export { rgbify } from "./color-utilities.js";
3
- export { TerminalClient } from "./terminal-client.js";
3
+ export { NeovimTerminalClient as TerminalClient } from "./neovim-terminal-client.js";
4
+ export { TerminalTerminalClient } from "./terminal-terminal-client.js";
@@ -4,7 +4,7 @@ import type { BlockingShellCommandOutput, RunExCommandOutput, RunLuaCodeOutput,
4
4
  import "./style.css";
5
5
  /** Manages the terminal state in the browser as well as the (browser's)
6
6
  * connection to the server side terminal application api. */
7
- export declare class TerminalClient {
7
+ export declare class NeovimTerminalClient {
8
8
  private readonly ready;
9
9
  private readonly tabId;
10
10
  private readonly terminal;
@@ -4,7 +4,7 @@ import "./style.css";
4
4
  import { getTabId, startTerminal } from "./websocket-client.js";
5
5
  /** Manages the terminal state in the browser as well as the (browser's)
6
6
  * connection to the server side terminal application api. */
7
- export class TerminalClient {
7
+ export class NeovimTerminalClient {
8
8
  ready;
9
9
  tabId;
10
10
  terminal;
@@ -40,7 +40,7 @@ export class TerminalClient {
40
40
  // start listening to Neovim stdout - this will take some (short) amount of
41
41
  // time to complete
42
42
  this.ready = new Promise(resolve => {
43
- console.log("Subscribing to Neovim stdout");
43
+ console.log("Subscribing to stdout");
44
44
  trpc.neovim.onStdout.subscribe({ client: tabId }, {
45
45
  onStarted() {
46
46
  resolve();
@@ -49,7 +49,7 @@ export class TerminalClient {
49
49
  terminal.write(data);
50
50
  },
51
51
  onError(err) {
52
- console.error(`Error from Neovim`, err);
52
+ console.error(`Error from the application`, err);
53
53
  },
54
54
  });
55
55
  });
@@ -0,0 +1,14 @@
1
+ import "@xterm/xterm/css/xterm.css";
2
+ import type { StartTerminalGenericArguments } from "../server/terminal/TerminalTestApplication.js";
3
+ import type { TestDirectory } from "../server/types.js";
4
+ import "./style.css";
5
+ /** Manages the terminal state in the browser as well as the (browser's)
6
+ * connection to the server side terminal application api. */
7
+ export declare class TerminalTerminalClient {
8
+ private readonly ready;
9
+ private readonly tabId;
10
+ private readonly terminal;
11
+ private readonly trpc;
12
+ constructor(app: HTMLElement);
13
+ startTerminalApplication(args: StartTerminalGenericArguments): Promise<TestDirectory>;
14
+ }
@@ -0,0 +1,72 @@
1
+ import { createTRPCClient, httpBatchLink, splitLink, unstable_httpSubscriptionLink } from "@trpc/client";
2
+ import "@xterm/xterm/css/xterm.css";
3
+ import "./style.css";
4
+ import { getTabId, startTerminal } from "./websocket-client.js";
5
+ /** Manages the terminal state in the browser as well as the (browser's)
6
+ * connection to the server side terminal application api. */
7
+ export class TerminalTerminalClient {
8
+ ready;
9
+ tabId;
10
+ terminal;
11
+ trpc;
12
+ constructor(app) {
13
+ const trpc = createTRPCClient({
14
+ links: [
15
+ splitLink({
16
+ condition: operation => operation.type === "subscription",
17
+ true: unstable_httpSubscriptionLink({
18
+ url: "/trpc",
19
+ }),
20
+ false: httpBatchLink({
21
+ url: "/trpc",
22
+ }),
23
+ }),
24
+ ],
25
+ });
26
+ this.trpc = trpc;
27
+ this.tabId = getTabId();
28
+ const tabId = this.tabId;
29
+ const terminal = startTerminal(app, {
30
+ onMouseEvent(data) {
31
+ void trpc.terminal.sendStdin.mutate({ tabId, data }).catch((error) => {
32
+ console.error(`Error sending mouse event`, error);
33
+ });
34
+ },
35
+ onKeyPress(event) {
36
+ void trpc.terminal.sendStdin.mutate({ tabId, data: event.key });
37
+ },
38
+ });
39
+ this.terminal = terminal;
40
+ // start listening to Neovim stdout - this will take some (short) amount of
41
+ // time to complete
42
+ this.ready = new Promise(resolve => {
43
+ console.log("Subscribing to stdout");
44
+ trpc.terminal.onStdout.subscribe({ client: tabId }, {
45
+ onStarted() {
46
+ resolve();
47
+ },
48
+ onData(data) {
49
+ terminal.write(data);
50
+ },
51
+ onError(err) {
52
+ console.error(`Error from the application`, err);
53
+ },
54
+ });
55
+ });
56
+ }
57
+ async startTerminalApplication(args) {
58
+ await this.ready;
59
+ const testDirectory = await this.trpc.terminal.start.mutate({
60
+ tabId: this.tabId,
61
+ startTerminalArguments: {
62
+ additionalEnvironmentVariables: args.additionalEnvironmentVariables,
63
+ commandToRun: args.commandToRun,
64
+ terminalDimensions: {
65
+ cols: this.terminal.cols,
66
+ rows: this.terminal.rows,
67
+ },
68
+ },
69
+ });
70
+ return testDirectory;
71
+ }
72
+ }
@@ -7,20 +7,12 @@ export declare const trpc: {
7
7
  }>;
8
8
  procedure: import("@trpc/server/unstable-core-do-not-import").ProcedureBuilder<object, object, object, typeof import("@trpc/server/unstable-core-do-not-import").unsetMarker, typeof import("@trpc/server/unstable-core-do-not-import").unsetMarker, typeof import("@trpc/server/unstable-core-do-not-import").unsetMarker, typeof import("@trpc/server/unstable-core-do-not-import").unsetMarker, false>;
9
9
  middleware: <$ContextOverrides>(fn: import("@trpc/server/unstable-core-do-not-import").MiddlewareFunction<object, object, object, $ContextOverrides, unknown>) => import("@trpc/server/unstable-core-do-not-import").MiddlewareBuilder<object, object, $ContextOverrides, unknown>;
10
- router: {
11
- <TInput extends import("@trpc/server").RouterRecord>(input: TInput): import("@trpc/server/unstable-core-do-not-import").BuiltRouter<{
12
- ctx: object;
13
- meta: object;
14
- errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
15
- transformer: false;
16
- }, TInput>;
17
- <TInput extends import("@trpc/server/unstable-core-do-not-import").CreateRouterOptions>(input: TInput): import("@trpc/server/unstable-core-do-not-import").BuiltRouter<{
18
- ctx: object;
19
- meta: object;
20
- errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
21
- transformer: false;
22
- }, import("@trpc/server/unstable-core-do-not-import").DecorateCreateRouterOptions<TInput>>;
23
- };
10
+ router: <TInput extends import("@trpc/server/unstable-core-do-not-import").CreateRouterOptions>(input: TInput) => import("@trpc/server/unstable-core-do-not-import").BuiltRouter<{
11
+ ctx: object;
12
+ meta: object;
13
+ errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
14
+ transformer: false;
15
+ }, import("@trpc/server/unstable-core-do-not-import").DecorateCreateRouterOptions<TInput>>;
24
16
  mergeRouters: typeof import("@trpc/server/unstable-core-do-not-import").mergeRouters;
25
17
  createCallerFactory: <TRecord extends import("@trpc/server").RouterRecord>(router: Pick<import("@trpc/server/unstable-core-do-not-import").Router<{
26
18
  ctx: object;
@@ -8,7 +8,10 @@ export async function createCypressSupportFileContents() {
8
8
  //
9
9
  // This file is autogenerated by tui-sandbox. Do not edit it directly.
10
10
  //
11
- import type { GenericNeovimBrowserApi } from "@tui-sandbox/library/dist/src/browser/neovim-client"
11
+ import type {
12
+ GenericNeovimBrowserApi,
13
+ GenericTerminalBrowserApi,
14
+ } from "@tui-sandbox/library/dist/src/browser/neovim-client"
12
15
  import type {
13
16
  BlockingCommandClientInput,
14
17
  ExCommandClientInput,
@@ -21,9 +24,19 @@ import type {
21
24
  StartNeovimGenericArguments,
22
25
  TestDirectory,
23
26
  } from "@tui-sandbox/library/dist/src/server/types"
27
+ import type { StartTerminalGenericArguments } from "@tui-sandbox/library/src/server/terminal/TerminalTestApplication"
24
28
  import type { OverrideProperties } from "type-fest"
25
29
  import type { MyTestDirectory, MyTestDirectoryFile } from "../../MyTestDirectory"
26
30
 
31
+ export type TerminalTestApplicationContext = {
32
+ /** Types text into the terminal, making the terminal application receive the
33
+ * keystrokes as input. Requires the application to be running. */
34
+ typeIntoTerminal(text: string, options?: Partial<Cypress.TypeOptions>): void
35
+
36
+ /** The test directory, providing type-safe access to its file and directory structure */
37
+ dir: TestDirectory<MyTestDirectory>
38
+ }
39
+
27
40
  /** The api that can be used in tests after a Neovim instance has been started. */
28
41
  export type NeovimContext = {
29
42
  /** Types text into the terminal, making the terminal application receive
@@ -91,6 +104,14 @@ Cypress.Commands.add("startNeovim", (startArguments?: MyStartNeovimServerArgumen
91
104
  })
92
105
  })
93
106
 
107
+ Cypress.Commands.add("startTerminalApplication", (args: StartTerminalGenericArguments) => {
108
+ cy.window().then(async win => {
109
+ const api: GenericTerminalBrowserApi = await win.startTerminalApplication(args)
110
+
111
+ return api
112
+ })
113
+ })
114
+
94
115
  Cypress.Commands.add("typeIntoTerminal", (text: string, options?: Partial<Cypress.TypeOptions>) => {
95
116
  // the syntax for keys is described here:
96
117
  // https://docs.cypress.io/api/commands/type
@@ -110,6 +131,7 @@ declare global {
110
131
  namespace Cypress {
111
132
  interface Chainable {
112
133
  startNeovim(args?: MyStartNeovimServerArguments): Chainable<NeovimContext>
134
+ startTerminalApplication(args: StartTerminalGenericArguments): Chainable<TerminalTestApplicationContext>
113
135
 
114
136
  /** Types text into the terminal, making the terminal application receive
115
137
  * the keystrokes as input. Requires neovim to be running. */
@@ -32,6 +32,7 @@ describe("dirtree", () => {
32
32
  name: z.literal("test-environment/"),
33
33
  type: z.literal("directory"),
34
34
  contents: z.object({
35
+ ".bashrc": z.object({ name: z.literal(".bashrc"), type: z.literal("file") }),
35
36
  ".config": z.object({
36
37
  name: z.literal(".config/"),
37
38
  type: z.literal("directory"),
@@ -120,6 +121,7 @@ describe("dirtree", () => {
120
121
  export type MyDirectoryTree = MyDirectoryTreeContentsSchemaType["contents"]
121
122
 
122
123
  export const testDirectoryFiles = z.enum([
124
+ ".bashrc",
123
125
  ".config/.gitkeep",
124
126
  ".config/nvim/init.lua",
125
127
  ".config/nvim/prepare.lua",
@@ -24,7 +24,7 @@ type ResettableState = {
24
24
  socketPath: string;
25
25
  client: Lazy<Promise<NeovimApiClient>>;
26
26
  };
27
- export declare class NeovimApplication {
27
+ export declare class NeovimApplication implements AsyncDisposable {
28
28
  private readonly testEnvironmentPath;
29
29
  readonly application: DisposableSingleApplication;
30
30
  state: ResettableState | undefined;
@@ -50,7 +50,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
50
50
  var e = new Error(message);
51
51
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
52
  });
53
- import fs, { rmdirSync } from "fs";
53
+ import fs from "fs";
54
54
  import nodePath from "path";
55
55
  import { expect, it } from "vitest";
56
56
  import { createTempDir } from "./createTempDir.js";
@@ -62,10 +62,11 @@ class TempDirectory {
62
62
  }
63
63
  static create() {
64
64
  const tmp = fs.mkdtempSync("test-temp-dir-");
65
- return new TempDirectory(tmp);
65
+ const absolutePath = nodePath.resolve(tmp);
66
+ return new TempDirectory(absolutePath);
66
67
  }
67
68
  [Symbol.dispose]() {
68
- rmdirSync(this.path, { recursive: true });
69
+ fs.rmdirSync(this.path, { recursive: true, maxRetries: 5 });
69
70
  }
70
71
  }
71
72
  it("should create a temp dir with no contents", async () => {
@@ -79,8 +80,8 @@ it("should create a temp dir with no contents", async () => {
79
80
  });
80
81
  expect(result.contents).toEqual({});
81
82
  expect(result.testEnvironmentPath).toEqual(dir.path);
82
- expect(result.testEnvironmentPath.startsWith("test-temp-dir-")).toBeTruthy();
83
- expect(result.testEnvironmentPathRelative.startsWith("testdirs")).toBeTruthy();
83
+ expect(result.testEnvironmentPath.includes("test-temp-dir-")).toBeTruthy();
84
+ expect(result.testEnvironmentPathRelative.includes("testdirs")).toBeTruthy();
84
85
  }
85
86
  catch (e_1) {
86
87
  env_1.error = e_1;
@@ -78,11 +78,16 @@ export async function installDependencies(testEnvironmentPath, config) {
78
78
  return;
79
79
  }
80
80
  console.log(`🚀 Running Neovim prepareFilePath ${prepareFilePath}...`);
81
+ let output = "";
81
82
  app.events.on("stdout", data => {
82
- console.log(` neovim output: ${data}`);
83
+ assert(data);
84
+ assert(typeof data === "string");
85
+ output += data;
83
86
  });
84
87
  await app.startNextAndKillCurrent(testDirectory, { filename: "empty.txt", headlessCmd: `lua dofile("${prepareFilePath}")` }, { cols: 80, rows: 24 });
85
88
  await app.application.untilExit();
89
+ console.log(`🚀 Neovim installDependencies output:`);
90
+ console.log(output);
86
91
  }
87
92
  catch (e_1) {
88
93
  env_1.error = e_1;
@@ -98,27 +98,67 @@ export declare function createAppRouter(config: DirectoriesConfig): Promise<impo
98
98
  errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
99
99
  transformer: false;
100
100
  }, import("@trpc/server/unstable-core-do-not-import").DecorateCreateRouterOptions<{
101
+ terminal: import("@trpc/server/unstable-core-do-not-import").BuiltRouter<{
102
+ ctx: object;
103
+ meta: object;
104
+ errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
105
+ transformer: false;
106
+ }, import("@trpc/server/unstable-core-do-not-import").DecorateCreateRouterOptions<{
107
+ onStdout: import("@trpc/server").TRPCSubscriptionProcedure<{
108
+ input: {
109
+ client: {
110
+ tabId: string;
111
+ };
112
+ };
113
+ output: AsyncIterable<string, void, unknown>;
114
+ }>;
115
+ start: import("@trpc/server").TRPCMutationProcedure<{
116
+ input: {
117
+ tabId: {
118
+ tabId: string;
119
+ };
120
+ startTerminalArguments: {
121
+ commandToRun: string[];
122
+ terminalDimensions: {
123
+ cols: number;
124
+ rows: number;
125
+ };
126
+ additionalEnvironmentVariables?: Record<string, string> | undefined;
127
+ };
128
+ };
129
+ output: void;
130
+ }>;
131
+ sendStdin: import("@trpc/server").TRPCMutationProcedure<{
132
+ input: {
133
+ data: string;
134
+ tabId: {
135
+ tabId: string;
136
+ };
137
+ };
138
+ output: void;
139
+ }>;
140
+ }>>;
101
141
  neovim: import("@trpc/server/unstable-core-do-not-import").BuiltRouter<{
102
142
  ctx: object;
103
143
  meta: object;
104
144
  errorShape: import("@trpc/server/unstable-core-do-not-import").DefaultErrorShape;
105
145
  transformer: false;
106
- }, {
146
+ }, import("@trpc/server/unstable-core-do-not-import").DecorateCreateRouterOptions<{
107
147
  start: import("@trpc/server").TRPCMutationProcedure<{
108
148
  input: {
109
149
  tabId: {
110
150
  tabId: string;
111
151
  };
112
152
  startNeovimArguments: {
113
- filename: string | {
114
- openInVerticalSplits: string[];
115
- };
116
153
  terminalDimensions: {
117
154
  cols: number;
118
155
  rows: number;
119
156
  };
120
- startupScriptModifications?: string[] | undefined;
157
+ filename: string | {
158
+ openInVerticalSplits: string[];
159
+ };
121
160
  additionalEnvironmentVariables?: Record<string, string> | undefined;
161
+ startupScriptModifications?: string[] | undefined;
122
162
  };
123
163
  };
124
164
  output: import("./types.js").TestDirectory;
@@ -174,7 +214,7 @@ export declare function createAppRouter(config: DirectoriesConfig): Promise<impo
174
214
  };
175
215
  output: import("./types.js").RunExCommandOutput;
176
216
  }>;
177
- }>;
217
+ }>>;
178
218
  }>>>;
179
219
  export type AppRouter = Awaited<ReturnType<typeof createAppRouter>>;
180
220
  export type RouterInput = inferRouterInputs<AppRouter>;
@@ -2,6 +2,7 @@ import "core-js/proposals/async-explicit-resource-management.js";
2
2
  import { z } from "zod";
3
3
  import { trpc } from "./connection/trpc.js";
4
4
  import * as neovim from "./neovim/index.js";
5
+ import * as terminal from "./terminal/index.js";
5
6
  import { TestServer } from "./TestServer.js";
6
7
  import { applicationAvailable } from "./utilities/applicationAvailable.js";
7
8
  import { tabIdSchema } from "./utilities/tabId.js";
@@ -29,6 +30,29 @@ export async function createAppRouter(config) {
29
30
  throw new Error("Neovim is not installed. Please install Neovim (nvim).");
30
31
  }
31
32
  const appRouter = trpc.router({
33
+ terminal: trpc.router({
34
+ onStdout: trpc.procedure.input(z.object({ client: tabIdSchema })).subscription(options => {
35
+ return terminal.initializeStdout(options.input, options.signal, config.testEnvironmentPath);
36
+ }),
37
+ start: trpc.procedure
38
+ .input(z.object({
39
+ tabId: tabIdSchema,
40
+ startTerminalArguments: z.object({
41
+ commandToRun: z.array(z.string()),
42
+ additionalEnvironmentVariables: z.record(z.string()).optional(),
43
+ terminalDimensions: z.object({
44
+ cols: z.number(),
45
+ rows: z.number(),
46
+ }),
47
+ }),
48
+ }))
49
+ .mutation(options => {
50
+ return terminal.start(options.input.startTerminalArguments.terminalDimensions, options.input.startTerminalArguments.commandToRun, options.input.tabId, config);
51
+ }),
52
+ sendStdin: trpc.procedure.input(z.object({ tabId: tabIdSchema, data: z.string() })).mutation(options => {
53
+ return terminal.sendStdin(options.input);
54
+ }),
55
+ }),
32
56
  neovim: trpc.router({
33
57
  start: trpc.procedure
34
58
  .input(z.object({
@@ -0,0 +1,22 @@
1
+ import EventEmitter from "events";
2
+ import type { TerminalDimensions } from "../neovim/NeovimApplication.js";
3
+ import type { TestDirectory } from "../types.js";
4
+ import { DisposableSingleApplication } from "../utilities/DisposableSingleApplication.js";
5
+ type ResettableState = {
6
+ testDirectory: TestDirectory;
7
+ };
8
+ export type StartTerminalGenericArguments = {
9
+ commandToRun: string[];
10
+ additionalEnvironmentVariables?: Record<string, string> | undefined;
11
+ };
12
+ export default class TerminalTestApplication implements AsyncDisposable {
13
+ private readonly testEnvironmentPath;
14
+ readonly application: DisposableSingleApplication;
15
+ state: ResettableState | undefined;
16
+ readonly events: EventEmitter;
17
+ constructor(testEnvironmentPath: string, application?: DisposableSingleApplication);
18
+ startNextAndKillCurrent(testDirectory: TestDirectory, startArgs: StartTerminalGenericArguments, terminalDimensions: TerminalDimensions): Promise<void>;
19
+ getEnvironmentVariables(testDirectory: TestDirectory, additionalEnvironmentVariables?: Record<string, string>): NodeJS.ProcessEnv;
20
+ [Symbol.asyncDispose](): Promise<void>;
21
+ }
22
+ export {};
@@ -0,0 +1,60 @@
1
+ import assert from "assert";
2
+ import { exec } from "child_process";
3
+ import EventEmitter from "events";
4
+ import { join } from "path";
5
+ import { DisposableSingleApplication } from "../utilities/DisposableSingleApplication.js";
6
+ import { TerminalApplication } from "../utilities/TerminalApplication.js";
7
+ export default class TerminalTestApplication {
8
+ testEnvironmentPath;
9
+ application;
10
+ state;
11
+ events;
12
+ constructor(testEnvironmentPath, application = new DisposableSingleApplication()) {
13
+ this.testEnvironmentPath = testEnvironmentPath;
14
+ this.application = application;
15
+ this.events = new EventEmitter();
16
+ }
17
+ async startNextAndKillCurrent(testDirectory, startArgs, terminalDimensions) {
18
+ await this[Symbol.asyncDispose]();
19
+ assert(this.state === undefined, "TerminalTestApplication state should be undefined after disposing so that no previous state is reused.");
20
+ const command = startArgs.commandToRun[0];
21
+ assert(command, "No command to run was provided.");
22
+ // TODO could check if the command is executable
23
+ const terminalArguments = startArgs.commandToRun.slice(1);
24
+ const stdout = this.events;
25
+ await this.application.startNextAndKillCurrent(async () => {
26
+ const env = this.getEnvironmentVariables(testDirectory, startArgs.additionalEnvironmentVariables);
27
+ return TerminalApplication.start({
28
+ command,
29
+ args: terminalArguments,
30
+ cwd: this.testEnvironmentPath,
31
+ env: env,
32
+ dimensions: terminalDimensions,
33
+ onStdoutOrStderr(data) {
34
+ data;
35
+ stdout.emit("stdout", data);
36
+ },
37
+ });
38
+ });
39
+ const processId = this.application.processId();
40
+ assert(processId !== undefined, "TerminalApplication was started without a process ID. This is a bug - please open an issue.");
41
+ this.state = { testDirectory };
42
+ console.log(`🚀 Started Terminal instance ${processId}`);
43
+ }
44
+ getEnvironmentVariables(testDirectory, additionalEnvironmentVariables) {
45
+ return {
46
+ ...process.env,
47
+ HOME: testDirectory.rootPathAbsolute,
48
+ XDG_CONFIG_HOME: join(testDirectory.rootPathAbsolute, ".config"),
49
+ XDG_DATA_HOME: join(testDirectory.testEnvironmentPath, ".repro", "data"),
50
+ ...additionalEnvironmentVariables,
51
+ };
52
+ }
53
+ async [Symbol.asyncDispose]() {
54
+ await this.application[Symbol.asyncDispose]();
55
+ if (!this.state)
56
+ return;
57
+ exec(`rm -rf ${this.state.testDirectory.rootPathAbsolute}`);
58
+ this.state = undefined;
59
+ }
60
+ }
@@ -0,0 +1,12 @@
1
+ import "core-js/proposals/async-explicit-resource-management.js";
2
+ import type { TerminalDimensions } from "../neovim/NeovimApplication.js";
3
+ import type { DirectoriesConfig } from "../updateTestdirectorySchemaFile.js";
4
+ import type { TabId } from "../utilities/tabId.js";
5
+ export declare function start(terminalDimensions: TerminalDimensions, commandToRun: string[], tabId: TabId, config: DirectoriesConfig): Promise<void>;
6
+ export declare function initializeStdout(options: {
7
+ client: TabId;
8
+ }, signal: AbortSignal | undefined, testEnvironmentPath: string): Promise<AsyncGenerator<string, void, unknown>>;
9
+ export declare function sendStdin(options: {
10
+ tabId: TabId;
11
+ data: string;
12
+ }): Promise<void>;
@@ -0,0 +1,40 @@
1
+ import assert from "assert";
2
+ import "core-js/proposals/async-explicit-resource-management.js";
3
+ import { prepareNewTestDirectory } from "../neovim/index.js";
4
+ import { convertEventEmitterToAsyncGenerator } from "../utilities/generator.js";
5
+ import { Lazy } from "../utilities/Lazy.js";
6
+ import TerminalTestApplication from "./TerminalTestApplication.js";
7
+ const terminals = new Map();
8
+ const resources = new Lazy(() => {
9
+ return new AsyncDisposableStack();
10
+ });
11
+ export async function start(terminalDimensions, commandToRun, tabId, config) {
12
+ const app = terminals.get(tabId.tabId);
13
+ assert(app, `Terminal with tabId ${tabId.tabId} not found.`);
14
+ const testDirectory = await prepareNewTestDirectory(config);
15
+ await app.startNextAndKillCurrent(testDirectory, { commandToRun }, terminalDimensions);
16
+ }
17
+ export async function initializeStdout(options, signal, testEnvironmentPath) {
18
+ const tabId = options.client.tabId;
19
+ const app = terminals.get(tabId) ?? new TerminalTestApplication(testEnvironmentPath);
20
+ if (terminals.get(tabId) === undefined) {
21
+ terminals.set(tabId, app);
22
+ resources.get().adopt(app, async (a) => {
23
+ await a[Symbol.asyncDispose]();
24
+ });
25
+ }
26
+ const stdout = convertEventEmitterToAsyncGenerator(app.events, "stdout");
27
+ signal?.addEventListener("abort", () => {
28
+ void app[Symbol.asyncDispose]().finally(() => {
29
+ terminals.delete(tabId);
30
+ });
31
+ });
32
+ return stdout;
33
+ }
34
+ export async function sendStdin(options) {
35
+ const tabId = options.tabId.tabId;
36
+ const app = terminals.get(tabId);
37
+ assert(app !== undefined, `Terminal instance for clientId not found - cannot send stdin. Maybe it's not started yet?`);
38
+ assert(app.application, `Terminal application not found for client id ${options.tabId.tabId}. Maybe it's not started yet?`);
39
+ await app.application.write(options.data);
40
+ }
@@ -26,6 +26,11 @@ export type TestDirectory<TContents extends object = object> = {
26
26
  testEnvironmentPathRelative: string;
27
27
  contents: TContents;
28
28
  };
29
+ export type TestEnvironmentCommonEnvironmentVariables = {
30
+ HOME: string;
31
+ XDG_CONFIG_HOME: string;
32
+ XDG_DATA_HOME: string;
33
+ };
29
34
  export type { StartNeovimGenericArguments } from "../server/neovim/NeovimApplication.js";
30
35
  export type BlockingShellCommandOutput = {
31
36
  type: "success";