@tui-sandbox/library 11.11.2 → 12.0.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 (92) hide show
  1. package/dist/browser/assets/index-CYUPHpRk.css +1 -0
  2. package/dist/browser/assets/index-DPQpUaDL.js +9 -0
  3. package/dist/browser/index.html +2 -2
  4. package/dist/src/browser/neovim-client.d.ts +2 -0
  5. package/dist/src/browser/neovim-client.js +2 -0
  6. package/dist/src/browser/neovim-client.js.map +1 -1
  7. package/dist/src/client/index.d.ts +1 -0
  8. package/dist/src/client/neovim-terminal-client.d.ts +0 -2
  9. package/dist/src/client/neovim-terminal-client.js +0 -2
  10. package/dist/src/client/neovim-terminal-client.js.map +1 -1
  11. package/dist/src/client/startTerminal.d.ts +0 -2
  12. package/dist/src/client/startTerminal.js +0 -2
  13. package/dist/src/client/startTerminal.js.map +1 -1
  14. package/dist/src/client/terminal-terminal-client.d.ts +0 -1
  15. package/dist/src/client/terminal-terminal-client.js +0 -1
  16. package/dist/src/client/terminal-terminal-client.js.map +1 -1
  17. package/dist/src/server/cypress-support/contents.js +9 -11
  18. package/dist/src/server/cypress-support/contents.js.map +1 -1
  19. package/dist/src/server/index.d.ts +4 -0
  20. package/dist/src/server/index.js.map +1 -1
  21. package/dist/tsconfig.tsbuildinfo +1 -1
  22. package/package.json +34 -8
  23. package/CHANGELOG.md +0 -941
  24. package/dist/browser/assets/index-BQzArJW3.js +0 -9
  25. package/dist/browser/assets/index-hkmOP7rU.css +0 -1
  26. package/index.html +0 -11
  27. package/src/browser/neovim-client.ts +0 -126
  28. package/src/client/MyNeovimConfigModification.test.ts +0 -25
  29. package/src/client/MyNeovimConfigModification.ts +0 -8
  30. package/src/client/color-utilities.test.ts +0 -10
  31. package/src/client/color-utilities.ts +0 -9
  32. package/src/client/cypress-assertions.ts +0 -35
  33. package/src/client/index.ts +0 -4
  34. package/src/client/neovim-terminal-client.ts +0 -130
  35. package/src/client/public/DejaVuSansMNerdFontMono-Regular.ttf +0 -0
  36. package/src/client/startTerminal.ts +0 -97
  37. package/src/client/style.css +0 -32
  38. package/src/client/terminal-config.ts +0 -25
  39. package/src/client/terminal-terminal-client.ts +0 -106
  40. package/src/client/validateMouseEvent.ts +0 -22
  41. package/src/scripts/commands/commandRun.ts +0 -68
  42. package/src/scripts/commands/commandTuiNeovimExec.ts +0 -36
  43. package/src/scripts/commands/commandTuiNeovimPrepare.ts +0 -12
  44. package/src/scripts/commands/commandTuiStart.ts +0 -36
  45. package/src/scripts/parseArguments.test.ts +0 -28
  46. package/src/scripts/parseArguments.ts +0 -66
  47. package/src/scripts/resolveConfig.test.ts +0 -78
  48. package/src/scripts/resolveTuiConfig.ts +0 -65
  49. package/src/scripts/tui.ts +0 -77
  50. package/src/server/TestServer.ts +0 -73
  51. package/src/server/applications/neovim/NeovimApplication.ts +0 -236
  52. package/src/server/applications/neovim/NeovimJavascriptApiClient.test.ts +0 -48
  53. package/src/server/applications/neovim/NeovimJavascriptApiClient.ts +0 -68
  54. package/src/server/applications/neovim/api.ts +0 -257
  55. package/src/server/applications/neovim/environment/TempDirectory.ts +0 -18
  56. package/src/server/applications/neovim/environment/createTempDir.test.ts +0 -29
  57. package/src/server/applications/neovim/environment/createTempDir.ts +0 -91
  58. package/src/server/applications/neovim/neovimRouter.ts +0 -100
  59. package/src/server/applications/neovim/prepareNewTestDirectory.test.ts +0 -32
  60. package/src/server/applications/neovim/prepareNewTestDirectory.ts +0 -21
  61. package/src/server/applications/terminal/TerminalTestApplication.ts +0 -101
  62. package/src/server/applications/terminal/api.ts +0 -89
  63. package/src/server/applications/terminal/runBlockingShellCommand.test.ts +0 -41
  64. package/src/server/applications/terminal/runBlockingShellCommand.ts +0 -64
  65. package/src/server/applications/terminal/terminalRouter.ts +0 -47
  66. package/src/server/blockingCommandInputSchema.test.ts +0 -24
  67. package/src/server/blockingCommandInputSchema.ts +0 -27
  68. package/src/server/config.test.ts +0 -7
  69. package/src/server/config.ts +0 -18
  70. package/src/server/connection/trpc.ts +0 -3
  71. package/src/server/cypress-support/contents.ts +0 -245
  72. package/src/server/cypress-support/createCypressSupportFile.test.ts +0 -69
  73. package/src/server/cypress-support/createCypressSupportFile.ts +0 -56
  74. package/src/server/dirtree/index.test.ts +0 -352
  75. package/src/server/dirtree/index.ts +0 -144
  76. package/src/server/dirtree/json-to-zod.ts +0 -60
  77. package/src/server/index.ts +0 -6
  78. package/src/server/server.ts +0 -34
  79. package/src/server/types.ts +0 -98
  80. package/src/server/updateTestdirectorySchemaFile.test.ts +0 -49
  81. package/src/server/updateTestdirectorySchemaFile.ts +0 -71
  82. package/src/server/utilities/DisposableSingleApplication.test.ts +0 -92
  83. package/src/server/utilities/DisposableSingleApplication.ts +0 -49
  84. package/src/server/utilities/Lazy.ts +0 -16
  85. package/src/server/utilities/TerminalApplication.ts +0 -100
  86. package/src/server/utilities/generator.test.ts +0 -50
  87. package/src/server/utilities/generator.ts +0 -12
  88. package/src/server/utilities/tabId.ts +0 -4
  89. package/src/server/utilities/timeout.ts +0 -3
  90. package/src/server/utilities/timeoutable.ts +0 -19
  91. package/tsconfig.json +0 -31
  92. package/vite.config.js +0 -27
