Skip to content

Commit ca1a80e

Browse files
AndreasArvidssonphillcopre-commit-ci-lite[bot]
authored
Show webview about missing installation dependencies (#2720)
If the user is missing some installation dependencies. eg talon, cursorless-talon, command server, we now show a web view informing them Fixes #1953 Fixes #528 <img width="689" alt="Screenshot 2025-01-11T15-11-03 - Visual Studio Code" src="https://github.com/user-attachments/assets/6692cd78-fe4b-437b-a67f-8e906a4ffa4e" /> ## Checklist - [/] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [/] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [/] I have not broken the cheatsheet --------- Co-authored-by: Phil Cohen <[email protected]> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent 66d77a6 commit ca1a80e

File tree

12 files changed

+422
-172
lines changed

12 files changed

+422
-172
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
},
4848
"pnpm": {
4949
"patchedDependencies": {
50-
"@docusaurus/theme-search-algolia": "patches/@[email protected]",
5150
"@types/[email protected]": "patches/@[email protected]",
5251
5352
},

packages/common/src/cursorlessCommandIds.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const cursorlessCommandIds = [
4747
"cursorless.recordScopeTests.saveActiveDocument",
4848
"cursorless.showCheatsheet",
4949
"cursorless.showDocumentation",
50+
"cursorless.showInstallationDependencies",
5051
"cursorless.showQuickPick",
5152
"cursorless.takeSnapshot",
5253
"cursorless.toggleDecorations",
@@ -89,6 +90,9 @@ export const cursorlessCommandDescriptions: Record<
8990
"Bulk save scope tests for the active document",
9091
),
9192
["cursorless.showDocumentation"]: new VisibleCommand("Show documentation"),
93+
["cursorless.showInstallationDependencies"]: new VisibleCommand(
94+
"Show installation dependencies",
95+
),
9296
["cursorless.showScopeVisualizer"]: new VisibleCommand(
9397
"Show the scope visualizer",
9498
),

packages/cursorless-neovim/src/registerCommands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export async function registerCommands(
8787
// Other commands
8888
["cursorless.showQuickPick"]: dummyCommandHandler,
8989
["cursorless.showDocumentation"]: dummyCommandHandler,
90+
["cursorless.showInstallationDependencies"]: dummyCommandHandler,
9091
["cursorless.private.logQuickActions"]: dummyCommandHandler,
9192

9293
// Hats

packages/cursorless-vscode/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@
104104
"command": "cursorless.showDocumentation",
105105
"title": "Cursorless: Show documentation"
106106
},
107+
{
108+
"command": "cursorless.showInstallationDependencies",
109+
"title": "Cursorless: Show installation dependencies"
110+
},
107111
{
108112
"command": "cursorless.showScopeVisualizer",
109113
"title": "Cursorless: Show the scope visualizer"
@@ -1275,6 +1279,7 @@
12751279
"@cursorless/node-common": "workspace:*",
12761280
"@cursorless/test-case-recorder": "workspace:*",
12771281
"@cursorless/vscode-common": "workspace:*",
1282+
"glob": "^11.0.0",
12781283
"itertools": "^2.3.2",
12791284
"lodash-es": "^4.17.21",
12801285
"nearley": "2.20.1",
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta http-equiv="Content-Security-Policy" content="META_CONTENT" />
5+
<style>
6+
.hide {
7+
display: none;
8+
}
9+
</style>
10+
</head>
11+
12+
<body>
13+
<h2>Cursorless extension is now running!</h2>
14+
15+
<a href="https://www.cursorless.org/docs/user/installation">
16+
Click here to learn how to install Cursorless
17+
</a>
18+
19+
<p>Let's check if all dependencies are installed.</p>
20+
21+
<div id="msg-talon" class="hide">
22+
<h4>Talon not installed</h4>
23+
<p>
24+
Cursorless requires Talon to function by voice.
25+
<br />
26+
You can download Talon from
27+
<a href="https://talonvoice.com">talonvoice.com</a>
28+
</p>
29+
<p>
30+
<i>
31+
If you're using Cursorless by keyboard, you can ignore this message.
32+
</i>
33+
</p>
34+
</div>
35+
36+
<div id="msg-cursorless-talon" class="hide">
37+
<h4>Cursorless Talon scripts missing</h4>
38+
<p>
39+
Cursorless requires Talon user scripts to function by voice.
40+
<br />
41+
The installation steps for the scripts are available at
42+
<a
43+
href="https://www.cursorless.org/docs/user/installation/#installing-the-talon-side"
44+
>
45+
github.com/cursorless-dev/cursorless-talon
46+
</a>
47+
</p>
48+
</div>
49+
50+
<div id="msg-command-server" class="hide">
51+
<h4>Command server extension not installed</h4>
52+
<p>
53+
Cursorless requires the command server extension to function by voice.
54+
<br />
55+
The extension is available at the
56+
<a
57+
href="https://marketplace.visualstudio.com/items?itemName=pokey.command-server"
58+
>
59+
Visual Studio Marketplace
60+
</a>
61+
</p>
62+
<p>
63+
<i>
64+
If you're using Cursorless by keyboard, you can ignore this message.
65+
</i>
66+
</p>
67+
</div>
68+
69+
<div id="msg-all-installed" class="hide">
70+
<h4>All dependencies are installed!</h4>
71+
</div>
72+
73+
<div style="margin-top: 1rem">
74+
<label>
75+
<input id="input-dont-show" type="checkbox" />
76+
Don't show again
77+
</label>
78+
</div>
79+
80+
<script src="SCRIPT_SOURCE"></script>
81+
</body>
82+
</html>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const vscode = acquireVsCodeApi();
2+
const msgTalon = document.getElementById("msg-talon");
3+
const msgCursorlessTalon = document.getElementById("msg-cursorless-talon");
4+
const msgCommandServer = document.getElementById("msg-command-server");
5+
const msgAllInstalled = document.getElementById("msg-all-installed");
6+
const inputDontShow = document.getElementById("input-dont-show");
7+
8+
inputDontShow.onchange = (e) => {
9+
const command = { type: "dontShow", checked: e.target.checked };
10+
vscode.postMessage(command);
11+
};
12+
13+
window.addEventListener("message", (event) => {
14+
const { dontShow, hasMissingDependencies, dependencies } = event.data;
15+
16+
hide(msgTalon, dependencies.talon);
17+
// No need to show missing Cursorless Talon if Talon itself is missing
18+
hide(msgCursorlessTalon, dependencies.cursorlessTalon || !dependencies.talon);
19+
hide(msgCommandServer, dependencies.commandServer);
20+
hide(msgAllInstalled, hasMissingDependencies);
21+
inputDontShow.checked = dontShow;
22+
});
23+
24+
function hide(element, doHide) {
25+
element.className = doHide ? "hide" : "";
26+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { COMMAND_SERVER_EXTENSION_ID } from "@cursorless/vscode-common";
2+
import { globSync } from "glob";
3+
import * as fs from "node:fs";
4+
import * as os from "node:os";
5+
import * as path from "node:path";
6+
import * as vscode from "vscode";
7+
8+
const STATE_KEY = "dontShowInstallationDependencies";
9+
10+
export class InstallationDependencies {
11+
private panel: vscode.WebviewPanel | undefined;
12+
13+
constructor(private extensionContext: vscode.ExtensionContext) {
14+
this.show = this.show.bind(this);
15+
this.maybeShow = this.maybeShow.bind(this);
16+
}
17+
18+
/**
19+
* Shows the installation dependencies webview.
20+
*/
21+
show() {
22+
this.createWebview();
23+
}
24+
25+
/**
26+
* Shows the installation dependencies webview if there are missing dependencies.
27+
*/
28+
maybeShow() {
29+
const state = this.getState();
30+
if (state.hasMissingDependencies && !state.dontShow) {
31+
this.createWebview();
32+
}
33+
}
34+
35+
private getState() {
36+
const dependencies = getDependencies();
37+
const hasMissingDependencies = Object.values(dependencies).some(
38+
(value) => !value,
39+
);
40+
return {
41+
dontShow: !!this.extensionContext.globalState.get<boolean>(STATE_KEY),
42+
hasMissingDependencies,
43+
dependencies,
44+
};
45+
}
46+
47+
private createWebview() {
48+
if (this.panel != null) {
49+
this.panel.reveal();
50+
return;
51+
}
52+
53+
this.panel = vscode.window.createWebviewPanel(
54+
"cursorless.installationDependencies",
55+
"Cursorless dependencies",
56+
{
57+
viewColumn: vscode.ViewColumn.Active,
58+
},
59+
{
60+
enableScripts: true,
61+
},
62+
);
63+
64+
this.panel.webview.html = this.getWebviewContent();
65+
66+
const updateWebview = () => {
67+
this.panel?.webview.postMessage(this.getState());
68+
};
69+
70+
this.panel.onDidChangeViewState(updateWebview);
71+
72+
this.panel.webview.onDidReceiveMessage((message) => {
73+
if (message.type === "dontShow") {
74+
const checked = message.checked;
75+
this.extensionContext.globalState.update(STATE_KEY, checked);
76+
} else {
77+
console.error(`Unknown message: ${message}`);
78+
}
79+
});
80+
81+
const interval = setInterval(updateWebview, 5000);
82+
83+
this.panel.onDidDispose(() => {
84+
clearInterval(interval);
85+
this.panel = undefined;
86+
});
87+
88+
this.panel.webview.postMessage(this.getState());
89+
}
90+
91+
private getWebviewContent() {
92+
if (this.panel == null) {
93+
throw new Error("Panel not created yet");
94+
}
95+
const htmlPath = this.getResourceUri("installationDependencies.html");
96+
const jsUri = this.getResourceUri("installationDependencies.js");
97+
const template = fs
98+
.readFileSync(htmlPath.fsPath, "utf8")
99+
.replace("META_CONTENT", `script-src ${this.panel.webview.cspSource};`)
100+
.replace("SCRIPT_SOURCE", jsUri.toString());
101+
return template;
102+
}
103+
104+
private getResourceUri(name: string): vscode.Uri {
105+
if (this.panel == null) {
106+
throw new Error("Panel not created yet");
107+
}
108+
return this.panel.webview.asWebviewUri(
109+
vscode.Uri.joinPath(
110+
this.extensionContext.extensionUri,
111+
"resources",
112+
name,
113+
),
114+
);
115+
}
116+
}
117+
118+
function getDependencies(): Record<string, boolean> {
119+
return {
120+
talon: talonHomeExists(),
121+
cursorlessTalon: cursorlessTalonExists(),
122+
commandServer: commandServerInstalled(),
123+
};
124+
}
125+
126+
function talonHomeExists() {
127+
return fs.existsSync(getTalonHomePath());
128+
}
129+
130+
function cursorlessTalonExists() {
131+
const talonUserPath = path.join(getTalonHomePath(), "user");
132+
const files = globSync("**/*/src/cursorless.talon", {
133+
cwd: talonUserPath,
134+
maxDepth: 3,
135+
});
136+
return files.length > 0;
137+
}
138+
139+
function commandServerInstalled() {
140+
const extension = vscode.extensions.getExtension(COMMAND_SERVER_EXTENSION_ID);
141+
return extension != null;
142+
}
143+
144+
function getTalonHomePath() {
145+
return os.platform() === "win32"
146+
? `${os.homedir()}\\AppData\\Roaming\\talon`
147+
: `${os.homedir()}/.talon`;
148+
}

packages/cursorless-vscode/src/extension.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import * as crypto from "crypto";
3838
import * as os from "node:os";
3939
import * as path from "node:path";
4040
import * as vscode from "vscode";
41+
import { InstallationDependencies } from "./InstallationDependencies";
4142
import { ReleaseNotes } from "./ReleaseNotes";
4243
import { ScopeTreeProvider } from "./ScopeTreeProvider";
4344
import type {
@@ -165,6 +166,8 @@ export async function activate(
165166
commandServerApi != null,
166167
);
167168

169+
const installationDependencies = new InstallationDependencies(context);
170+
168171
context.subscriptions.push(storedTargetHighlighter(vscodeIDE, storedTargets));
169172

170173
const vscodeTutorial = createTutorial(
@@ -189,11 +192,14 @@ export async function activate(
189192
keyboardCommands,
190193
hats,
191194
vscodeTutorial,
195+
installationDependencies,
192196
storedTargets,
193197
);
194198

195199
void new ReleaseNotes(vscodeApi, context, normalizedIde.messages).maybeShow();
196200

201+
installationDependencies.maybeShow();
202+
197203
return {
198204
testHelpers:
199205
normalizedIde.runMode === "test"

packages/cursorless-vscode/src/registerCommands.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
TestCaseRecorder,
1818
} from "@cursorless/test-case-recorder";
1919
import * as vscode from "vscode";
20+
import type { InstallationDependencies } from "./InstallationDependencies";
2021
import type { ScopeVisualizer } from "./ScopeVisualizerCommandApi";
2122
import type { VscodeTutorial } from "./VscodeTutorial";
2223
import { showDocumentation, showQuickPick } from "./commands";
@@ -36,6 +37,7 @@ export function registerCommands(
3637
keyboardCommands: KeyboardCommands,
3738
hats: VscodeHats,
3839
tutorial: VscodeTutorial,
40+
installationDependencies: InstallationDependencies,
3941
storedTargets: StoredTargetMap,
4042
): void {
4143
const runCommandWrapper = async (run: () => Promise<unknown>) => {
@@ -82,6 +84,7 @@ export function registerCommands(
8284
// Other commands
8385
["cursorless.showQuickPick"]: showQuickPick,
8486
["cursorless.showDocumentation"]: showDocumentation,
87+
["cursorless.showInstallationDependencies"]: installationDependencies.show,
8588

8689
["cursorless.private.logQuickActions"]: logQuickActions,
8790

packages/cursorless-vscode/src/scripts/populateDist/assets.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ export const assets: Asset[] = [
4242
source: "resources/font_measurements.js",
4343
destination: "resources/font_measurements.js",
4444
},
45+
{
46+
source: "resources/installationDependencies.html",
47+
destination: "resources/installationDependencies.html",
48+
},
49+
{
50+
source: "resources/installationDependencies.js",
51+
destination: "resources/installationDependencies.js",
52+
},
4553
{ source: "../../schemas", destination: "schemas" },
4654
{
4755
source: "../../third-party-licenses.csv",

0 commit comments

Comments
 (0)