Skip to content

Commit fbb0c4d

Browse files
authored
windows, have a new "local" conn option for Git Bash if installed (#2666)
1 parent 28eeec8 commit fbb0c4d

File tree

22 files changed

+336
-45
lines changed

22 files changed

+336
-45
lines changed

Taskfile.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ tasks:
5454
WCLOUD_ENDPOINT: "https://api-dev.waveterm.dev/central"
5555
WCLOUD_WS_ENDPOINT: "wss://wsapi-dev.waveterm.dev/"
5656

57+
electron:winquickdev:
58+
desc: Run the Electron application via the Vite dev server (quick dev - Windows amd64 only, no generate, no wsh).
59+
cmd: npm run dev
60+
deps:
61+
- npm:install
62+
- build:backend:quickdev:windows
63+
env:
64+
WAVETERM_ENVFILE: "{{.ROOT_DIR}}/.env"
65+
WCLOUD_ENDPOINT: "https://api-dev.waveterm.dev/central"
66+
WCLOUD_WS_ENDPOINT: "wss://wsapi-dev.waveterm.dev/"
67+
5768
docs:npm:install:
5869
desc: Runs `npm install` in docs directory
5970
internal: true
@@ -186,6 +197,25 @@ tasks:
186197
generates:
187198
- dist/bin/wavesrv.*
188199

200+
build:backend:quickdev:windows:
201+
desc: Build only the wavesrv component for quickdev (Windows amd64 only, no generate, no wsh).
202+
platforms: [windows]
203+
cmds:
204+
- task: build:server:internal
205+
vars:
206+
ARCHS: amd64
207+
GO_ENV_VARS: CC="zig cc -target x86_64-windows-gnu"
208+
deps:
209+
- go:mod:tidy
210+
sources:
211+
- "cmd/server/*.go"
212+
- "pkg/**/*.go"
213+
- "pkg/**/*.json"
214+
- "pkg/**/*.sh"
215+
- "tsunami/**/*.go"
216+
generates:
217+
- dist/bin/wavesrv.x64.exe
218+
189219
build:server:windows:
190220
desc: Build the wavesrv component for Windows platforms (only generates artifacts for the current architecture).
191221
platforms: [windows]

frontend/app/block/blockframe.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,8 @@ const BlockFrame_Header = ({
254254
icon: "link-slash",
255255
title: "wsh is not installed for this connection",
256256
};
257-
const showNoWshButton = manageConnection && wshProblem && !util.isBlank(connName) && !connName.startsWith("aws:");
257+
const showNoWshButton =
258+
manageConnection && wshProblem && !util.isLocalConnName(connName) && !connName.startsWith("aws:");
258259

259260
return (
260261
<div
@@ -600,7 +601,7 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
600601
return;
601602
}
602603
const connName = blockData?.meta?.connection;
603-
if (!util.isBlank(connName)) {
604+
if (!util.isLocalConnName(connName)) {
604605
console.log("ensure conn", nodeModel.blockId, connName);
605606
RpcApi.ConnEnsureCommand(
606607
TabRpcClient,

frontend/app/block/blockutil.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export const ConnectionButton = React.memo(
160160
React.forwardRef<HTMLDivElement, ConnectionButtonProps>(
161161
({ connection, changeConnModalAtom }: ConnectionButtonProps, ref) => {
162162
const [connModalOpen, setConnModalOpen] = jotai.useAtom(changeConnModalAtom);
163-
const isLocal = util.isBlank(connection);
163+
const isLocal = util.isLocalConnName(connection);
164164
const connStatusAtom = getConnStatusAtom(connection);
165165
const connStatus = jotai.useAtomValue(connStatusAtom);
166166
let showDisconnectedSlash = false;
@@ -172,9 +172,15 @@ export const ConnectionButton = React.memo(
172172
};
173173
let titleText = null;
174174
let shouldSpin = false;
175+
let connDisplayName: string = null;
175176
if (isLocal) {
176177
color = "var(--grey-text-color)";
177-
titleText = "Connected to Local Machine";
178+
if (connection === "local:gitbash") {
179+
titleText = "Connected to Git Bash";
180+
connDisplayName = "Git Bash";
181+
} else {
182+
titleText = "Connected to Local Machine";
183+
}
178184
connIconElem = (
179185
<i
180186
className={clsx(util.makeIconClass("laptop", false), "fa-stack-1x")}
@@ -232,7 +238,11 @@ export const ConnectionButton = React.memo(
232238
}}
233239
/>
234240
</span>
235-
{isLocal ? null : <div className="connection-name ellipsis">{connection}</div>}
241+
{connDisplayName ? (
242+
<div className="connection-name ellipsis">{connDisplayName}</div>
243+
) : isLocal ? null : (
244+
<div className="connection-name ellipsis">{connection}</div>
245+
)}
236246
</div>
237247
);
238248
}

frontend/app/modals/conntypeahead.tsx

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,8 @@
33

44
import { computeConnColorNum } from "@/app/block/blockutil";
55
import { TypeAheadModal } from "@/app/modals/typeaheadmodal";
6-
import {
7-
atoms,
8-
createBlock,
9-
getApi,
10-
getConnStatusAtom,
11-
getHostName,
12-
getUserName,
13-
globalStore,
14-
WOS,
15-
} from "@/app/store/global";
6+
import { ConnectionsModel } from "@/app/store/connections-model";
7+
import { atoms, createBlock, getConnStatusAtom, getHostName, getUserName, globalStore, WOS } from "@/app/store/global";
168
import { globalRefocusWithTimeout } from "@/app/store/keymodel";
179
import { RpcApi } from "@/app/store/wshclientapi";
1810
import { TabRpcClient } from "@/app/store/wshrpcutil";
@@ -107,7 +99,7 @@ function createFilteredLocalSuggestionItem(
10799
iconColor: "var(--grey-text-color)",
108100
value: "",
109101
label: localName,
110-
current: connection == null,
102+
current: util.isBlank(connection),
111103
};
112104
return [localSuggestion];
113105
}
@@ -172,12 +164,26 @@ function getLocalSuggestions(
172164
connSelected: string,
173165
connStatusMap: Map<string, ConnStatus>,
174166
fullConfig: FullConfigType,
175-
filterOutNowsh: boolean
167+
filterOutNowsh: boolean,
168+
hasGitBash: boolean
176169
): SuggestionConnectionScope | null {
177170
const wslFiltered = filterConnections(connList, connSelected, fullConfig, filterOutNowsh);
178171
const wslSuggestionItems = createWslSuggestionItems(wslFiltered, connection, connStatusMap);
179172
const localSuggestionItem = createFilteredLocalSuggestionItem(localName, connection, connSelected);
180-
const combinedSuggestionItems = [...localSuggestionItem, ...wslSuggestionItems];
173+
174+
const gitBashItems: Array<SuggestionConnectionItem> = [];
175+
if (hasGitBash && "Git Bash".toLowerCase().includes(connSelected.toLowerCase())) {
176+
gitBashItems.push({
177+
status: "connected",
178+
icon: "laptop",
179+
iconColor: "var(--grey-text-color)",
180+
value: "local:gitbash",
181+
label: "Git Bash",
182+
current: connection === "local:gitbash",
183+
});
184+
}
185+
186+
const combinedSuggestionItems = [...localSuggestionItem, ...gitBashItems, ...wslSuggestionItems];
181187
const sortedSuggestionItems = sortConnSuggestionItems(combinedSuggestionItems, fullConfig);
182188
if (sortedSuggestionItems.length == 0) {
183189
return null;
@@ -235,7 +241,7 @@ function getDisconnectItem(
235241
connection: string,
236242
connStatusMap: Map<string, ConnStatus>
237243
): SuggestionConnectionItem | null {
238-
if (!connection) {
244+
if (util.isLocalConnName(connection)) {
239245
return null;
240246
}
241247
const connStatus = connStatusMap.get(connection);
@@ -346,6 +352,7 @@ const ChangeConnectionBlockModal = React.memo(
346352
const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom);
347353
let filterOutNowsh = util.useAtomValueSafe(viewModel.filterOutNowsh) ?? true;
348354
const showS3 = util.useAtomValueSafe(viewModel.showS3) ?? false;
355+
const hasGitBash = jotai.useAtomValue(ConnectionsModel.getInstance().hasGitBashAtom);
349356

350357
let maxActiveConnNum = 1;
351358
for (const conn of allConnStatus) {
@@ -402,7 +409,7 @@ const ChangeConnectionBlockModal = React.memo(
402409
oref: WOS.makeORef("block", blockId),
403410
meta: { connection: connName, file: newFile, "cmd:cwd": null },
404411
});
405-
412+
406413
try {
407414
await RpcApi.ConnEnsureCommand(
408415
TabRpcClient,
@@ -425,7 +432,8 @@ const ChangeConnectionBlockModal = React.memo(
425432
connSelected,
426433
connStatusMap,
427434
fullConfig,
428-
filterOutNowsh
435+
filterOutNowsh,
436+
hasGitBash
429437
);
430438
const remoteSuggestions = getRemoteSuggestions(
431439
connList,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright 2025, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { RpcApi } from "@/app/store/wshclientapi";
5+
import { TabRpcClient } from "@/app/store/wshrpcutil";
6+
import { isWindows } from "@/util/platformutil";
7+
import { atom, type Atom, type PrimitiveAtom } from "jotai";
8+
import { globalStore } from "./jotaiStore";
9+
10+
class ConnectionsModel {
11+
private static instance: ConnectionsModel;
12+
gitBashPathAtom: PrimitiveAtom<string> = atom("") as PrimitiveAtom<string>;
13+
hasGitBashAtom: Atom<boolean>;
14+
15+
private constructor() {
16+
this.hasGitBashAtom = atom((get) => {
17+
if (!isWindows()) {
18+
return false;
19+
}
20+
const path = get(this.gitBashPathAtom);
21+
return path !== "";
22+
});
23+
this.loadGitBashPath();
24+
}
25+
26+
static getInstance(): ConnectionsModel {
27+
if (!ConnectionsModel.instance) {
28+
ConnectionsModel.instance = new ConnectionsModel();
29+
}
30+
return ConnectionsModel.instance;
31+
}
32+
33+
async loadGitBashPath(rescan: boolean = false): Promise<void> {
34+
if (!isWindows()) {
35+
return;
36+
}
37+
try {
38+
const path = await RpcApi.FindGitBashCommand(TabRpcClient, rescan, { timeout: 2000 });
39+
globalStore.set(this.gitBashPathAtom, path);
40+
} catch (error) {
41+
console.error("Failed to find git bash path:", error);
42+
globalStore.set(this.gitBashPathAtom, "");
43+
}
44+
}
45+
46+
getGitBashPath(): string {
47+
return globalStore.get(this.gitBashPathAtom);
48+
}
49+
}
50+
51+
export { ConnectionsModel };

frontend/app/store/global.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@ import {
1717
import { getWebServerEndpoint } from "@/util/endpoints";
1818
import { fetch } from "@/util/fetchutil";
1919
import { setPlatform } from "@/util/platformutil";
20-
import { base64ToString, deepCompareReturnPrev, fireAndForget, getPrefixedSettings, isBlank } from "@/util/util";
20+
import {
21+
base64ToString,
22+
deepCompareReturnPrev,
23+
fireAndForget,
24+
getPrefixedSettings,
25+
isBlank,
26+
isLocalConnName,
27+
} from "@/util/util";
2128
import { atom, Atom, PrimitiveAtom, useAtomValue } from "jotai";
2229
import { globalStore } from "./jotaiStore";
2330
import { modalsModel } from "./modalmodel";
@@ -730,8 +737,7 @@ function getConnStatusAtom(conn: string): PrimitiveAtom<ConnStatus> {
730737
const connStatusMap = globalStore.get(ConnStatusMapAtom);
731738
let rtn = connStatusMap.get(conn);
732739
if (rtn == null) {
733-
if (isBlank(conn)) {
734-
// create a fake "local" status atom that's always connected
740+
if (isLocalConnName(conn)) {
735741
const connStatus: ConnStatus = {
736742
connection: conn,
737743
connected: true,

frontend/app/store/wshclientapi.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,11 @@ class RpcApiType {
282282
return client.wshRpcCall("filewrite", data, opts);
283283
}
284284

285+
// command "findgitbash" [call]
286+
FindGitBashCommand(client: WshClient, data: boolean, opts?: RpcOpts): Promise<string> {
287+
return client.wshRpcCall("findgitbash", data, opts);
288+
}
289+
285290
// command "focuswindow" [call]
286291
FocusWindowCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
287292
return client.wshRpcCall("focuswindow", data, opts);

frontend/app/view/term/termwrap.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -526,14 +526,14 @@ export class TermWrap {
526526
oref: WOS.makeORef("block", this.blockId),
527527
});
528528

529-
if (rtInfo["shell:integration"]) {
529+
if (rtInfo && rtInfo["shell:integration"]) {
530530
const shellState = rtInfo["shell:state"] as ShellIntegrationStatus;
531531
globalStore.set(this.shellIntegrationStatusAtom, shellState || null);
532532
} else {
533533
globalStore.set(this.shellIntegrationStatusAtom, null);
534534
}
535535

536-
const lastCmd = rtInfo["shell:lastcmd"];
536+
const lastCmd = rtInfo ? rtInfo["shell:lastcmd"] : null;
537537
globalStore.set(this.lastCommandAtom, lastCmd || null);
538538
} catch (e) {
539539
console.log("Error loading runtime info:", e);

frontend/types/gotypes.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,6 +1082,7 @@ declare global {
10821082
"term:disablewebgl"?: boolean;
10831083
"term:localshellpath"?: string;
10841084
"term:localshellopts"?: string[];
1085+
"term:gitbashpath"?: string;
10851086
"term:scrollback"?: number;
10861087
"term:copyonselect"?: boolean;
10871088
"term:transparency"?: number;

frontend/util/util.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ function isBlank(str: string): boolean {
1212
return str == null || str == "";
1313
}
1414

15+
function isLocalConnName(connName: string): boolean {
16+
if (isBlank(connName)) {
17+
return true;
18+
}
19+
return connName === "local" || connName.startsWith("local:");
20+
}
21+
1522
function base64ToString(b64: string): string {
1623
if (b64 == null) {
1724
return null;
@@ -509,6 +516,7 @@ export {
509516
getPromiseState,
510517
getPromiseValue,
511518
isBlank,
519+
isLocalConnName,
512520
jotaiLoadableValue,
513521
jsonDeepEqual,
514522
lazy,

0 commit comments

Comments
 (0)