@@ -1 +0,0 @@
1
- .xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;inset:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;inset:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}@font-face{font-family:DejaVuSansMNerdFontMono;src:url(/assets/DejaVuSansMNerdFontMono-Regular-CRJgiq0O.ttf);font-weight:400;font-style:normal}:root{color-scheme:dark;background-color:#000}#app{display:flex;height:100vh;width:100vw}*{font-family:DejaVuSansMNerdFontMono,monospace}.xterm .xterm-viewport{overflow-y:hidden}body{overflow-y:hidden;overflow-x:hidden;margin:0}
package/index.html DELETED
@@ -1,11 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <title>tui-sandbox integration tests</title>
6
- </head>
7
- <body>
8
- <div id="app"></div>
9
- <script type="module" src="./src/browser/neovim-client.ts"></script>
10
- </body>
11
- </html>
@@ -1,126 +0,0 @@
1
- import type { Terminal } from "@xterm/xterm"
2
- import { NeovimTerminalClient } from "../client/neovim-terminal-client.js"
3
- import type { TuiTerminalApi } from "../client/startTerminal.js"
4
- import { TerminalTerminalClient } from "../client/terminal-terminal-client.js"
5
- import type {
6
- ExCommandClientInput,
7
- LuaCodeClientInput,
8
- PollLuaCodeClientInput,
9
- RunLuaFileClientInput,
10
- } from "../server/applications/neovim/neovimRouter.js"
11
- import type { StartTerminalGenericArguments } from "../server/applications/terminal/TerminalTestApplication.js"
12
- import type { BlockingCommandClientInput } from "../server/blockingCommandInputSchema.js"
13
- import type {
14
- AllKeys,
15
- BlockingShellCommandOutput,
16
- RunExCommandOutput,
17
- RunLuaCodeOutput,
18
- StartNeovimGenericArguments,
19
- TestDirectory,
20
- } from "../server/types.js"
21
- import { Lazy } from "../server/utilities/Lazy.js"
22
-
23
- const app = document.querySelector<HTMLElement>("#app")
24
- if (!app) {
25
- throw new Error("No app element found")
26
- }
27
-
28
- // limitation: right now only one client can be used in the same test
29
- const neovimClient = new Lazy(() => new NeovimTerminalClient(app))
30
- const terminalClient = new Lazy(() => new TerminalTerminalClient(app))
31
-
32
- export type GenericNeovimBrowserApi = {
33
- runBlockingShellCommand(input: BlockingCommandClientInput): Promise<BlockingShellCommandOutput>
34
- runLuaCode(input: LuaCodeClientInput): Promise<RunLuaCodeOutput>
35
- doFile(input: RunLuaFileClientInput): Promise<RunLuaCodeOutput>
36
- waitForLuaCode(input: PollLuaCodeClientInput): Promise<RunLuaCodeOutput>
37
- runExCommand(input: ExCommandClientInput): Promise<RunExCommandOutput>
38
- dir: TestDirectory
39
- }
40
-
41
- /** Entrypoint for the test runner (cypress) */
42
- window.startNeovim = async function (startArgs?: StartNeovimGenericArguments): Promise<GenericNeovimBrowserApi> {
43
- const neovim = neovimClient.get()
44
- const testDirectory = await neovim.startNeovim({
45
- additionalEnvironmentVariables: startArgs?.additionalEnvironmentVariables,
46
- filename: startArgs?.filename ?? "initial-file.txt",
47
- startupScriptModifications: startArgs?.startupScriptModifications ?? [],
48
- headlessCmd: undefined,
49
- NVIM_APPNAME: startArgs?.NVIM_APPNAME,
50
- } satisfies AllKeys<StartNeovimGenericArguments>)
51
-
52
- const neovimBrowserApi: GenericNeovimBrowserApi = {
53
- runBlockingShellCommand(input: BlockingCommandClientInput): Promise<BlockingShellCommandOutput> {
54
- return neovim.runBlockingShellCommand(input)
55
- },
56
- runLuaCode(input) {
57
- return neovim.runLuaCode(input)
58
- },
59
- doFile(input) {
60
- return neovim.doFile(input)
61
- },
62
- waitForLuaCode(input) {
63
- return neovim.waitForLuaCode(input)
64
- },
65
- runExCommand(input) {
66
- return neovim.runExCommand(input)
67
- },
68
- dir: testDirectory,
69
- }
70
-
71
- return neovimBrowserApi
72
- }
73
-
74
- declare global {
75
- interface Window {
76
- startNeovim(startArguments?: StartNeovimGenericArguments): Promise<GenericNeovimBrowserApi>
77
- startTerminalApplication(args: StartTerminalBrowserArguments): Promise<GenericTerminalBrowserApi>
78
- }
79
- }
80
-
81
- export type GenericTerminalBrowserApi = {
82
- dir: TestDirectory
83
- runBlockingShellCommand(input: BlockingCommandClientInput): Promise<BlockingShellCommandOutput>
84
- }
85
-
86
- export type BrowserTerminalSettings = {
87
- configureTerminal?: (term: {
88
- terminal: Terminal
89
- api: TuiTerminalApi
90
- recipes: {
91
- /** Make the terminal respond to "DA1—Primary Device Attributes" requests.
92
- *
93
- * In this DA exchange, the host asks for the terminal's architectural class and basic attributes.
94
- * https://vt100.net/docs/vt510-rm/DA1.html
95
- *
96
- * Terminal Response
97
- *
98
- * The terminal responds by sending its architectural class and basic
99
- * attributes to the host. This response depends on the terminal's current
100
- * operating VT level.
101
- */
102
- supportDA1: () => void
103
- }
104
- }) => void
105
- }
106
-
107
- export type StartTerminalBrowserArguments = {
108
- serverSettings: StartTerminalGenericArguments
109
- browserSettings: BrowserTerminalSettings
110
- }
111
-
112
- /** Entrypoint for the test runner (cypress) */
113
- window.startTerminalApplication = async function (
114
- args: StartTerminalBrowserArguments
115
- ): Promise<GenericTerminalBrowserApi> {
116
- const terminal = terminalClient.get()
117
- const testDirectory = await terminal.startTerminalApplication(args)
118
-
119
- const terminalBrowserApi: GenericTerminalBrowserApi = {
120
- dir: testDirectory,
121
- runBlockingShellCommand(input) {
122
- return terminal.runBlockingShellCommand(input)
123
- },
124
- }
125
- return terminalBrowserApi
126
- }
@@ -1,25 +0,0 @@
1
- import { assertType, it } from "vitest"
2
- import * as z from "zod"
3
- import type { MyNeovimConfigModification } from "./MyNeovimConfigModification.js"
4
-
5
- const testDirectoryFiles = z.enum([
6
- "config-modifications/add_command_to_count_open_buffers.lua",
7
- "config-modifications/add_command_to_update_buffer_after_timeout.lua",
8
- "config-modifications/don't_crash_when_modification_contains_unescaped_characters\".lua",
9
- "config-modifications/subdir/subdir-modification.lua",
10
- "config-modifications/subdir",
11
- "config-modifications",
12
- ])
13
- type MyTestDirectoryFile = z.infer<typeof testDirectoryFiles>
14
-
15
- type result = MyNeovimConfigModification<MyTestDirectoryFile>
16
-
17
- it("returns config-modifications recursively", () => {
18
- assertType<
19
- | "add_command_to_count_open_buffers.lua"
20
- | "add_command_to_update_buffer_after_timeout.lua"
21
- | "don't_crash_when_modification_contains_unescaped_characters\".lua"
22
- | "subdir/subdir-modification.lua"
23
- | "subdir"
24
- >(1 as unknown as result)
25
- })
@@ -1,8 +0,0 @@
1
- /** Returns all the available Neovim config-modifications, recursively.
2
- * @type T - the MyTestDirectoryFile type, which is generated by tui-sandbox
3
- */
4
- export type MyNeovimConfigModification<T extends string> = T extends `config-modifications/${infer Rest}`
5
- ? Rest extends ""
6
- ? never
7
- : Rest
8
- : never
@@ -1,10 +0,0 @@
1
- import { describe, expect, it } from "vitest"
2
- import type { RgbColor } from "./color-utilities.js"
3
- import { rgbify } from "./color-utilities.js"
4
-
5
- describe("rgbify", () => {
6
- it("converts a catppuccin RGB color to a CSS color string", () => {
7
- const color = { r: 1, g: 2, b: 3 } satisfies RgbColor
8
- expect(rgbify(color)).toEqual("rgb(1, 2, 3)")
9
- })
10
- })
@@ -1,9 +0,0 @@
1
- import type { flavors } from "@catppuccin/palette"
2
-
3
- export type RgbColor = (typeof flavors.macchiato.colors)["surface0"]["rgb"]
4
-
5
- /** Convert a catppuccin RGB color to a CSS color string. This way you can
6
- * assert that text that's visible on the screen has a specific color. */
7
- export function rgbify(color: RgbColor): string {
8
- return `rgb(${color.r.toString()}, ${color.g.toString()}, ${color.b.toString()})`
9
- }
@@ -1,35 +0,0 @@
1
- /// <reference types="cypress" />
2
-
3
- /** Problem: cypress provides the `contains` method, but it only checks the
4
- * first match on the page.
5
- *
6
- * Solution: we need to check all elements on the page and filter them
7
- * by the text we are looking for. Then we can check if the background
8
- * color of the element is the same as the one we are looking for.
9
- *
10
- * Limitation: text spanning multiple lines will not be detected.
11
- */
12
- export function textIsVisibleWithColor(text: string, color: string): Cypress.Chainable<JQuery> {
13
- return cy
14
- .get("div.xterm-rows span")
15
- .filter(`:contains(${text})`)
16
- .should("exist") // ensures there's at least one match
17
- .should($els => {
18
- const colors = $els.map((_, el) => window.getComputedStyle(el).color).toArray()
19
- expect(colors).to.include(color)
20
- })
21
- }
22
-
23
- /** Like `textIsVisibleWithColor`, but checks the background color instead
24
- * of the text color.
25
- */
26
- export function textIsVisibleWithBackgroundColor(text: string, color: string): Cypress.Chainable<JQuery> {
27
- return cy
28
- .get("div.xterm-rows span")
29
- .filter(`:contains(${text})`)
30
- .should("exist") // ensures there's at least one match
31
- .should($els => {
32
- const colors = $els.map((_, el) => window.getComputedStyle(el).backgroundColor).toArray()
33
- expect(colors).to.include(color)
34
- })
35
- }
@@ -1,4 +0,0 @@
1
- // This is the public client api. Semantic versioning will be applied to this.
2
-
3
- export { rgbify } from "./color-utilities.js"
4
- export { textIsVisibleWithBackgroundColor, textIsVisibleWithColor } from "./cypress-assertions.js"
@@ -1,130 +0,0 @@
1
- import { createTRPCClient, httpBatchLink, httpSubscriptionLink, splitLink } from "@trpc/client"
2
- import type { Terminal } from "@xterm/xterm"
3
- import "@xterm/xterm/css/xterm.css"
4
- import type {
5
- ExCommandClientInput,
6
- LuaCodeClientInput,
7
- PollLuaCodeClientInput,
8
- RunLuaFileClientInput,
9
- } from "../server/applications/neovim/neovimRouter.js"
10
- import type { BlockingCommandClientInput } from "../server/blockingCommandInputSchema.js"
11
- import type { AppRouter } from "../server/server.js"
12
- import type {
13
- BlockingShellCommandOutput,
14
- RunExCommandOutput,
15
- RunLuaCodeOutput,
16
- StartNeovimGenericArguments,
17
- TestDirectory,
18
- } from "../server/types.js"
19
- import { getTabId, startTerminal } from "./startTerminal.js"
20
- import "./style.css"
21
-
22
- /** Manages the terminal state in the browser as well as the (browser's)
23
- * connection to the server side terminal application api. */
24
- export class NeovimTerminalClient {
25
- private readonly ready: Promise<void>
26
- private readonly tabId: { tabId: string }
27
- private readonly terminal: Terminal
28
- private readonly trpc: ReturnType<typeof createTRPCClient<AppRouter>>
29
-
30
- constructor(app: HTMLElement) {
31
- const trpc = createTRPCClient<AppRouter>({
32
- links: [
33
- splitLink({
34
- condition: operation => operation.type === "subscription",
35
- true: httpSubscriptionLink({
36
- url: "/trpc",
37
- }),
38
- false: httpBatchLink({
39
- url: "/trpc",
40
- }),
41
- }),
42
- ],
43
- })
44
- this.trpc = trpc
45
-
46
- this.tabId = getTabId()
47
- const tabId = this.tabId
48
-
49
- const terminal = startTerminal(app, {
50
- onMouseEvent(data: string) {
51
- void trpc.neovim.sendStdin.mutate({ tabId, data }).catch((error: unknown) => {
52
- console.error(`Error sending mouse event`, error)
53
- })
54
- },
55
- onKeyPress(event) {
56
- void trpc.neovim.sendStdin.mutate({ tabId, data: event.key })
57
- },
58
- })
59
- this.terminal = terminal
60
-
61
- // start listening to Neovim stdout - this will take some (short) amount of
62
- // time to complete
63
- this.ready = new Promise<void>(resolve => {
64
- console.log("Subscribing to stdout")
65
- trpc.neovim.onStdout.subscribe(
66
- { client: tabId },
67
- {
68
- onStarted() {
69
- resolve()
70
- },
71
- onData(data: string) {
72
- terminal.write(data)
73
- },
74
- onError(err: unknown) {
75
- console.error(`Error from the application`, err)
76
- },
77
- }
78
- )
79
- })
80
- }
81
-
82
- public async startNeovim(args: StartNeovimGenericArguments): Promise<TestDirectory> {
83
- await this.ready
84
-
85
- const testDirectory = await this.trpc.neovim.start.mutate({
86
- startNeovimArguments: {
87
- filename: args.filename,
88
- additionalEnvironmentVariables: args.additionalEnvironmentVariables,
89
- startupScriptModifications: args.startupScriptModifications,
90
- NVIM_APPNAME: args.NVIM_APPNAME,
91
- },
92
- terminalDimensions: {
93
- cols: this.terminal.cols,
94
- rows: this.terminal.rows,
95
- },
96
- tabId: this.tabId,
97
- })
98
-
99
- return testDirectory
100
- }
101
-
102
- public async runBlockingShellCommand(input: BlockingCommandClientInput): Promise<BlockingShellCommandOutput> {
103
- await this.ready
104
- return this.trpc.neovim.runBlockingShellCommand.mutate({ ...input, tabId: this.tabId })
105
- }
106
-
107
- public async runLuaCode(input: LuaCodeClientInput): Promise<RunLuaCodeOutput> {
108
- await this.ready
109
- return this.trpc.neovim.runLuaCode.mutate({ ...input, tabId: this.tabId })
110
- }
111
-
112
- public async doFile(input: RunLuaFileClientInput): Promise<RunExCommandOutput> {
113
- await this.ready
114
- return this.trpc.neovim.runExCommand.mutate({
115
- ...input,
116
- tabId: this.tabId,
117
- command: `lua dofile("${input.luaFile}")`,
118
- })
119
- }
120
-
121
- public async waitForLuaCode(input: PollLuaCodeClientInput): Promise<RunLuaCodeOutput> {
122
- await this.ready
123
- return this.trpc.neovim.waitForLuaCode.mutate({ ...input, tabId: this.tabId })
124
- }
125
-
126
- public async runExCommand(input: ExCommandClientInput): Promise<RunExCommandOutput> {
127
- await this.ready
128
- return this.trpc.neovim.runExCommand.mutate({ ...input, tabId: this.tabId })
129
- }
130
- }
@@ -1,97 +0,0 @@
1
- import { flavors } from "@catppuccin/palette"
2
- import { FitAddon } from "@xterm/addon-fit"
3
- import { Unicode11Addon } from "@xterm/addon-unicode11"
4
- import { Terminal } from "@xterm/xterm"
5
- import "@xterm/xterm/css/xterm.css"
6
- import * as z from "zod"
7
- import type { TabId } from "../server/utilities/tabId.ts"
8
- import "./style.css"
9
- import { validateMouseEvent } from "./validateMouseEvent.js"
10
-
11
- export type TuiTerminalApi = {
12
- onMouseEvent: (data: string) => void
13
- onKeyPress: (event: { key: string; domEvent: KeyboardEvent }) => void
14
- }
15
- export function startTerminal(app: HTMLElement, api: TuiTerminalApi): Terminal {
16
- const terminal = new Terminal({
17
- allowProposedApi: true,
18
- cursorBlink: false,
19
- convertEol: true,
20
- fontSize: 13,
21
- })
22
-
23
- const colors = flavors.macchiato.colors
24
- terminal.options.theme = {
25
- background: colors.base.hex,
26
- black: colors.crust.hex,
27
- brightBlack: colors.surface2.hex,
28
- blue: colors.blue.hex,
29
- brightBlue: colors.blue.hex,
30
- brightCyan: colors.sky.hex,
31
- brightRed: colors.maroon.hex,
32
- brightYellow: colors.yellow.hex,
33
- cursor: colors.text.hex,
34
- cyan: colors.sky.hex,
35
- foreground: colors.text.hex,
36
- green: colors.green.hex,
37
- magenta: colors.lavender.hex,
38
- red: colors.red.hex,
39
- white: colors.text.hex,
40
- yellow: colors.yellow.hex,
41
- }
42
-
43
- // The FitAddon makes the terminal fit the size of the container, the entire
44
- // page in this case
45
- const fitAddon = new FitAddon()
46
- terminal.loadAddon(fitAddon)
47
-
48
- // The Unicode11Addon fixes emoji rendering issues. Without it, emoji are
49
- // displayed as truncated (partial) images.
50
- const unicode11Addon = new Unicode11Addon()
51
- terminal.loadAddon(unicode11Addon)
52
- terminal.unicode.activeVersion = "11"
53
-
54
- terminal.open(app)
55
- fitAddon.fit()
56
-
57
- window.addEventListener("resize", () => {
58
- fitAddon.fit()
59
- })
60
-
61
- terminal.onData(data => {
62
- data satisfies string
63
- // Send mouse clicks to the terminal application
64
- //
65
- // this gets called for mouse events. However, some mouse events seem to
66
- // confuse Neovim, so for now let's just send click events
67
-
68
- if (typeof data !== "string") {
69
- throw new Error(`unexpected onData message type: '${JSON.stringify(data)}'`)
70
- }
71
-
72
- const mouseEvent = validateMouseEvent(data)
73
- if (mouseEvent) {
74
- api.onMouseEvent(mouseEvent)
75
- }
76
- })
77
-
78
- terminal.onKey(event => {
79
- api.onKeyPress(event)
80
- })
81
-
82
- return terminal
83
- }
84
-
85
- /** An identifier unique to a browser tab, so that each tab can have its own
86
- * unique session that persists across page reloads. */
87
- export function getTabId(): TabId {
88
- // Other tabs will have a different id because sessionStorage is unique to
89
- // each tab.
90
- let tabId = z.string().safeParse(sessionStorage.getItem("tabId")).data
91
- if (!tabId) {
92
- tabId = Math.random().toString(36)
93
- sessionStorage.setItem("tabId", tabId)
94
- }
95
-
96
- return { tabId }
97
- }
@@ -1,32 +0,0 @@
1
- @font-face {
2
- font-family: "DejaVuSansMNerdFontMono";
3
- src: url("./public/DejaVuSansMNerdFontMono-Regular.ttf");
4
- font-weight: normal;
5
- font-style: normal;
6
- }
7
-
8
- :root {
9
- color-scheme: dark;
10
- background-color: black;
11
- }
12
-
13
- #app {
14
- display: flex;
15
- height: 100vh;
16
- width: 100vw;
17
- }
18
-
19
- * {
20
- font-family: "DejaVuSansMNerdFontMono", monospace;
21
- }
22
-
23
- .xterm .xterm-viewport {
24
- /* hide the scrollbar */
25
- overflow-y: hidden;
26
- }
27
-
28
- body {
29
- overflow-y: hidden;
30
- overflow-x: hidden;
31
- margin: 0;
32
- }
@@ -1,25 +0,0 @@
1
- import type { Terminal } from "@xterm/xterm"
2
- import type { TuiTerminalApi } from "./startTerminal.js"
3
-
4
- /** DA1—Primary Device Attributes
5
- * In this DA exchange, the host asks for the terminal's architectural class and basic attributes.
6
- * https://vt100.net/docs/vt510-rm/DA1.html
7
- *
8
- * Terminal Response
9
- * The terminal responds by sending its architectural class and basic
10
- * attributes to the host. This response depends on the terminal's current
11
- * operating VT level.
12
- */
13
- export function supportDA1(terminal: Terminal, api: TuiTerminalApi): void {
14
- // Register a CSI handler for the 'c' command (ESC [ c)
15
- terminal.parser.registerCsiHandler({ final: "c" }, () => {
16
- // Emit a fake DA1 response: ESC [ ? 1 ; 2 c
17
- api.onKeyPress({
18
- key: "\x1b" + ("[?1;2c" satisfies FakeDA1Response),
19
- domEvent: new KeyboardEvent("keydown", { key: "Escape" }),
20
- })
21
- return true // prevent default handling
22
- })
23
- }
24
-
25
- export type FakeDA1Response = "[?1;2c"
@@ -1,106 +0,0 @@
1
- import { createTRPCClient, httpBatchLink, httpSubscriptionLink, splitLink } from "@trpc/client"
2
- import type { Terminal } from "@xterm/xterm"
3
- import "@xterm/xterm/css/xterm.css"
4
- import type { StartTerminalBrowserArguments } from "../browser/neovim-client.js"
5
- import type { BlockingCommandClientInput } from "../server/blockingCommandInputSchema.js"
6
- import type { AppRouter } from "../server/server.js"
7
- import type { BlockingShellCommandOutput, ServerTestDirectory } from "../server/types.js"
8
- import type { TuiTerminalApi } from "./startTerminal.js"
9
- import { getTabId, startTerminal } from "./startTerminal.js"
10
- import { supportDA1 } from "./terminal-config.js"
11
-
12
- /** Manages the terminal state in the browser as well as the (browser's)
13
- * connection to the server side terminal application api. */
14
- export class TerminalTerminalClient {
15
- private readonly ready: Promise<void>
16
- private readonly tabId: { tabId: string }
17
- private readonly terminal: Terminal
18
- private readonly trpc: ReturnType<typeof createTRPCClient<AppRouter>>
19
- terminalApi: TuiTerminalApi
20
-
21
- constructor(app: HTMLElement) {
22
- const trpc = createTRPCClient<AppRouter>({
23
- links: [
24
- splitLink({
25
- condition: operation => operation.type === "subscription",
26
- true: httpSubscriptionLink({
27
- url: "/trpc",
28
- }),
29
- false: httpBatchLink({
30
- url: "/trpc",
31
- }),
32
- }),
33
- ],
34
- })
35
- this.trpc = trpc
36
-
37
- this.tabId = getTabId()
38
- const tabId = this.tabId
39
-
40
- this.terminalApi = {
41
- onMouseEvent(data: string) {
42
- void trpc.terminal.sendStdin.mutate({ tabId, data }).catch((error: unknown) => {
43
- console.error(`Error sending mouse event`, error)
44
- })
45
- },
46
- onKeyPress(event) {
47
- void trpc.terminal.sendStdin.mutate({ tabId, data: event.key })
48
- },
49
- }
50
- const terminal = startTerminal(app, this.terminalApi)
51
- this.terminal = terminal
52
-
53
- // start listening to stdout - this will take some (short) amount of time
54
- // to complete
55
- this.ready = new Promise<void>(resolve => {
56
- console.log("Subscribing to stdout")
57
- trpc.terminal.onStdout.subscribe(
58
- { client: tabId },
59
- {
60
- onStarted() {
61
- resolve()
62
- },
63
- onData(data: string) {
64
- terminal.write(data)
65
- },
66
- onError(err: unknown) {
67
- console.error(`Error from the application`, err)
68
- },
69
- }
70
- )
71
- })
72
- }
73
-
74
- public async startTerminalApplication(args: StartTerminalBrowserArguments): Promise<ServerTestDirectory> {
75
- await this.ready
76
-
77
- args.browserSettings.configureTerminal?.({
78
- terminal: this.terminal,
79
- api: this.terminalApi,
80
- recipes: {
81
- supportDA1: () => {
82
- supportDA1(this.terminal, this.terminalApi)
83
- },
84
- },
85
- })
86
-
87
- const testDirectory = await this.trpc.terminal.start.mutate({
88
- tabId: this.tabId,
89
- startTerminalArguments: {
90
- additionalEnvironmentVariables: args.serverSettings.additionalEnvironmentVariables,
91
- commandToRun: args.serverSettings.commandToRun,
92
- terminalDimensions: {
93
- cols: this.terminal.cols,
94
- rows: this.terminal.rows,
95
- },
96
- },
97
- })
98
-
99
- return testDirectory
100
- }
101
-
102
- public async runBlockingShellCommand(input: BlockingCommandClientInput): Promise<BlockingShellCommandOutput> {
103
- await this.ready
104
- return this.trpc.terminal.runBlockingShellCommand.mutate({ ...input, tabId: this.tabId })
105
- }
106
- }
@@ -1,22 +0,0 @@
1
- // Function to parse mouse events
2
- // https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Button-event-tracking
3
- export function validateMouseEvent(data: string): string | undefined {
4
- // oxlint-disable-next-line no-control-regex
5
- const match = /\x1b\[<(\d+);(\d+);(\d+)([mM])/.exec(data)
6
- if (!match) {
7
- return
8
- }
9
-
10
- if (!match[1] || !match[2] || !match[3] || !match[4]) {
11
- throw new Error(`Mouse event: Invalid match for data ${data}`)
12
- }
13
-
14
- const buttonCode = parseInt(match[1], 10)
15
- const column = parseInt(match[2], 10)
16
- const row = parseInt(match[3], 10)
17
- const isRelease = match[4] === "m"
18
-
19
- console.log(`Mouse event: buttonCode=${buttonCode}, column=${column}, row=${row}, isRelease=${isRelease}`)
20
-
21
- return data
22
- }