@tui-sandbox/library 11.11.1 → 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.
- package/dist/browser/assets/index-CYUPHpRk.css +1 -0
- package/dist/browser/assets/index-DPQpUaDL.js +9 -0
- package/dist/browser/index.html +2 -2
- package/dist/src/browser/neovim-client.d.ts +2 -0
- package/dist/src/browser/neovim-client.js +2 -0
- package/dist/src/browser/neovim-client.js.map +1 -1
- package/dist/src/client/index.d.ts +1 -0
- package/dist/src/client/neovim-terminal-client.d.ts +0 -2
- package/dist/src/client/neovim-terminal-client.js +0 -2
- package/dist/src/client/neovim-terminal-client.js.map +1 -1
- package/dist/src/client/startTerminal.d.ts +0 -2
- package/dist/src/client/startTerminal.js +0 -2
- package/dist/src/client/startTerminal.js.map +1 -1
- package/dist/src/client/terminal-terminal-client.d.ts +0 -1
- package/dist/src/client/terminal-terminal-client.js +0 -1
- package/dist/src/client/terminal-terminal-client.js.map +1 -1
- package/dist/src/server/cypress-support/contents.js +9 -11
- package/dist/src/server/cypress-support/contents.js.map +1 -1
- package/dist/src/server/index.d.ts +4 -0
- package/dist/src/server/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +35 -9
- package/CHANGELOG.md +0 -933
- package/dist/browser/assets/index-BQzArJW3.js +0 -9
- package/dist/browser/assets/index-hkmOP7rU.css +0 -1
- package/index.html +0 -11
- package/src/browser/neovim-client.ts +0 -126
- package/src/client/MyNeovimConfigModification.test.ts +0 -25
- package/src/client/MyNeovimConfigModification.ts +0 -8
- package/src/client/color-utilities.test.ts +0 -10
- package/src/client/color-utilities.ts +0 -9
- package/src/client/cypress-assertions.ts +0 -35
- package/src/client/index.ts +0 -4
- package/src/client/neovim-terminal-client.ts +0 -130
- package/src/client/public/DejaVuSansMNerdFontMono-Regular.ttf +0 -0
- package/src/client/startTerminal.ts +0 -97
- package/src/client/style.css +0 -32
- package/src/client/terminal-config.ts +0 -25
- package/src/client/terminal-terminal-client.ts +0 -106
- package/src/client/validateMouseEvent.ts +0 -22
- package/src/scripts/commands/commandRun.ts +0 -68
- package/src/scripts/commands/commandTuiNeovimExec.ts +0 -36
- package/src/scripts/commands/commandTuiNeovimPrepare.ts +0 -12
- package/src/scripts/commands/commandTuiStart.ts +0 -36
- package/src/scripts/parseArguments.test.ts +0 -28
- package/src/scripts/parseArguments.ts +0 -66
- package/src/scripts/resolveConfig.test.ts +0 -78
- package/src/scripts/resolveTuiConfig.ts +0 -65
- package/src/scripts/tui.ts +0 -77
- package/src/server/TestServer.ts +0 -73
- package/src/server/applications/neovim/NeovimApplication.ts +0 -236
- package/src/server/applications/neovim/NeovimJavascriptApiClient.test.ts +0 -48
- package/src/server/applications/neovim/NeovimJavascriptApiClient.ts +0 -68
- package/src/server/applications/neovim/api.ts +0 -257
- package/src/server/applications/neovim/environment/TempDirectory.ts +0 -18
- package/src/server/applications/neovim/environment/createTempDir.test.ts +0 -29
- package/src/server/applications/neovim/environment/createTempDir.ts +0 -91
- package/src/server/applications/neovim/neovimRouter.ts +0 -100
- package/src/server/applications/neovim/prepareNewTestDirectory.test.ts +0 -32
- package/src/server/applications/neovim/prepareNewTestDirectory.ts +0 -21
- package/src/server/applications/terminal/TerminalTestApplication.ts +0 -101
- package/src/server/applications/terminal/api.ts +0 -89
- package/src/server/applications/terminal/runBlockingShellCommand.test.ts +0 -41
- package/src/server/applications/terminal/runBlockingShellCommand.ts +0 -64
- package/src/server/applications/terminal/terminalRouter.ts +0 -47
- package/src/server/blockingCommandInputSchema.test.ts +0 -24
- package/src/server/blockingCommandInputSchema.ts +0 -27
- package/src/server/config.test.ts +0 -7
- package/src/server/config.ts +0 -18
- package/src/server/connection/trpc.ts +0 -3
- package/src/server/cypress-support/contents.ts +0 -245
- package/src/server/cypress-support/createCypressSupportFile.test.ts +0 -69
- package/src/server/cypress-support/createCypressSupportFile.ts +0 -56
- package/src/server/dirtree/index.test.ts +0 -352
- package/src/server/dirtree/index.ts +0 -144
- package/src/server/dirtree/json-to-zod.ts +0 -60
- package/src/server/index.ts +0 -6
- package/src/server/server.ts +0 -34
- package/src/server/types.ts +0 -98
- package/src/server/updateTestdirectorySchemaFile.test.ts +0 -49
- package/src/server/updateTestdirectorySchemaFile.ts +0 -71
- package/src/server/utilities/DisposableSingleApplication.test.ts +0 -92
- package/src/server/utilities/DisposableSingleApplication.ts +0 -49
- package/src/server/utilities/Lazy.ts +0 -16
- package/src/server/utilities/TerminalApplication.ts +0 -100
- package/src/server/utilities/generator.test.ts +0 -50
- package/src/server/utilities/generator.ts +0 -12
- package/src/server/utilities/tabId.ts +0 -4
- package/src/server/utilities/timeout.ts +0 -3
- package/src/server/utilities/timeoutable.ts +0 -19
- package/tsconfig.json +0 -31
- 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,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
|
-
}
|
package/src/client/index.ts
DELETED
|
@@ -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
|
-
}
|
|
Binary file
|
|
@@ -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
|
-
}
|
package/src/client/style.css
DELETED
|
@@ -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
|
-
}
|